vintage-chat / web /app.js
morrissu12's picture
Update Space scaffold
361d814 verified
Raw
History Blame Contribute Delete
8.09 kB
// Vintage Chat frontend
const $ = (id) => document.getElementById(id);
const el = {
modelInfo: $("model-info"),
footerCkpt: $("footer-checkpoint"),
footerDevice: $("footer-device"),
registerTabs: $("register-tabs"),
examplePrompts: $("example-prompts"),
prompt: $("prompt"),
generateBtn: $("generate-btn"),
cancelBtn: $("cancel-btn"),
status: $("status"),
output: $("output"),
copyBtn: $("copy-btn"),
translateControls: $("translate-controls"),
translateInput: $("translate-input"),
translateOutput: $("translate-output"),
resolvedPrompt: $("resolved-prompt"),
gloss: $("gloss"),
glossText: $("gloss-text"),
maxNewTokens: $("max-new-tokens"),
temperature: $("temperature"),
topK: $("top-k"),
topP: $("top-p"),
repPenalty: $("rep-penalty"),
seed: $("seed"),
};
let examples = { registers: [] };
let activeRegister = null;
let abortCtrl = null;
let translationAvailable = false;
async function loadConfig() {
try {
const r = await fetch("/api/config");
const cfg = await r.json();
el.footerCkpt.textContent = cfg.model.checkpoint || "—";
el.footerDevice.textContent = `${cfg.model.device}${cfg.model.dtype ? " · " + cfg.model.dtype : ""}`;
el.modelInfo.textContent = `${cfg.model.checkpoint?.split("/").slice(-2).join("/") || "model"} · ${cfg.model.device}`;
translationAvailable = !!cfg.translation_available;
el.translateControls.hidden = !translationAvailable;
} catch (e) {
el.modelInfo.textContent = "(config unavailable)";
}
}
async function loadExamples() {
const r = await fetch("/api/examples");
examples = await r.json();
renderRegisterTabs();
if (examples.registers?.length) setActiveRegister(examples.registers[0].id);
}
function renderRegisterTabs() {
el.registerTabs.innerHTML = "";
for (const reg of examples.registers || []) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "register-tab";
btn.dataset.id = reg.id;
btn.textContent = `${reg.label} · ${reg.label_en}`;
btn.addEventListener("click", () => setActiveRegister(reg.id));
el.registerTabs.appendChild(btn);
}
}
function setActiveRegister(id) {
activeRegister = id;
for (const btn of el.registerTabs.querySelectorAll(".register-tab")) {
btn.classList.toggle("active", btn.dataset.id === id);
}
const reg = examples.registers.find((r) => r.id === id);
if (!reg) return;
el.examplePrompts.innerHTML = "";
const desc = document.createElement("div");
desc.className = "register-description";
desc.textContent = reg.description || "";
el.examplePrompts.appendChild(desc);
for (const p of reg.prompts || []) {
const b = document.createElement("button");
b.type = "button";
b.className = "example-prompt";
b.textContent = p;
b.addEventListener("click", () => {
el.prompt.value = p;
el.translateInput.checked = false;
el.prompt.focus();
});
el.examplePrompts.appendChild(b);
}
}
function readParams() {
const numeric = (input, fallback) => {
const v = parseFloat(input.value);
return Number.isFinite(v) ? v : fallback;
};
return {
max_new_tokens: parseInt(el.maxNewTokens.value, 10) || 200,
temperature: numeric(el.temperature, 0.8),
top_k: parseInt(el.topK.value, 10) ?? 50,
top_p: numeric(el.topP, 0.9),
repetition_penalty: numeric(el.repPenalty, 1.3),
seed: el.seed.value ? parseInt(el.seed.value, 10) : null,
};
}
function setStatus(msg, kind = "info") {
el.status.textContent = msg;
el.status.className = "status" + (kind === "err" ? " err" : "");
}
async function translateInput(text) {
setStatus("Translating input → 文言文…");
const r = await fetch("/api/translate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, direction: "to_classical", language: "auto" }),
});
if (!r.ok) throw new Error(`Translation failed (${r.status})`);
const data = await r.json();
return data.text;
}
async function translateOutput(text) {
setStatus("Glossing output → modern…");
const r = await fetch("/api/translate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, direction: "to_modern", language: "Modern Chinese" }),
});
if (!r.ok) throw new Error(`Gloss failed (${r.status})`);
const data = await r.json();
return data.text;
}
async function runGenerate() {
const rawPrompt = el.prompt.value.trim();
if (!rawPrompt) return;
el.generateBtn.disabled = true;
el.cancelBtn.disabled = false;
el.copyBtn.disabled = true;
el.output.textContent = "";
el.gloss.hidden = true;
el.glossText.textContent = "";
el.resolvedPrompt.hidden = true;
el.resolvedPrompt.textContent = "";
setStatus("");
let prompt = rawPrompt;
try {
if (translationAvailable && el.translateInput.checked) {
prompt = await translateInput(rawPrompt);
el.resolvedPrompt.textContent = prompt;
el.resolvedPrompt.hidden = false;
}
} catch (e) {
setStatus(e.message || String(e), "err");
el.generateBtn.disabled = false;
el.cancelBtn.disabled = true;
return;
}
const params = readParams();
abortCtrl = new AbortController();
setStatus("Generating…");
try {
const r = await fetch("/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
body: JSON.stringify({ prompt, ...params }),
signal: abortCtrl.signal,
});
if (!r.ok || !r.body) {
const t = await r.text().catch(() => "");
throw new Error(`Server error ${r.status}: ${t.slice(0, 200)}`);
}
const reader = r.body.getReader();
const dec = new TextDecoder();
let buf = "";
let acc = "";
el.output.textContent = prompt;
const FRAME_RE = /\r?\n\r?\n/;
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
let match;
while ((match = FRAME_RE.exec(buf))) {
const frame = buf.slice(0, match.index);
buf = buf.slice(match.index + match[0].length);
const lines = frame.split(/\r?\n/);
let event = "message";
let data = "";
for (const line of lines) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) data += line.slice(5).trim();
}
if (event === "delta" && data) {
try {
const parsed = JSON.parse(data);
acc += parsed.text || "";
el.output.textContent = prompt + acc;
} catch {}
} else if (event === "error") {
try { setStatus(JSON.parse(data).message || "error", "err"); } catch { setStatus("error", "err"); }
} else if (event === "done") {
/* end */
}
}
}
el.copyBtn.disabled = !acc;
setStatus(`Done · ${acc.length} chars`);
if (translationAvailable && el.translateOutput.checked && acc) {
try {
const gloss = await translateOutput(prompt + acc);
el.glossText.textContent = gloss;
el.gloss.hidden = false;
setStatus(`Done · ${acc.length} chars · glossed`);
} catch (e) {
setStatus(e.message || String(e), "err");
}
}
} catch (e) {
if (e.name === "AbortError") {
setStatus("Cancelled");
} else {
setStatus(e.message || String(e), "err");
}
} finally {
el.generateBtn.disabled = false;
el.cancelBtn.disabled = true;
abortCtrl = null;
}
}
el.generateBtn.addEventListener("click", runGenerate);
el.cancelBtn.addEventListener("click", () => abortCtrl?.abort());
el.copyBtn.addEventListener("click", async () => {
try { await navigator.clipboard.writeText(el.output.textContent); setStatus("Copied"); } catch {}
});
el.prompt.addEventListener("keydown", (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
e.preventDefault();
runGenerate();
}
});
loadConfig();
loadExamples();