Spaces:
Build error
Build error
| 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); | |