import {
env,
pipeline,
TextStreamer,
} from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0";
const MODEL_ID = "onnx-community/gemma-3-270m-it-ONNX";
const MODEL_REVISION = "2dbbfdb1b59bd034eb959428c6a7da9dd7ea27f0";
const STORE_KEY = "spinor-gpt-sessions-v1";
const SETTINGS_KEY = "spinor-gpt-settings-v1";
const MAX_HISTORY_MESSAGES = 12;
const MAX_EVIDENCE_CHARS = 9000;
env.allowRemoteModels = true;
env.allowLocalModels = false;
env.useBrowserCache = true;
const $ = (selector) => document.querySelector(selector);
const elements = {
sidebar: $("#sidebar"),
history: $("#history"),
newChat: $("#new-chat"),
exportChat: $("#export-chat"),
clearData: $("#clear-data"),
mobileMenu: $("#mobile-menu"),
mode: $("#runtime-mode"),
modelTitle: $("#model-title"),
status: $("#runtime-status"),
modeFootnote: $("#mode-footnote"),
empty: $("#empty-state"),
messages: $("#messages"),
stage: $("#chat-stage"),
prompt: $("#prompt"),
send: $("#send"),
files: $("#knowledge-files"),
knowledgeStrip: $("#knowledge-strip"),
settingsButton: $("#settings-button"),
closeSettings: $("#close-settings"),
settingsPanel: $("#settings-panel"),
scrim: $("#scrim"),
system: $("#system-prompt"),
temperature: $("#temperature"),
temperatureValue: $("#temperature-value"),
maxTokens: $("#max-tokens"),
maxTokensValue: $("#max-tokens-value"),
nativeSettings: $("#native-settings"),
nativeEndpoint: $("#native-endpoint"),
evidenceLock: $("#evidence-lock"),
settingsRuntimeName: $("#settings-runtime-name"),
settingsRuntimeDetail: $("#settings-runtime-detail"),
loadModal: $("#load-modal"),
loadMessage: $("#load-message"),
progressFill: $("#progress-fill"),
progressDetail: $("#progress-detail"),
toast: $("#toast"),
};
const state = {
generator: null,
loadingPromise: null,
generating: false,
sessions: [],
currentId: null,
evidence: [],
fileProgress: new Map(),
};
function createId() {
return `${Date.now().toString(36)}-${crypto.getRandomValues(new Uint32Array(1))[0].toString(36)}`;
}
function defaultSession() {
return { id: createId(), title: "New conversation", messages: [], updated: Date.now() };
}
function currentSession() {
return state.sessions.find((session) => session.id === state.currentId);
}
function loadState() {
try {
state.sessions = JSON.parse(localStorage.getItem(STORE_KEY)) || [];
} catch {
state.sessions = [];
}
state.sessions = state.sessions.map((session) => ({
...session,
messages: (session.messages || []).filter(
(message) => !(message.role === "assistant" && /^Runtime error:/i.test(message.content || "")),
),
}));
if (!state.sessions.length) state.sessions = [defaultSession()];
state.currentId = state.sessions[0].id;
try {
const settings = JSON.parse(localStorage.getItem(SETTINGS_KEY)) || {};
elements.mode.value = settings.mode || "auto";
if (settings.system) elements.system.value = settings.system;
if (settings.temperature != null) elements.temperature.value = settings.temperature;
if (settings.maxTokens != null) elements.maxTokens.value = settings.maxTokens;
if (settings.endpoint) elements.nativeEndpoint.value = settings.endpoint;
elements.evidenceLock.checked = Boolean(settings.evidenceLock);
if (
location.protocol === "https:"
&& elements.mode.value === "native"
&& elements.nativeEndpoint.value.trim().startsWith("http://")
) {
elements.mode.value = "auto";
}
} catch {}
}
function persist() {
localStorage.setItem(STORE_KEY, JSON.stringify(state.sessions));
localStorage.setItem(
SETTINGS_KEY,
JSON.stringify({
mode: elements.mode.value,
system: elements.system.value,
temperature: Number(elements.temperature.value),
maxTokens: Number(elements.maxTokens.value),
endpoint: elements.nativeEndpoint.value.trim(),
evidenceLock: elements.evidenceLock.checked,
}),
);
}
function escapeHtml(value) {
return value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}
function runtimeErrorMessage(error) {
if (typeof error === "string" && error.trim()) return error;
if (error?.message) return String(error.message);
if (error?.name) return String(error.name);
return "The browser model could not start. Reload the page and try Browser WebGPU again.";
}
function renderMarkdown(value) {
const blocks = escapeHtml(value).split(/```/);
return blocks
.map((block, index) => {
if (index % 2) return `
${block.replace(/^\w+\n/, "")}
`;
return block
.split(/\n{2,}/)
.map((paragraph) => `${paragraph
.replace(/`([^`]+)`/g, "$1")
.replace(/\*\*([^*]+)\*\*/g, "$1")
.replaceAll("\n", "
")}
`)
.join("");
})
.join("");
}
function renderSessions() {
const ordered = [...state.sessions].sort((a, b) => b.updated - a.updated);
elements.history.replaceChildren(
...ordered.map((session) => {
const button = document.createElement("button");
button.className = `history-item${session.id === state.currentId ? " active" : ""}`;
button.textContent = session.title;
button.onclick = () => {
state.currentId = session.id;
state.evidence = [];
renderAll();
elements.sidebar.classList.remove("open");
};
return button;
}),
);
}
function makeMessage(message, streaming = false) {
const article = document.createElement("article");
article.className = `message ${message.role}`;
const avatar = document.createElement("div");
avatar.className = "avatar";
avatar.textContent = message.role === "assistant" ? "ψ" : "You";
const body = document.createElement("div");
const head = document.createElement("div");
head.className = "message-head";
head.textContent = message.role === "assistant" ? "Spinor GPT" : "You";
const content = document.createElement("div");
content.className = `message-content${streaming ? " cursor" : ""}`;
content.innerHTML = renderMarkdown(message.content || "");
body.append(head, content);
article.append(avatar, body);
return { article, content };
}
function renderMessages() {
const session = currentSession();
const hasMessages = Boolean(session?.messages.length);
elements.empty.classList.toggle("hidden", hasMessages);
elements.messages.replaceChildren();
if (hasMessages) {
for (const message of session.messages) elements.messages.append(makeMessage(message).article);
requestAnimationFrame(() => { elements.stage.scrollTop = elements.stage.scrollHeight; });
}
}
function renderEvidence() {
elements.knowledgeStrip.classList.toggle("hidden", !state.evidence.length);
elements.knowledgeStrip.replaceChildren(
...state.evidence.map((file) => {
const chip = document.createElement("span");
chip.className = "knowledge-chip";
chip.textContent = `${file.name} · ${Math.ceil(file.text.length / 1000)}k`;
return chip;
}),
);
}
function renderAll() {
renderSessions();
renderMessages();
renderEvidence();
updateModeUI();
}
function setStatus(label, kind = "") {
elements.status.className = `status-pill ${kind}`;
elements.status.innerHTML = `${escapeHtml(label)}`;
}
function toast(message) {
elements.toast.textContent = message;
elements.toast.classList.remove("hidden");
clearTimeout(toast.timeout);
toast.timeout = setTimeout(() => elements.toast.classList.add("hidden"), 2600);
}
function nativeEndpointBlocked() {
return location.protocol === "https:"
&& elements.nativeEndpoint.value.trim().startsWith("http://");
}
async function useBrowserFallback(messages, contentElement, reason) {
elements.mode.value = "browser";
updateModeUI();
toast(reason);
return browserGenerate(messages, contentElement);
}
function updateModeUI() {
if (elements.mode.value === "native" && nativeEndpointBlocked()) {
elements.mode.value = "browser";
toast("Native Spinor needs a local page or HTTPS endpoint. Using Browser WebGPU here.");
}
const native = elements.mode.value === "native";
const automatic = elements.mode.value === "auto";
elements.modelTitle.textContent = native ? "Native Spinor · MLX" : automatic ? "Auto relay" : "Browser WebGPU";
elements.modeFootnote.textContent = native
? "Connected only to your configured endpoint"
: automatic ? "Native when reachable · browser fallback when offline" : "Browser model runs on your device";
elements.nativeSettings.classList.toggle("hidden", !native && !automatic);
elements.settingsRuntimeName.textContent = native ? "Spinor GPT 7B · MLX q4" : automatic ? "Native + browser failover" : "Gemma 3 270M · FP16";
elements.settingsRuntimeDetail.textContent = native
? "Apple MLX · quaternion query rotation"
: automatic ? "HTTPS relay · FP16 WebGPU fallback" : `${navigator.gpu ? "WebGPU" : "WebGPU unavailable"} · Transformers.js`;
setStatus(native ? "Endpoint idle" : automatic ? "Failover armed" : state.generator ? "Model loaded" : "Ready to load", state.generator && !native ? "ready" : "");
persist();
}
function updateLoadingProgress(info) {
if (info.status === "progress") {
state.fileProgress.set(info.file, Number(info.progress) || 0);
const values = [...state.fileProgress.values()];
const average = values.reduce((sum, value) => sum + value, 0) / values.length;
elements.progressFill.style.width = `${Math.max(3, average)}%`;
elements.loadMessage.textContent = `Downloading ${info.file.split("/").pop()}…`;
if (info.loaded && info.total) {
elements.progressDetail.textContent = `${(info.loaded / 1048576).toFixed(1)} of ${(info.total / 1048576).toFixed(1)} MB · cached after first use`;
}
} else if (info.status === "ready") {
elements.progressFill.style.width = "100%";
elements.loadMessage.textContent = "Model ready";
}
}
async function ensureBrowserModel() {
if (state.generator) return state.generator;
if (state.loadingPromise) return state.loadingPromise;
if (!navigator.gpu) {
throw new Error("Browser inference requires WebGPU. Open this Space in a current Chrome, Edge, or Safari browser, or connect a native HTTPS endpoint.");
}
elements.loadModal.classList.remove("hidden");
elements.scrim.classList.remove("hidden");
state.fileProgress.clear();
setStatus("Loading model");
state.loadingPromise = pipeline("text-generation", MODEL_ID, {
device: "webgpu",
dtype: "fp16",
revision: MODEL_REVISION,
progress_callback: updateLoadingProgress,
})
.then((generator) => {
state.generator = generator;
setStatus("Model loaded", "ready");
return generator;
})
.finally(() => {
state.loadingPromise = null;
setTimeout(() => {
elements.loadModal.classList.add("hidden");
if (!elements.settingsPanel.classList.contains("open")) elements.scrim.classList.add("hidden");
}, 350);
});
return state.loadingPromise;
}
function evidenceSystemPrompt() {
if (!state.evidence.length) return "";
const combined = state.evidence
.map((file, index) => `[S${index + 1}] ${file.name}\n${file.text}`)
.join("\n\n")
.slice(0, MAX_EVIDENCE_CHARS);
const rule = elements.evidenceLock.checked
? "Use only this evidence for factual claims. Cite [S1], [S2], etc. If it does not answer the request, say: Insufficient evidence."
: "Use the attached material when relevant and cite it as [S1], [S2], etc.";
return `\n\n${rule}\n\nAttached evidence:\n${combined}`;
}
async function browserGenerate(messages, contentElement) {
const generator = await ensureBrowserModel();
let generated = "";
const streamer = new TextStreamer(generator.tokenizer, {
skip_prompt: true,
skip_special_tokens: true,
callback_function: (token) => {
generated += token;
contentElement.innerHTML = renderMarkdown(generated);
elements.stage.scrollTop = elements.stage.scrollHeight;
},
});
const temperature = Number(elements.temperature.value);
const result = await generator(messages, {
max_new_tokens: Number(elements.maxTokens.value),
do_sample: temperature > 0,
temperature: Math.max(temperature, 0.01),
top_p: 0.9,
repetition_penalty: 1.08,
streamer,
});
if (!generated) {
const output = result?.[0]?.generated_text;
if (Array.isArray(output)) generated = output.at(-1)?.content || "";
else generated = String(output || "");
}
return generated.trim();
}
async function nativeGenerate(messages, contentElement) {
const endpoint = elements.nativeEndpoint.value.trim().replace(/\/$/, "");
if (nativeEndpointBlocked()) {
return useBrowserFallback(
messages,
contentElement,
"Native endpoint is unavailable from this hosted page — using Browser WebGPU.",
);
}
setStatus("Generating");
let response;
try {
response = await fetch(`${endpoint}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "spinor-gpt:7b",
messages,
stream: false,
temperature: Number(elements.temperature.value),
num_predict: Number(elements.maxTokens.value),
}),
});
} catch (error) {
console.warn("Native Spinor endpoint is offline", error);
return useBrowserFallback(
messages,
contentElement,
"Native Spinor is offline — using Browser WebGPU.",
);
}
if (!response.ok) throw new Error(`Native endpoint returned HTTP ${response.status}`);
const payload = await response.json();
if (payload.error) throw new Error(payload.error);
setStatus(`${Number(payload.tokens_per_second || 0).toFixed(1)} tok/s`, "ready");
return String(payload.message?.content || payload.response || "").trim();
}
async function nativeReachable() {
const endpoint = elements.nativeEndpoint.value.trim().replace(/\/$/, "");
if (!endpoint) return false;
if (location.protocol === "https:" && endpoint.startsWith("http://")) return false;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 1600);
try {
const response = await fetch(`${endpoint}/health`, { signal: controller.signal });
return response.ok;
} catch {
return false;
} finally {
clearTimeout(timeout);
}
}
async function autoGenerate(messages, contentElement) {
setStatus("Checking native relay");
if (await nativeReachable()) {
setStatus("Native relay online", "ready");
return nativeGenerate(messages, contentElement);
}
toast("Native relay is offline — using the private browser model.");
setStatus("Browser fallback");
return browserGenerate(messages, contentElement);
}
async function sendMessage() {
const prompt = elements.prompt.value.trim();
if (!prompt || state.generating) return;
const session = currentSession();
if (!session.messages.length) session.title = prompt.slice(0, 38) + (prompt.length > 38 ? "…" : "");
session.messages.push({ role: "user", content: prompt });
session.updated = Date.now();
elements.prompt.value = "";
autoResize();
state.generating = true;
elements.send.disabled = true;
renderAll();
const assistant = { role: "assistant", content: "" };
const rendered = makeMessage(assistant, true);
elements.messages.append(rendered.article);
elements.stage.scrollTop = elements.stage.scrollHeight;
const system = elements.system.value.trim() + evidenceSystemPrompt();
const usableHistory = session.messages
.filter((message) => !(message.role === "assistant" && /^Runtime error:/i.test(message.content || "")))
.slice(-MAX_HISTORY_MESSAGES);
const conversation = [
{ role: "system", content: system },
...usableHistory,
];
try {
const answer = elements.mode.value === "browser"
? await browserGenerate(conversation, rendered.content)
: elements.mode.value === "native"
? await nativeGenerate(conversation, rendered.content)
: await autoGenerate(conversation, rendered.content);
assistant.content = answer || "I could not produce a response.";
session.messages.push(assistant);
rendered.content.innerHTML = renderMarkdown(assistant.content);
rendered.content.classList.remove("cursor");
} catch (error) {
const message = runtimeErrorMessage(error);
console.error("Spinor GPT runtime failure", error);
assistant.content = `Runtime error: ${message}`;
session.messages.push(assistant);
rendered.content.innerHTML = renderMarkdown(assistant.content);
rendered.content.classList.remove("cursor");
setStatus("Runtime error", "error");
toast(message);
} finally {
state.generating = false;
elements.send.disabled = false;
session.updated = Date.now();
persist();
renderSessions();
elements.stage.scrollTop = elements.stage.scrollHeight;
}
}
function autoResize() {
elements.prompt.style.height = "auto";
elements.prompt.style.height = `${Math.min(elements.prompt.scrollHeight, 160)}px`;
}
function newConversation() {
const session = defaultSession();
state.sessions.unshift(session);
state.currentId = session.id;
state.evidence = [];
persist();
renderAll();
elements.prompt.focus();
}
function openSettings(open) {
elements.settingsPanel.classList.toggle("open", open);
elements.scrim.classList.toggle("hidden", !open);
}
async function attachFiles(event) {
const files = [...event.target.files];
const loaded = [];
for (const file of files) {
if (file.size > 1_000_000) {
toast(`${file.name} is larger than 1 MB and was skipped.`);
continue;
}
loaded.push({ name: file.name, text: (await file.text()).slice(0, MAX_EVIDENCE_CHARS) });
}
state.evidence = [...state.evidence, ...loaded].slice(-5);
renderEvidence();
event.target.value = "";
}
function exportConversation() {
const session = currentSession();
const text = session.messages.map((message) => `## ${message.role === "user" ? "You" : "Spinor GPT"}\n\n${message.content}`).join("\n\n");
const blob = new Blob([`# ${session.title}\n\n${text}\n`], { type: "text/markdown" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = `${session.title.replace(/[^a-z0-9]+/gi, "-").toLowerCase() || "spinor-chat"}.md`;
link.click();
URL.revokeObjectURL(link.href);
}
elements.newChat.onclick = newConversation;
elements.send.onclick = sendMessage;
elements.prompt.addEventListener("input", autoResize);
elements.prompt.addEventListener("keydown", (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
});
elements.files.onchange = attachFiles;
elements.mode.onchange = updateModeUI;
elements.settingsButton.onclick = () => openSettings(true);
elements.closeSettings.onclick = () => openSettings(false);
elements.scrim.onclick = () => openSettings(false);
elements.mobileMenu.onclick = () => elements.sidebar.classList.toggle("open");
elements.exportChat.onclick = exportConversation;
elements.clearData.onclick = () => {
if (!confirm("Delete every locally saved conversation and setting?")) return;
localStorage.removeItem(STORE_KEY);
localStorage.removeItem(SETTINGS_KEY);
state.sessions = [defaultSession()];
state.currentId = state.sessions[0].id;
state.evidence = [];
renderAll();
};
elements.temperature.oninput = () => { elements.temperatureValue.value = elements.temperature.value; persist(); };
elements.maxTokens.oninput = () => { elements.maxTokensValue.value = elements.maxTokens.value; persist(); };
elements.system.onchange = persist;
elements.nativeEndpoint.onchange = updateModeUI;
elements.evidenceLock.onchange = persist;
document.querySelectorAll(".suggestion").forEach((button) => {
button.onclick = () => {
elements.prompt.value = button.dataset.prompt;
autoResize();
elements.prompt.focus();
};
});
window.addEventListener("pagehide", () => {
if (state.generator) state.generator.dispose();
});
loadState();
elements.temperatureValue.value = elements.temperature.value;
elements.maxTokensValue.value = elements.maxTokens.value;
renderAll();
elements.prompt.focus();