277 lines
11 KiB
JavaScript
277 lines
11 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
const connectBtn = document.getElementById('connectBtn');
|
|
if (!connectBtn) return; // Exit if not logged in
|
|
|
|
const disconnectBtn = document.getElementById('disconnectBtn');
|
|
const tiktokUsernameInput = document.getElementById('tiktokUsername');
|
|
const connectionStatus = document.getElementById('connectionStatus');
|
|
const commentFeed = document.getElementById('commentFeed');
|
|
const emptyFeed = document.getElementById('emptyFeed');
|
|
const voiceSelect = document.getElementById('voiceSelect');
|
|
const rateRange = document.getElementById('rateRange');
|
|
const rateValue = document.getElementById('rateValue');
|
|
const autoReplyToggle = document.getElementById('autoReplyToggle');
|
|
const simulateBtn = document.getElementById('simulateBtn');
|
|
const manualCommentInput = document.getElementById('manualComment');
|
|
const historyTableBody = document.getElementById('historyTableBody');
|
|
const commentCountBadge = document.getElementById('commentCount');
|
|
const toastContainer = document.getElementById('toastContainer');
|
|
const aiPersonality = document.getElementById('aiPersonality');
|
|
|
|
let isConnected = false;
|
|
let commentCount = 0;
|
|
let synth = window.speechSynthesis;
|
|
let voices = [];
|
|
let pollInterval = null;
|
|
let lastEventId = 0;
|
|
|
|
// Load voices
|
|
function populateVoiceList() {
|
|
if (!synth) return;
|
|
voices = synth.getVoices().sort(function (a, b) {
|
|
const aname = a.name.toUpperCase();
|
|
const bname = b.name.toUpperCase();
|
|
if (aname < bname) return -1;
|
|
else if (aname > bname) return 1;
|
|
return 0;
|
|
});
|
|
if (voiceSelect) {
|
|
voiceSelect.innerHTML = '';
|
|
voices.forEach((voice, i) => {
|
|
const option = document.createElement('option');
|
|
option.textContent = `${voice.name} (${voice.lang})`;
|
|
if (voice.default) option.textContent += ' -- DEFAULT';
|
|
option.setAttribute('data-lang', voice.lang);
|
|
option.setAttribute('data-name', voice.name);
|
|
voiceSelect.appendChild(option);
|
|
});
|
|
}
|
|
}
|
|
|
|
populateVoiceList();
|
|
if (speechSynthesis.onvoiceschanged !== undefined) {
|
|
speechSynthesis.onvoiceschanged = populateVoiceList;
|
|
}
|
|
|
|
if (rateRange) {
|
|
rateRange.addEventListener('input', () => {
|
|
if (rateValue) rateValue.textContent = rateRange.value;
|
|
});
|
|
}
|
|
|
|
if (aiPersonality) {
|
|
aiPersonality.addEventListener('change', async () => {
|
|
const personality = aiPersonality.value;
|
|
try {
|
|
const resp = await fetch('api/update_personality.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ personality })
|
|
});
|
|
const data = await resp.json();
|
|
if (data.success) {
|
|
showToast('Settings Saved', 'AI personality updated successfully.', 'success');
|
|
} else {
|
|
showToast('Error', data.error || 'Failed to update personality', 'danger');
|
|
}
|
|
} catch (err) {
|
|
showToast('Network Error', 'Could not reach settings API.', 'danger');
|
|
}
|
|
});
|
|
}
|
|
|
|
async function startBridge(username) {
|
|
try {
|
|
const resp = await fetch(`api/bridge_control.php?action=start&username=${username}`);
|
|
const data = await resp.json();
|
|
if (data.success) {
|
|
isConnected = true;
|
|
connectBtn.classList.add('d-none');
|
|
if (disconnectBtn) disconnectBtn.classList.remove('d-none');
|
|
if (connectionStatus) connectionStatus.innerHTML = '<span class="status-dot bg-success me-1 pulse"></span> Live connection active: @' + username;
|
|
if (emptyFeed) emptyFeed.classList.add('d-none');
|
|
showToast('Connected', `Started listening to @${username}. Looking for comments...`, 'success');
|
|
|
|
startPolling(username);
|
|
} else {
|
|
showToast('Bridge Error', data.error || 'Failed to start bridge', 'danger');
|
|
}
|
|
} catch (err) {
|
|
showToast('Network Error', 'Could not reach bridge control.', 'danger');
|
|
}
|
|
}
|
|
|
|
async function stopBridge(username) {
|
|
try {
|
|
await fetch(`api/bridge_control.php?action=stop&username=${username}`);
|
|
isConnected = false;
|
|
connectBtn.classList.remove('d-none');
|
|
if (disconnectBtn) disconnectBtn.classList.add('d-none');
|
|
if (connectionStatus) connectionStatus.innerHTML = '<span class="status-dot bg-secondary me-1"></span> Disconnected';
|
|
showToast('Disconnected', 'Stopped bridge.', 'warning');
|
|
|
|
if (pollInterval) clearInterval(pollInterval);
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
function startPolling(username) {
|
|
if (pollInterval) clearInterval(pollInterval);
|
|
|
|
pollInterval = setInterval(async () => {
|
|
if (!isConnected) return;
|
|
|
|
try {
|
|
const resp = await fetch(`api/get_updates.php?username=${username}`);
|
|
const data = await resp.json();
|
|
|
|
if (data.events && data.events.length > 0) {
|
|
data.events.forEach(event => {
|
|
// Only add if not already seen
|
|
if (event.id > lastEventId) {
|
|
addEventToFeed(event);
|
|
lastEventId = event.id;
|
|
}
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error('Polling error:', err);
|
|
}
|
|
}, 3000); // Poll every 3 seconds
|
|
}
|
|
|
|
function addEventToFeed(event) {
|
|
commentCount++;
|
|
if (commentCountBadge) commentCountBadge.textContent = `${commentCount} events`;
|
|
|
|
const isSystem = event.comment.includes('sent') && event.comment.includes('x');
|
|
|
|
const commentItem = document.createElement('div');
|
|
commentItem.className = isSystem ? 'comment-item border-start border-4 border-tiktok-red' : 'comment-item';
|
|
commentItem.innerHTML = `
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<span class="comment-author" style="${isSystem ? 'color: var(--tiktok-red)' : ''}">${event.author}:</span>
|
|
<span class="comment-text">${event.comment}</span>
|
|
</div>
|
|
<small class="text-secondary opacity-50" style="font-size: 0.7rem">${new Date(event.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</small>
|
|
</div>
|
|
<div class="ai-reply-box mt-1">AI: ${event.reply}</div>
|
|
`;
|
|
|
|
if (commentFeed) commentFeed.prepend(commentItem);
|
|
|
|
// TTS
|
|
speak(event.reply);
|
|
|
|
// Update history table
|
|
updateHistoryTable(event);
|
|
}
|
|
|
|
connectBtn.addEventListener('click', () => {
|
|
const username = tiktokUsernameInput.value.trim();
|
|
if (!username) {
|
|
showToast('Error', 'Please enter a TikTok username.', 'danger');
|
|
return;
|
|
}
|
|
startBridge(username);
|
|
});
|
|
|
|
if (disconnectBtn) {
|
|
disconnectBtn.addEventListener('click', () => {
|
|
const username = tiktokUsernameInput.value.trim();
|
|
stopBridge(username);
|
|
});
|
|
}
|
|
|
|
if (simulateBtn) {
|
|
simulateBtn.addEventListener('click', async () => {
|
|
const commentText = manualCommentInput.value.trim();
|
|
const username = tiktokUsernameInput.value.trim();
|
|
if (!commentText || !username) return;
|
|
|
|
try {
|
|
// Manually insert into DB to test
|
|
await fetch('api/process_comment.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ author: 'TestUser', comment: commentText, username })
|
|
});
|
|
manualCommentInput.value = '';
|
|
// Polling will pick it up
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
function speak(text) {
|
|
if (!text || !synth) return;
|
|
|
|
const utterThis = new SpeechSynthesisUtterance(text);
|
|
const selectedOption = voiceSelect?.selectedOptions[0]?.getAttribute('data-name');
|
|
if (selectedOption) {
|
|
for (let i = 0; i < voices.length; i++) {
|
|
if (voices[i].name === selectedOption) {
|
|
utterThis.voice = voices[i];
|
|
}
|
|
}
|
|
}
|
|
utterThis.pitch = 1;
|
|
utterThis.rate = rateRange?.value || 1.0;
|
|
synth.speak(utterThis);
|
|
}
|
|
|
|
function updateHistoryTable(event) {
|
|
if (!historyTableBody) return;
|
|
const row = document.createElement('tr');
|
|
row.className = 'border-secondary';
|
|
const time = new Date(event.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
|
|
row.innerHTML = `
|
|
<td class="text-secondary small">${time}</td>
|
|
<td><strong>${event.author}</strong></td>
|
|
<td class="text-secondary">${event.comment}</td>
|
|
<td class="text-tiktok-cyan">${event.reply}</td>
|
|
`;
|
|
|
|
if (historyTableBody.firstChild && historyTableBody.firstChild.tagName === 'TR' && historyTableBody.firstChild.innerText.includes('No history yet')) {
|
|
historyTableBody.innerHTML = '';
|
|
}
|
|
|
|
historyTableBody.prepend(row);
|
|
|
|
if (historyTableBody.children.length > 20) {
|
|
historyTableBody.removeChild(historyTableBody.lastChild);
|
|
}
|
|
}
|
|
|
|
function showToast(title, message, type = 'info') {
|
|
if (!toastContainer) return;
|
|
const toastId = 'toast-' + Date.now();
|
|
const toastHtml = `
|
|
<div id="${toastId}" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
|
<div class="toast-header bg-dark text-light border-secondary">
|
|
<strong class="me-auto ${type === 'danger' ? 'text-danger' : (type === 'success' ? 'text-tiktok-cyan' : '')}">${title}</strong>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
</div>
|
|
<div class="toast-body">
|
|
${message}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
|
|
const toastElement = document.getElementById(toastId);
|
|
if (toastElement) {
|
|
const toast = new bootstrap.Toast(toastElement);
|
|
toast.show();
|
|
|
|
toastElement.addEventListener('hidden.bs.toast', () => {
|
|
toastElement.remove();
|
|
});
|
|
}
|
|
}
|
|
});
|