/* Intyoza chat page. Needs common.js loaded first (t, getLang, setLang, setTheme, ...). */ let chatId = null, user = null, userInfo = null, guest = [], authMode = "login", allChats = []; const $ = id => document.getElementById(id); async function api(path, opts = {}) { try { const res = await fetch(path, { headers: { "Content-Type": "application/json" }, credentials: "include", ...opts }); return await res.json(); } catch (e) { return { ok: false, message: t("net_err") }; } } const initial = n => (n || "?")[0].toUpperCase(); const avatarMarkup = (n, a) => a ? `` : initial(n); const displayTitle = s => (s === "New chat" ? t("new_chat") : s); let toastTimer; function toast(msg) { let el = $("toast"); if (!el) { el = document.createElement("div"); el.id = "toast"; document.body.appendChild(el); } el.textContent = msg; el.classList.add("show"); clearTimeout(toastTimer); toastTimer = setTimeout(() => el.classList.remove("show"), 2200); } /* ---------- message formatting ---------- */ function escapeHtml(str) { return str.replace(/&/g, "&").replace(//g, ">"); } function formatMessage(text) { let safe = escapeHtml(text); safe = safe.replace(/\*\*(.+?)\*\*/g, "$1"); safe = safe.replace(/\*(.+?)\*/g, "$1"); // words with a circumflex = amasaku/ubutinde marks (any letters, apostrophes allowed) safe = safe.replace(/[\p{L}']*[âêîôûÂÊÎÔÛ][\p{L}']*/gu, '$&'); return safe; } /* ---------- icons ---------- */ const ICON = { copy: ``, check: ``, del: ``, mark: ``, }; /* ---------- messages ---------- */ function makeRow(id, role, text, imageUrl, isError, sources) { const row = document.createElement("div"); row.className = "row " + (role === "user" ? "user" : "bot"); if (id) row.dataset.id = id; const b = document.createElement("div"); b.className = "bubble" + (isError ? " err" : ""); if (imageUrl) { const img = document.createElement("img"); img.src = imageUrl; img.className = "bubble-image"; b.appendChild(img); } if (text) { const p = document.createElement("div"); p.innerHTML = formatMessage(text); b.appendChild(p); } const acts = document.createElement("div"); acts.className = "acts"; const mkBtn = (icon, titleKey) => { const el = document.createElement("button"); el.className = "act"; el.title = t(titleKey); el.dataset.titleKey = titleKey; el.innerHTML = icon; return el; }; const copy = mkBtn(ICON.copy, "copy"); copy.onclick = () => { navigator.clipboard.writeText(text || ""); copy.innerHTML = ICON.check; setTimeout(() => (copy.innerHTML = ICON.copy), 1400); }; acts.appendChild(copy); if (role !== "user" && !isError && text) { const mark = mkBtn(ICON.mark, "save_phrase"); mark.onclick = async () => { if (!user) { toast(t("saved_login")); return; } const d = await api("/api/saved", { method: "POST", body: JSON.stringify({ text }) }); if (d.ok) { mark.classList.add("on"); toast(t("saved_ok")); } }; acts.appendChild(mark); } const del = mkBtn(ICON.del, "del"); del.onclick = async () => { if (row.dataset.id) await api(`/api/messages/${row.dataset.id}`, { method: "DELETE" }); row.remove(); checkEmpty(); }; acts.appendChild(del); const src = renderSources(sources); if (src) row.append(b, src, acts); else row.append(b, acts); return row; } /* collapsible "Inkomoko (n)" list under a reply (news from igihe.com etc.) */ function renderSources(sources) { if (!sources || !sources.length) return null; const d = document.createElement("details"); d.className = "sources"; const sm = document.createElement("summary"); sm.textContent = `${t("sources_label")} (${sources.length})`; const ul = document.createElement("ul"); sources.forEach(s => { const li = document.createElement("li"); const a = document.createElement("a"); a.href = s.url; a.target = "_blank"; a.rel = "noopener noreferrer"; a.textContent = s.title; const host = document.createElement("small"); let h = ""; try { h = new URL(s.url).hostname.replace(/^www\./, ""); } catch (e) {} host.textContent = [h, s.section].filter(Boolean).join(" · "); li.append(a, host); ul.appendChild(li); }); d.append(sm, ul); return d; } /* suggested follow-up questions under the latest reply */ let suggestSeq = 0; function clearSuggestions() { suggestSeq++; document.querySelectorAll(".suggest").forEach(e => e.remove()); } function showSuggestions(list) { document.querySelectorAll(".suggest").forEach(e => e.remove()); if (!list || !list.length) return; const box = document.createElement("div"); box.className = "suggest"; const lab = document.createElement("p"); lab.className = "suggest-label"; lab.textContent = t("suggest_label"); box.appendChild(lab); list.forEach(q => { const b = document.createElement("button"); b.className = "suggest-chip"; b.textContent = q; b.onclick = () => { $("msg-box").value = q; send(); }; box.appendChild(b); }); $("chatbox").appendChild(box); $("chatbox").scrollTop = $("chatbox").scrollHeight; } async function fetchSuggestions(userText, reply, messageId) { const seq = ++suggestSeq; const d = await api("/api/suggest", { method: "POST", body: JSON.stringify({ user: userText, assistant: reply, message_id: messageId || null }) }); if (seq === suggestSeq && d.ok) showSuggestions(d.suggestions); } function checkEmpty() { const box = $("chatbox"); if (!box.querySelector(".row") && !box.querySelector("#empty-state")) renderEmptyState(); } function renderEmptyState() { $("chatbox").innerHTML = `

`; applyI18n(); bindChips(); } function bindChips() { document.querySelectorAll(".chip").forEach(c => c.onclick = () => { $("msg-box").value = c.textContent; send(); }); } function render(list) { const box = $("chatbox"); box.innerHTML = ""; if (!list.length) { renderEmptyState(); return; } list.forEach(m => box.appendChild(makeRow(m.id, m.role, m.content, null, false, m.sources))); const last = list[list.length - 1]; if (last && last.role !== "user" && last.suggestions && last.suggestions.length) showSuggestions(last.suggestions); box.scrollTop = box.scrollHeight; } /* ---------- thinking indicator (rotating simple phrases) ---------- */ let thinkTimer = null; function showTyping() { const r = document.createElement("div"); r.className = "row bot typing"; r.id = "typing"; r.innerHTML = `
`; $("chatbox").appendChild(r); $("chatbox").scrollTop = $("chatbox").scrollHeight; const el = r.querySelector(".think-text"); const phrases = t("think"); let i = 0; el.textContent = phrases[0]; thinkTimer = setInterval(() => { el.classList.add("fade"); setTimeout(() => { i = (i + 1) % phrases.length; el.textContent = phrases[i]; el.classList.remove("fade"); }, 260); }, 2600); } function hideTyping() { clearInterval(thinkTimer); thinkTimer = null; $("typing")?.remove(); } /* ---------- sidebar ---------- */ function closeMenus() { document.querySelectorAll(".menu.open").forEach(m => m.classList.remove("open")); document.querySelectorAll(".dots-btn.open").forEach(b => b.classList.remove("open")); } document.addEventListener("click", closeMenus); $("chat-list").addEventListener("scroll", closeMenus, { passive: true }); function paintChats(list) { const wrap = $("chat-list"); wrap.innerHTML = ""; $("chat-empty").classList.toggle("hidden", list.length > 0 || !user); list.forEach(c => { const row = document.createElement("div"); row.className = "chat-row" + (c.id === chatId ? " active" : ""); const name = document.createElement("div"); name.className = "chat-name"; name.textContent = displayTitle(c.title); name.title = displayTitle(c.title); name.onclick = () => openChat(c.id, c.title); const dots = document.createElement("button"); dots.className = "dots-btn"; dots.innerHTML = ``; const menu = document.createElement("div"); menu.className = "menu"; menu.innerHTML = ` `; dots.onclick = e => { e.stopPropagation(); const wasOpen = menu.classList.contains("open"); closeMenus(); if (!wasOpen) { const r = dots.getBoundingClientRect(); menu.style.top = (r.bottom + 6) + "px"; menu.style.left = Math.min(r.left, window.innerWidth - 170) + "px"; menu.classList.add("open"); dots.classList.add("open"); } }; menu.querySelector('[data-a="rename"]').onclick = e => { e.stopPropagation(); closeMenus(); inlineRename(row, name, c); }; menu.querySelector('[data-a="delete"]').onclick = async e => { e.stopPropagation(); closeMenus(); await api(`/api/chats/${c.id}`, { method: "DELETE" }); const rest = await loadChats(); if (c.id === chatId) { rest.length ? openChat(rest[0].id, rest[0].title) : newChat(); } }; row.append(dots, name, menu); wrap.appendChild(row); }); } function inlineRename(row, name, c) { const inp = document.createElement("input"); inp.className = "rename-input"; inp.value = displayTitle(c.title); row.replaceChild(inp, name); inp.focus(); inp.select(); let done = false; const commit = async () => { if (done) return; done = true; const title = inp.value.trim() || "New chat"; await api(`/api/chats/${c.id}`, { method: "PATCH", body: JSON.stringify({ title }) }); if (c.id === chatId) $("chat-title").textContent = displayTitle(title); loadChats(); }; inp.onkeydown = e => { if (e.key === "Enter") inp.blur(); if (e.key === "Escape") { done = true; loadChats(); } }; inp.onblur = commit; inp.onclick = e => e.stopPropagation(); } async function loadChats() { const d = await api("/api/chats"); allChats = d.chats || []; applySearch(); return allChats; } function applySearch() { const q = $("chat-search").value.trim().toLowerCase(); paintChats(q ? allChats.filter(c => displayTitle(c.title).toLowerCase().includes(q)) : allChats); } $("chat-search").oninput = applySearch; async function openChat(id, title) { chatId = id; $("chat-title").textContent = displayTitle(title); const d = await api(`/api/chats/${id}/messages`); render(d.messages || []); applySearch(); } async function newChat() { if (!user) { chatId = null; guest = []; render([]); $("chat-title").textContent = t("new_chat"); return; } const d = await api("/api/chats", { method: "POST" }); if (d.ok) { chatId = d.id; $("chat-title").textContent = displayTitle(d.title); render([]); loadChats(); } } /* ---------- attach image ---------- */ let pendingImage = null; $("attach-btn").onclick = () => $("attach-input").click(); $("attach-input").onchange = e => { const f = e.target.files[0]; if (!f) return; if (f.size > 4_000_000) { alert(t("img_big")); return; } const r = new FileReader(); r.onload = () => { pendingImage = r.result; $("attach-thumb").src = pendingImage; $("attach-preview").classList.remove("hidden"); }; r.readAsDataURL(f); }; $("attach-remove").onclick = () => { pendingImage = null; $("attach-input").value = ""; $("attach-preview").classList.add("hidden"); }; /* ---------- microphone (Web Speech API) ---------- */ const SR = window.SpeechRecognition || window.webkitSpeechRecognition; let recognizer = null, listening = false; if (SR) { recognizer = new SR(); recognizer.continuous = true; recognizer.interimResults = true; recognizer.onresult = e => { let transcript = ""; for (let i = 0; i < e.results.length; i++) transcript += e.results[i][0].transcript; $("msg-box").value = transcript; $("msg-box").dispatchEvent(new Event("input")); }; recognizer.onend = () => { listening = false; $("mic-btn").classList.remove("recording"); }; recognizer.onerror = e => { listening = false; $("mic-btn").classList.remove("recording"); if (e.error === "not-allowed") alert(t("mic_blocked")); if (e.error === "language-not-supported") toast(t("mic_lang")); }; } else { $("mic-btn").style.display = "none"; } $("mic-btn").onclick = () => { if (!recognizer) return; if (listening) { recognizer.stop(); return; } // Speech language follows the interface language (Kinyarwanda support depends on the browser). recognizer.lang = getLang() === "rw" ? "rw-RW" : "en-US"; try { recognizer.start(); listening = true; $("mic-btn").classList.add("recording"); } catch (e) { /* already running */ } }; /* ---------- send ---------- */ let activeController = null; const sendIconSVG = ``; const stopIconSVG = ``; let thinking = false; function setThinking(on) { thinking = on; const btn = $("send-btn"); if (on) { btn.innerHTML = stopIconSVG; btn.title = t("stop"); btn.classList.add("stopping"); btn.onclick = () => { if (activeController) activeController.abort(); }; } else { btn.innerHTML = sendIconSVG; btn.title = t("send"); btn.classList.remove("stopping"); btn.onclick = send; } } function aiErrorText(d) { if (d.code === "rate_limited") return t("rate_limited"); if (d.code && I18N_HAS("err_" + d.code)) return t("err_" + d.code); return d.message || t("something_wrong"); } const I18N_HAS = k => t(k) !== k; async function send() { if (thinking) return; const box = $("msg-box"); const text = box.value.trim(); const imageToSend = pendingImage; if (!text && !imageToSend) return; if (listening && recognizer) recognizer.stop(); clearSuggestions(); box.value = ""; box.style.height = "auto"; $("empty-state")?.remove(); const userRow = makeRow(null, "user", text, imageToSend); $("chatbox").appendChild(userRow); $("chatbox").scrollTop = $("chatbox").scrollHeight; // No "mode" any more: the tutor adapts to the learner by itself on the server. const payload = { message: text }; if (imageToSend) payload.image = imageToSend; if (user && chatId) payload.chat_id = chatId; else payload.history = guest.slice(-20); pendingImage = null; $("attach-input").value = ""; $("attach-preview").classList.add("hidden"); showTyping(); setThinking(true); activeController = new AbortController(); let d; try { const res = await fetch("/api/message", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify(payload), signal: activeController.signal, }); d = await res.json(); } catch (e) { if (e.name === "AbortError") { hideTyping(); setThinking(false); return; } d = { ok: false, message: t("net_err") }; } setThinking(false); hideTyping(); if (!d.ok) { $("chatbox").appendChild(makeRow(null, "bot", aiErrorText(d), null, true)); $("chatbox").scrollTop = $("chatbox").scrollHeight; return; } if (d.user_message_id) userRow.dataset.id = d.user_message_id; $("chatbox").appendChild(makeRow(d.assistant_message_id, "bot", d.reply, null, false, d.sources)); $("chatbox").scrollTop = $("chatbox").scrollHeight; fetchSuggestions(text, d.reply, d.assistant_message_id); if (d.new_title) { $("chat-title").textContent = displayTitle(d.new_title); loadChats(); } else if (user) loadChats(); if (!user) guest.push({ role: "user", content: text }, { role: "assistant", content: d.reply }); } /* ---------- auth ---------- */ function paintAuth() { const login = authMode === "login"; $("auth-title").textContent = t(login ? "auth_title_login" : "auth_title_signup"); $("auth-sub").textContent = t(login ? "auth_sub_login" : "auth_sub_signup"); $("auth-submit").querySelector(".btn-label").textContent = t(login ? "login" : "signup"); $("switch-text").textContent = t(login ? "new_here" : "have_acct"); $("auth-switch").textContent = t(login ? "create_acct" : "login_instead"); } function setAuthMode(m) { authMode = m; paintAuth(); $("auth-status").textContent = ""; $("auth-status").className = "status"; } function loading(on) { const b = $("auth-submit"); b.disabled = on; b.querySelector(".spinner").classList.toggle("hidden", !on); b.querySelector(".btn-label").style.opacity = on ? ".55" : "1"; } $("auth-switch").onclick = () => setAuthMode(authMode === "login" ? "signup" : "login"); $("login-nav-btn").onclick = () => { setAuthMode("login"); showAuth(); }; $("signup-nav-btn").onclick = () => { setAuthMode("signup"); showAuth(); }; function showAuth() { $("auth-view").classList.remove("hidden"); $("app-view").classList.add("hidden"); } function showApp() { $("auth-view").classList.add("hidden"); $("app-view").classList.remove("hidden"); } function authMessage(d) { if (d.code && I18N_HAS("err_" + d.code)) return t("err_" + d.code); return d.message || ""; } $("auth-submit").onclick = async () => { const username = $("auth-username").value.trim(); const password = $("auth-password").value; const st = $("auth-status"); if (!username || !password) { st.textContent = t("err_missing_fields"); st.className = "status error"; return; } loading(true); const path = authMode === "login" ? "/api/login" : "/api/signup"; const d = await api(path, { method: "POST", body: JSON.stringify({ username, password }) }); loading(false); st.textContent = d.ok ? "" : authMessage(d); st.className = "status " + (d.ok ? "ok" : "error"); if (d.ok && authMode === "login") { await refresh(); } else if (d.ok) { setAuthMode("login"); st.textContent = t("acct_created"); st.className = "status ok"; } }; $("auth-password").onkeydown = e => { if (e.key === "Enter") $("auth-submit").click(); }; document.querySelectorAll(".oauth-btn").forEach(b => { b.onclick = async () => { const p = b.dataset.provider; const s = await api("/api/oauth-status"); if (!s[p]) { const n = $("oauth-note"); n.textContent = t("oauth_unconf", { p: p === "google" ? "Google" : "GitHub" }); n.classList.remove("hidden"); return; } window.location.href = `/auth/${p}`; }; }); $("logout-btn").onclick = async () => { await api("/api/logout", { method: "POST" }); chatId = null; guest = []; closeDash(); await refresh(); }; /* ---------- dashboard ---------- */ function openDash() { if (!user) return; $("dash-modal").classList.remove("hidden"); $("dash-status").textContent = ""; fillDashHeader(); syncSegs(); $("set-name").value = userInfo?.display_name || user; $("pw-box").classList.toggle("hidden", !userInfo?.has_password); switchTab("overview"); loadDash(); } function closeDash() { $("dash-modal").classList.add("hidden"); } function fillDashHeader() { const shown = userInfo?.display_name || user; $("dash-name").textContent = shown; const d = userInfo?.created_at ? new Date(userInfo.created_at).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }) : ""; $("dash-sub").textContent = d ? t("joined", { d }) : "@" + user; $("modal-avatar").innerHTML = avatarMarkup(shown, userInfo?.avatar); } function switchTab(name) { document.querySelectorAll(".tab").forEach(b => b.classList.toggle("active", b.dataset.tab === name)); document.querySelectorAll(".tab-pane").forEach(p => p.classList.toggle("hidden", p.id !== "tab-" + name)); if (name === "saved") loadSaved(); if (name === "overview") loadDash(); } document.querySelectorAll(".tab").forEach(b => b.onclick = () => switchTab(b.dataset.tab)); $("profile-btn").onclick = openDash; $("dash-close").onclick = closeDash; $("modal-close").onclick = closeDash; $("dash-modal").addEventListener("click", e => { if (e.target === $("dash-modal")) closeDash(); }); document.addEventListener("keydown", e => { if (e.key === "Escape") closeDash(); }); async function loadDash() { const d = await api(`/api/dashboard?tz=${new Date().getTimezoneOffset()}`); if (!d.ok) return; $("st-chats").textContent = d.stats.chats; $("st-msgs").textContent = d.stats.messages; $("st-days").textContent = d.stats.active_days; $("st-streak").textContent = d.stats.streak; $("st-saved").textContent = d.stats.saved; const max = Math.max(1, ...d.activity.map(a => a.count)); const days = t("days"); $("chart").innerHTML = d.activity.map(a => { const wd = days[new Date(a.date + "T00:00:00").getDay()]; const h = a.count ? Math.max(8, Math.round(a.count / max * 100)) : 0; return `
${wd}
`; }).join(""); } async function loadSaved() { const d = await api("/api/saved"); const list = $("saved-list"); list.innerHTML = ""; const items = d.saved || []; $("saved-empty").classList.toggle("hidden", items.length > 0); items.forEach(s => { const it = document.createElement("div"); it.className = "saved-item"; const p = document.createElement("div"); const txt = document.createElement("p"); txt.innerHTML = formatMessage(s.text); const sm = document.createElement("small"); sm.textContent = new Date(s.created_at).toLocaleDateString(); p.style.flex = "1"; p.append(txt, sm); const cp = document.createElement("button"); cp.className = "act"; cp.title = t("copy"); cp.innerHTML = ICON.copy; cp.onclick = () => { navigator.clipboard.writeText(s.text); toast(t("copy")); }; const rm = document.createElement("button"); rm.className = "act"; rm.title = t("del"); rm.innerHTML = ICON.del; rm.onclick = async () => { await api(`/api/saved/${s.id}`, { method: "DELETE" }); loadSaved(); loadDash(); }; it.append(p, cp, rm); list.appendChild(it); }); } function dashStatus(msg, ok) { const st = $("dash-status"); st.textContent = msg; st.className = "status " + (ok ? "ok" : "error"); } $("set-name-save").onclick = async () => { const name = $("set-name").value.trim(); if (!name) return; const d = await api("/api/profile", { method: "PATCH", body: JSON.stringify({ display_name: name }) }); if (d.ok) { userInfo.display_name = name; fillDashHeader(); paintNav(); paintWelcome(); dashStatus(t("name_saved"), true); } else dashStatus(authMessage(d), false); }; function syncSegs() { document.querySelectorAll("[data-theme-val]").forEach(b => b.classList.toggle("active", b.dataset.themeVal === getTheme())); document.querySelectorAll("[data-lang-val]").forEach(b => b.classList.toggle("active", b.dataset.langVal === getLang())); } document.querySelectorAll("[data-theme-val]").forEach(b => b.onclick = () => setTheme(b.dataset.themeVal)); document.querySelectorAll("[data-lang-val]").forEach(b => b.onclick = () => setLang(b.dataset.langVal)); $("pw-save").onclick = async () => { const d = await api("/api/profile/password", { method: "POST", body: JSON.stringify({ current: $("pw-cur").value, new: $("pw-new").value }) }); if (d.ok) { $("pw-cur").value = ""; $("pw-new").value = ""; dashStatus(t("pw_ok"), true); } else dashStatus(authMessage(d), false); }; $("export-btn").onclick = async () => { const res = await fetch("/api/export", { credentials: "include" }); const blob = await res.blob(); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "intyoza-export.json"; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 2000); }; $("clear-btn").onclick = async () => { if (!confirm(t("confirm_clear"))) return; await api("/api/chats/clear", { method: "POST" }); chatId = null; closeDash(); await refresh(); }; $("delete-acct-btn").onclick = async () => { if (prompt(t("del_prompt")) !== "DELETE") return; const d = await api("/api/account", { method: "DELETE", body: JSON.stringify({ confirm: "DELETE" }) }); if (d.ok) { chatId = null; guest = []; closeDash(); await refresh(); } }; /* avatar: crop to a square and shrink before upload */ function resizeImage(file, size = 256) { return new Promise((resolve, reject) => { const img = new Image(); const url = URL.createObjectURL(file); img.onload = () => { const c = document.createElement("canvas"); c.width = c.height = size; const s = Math.min(img.width, img.height); c.getContext("2d").drawImage(img, (img.width - s) / 2, (img.height - s) / 2, s, s, 0, 0, size, size); URL.revokeObjectURL(url); resolve(c.toDataURL("image/jpeg", 0.86)); }; img.onerror = reject; img.src = url; }); } $("avatar-input").onchange = async e => { const f = e.target.files[0]; if (!f) return; try { const dataUrl = await resizeImage(f); const d = await api("/api/profile/avatar", { method: "POST", body: JSON.stringify({ avatar: dataUrl }) }); if (d.ok) { userInfo.avatar = d.avatar; fillDashHeader(); paintNav(); } } catch (err) { /* unreadable image: ignore */ } }; /* ---------- session + repaint ---------- */ function paintNav() { const shown = userInfo?.display_name || user; $("nav-name").textContent = shown; $("nav-avatar").innerHTML = avatarMarkup(shown, userInfo?.avatar); } function paintWelcome() { $("welcome-line").textContent = user ? t("welcome_back", { name: userInfo?.display_name || user }) : t("panel_sub"); } async function refresh() { const d = await api("/api/session"); user = d.username || null; userInfo = user ? d : null; if (user) { $("guest-nav").classList.add("hidden"); $("user-nav").classList.remove("hidden"); paintNav(); paintWelcome(); $("sidebar-hint").classList.add("hidden"); // Saved preferences follow the account; first time, push the local ones up. if (d.prefs && (d.prefs.lang || d.prefs.theme)) { if (d.prefs.theme && d.prefs.theme !== getTheme()) setTheme(d.prefs.theme, true); if (d.prefs.lang && d.prefs.lang !== getLang()) setLang(d.prefs.lang, true); } else { api("/api/profile", { method: "PATCH", body: JSON.stringify({ prefs: { theme: getTheme(), lang: getLang() } }) }); } const chats = await loadChats(); chats.length ? openChat(chats[0].id, chats[0].title) : await newChat(); } else { $("guest-nav").classList.remove("hidden"); $("user-nav").classList.add("hidden"); paintWelcome(); $("sidebar-hint").classList.remove("hidden"); $("chat-empty").classList.add("hidden"); $("chat-list").innerHTML = ""; $("chat-title").textContent = t("new_chat"); render(guest); } showApp(); } /* theme or language changed (from the header buttons, the dashboard, or the server) */ window.addEventListener("uichange", e => { paintAuth(); paintWelcome(); syncSegs(); setThinking(thinking); if (!$("dash-modal").classList.contains("hidden")) { fillDashHeader(); loadDash(); } if (!user && !$("chatbox").querySelector(".row")) $("chat-title").textContent = t("new_chat"); if (user) { applySearch(); const cur = allChats.find(c => c.id === chatId); if (cur) $("chat-title").textContent = displayTitle(cur.title); } document.querySelectorAll(".act[data-title-key]").forEach(b => { b.title = t(b.dataset.titleKey); }); if (user && !e.detail.fromServer) { api("/api/profile", { method: "PATCH", body: JSON.stringify({ prefs: { theme: e.detail.theme, lang: e.detail.lang } }) }); } }); /* ---------- misc ---------- */ $("new-chat-btn").onclick = newChat; $("msg-box").onkeydown = e => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }; $("msg-box").oninput = function () { this.style.height = "auto"; this.style.height = Math.min(this.scrollHeight, 150) + "px"; }; if (new URLSearchParams(location.search).get("oauth_error")) { history.replaceState({}, "", "/chat"); } setAuthMode("login"); bindChips(); setThinking(false); syncSegs(); refresh();