const API_BASE = "/api/cloud"; const STARTERS = [ "今晚沿岸有雾,怎么判断能不能开航?", "用一段话解释什么是上下文窗口。", "帮我把这段需求收成三条验收标准:做一个能记住对话的助手。" ]; const thread = document.getElementById("thread"); const form = document.getElementById("composer"); const prompt = document.getElementById("prompt"); const send = document.getElementById("send"); const linkStatus = document.getElementById("link-status"); const starters = document.getElementById("starters"); const KEY = "beacon-watch.thread"; function esc(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } function loadHistory() { try { const raw = localStorage.getItem(KEY); const parsed = raw ? JSON.parse(raw) : []; return Array.isArray(parsed) ? parsed : []; } catch { return []; } } function saveHistory(items) { localStorage.setItem(KEY, JSON.stringify(items.slice(-24))); } function render(items) { if (!items.length) { thread.innerHTML = '
  • 灯塔

    本班还是空白。从左侧笔记开始,或直接在下面写下第一句。

  • '; return; } thread.innerHTML = items .map(function (item) { const cls = item.role === "user" ? "entry user" : item.role === "error" ? "entry error" : "entry"; const who = item.role === "user" ? "来访" : item.role === "error" ? "断线" : "夜航员"; return ( '
  • ' + who + '

    ' + esc(item.content) + "

  • " ); }) .join(""); thread.scrollTop = thread.scrollHeight; } async function ping() { try { const res = await fetch(API_BASE + "/health"); const data = await res.json(); if (data && data.bindings && data.bindings.model) { linkStatus.textContent = "灯芯已接上"; } else { linkStatus.textContent = "灯还在,模型令牌未接通"; } } catch { linkStatus.textContent = "岸上链路未通"; } } async function ask(question) { const history = loadHistory(); history.push({ role: "user", content: question }); saveHistory(history); render(history); document.body.classList.add("is-thinking"); send.disabled = true; try { const res = await fetch(API_BASE + "/chat", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ messages: history.filter(function (item) { return item.role === "user" || item.role === "assistant"; }) }) }); const data = await res.json(); if (!res.ok || data.ok === false) { throw new Error(data.error || "HTTP " + res.status); } history.push({ role: "assistant", content: data.answer }); } catch (error) { history.push({ role: "error", content: error.message || "发灯失败" }); } finally { saveHistory(history); render(history); document.body.classList.remove("is-thinking"); send.disabled = false; prompt.focus(); } } STARTERS.forEach(function (text) { const button = document.createElement("button"); button.type = "button"; button.textContent = text; button.addEventListener("click", function () { ask(text); }); starters.appendChild(button); }); form.addEventListener("submit", function (event) { event.preventDefault(); const question = prompt.value.trim(); if (!question) { return; } prompt.value = ""; ask(question); }); prompt.addEventListener("keydown", function (event) { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); form.requestSubmit(); } }); document.getElementById("clear").addEventListener("click", function () { saveHistory([]); render([]); }); render(loadHistory()); ping();