async function fetchStatus() { try { const url = new URL("/status", window.location.origin); url.searchParams.set("_", Date.now().toString()); const resp = await fetchWithTimeout(url, {}, 2000); if (!resp.ok) throw new Error("status error"); return await resp.json(); } catch (e) { return { ollama_connected: false, error: true }; } } const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function fetchWithTimeout(url, options = {}, timeoutMs = 2000) { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeoutMs); try { return await fetch(url, { ...options, signal: controller.signal }); } finally { clearTimeout(id); } } async function waitForStatus(timeoutMs = 15000) { const deadline = Date.now() + timeoutMs; while (true) { try { const url = new URL("/status", window.location.origin); url.searchParams.set("_", Date.now().toString()); const resp = await fetchWithTimeout(url, {}, 2000); if (resp.ok) return await resp.json(); } catch (e) { } if (Date.now() >= deadline) return null; await sleep(500); } } async function waitForPersonalityData(timeoutMs = 15000) { const loadingText = document.querySelector("#loading p"); let attempts = 0; const deadline = Date.now() + timeoutMs; while (true) { attempts += 1; try { const url = new URL("/personalities", window.location.origin); url.searchParams.set("_", Date.now().toString()); const resp = await fetchWithTimeout(url, {}, 2000); if (resp.ok) return await resp.json(); } catch (e) { } if (loadingText) { loadingText.textContent = attempts > 8 ? "Starting backend…" : "Loading…"; } if (Date.now() >= deadline) return null; await sleep(500); } } // ---------- Personalities API ---------- async function getPersonalities() { const url = new URL("/personalities", window.location.origin); url.searchParams.set("_", Date.now().toString()); const resp = await fetchWithTimeout(url, {}, 2000); if (!resp.ok) throw new Error("list_failed"); return await resp.json(); } async function loadPersonality(name) { const url = new URL("/personalities/load", window.location.origin); url.searchParams.set("name", name); url.searchParams.set("_", Date.now().toString()); const resp = await fetchWithTimeout(url, {}, 3000); if (!resp.ok) throw new Error("load_failed"); return await resp.json(); } async function savePersonality(payload) { // Try JSON POST first const saveUrl = new URL("/personalities/save", window.location.origin); saveUrl.searchParams.set("_", Date.now().toString()); let resp = await fetchWithTimeout(saveUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }, 5000); if (resp.ok) return await resp.json(); // Fallback to form-encoded POST try { const form = new URLSearchParams(); form.set("name", payload.name || ""); form.set("instructions", payload.instructions || ""); form.set("tools_text", payload.tools_text || ""); form.set("voice", payload.voice || "cedar"); const url = new URL("/personalities/save_raw", window.location.origin); url.searchParams.set("_", Date.now().toString()); resp = await fetchWithTimeout(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: form.toString(), }, 5000); if (resp.ok) return await resp.json(); } catch { } // Fallback to GET (query params) try { const url = new URL("/personalities/save_raw", window.location.origin); url.searchParams.set("name", payload.name || ""); url.searchParams.set("instructions", payload.instructions || ""); url.searchParams.set("tools_text", payload.tools_text || ""); url.searchParams.set("voice", payload.voice || "cedar"); url.searchParams.set("_", Date.now().toString()); resp = await fetchWithTimeout(url, { method: "GET" }, 5000); if (resp.ok) return await resp.json(); } catch { } const data = await resp.json().catch(() => ({})); throw new Error(data.error || "save_failed"); } async function applyPersonality(name, { persist = false } = {}) { const url = new URL("/personalities/apply", window.location.origin); url.searchParams.set("name", name || ""); if (persist) { url.searchParams.set("persist", "1"); } url.searchParams.set("_", Date.now().toString()); const resp = await fetchWithTimeout(url, { method: "POST" }, 5000); if (!resp.ok) { const data = await resp.json().catch(() => ({})); throw new Error(data.error || "apply_failed"); } return await resp.json(); } async function getVoices() { try { const url = new URL("/voices", window.location.origin); url.searchParams.set("_", Date.now().toString()); const resp = await fetchWithTimeout(url, {}, 3000); if (!resp.ok) throw new Error("voices_failed"); return await resp.json(); } catch (e) { return ["en-US-AriaNeural"]; } } function show(el, flag) { el.classList.toggle("hidden", !flag); } async function init() { const loading = document.getElementById("loading"); show(loading, true); const configuredPanel = document.getElementById("configured"); const ollamaErrorPanel = document.getElementById("ollama-error"); const personalityPanel = document.getElementById("personality-panel"); const retryBtn = document.getElementById("retry-btn"); const modelNameEl = document.getElementById("model-name"); // Personality elements const pSelect = document.getElementById("personality-select"); const pApply = document.getElementById("apply-personality"); const pPersist = document.getElementById("persist-personality"); const pNew = document.getElementById("new-personality"); const pSave = document.getElementById("save-personality"); const pStartupLabel = document.getElementById("startup-label"); const pName = document.getElementById("personality-name"); const pInstr = document.getElementById("instructions-ta"); const pTools = document.getElementById("tools-ta"); const pStatus = document.getElementById("personality-status"); const pVoice = document.getElementById("voice-select"); const pAvail = document.getElementById("tools-available"); const AUTO_WITH = { dance: ["stop_dance"], play_emotion: ["stop_emotion"], }; show(configuredPanel, false); show(ollamaErrorPanel, false); show(personalityPanel, false); // Check Ollama status const st = (await waitForStatus()) || { ollama_connected: false }; if (st.ollama_connected) { show(configuredPanel, true); if (modelNameEl && st.model) modelNameEl.textContent = st.model; } else { show(ollamaErrorPanel, true); show(loading, false); retryBtn.addEventListener("click", () => { window.location.reload(); }); return; } // Wait until backend routes are ready before rendering personalities UI const list = (await waitForPersonalityData()) || { choices: [] }; if (!list.choices.length) { pStatus.textContent = "Personality endpoints not ready yet. Retry shortly."; pStatus.className = "status warn"; show(loading, false); return; } // Initialize personalities UI try { const choices = Array.isArray(list.choices) ? list.choices : []; const DEFAULT_OPTION = choices[0] || "(built-in default)"; const startupChoice = choices.includes(list.startup) ? list.startup : DEFAULT_OPTION; const currentChoice = choices.includes(list.current) ? list.current : startupChoice; function setStartupLabel(name) { const display = name && name !== DEFAULT_OPTION ? name : "Built-in default"; pStartupLabel.textContent = `Launch on start: ${display}`; } // Populate select pSelect.innerHTML = ""; for (const n of choices) { const opt = document.createElement("option"); opt.value = n; opt.textContent = n; pSelect.appendChild(opt); } if (choices.length) { const preferred = choices.includes(startupChoice) ? startupChoice : currentChoice; pSelect.value = preferred; } const voices = await getVoices(); pVoice.innerHTML = ""; for (const v of voices) { const opt = document.createElement("option"); opt.value = v; opt.textContent = v; pVoice.appendChild(opt); } setStartupLabel(startupChoice); function renderToolCheckboxes(available, enabled) { pAvail.innerHTML = ""; const enabledSet = new Set(enabled); for (const t of available) { const wrap = document.createElement("div"); wrap.className = "chk"; const id = `tool-${t}`; const cb = document.createElement("input"); cb.type = "checkbox"; cb.id = id; cb.value = t; cb.checked = enabledSet.has(t); const lab = document.createElement("label"); lab.htmlFor = id; lab.textContent = t; wrap.appendChild(cb); wrap.appendChild(lab); pAvail.appendChild(wrap); } } function getSelectedTools() { const selected = new Set(); pAvail.querySelectorAll('input[type="checkbox"]').forEach((el) => { if (el.checked) selected.add(el.value); }); // Auto-include dependencies for (const [main, deps] of Object.entries(AUTO_WITH)) { if (selected.has(main)) { for (const d of deps) selected.add(d); } } return Array.from(selected); } function syncToolsTextarea() { const selected = getSelectedTools(); const comments = pTools.value .split("\n") .filter((ln) => ln.trim().startsWith("#")); const body = selected.join("\n"); pTools.value = (comments.join("\n") + (comments.length ? "\n" : "") + body).trim() + "\n"; } function attachToolHandlers() { pAvail.addEventListener("change", (ev) => { const target = ev.target; if (!(target instanceof HTMLInputElement) || target.type !== "checkbox") return; const name = target.value; // If a main tool toggled, propagate to deps if (AUTO_WITH[name]) { for (const dep of AUTO_WITH[name]) { const depEl = pAvail.querySelector(`input[value="${dep}"]`); if (depEl) depEl.checked = target.checked || depEl.checked; } } syncToolsTextarea(); }); } async function loadSelected() { const selected = pSelect.value; const data = await loadPersonality(selected); pInstr.value = data.instructions || ""; pTools.value = data.tools_text || ""; pVoice.value = data.voice || "en-US-AriaNeural"; // Available tools as checkboxes renderToolCheckboxes(data.available_tools, data.enabled_tools); attachToolHandlers(); // Default name field to last segment of selection const idx = selected.lastIndexOf("/"); pName.value = idx >= 0 ? selected.slice(idx + 1) : ""; pStatus.textContent = `Loaded ${selected}`; pStatus.className = "status"; } pSelect.addEventListener("change", loadSelected); await loadSelected(); show(personalityPanel, true); pApply.addEventListener("click", async () => { pStatus.textContent = "Applying..."; pStatus.className = "status"; try { const res = await applyPersonality(pSelect.value); if (res.startup) setStartupLabel(res.startup); pStatus.textContent = res.status || "Applied."; pStatus.className = "status ok"; } catch (e) { pStatus.textContent = `Failed to apply${e.message ? ": " + e.message : ""}`; pStatus.className = "status error"; } }); pPersist.addEventListener("click", async () => { pStatus.textContent = "Saving for startup..."; pStatus.className = "status"; try { const res = await applyPersonality(pSelect.value, { persist: true }); if (res.startup) setStartupLabel(res.startup); pStatus.textContent = res.status || "Saved for startup."; pStatus.className = "status ok"; } catch (e) { pStatus.textContent = `Failed to persist${e.message ? ": " + e.message : ""}`; pStatus.className = "status error"; } }); pNew.addEventListener("click", () => { pName.value = ""; pInstr.value = "# Write your instructions here\n# e.g., Keep responses concise and friendly."; pTools.value = "# tools enabled for this profile\n"; // Keep available tools list, clear selection pAvail.querySelectorAll('input[type="checkbox"]').forEach((el) => { el.checked = false; }); pVoice.value = "en-US-AriaNeural"; pStatus.textContent = "Fill fields and click Save."; pStatus.className = "status"; }); pSave.addEventListener("click", async () => { const name = (pName.value || "").trim(); if (!name) { pStatus.textContent = "Enter a valid name."; pStatus.className = "status warn"; return; } pStatus.textContent = "Saving..."; pStatus.className = "status"; try { // Ensure tools.txt reflects checkbox selection and auto-includes syncToolsTextarea(); const res = await savePersonality({ name, instructions: pInstr.value || "", tools_text: pTools.value || "", voice: pVoice.value || "en-US-AriaNeural", }); // Refresh select choices pSelect.innerHTML = ""; for (const n of res.choices) { const opt = document.createElement("option"); opt.value = n; opt.textContent = n; if (n === res.value) opt.selected = true; pSelect.appendChild(opt); } pStatus.textContent = "Saved."; pStatus.className = "status ok"; // Auto-apply try { await applyPersonality(pSelect.value); } catch { } } catch (e) { pStatus.textContent = "Failed to save."; pStatus.className = "status error"; } }); } catch (e) { pStatus.textContent = "UI failed to load. Please refresh."; pStatus.className = "status warn"; } finally { // Hide loading when initial setup is done show(loading, false); } } window.addEventListener("DOMContentLoaded", init);