const $ = (id) => document.getElementById(id); const authDiv = $("auth"), chatDiv = $("chat"), authMsg = $("authMsg"); const messagesDiv = $("messages"), modelSelect = $("model"); const modelMenuButton = $("modelMenuButton"), modelPopover = $("modelPopover"), modelBadgeName = $("modelBadgeName"); const modelCount = $("modelCount"); const modelHelpTitle = $("modelHelpTitle"), modelHelpBadge = $("modelHelpBadge"), modelHelpSummary = $("modelHelpSummary"); const loginForm = $("loginForm"), registerForm = $("registerForm"), msgInput = $("msgInput"), sendBtn = $("send"); const loginUsernameInput = $("loginUsername"), loginPasswordInput = $("loginPassword"); const registerUsernameInput = $("registerUsername"), registerPasswordInput = $("registerPassword"); const loginSubmit = $("loginSubmit"), registerSubmit = $("registerSubmit"); const chatList = $("chatList"), chatSearchInput = $("chatSearch"), newChatBtn = $("newChat"); const renameChatBtn = $("renameChat"), clearChatBtn = $("clearChat"), exportChatBtn = $("exportChat"); const activeChatTitle = $("activeChatTitle"), sessionUser = $("sessionUser"); const chatModePill = $("chatModePill"), chatCharacterPill = $("chatCharacterPill"), chatUserPersonaPill = $("chatUserPersonaPill"); const personaList = $("personaList"), userPersonaList = $("userPersonaList"); const personaForm = $("personaForm"), personaFormTitle = $("personaFormTitle"), personaTypeSelect = $("personaType"); const personaNameInput = $("personaName"), personaPronounsInput = $("personaPronouns"), personaAppearanceInput = $("personaAppearance"); const personaBackgroundInput = $("personaBackground"), personaDetailsInput = $("personaDetails"); const personaExamplesField = $("personaExamplesField"), personaExampleDialoguesInput = $("personaExampleDialogues"); const personaFormNotice = $("personaFormNotice"); const roleplayNewPersonaBtn = $("roleplayNewPersona"), clearUserPersonaBtn = $("clearUserPersona"); const activePersonaStatus = $("activePersonaStatus"), activeUserPersonaStatus = $("activeUserPersonaStatus"); const personaModal = $("personaModal"), personaCloseBtn = $("personaClose"), personaMenuButton = $("personaMenuButton"), personaPopover = $("personaPopover"); const roleplayStarterModal = $("roleplayStarterModal"), roleplayStarterClose = $("roleplayStarterClose"), roleplayStarterCancel = $("roleplayStarterCancel"), roleplayStarterConfirm = $("roleplayStarterConfirm"); const roleplayStarterNotice = $("roleplayStarterNotice"), roleplayCharacterSelect = $("roleplayCharacterSelect"), roleplayUserPersonaSelect = $("roleplayUserPersonaSelect"); const popupModal = $("popupModal"), popupCloseBtn = $("popupClose"), popupCancelBtn = $("popupCancel"), popupConfirmBtn = $("popupConfirm"); const popupTitle = $("popupTitle"), popupEyebrow = $("popupEyebrow"), popupDescription = $("popupDescription"), popupField = $("popupField"); const popupInputLabel = $("popupInputLabel"), popupInput = $("popupInput"); const authScreens = document.querySelectorAll(".auth-screen"), toggleButtons = document.querySelectorAll(".auth-toggle .toggle"); const themeNameTargets = document.querySelectorAll("[data-theme-name]"), themeLogoTargets = document.querySelectorAll("[data-theme-logo]"); const requestedChatId = Number(new URLSearchParams(window.location.search).get("chat")) || null; let isProcessing = false, activeChatId = null, editingPersonaId = null, editingPersonaType = "assistant", currentUsername = ""; let chatSessions = [], currentMessages = [], assistantPersonas = [], userPersonas = []; let activeUserPersonaId = null, currentSummary = null, publishedPersonaIds = new Set(); let popupResolver = null, popupMode = null, popupLastFocus = null; let roleplayPresetCharacterId = null; const themes = { "fakegpt": {name: "FakeGPT", short: "FG"}, "fraud": {name: "Fraud", short: "FR"}, "germini": {name: "Germini", short: "GE"}, "slopilot": {name: "Slopilot", short: "SP"}, "beta-ai": {name: "Beta AI", short: "BA"}, "confusity": {name: "Confusity", short: "CF"} }; const defaultModelProfile = { badge: "General", summary: "General-purpose chat model. Start here if you are unsure which model to use." }; 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 { return {error: "Network error - please try again."}; } } const get = (url) => request(url, null, "GET"); const post = (url, data) => request(url, data, "POST"); const put = (url, data) => request(url, data, "PUT"); const del = (url) => request(url, null, "DELETE"); function setAuthMessage(message, state = "") { authMsg.textContent = message; authMsg.className = state ? `status ${state}` : "status"; } function setNotice(message, state = "") { return {message, state}; } function setPersonaFormNotice(message = "", state = "") { if (!personaFormNotice) return; if (!message) { personaFormNotice.textContent = ""; personaFormNotice.className = "status hidden"; return; } personaFormNotice.textContent = message; personaFormNotice.className = state ? `status ${state}` : "status"; } function setMessageContent(el, content) { const markdownParser = window.marked; const purifier = window.DOMPurify; if (markdownParser?.parse && purifier?.sanitize) { el.innerHTML = purifier.sanitize(markdownParser.parse(content, {breaks: true})); } else { el.textContent = content; } } function getChatById(id) { return chatSessions.find((chat) => chat.id === id); } function addMessage(content, isUser = false, isLoading = false) { const msgDiv = document.createElement("div"); msgDiv.className = `msg ${isUser ? "user" : "bot"}${isLoading ? " loading" : ""}`; if (isLoading) { msgDiv.innerHTML = `
Thinking
`; } else { const header = document.createElement("div"), body = document.createElement("div"); header.className = "msg-header"; body.className = "msg-content"; header.textContent = isUser ? "You" : "Assistant"; setMessageContent(body, content); msgDiv.append(header, body); } messagesDiv.appendChild(msgDiv); messagesDiv.scrollTop = messagesDiv.scrollHeight; return msgDiv; } function renderMessages() { messagesDiv.innerHTML = ""; currentMessages.forEach((msg) => addMessage(msg.content, msg.role === "user")); updateChatParticipants(); updateChatActionState(); } function updateWorkspaceCopy() { const activeChat = getChatById(activeChatId); if (activeChatTitle) { activeChatTitle.textContent = activeChat ? activeChat.title : "New chat"; } if (sessionUser) { sessionUser.textContent = currentUsername || "-"; } updateChatParticipants(); } function setRoleplayStarterNotice(message = "", state = "") { if (!roleplayStarterNotice) return; if (!message) { roleplayStarterNotice.textContent = ""; roleplayStarterNotice.className = "status hidden"; return; } roleplayStarterNotice.textContent = message; roleplayStarterNotice.className = state ? `status ${state}` : "status"; } function updateChatParticipants() { const chat = getChatById(activeChatId); if (!chatModePill || !chatCharacterPill || !chatUserPersonaPill) return; if (!chat) { chatModePill.textContent = "No chat selected"; chatCharacterPill.classList.add("hidden"); chatUserPersonaPill.classList.add("hidden"); return; } if (chat.assistant_persona_id) { chatModePill.textContent = "Roleplay mode"; chatCharacterPill.textContent = `Character: ${chat.assistant_persona_name || chat.title}`; chatCharacterPill.classList.remove("hidden"); if (chat.user_persona_id) { chatUserPersonaPill.textContent = `You as: ${chat.user_persona_name || "User persona"}`; chatUserPersonaPill.classList.remove("hidden"); } else { chatUserPersonaPill.textContent = "You as: yourself"; chatUserPersonaPill.classList.remove("hidden"); } return; } chatModePill.textContent = "Assistant mode"; chatCharacterPill.classList.add("hidden"); chatUserPersonaPill.classList.add("hidden"); } function updateContextRail() { return modelSelect?.selectedOptions[0]?.textContent || ""; } function getModelProfile(modelId, modelName) { const haystack = `${modelId || ""} ${modelName || ""}`.toLowerCase(); if (haystack.includes("codellama") || haystack.includes("code")) { return { badge: "Coding", summary: "Best for code generation, debugging, and technical explanations. Usually stronger on programming tasks than on creative chat." }; } if (haystack.includes("gemma")) { return { badge: "Fast", summary: "A lighter general chat model. Good for quick answers and lower-latency replies, with less depth than larger models." }; } if (haystack.includes("dolphin")) { return { badge: "Chatty", summary: "Instruction-following conversational model. Good for open-ended chat, brainstorming, and longer natural responses." }; } if (haystack.includes("mistral")) { return { badge: "Balanced", summary: "Strong default for everyday use. Usually a good balance between speed, clarity, and response quality." }; } if (haystack.includes("catgirl") || haystack.includes("femboy") || haystack.includes("buenzli")) { return { badge: "Persona", summary: "Specialized character-style model. Best for roleplay or stylized voice, and less reliable for factual or neutral answers." }; } return defaultModelProfile; } function updateModelHelp() { if (!modelSelect || !modelHelpTitle || !modelHelpBadge || !modelHelpSummary) return; const selectedOption = modelSelect.selectedOptions[0]; if (!selectedOption || selectedOption.disabled) { if (modelCount) modelCount.textContent = "No models"; if (modelBadgeName) modelBadgeName.textContent = "Unavailable"; modelHelpBadge.dataset.badge = "waiting"; modelHelpTitle.textContent = "Choose a model"; modelHelpBadge.textContent = "Waiting"; modelHelpSummary.textContent = "Pick a model to see what it is best suited for."; return; } if (modelCount) { const totalModels = Array.from(modelSelect.options).filter((option) => !option.disabled).length; modelCount.textContent = `${totalModels} ${totalModels === 1 ? "model" : "models"}`; } const modelId = selectedOption.value; const modelName = selectedOption.textContent || modelId; const profile = getModelProfile(modelId, modelName); if (modelBadgeName) modelBadgeName.textContent = modelName; modelHelpTitle.textContent = modelName; modelHelpBadge.textContent = profile.badge; modelHelpBadge.dataset.badge = profile.badge.toLowerCase(); modelHelpSummary.textContent = profile.summary; } function applyTheme(themeKey, persist = true) { const nextTheme = themes[themeKey] ? themeKey : "fakegpt"; const theme = themes[nextTheme]; document.body.dataset.theme = nextTheme; document.title = theme.name; themeNameTargets.forEach((target) => { target.textContent = theme.name; }); themeLogoTargets.forEach((target) => { target.textContent = theme.short; }); if (persist) localStorage.setItem("krishd-theme", nextTheme); updateWorkspaceCopy(); updateContextRail(); } function updateChatActionState() { const activeChat = getChatById(activeChatId); const hasChat = Boolean(activeChat); renameChatBtn.disabled = !hasChat || Boolean(activeChat?.assistant_persona_id); clearChatBtn.disabled = !hasChat; exportChatBtn.disabled = !hasChat || currentMessages.length === 0; } function renderChatList() { const query = chatSearchInput.value.trim().toLowerCase(); const filtered = chatSessions.filter((chat) => !query || chat.title.toLowerCase().includes(query)); chatList.innerHTML = ""; if (!filtered.length) { const empty = document.createElement("p"); empty.className = "status"; empty.textContent = query ? "No chats match this search." : "No chats yet. Start a new one."; chatList.appendChild(empty); return; } filtered.forEach((chat) => { const item = document.createElement("div"), content = document.createElement("div"), title = document.createElement("span"), meta = document.createElement("small"), remove = document.createElement("button"); item.className = `chat-item${chat.id === activeChatId ? " active" : ""}`; content.className = "chat-item-copy"; title.textContent = chat.title; meta.className = "chat-item-meta"; meta.textContent = chat.assistant_persona_id ? `Roleplay${chat.user_persona_name ? ` ยท ${chat.user_persona_name}` : ""}` : "Assistant chat"; remove.type = "button"; remove.textContent = "Delete"; remove.addEventListener("click", async (event) => { event.stopPropagation(); await deleteChat(chat.id); }); content.append(title, meta); item.append(content, remove); item.addEventListener("click", () => setActiveChat(chat.id)); chatList.appendChild(item); }); } function updateActivePersonaStatus(items, activeId, statusEl, emptyText, prefix) { const active = items.find((persona) => persona.id === activeId); statusEl.textContent = active ? `${prefix}: ${active.name}` : emptyText; } function setActivePersonaStatus() { if (activePersonaStatus) { activePersonaStatus.textContent = assistantPersonas.length ? "Pick a character, choose a user persona, and start the roleplay." : "No characters yet. Create one to start a roleplay."; } updateActivePersonaStatus(userPersonas, activeUserPersonaId, activeUserPersonaStatus, "No default user persona set.", "Default user persona"); if (clearUserPersonaBtn) clearUserPersonaBtn.disabled = !activeUserPersonaId; updateContextRail(); } function openPersonaForm(persona = null, personaType = "assistant") { closePersonaPopover(); closePopup(false); personaModal.classList.remove("hidden"); if (persona) { editingPersonaId = persona.id; editingPersonaType = persona.persona_type || "assistant"; personaFormTitle.textContent = `Edit ${editingPersonaType === "user" ? "your persona" : "AI Character"}`; 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 || ""; personaExampleDialoguesInput.value = persona.example_dialogues || ""; } else { editingPersonaId = null; editingPersonaType = personaType; personaFormTitle.textContent = `Create ${personaType === "user" ? "your persona" : "AI Character"}`; personaTypeSelect.value = personaType; personaTypeSelect.disabled = false; personaNameInput.value = ""; personaPronounsInput.value = ""; personaAppearanceInput.value = ""; personaBackgroundInput.value = ""; personaDetailsInput.value = ""; personaExampleDialoguesInput.value = ""; } personaExamplesField.classList.toggle("hidden", personaTypeSelect.value !== "assistant"); setPersonaFormNotice(""); personaNameInput.focus(); } function closePersonaForm() { personaModal.classList.add("hidden"); editingPersonaId = null; editingPersonaType = "assistant"; personaTypeSelect.disabled = false; setPersonaFormNotice(""); } function closePersonaPopover() { personaPopover.classList.add("hidden"); personaMenuButton.setAttribute("aria-expanded", "false"); } function populateRoleplayStarter() { if (!roleplayCharacterSelect || !roleplayUserPersonaSelect) return; roleplayCharacterSelect.innerHTML = ""; assistantPersonas.forEach((persona) => { const option = document.createElement("option"); option.value = String(persona.id); option.textContent = persona.name; option.selected = persona.id === roleplayPresetCharacterId; roleplayCharacterSelect.appendChild(option); }); roleplayUserPersonaSelect.innerHTML = ""; const noneOption = document.createElement("option"); noneOption.value = ""; noneOption.textContent = "No user persona"; roleplayUserPersonaSelect.appendChild(noneOption); userPersonas.forEach((persona) => { const option = document.createElement("option"); option.value = String(persona.id); option.textContent = persona.name; option.selected = persona.id === activeUserPersonaId; roleplayUserPersonaSelect.appendChild(option); }); } function openRoleplayStarter(characterId = null) { closePersonaPopover(); closeModelPopover(); roleplayPresetCharacterId = characterId; populateRoleplayStarter(); setRoleplayStarterNotice(""); roleplayStarterModal.classList.remove("hidden"); if (roleplayCharacterSelect.options.length) roleplayCharacterSelect.focus(); } function closeRoleplayStarter() { if (!roleplayStarterModal) return; roleplayStarterModal.classList.add("hidden"); roleplayPresetCharacterId = null; roleplayStarterConfirm.disabled = false; setRoleplayStarterNotice(""); } function closeModelPopover() { modelPopover.classList.add("hidden"); modelMenuButton.setAttribute("aria-expanded", "false"); } function isPopupOpen() { return popupModal && !popupModal.classList.contains("hidden"); } function finishPopup(result) { if (typeof popupResolver === "function") popupResolver(result); popupResolver = null; popupMode = null; } function closePopup(resolveValue = null) { if (!popupModal || popupModal.classList.contains("hidden")) return; popupModal.classList.add("hidden"); popupField.classList.add("hidden"); popupInput.value = ""; popupConfirmBtn.disabled = false; const nextFocus = popupLastFocus; popupLastFocus = null; finishPopup(resolveValue); if (nextFocus instanceof HTMLElement) nextFocus.focus(); } function openPopup(options) { if (!popupModal) return Promise.resolve(null); if (isPopupOpen()) closePopup(null); popupLastFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null; popupMode = options.mode; popupEyebrow.textContent = options.eyebrow || "Action"; popupTitle.textContent = options.title || "Confirm action"; popupDescription.textContent = options.description || ""; popupConfirmBtn.textContent = options.confirmLabel || "Confirm"; popupCancelBtn.textContent = options.cancelLabel || "Cancel"; popupConfirmBtn.classList.toggle("danger", Boolean(options.danger)); popupField.classList.toggle("hidden", options.mode !== "prompt"); if (options.mode === "prompt") { popupInputLabel.textContent = options.label || "Value"; popupInput.value = options.value || ""; popupInput.placeholder = options.placeholder || ""; } else { popupInput.value = ""; } popupModal.classList.remove("hidden"); return new Promise((resolve) => { popupResolver = resolve; requestAnimationFrame(() => { if (options.mode === "prompt") { popupInput.focus(); popupInput.select(); } else { popupConfirmBtn.focus(); } }); }); } function confirmPopup(options) { return openPopup({...options, mode: "confirm"}).then((result) => Boolean(result)); } function promptPopup(options) { return openPopup({...options, mode: "prompt"}).then((result) => typeof result === "string" ? result : null); } function renderPersonaList(items, activeId, listElement, personaType) { if (!listElement) { setActivePersonaStatus(); return; } listElement.innerHTML = ""; if (!items.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; } items.forEach((persona) => { const item = document.createElement("div"), titleRow = document.createElement("div"), title = document.createElement("h4"); const tag = document.createElement("span"), meta = document.createElement("p"), actions = document.createElement("div"); item.className = `persona-item${personaType === "user" && persona.id === activeId ? " active" : ""}`; titleRow.className = "persona-title-row"; actions.className = "persona-item-actions"; tag.className = `persona-role-tag ${personaType === "user" ? "user" : "ai"}`; tag.textContent = personaType === "user" ? "You" : "AI"; title.textContent = persona.name; meta.textContent = persona.pronouns ? `Pronouns: ${persona.pronouns}` : "Pronouns: n/a"; [[personaType === "assistant" ? "Start roleplay" : "Set default", () => equipPersona(persona.id, personaType), personaType === "user" && persona.id === activeId], ["Edit", () => openPersonaForm(persona), false], [publishedPersonaIds.has(persona.id) ? "Update listing" : "Publish", () => publishPersona(persona.id), false], ["Delete", () => deletePersona(persona.id), false]] .forEach(([label, handler, disabled]) => { const btn = document.createElement("button"); btn.type = "button"; btn.textContent = label; btn.disabled = disabled; btn.addEventListener("click", handler); actions.appendChild(btn); }); if (publishedPersonaIds.has(persona.id)) { const btn = document.createElement("button"); btn.type = "button"; btn.textContent = "Unpublish"; btn.addEventListener("click", () => unpublishPersona(persona.id)); actions.appendChild(btn); } titleRow.append(title, tag); item.append(titleRow, meta, actions); listElement.appendChild(item); }); setActivePersonaStatus(); } async function loadSummary() { updateWorkspaceCopy(); return currentSummary; } async function loadPersonas() { const res = await get("/personas"); if (res.error) return setNotice(res.error, "error"); assistantPersonas = res.assistantPersonas || []; userPersonas = res.userPersonas || []; activeUserPersonaId = res.activeUserPersonaId || null; publishedPersonaIds = new Set(res.publishedPersonaIds || []); renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user"); populateRoleplayStarter(); setActivePersonaStatus(); updateWorkspaceCopy(); } async function equipPersona(id, type) { if (type === "assistant") { openRoleplayStarter(id); return; } const res = await post(`/personas/${id}/equip-user`, {}); if (res.error) return setNotice(res.error, "error"); activeUserPersonaId = id; renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user"); setNotice("User persona updated.", "success"); } async function clearPersona(type) { if (type !== "user") return; const res = await post("/personas/user/clear", {}); if (res.error) return setNotice(res.error, "error"); activeUserPersonaId = null; renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user"); setNotice("Persona cleared.", "success"); } 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(), exampleDialogues: personaTypeSelect.value === "assistant" ? personaExampleDialoguesInput.value.trim() : "" }; if (!payload.name) return setPersonaFormNotice("Persona name is required.", "error"); const res = editingPersonaId ? await put(`/personas/${editingPersonaId}`, payload) : await post("/personas", payload); if (res.error) return setPersonaFormNotice(res.error, "error"); const wasEditing = Boolean(editingPersonaId); await loadPersonas(); if (payload.personaType === "assistant") await loadChatSessions(); closePersonaForm(); setNotice(wasEditing ? "Persona updated." : "Persona created.", "success"); } async function deletePersona(id) { const persona = assistantPersonas.find((item) => item.id === id) || userPersonas.find((item) => item.id === id); if (!persona) return; const confirmed = await confirmPopup({ eyebrow: persona.persona_type === "user" ? "Your persona" : "AI Character", title: "Delete persona", description: `Delete "${persona.name}"? This removes it from your library and any active slot.`, confirmLabel: "Delete", danger: true }); if (!confirmed) return; const res = await del(`/personas/${id}`); if (res.error) return setNotice(res.error, "error"); await loadPersonas(); if (persona.persona_type === "assistant") await loadChatSessions(); setNotice("Persona deleted.", "success"); } async function publishPersona(id) { const res = await post(`/personas/${id}/publish`, {}); if (res.error) return setNotice(res.error, "error"); await loadPersonas(); setNotice("Persona published.", "success"); } async function unpublishPersona(id) { const res = await post(`/personas/${id}/unpublish`, {}); if (res.error) return setNotice(res.error, "error"); await loadPersonas(); setNotice("Persona unpublished.", "success"); } function setLoadingState(loading) { isProcessing = loading; sendBtn.disabled = loading; msgInput.disabled = loading; sendBtn.classList.toggle("loading", loading); msgInput.placeholder = loading ? "Processing response..." : "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(""); } async function setActiveChat(id) { activeChatId = id; updateWorkspaceCopy(); renderChatList(); updateChatActionState(); const res = await get(`/chats/${id}/messages`); if (res.error) return setNotice(res.error, "error"); const existing = getChatById(id); if (res.chat && existing) Object.assign(existing, res.chat); currentMessages = res.messages || []; renderMessages(); } async function loadChatSessions() { const res = await get("/chats"); if (res.error) return setNotice(res.error, "error"); chatSessions = res.chats || []; renderChatList(); if (chatSessions.length) { const preferredChatId = (requestedChatId && getChatById(requestedChatId)) ? requestedChatId : activeChatId && getChatById(activeChatId) ? activeChatId : chatSessions[0].id; return setActiveChat(preferredChatId); } return createNewChat(); } async function createNewChat() { const res = await post("/chats", {title: "New chat"}); if (res.error || !res.chat) return setNotice(res.error || "Unable to create chat.", "error"); chatSessions.unshift(res.chat); await loadSummary(); renderChatList(); await setActiveChat(res.chat.id); setNotice("New chat created.", "success"); } async function startRoleplay() { const assistantPersonaId = Number(roleplayCharacterSelect.value); const userPersonaId = roleplayUserPersonaSelect.value ? Number(roleplayUserPersonaSelect.value) : null; if (!assistantPersonaId) { return setRoleplayStarterNotice("Choose a character to start the roleplay.", "error"); } roleplayStarterConfirm.disabled = true; setRoleplayStarterNotice("Starting roleplay..."); const res = await post("/roleplays/start", { assistantPersonaId, userPersonaId, model: modelSelect.value }); roleplayStarterConfirm.disabled = false; if (res.error || !res.chat) { return setRoleplayStarterNotice(res.error || "Unable to start roleplay.", "error"); } closeRoleplayStarter(); await loadChatSessions(); await setActiveChat(res.chat.id); setNotice(res.generatedInitialMessage ? "Roleplay started." : "Roleplay reopened.", "success"); } async function renameChat(id) { const chat = getChatById(id); const nextTitle = chat ? await promptPopup({ eyebrow: "Conversation", title: "Rename chat", description: "Choose a new title for this thread.", label: "Chat title", value: chat.title, placeholder: "New chat", confirmLabel: "Save" }) : ""; const title = (nextTitle || "").trim(); if (!chat || !title) return; const res = await put(`/chats/${id}`, {title}); if (res.error) return setNotice(res.error, "error"); chat.title = title; updateWorkspaceCopy(); renderChatList(); setNotice("Chat renamed.", "success"); } async function clearChat(id) { const chat = getChatById(id); if (!chat) return; const confirmed = await confirmPopup({ eyebrow: "Conversation", title: "Clear messages", description: `Clear all messages in "${chat.title}"? The chat will stay, but its history will be removed.`, confirmLabel: "Clear", danger: true }); if (!confirmed) return; const res = await post(`/chats/${id}/clear`, {}); if (res.error) return setNotice(res.error, "error"); currentMessages = []; renderMessages(); await loadSummary(); setNotice("Chat cleared.", "success"); } function exportCurrentChat() { const chat = getChatById(activeChatId); if (!chat || !currentMessages.length) return; const lines = [`Chat: ${chat.title}`, `Exported: ${new Date().toISOString()}`, ""]; currentMessages.forEach((msg) => { lines.push(`${msg.role === "user" ? "You" : "Assistant"}:`); lines.push(msg.content, ""); }); const blob = new Blob([lines.join("\n")], {type: "text/plain;charset=utf-8"}), url = URL.createObjectURL(blob), link = document.createElement("a"); link.href = url; link.download = `${(chat.title.replace(/[^a-z0-9-_]+/gi, "-").replace(/^-+|-+$/g, "") || "chat")}.txt`; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); setNotice("Chat exported.", "success"); } async function deleteChat(id) { const chat = getChatById(id); if (!chat) return; const confirmed = await confirmPopup({ eyebrow: "Conversation", title: "Delete chat", description: `Delete "${chat.title}"? This permanently removes the conversation.`, confirmLabel: "Delete", danger: true }); if (!confirmed) return; const res = await del(`/chats/${id}`); if (res.error) return setNotice(res.error, "error"); chatSessions = chatSessions.filter((item) => item.id !== id); if (activeChatId === id) { activeChatId = null; currentMessages = []; renderMessages(); } await loadSummary(); renderChatList(); if (chatSessions.length) await setActiveChat(chatSessions[0].id); else await createNewChat(); setNotice("Chat deleted.", "success"); } async function displayModels() { const res = await get("/models"); const models = Array.isArray(res.models) ? res.models : null; if (res.error || !models) { modelSelect.innerHTML = ""; updateModelHelp(); updateContextRail(); return setNotice("Models could not be loaded.", "error"); } modelSelect.innerHTML = ""; models.forEach((model, index) => { const option = document.createElement("option"); option.value = model.model; option.textContent = model.name; option.selected = index === 0; modelSelect.appendChild(option); }); updateModelHelp(); updateContextRail(); } async function checkSession() { const res = await get("/session"); if (!res.user) return false; currentUsername = res.user; authDiv.style.display = "none"; chatDiv.style.display = "block"; await Promise.all([displayModels(), loadSummary(), loadChatSessions(), loadPersonas()]); msgInput.focus(); return true; } async function handleAuth(endpoint, credentials) { const {username, password} = credentials, submitBtn = endpoint === "login" ? loginSubmit : registerSubmit; if (!username || !password) return setAuthMessage("Please enter both username and password.", "error"); submitBtn.disabled = true; setAuthMessage("Processing..."); const res = await post(`/${endpoint}`, {username, password}); submitBtn.disabled = false; if (res.error) return setAuthMessage(res.error, "error"); if (endpoint === "login") { currentUsername = username; authDiv.style.display = "none"; chatDiv.style.display = "block"; await Promise.all([displayModels(), loadSummary(), loadChatSessions(), loadPersonas()]); setNotice("Ready.", "success"); 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 message = msgInput.value.trim(); if (!message) return; if (!activeChatId) await createNewChat(); const currentChat = getChatById(activeChatId); if (currentChat && !currentChat.assistant_persona_id && currentChat.title === "New chat") { const autoTitle = message.length > 40 ? `${message.slice(0, 40)}...` : message; currentChat.title = autoTitle; updateWorkspaceCopy(); renderChatList(); await put(`/chats/${activeChatId}`, {title: autoTitle}); } currentMessages.push({role: "user", content: message}); addMessage(message, true); updateChatActionState(); msgInput.value = ""; msgInput.style.height = "auto"; setLoadingState(true); setNotice("Generating reply..."); const loadingMsg = addMessage("", false, true); try { const res = await post("/chat", {message, model: modelSelect.value, chatId: activeChatId}); if (messagesDiv.contains(loadingMsg)) messagesDiv.removeChild(loadingMsg); if (res.reply) { currentMessages.push({role: "bot", content: res.reply}); addMessage(res.reply); await loadSummary(); setNotice("Reply received.", "success"); } else { addMessage(`[Error: ${res.error}]`); setNotice(res.error || "Failed to generate reply.", "error"); } } catch { if (messagesDiv.contains(loadingMsg)) messagesDiv.removeChild(loadingMsg); addMessage("[Error: Network error]"); setNotice("Network error while sending message.", "error"); } finally { setLoadingState(false); msgInput.focus(); updateChatActionState(); } } function togglePersonaPopover(event) { event.stopPropagation(); closeModelPopover(); const isHidden = personaPopover.classList.contains("hidden"); personaPopover.classList.toggle("hidden", !isHidden); personaMenuButton.setAttribute("aria-expanded", String(isHidden)); } function toggleModelPopover(event) { event.stopPropagation(); closePersonaPopover(); const isHidden = modelPopover.classList.contains("hidden"); modelPopover.classList.toggle("hidden", !isHidden); modelMenuButton.setAttribute("aria-expanded", String(isHidden)); } toggleButtons.forEach((btn) => btn.addEventListener("click", () => showAuthScreen(btn.dataset.target))); loginForm.addEventListener("submit", (event) => { event.preventDefault(); void handleAuth("login", {username: loginUsernameInput.value.trim(), password: loginPasswordInput.value.trim()}); }); registerForm.addEventListener("submit", (event) => { event.preventDefault(); void handleAuth("register", {username: registerUsernameInput.value.trim(), password: registerPasswordInput.value.trim()}); }); $("logout").addEventListener("click", async () => { await post("/logout", {}); chatDiv.style.display = "none"; authDiv.style.display = "grid"; activeChatId = null; currentUsername = ""; currentSummary = null; chatSessions = []; currentMessages = []; assistantPersonas = []; userPersonas = []; activeUserPersonaId = null; publishedPersonaIds = new Set(); messagesDiv.innerHTML = ""; renderChatList(); updateChatActionState(); showAuthScreen("login"); setAuthMessage("Logged out.", "success"); setNotice("Ready."); }); sendBtn.addEventListener("click", () => { void sendMessage(); }); msgInput.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); void sendMessage(); } }); msgInput.addEventListener("input", function () { this.style.height = "auto"; this.style.height = `${Math.min(this.scrollHeight, 180)}px`; }); newChatBtn.addEventListener("click", () => { void createNewChat(); }); renameChatBtn.addEventListener("click", () => { if (activeChatId) void renameChat(activeChatId); }); clearChatBtn.addEventListener("click", () => { if (activeChatId) void clearChat(activeChatId); }); exportChatBtn.addEventListener("click", exportCurrentChat); chatSearchInput.addEventListener("input", renderChatList); modelSelect.addEventListener("change", () => { updateModelHelp(); updateContextRail(); }); roleplayNewPersonaBtn.addEventListener("click", () => openPersonaForm(null, "assistant")); clearUserPersonaBtn.addEventListener("click", () => { void clearPersona("user"); }); personaForm.addEventListener("submit", (event) => { event.preventDefault(); void savePersona(); }); personaCloseBtn.addEventListener("click", closePersonaForm); personaTypeSelect.addEventListener("change", () => { const isAssistant = personaTypeSelect.value === "assistant"; personaExamplesField.classList.toggle("hidden", !isAssistant); if (!isAssistant) personaExampleDialoguesInput.value = ""; }); personaModal.addEventListener("click", (event) => { if (event.target === personaModal) closePersonaForm(); }); roleplayStarterClose.addEventListener("click", closeRoleplayStarter); roleplayStarterCancel.addEventListener("click", closeRoleplayStarter); roleplayStarterConfirm.addEventListener("click", () => { void startRoleplay(); }); roleplayStarterModal.addEventListener("click", (event) => { if (event.target === roleplayStarterModal) closeRoleplayStarter(); }); popupConfirmBtn.addEventListener("click", () => { if (popupMode === "prompt") { closePopup(popupInput.value); return; } closePopup(true); }); popupCancelBtn.addEventListener("click", () => closePopup(false)); popupCloseBtn.addEventListener("click", () => closePopup(false)); popupModal.addEventListener("click", (event) => { if (event.target === popupModal) closePopup(false); }); popupInput.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); closePopup(popupInput.value); } }); modelMenuButton.addEventListener("click", toggleModelPopover); modelPopover.addEventListener("click", (event) => event.stopPropagation()); personaMenuButton.addEventListener("click", togglePersonaPopover); personaPopover.addEventListener("click", (event) => event.stopPropagation()); document.addEventListener("click", () => { closeModelPopover(); closePersonaPopover(); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeModelPopover(); closePersonaPopover(); closeRoleplayStarter(); closePersonaForm(); closePopup(false); } }); window.addEventListener("load", async () => { applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false); setNotice("Ready."); updateChatActionState(); await checkSession(); });