murmur-rag 0.1.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +23 -19
  2. package/dist/cdn/anthropic-dc7h9prc.js +2 -0
  3. package/dist/cdn/chunk-hpx6rpea.js +1 -0
  4. package/dist/cdn/{embed-hn0r8fmg.js → embed-6gef7ym5.js} +2 -2
  5. package/dist/cdn/{embed-59r4jck6.js → embed-acjpjh6t.js} +3 -3
  6. package/dist/cdn/embed-fd96s8qk.js +44 -0
  7. package/dist/cdn/embed-jdraznwg.js +83 -0
  8. package/dist/cdn/{embed-q6y6vgv1.js → embed-krrjjyf5.js} +4 -4
  9. package/dist/cdn/{embed-we24qtes.js → embed-ssqsb38w.js} +2 -2
  10. package/dist/cdn/embed-z32y952f.js +9 -0
  11. package/dist/cdn/google-yywkqdph.js +14 -0
  12. package/dist/cdn/index-bq0x71h1.js +1 -0
  13. package/dist/cdn/index-gyd02jvt.js +9 -0
  14. package/dist/cdn/index-jt6n0d28.js +1 -0
  15. package/dist/cdn/index-nesgnf2n.js +1 -0
  16. package/dist/cdn/index-wgzgvzaf.js +1 -0
  17. package/dist/cdn/mistral-0mg4kk7s.js +3 -0
  18. package/dist/cdn/murmur-j1ky5vmr.js +5 -0
  19. package/dist/cdn/murmur.js +24 -5
  20. package/dist/cdn/openai-compatible-zhaj1r43.js +11 -0
  21. package/dist/cdn/openai-g1685z9v.js +18 -0
  22. package/dist/cdn/retriever-dqxzjaqk.js +1 -0
  23. package/dist/cdn/transformers.web-3kmbvcbt.js +46 -0
  24. package/dist/cli.js +10 -5
  25. package/dist/lib/chunks/{index-fs6gmhz9.js → index-8h9c8ekq.js} +4 -2
  26. package/dist/lib/chunks/{index-0x7gav29.js → index-92w507gk.js} +304 -27
  27. package/dist/lib/chunks/{index-6re28wc1.js → index-b77xqap3.js} +13 -1
  28. package/dist/lib/chunks/{index-z8pn9sdj.js → index-p86sy3y4.js} +17 -4
  29. package/dist/lib/chunks/{murmur-wq1d15n7.js → murmur-t36hd4vt.js} +2 -2
  30. package/dist/lib/chunks/{retriever-1rw4z2n6.js → retriever-wxshtnm0.js} +1 -1
  31. package/dist/lib/embed.js +4 -2
  32. package/dist/lib/index.js +5 -3
  33. package/dist/lib/indexer/index.js +10 -5
  34. package/dist/lib/local/index.js +163 -0
  35. package/dist/lib/rag/index.js +13 -11
  36. package/dist/lib/widget/index.js +2 -2
  37. package/dist/types/local/index.d.ts +56 -0
  38. package/dist/types/rag/ground.d.ts +7 -0
  39. package/dist/types/rag/index.d.ts +1 -1
  40. package/dist/types/widget/element.d.ts +3 -0
  41. package/dist/types/widget/i18n.d.ts +14 -0
  42. package/dist/types/widget/session.d.ts +20 -1
  43. package/dist/types/widget/styles.d.ts +1 -1
  44. package/package.json +18 -3
  45. package/dist/cdn/anthropic-w7nkjk5e.js +0 -2
  46. package/dist/cdn/chunk-e0j28zmb.js +0 -1
  47. package/dist/cdn/embed-2xm309sb.js +0 -7
  48. package/dist/cdn/embed-n7jskdxd.js +0 -44
  49. package/dist/cdn/embed-sgjh2kg2.js +0 -83
  50. package/dist/cdn/google-qha5xyx4.js +0 -14
  51. package/dist/cdn/index-11c9cspb.js +0 -1
  52. package/dist/cdn/index-e5entetm.js +0 -1
  53. package/dist/cdn/index-kk9hgxdc.js +0 -1
  54. package/dist/cdn/index-zrmby2xc.js +0 -9
  55. package/dist/cdn/mistral-16bekg8b.js +0 -3
  56. package/dist/cdn/murmur-5vkc01ns.js +0 -5
  57. package/dist/cdn/openai-compatible-za7hc1fv.js +0 -11
  58. package/dist/cdn/openai-dhy4fp4c.js +0 -18
  59. package/dist/cdn/retriever-k9mmdcz5.js +0 -1
