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:
parent
9589537492
commit
7655720e11
18 changed files with 709 additions and 17 deletions
|
|
@ -92,7 +92,7 @@
|
|||
<script type="importmap">
|
||||
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } }
|
||||
</script>
|
||||
<script src="../js/i18n.js?v=20260920b"></script>
|
||||
<script src="../js/i18n.js?v=20260920c"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
|
|||
19
i18n/de.json
19
i18n/de.json
|
|
@ -591,5 +591,22 @@
|
|||
"Sell a name from your dashboard →": "Einen Namen im Dashboard verkaufen →",
|
||||
"Register a new name": "Neuen Namen registrieren",
|
||||
"↓ 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"
|
||||
}
|
||||
|
|
|
|||
19
i18n/el.json
19
i18n/el.json
|
|
@ -591,5 +591,22 @@
|
|||
"Sell a name from your dashboard →": "Πούλησε ένα όνομα από τον πίνακα ελέγχου →",
|
||||
"Register a new name": "Καταχώρηση νέου ονόματος",
|
||||
"↓ 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": "Στοπ"
|
||||
}
|
||||
|
|
|
|||
19
i18n/es.json
19
i18n/es.json
|
|
@ -591,5 +591,22 @@
|
|||
"Sell a name from your dashboard →": "Vender un nombre desde tu panel →",
|
||||
"Register a new name": "Registrar un nombre nuevo",
|
||||
"↓ 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"
|
||||
}
|
||||
|
|
|
|||
19
i18n/fr.json
19
i18n/fr.json
|
|
@ -591,5 +591,22 @@
|
|||
"Sell a name from your dashboard →": "Vendre un nom depuis le tableau de bord →",
|
||||
"Register a new name": "Enregistrer un nouveau nom",
|
||||
"↓ 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"
|
||||
}
|
||||
|
|
|
|||
19
i18n/pt.json
19
i18n/pt.json
|
|
@ -591,5 +591,22 @@
|
|||
"Sell a name from your dashboard →": "Vender um nome a partir do painel →",
|
||||
"Register a new name": "Registar um nome novo",
|
||||
"↓ 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"
|
||||
}
|
||||
|
|
|
|||
19
i18n/ru.json
19
i18n/ru.json
|
|
@ -591,5 +591,22 @@
|
|||
"Sell a name from your dashboard →": "Продать имя из панели →",
|
||||
"Register a new name": "Зарегистрировать новое имя",
|
||||
"↓ 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": "Стоп"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
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>
|
||||
<script src="./js/i18n.js?v=20260920b"></script>
|
||||
<script src="./js/i18n.js?v=20260920c"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
|
|||
7
js/ai-worker.js
Normal file
7
js/ai-worker.js
Normal 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);
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
(function () {
|
||||
var STORE = "sirius-lang";
|
||||
var CACHE = "sirius-i18n:";
|
||||
var VERSION = "20260920b"; // bump when dictionaries change
|
||||
var VERSION = "20260920c"; // bump when dictionaries change
|
||||
var LANGS = {
|
||||
en: "English",
|
||||
de: "Deutsch",
|
||||
|
|
|
|||
223
js/studio-ai.js
Normal file
223
js/studio-ai.js
Normal 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 {} }
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
// 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 { initAssistant } from "./studio-ai.js?v=20260920ai";
|
||||
|
||||
const API = "https://silentmode.st";
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
|
@ -294,6 +295,7 @@ function initEditor() {
|
|||
});
|
||||
editor.on("update", () => { dirty = true; $("btn-draft").disabled = false; });
|
||||
setupEditing();
|
||||
try { initAssistant(editor, { esc, name }); } catch (e) { console.warn("assistant unavailable:", e); }
|
||||
// Ctrl/Cmd+S saves a draft.
|
||||
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 = ""; } });
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
@media (max-width:560px){.row{grid-template-columns:1fr auto}.row .price{grid-column:1/-1;text-align:left}}
|
||||
</style>
|
||||
<script src="./js/i18n.js?v=20260920b"></script>
|
||||
<script src="./js/i18n.js?v=20260920c"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="topnav">
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@
|
|||
.addrow{grid-template-columns:1fr 1fr}
|
||||
}
|
||||
</style>
|
||||
<script src="./js/i18n.js?v=20260920b"></script>
|
||||
<script src="./js/i18n.js?v=20260920c"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
|
|||
64
studio.html
64
studio.html
|
|
@ -39,8 +39,27 @@
|
|||
.btn.small{padding:5px 10px;font-size:12px}
|
||||
.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}
|
||||
#editor{flex:1;min-height:0}
|
||||
#gjs{height:100%}
|
||||
#editor{flex:1;min-height:0;display:flex;min-width:0}
|
||||
#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,
|
||||
near-white text (>= 12:1), acid accents. The preset's own mauve palette is
|
||||
switched off in studio.js (useCustomTheme:false). */
|
||||
|
|
@ -112,7 +131,7 @@
|
|||
.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}
|
||||
</style>
|
||||
<script src="./js/i18n.js?v=20260920b"></script>
|
||||
<script src="./js/i18n.js?v=20260920c"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bar">
|
||||
|
|
@ -127,6 +146,7 @@
|
|||
</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-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="st" id="status">Loading…</span>
|
||||
<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>
|
||||
</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 -->
|
||||
<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/style-bg.js?v=2.0.2"></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>
|
||||
</html>
|
||||
|
|
|
|||
2
tld.html
2
tld.html
|
|
@ -115,7 +115,7 @@
|
|||
<script type="importmap">
|
||||
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } }
|
||||
</script>
|
||||
<script src="./js/i18n.js?v=20260920b"></script>
|
||||
<script src="./js/i18n.js?v=20260920c"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
|
|||
211
vendor/web-llm/LICENSE.txt
vendored
Normal file
211
vendor/web-llm/LICENSE.txt
vendored
Normal 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
93
vendor/web-llm/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue