Abduallah Abuhassan
Initialize Git LFS and add project files with binary tracking
b82aa95
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);