feat(studio): AI assistant that runs on the user's own device

An Assistant drawer in Sirius Studio with two providers and no key, no
server and no spend. "This browser" runs an open model on the user's
GPU through WebGPU with WebLLM (vendored, Apache-2.0); weights download
once from the MLC mirror and stay in the browser cache. "Local
endpoint" talks to an OpenAI-compatible runtime on the user's machine
(Ollama, LM Studio), which unlocks larger models on a real GPU. Nothing
the user writes or builds leaves their device in either mode.

Three verbs: make a section, rewrite the selected text, restyle the
selection. The model returns HTML and CSS as data; Studio sanitises it
(no scripts, frames, handlers or imports) and inserts it through the
editor, with Undo. Model output is never executed.

The quantisation is chosen per GPU: q4f16 when the adapter exposes
shader-f16, q4f32 otherwise (Pascal-era cards lack it). Small models
often answer with bare HTML instead of JSON, so the parser accepts
both, prompts avoid literal placeholders one model echoed back, and an
out-of-memory or disposed runtime is reported as "pick a smaller
model" with the engine reset. Switching models starts a fresh worker.
Verified on an NVIDIA Pascal card: SmolLM2 360M rewrites text, Qwen2.5
Coder 0.5B builds a section; the 1.5B f32 build exceeded that card's
memory and now fails gracefully.
This commit is contained in:
Local Dev 2026-09-20 15:22:34 +02:00
parent 9589537492
commit 7655720e11
18 changed files with 709 additions and 17 deletions

View file

@ -92,7 +92,7 @@
<script type="importmap"> <script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } } { "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } }
</script> </script>
<script src="../js/i18n.js?v=20260920b"></script> <script src="../js/i18n.js?v=20260920c"></script>
</head> </head>
<body> <body>

View file

@ -591,5 +591,22 @@
"Sell a name from your dashboard →": "Einen Namen im Dashboard verkaufen →", "Sell a name from your dashboard →": "Einen Namen im Dashboard verkaufen →",
"Register a new name": "Neuen Namen registrieren", "Register a new name": "Neuen Namen registrieren",
"↓ Import live site": "↓ Live-Seite importieren", "↓ Import live site": "↓ Live-Seite importieren",
"Import the live site": "Live-Seite importieren" "Import the live site": "Live-Seite importieren",
"✨ Assistant": "✨ Assistent",
"Runs on your own device. Nothing you write or build is sent to Sirius.X or to any AI vendor.": "Läuft auf deinem eigenen Gerät. Nichts, was du schreibst oder baust, wird an Sirius.X oder einen KI-Anbieter gesendet.",
"Runs on": "Läuft auf",
"This browser — your GPU, private": "Dieser Browser — deine GPU, privat",
"Local endpoint — Ollama, LM Studio": "Lokaler Endpunkt — Ollama, LM Studio",
"Model": "Modell",
"Downloads once, then stays in the browser cache. Needs WebGPU (current Chrome, Edge, Firefox, Safari).": "Wird einmal geladen und bleibt im Browser-Cache. Braucht WebGPU (aktuelles Chrome, Edge, Firefox, Safari).",
"Load model": "Modell laden",
"Endpoint": "Endpunkt",
"Connect": "Verbinden",
"What do you want?": "Was möchtest du?",
"Make a section": "Abschnitt erstellen",
"Rewrite selected text": "Markierten Text umschreiben",
"Restyle selection": "Auswahl umgestalten",
"Nothing selected": "Nichts ausgewählt",
"Undo last change": "Letzte Änderung rückgängig",
"Stop": "Stopp"
} }

View file

@ -591,5 +591,22 @@
"Sell a name from your dashboard →": "Πούλησε ένα όνομα από τον πίνακα ελέγχου →", "Sell a name from your dashboard →": "Πούλησε ένα όνομα από τον πίνακα ελέγχου →",
"Register a new name": "Καταχώρηση νέου ονόματος", "Register a new name": "Καταχώρηση νέου ονόματος",
"↓ Import live site": "↓ Εισαγωγή ζωντανής σελίδας", "↓ Import live site": "↓ Εισαγωγή ζωντανής σελίδας",
"Import the live site": "Εισαγωγή ζωντανής σελίδας" "Import the live site": "Εισαγωγή ζωντανής σελίδας",
"✨ Assistant": "✨ Βοηθός",
"Runs on your own device. Nothing you write or build is sent to Sirius.X or to any AI vendor.": "Τρέχει στη δική σου συσκευή. Τίποτα από όσα γράφεις ή φτιάχνεις δεν στέλνεται στο Sirius.X ή σε πάροχο AI.",
"Runs on": "Τρέχει σε",
"This browser — your GPU, private": "Αυτός ο browser — η GPU σου, ιδιωτικά",
"Local endpoint — Ollama, LM Studio": "Τοπικό endpoint — Ollama, LM Studio",
"Model": "Μοντέλο",
"Downloads once, then stays in the browser cache. Needs WebGPU (current Chrome, Edge, Firefox, Safari).": "Κατεβαίνει μία φορά και μένει στην cache του browser. Χρειάζεται WebGPU (σύγχρονο Chrome, Edge, Firefox, Safari).",
"Load model": "Φόρτωση μοντέλου",
"Endpoint": "Endpoint",
"Connect": "Σύνδεση",
"What do you want?": "Τι θέλεις;",
"Make a section": "Φτιάξε ενότητα",
"Rewrite selected text": "Ξαναγράψε το επιλεγμένο κείμενο",
"Restyle selection": "Άλλαξε στυλ στην επιλογή",
"Nothing selected": "Τίποτα επιλεγμένο",
"Undo last change": "Αναίρεση τελευταίας αλλαγής",
"Stop": "Στοπ"
} }