package/dist/cli.js CHANGED
@@ -99,7 +99,7 @@ function extractSections(doc, { root = DEFAULT_ROOT, exclude = DEFAULT_EXCLUDE }
99
99
  return { title, sections: [] };
100
100
  const sections = [];
101
101
  let cur = { heading: "", anchor: "", text: "" };
102
- const used = new Set;
102
+ const slugs = new Map;
103
103
  const walk = (node) => {
104
104
  if (node.nodeType === 3) {
105
105
  cur.text += (node.textContent ?? "").replace(/\s+/g, " ");
@@ -114,16 +114,21 @@ function extractSections(doc, { root = DEFAULT_ROOT, exclude = DEFAULT_EXCLUDE }
114
114
  if (/^H[1-3]$/.test(tag)) {
115
115
  sections.push(cur);
116
116
  const heading = (el.textContent ?? "").replace(/\s+/g, " ").trim();
117
- let anchor = el.id || el.closest("[id]")?.id || slugify(heading);
118
- while (used.has(anchor))
119
- anchor += "-";
120
- used.add(anchor);
117
+ let anchor = el.id || el.closest("[id]")?.id || "";
118
+ if (!anchor) {
119
+ const base = slugify(heading);
120
+ const n = slugs.get(base) ?? 0;
121
+ slugs.set(base, n + 1);
122
+ anchor = n ? `${base}-${n}` : base;
123
+ }
121
124
  cur = { heading, anchor, text: "" };
122
125
  return;
123
126
  }
124
127
  if (tag === "BR")
125
128
  cur.text += `
126
129
  `;
130
+ if ((tag === "TD" || tag === "TH") && !/(^|\n)\s*$/.test(cur.text))
131
+ cur.text += " | ";
127
132
  const block = BLOCK.test(tag);
128
133
  if (block)
129
134
  cur.text += `
@@ -1,12 +1,13 @@
1
1
  import {
2
2
  bm25Retriever2
3
- } from "./index-z8pn9sdj.js";
3
+ } from "./index-p86sy3y4.js";
4
4
  import {
5
5
  estimateTokens2,
6
6
  formatSources2,
7
7
  groundingRules2,
8
+ cleanAnswer2,
8
9
  citedSources2
9
- } from "./index-6re28wc1.js";
10
+ } from "./index-b77xqap3.js";
10
11
 
11
12
  // src/core/murmur.ts
12
13
  import { streamText } from "ai";
@@ -67,6 +68,7 @@ ${formatSources2(passages)}` : "Sources: none matched this question. Say you cou
67
68
  text += part.text;
68
69
  yield part.text;
69
70
  }
71
+ text = cleanAnswer2(text);
70
72
  settle({ text, cited: citedSources2(text, passages.length) });
71
73
  } catch (error) {
72
74
  fail(error);
@@ -1,6 +1,7 @@
1
1
  import {
2
+ cleanAnswer2,
2
3
  passageHref2
3
- } from "./index-6re28wc1.js";
4
+ } from "./index-b77xqap3.js";
4
5
  var __chunkGraphs;
5
6
  var __chunkSeen;
6
7
  var __chunkNonce;
@@ -54,7 +55,7 @@ var en = {
54
55
  errorOther: "The answer failed:",
55
56
  retry: "Try again",
56
57
  setupTitle: "Connect an AI model",
57
- setupIntro: "This site lets you use your own AI key. It stays in your browser, encrypted, and is only sent to the provider you pick.",
58
+ setupIntro: "Pick how this site answers you: a model that runs in your browser, or your own AI key, kept encrypted in your browser and sent only to the provider you pick.",
58
59
  provider: "Provider",
59
60
  model: "Model",
60
61
  apiKey: "API key",
@@ -73,7 +74,21 @@ var en = {
73
74
  catalogError: "Could not load the model list.",
74
75
  searchModels: "Search models",
75
76
  recommended: "Recommended",
76
- otherProviders: "Other providers"
77
+ otherProviders: "Other providers",
78
+ localOption: "Run a model in this browser",
79
+ localOptionHint: "{name}, about {size} downloaded once, then kept in this browser. Needs WebGPU; nothing you type leaves your device.",
80
+ localActive: "Running in this browser",
81
+ orKey: "or connect a provider with your key",
82
+ useKey: "Use an API key instead",
83
+ localConsent: "This assistant can run entirely in your browser with {name}, an AI model of about {size}. It downloads once, then stays in this browser; your questions never leave your device.",
84
+ localDownload: "Download and answer",
85
+ localLoading: "Downloading the model…",
86
+ localLoadingLabel: "Model download",
87
+ localPreparing: "Preparing the model on your GPU…",
88
+ localNoWebGPU: "This browser cannot run the model locally (no WebGPU). Try a recent Chrome, Edge or Safari, or use an API key.",
89
+ localLoadFailed: "The model could not be loaded in this browser.",
90
+ gigabytes: "GB",
91
+ megabytes: "MB"
77
92
  };
78
93
  var fr = {
79
94
  open: "Poser une question",
@@ -96,7 +111,7 @@ var fr = {
96
111
  errorOther: "La réponse a échoué :",
97
112
  retry: "Réessayer",
98
113
  setupTitle: "Connecter un modèle d'IA",
99
- setupIntro: "Ce site vous laisse utiliser votre propre clé d'IA. Elle reste dans votre navigateur, chiffrée, et n'est envoyée qu'au fournisseur choisi.",
114
+ setupIntro: "Choisissez comment ce site vous répond : un modèle qui tourne dans votre navigateur, ou votre propre clé d'IA, gardée chiffrée dans votre navigateur et envoyée seulement au fournisseur choisi.",
100
115
  provider: "Fournisseur",
101
116
  model: "Modèle",
102
117
  apiKey: "Clé API",
@@ -115,7 +130,21 @@ var fr = {
115
130
  catalogError: "Impossible de charger la liste des modèles.",
116
131
  searchModels: "Chercher un modèle",
117
132
  recommended: "Recommandés",
118
- otherProviders: "Autres fournisseurs"
133
+ otherProviders: "Autres fournisseurs",
134
+ localOption: "Faire tourner un modèle dans ce navigateur",
135
+ localOptionHint: "{name}, environ {size} téléchargés une fois puis gardés dans ce navigateur. Nécessite WebGPU ; rien de ce que vous tapez ne quitte votre appareil.",
136
+ localActive: "Modèle actif dans ce navigateur",
137
+ orKey: "ou connectez un fournisseur avec votre clé",
138
+ useKey: "Utiliser une clé API à la place",
139
+ localConsent: "Cet assistant peut fonctionner entièrement dans votre navigateur avec {name}, un modèle d'IA d'environ {size}. Il se télécharge une fois puis reste dans ce navigateur ; vos questions ne quittent jamais votre appareil.",
140
+ localDownload: "Télécharger et répondre",
141
+ localLoading: "Téléchargement du modèle…",
142
+ localLoadingLabel: "Téléchargement du modèle",
143
+ localPreparing: "Préparation du modèle sur votre GPU…",
144
+ localNoWebGPU: "Ce navigateur ne peut pas faire tourner le modèle en local (pas de WebGPU). Essayez un Chrome, Edge ou Safari récent, ou utilisez une clé API.",
145
+ localLoadFailed: "Le modèle n'a pas pu être chargé dans ce navigateur.",
146
+ gigabytes: "Go",
147
+ megabytes: "Mo"
119
148
  };
120
149
  var MESSAGES2 = { en, fr };
121
150
  function pickLanguage2(...candidates) {
@@ -194,6 +223,13 @@ function inline(doc, text, cite, out) {
194
223
  if (last < text.length)
195
224
  out.appendChild(doc.createTextNode(text.slice(last)));
196
225
  }
226
+ var TABLE_RULE = /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)*\|?\s*$/;
227
+ function lineBreak(doc, previous) {
228
+ return / {2,}$|\\$/.test(previous) ? doc.createElement("br") : doc.createTextNode(" ");
229
+ }
230
+ function cells(row) {
231
+ return row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
232
+ }
197
233
  function renderMarkdown2(target, markdown, cite) {
198
234
  const doc = target.ownerDocument;
199
235
  const frag = doc.createDocumentFragment();
@@ -206,7 +242,11 @@ function renderMarkdown2(target, markdown, cite) {
206
242
  if (!para.length)
207
243
  return;
208
244
  const p = doc.createElement("p");
209
- inline(doc, para.join(" "), cite, p);
245
+ para.forEach((raw, n) => {
246
+ if (n)
247
+ p.appendChild(lineBreak(doc, para[n - 1] ?? ""));
248
+ inline(doc, raw.trim(), cite, p);
249
+ });
210
250
  frag.appendChild(p);
211
251
  para = [];
212
252
  };
@@ -215,20 +255,68 @@ function renderMarkdown2(target, markdown, cite) {
215
255
  };
216
256
  for (let i = 0;i < lines.length; i++) {
217
257
  const line = lines[i] ?? "";
218
- const fence = /^\s*```/.exec(line);
258
+ const fence = /^(\s*)```/.exec(line);
219
259
  if (fence) {
220
260
  flushPara();
221
- closeList();
261
+ const indent = fence[1]?.length ?? 0;
262
+ const owner = indent >= 2 && list ? list.lastElementChild : null;
263
+ if (!owner)
264
+ closeList();
222
265
  const body = [];
223
- for (i++;i < lines.length && !/^\s*```/.test(lines[i] ?? ""); i++)
224
- body.push(lines[i] ?? "");
266
+ for (i++;i < lines.length && !/^\s*```/.test(lines[i] ?? ""); i++) {
267
+ const raw = lines[i] ?? "";
268
+ body.push(raw.startsWith(" ".repeat(indent)) ? raw.slice(indent) : raw.trimStart());
269
+ }
225
270
  const pre = doc.createElement("pre");
226
271
  pre.tabIndex = 0;
227
272
  const code = doc.createElement("code");
228
273
  code.textContent = body.join(`
229
274
  `);
230
275
  pre.appendChild(code);
231
- frag.appendChild(pre);
276
+ (owner ?? frag).appendChild(pre);
277
+ continue;
278
+ }
279
+ if (/^\s*([-*_])(?:\s*\1){2,}\s*$/.test(line)) {
280
+ flushPara();
281
+ closeList();
282
+ frag.appendChild(doc.createElement("hr"));
283
+ continue;
284
+ }
285
+ if (line.includes("|") && TABLE_RULE.test(lines[i + 1] ?? "")) {
286
+ flushPara();
287
+ closeList();
288
+ const rows = [cells(line)];
289
+ for (i += 2;i < lines.length && (lines[i] ?? "").includes("|"); i++)
290
+ rows.push(cells(lines[i] ?? ""));
291
+ i--;
292
+ const wrap = doc.createElement("div");
293
+ wrap.className = "table";
294
+ wrap.tabIndex = 0;
295
+ const table = doc.createElement("table");
296
+ rows.forEach((row, r) => {
297
+ const tr = doc.createElement("tr");
298
+ for (const cell of row) {
299
+ const td = doc.createElement(r === 0 ? "th" : "td");
300
+ inline(doc, cell, cite, td);
301
+ tr.appendChild(td);
302
+ }
303
+ table.appendChild(tr);
304
+ });
305
+ wrap.appendChild(table);
306
+ frag.appendChild(wrap);
307
+ continue;
308
+ }
309
+ const quote = /^\s*>\s?(.*)$/.exec(line);
310
+ if (quote) {
311
+ flushPara();
312
+ closeList();
313
+ const body = [quote[1] ?? ""];
314
+ for (;/^\s*>/.test(lines[i + 1] ?? ""); i++)
315
+ body.push((lines[i + 1] ?? "").replace(/^\s*>\s?/, ""));
316
+ const blockquote = doc.createElement("blockquote");
317
+ renderMarkdown2(blockquote, body.join(`
318
+ `), cite);
319
+ frag.appendChild(blockquote);
232
320
  continue;
233
321
  }
234
322
  const item = /^\s*(?:([-*+])|(\d+)[.)])\s+(.*)$/.exec(line);
@@ -237,10 +325,12 @@ function renderMarkdown2(target, markdown, cite) {
237
325
  const tag = item[1] ? "ul" : "ol";
238
326
  if (!list || list.tagName.toLowerCase() !== tag) {
239
327
  list = doc.createElement(tag);
328
+ if (tag === "ol" && item[2] && item[2] !== "1")
329
+ list.setAttribute("start", item[2]);
240
330
  frag.appendChild(list);
241
331
  }
242
332
  const li = doc.createElement("li");
243
- inline(doc, item[3] ?? "", cite, li);
333
+ inline(doc, (item[3] ?? "").trimEnd(), cite, li);
244
334
  list.appendChild(li);
245
335
  continue;
246
336
  }
@@ -257,33 +347,41 @@ function renderMarkdown2(target, markdown, cite) {
257
347
  }
258
348
  if (!line.trim()) {
259
349
  flushPara();
260
- closeList();
350
+ const next = lines.slice(i + 1).find((l) => l.trim()) ?? "";
351
+ const same = list?.tagName.toLowerCase() === "ol" ? /^\s*\d+[.)]\s+/ : /^\s*[-*+]\s+/;
352
+ if (!list || !same.test(next))
353
+ closeList();
261
354
  continue;
262
355
  }
263
356
  if (list && /^\s{2,}\S/.test(line)) {
264
357
  const li = list.lastElementChild;
265
358
  if (li) {
266
- li.appendChild(doc.createTextNode(" "));
359
+ li.appendChild(lineBreak(doc, lines[i - 1] ?? ""));
267
360
  inline(doc, line.trim(), cite, li);
268
361
  continue;
269
362
  }
270
363
  }
271
364
  closeList();
272
- para.push(line.trim());
365
+ para.push(line);
273
366
  }
274
367
  flushPara();
275
368
  target.replaceChildren(frag);
276
369
  }
277
370
 
278
371
  // src/widget/session.ts
372
+ var LOCAL_SEARCH = { limit: 4, budgetTokens: 1500 };
373
+
279
374
  class ChatSession {
280
375
  options;
281
376
  turns = [];
282
377
  engine = null;
283
378
  storePromise = null;
284
379
  historyKey;
380
+ key;
381
+ onProgress = null;
285
382
  constructor(options) {
286
383
  this.options = options;
384
+ this.key = options.storageKey ?? "murmur";
287
385
  this.historyKey = `${options.storageKey ?? "murmur"}-chat:${location.pathname.split("/")[1] ?? ""}`;
288
386
  try {
289
387
  const saved = sessionStorage.getItem(this.historyKey);
@@ -296,8 +394,29 @@ class ChatSession {
296
394
  get mode() {
297
395
  if (this.options.model)
298
396
  return "custom";
397
+ if (this.options.local || this.visitorChoseLocal)
398
+ return "local";
299
399
  return this.options.llm?.endpoint ? "proxy" : "visitor";
300
400
  }
401
+ async localSpec() {
402
+ const { local } = this.options;
403
+ if (local && typeof local === "object")
404
+ return local;
405
+ return (await (__preload("nb4hdy2t"), import("../local/index.js"))).DEFAULT_LOCAL_MODEL;
406
+ }
407
+ get visitorChoseLocal() {
408
+ return readLocal(`${this.key}-local`) === "1";
409
+ }
410
+ useLocal(on) {
411
+ writeLocal(`${this.key}-local`, on ? "1" : null);
412
+ this.engine = null;
413
+ }
414
+ get needsDownloadConsent() {
415
+ return this.mode === "local" && readLocal(`${this.key}-local-consent`) !== "1";
416
+ }
417
+ agreeToDownload() {
418
+ writeLocal(`${this.key}-local-consent`, "1");
419
+ }
301
420
  persist() {
302
421
  try {
303
422
  sessionStorage.setItem(this.historyKey, JSON.stringify(this.turns.slice(-30)));
@@ -327,6 +446,9 @@ class ChatSession {
327
446
  async needsSetup() {
328
447
  return this.mode === "visitor" && !await this.settings();
329
448
  }
449
+ async canRunLocally() {
450
+ return (await (__preload("nb4hdy2t"), import("../local/index.js"))).hasWebGPU();
451
+ }
330
452
  async loadIndex() {
331
453
  const { index } = this.options;
332
454
  if (index && typeof index !== "string")
@@ -341,23 +463,34 @@ class ChatSession {
341
463
  throw new Error("murmur: not a murmur index");
342
464
  return data;
343
465
  }
344
- const { indexDocument } = await (__preload("hp2p9tp9"), import("../indexer/index.js"));
466
+ const { indexDocument } = await (__preload("dj4321kh"), import("../indexer/index.js"));
345
467
  return indexDocument(document, location.pathname + location.search);
346
468
  }
347
469
  createEngine() {
348
470
  return (async () => {
349
471
  const [{ createMurmur }, retriever] = await Promise.all([
350
- (__preload("wq1d15n7"), import("./murmur-wq1d15n7.js")),
351
- this.options.retriever ?? (__preload("1rw4z2n6"), import("./retriever-1rw4z2n6.js")).then(async (r) => r.bm25Retriever(await this.loadIndex()))
472
+ (__preload("t36hd4vt"), import("./murmur-t36hd4vt.js")),
473
+ this.options.retriever ?? (__preload("wxshtnm0"), import("./retriever-wxshtnm0.js")).then(async (r) => r.bm25Retriever(await this.loadIndex()))
352
474
  ]);
353
- const model = this.options.model ?? (async () => {
475
+ const local = this.mode === "local";
476
+ const model = this.options.model ?? (local ? async () => {
477
+ const { localModel } = await (__preload("nb4hdy2t"), import("../local/index.js"));
478
+ return localModel({
479
+ spec: await this.localSpec(),
480
+ onProgress: (p) => this.onProgress?.(p)
481
+ });
482
+ } : async () => {
354
483
  const settings = await this.settings();
355
484
  if (!settings)
356
485
  throw new SetupNeeded2;
357
486
  const { loadModel } = await (__preload("vb9smwta"), import("../llm/index.js"));
358
487
  return loadModel(settings);
359
488
  });
360
- const opts = { retriever, model };
489
+ const opts = {
490
+ retriever,
491
+ model,
492
+ ...local ? LOCAL_SEARCH : {}
493
+ };
361
494
  if (this.options.instructions)
362
495
  opts.instructions = this.options.instructions;
363
496
  if (this.options.lang)
@@ -388,7 +521,9 @@ class ChatSession {
388
521
  turn.content += part;
389
522
  onText(turn.content, turn);
390
523
  }
391
- turn.cited = (await answer.done).cited;
524
+ const done = await answer.done;
525
+ turn.content = done.text;
526
+ turn.cited = done.cited;
392
527
  } catch (error) {
393
528
  if (signal.aborted) {
394
529
  turn.cited = [];
@@ -404,9 +539,12 @@ class ChatSession {
404
539
  return turn;
405
540
  }
406
541
  async describe(error) {
542
+ if (error instanceof Error && error.name === "LocalModelError") {
543
+ return `local-${error.code ?? "load"}|${error.message}`;
544
+ }
407
545
  try {
408
546
  const { describeError } = await (__preload("vb9smwta"), import("../llm/index.js"));
409
- const settings = this.mode === "custom" ? null : await this.settings();
547
+ const settings = this.mode === "custom" || this.mode === "local" ? null : await this.settings();
410
548
  const info = describeError(error, settings?.apiKey);
411
549
  return `${info.code}|${info.message}`;
412
550
  } catch {
@@ -414,6 +552,21 @@ class ChatSession {
414
552
  }
415
553
  }
416
554
  }
555
+ function readLocal(key) {
556
+ try {
557
+ return localStorage.getItem(key);
558
+ } catch {
559
+ return null;
560
+ }
561
+ }
562
+ function writeLocal(key, value) {
563
+ try {
564
+ if (value === null)
565
+ localStorage.removeItem(key);
566
+ else
567
+ localStorage.setItem(key, value);
568
+ } catch {}
569
+ }
417
570
 
418
571
  class SetupNeeded2 extends Error {
419
572
  constructor() {
@@ -528,6 +681,13 @@ header .icon-btn:focus-visible { outline-color: var(--accent-ink); }
528
681
  .msg code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.88em; background: var(--raised); padding: 0.1em 0.35em; border-radius: 5px; }
529
682
  .msg pre { margin: 0 0 0.6em; padding: 10px 12px; background: var(--raised); border-radius: 10px; overflow-x: auto; }
530
683
  .msg pre code { background: none; padding: 0; }
684
+ .msg .table { margin: 0 0 0.6em; overflow-x: auto; border-radius: 10px; box-shadow: inset 0 0 0 1px var(--line); }
685
+ .msg table { width: 100%; border-collapse: collapse; font-size: 14px; }
686
+ .msg th, .msg td { padding: 6px 10px; text-align: left; vertical-align: top; border-bottom: 1px solid var(--line); }
687
+ .msg tr:last-child td { border-bottom: 0; }
688
+ .msg th { font-weight: 650; background: var(--raised); }
689
+ .msg blockquote { margin: 0 0 0.6em; padding: 2px 0 2px 12px; color: var(--muted); box-shadow: inset 1px 0 0 var(--line); }
690
+ .msg hr { margin: 0.8em 0; border: 0; border-top: 1px solid var(--line); }
531
691
  .user {
532
692
  align-self: flex-end; max-width: 85%;
533
693
  padding: 9px 14px; border-radius: 16px 16px 4px 16px;
@@ -614,6 +774,17 @@ footer a:hover { text-decoration: underline; }
614
774
  .result.ok { color: inherit; }
615
775
  .result.ok::before { content: ""; display: inline-block; width: 8px; height: 8px; margin-right: 8px; border-radius: 50%; background: var(--lamp); }
616
776
  .result.fail { color: var(--danger); }
777
+ .local-choice { display: grid; gap: 6px; padding: 14px; border-radius: 12px; background: var(--raised); }
778
+ .local-choice small { color: var(--muted); font-size: 12.5px; }
779
+ .local-choice .link-btn { margin-top: 2px; }
780
+ p.or { margin: 0; color: var(--muted); font-size: 13px; text-align: center; }
781
+ .consent { display: grid; gap: 10px; padding: 14px; border-radius: 12px; background: var(--raised); }
782
+ .consent p { margin: 0; }
783
+ .status .grow { display: grid; gap: 6px; flex: 1; }
784
+ progress { width: 100%; height: 6px; appearance: none; border: 0; border-radius: 3px; background: var(--line); overflow: hidden; }
785
+ progress::-webkit-progress-bar { background: var(--line); }
786
+ progress::-webkit-progress-value { background: var(--lamp); }
787
+ progress::-moz-progress-bar { background: var(--lamp); }
617
788
  .link-btn { padding: 0; border: 0; background: none; color: var(--muted); font-size: 13px; text-decoration: underline; text-underline-offset: 0.2em; justify-self: start; }
618
789
 
619
790
  .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@@ -628,8 +799,17 @@ var ERROR_KEYS = {
628
799
  auth: "errorAuth",
629
800
  "auth-or-network": "errorNetwork",
630
801
  "rate-limit": "errorRate",
631
- overloaded: "errorRate"
802
+ overloaded: "errorRate",
803
+ "local-webgpu": "localNoWebGPU",
804
+ "local-load": "localLoadFailed"
632
805
  };
806
+ function size(megabytes, lang, t) {
807
+ if (megabytes >= 1000) {
808
+ const n = (megabytes / 1000).toLocaleString(lang, { maximumFractionDigits: 1 });
809
+ return `${n} ${t.gigabytes}`;
810
+ }
811
+ return `${Math.round(megabytes).toLocaleString(lang)} ${t.megabytes}`;
812
+ }
633
813
  var Base = typeof HTMLElement === "undefined" ? class {
634
814
  } : HTMLElement;
635
815
 
@@ -637,6 +817,7 @@ class MurmurChat2 extends Base {
637
817
  options = {};
638
818
  session = null;
639
819
  t = messagesFor2("en");
820
+ uiLang = "en";
640
821
  root;
641
822
  launcher;
642
823
  panel;
@@ -698,6 +879,7 @@ class MurmurChat2 extends Base {
698
879
  build() {
699
880
  const o = this.options;
700
881
  const lang = pickLanguage2(o.lang, document.documentElement.lang, navigator.language);
882
+ this.uiLang = lang;
701
883
  const overrides = { ...o.messages };
702
884
  this.t = messagesFor2(lang, overrides);
703
885
  if (o.position)
@@ -736,7 +918,7 @@ class MurmurChat2 extends Base {
736
918
  this.input.focus();
737
919
  });
738
920
  this.settingsBtn = iconButton(doc, "settings", this.t.settings);
739
- this.settingsBtn.hidden = !!o.model || !!o.llm?.endpoint;
921
+ this.settingsBtn.hidden = !!o.model || !!o.llm?.endpoint || !!o.local;
740
922
  this.settingsBtn.addEventListener("click", () => void this.showSetup());
741
923
  const closeBtn = iconButton(doc, "close", this.t.close);
742
924
  closeBtn.hidden = this.hasAttribute("inline");
@@ -861,7 +1043,7 @@ class MurmurChat2 extends Base {
861
1043
  }
862
1044
  const passages = turn.passages ?? [];
863
1045
  const body = el(doc, "div");
864
- renderMarkdown2(body, turn.content, (n) => {
1046
+ renderMarkdown2(body, cleanAnswer2(turn.content), (n) => {
865
1047
  const p = passages[n - 1];
866
1048
  if (!p)
867
1049
  return null;
@@ -904,6 +1086,10 @@ class MurmurChat2 extends Base {
904
1086
  const question = this.input.value.trim();
905
1087
  if (!question || this.controller || !this.session)
906
1088
  return;
1089
+ if (this.session.needsDownloadConsent) {
1090
+ await this.showConsent(question);
1091
+ return;
1092
+ }
907
1093
  if (await this.session.needsSetup()) {
908
1094
  await this.showSetup(question);
909
1095
  return;
@@ -919,6 +1105,15 @@ class MurmurChat2 extends Base {
919
1105
  this.log.append(node);
920
1106
  this.log.scrollTop = this.log.scrollHeight;
921
1107
  this.log.setAttribute("aria-busy", "true");
1108
+ this.session.onProgress = (p) => {
1109
+ if (!status.isConnected)
1110
+ return;
1111
+ const bar = el(doc, "progress", { max: "1", value: String(p.progress) });
1112
+ bar.setAttribute("aria-label", this.t.localLoadingLabel);
1113
+ const label = p.progress >= 1 ? this.t.localPreparing : `${this.t.localLoading} ${Math.round(p.progress * 100)} % · ${Math.round(p.loaded / 1e6).toLocaleString(this.uiLang)} / ${size(p.total / 1e6, this.uiLang, this.t)}`;
1114
+ status.replaceChildren(el(doc, "span", { class: "lamp busy", "aria-hidden": "true" }), el(doc, "span", { class: "grow" }));
1115
+ status.lastElementChild?.append(doc.createTextNode(label), bar);
1116
+ };
922
1117
  const controller = new AbortController;
923
1118
  this.controller = controller;
924
1119
  this.setSendMode("stop");
@@ -952,6 +1147,52 @@ class MurmurChat2 extends Base {
952
1147
  this.log.scrollTop = this.log.scrollHeight;
953
1148
  }
954
1149
  }
1150
+ async showConsent(question) {
1151
+ if (!this.session)
1152
+ return;
1153
+ const doc = this.ownerDocument;
1154
+ const t = this.t;
1155
+ const session = this.session;
1156
+ const spec = await session.localSpec();
1157
+ const card = el(doc, "div", { class: "msg bot consent" });
1158
+ const row = el(doc, "div", { class: "row" });
1159
+ const ownerLocal = !!this.options.local;
1160
+ const useKey = () => {
1161
+ card.remove();
1162
+ session.useLocal(false);
1163
+ this.showSetup(question);
1164
+ };
1165
+ if (!await session.canRunLocally()) {
1166
+ card.append(text(doc, "p", t.localNoWebGPU));
1167
+ if (!ownerLocal) {
1168
+ const key = el(doc, "button", { class: "text-btn", type: "button" });
1169
+ key.textContent = t.useKey;
1170
+ key.addEventListener("click", useKey);
1171
+ row.append(key);
1172
+ }
1173
+ } else {
1174
+ card.append(text(doc, "p", t.localConsent.replace("{name}", spec.name).replace("{size}", size(spec.sizeMB, this.uiLang, t))));
1175
+ const go = el(doc, "button", { class: "primary", type: "button" });
1176
+ go.textContent = t.localDownload;
1177
+ go.addEventListener("click", () => {
1178
+ session.agreeToDownload();
1179
+ card.remove();
1180
+ this.input.value = question;
1181
+ this.submit();
1182
+ });
1183
+ row.append(go);
1184
+ if (!ownerLocal) {
1185
+ const key = el(doc, "button", { class: "link-btn", type: "button" });
1186
+ key.textContent = t.useKey;
1187
+ key.addEventListener("click", useKey);
1188
+ row.append(key);
1189
+ }
1190
+ }
1191
+ card.append(row);
1192
+ this.log.append(card);
1193
+ this.log.scrollTop = this.log.scrollHeight;
1194
+ row.querySelector("button")?.focus();
1195
+ }
955
1196
  async showSetup(pending) {
956
1197
  if (!this.session)
957
1198
  return;
@@ -968,6 +1209,40 @@ class MurmurChat2 extends Base {
968
1209
  const heading = text(doc, "h3", t.setupTitle);
969
1210
  heading.style.cssText = "margin:0;font-size:17px;font-weight:650";
970
1211
  const intro = text(doc, "p", t.setupIntro, "intro");
1212
+ const session = this.session;
1213
+ const spec = await session.localSpec();
1214
+ const inBrowser = el(doc, "div", { class: "local-choice" });
1215
+ const localBtn = el(doc, "button", { class: "primary", type: "button" });
1216
+ localBtn.textContent = t.localOption;
1217
+ const localHint = text(doc, "small", t.localOptionHint.replace("{name}", spec.name).replace("{size}", size(spec.sizeMB, this.uiLang, t)));
1218
+ inBrowser.append(localBtn, localHint);
1219
+ localBtn.addEventListener("click", () => {
1220
+ session.useLocal(true);
1221
+ this.hideSetup();
1222
+ if (pending) {
1223
+ this.input.value = pending;
1224
+ this.submit();
1225
+ }
1226
+ });
1227
+ session.canRunLocally().then((ok) => {
1228
+ localBtn.disabled = !ok;
1229
+ if (!ok)
1230
+ localHint.textContent = t.localNoWebGPU;
1231
+ });
1232
+ if (session.mode === "local") {
1233
+ localBtn.textContent = t.localActive;
1234
+ localBtn.disabled = true;
1235
+ const back = el(doc, "button", { class: "link-btn", type: "button" });
1236
+ back.textContent = t.useKey;
1237
+ back.addEventListener("click", () => {
1238
+ session.useLocal(false);
1239
+ localBtn.textContent = t.localOption;
1240
+ localBtn.disabled = false;
1241
+ back.remove();
1242
+ });
1243
+ inBrowser.append(back);
1244
+ }
1245
+ const or = text(doc, "p", t.orKey, "or");
971
1246
  const local = checkbox(doc, t.local);
972
1247
  const providerField = field(doc, t.provider, el(doc, "select"));
973
1248
  const providerSelect = providerField.control;
@@ -1014,6 +1289,7 @@ class MurmurChat2 extends Base {
1014
1289
  const saved = await store.save();
1015
1290
  if (!saved.ok)
1016
1291
  return;
1292
+ session.useLocal(false);
1017
1293
  this.hideSetup();
1018
1294
  if (pending) {
1019
1295
  this.input.value = pending;
@@ -1023,7 +1299,7 @@ class MurmurChat2 extends Base {
1023
1299
  forget.addEventListener("click", () => void store.clear());
1024
1300
  const testRow = el(doc, "div", { class: "row" });
1025
1301
  testRow.append(testBtn, result);
1026
- this.setupView.replaceChildren(back, heading, intro, local.label, providerField.label, modelField.label, presets, endpointField.label, customModelField.label, keyField.label, remember.label, testRow, saveBtn, forget);
1302
+ this.setupView.replaceChildren(back, heading, intro, inBrowser, or, local.label, providerField.label, modelField.label, presets, endpointField.label, customModelField.label, keyField.label, remember.label, testRow, saveBtn, forget);
1027
1303
  const draw = (s) => {
1028
1304
  const custom = s.draft.custom;
1029
1305
  local.input.checked = custom;
@@ -1055,7 +1331,8 @@ class MurmurChat2 extends Base {
1055
1331
  };
1056
1332
  this.unsubscribe?.();
1057
1333
  this.unsubscribe = store.subscribe(draw);
1058
- (store.getState().draft.custom ? endpointInput : providerSelect).focus();
1334
+ this.setupView.scrollTop = 0;
1335
+ back.focus();
1059
1336
  }
1060
1337
  hideSetup() {
1061
1338
  this.unsubscribe?.();
@@ -22,11 +22,23 @@ function groundingRules2(language) {
22
22
  "Cite the sources you use with their number in square brackets, like [1] or [2][3], right after the sentence they support.",
23
23
  "If the sources do not contain the answer, say so plainly and suggest where on the site to look; never invent facts, links or prices.",
24
24
  "Be concise: a few short paragraphs or a short list. Use Markdown for emphasis, lists and code.",
25
+ "Do not end with a list of sources: the interface shows them under your answer.",
25
26
  language ? `Write in the language whose code is "${language}" unless the visitor writes in another language.` : "Write in the visitor's language.",
26
27
  SOURCES_RULE2
27
28
  ].join(`
28
29
  `);
29
30
  }
31
+ function cleanAnswer2(text) {
32
+ return text.replace(/<think>[\s\S]*?(<\/think>|$)/gi, "").replace(/[ \t]*<<<\s*(?:END\s+)?SOURCE\b[^>\n]*>{0,3}\.?/gi, "").replace(/<{1,3}(?:\s*E(?:N(?:D)?)?)?\s*(?:S(?:O(?:U(?:R(?:C(?:E)?)?)?)?)?)?$/i, (m) => m.startsWith("<<") || /[A-Z]/i.test(m) ? "" : m).replace(/【\s*(\d{1,2})\s*】/g, "[$1]").replace(/\[\s*(?:sources?|src|réf\.?|ref\.?)\s*(\d{1,2})\s*\]/gi, "[$1]").replace(/\[\s*(\d{1,2})\s*[-–]\s*(\d{1,2})\s*\]/g, (all, a, b) => {
33
+ const from = Number(a);
34
+ const to = Number(b);
35
+ if (to <= from || to - from > 8)
36
+ return all;
37
+ return Array.from({ length: to - from + 1 }, (_, i) => `[${from + i}]`).join("");
38
+ }).replace(/\[\s*(\d{1,2}(?:\s*[,;]\s*\d{1,2})+)\s*\]/g, (_, list) => list.split(/[,;]/).map((n) => `[${n.trim()}]`).join("")).replace(/\n+\s*\**\s*(?:sources?|références?|references?)\s*:?\s*\**\s*:?\s*(?:\[?\d{1,2}\]?[\s,;.]*)+$/i, "").replace(/\n{3,}/g, `
39
+
40
+ `).trimEnd();
41
+ }
30
42
  function citedSources2(answer, count) {
31
43
  const out = [];
32
44
  for (const m of answer.matchAll(/\[(\d{1,2})\]/g)) {
@@ -43,4 +55,4 @@ function passageHref2(p) {
43
55
  return `${base}#${encodeURIComponent(p.anchor)}`;
44
56
  }
45
57
 
46
- export { estimateTokens2, formatSources2, SOURCES_RULE2, groundingRules2, citedSources2, passageHref2 };
58
+ export { estimateTokens2, formatSources2, SOURCES_RULE2, groundingRules2, cleanAnswer2, citedSources2, passageHref2 };