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 userPersonaList = document.getElementById("userPersonaList"); const personaForm = document.getElementById("personaForm"); const personaFormTitle = document.getElementById("personaFormTitle"); const personaTypeSelect = document.getElementById("personaType"); 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 newUserPersonaBtn = document.getElementById("newUserPersona"); const clearPersonaBtn = document.getElementById("clearPersona"); const clearUserPersonaBtn = document.getElementById("clearUserPersona"); const personaCancelBtn = document.getElementById("personaCancel"); const activePersonaStatus = document.getElementById("activePersonaStatus"); const activeUserPersonaStatus = document.getElementById("activeUserPersonaStatus"); const personaModal = document.getElementById("personaModal"); const personaCloseBtn = document.getElementById("personaClose"); const personaMenuButton = document.getElementById("personaMenuButton"); const personaPopover = document.getElementById("personaPopover"); const marketPersonaList = document.getElementById("marketPersonaList"); const marketUserPersonaList = document.getElementById("marketUserPersonaList"); const refreshMarketBtn = document.getElementById("refreshMarket"); let isProcessing = false; let chatSessions = []; let activeChatId = null; let assistantPersonas = []; let userPersonas = []; let activePersonaId = null; let activeUserPersonaId = null; let editingPersonaId = null; let editingPersonaType = "assistant"; let marketPersonas = []; let publishedPersonaIds = new Set(); 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 updateActivePersonaStatus(personaItems, activeId, statusElement, emptyMessage, activePrefix) { const activePersona = personaItems.find(persona => persona.id === activeId); if (activePersona) { statusElement.textContent = `${activePrefix}: ${activePersona.name}`; } else { statusElement.textContent = emptyMessage; } } function setActivePersonaStatus() { updateActivePersonaStatus( assistantPersonas, activePersonaId, activePersonaStatus, 'No persona equipped.', 'AI equipped' ); updateActivePersonaStatus( userPersonas, activeUserPersonaId, activeUserPersonaStatus, 'No user persona set.', 'You are' ); clearPersonaBtn.disabled = !activePersonaId; clearUserPersonaBtn.disabled = !activeUserPersonaId; } function openPersonaForm(persona = null, personaType = "assistant") { closePersonaPopover(); personaModal.classList.remove("hidden"); if (persona) { editingPersonaId = persona.id; editingPersonaType = persona.persona_type || "assistant"; personaFormTitle.textContent = `Edit ${editingPersonaType === "user" ? "your persona" : "AI persona"}`; personaTypeSelect.value = editingPersonaType; personaTypeSelect.disabled = true; personaNameInput.value = persona.name || ""; personaPronounsInput.value = persona.pronouns || ""; personaAppearanceInput.value = persona.appearance || ""; personaBackgroundInput.value = persona.background || ""; personaDetailsInput.value = persona.details || ""; } else { editingPersonaId = null; editingPersonaType = personaType; personaFormTitle.textContent = `Create ${personaType === "user" ? "your persona" : "AI persona"}`; personaTypeSelect.value = personaType; personaTypeSelect.disabled = false; personaNameInput.value = ""; personaPronounsInput.value = ""; personaAppearanceInput.value = ""; personaBackgroundInput.value = ""; personaDetailsInput.value = ""; } personaNameInput.focus(); } function closePersonaForm() { personaModal.classList.add("hidden"); editingPersonaId = null; editingPersonaType = "assistant"; personaTypeSelect.disabled = false; } function renderPersonaList(personaItems, activeId, listElement, personaType) { listElement.innerHTML = ''; if (!personaItems.length) { const empty = document.createElement('p'); empty.className = 'status persona-status'; empty.textContent = 'No personas yet. Create one to get started.'; listElement.appendChild(empty); setActivePersonaStatus(); return; } personaItems.forEach(persona => { const item = document.createElement('div'); item.className = `persona-item${persona.id === activeId ? ' 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 === activeId ? 'Equipped' : 'Equip'; equipBtn.disabled = persona.id === activeId; equipBtn.addEventListener('click', () => equipPersona(persona.id, personaType)); const editBtn = document.createElement('button'); editBtn.type = 'button'; editBtn.textContent = 'Edit'; editBtn.addEventListener('click', () => openPersonaForm(persona)); const publishBtn = document.createElement('button'); publishBtn.type = 'button'; const isPublished = publishedPersonaIds.has(persona.id); publishBtn.textContent = isPublished ? 'Update listing' : 'Publish'; publishBtn.addEventListener('click', () => publishPersona(persona.id)); 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(publishBtn); actions.appendChild(deleteBtn); item.appendChild(title); item.appendChild(meta); item.appendChild(actions); listElement.appendChild(item); }); setActivePersonaStatus(); } async function loadPersonas() { const res = await get('/personas'); if (res.error) return; assistantPersonas = res.assistantPersonas || []; userPersonas = res.userPersonas || []; activePersonaId = res.activePersonaId || null; activeUserPersonaId = res.activeUserPersonaId || null; publishedPersonaIds = new Set(res.publishedPersonaIds || []); renderPersonaList(assistantPersonas, activePersonaId, personaList, "assistant"); renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user"); } function renderMarketList(personaItems, listElement) { listElement.innerHTML = ''; if (!personaItems.length) { const empty = document.createElement('p'); empty.className = 'status persona-status'; empty.textContent = 'No market personas yet. Be the first to publish!'; listElement.appendChild(empty); return; } personaItems.forEach(persona => { const item = document.createElement('div'); item.className = 'persona-item'; const title = document.createElement('h4'); title.textContent = persona.name; const meta = document.createElement('p'); const pronouns = persona.pronouns ? `Pronouns: ${persona.pronouns}` : 'Pronouns: —'; meta.textContent = `By ${persona.creator_username} • ${pronouns}`; const actions = document.createElement('div'); actions.className = 'persona-item-actions'; const getBtn = document.createElement('button'); getBtn.type = 'button'; getBtn.textContent = 'Get & equip'; getBtn.addEventListener('click', () => collectMarketPersona(persona.id, persona.persona_type)); actions.appendChild(getBtn); item.appendChild(title); item.appendChild(meta); item.appendChild(actions); listElement.appendChild(item); }); } async function loadMarketPersonas() { const res = await get('/personas/market'); if (res.error) return; marketPersonas = res.personas || []; const assistantMarket = marketPersonas.filter(persona => persona.persona_type === 'assistant'); const userMarket = marketPersonas.filter(persona => persona.persona_type === 'user'); renderMarketList(assistantMarket, marketPersonaList); renderMarketList(userMarket, marketUserPersonaList); } async function equipPersona(personaId, personaType) { const endpoint = personaType === "user" ? `/personas/${personaId}/equip-user` : `/personas/${personaId}/equip`; const res = await post(endpoint, {}); if (res.error) return; if (personaType === "user") { activeUserPersonaId = personaId; } else { activePersonaId = personaId; } renderPersonaList(assistantPersonas, activePersonaId, personaList, "assistant"); renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user"); } async function clearPersona(personaType) { const endpoint = personaType === "user" ? '/personas/user/clear' : '/personas/clear'; const res = await post(endpoint, {}); if (res.error) return; if (personaType === "user") { activeUserPersonaId = null; } else { activePersonaId = null; } renderPersonaList(assistantPersonas, activePersonaId, personaList, "assistant"); renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user"); } async function savePersona() { const payload = { personaType: personaTypeSelect.value, 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 = assistantPersonas.find(item => item.id === personaId) || userPersonas.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(); } async function publishPersona(personaId) { const res = await post(`/personas/${personaId}/publish`, {}); if (res.error) return; await loadPersonas(); await loadMarketPersonas(); } async function collectMarketPersona(marketId, personaType) { const res = await post(`/personas/market/${marketId}/collect`, {equip: true}); if (res.error) return; if (personaType === "user") { activeUserPersonaId = res.activeUserPersonaId || res.persona?.id || null; } else { activePersonaId = res.activePersonaId || res.persona?.id || null; } 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(); await loadMarketPersonas(); msgInput.focus(); return true; } return false; } async function loadServerChat() { await loadChatSessions(); await loadPersonas(); await loadMarketPersonas(); } 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; assistantPersonas = []; userPersonas = []; activePersonaId = null; activeUserPersonaId = null; marketPersonas = []; publishedPersonaIds = new Set(); 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()); newUserPersonaBtn.addEventListener('click', () => openPersonaForm(null, "user")); clearPersonaBtn.addEventListener('click', () => clearPersona("assistant")); clearUserPersonaBtn.addEventListener('click', () => clearPersona("user")); refreshMarketBtn.addEventListener('click', () => loadMarketPersonas()); personaForm.addEventListener('submit', (event) => { event.preventDefault(); savePersona(); }); personaCloseBtn.addEventListener("click", closePersonaForm); personaModal.addEventListener("click", (e) => { if (e.target === personaModal) closePersonaForm(); }); function closePersonaPopover() { personaPopover.classList.add("hidden"); personaMenuButton.setAttribute("aria-expanded", "false"); } function togglePersonaPopover(event) { event.stopPropagation(); const isHidden = personaPopover.classList.contains("hidden"); if (isHidden) { personaPopover.classList.remove("hidden"); personaMenuButton.setAttribute("aria-expanded", "true"); } else { closePersonaPopover(); } } personaMenuButton.addEventListener("click", togglePersonaPopover); personaPopover.addEventListener("click", (event) => { event.stopPropagation(); }); document.addEventListener("click", () => { closePersonaPopover(); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closePersonaPopover(); } }); window.addEventListener('load', () => { checkSession(); displayModels(); });