beacon-watch
143 行
3.9 KiB
JavaScript
143 行
3.9 KiB
JavaScript
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 =
|
|
'<li class="entry"><span class="who">灯塔</span><p class="text">本班还是空白。从左侧笔记开始,或直接在下面写下第一句。</p></li>';
|
|
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 (
|
|
'<li class="' +
|
|
cls +
|
|
'"><span class="who">' +
|
|
who +
|
|
'</span><p class="text">' +
|
|
esc(item.content) +
|
|
"</p></li>"
|
|
);
|
|
})
|
|
.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();
|