View file

@ -591,5 +591,22 @@
"Sell a name from your dashboard →": "Vender un nombre desde tu panel →", "Sell a name from your dashboard →": "Vender un nombre desde tu panel →",
"Register a new name": "Registrar un nombre nuevo", "Register a new name": "Registrar un nombre nuevo",
"↓ Import live site": "↓ Importar el sitio en vivo", "↓ Import live site": "↓ Importar el sitio en vivo",
"Import the live site": "Importar el sitio en vivo" "Import the live site": "Importar el sitio en vivo",
"✨ Assistant": "✨ Asistente",
"Runs on your own device. Nothing you write or build is sent to Sirius.X or to any AI vendor.": "Funciona en tu propio dispositivo. Nada de lo que escribes o construyes se envía a Sirius.X ni a ningún proveedor de IA.",
"Runs on": "Se ejecuta en",
"This browser — your GPU, private": "Este navegador — tu GPU, privado",
"Local endpoint — Ollama, LM Studio": "Servidor local — Ollama, LM Studio",
"Model": "Modelo",
"Downloads once, then stays in the browser cache. Needs WebGPU (current Chrome, Edge, Firefox, Safari).": "Se descarga una vez y queda en la caché del navegador. Requiere WebGPU (Chrome, Edge, Firefox, Safari actuales).",
"Load model": "Cargar modelo",
"Endpoint": "Endpoint",
"Connect": "Conectar",
"What do you want?": "¿Qué quieres?",
"Make a section": "Crear una sección",
"Rewrite selected text": "Reescribir el texto seleccionado",
"Restyle selection": "Cambiar estilo de la selección",
"Nothing selected": "Nada seleccionado",
"Undo last change": "Deshacer el último cambio",
"Stop": "Detener"
} }

View file

@ -591,5 +591,22 @@
"Sell a name from your dashboard →": "Vendre un nom depuis le tableau de bord →", "Sell a name from your dashboard →": "Vendre un nom depuis le tableau de bord →",
"Register a new name": "Enregistrer un nouveau nom", "Register a new name": "Enregistrer un nouveau nom",
"↓ Import live site": "↓ Importer le site en ligne", "↓ Import live site": "↓ Importer le site en ligne",
"Import the live site": "Importer le site en ligne" "Import the live site": "Importer le site en ligne",
"✨ Assistant": "✨ Assistant",
"Runs on your own device. Nothing you write or build is sent to Sirius.X or to any AI vendor.": "Fonctionne sur votre propre appareil. Rien de ce que vous écrivez ou construisez n'est envoyé à Sirius.X ni à un fournisseur d'IA.",
"Runs on": "S'exécute sur",
"This browser — your GPU, private": "Ce navigateur — votre GPU, privé",
"Local endpoint — Ollama, LM Studio": "Point local — Ollama, LM Studio",
"Model": "Modèle",
"Downloads once, then stays in the browser cache. Needs WebGPU (current Chrome, Edge, Firefox, Safari).": "Téléchargé une fois, puis conservé dans le cache du navigateur. Nécessite WebGPU (Chrome, Edge, Firefox, Safari récents).",
"Load model": "Charger le modèle",
"Endpoint": "Point d'accès",
"Connect": "Connecter",
"What do you want?": "Que voulez-vous ?",
"Make a section": "Créer une section",
"Rewrite selected text": "Réécrire le texte sélectionné",
"Restyle selection": "Restyler la sélection",
"Nothing selected": "Rien de sélectionné",
"Undo last change": "Annuler la dernière modification",
"Stop": "Arrêter"
} }

View file

@ -591,5 +591,22 @@
"Sell a name from your dashboard →": "Vender um nome a partir do painel →", "Sell a name from your dashboard →": "Vender um nome a partir do painel →",
"Register a new name": "Registar um nome novo", "Register a new name": "Registar um nome novo",
"↓ Import live site": "↓ Importar o site em produção", "↓ Import live site": "↓ Importar o site em produção",
"Import the live site": "Importar o site em produção" "Import the live site": "Importar o site em produção",
"✨ Assistant": "✨ Assistente",
"Runs on your own device. Nothing you write or build is sent to Sirius.X or to any AI vendor.": "Corre no seu próprio dispositivo. Nada do que escreve ou constrói é enviado para o Sirius.X ou para qualquer fornecedor de IA.",
"Runs on": "Corre em",
"This browser — your GPU, private": "Este navegador — a sua GPU, privado",
"Local endpoint — Ollama, LM Studio": "Servidor local — Ollama, LM Studio",
"Model": "Modelo",
"Downloads once, then stays in the browser cache. Needs WebGPU (current Chrome, Edge, Firefox, Safari).": "Descarrega uma vez e fica na cache do navegador. Precisa de WebGPU (Chrome, Edge, Firefox, Safari atuais).",
"Load model": "Carregar modelo",
"Endpoint": "Endpoint",
"Connect": "Ligar",
"What do you want?": "O que pretende?",
"Make a section": "Criar uma secção",
"Rewrite selected text": "Reescrever o texto selecionado",
"Restyle selection": "Restilizar a seleção",
"Nothing selected": "Nada selecionado",
"Undo last change": "Anular a última alteração",
"Stop": "Parar"
} }

