import "./styles.css"; import { DEFAULT_PROMPT, DEFAULT_SETTINGS, DEFAULT_SYSTEM_PROMPT, MODEL_ID, PRESETS, type GenerationSettings, type Precision, } from "./model"; import type { WorkerRequest, WorkerResponse } from "./messages"; const app = document.querySelector("#app"); if (!app) { throw new Error("Missing #app root"); } let jobId = 0; let isGenerating = false; let generatedText = ""; const diagnostics: string[] = []; const worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module" }); app.innerHTML = `

Transformers.js / WebGPU

LFM2.5 230M

Checking WebGPU
${PRESETS.map( (preset) => ``, ).join("")}

Local output

Response

Idle

          
          

Diagnostics

Debug log


          
`; const runtimeStatus = element("#runtime-status"); const runtimeLabel = element("#runtime-label"); const form = element("#generation-form"); const systemPrompt = element("#system-prompt"); const promptInput = element("#prompt"); const precision = element("#precision"); const maxNewTokens = element("#max-new-tokens"); const temperature = element("#temperature"); const topK = element("#top-k"); const repetitionPenalty = element("#repetition-penalty"); const generateButton = element("#generate"); const stopButton = element("#stop"); const clearButton = element("#clear"); const output = element("#output"); const metric = element("#metric"); const progressBar = element("#progress-bar"); const tokenRail = element("#token-rail"); const diagnosticsOutput = element("#diagnostics"); const copyDiagnosticsButton = element("#copy-diagnostics"); systemPrompt.value = DEFAULT_SYSTEM_PROMPT; promptInput.value = DEFAULT_PROMPT; precision.value = DEFAULT_SETTINGS.precision; maxNewTokens.value = String(DEFAULT_SETTINGS.maxNewTokens); temperature.value = String(DEFAULT_SETTINGS.temperature); topK.value = String(DEFAULT_SETTINGS.topK); repetitionPenalty.value = String(DEFAULT_SETTINGS.repetitionPenalty); updateWebGpuStatus(); appendDiagnostic("App initialized", clientEnvironment()); setBusy(false); form.addEventListener("submit", (event) => { event.preventDefault(); startGeneration(); }); stopButton.addEventListener("click", () => { jobId += 1; const request: WorkerRequest = { type: "reset", jobId }; worker.postMessage(request); setBusy(false); metric.textContent = "Stopped"; }); clearButton.addEventListener("click", () => { generatedText = ""; output.textContent = ""; tokenRail.innerHTML = ""; diagnostics.length = 0; diagnosticsOutput.textContent = ""; metric.textContent = "Idle"; progressBar.style.width = "0%"; appendDiagnostic("Cleared output", clientEnvironment()); }); copyDiagnosticsButton.addEventListener("click", async () => { const textToCopy = diagnostics.join("\n"); await navigator.clipboard.writeText(textToCopy); copyDiagnosticsButton.textContent = "Copied"; window.setTimeout(() => { copyDiagnosticsButton.textContent = "Copy log"; }, 1400); }); for (const button of document.querySelectorAll(".preset")) { button.addEventListener("click", () => { const preset = PRESETS.find((item) => item.id === button.dataset.preset); if (preset) promptInput.value = preset.prompt; }); } worker.addEventListener("message", (event: MessageEvent) => { const message = event.data; if (message.jobId !== jobId) return; switch (message.type) { case "status": metric.textContent = message.message; break; case "progress": progressBar.style.width = `${message.progress}%`; metric.textContent = message.progress > 0 ? `${message.status}: ${message.progress.toFixed(0)}%` : `${message.status}: ${message.file}`; appendDiagnostic("Worker progress", message); break; case "debug": appendDiagnostic(message.message, message.data); break; case "token": generatedText += message.text; output.textContent = generatedText; appendTokenMark(message.text); output.scrollTop = output.scrollHeight; break; case "complete": generatedText = message.text || generatedText; output.textContent = generatedText; metric.textContent = `${Math.max(0.1, message.elapsedMs / 1000).toFixed(1)}s`; progressBar.style.width = "100%"; appendDiagnostic("Worker complete", { elapsedMs: message.elapsedMs, textChars: message.text.length }); setBusy(false); break; case "error": metric.textContent = "Error"; output.textContent = message.message; appendDiagnostic("Worker error", message.data ?? message.message); runtimeStatus.classList.add("runtime-error"); setBusy(false); break; } }); function element(selector: string) { const found = document.querySelector(selector); if (!found) throw new Error(`Missing element: ${selector}`); return found; } function readSettings(): GenerationSettings { return { precision: precision.value as Precision, maxNewTokens: readNumber(maxNewTokens, DEFAULT_SETTINGS.maxNewTokens), temperature: readNumber(temperature, DEFAULT_SETTINGS.temperature), topK: readNumber(topK, DEFAULT_SETTINGS.topK), repetitionPenalty: readNumber(repetitionPenalty, DEFAULT_SETTINGS.repetitionPenalty), }; } function readNumber(input: HTMLInputElement, fallback: number) { const value = Number(input.value); return Number.isFinite(value) ? value : fallback; } function startGeneration() { if (isGenerating) return; generatedText = ""; output.textContent = ""; tokenRail.innerHTML = ""; progressBar.style.width = "0%"; metric.textContent = "Queueing"; setBusy(true); jobId += 1; const request: WorkerRequest = { type: "generate", jobId, systemPrompt: systemPrompt.value.trim() || DEFAULT_SYSTEM_PROMPT, prompt: promptInput.value.trim(), settings: readSettings(), }; appendDiagnostic("Main generate request", { jobId, settings: request.settings, systemPromptChars: request.systemPrompt.length, promptChars: request.prompt.length, environment: clientEnvironment(), }); worker.postMessage(request); } function setBusy(nextBusy: boolean) { isGenerating = nextBusy; generateButton.disabled = nextBusy; stopButton.disabled = !nextBusy; generateButton.textContent = nextBusy ? "Generating" : "Generate"; } function appendTokenMark(text: string) { const mark = document.createElement("span"); mark.className = "token-mark"; mark.style.setProperty("--token-size", `${Math.min(42, Math.max(8, text.length * 3))}px`); tokenRail.append(mark); while (tokenRail.childElementCount > 56) { tokenRail.firstElementChild?.remove(); } } function updateWebGpuStatus() { const hasWebGpu = "gpu" in navigator; runtimeStatus.classList.toggle("runtime-ready", hasWebGpu); runtimeStatus.classList.toggle("runtime-error", !hasWebGpu); runtimeLabel.textContent = hasWebGpu ? `WebGPU ready / ${MODEL_ID}` : "WebGPU unavailable"; } worker.addEventListener("error", (event) => { appendDiagnostic("Worker script error event", { message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno, }); }); window.addEventListener("error", (event) => { appendDiagnostic("Window error", { message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno, error: event.error instanceof Error ? event.error.stack : String(event.error), }); }); window.addEventListener("unhandledrejection", (event) => { appendDiagnostic("Window unhandled rejection", { reason: event.reason instanceof Error ? event.reason.stack : String(event.reason), }); }); function clientEnvironment() { return { href: window.location.href, userAgent: navigator.userAgent, hasWebGpu: "gpu" in navigator, crossOriginIsolated: window.crossOriginIsolated, hardwareConcurrency: navigator.hardwareConcurrency, deviceMemory: "deviceMemory" in navigator ? (navigator as Navigator & { deviceMemory?: number }).deviceMemory : undefined, }; } function appendDiagnostic(message: string, data?: unknown) { const stamp = new Date().toISOString(); const suffix = data === undefined ? "" : `\n${safeJson(data)}`; diagnostics.push(`[${stamp}] ${message}${suffix}`); while (diagnostics.length > 220) diagnostics.shift(); diagnosticsOutput.textContent = diagnostics.join("\n\n"); diagnosticsOutput.scrollTop = diagnosticsOutput.scrollHeight; } function safeJson(value: unknown) { try { return JSON.stringify(value, null, 2); } catch (error) { return String(error); } }