const authDiv = document.getElementById("auth"); const chatDiv = document.getElementById("chat"); const authMsg = document.getElementById("authMsg"); const messagesDiv = document.getElementById("messages"); const loginForm = document.getElementById("loginForm"); const registerForm = document.getElementById("registerForm"); const loginUsernameInput = document.getElementById("loginUsername"); const loginPasswordInput = document.getElementById("loginPassword"); const registerUsernameInput = document.getElementById("registerUsername"); const registerPasswordInput = document.getElementById("registerPassword"); const loginSubmit = document.getElementById("loginSubmit"); const registerSubmit = document.getElementById("registerSubmit"); const msgInput = document.getElementById("msgInput"); const sendBtn = document.getElementById("send"); const authScreens = document.querySelectorAll(".auth-screen"); const toggleButtons = document.querySelectorAll(".auth-toggle .toggle"); const chatList = document.getElementById("chatList"); const newChatBtn = document.getElementById("newChat"); const renameChatBtn = document.getElementById("renameChat"); const deleteChatBtn = document.getElementById("deleteChat"); const activeChatTitle = document.getElementById("activeChatTitle"); const personaList = document.getElementById("personaList"); const personaForm = document.getElementById("personaForm"); const personaFormTitle = document.getElementById("personaFormTitle"); const personaNameInput = document.getElementById("personaName"); const personaPronounsInput = document.getElementById("personaPronouns"); const personaAppearanceInput = document.getElementById("personaAppearance"); const personaBackgroundInput = document.getElementById("personaBackground"); const personaDetailsInput = document.getElementById("personaDetails"); const newPersonaBtn = document.getElementById("newPersona"); const clearPersonaBtn = document.getElementById("clearPersona"); const personaCancelBtn = document.getElementById("personaCancel"); const activePersonaStatus = document.getElementById("activePersonaStatus"); let isProcessing = false; let chatSessions = []; let activeChatId = null; let personas = []; let activePersonaId = null; let editingPersonaId = null; const displayModels = async () => { const select = document.getElementById("model"); fetch("/models") .then(response => response.json()) .then(data => { select.innerHTML = ""; data.models.forEach(model => { const option = document.createElement("option"); option.value = model.model; option.textContent = model.name; select.appendChild(option); }); }) .catch(err => { console.error("Failed to load models:", err); select.innerHTML = ``; }); } async function request(url, data, method = "POST") { try { const res = await fetch(url, { method, headers: {"Content-Type": "application/json"}, body: data ? JSON.stringify(data) : undefined }); return await res.json(); } catch (err) { return {error: "Network error — please try again."}; } } async function post(url, data) { return request(url, data, "POST"); } async function get(url) { try { const res = await fetch(url); return await res.json(); } catch (err) { return {error: "Network error — please try again."}; } } function setAuthMessage(msg, state = "") { authMsg.textContent = msg; authMsg.className = state ? `status ${state}` : 'status'; } function setMessageContent(element, content) { if (window.marked && window.DOMPurify) { const rendered = marked.parse(content, {breaks: true}); element.innerHTML = DOMPurify.sanitize(rendered); return; } element.textContent = content; } function addMessage(content, isUser = false, isLoading = false, saveToHistory = true) { const msgDiv = document.createElement('div'); msgDiv.className = `msg ${isUser ? 'user' : 'bot'}${isLoading ? ' loading' : ''}`; if (isLoading) { msgDiv.innerHTML = `
`; } else { const header = document.createElement('div'); header.className = 'msg-header'; header.textContent = isUser ? 'You' : 'DeepFake'; const body = document.createElement('div'); body.className = 'msg-content'; setMessageContent(body, content); msgDiv.appendChild(header); msgDiv.appendChild(body); } messagesDiv.appendChild(msgDiv); setTimeout(() => { messagesDiv.scrollTop = messagesDiv.scrollHeight; }, 100); return msgDiv; } function clearMessages() { messagesDiv.innerHTML = ''; } function getChatById(chatId) { return chatSessions.find(chat => chat.id === chatId); } function bumpChat(chatId) { const index = chatSessions.findIndex(chat => chat.id === chatId); if (index <= 0) return; const [chat] = chatSessions.splice(index, 1); chatSessions.unshift(chat); renderChatList(); } function updateChatActionState() { const hasChat = Boolean(activeChatId); renameChatBtn.disabled = !hasChat; deleteChatBtn.disabled = !hasChat; } function renderChatList() { chatList.innerHTML = ''; if (!chatSessions.length) { const empty = document.createElement('p'); empty.className = 'status'; empty.textContent = 'No chats yet. Start a new one!'; chatList.appendChild(empty); return; } chatSessions.forEach(chat => { const item = document.createElement('div'); item.className = `chat-item${chat.id === activeChatId ? ' active' : ''}`; const title = document.createElement('span'); title.textContent = chat.title; const actions = document.createElement('button'); actions.type = 'button'; actions.textContent = '✕'; actions.title = 'Delete chat'; actions.addEventListener('click', (event) => { event.stopPropagation(); deleteChat(chat.id); }); item.appendChild(title); item.appendChild(actions); item.addEventListener('click', () => setActiveChat(chat.id)); chatList.appendChild(item); }); } function setActivePersonaStatus() { const activePersona = personas.find(persona => persona.id === activePersonaId); if (activePersona) { activePersonaStatus.textContent = `Equipped: ${activePersona.name}`; } else { activePersonaStatus.textContent = 'No persona equipped.'; } clearPersonaBtn.disabled = !activePersonaId; } function openPersonaForm(persona = null) { personaForm.classList.remove('hidden'); if (persona) { editingPersonaId = persona.id; personaFormTitle.textContent = 'Edit persona'; personaNameInput.value = persona.name || ''; personaPronounsInput.value = persona.pronouns || ''; personaAppearanceInput.value = persona.appearance || ''; personaBackgroundInput.value = persona.background || ''; personaDetailsInput.value = persona.details || ''; } else { editingPersonaId = null; personaFormTitle.textContent = 'Create persona'; personaNameInput.value = ''; personaPronounsInput.value = ''; personaAppearanceInput.value = ''; personaBackgroundInput.value = ''; personaDetailsInput.value = ''; } personaNameInput.focus(); } function closePersonaForm() { personaForm.classList.add('hidden'); editingPersonaId = null; } function renderPersonaList() { personaList.innerHTML = ''; if (!personas.length) { const empty = document.createElement('p'); empty.className = 'status persona-status'; empty.textContent = 'No personas yet. Create one to get started.'; personaList.appendChild(empty); setActivePersonaStatus(); return; } personas.forEach(persona => { const item = document.createElement('div'); item.className = `persona-item${persona.id === activePersonaId ? ' active' : ''}`; const title = document.createElement('h4'); title.textContent = persona.name; const meta = document.createElement('p'); meta.textContent = persona.pronouns ? `Pronouns: ${persona.pronouns}` : 'Pronouns: —'; const actions = document.createElement('div'); actions.className = 'persona-item-actions'; const equipBtn = document.createElement('button'); equipBtn.type = 'button'; equipBtn.textContent = persona.id === activePersonaId ? 'Equipped' : 'Equip'; equipBtn.disabled = persona.id === activePersonaId; equipBtn.addEventListener('click', () => equipPersona(persona.id)); const editBtn = document.createElement('button'); editBtn.type = 'button'; editBtn.textContent = 'Edit'; editBtn.addEventListener('click', () => openPersonaForm(persona)); const deleteBtn = document.createElement('button'); deleteBtn.type = 'button'; deleteBtn.textContent = 'Delete'; deleteBtn.addEventListener('click', () => deletePersona(persona.id)); actions.appendChild(equipBtn); actions.appendChild(editBtn); actions.appendChild(deleteBtn); item.appendChild(title); item.appendChild(meta); item.appendChild(actions); personaList.appendChild(item); }); setActivePersonaStatus(); } async function loadPersonas() { const res = await get('/personas'); if (res.error) return; personas = res.personas || []; activePersonaId = res.activePersonaId || null; renderPersonaList(); } async function equipPersona(personaId) { const res = await post(`/personas/${personaId}/equip`, {}); if (res.error) return; activePersonaId = personaId; renderPersonaList(); } async function clearPersona() { const res = await post('/personas/clear', {}); if (res.error) return; activePersonaId = null; renderPersonaList(); } async function savePersona() { const payload = { name: personaNameInput.value.trim(), pronouns: personaPronounsInput.value.trim(), appearance: personaAppearanceInput.value.trim(), background: personaBackgroundInput.value.trim(), details: personaDetailsInput.value.trim() }; if (!payload.name) return; let res; if (editingPersonaId) { res = await request(`/personas/${editingPersonaId}`, payload, "PUT"); } else { res = await post('/personas', payload); } if (res.error) return; await loadPersonas(); closePersonaForm(); } async function deletePersona(personaId) { const persona = personas.find(item => item.id === personaId); if (!persona) return; const confirmDelete = confirm(`Delete persona "${persona.name}"?`); if (!confirmDelete) return; const res = await request(`/personas/${personaId}`, null, "DELETE"); if (res.error) return; await loadPersonas(); } function setLoadingState(loading) { isProcessing = loading; sendBtn.disabled = loading; msgInput.disabled = loading; if (loading) { sendBtn.classList.add('loading'); msgInput.placeholder = "Processing response..."; } else { sendBtn.classList.remove('loading'); msgInput.placeholder = "Type your message..."; } } function showAuthScreen(target) { authScreens.forEach(screen => { screen.classList.toggle('active', screen.id === `${target}Screen`); }); toggleButtons.forEach(btn => { btn.classList.toggle('active', btn.dataset.target === target); }); setAuthMessage('', ''); } function setActiveChat(chatId) { activeChatId = chatId; const chat = getChatById(chatId); activeChatTitle.textContent = chat ? chat.title : 'New chat'; updateChatActionState(); renderChatList(); if (chatId) { loadChatMessages(chatId); } else { clearMessages(); } } async function loadChatSessions() { const res = await get('/chats'); if (res.error) return; chatSessions = res.chats || []; renderChatList(); const activeExists = chatSessions.find(chat => chat.id === activeChatId); if (activeExists) { setActiveChat(activeChatId); return; } if (!chatSessions.length) { await createNewChat(); return; } setActiveChat(chatSessions[0].id); } async function loadChatMessages(chatId) { const res = await get(`/chats/${chatId}/messages`); if (res.error) return; clearMessages(); (res.messages || []).forEach(msg => { addMessage(msg.content, msg.role === 'user', false, false); }); messagesDiv.scrollTop = messagesDiv.scrollHeight; } async function createNewChat() { const res = await post('/chats', {title: 'New chat'}); if (res.error || !res.chat) return; chatSessions.unshift(res.chat); setActiveChat(res.chat.id); renderChatList(); } async function renameChat(chatId) { const chat = getChatById(chatId); if (!chat) return; const nextTitle = prompt('Rename chat', chat.title); if (!nextTitle) return; const trimmedTitle = nextTitle.trim(); if (!trimmedTitle) return; const res = await request(`/chats/${chatId}`, {title: trimmedTitle}, "PUT"); if (res.error) return; chat.title = trimmedTitle; activeChatTitle.textContent = trimmedTitle; renderChatList(); } async function deleteChat(chatId) { const chat = getChatById(chatId); if (!chat) return; const confirmDelete = confirm(`Delete "${chat.title}"? This cannot be undone.`); if (!confirmDelete) return; const res = await request(`/chats/${chatId}`, null, "DELETE"); if (res.error) return; chatSessions = chatSessions.filter(item => item.id !== chatId); if (activeChatId === chatId) { if (chatSessions.length) { setActiveChat(chatSessions[0].id); } else { activeChatId = null; await createNewChat(); } } renderChatList(); } async function checkSession() { const res = await get('/session'); if (res.user) { authDiv.style.display = "none"; chatDiv.style.display = "block"; await loadChatSessions(); await loadPersonas(); msgInput.focus(); return true; } return false; } async function loadServerChat() { await loadChatSessions(); await loadPersonas(); } async function handleAuth(endpoint, credentials) { const {username, password} = credentials; const submitBtn = endpoint === "login" ? loginSubmit : registerSubmit; if (!username || !password) { setAuthMessage("Please enter both username and password.", 'error'); return; } submitBtn.disabled = true; setAuthMessage("Processing...", ''); const res = await post(`/${endpoint}`, {username, password}); submitBtn.disabled = false; if (res.error) { setAuthMessage(res.error, 'error'); } else { if (endpoint === "login") { setAuthMessage("Login successful! Redirecting to chat...", 'success'); authDiv.style.display = "none"; chatDiv.style.display = "block"; await loadServerChat(); msgInput.focus(); } else { setAuthMessage("Registration successful — you can now log in!", 'success'); showAuthScreen('login'); loginUsernameInput.value = username; loginPasswordInput.focus(); } } } async function sendMessage() { if (isProcessing) return; const msg = msgInput.value.trim(); if (!msg) return; if (!activeChatId) { await createNewChat(); } const currentChat = getChatById(activeChatId); if (currentChat && currentChat.title === 'New chat') { const autoTitle = msg.length > 40 ? `${msg.slice(0, 40)}…` : msg; currentChat.title = autoTitle; activeChatTitle.textContent = autoTitle; renderChatList(); await request(`/chats/${activeChatId}`, {title: autoTitle}, "PUT"); } addMessage(msg, true); msgInput.value = ""; msgInput.style.height = 'auto'; setLoadingState(true); const loadingMsg = addMessage('', false, true, false); try { const model = document.getElementById("model").value; const res = await post("/chat", {message: msg, model, chatId: activeChatId}); if (messagesDiv.contains(loadingMsg)) { messagesDiv.removeChild(loadingMsg); } if (res.reply) { addMessage(res.reply, false); bumpChat(activeChatId); } else { addMessage(`[Error: ${res.error}]`, false); } } catch (error) { if (messagesDiv.contains(loadingMsg)) { messagesDiv.removeChild(loadingMsg); } addMessage(`[Error: Network error]`, false); } finally { setLoadingState(false); msgInput.focus(); } } toggleButtons.forEach(btn => { btn.addEventListener('click', () => showAuthScreen(btn.dataset.target)); }); loginForm.addEventListener('submit', (e) => { e.preventDefault(); handleAuth("login", { username: loginUsernameInput.value.trim(), password: loginPasswordInput.value.trim() }); }); registerForm.addEventListener('submit', (e) => { e.preventDefault(); handleAuth("register", { username: registerUsernameInput.value.trim(), password: registerPasswordInput.value.trim() }); }); document.getElementById("logout").onclick = async () => { await post("/logout", {}); chatDiv.style.display = "none"; authDiv.style.display = "grid"; clearMessages(); chatSessions = []; activeChatId = null; personas = []; activePersonaId = null; setAuthMessage("Logged out.", 'success'); showAuthScreen('login'); loginUsernameInput.value = ""; loginPasswordInput.value = ""; }; sendBtn.onclick = sendMessage; msgInput.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); newChatBtn.addEventListener('click', () => createNewChat()); renameChatBtn.addEventListener('click', () => { if (activeChatId) { renameChat(activeChatId); } }); deleteChatBtn.addEventListener('click', () => { if (activeChatId) { deleteChat(activeChatId); } }); msgInput.addEventListener('input', function () { this.style.height = 'auto'; this.style.height = Math.min(this.scrollHeight, 120) + 'px'; }); newPersonaBtn.addEventListener('click', () => openPersonaForm()); personaCancelBtn.addEventListener('click', () => closePersonaForm()); clearPersonaBtn.addEventListener('click', () => clearPersona()); personaForm.addEventListener('submit', (event) => { event.preventDefault(); savePersona(); }); window.addEventListener('load', () => { checkSession(); displayModels(); });