File size: 21,493 Bytes
65bce6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
// app.js β€” the ultimate input field on WebGPU, with bankML beside it as a separate engine.
//   generic mode (GPU): Agate draws (onnxruntime-web on WebGPU, Logolabs' MIT code and models) or Qwen3-0.6B writes
//                       (transformers.js on WebGPU) β€” the ⧉ GPU button chooses;
//   bankML mode (CPU):  bankML's own engine compiled to WebAssembly (browseML), sha256-verified, a receipt on every answer.
// Nothing leaves the browser except the model downloads, which are cached after the first time.
import * as browseml from "./browseml.js";

const $ = (id) => document.getElementById(id);
const secs = (s) => (s < 60 ? `${s.toFixed(1)} s` : `${Math.floor(s / 60)} min ${Math.round(s % 60)} s`);
const mb = (b) => `${(b / 1e6).toFixed(0)} MB`;
const sha256hex = async (data) => [...new Uint8Array(await crypto.subtle.digest("SHA-256", typeof data === "string" ? new TextEncoder().encode(data) : data))]
  .map((b) => b.toString(16).padStart(2, "0")).join("");

// ── settings, kept in this browser ───────────────────────────────────────────────────────────────────────────
const KEY = "ultimate-webgpu:settings";
const DEFAULTS = { gpu: "qwen", tokens: 384, temp: 0.7, steps: 50, cfg: 3.0, res: 512, seed: null };
let settings = { ...DEFAULTS };
try { settings = { ...DEFAULTS, ...JSON.parse(localStorage.getItem(KEY) || "{}") }; } catch { /* storage off */ }
const save = () => { try { localStorage.setItem(KEY, JSON.stringify(settings)); } catch { /* storage off */ } showEngines(); };

// ── WebGPU on this machine ───────────────────────────────────────────────────────────────────────────────────
let gpu = { ok: false, why: "not checked" };
async function checkGpu() {
  if (!("gpu" in navigator)) gpu = { ok: false, why: "this browser has no WebGPU (navigator.gpu is missing): the GPU engines fall back to the CPU, slowly" };
  else {
    try {
      const a = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
      if (!a) gpu = { ok: false, why: "WebGPU is present but no GPU adapter is available" };
      else {
        const info = a.info || {};
        gpu = { ok: true, name: [info.vendor, info.architecture, info.description].filter(Boolean).join(" ") || "a GPU", f16: a.features.has("shader-f16"), maxBuffer: a.limits.maxBufferSize };
      }
    } catch (e) { gpu = { ok: false, why: `WebGPU adapter request failed: ${e.message || e}` }; }
  }
  $("gpu").textContent = gpu.ok ? `WebGPU: ${gpu.name}${gpu.f16 ? " Β· shader-f16" : ""}` : `No WebGPU: ${gpu.why}`;
  $("gpu").className = `gpu ${gpu.ok ? "ok" : "no"}`;
}