View file

@ -591,5 +591,22 @@
"Sell a name from your dashboard →": "Продать имя из панели →", "Sell a name from your dashboard →": "Продать имя из панели →",
"Register a new name": "Зарегистрировать новое имя", "Register a new name": "Зарегистрировать новое имя",
"↓ Import live site": "↓ Импортировать текущий сайт", "↓ Import live site": "↓ Импортировать текущий сайт",
"Import the live site": "Импортировать текущий сайт" "Import the live site": "Импортировать текущий сайт",
"✨ Assistant": "✨ Ассистент",
"Runs on your own device. Nothing you write or build is sent to Sirius.X or to any AI vendor.": "Работает на вашем устройстве. Ничего из того, что вы пишете или строите, не отправляется в Sirius.X или поставщику ИИ.",
"Runs on": "Работает на",
"This browser — your GPU, private": "Этот браузер — ваша GPU, приватно",
"Local endpoint — Ollama, LM Studio": "Локальный сервер — Ollama, LM Studio",
"Model": "Модель",
"Downloads once, then stays in the browser cache. Needs WebGPU (current Chrome, Edge, Firefox, Safari).": "Скачивается один раз и остаётся в кэше браузера. Нужен WebGPU (актуальные Chrome, Edge, Firefox, Safari).",
"Load model": "Загрузить модель",
"Endpoint": "Адрес сервера",
"Connect": "Подключить",
"What do you want?": "Что нужно сделать?",
"Make a section": "Создать секцию",
"Rewrite selected text": "Переписать выбранный текст",
"Restyle selection": "Изменить стиль выбранного",
"Nothing selected": "Ничего не выбрано",
"Undo last change": "Отменить последнее изменение",
"Stop": "Стоп"
} }

View file

@ -130,7 +130,7 @@
.verify dd{margin:2px 0 0;font-family:ui-monospace,monospace;font-size:12.5px;word-break:break-all;color:var(--ink)} .verify dd{margin:2px 0 0;font-family:ui-monospace,monospace;font-size:12.5px;word-break:break-all;color:var(--ink)}
footer{border-top:1px solid var(--line);padding:2.4rem 0 3.5rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem} footer{border-top:1px solid var(--line);padding:2.4rem 0 3.5rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem}
</style> </style>
<script src="./js/i18n.js?v=20260920b"></script> <script src="./js/i18n.js?v=20260920c"></script>
</head> </head>
<body> <body>

7
js/ai-worker.js Normal file
View file

@ -0,0 +1,7 @@
// Web Worker for the in-browser assistant: runs the WebLLM engine off the UI
// thread. Weights and the model library are fetched by WebLLM itself and
// cached by the browser; nothing is sent anywhere.
import { WebWorkerMLCEngineHandler } from "../vendor/web-llm/index.js?v=0.2.85";
const handler = new WebWorkerMLCEngineHandler();
self.onmessage = (msg) => handler.onmessage(msg);

View file

@ -22,7 +22,7 @@
(function () { (function () {
var STORE = "sirius-lang"; var STORE = "sirius-lang";
var CACHE = "sirius-i18n:"; var CACHE = "sirius-i18n:";
var VERSION = "20260920b"; // bump when dictionaries change var VERSION = "20260920c"; // bump when dictionaries change
var LANGS = { var LANGS = {
en: "English", en: "English",
de: "Deutsch", de: "Deutsch",

223
js/studio-ai.js Normal file
View file

@ -0,0 +1,223 @@
// Sirius Studio — Assistant panel.
//
// Two providers, both on the user's own hardware, no key and no spend:
// webllm — the model runs inside this tab on the GPU through WebGPU
// (vendor/web-llm, Apache-2.0). Weights download once from the
// MLC mirror and stay in the browser cache.
// local — any OpenAI-compatible endpoint on the user's machine (Ollama,
// LM Studio, llama.cpp). The page talks to localhost directly; the
// runtime has to allow this origin (OLLAMA_ORIGINS=https://silentmode.st).
//
// The model returns data (HTML + CSS or text as JSON); Studio inserts it
// through the editor API. Model output is never executed as code.
const LS_KEY = "siriusAi";
// Base ids; the quantisation suffix is chosen at load time: q4f16 when the
// GPU exposes the shader-f16 feature (half the memory), q4f32 otherwise.
const WEBLLM_MODELS = [
{ id: "Qwen2.5-Coder-1.5B-Instruct", label: "Qwen2.5 Coder 1.5B · best HTML/CSS (≈1 GB download)" },
{ id: "Qwen2.5-1.5B-Instruct", label: "Qwen2.5 1.5B · balanced copy + layout (≈1 GB)" },
{ id: "Qwen2.5-Coder-0.5B-Instruct", label: "Qwen2.5 Coder 0.5B · light, still builds sections (≈400 MB, 1 GB GPU)" },
{ id: "SmolLM2-360M-Instruct", label: "SmolLM2 360M · weakest machines; rewrites text, rarely builds sections (≈250 MB)" },
{ id: "Qwen2.5-Coder-3B-Instruct", label: "Qwen2.5 Coder 3B · strongest here (≈2 GB, needs 4 GB GPU)" },
{ id: "Llama-3.2-3B-Instruct", label: "Llama 3.2 3B · better prose (≈2 GB, needs 4 GB GPU)" },
];
let gpuF16 = null;
async function gpuSupportsF16() {
if (gpuF16 != null) return gpuF16;
try { const a = await navigator.gpu.requestAdapter(); gpuF16 = !!a?.features?.has("shader-f16"); } catch { gpuF16 = false; }
return gpuF16;
}
const fullModelId = async (base) => `${base}-q4${(await gpuSupportsF16()) ? "f16" : "f32"}_1-MLC`;
export function initAssistant(editor, { esc, name }) {
const $ = (id) => document.getElementById(id);
const panel = $("ai"); if (!panel) return;
const settings = Object.assign({ provider: "webllm", model: WEBLLM_MODELS[0].id, url: "http://localhost:11434/v1", lmodel: "" }, load());
let engine = null, engineModel = null, worker = null, webllm = null, busy = false, abort = null, lastApplied = false;
// ---------- ui ----------
$("ai-model").innerHTML = WEBLLM_MODELS.map((m) => `<option value="${m.id}">${esc(m.label)}</option>`).join("");
$("ai-model").value = settings.model;
if (!$("ai-model").value) { $("ai-model").value = WEBLLM_MODELS[0].id; settings.model = WEBLLM_MODELS[0].id; }
$("ai-provider").value = settings.provider;
$("ai-url").value = settings.url;
$("ai-lmodel").value = settings.lmodel;
const showProvider = () => { const w = $("ai-provider").value === "webllm"; $("ai-webllm").hidden = !w; $("ai-local").hidden = w; };
showProvider();
const st = (text, cls = "") => { const el = $("ai-status"); el.textContent = text; el.className = "ai-status " + cls; };
const out = (text) => { const el = $("ai-out"); el.textContent = text; el.scrollTop = 1e6; };
const persist = () => { settings.provider = $("ai-provider").value; settings.model = $("ai-model").value; settings.url = $("ai-url").value.trim().replace(/\/+$/, ""); settings.lmodel = $("ai-lmodel").value.trim(); save(settings); };
$("btn-ai").addEventListener("click", () => { panel.hidden = !panel.hidden; $("btn-ai").classList.toggle("on", !panel.hidden); setTimeout(() => editor.refresh(), 60); if (!panel.hidden) paintTarget(); });
$("ai-close").addEventListener("click", () => { panel.hidden = true; $("btn-ai").classList.remove("on"); setTimeout(() => editor.refresh(), 60); });
$("ai-provider").addEventListener("change", () => { showProvider(); persist(); st(readyText(), ""); });
$("ai-model").addEventListener("change", persist);
$("ai-url").addEventListener("change", persist);
$("ai-lmodel").addEventListener("change", persist);
$("ai-load").addEventListener("click", () => ensureWebLLM().catch((e) => st(e.message || String(e), "err")));
$("ai-connect").addEventListener("click", () => probeLocal().catch((e) => st(e.message || String(e), "err")));
$("ai-undo").addEventListener("click", () => { if (lastApplied) { editor.UndoManager.undo(); lastApplied = false; $("ai-undo").hidden = true; } });
$("ai-stop").addEventListener("click", () => { try { abort?.abort(); engine?.interruptGenerate?.(); } catch {} });
panel.querySelectorAll("[data-verb]").forEach((b) => b.addEventListener("click", () => run(b.dataset.verb).catch((e) => { st(friendly(e), "err"); })));
function friendly(e) {
const m = e?.message || String(e);
if (/disposed|out of memory|OutOfMemory|device lost|DeviceLost/i.test(m)) {
try { worker?.terminate(); } catch {} engine = null; engineModel = null;
return "Your GPU ran out of memory for this model. Pick a smaller one (Qwen2.5 Coder 0.5B or SmolLM2) and press Load model again.";
}
return m;
}
editor.on("component:selected component:deselected component:toggled", paintTarget);
if (!("gpu" in navigator)) { const o = $("ai-provider").querySelector('option[value="webllm"]'); if (o) o.textContent += " — no WebGPU here"; if (settings.provider === "webllm") st("This browser has no WebGPU. Use a local endpoint, or a browser with WebGPU.", "warn"); else st(readyText()); }
else st(readyText());
function readyText() { return $("ai-provider").value === "webllm" ? (engine && engineModel === $("ai-model").value ? "Model loaded — ask away" : "Model not loaded yet — press Load model (once)") : "Press Connect to check the local endpoint"; }
function selected() { return editor.getSelected(); }
function isText(c) { return !!c && (c.get("type") === "text" || c.is?.("text")); }
function paintTarget() {
const c = selected();
const t = $("ai-target");
if (!c) { t.textContent = "Nothing selected — “Make a section” adds at the end of the page."; return; }
const tag = c.get("tagName") || "element", cls = (c.getClasses?.() || []).slice(0, 2).join("."), text = (c.getEl?.()?.innerText || "").trim().slice(0, 60);
t.textContent = `Selected: <${tag}${cls ? "." + cls : ""}>${text ? " “" + text + (text.length >= 60 ? "…" : "") + "”" : ""}`;
}
// ---------- providers ----------
async function ensureWebLLM() {
if (!("gpu" in navigator)) throw new Error("WebGPU is not available in this browser.");
const base = $("ai-model").value;
if (engine && engineModel === base) return engine;
const modelId = await fullModelId(base);
if (!webllm) { st("Loading the engine…", "busy"); webllm = await import("../vendor/web-llm/index.js?v=0.2.85"); }
$("ai-load").disabled = true;
try {
const progress = (r) => st((r.text || "Loading…").replace(/\[.*?\]\s*/g, "").slice(0, 140), "busy");
// Switching models: a fresh worker every time. Reloading inside the same
// engine left the runtime with disposed objects ("Object has already
// been disposed" on the next generation).
if (engine) { try { await engine.unload?.(); } catch {} try { worker?.terminate(); } catch {} engine = null; engineModel = null; }
worker = new Worker(new URL("./ai-worker.js?v=20260920ai", import.meta.url), { type: "module" });
engine = await webllm.CreateWebWorkerMLCEngine(worker, modelId, { initProgressCallback: progress });
engineModel = base;
st(`Model loaded (${(await gpuSupportsF16()) ? "f16" : "f32 — this GPU has no f16 shaders, so the slightly larger build"}) — runs on this device, nothing leaves it`, "ok");
return engine;
} finally { $("ai-load").disabled = false; }
}
async function probeLocal() {
persist();
st("Checking " + settings.url + "…", "busy");
const r = await fetch(settings.url + "/models", { signal: AbortSignal.timeout(6000) }).catch((e) => { throw new Error(`Cannot reach ${settings.url}: ${e.message}. Is the runtime running and is this origin allowed (e.g. OLLAMA_ORIGINS=${location.origin})?`); });
if (!r.ok) throw new Error(`Endpoint answered ${r.status}`);
const j = await r.json().catch(() => ({}));
const ids = (j.data || []).map((m) => m.id).filter(Boolean);
$("ai-lmodels").innerHTML = ids.map((id) => `<option value="${esc(id)}">`).join("");
if (!settings.lmodel && ids.length) { $("ai-lmodel").value = ids[0]; persist(); }
st(`Connected — ${ids.length} model${ids.length === 1 ? "" : "s"} available${settings.lmodel ? ", using " + settings.lmodel : ""}`, "ok");
}
async function complete(messages, onToken) {
const provider = $("ai-provider").value;
let text = "";
if (provider === "webllm") {
const eng = await ensureWebLLM();
st("Thinking on your GPU…", "busy");
const stream = await eng.chat.completions.create({ messages, stream: true, temperature: 0.5, max_tokens: 1400 });
for await (const chunk of stream) { const d = chunk.choices?.[0]?.delta?.content || ""; if (d) { text += d; onToken(text); } }
return text;
}
persist();
if (!settings.lmodel) throw new Error("Pick a model name for the local endpoint first (press Connect to list them).");
abort = new AbortController();
st(`Asking ${settings.lmodel} at ${settings.url}`, "busy");
const r = await fetch(settings.url + "/chat/completions", { method: "POST", headers: { "content-type": "application/json" }, signal: abort.signal, body: JSON.stringify({ model: settings.lmodel, messages, stream: true, temperature: 0.5 }) });
if (!r.ok) throw new Error(`Endpoint answered ${r.status}: ${(await r.text().catch(() => "")).slice(0, 200)}`);
const reader = r.body.getReader(); const dec = new TextDecoder(); let buf = "";
for (;;) {
const { value, done } = await reader.read(); if (done) break;
buf += dec.decode(value, { stream: true });
let nl; while ((nl = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1);
if (!line.startsWith("data:")) continue;
const data = line.slice(5).trim(); if (data === "[DONE]") continue;
try { const d = JSON.parse(data).choices?.[0]?.delta?.content || ""; if (d) { text += d; onToken(text); } } catch {}
}
}
return text;
}
// ---------- prompts + apply ----------
const SYSTEM = `You are a web designer working inside a visual site builder. Reply with a single JSON object and nothing else: no prose, no markdown fences, no explanations. Never emit <script>, <iframe>, <link>, <style> tags, event handlers or external URLs other than images already given. Write clean semantic HTML and plain CSS. Every CSS class you create starts with the given prefix and every rule targets those classes. Keep copy short and honest; keep the language of the page.`;
function palette() {
try {
const d = document.querySelector("#gjs iframe").contentDocument; const cs = getComputedStyle(d.body);
return `page background ${cs.backgroundColor}, text ${cs.color}, font ${cs.fontFamily.split(",")[0]}`;
} catch { return "unknown"; }
}
const strip = (raw) => String(raw).replace(/<think>[\s\S]*?<\/think>/g, "").replace(/```(?:json|html|css)?/gi, "").trim();
function parseJson(raw) {
let s = strip(raw);
const a = s.indexOf("{"), b = s.lastIndexOf("}");
if (a >= 0 && b > a) s = s.slice(a, b + 1);
try { const j = JSON.parse(s); return j && typeof j === "object" ? j : null; } catch { return null; }
}
// Small models often answer with bare HTML or CSS instead of the JSON we
// asked for. Accept that too: tags → html (+ any <style> as css); a block
// of rules without tags → css.
function parseLoose(raw) {
const s = strip(raw);
if (/<[a-z][^>]*>/i.test(s)) {
const css = (s.match(/<style[^>]*>[\s\S]*?<\/style>/gi) || []).map((m) => m.replace(/<\/?style[^>]*>/gi, "")).join("\n");
const html = s.replace(/<style[\s\S]*?<\/style>/gi, "").replace(/^[\s\S]*?(?=<)/, "").trim();
return html ? { html, css } : null;
}
if (/\{[\s\S]*\}/.test(s) && /[:;]/.test(s)) return { css: s.slice(s.indexOf(".") >= 0 ? 0 : 0) };
return null;
}
const sanitize = (html) => String(html || "").replace(/<\s*(script|iframe|object|embed|link|meta|style)[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, "").replace(/<\s*(script|iframe|object|embed|link|meta)[^>]*\/?>/gi, "").replace(/\son\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, "").replace(/javascript:/gi, "");
const sanitizeCss = (css) => String(css || "").replace(/@import[^;]*;/gi, "").replace(/expression\s*\(/gi, "").replace(/url\(\s*['"]?\s*(?!data:image|https?:)[^)]*\)/gi, "none");
async function run(verb) {
if (busy) return;
const ask = $("ai-prompt").value.trim();
const sel = selected();
if (verb !== "section" && !sel) { st("Select something on the page first.", "warn"); return; }
if (verb === "text" && !isText(sel)) { st("Select a text block (heading, paragraph, button label) to rewrite.", "warn"); return; }
if (!ask && verb !== "text") { st("Describe what you want first.", "warn"); return; }
busy = true; $("ai-stop").hidden = false; $("ai-undo").hidden = true; out("");
const prefix = "ai" + Math.random().toString(36).slice(2, 6);
try {
let messages, raw, data;
if (verb === "section") {
messages = [{ role: "system", content: SYSTEM }, { role: "user", content: `Create one <section> for the site "${name}". Request: ${ask}. Match this palette unless told otherwise: ${palette()}. Class prefix: "${prefix}-". Answer with a JSON object with two string keys, html (the complete section markup) and css (the rules for its classes).` }];
raw = await complete(messages, out); data = parseJson(raw); if (!/<[a-z][^>]*>/i.test(data?.html || "")) data = parseLoose(raw);
if (!/<[a-z][^>]*>/i.test(data?.html || "")) throw new Error("The model did not return a usable section. Try again, rephrase, or pick a larger model (Qwen2.5 Coder).");
const html = sanitize(data.html), css = sanitizeCss(data.css);
const parent = sel?.parent?.(); const at = sel ? sel.index() + 1 : undefined;
const added = parent ? parent.components().add(html, { at }) : editor.addComponents(html);
if (css) editor.Css.addRules(css);
const first = Array.isArray(added) ? added[0] : added; if (first) { editor.select(first); first.getEl?.()?.scrollIntoView?.({ behavior: "smooth", block: "center" }); }
lastApplied = true; st("Section added — edit it like any other block", "ok");
} else if (verb === "text") {
const current = (sel.getEl()?.innerText || sel.toHTML().replace(/<[^>]+>/g, " ")).trim();
messages = [{ role: "system", content: SYSTEM }, { role: "user", content: `Rewrite this text${ask ? " — instruction: " + ask : " to be clearer and shorter"}. Keep its language and meaning, no quotes around it. Text: """${current.slice(0, 1500)}""" Answer with a JSON object with one string key, text, holding the rewritten text.` }];
raw = await complete(messages, out); data = parseJson(raw);
const text = (data?.text ?? "").trim() || String(raw).replace(/<think>[\s\S]*?<\/think>/g, "").trim();
if (!text) throw new Error("Empty answer.");
sel.components(esc(text));
lastApplied = true; st("Text replaced", "ok");
} else if (verb === "style") {
sel.addClass(prefix);
const snippet = sel.toHTML().slice(0, 1500);
messages = [{ role: "system", content: SYSTEM }, { role: "user", content: `Restyle this element. Request: ${ask}. Page palette: ${palette()}. The element already has the class "${prefix}"; write CSS rules for ".${prefix}" and its descendants only. Element: ${snippet} Answer with a JSON object with one string key, css, holding the rules.` }];
raw = await complete(messages, out); data = parseJson(raw); if (!/\{[\s\S]*:[\s\S]*\}/.test(data?.css || "")) data = parseLoose(raw);
if (!/\{[\s\S]*:[\s\S]*\}/.test(data?.css || "")) { sel.removeClass(prefix); throw new Error("The model did not return usable CSS. Try again, rephrase, or pick a larger model."); }
editor.Css.addRules(sanitizeCss(data.css));
lastApplied = true; st("Styles applied to the selection", "ok");
}
$("ai-undo").hidden = false;
} finally { busy = false; $("ai-stop").hidden = true; abort = null; }
}
function load() { try { return JSON.parse(localStorage.getItem(LS_KEY) || "{}"); } catch { return {}; } }
function save(s) { try { localStorage.setItem(LS_KEY, JSON.stringify(s)); } catch {} }
}

View file

@ -12,6 +12,7 @@
// against the current NFT owner and refuses writes outside bns/<name>/. // against the current NFT owner and refuses writes outside bns/<name>/.
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260917market"; import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260917market";
import { initAssistant } from "./studio-ai.js?v=20260920ai";
const API = "https://silentmode.st"; const API = "https://silentmode.st";
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
@ -294,6 +295,7 @@ function initEditor() {
}); });
editor.on("update", () => { dirty = true; $("btn-draft").disabled = false; }); editor.on("update", () => { dirty = true; $("btn-draft").disabled = false; });
setupEditing(); setupEditing();
try { initAssistant(editor, { esc, name }); } catch (e) { console.warn("assistant unavailable:", e); }
// Ctrl/Cmd+S saves a draft. // Ctrl/Cmd+S saves a draft.
document.addEventListener("keydown", (e) => { if ((e.ctrlKey || e.metaKey) && e.key === "s") { e.preventDefault(); saveDraft(); } }); document.addEventListener("keydown", (e) => { if ((e.ctrlKey || e.metaKey) && e.key === "s") { e.preventDefault(); saveDraft(); } });
window.addEventListener("beforeunload", (e) => { if (dirty) { e.preventDefault(); e.returnValue = ""; } }); window.addEventListener("beforeunload", (e) => { if (dirty) { e.preventDefault(); e.returnValue = ""; } });

View file

@ -74,7 +74,7 @@
footer{border-top:1px solid var(--line);padding:2rem 0 3rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem} footer{border-top:1px solid var(--line);padding:2rem 0 3rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem}
@media (max-width:560px){.row{grid-template-columns:1fr auto}.row .price{grid-column:1/-1;text-align:left}} @media (max-width:560px){.row{grid-template-columns:1fr auto}.row .price{grid-column:1/-1;text-align:left}}
</style> </style>
<script src="./js/i18n.js?v=20260920b"></script> <script src="./js/i18n.js?v=20260920c"></script>
</head> </head>
<body> <body>
<nav class="topnav"> <nav class="topnav">

View file

@ -259,7 +259,7 @@
.addrow{grid-template-columns:1fr 1fr} .addrow{grid-template-columns:1fr 1fr}
} }
</style> </style>
<script src="./js/i18n.js?v=20260920b"></script> <script src="./js/i18n.js?v=20260920c"></script>
</head> </head>
<body> <body>

View file

@ -39,8 +39,27 @@
.btn.small{padding:5px 10px;font-size:12px} .btn.small{padding:5px 10px;font-size:12px}
.btn:disabled{opacity:.45;cursor:not-allowed} .btn:disabled{opacity:.45;cursor:not-allowed}
select.sel{padding:6px 28px 6px 10px;border-radius:9px;background:var(--panel);color:var(--ink);border:1px solid var(--line);font:inherit;font-size:13px} select.sel{padding:6px 28px 6px 10px;border-radius:9px;background:var(--panel);color:var(--ink);border:1px solid var(--line);font:inherit;font-size:13px}
#editor{flex:1;min-height:0} #editor{flex:1;min-height:0;display:flex;min-width:0}
#gjs{height:100%} #gjs{height:100%;flex:1;min-width:0}
/* assistant drawer */
.ai{width:340px;flex:none;display:flex;flex-direction:column;gap:8px;padding:12px 14px;background:#0e131b;border-left:1px solid var(--line);overflow:auto;font-size:13px}
.ai[hidden]{display:none}
.ai-head{display:flex;align-items:center;justify-content:space-between}
.ai-head b{font-size:14px}
.ai .x{background:none;border:0;color:var(--dim);font-size:20px;cursor:pointer;line-height:1}
.ai label{display:block;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--dim);margin:6px 0 3px}
.ai select,.ai input,.ai textarea{width:100%;padding:8px 10px;border-radius:8px;border:1px solid rgba(255,255,255,.12);background:#070a12;color:var(--ink);font:inherit;font-size:13px}
.ai textarea{resize:vertical;min-height:72px}
.ai-hint{color:var(--dim);font-size:11.5px;line-height:1.4}
.ai-hint code{font-size:11px}
.ai-status{font-size:12px;color:var(--mut);padding:7px 10px;border-radius:8px;background:rgba(255,255,255,.04);border-left:3px solid rgba(255,255,255,.12);min-height:30px}
.ai-status.ok{border-left-color:var(--ok);color:var(--ok)} .ai-status.err{border-left-color:var(--taken);color:var(--taken)} .ai-status.busy{border-left-color:var(--warn)} .ai-status.warn{border-left-color:var(--warn);color:var(--warn)}
.ai-verbs{display:flex;flex-wrap:wrap;gap:6px}
.ai-target{font-size:11.5px;color:var(--dim);font-family:var(--mono);word-break:break-word}
.ai-out{flex:1;min-height:80px;max-height:40vh;margin:0;padding:8px 10px;background:#070a12;border:1px solid rgba(255,255,255,.08);border-radius:8px;font-family:var(--mono);font-size:11px;color:var(--mut);white-space:pre-wrap;word-break:break-word;overflow:auto}
.ai-actions{display:flex;gap:6px}
.bar .btn.on{background:rgba(214,255,61,.14);border-color:rgba(214,255,61,.4);color:var(--acid)}
@media (max-width:900px){.ai{position:fixed;right:0;top:0;bottom:0;width:min(340px,92vw);z-index:60;box-shadow:-12px 0 30px rgba(0,0,0,.5)}}
/* Editor theme: GrapesJS 0.23 is themed through CSS variables. Dark panels, /* Editor theme: GrapesJS 0.23 is themed through CSS variables. Dark panels,
near-white text (>= 12:1), acid accents. The preset's own mauve palette is near-white text (>= 12:1), acid accents. The preset's own mauve palette is
switched off in studio.js (useCustomTheme:false). */ switched off in studio.js (useCustomTheme:false). */
@ -112,7 +131,7 @@
.muted{color:var(--mut);font-size:13px} .muted{color:var(--mut);font-size:13px}
code{font-family:var(--mono);background:#0e131b;border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-size:12px} code{font-family:var(--mono);background:#0e131b;border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-size:12px}
</style> </style>
<script src="./js/i18n.js?v=20260920b"></script> <script src="./js/i18n.js?v=20260920c"></script>
</head> </head>
<body> <body>
<div class="bar"> <div class="bar">
@ -127,6 +146,7 @@
</select> </select>
<button class="btn ghost small" id="btn-preview" title="Preview the exported page in a new tab">Preview</button> <button class="btn ghost small" id="btn-preview" title="Preview the exported page in a new tab">Preview</button>
<button class="btn ghost small" id="btn-import" title="Replace the editor contents with the page the name serves right now" hidden>↓ Import live site</button> <button class="btn ghost small" id="btn-import" title="Replace the editor contents with the page the name serves right now" hidden>↓ Import live site</button>
<button class="btn ghost small" id="btn-ai" title="AI assistant — runs on your own device">✨ Assistant</button>
<span class="sp"></span> <span class="sp"></span>
<span class="st" id="status">Loading…</span> <span class="st" id="status">Loading…</span>
<button class="btn ghost small" id="btn-draft" disabled>Save draft</button> <button class="btn ghost small" id="btn-draft" disabled>Save draft</button>
@ -134,7 +154,41 @@
<a class="btn ghost small" id="btn-view" href="#" target="_blank" rel="noopener" hidden>View site →</a> <a class="btn ghost small" id="btn-view" href="#" target="_blank" rel="noopener" hidden>View site →</a>
</div> </div>
<div class="notice" id="notice" hidden><span id="notice-text"></span> <button class="btn ghost small" id="notice-import">Import the live site</button> <button class="x" id="notice-close" title="dismiss">×</button></div> <div class="notice" id="notice" hidden><span id="notice-text"></span> <button class="btn ghost small" id="notice-import">Import the live site</button> <button class="x" id="notice-close" title="dismiss">×</button></div>
<div id="editor"><div id="gjs"></div></div> <div id="editor"><div id="gjs"></div>
<aside class="ai" id="ai" hidden>
<div class="ai-head"><b>✨ Assistant</b><button class="x" id="ai-close" title="close">×</button></div>
<p class="ai-hint" style="margin:0">Runs on your own device. Nothing you write or build is sent to Sirius.X or to any AI vendor.</p>
<label>Runs on</label>
<select id="ai-provider">
<option value="webllm">This browser — your GPU, private</option>
<option value="local">Local endpoint — Ollama, LM Studio</option>
</select>
<div id="ai-webllm">
<label>Model</label>
<select id="ai-model"></select>
<div class="ai-hint">Downloads once, then stays in the browser cache. Needs WebGPU (current Chrome, Edge, Firefox, Safari).</div>
<div class="ai-actions" style="margin-top:6px"><button class="btn ghost small" id="ai-load">Load model</button></div>
</div>
<div id="ai-local" hidden>
<label>Endpoint</label>
<input type="text" id="ai-url" value="http://localhost:11434/v1" spellcheck="false">
<label>Model</label>
<input type="text" id="ai-lmodel" list="ai-lmodels" placeholder="qwen2.5-coder:7b" spellcheck="false"><datalist id="ai-lmodels"></datalist>
<div class="ai-hint">Allow this site in your runtime first, e.g. <code>OLLAMA_ORIGINS=https://silentmode.st ollama serve</code>. LM Studio: enable CORS in its server settings (port 1234).</div>
<div class="ai-actions" style="margin-top:6px"><button class="btn ghost small" id="ai-connect">Connect</button></div>
</div>
<div class="ai-status" id="ai-status"></div>
<label>What do you want?</label>
<textarea id="ai-prompt" placeholder="A hero section for a bakery in Athens, warm colours, one button that says Order"></textarea>
<div class="ai-verbs">
<button class="btn acid small" data-verb="section">Make a section</button>
<button class="btn ghost small" data-verb="text">Rewrite selected text</button>
<button class="btn ghost small" data-verb="style">Restyle selection</button>
</div>
<div class="ai-target" id="ai-target">Nothing selected</div>
<pre class="ai-out" id="ai-out"></pre>
<div class="ai-actions"><button class="btn ghost small" id="ai-undo" hidden>Undo last change</button><button class="btn ghost small" id="ai-stop" hidden>Stop</button></div>
</aside></div>
<!-- sign-in gate --> <!-- sign-in gate -->
<div class="overlay" id="gate" hidden> <div class="overlay" id="gate" hidden>
@ -188,6 +242,6 @@
<script src="./vendor/grapesjs/countdown.js?v=1.0.2"></script> <script src="./vendor/grapesjs/countdown.js?v=1.0.2"></script>
<script src="./vendor/grapesjs/style-bg.js?v=2.0.2"></script> <script src="./vendor/grapesjs/style-bg.js?v=2.0.2"></script>
<script src="./vendor/grapesjs/touch.js?v=0.1.1"></script> <script src="./vendor/grapesjs/touch.js?v=0.1.1"></script>
<script type="module" src="./js/studio.js?v=20260920tilda"></script> <script type="module" src="./js/studio.js?v=20260920ai"></script>
</body> </body>
</html> </html>

View file

@ -115,7 +115,7 @@
<script type="importmap"> <script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } } { "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } }
</script> </script>
<script src="./js/i18n.js?v=20260920b"></script> <script src="./js/i18n.js?v=20260920c"></script>
</head> </head>
<body> <body>

211
vendor/web-llm/LICENSE.txt vendored Normal file
View file

@ -0,0 +1,211 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
------------------------------------------------------------------------------------
This product bundles various third-party components under other open source licenses.
This section summarizes those components and their licenses. See licenses/
for text of these licenses.
Apache Software Foundation License 2.0
--------------------------------------
src/openai_api_protocols

93
vendor/web-llm/index.js vendored Normal file

File diff suppressed because one or more lines are too long