Spaces:
Runtime error
Runtime error
| // 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(); | |