// ── engine 1, GPU: Agate (text β†’ image), loaded from Logolabs' repos and cached ─────────────────────────────────
let agateMod = null, agate = null, agateLoading = null;
async function loadAgate(progress) {
  if (agate) return agate;
  if (agateLoading) return agateLoading;
  agateLoading = (async () => {
    agateMod = agateMod || (await import("./agate/agate.js"));
    const a = new agateMod.Agate({ version: "003", ep: gpu.ok ? "webgpu" : "wasm" });
    const t0 = performance.now();
    await a.load(({ phase, file, loaded, total }) => {
      progress(phase === "download" ? `Agate: downloading ${file}, ${mb(loaded)} of ${mb(total)} (once; then from this browser's cache)` : `Agate: preparing ${file} on ${gpu.ok ? "WebGPU" : "the CPU"}…`);
    }, settings.res);
    a.loadMs = performance.now() - t0;
    agate = a;
    return a;
  })().finally(() => { agateLoading = null; });
  return agateLoading;
}
async function askAgate(prompt, progress) {
  const a = await loadAgate(progress);
  const seed = settings.seed ?? Math.floor(Math.random() * 2 ** 31);
  const t0 = performance.now();
  const r = await a.generate({ prompt, seed, steps: settings.steps, cfg: settings.cfg, watermark: true,
    onStep: (i, n) => progress(`Agate on ${gpu.ok ? "WebGPU" : "the CPU"}: step ${i} of ${n}`) });
  const c = document.createElement("canvas"); c.width = r.width; c.height = r.height;
  c.getContext("2d").putImageData(new ImageData(r.rgba, r.width, r.height), 0, 0);
  const png = await new Promise((ok) => c.toBlob(ok, "image/png"));
  const bytes = agateMod ? (await import("./agate/marking.js")).addPngText(new Uint8Array(await png.arrayBuffer()), a.marks.info) : new Uint8Array(await png.arrayBuffer());
  const s = (performance.now() - t0) / 1000;
  return { bytes, width: r.width, height: r.height, seed, seconds: s, timings: r.timings, prepared: r.prepared };
}

// ── engine 2, GPU: Qwen3-0.6B (text) through transformers.js ────────────────────────────────────────────────────
const QWEN = { id: "onnx-community/Qwen3-0.6B-ONNX", revision: "1e0a4a196ecabdf9a879664110574563d3f372d3", dtype: "q4f16", label: "Qwen3-0.6B" };
let tjs = null, qwen = null, qwenLoading = null;
async function loadQwen(progress) {
  if (qwen) return qwen;
  if (qwenLoading) return qwenLoading;
  qwenLoading = (async () => {
    tjs = tjs || (await import("https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0/dist/transformers.min.js"));
    const files = {};
    qwen = await tjs.pipeline("text-generation", QWEN.id, {
      device: gpu.ok ? "webgpu" : "wasm", dtype: gpu.ok ? QWEN.dtype : "int8", revision: QWEN.revision,
      progress_callback: (p) => {
        if (p.status === "progress" && p.total) { files[p.file] = [p.loaded, p.total];
          const [l, t] = Object.values(files).reduce(([a, b], [x, y]) => [a + x, b + y], [0, 0]);
          progress(`${QWEN.label}: downloading ${mb(l)} of ${mb(t)} (once; then from this browser's cache)`); }
        else if (p.status === "ready") progress(`${QWEN.label}: preparing on ${gpu.ok ? "WebGPU" : "the CPU"}…`);
      },
    });
    return qwen;
  })().finally(() => { qwenLoading = null; });
  return qwenLoading;
}
async function askQwen(hist, prompt, push, progress) {
  const gen = await loadQwen(progress);
  const messages = [{ role: "system", content: "You are a helpful, concise assistant running in the visitor's browser on WebGPU. Answer plainly." },
    ...hist.slice(-8), { role: "user", content: `${prompt} /no_think` }];
  const t0 = performance.now();
  let n = 0, first = null, text = "", raw = "", shown = 0;
  progress(`${QWEN.label} on ${gpu.ok ? "WebGPU" : "the CPU"}: reading the prompt…`);
  const streamer = new tjs.TextStreamer(gen.tokenizer, { skip_prompt: true, skip_special_tokens: true,
    callback_function: (t) => {
      if (!t) return;
      if (first === null) first = (performance.now() - t0) / 1000;
      n++; raw += t;
      // Qwen3's reasoning block (empty under /no_think) is not part of the answer: shown neither streaming nor after
      const open = raw.indexOf("<think>"), close = raw.indexOf("</think>");
      if (open !== -1 && close === -1) return;
      const clean = (close !== -1 ? raw.slice(close + 8) : raw).replace(/^\s+/, "");
      if (clean.length > shown) { push(clean.slice(shown)); shown = clean.length; text = clean; }
    } });
  await gen(messages, { max_new_tokens: settings.tokens, do_sample: settings.temp > 0, temperature: settings.temp || undefined, streamer });

  const s = (performance.now() - t0) / 1000;
  return { text, seconds: s, ttft: first, pieces: n, tps: first !== null && s > first ? (n - 1) / (s - first) : null };
}

// ── engine 3, CPU: bankML in this browser (browseML), separate, verified ────────────────────────────────────────
let bankPersona = null, browsed = null;
async function loadBank(progress) {
  if (browseml.isReady()) return browsed;
  if (!bankPersona) bankPersona = await (await fetch("bankml.persona")).json();
  const t0 = performance.now();
  browsed = await browseml.load((got, total, phase) => {
    if (phase === "download") progress(`bankML: downloading ${browseml.MODEL.title}, ${mb(got)} of ${mb(total)} (once)`);
    else if (phase === "cache") progress(`bankML: the model is in this browser (${mb(got)})`);
    else if (phase === "verify") progress(`bankML: checking the model's sha256 against its pin…`);
  }, () => {});
  browsed.loadMs = performance.now() - t0;
  return browsed;
}
async function askBank(hist, prompt, push, progress) {
  const v = await loadBank(progress);
  const where = `WHERE YOU ARE RUNNING (measured by bankML just now): ${v.model} (sha256 ${v.sha256.slice(0, 16)}…), verified by bankML ${v.version}, ` +
    `in the visitor's browser as WebAssembly (browseML) on ${v.threads} CPU thread${v.threads > 1 ? "s" : ""}, beside a WebGPU page; nothing is sent anywhere.`;
  const msgs = [{ role: "system", content: (bankPersona?.system_prompt || "You are bankML.") + "\n\n" + where }, ...hist.slice(-8), { role: "user", content: prompt }];
  const t0 = performance.now();
  let text = "";
  progress(`bankML on ${v.threads} CPU thread${v.threads > 1 ? "s" : ""}: reading the prompt…`);
  const result = await browseml.chat({ messages: msgs, max_tokens: settings.tokens }, (piece) => { text += piece; push(piece); }, () => {});
  const receipt = result.bankml_receipt;
  const ok = !!receipt && (await sha256hex(text)) === receipt.response_sha256;
  return { text, ok, receipt, usage: result.usage || {}, timings: result.timings || {}, seconds: (performance.now() - t0) / 1000, v };
}

// ── one interface over the three, for the field ──────────────────────────────────────────────────────────────
function streamOf(run) {
  const q = []; let wake = null, end = false, err = null;
  const poke = () => { const w = wake; wake = null; w && w(); };
  run((x) => { q.push(x); poke(); }).then(() => { end = true; poke(); }, (e) => { err = e; end = true; poke(); });
  return (async function* () { for (;;) { while (q.length) yield q.shift(); if (end) { if (err) throw err; return; } await new Promise((r) => { wake = r; }); } })();
}
const statusListeners = [];
const which = (mode) => (mode === "persona" ? "bankml" : settings.gpu);
const NAME = {
  get agate() { return `Agate (${gpu.ok ? "WebGPU" : "CPU, no GPU here"})`; },
  get qwen() { return `${QWEN.label} (${gpu.ok ? "WebGPU" : "CPU, no GPU here"})`; },
  bankml: "bankML (CPU)",
};
function showEngines() {
  $("engines").textContent = `generic mode: ${NAME[settings.gpu]} Β· bankML mode: bankML on the CPU, with a receipt`;
  statusListeners.forEach((f) => f());
}

const engine = {
  async *ask(hist, message, done, _window, progress = () => {}, mode = "chat") {
    const e = which(mode), prompt = message.trim(), date = new Date().toISOString().slice(0, 10);
    let v = null;
    yield* streamOf(async (push) => {
      try {
        if (e === "agate") {
          const r = await askAgate(prompt, progress);
          const url = URL.createObjectURL(new Blob([r.bytes], { type: "image/png" }));
          const sha = await sha256hex(r.bytes);
          push(`πŸ–Ό ${prompt}`);
          v = { ok: true, line: `Agate 003 Β· ${r.width}px Β· ${secs(r.seconds)}`,
            details: `Agate preview 003 on ${gpu.ok ? `WebGPU (${gpu.name})` : "the CPU"}: ${settings.steps} steps, cfg ${settings.cfg}, seed ${r.seed}, ${r.width}Γ—${r.height} Β· ${secs(r.seconds)} Β· marked AI-generated (AGATE003 watermark + PNG fields) Β· sha256 ${sha.slice(0, 16)}…`,
            media: { kind: "image", url, name: `agate-003-${r.width}px-seed${r.seed}.png`, caption: "AI-generated Β· Agate preview 003" },
            record: { id: `agate-${Date.now()}`, date, mode: "image", question: prompt, answer: `image/png ${r.width}x${r.height}, ${r.bytes.byteLength} bytes, sha256 ${sha}`,
              carrier: { engine: `onnxruntime-web 1.30.0 ${gpu.ok ? "WebGPU" : "WASM"}`, model: "Logolabs/agate-preview-003", where: `this browser${gpu.ok ? `, ${gpu.name}` : ""}` },
              deliveries: 1, finish: "stop", parameters: { steps: settings.steps, cfg: settings.cfg, seed: r.seed, resolution: settings.res },
              timings: { seconds: r.seconds }, response_sha256: sha, marked_ai_generated: true } };
        } else if (e === "qwen") {
          const r = await askQwen(hist, prompt, push, progress);
          if (!r.text.trim()) push("(no answer: the model wrote nothing)");
          hist.push({ role: "user", content: prompt }, { role: "assistant", content: r.text });
          v = { ok: !!r.text.trim(), line: `${QWEN.label} Β· ${r.pieces} pieces Β· ${r.tps ? r.tps.toFixed(1) + " tok/s Β· " : ""}${secs(r.seconds)}`,
            details: `${QWEN.label} ${gpu.ok ? QWEN.dtype : "int8"} on ${gpu.ok ? `WebGPU (${gpu.name})` : "the CPU"} via transformers.js 4.2.0 Β· first token ${r.ttft?.toFixed(2) ?? "β€”"} s Β· ${r.pieces} pieces Β· ${secs(r.seconds)} Β· no receipt (only bankML puts one on an answer)`,
            record: { id: `qwen-${Date.now()}`, date, mode: "text", question: prompt, answer: r.text,
              carrier: { engine: `transformers.js 4.2.0 ${gpu.ok ? "WebGPU" : "WASM"}`, model: `${QWEN.id}@${QWEN.revision.slice(0, 7)} ${gpu.ok ? QWEN.dtype : "int8"}`, where: `this browser${gpu.ok ? `, ${gpu.name}` : ""}` },
              deliveries: 1, finish: "stop", limit: settings.tokens, timings: { ttft_s: r.ttft, seconds: r.seconds, pieces_per_second: r.tps },
              response_sha256: await sha256hex(r.text) } };
        } else {
          const r = await askBank(hist, prompt, push, progress);
          hist.push({ role: "user", content: prompt }, { role: "assistant", content: r.text });
          const u = r.usage, tm = r.timings;
          v = { ok: r.ok, line: `bankML Β· ${u.completion_tokens ?? "?"} tokens Β· ${Number(tm.predicted_per_second || 0).toFixed(2)} tok/s Β· ${r.ok ? "βœ“ receipt" : "βœ— receipt"}`,
            details: `${r.ok ? "βœ“ receipt: the answer's sha256 matches bankML's" : r.receipt ? "βœ— receipt: the answer does not match its sha256" : "βœ— no receipt came with this answer"} Β· ${u.prompt_tokens ?? "?"} + ${u.completion_tokens ?? "?"} tokens counted by bankML ${r.v.version} Β· ${secs(r.seconds)} Β· ${r.v.model} (sha256 ${r.v.sha256.slice(0, 12)}…) on ${r.v.threads} CPU thread${r.v.threads > 1 ? "s" : ""}`,
            record: { id: `bankml-${Date.now()}`, date, mode: "bankML", question: prompt, answer: r.text,
              carrier: { engine: `bankML ${r.v.version} browseML (WebAssembly)`, model: r.v.model.replace(/\.gguf$/, ""), sha256: r.v.sha256, system_prompt: "bankml.persona system_prompt", where: "this browser (browseML)" },
              deliveries: 1, finish: "stop", limit: settings.tokens, usage: u,
              timings: { ttft_s: tm.prompt_ms != null ? tm.prompt_ms / 1000 : undefined, seconds: r.seconds, predicted_per_second: tm.predicted_per_second },
              response_sha256: await sha256hex(r.text), receipt: { ok: r.ok } } };
        }
      } catch (err) {
        const m = `${NAME[e]} did not answer: ${String(err?.message || err).slice(0, 300)}`;
        push(m);
        v = { ok: false, line: m };
      }
    });
    done(v || { ok: false, line: "no answer" });
  },
  status() { return `${NAME[settings.gpu]} Β· bankML ${browseml.isReady() ? "βœ“" : "β€”"}`; },
  onStatus(f) { statusListeners.push(f); },
  async ping() {
    return { ok: gpu.ok, line: `${gpu.ok ? `WebGPU: ${gpu.name}${gpu.f16 ? ", shader-f16" : ""}, max buffer ${mb(gpu.maxBuffer)}` : `no WebGPU: ${gpu.why}`} Β· Agate ${agate ? "loaded" : "not loaded"} Β· ${QWEN.label} ${qwen ? "loaded" : "not loaded"} Β· bankML ${browseml.isReady() ? `loaded, ${browsed?.threads} thread(s)` : "not loaded"}` };
  },
};

// ── the button set; every one is also a T-mode command ───────────────────────────────────────────────────────
const num = (x, lo, hi) => { const n = Number(x); return Number.isFinite(n) && n >= lo && n <= hi ? n : undefined; };
const setting = (cmd, key, lo, hi, int, what) => ({
  id: `wg-${cmd}`, label: cmd, command: cmd, icon: "βš™", defaultHidden: true, description: `${cmd} <${lo}-${hi}> - ${what}`,
  run(ctx) {
    if (key === "seed" && ctx.args[0] === "random") { settings.seed = null; save(); return ctx.print("seed: random"); }
    const n = num(ctx.args[0], lo, hi);
    if (n === undefined) return ctx.print(`${cmd}: now ${settings[key] ?? "random"}; give ${lo} to ${hi}${key === "seed" ? " or random" : ""}`, "err");
    settings[key] = int ? Math.round(n) : n; save(); ctx.print(`${cmd}: ${settings[key]}`);
    if (key === "res" && agate) { agate = null; ctx.print("Agate reloads at the new resolution on the next image"); }
  },
});
engine.actions = [
  { id: "wg-gpu", label: "GPU engine: Qwen3 text or Agate images", command: "gpu", aliases: ["engine"], icon: "⧉",
    description: "gpu [qwen|agate] - what the generic mode runs on WebGPU (no argument: switch)",
    run(ctx) {
      const a = ctx.args[0];
      settings.gpu = a === "agate" || a === "image" ? "agate" : a === "qwen" || a === "text" ? "qwen" : settings.gpu === "qwen" ? "agate" : "qwen";
      save(); ctx.print(`generic mode runs ${NAME[settings.gpu]}`);
    } },
  { id: "wg-load", label: "Load the models", command: "load", icon: "⬇",
    description: "load [agate|qwen|bankml|all] - download (once) and prepare a model now, instead of on the first question",
    async run(ctx) {
      const w = ctx.args[0] || (ctx.mode === "persona" ? "bankml" : settings.gpu);
      const list = w === "all" ? ["agate", "qwen", "bankml"] : [w];
      for (const k of list) {
        const t0 = performance.now(), say = (t) => {};
        try {
          if (k === "agate") await loadAgate(say); else if (k === "qwen") await loadQwen(say); else await loadBank(say);
          ctx.print(`βœ“ ${NAME[k]} ready in ${secs((performance.now() - t0) / 1000)}`);
        } catch (e) { ctx.print(`${NAME[k]} could not load: ${String(e?.message || e).slice(0, 200)}`, "err"); }
      }
      showEngines();
    } },
  { id: "wg-example", label: "An example", command: "example", aliases: ["ex"], icon: "✦",
    description: "example [n] - an example for the engine now (no number: list them)",
    run(ctx) {
      const ex = ctx.mode === "persona" ? ["Who are you, and how do I know this answer is yours?", "What is a receipt on an answer?"]
        : settings.gpu === "agate" ? ["A lighthouse on a cliff at dusk, oil painting", "A red panda reading a book in a library, watercolor", "A neon sign that says \"WEBGPU\" on a brick wall"]
        : ["Explain WebGPU in three sentences.", "Write a haiku about a GPU.", "What is the difference between a CPU and a GPU?"];
      const n = Number(ctx.args[0]);
      if (!n || !ex[n - 1]) { ex.forEach((e, i) => ctx.print(`${i + 1}. ${e}`)); return; }
      ctx.send(ex[n - 1]);
    } },
  setting("tokens", "tokens", 16, 2048, true, "the most a text answer may use"),
  setting("temp", "temp", 0, 1.5, false, "Qwen3's temperature (0 answers the same each time)"),
  setting("steps", "steps", 4, 100, true, "Agate's sampling steps (fewer is faster)"),
  setting("cfg", "cfg", 1, 10, false, "Agate's guidance scale"),
  setting("res", "res", 256, 512, true, "Agate's resolution: 512 or 256"),
  setting("seed", "seed", 0, 2 ** 31 - 1, true, "Agate's seed (random: a new one each image)"),
  { id: "wg-help", label: "WebGPU commands", command: "webgpu", aliases: ["wg"], icon: "⧉", defaultHidden: true, description: "webgpu - these commands",
    run(ctx) { for (const a of engine.actions) ctx.print(`  ${a.description}`); ctx.print("  bankML - the bankML mode Β· > the generic mode Β· ping Β· last Β· details Β· help"); } },
];

// ── boot ─────────────────────────────────────────────────────────────────────────────────────────────────────
showEngines();
checkGpu().then(showEngines);
(() => {
  const ui = window.BankmlSpaceUIF;
  if (!ui) return;
  if (ui.isOutputWindow()) document.body.classList.add("uif-popout");
  ui.mount($("uif-root"), engine, {
    placeholder: "Ask, or describe an image (⧉ switches Qwen3 / Agate) Β· bankML for the verified CPU engine Β· T terminal",
    terminalPlaceholder: "T mode: webgpu Β· gpu Β· load all Β· steps Β· seed Β· tokens Β· ping Β· last Β· help",
    anchor: $("top"), storageKey: "ultimate-webgpu",
    persona: { label: "bankML", title: "bankML mode: bankML answers on your CPU, verified, with a receipt",
      placeholder: "bankML: verified answers on your CPU, with a receipt (Enter) Β· > the GPU Β· T terminal" },
    pill: { idle: "no answer yet", ok: "βœ“ done", bad: "βœ— not done" },
    modeBar: {
      chat: ["wg-gpu", "wg-load", "wg-example", "spawn", "outputs"],
      persona: ["wg-load", "wg-example", "spawn", "outputs"],
      terminal: ["wg-help", "wg-gpu", "wg-load", "last", "details", "ping", "outputs", "clear"],
    },
  });
})();