murmur-rag 1.0.0 → 1.2.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 +27 -23
  2. package/dist/cdn/anthropic-ynz7717b.js +2 -0
  3. package/dist/cdn/chunk-hpx6rpea.js +1 -0
  4. package/dist/cdn/{embed-59r4jck6.js → embed-0m8xctqy.js} +3 -3
  5. package/dist/cdn/{embed-pnm9593k.js → embed-7pk91zq8.js} +2 -2
  6. package/dist/cdn/embed-dbkjhg16.js +83 -0
  7. package/dist/cdn/{embed-q6y6vgv1.js → embed-krrjjyf5.js} +4 -4
  8. package/dist/cdn/embed-n5nzhrf2.js +9 -0
  9. package/dist/cdn/{embed-hn0r8fmg.js → embed-rp9gb1qq.js} +2 -2
  10. package/dist/cdn/embed-xtkxyrpe.js +44 -0
  11. package/dist/cdn/google-k5kwa54v.js +14 -0
  12. package/dist/cdn/index-db4ne93m.js +1 -0
  13. package/dist/cdn/index-gyd02jvt.js +9 -0
  14. package/dist/cdn/index-qc8kgsdt.js +1 -0
  15. package/dist/cdn/index-t0qn2m4r.js +1 -0
  16. package/dist/cdn/index-y0faj89r.js +1 -0
  17. package/dist/cdn/mistral-p2qcnn42.js +3 -0
  18. package/dist/cdn/murmur-d6t6cjc7.js +5 -0
  19. package/dist/cdn/murmur.js +24 -5
  20. package/dist/cdn/openai-compatible-psnkrpxy.js +11 -0
  21. package/dist/cdn/openai-d3q1d6cz.js +18 -0
  22. package/dist/cdn/retriever-6qvn2maq.js +1 -0
  23. package/dist/cdn/transformers.web-xn16v7pz.js +46 -0
  24. package/dist/cli.js +2 -0
  25. package/dist/lib/chunks/{index-ynjdey8j.js → index-8h9c8ekq.js} +4 -2
  26. package/dist/lib/chunks/{index-6re28wc1.js → index-b77xqap3.js} +13 -1
  27. package/dist/lib/chunks/{index-10g918kt.js → index-p86sy3y4.js} +1 -1
  28. package/dist/lib/chunks/{index-4at214g0.js → index-y3v46yjp.js} +314 -27
  29. package/dist/lib/chunks/{murmur-c1c79eh8.js → murmur-t36hd4vt.js} +2 -2
  30. package/dist/lib/chunks/{retriever-h8nz6jsc.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 +2 -0
  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 +32 -1
  43. package/dist/types/widget/styles.d.ts +1 -1
  44. package/package.json +17 -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-hv290nbv.js +0 -9
  54. package/dist/cdn/index-kk9hgxdc.js +0 -1
  55. package/dist/cdn/mistral-16bekg8b.js +0 -3
  56. package/dist/cdn/murmur-bnm335nb.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-m8peqj5t.js +0 -1
package/dist/cli.js CHANGED
@@ -127,6 +127,8 @@ function extractSections(doc, { root = DEFAULT_ROOT, exclude = DEFAULT_EXCLUDE }
127
127
  if (tag === "BR")
128
128
  cur.text += `
129
129
  `;
130
+ if ((tag === "TD" || tag === "TH") && !/(^|\n)\s*$/.test(cur.text))
131
+ cur.text += " | ";
130
132
  const block = BLOCK.test(tag);
131
133
  if (block)
132
134
  cur.text += `
@@ -1,12 +1,13 @@
1
1
  import {
2
2
  bm25Retriever2
3
- } from "./index-10g918kt.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);
@@ -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 };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  estimateTokens2
3
- } from "./index-6re28wc1.js";
3
+ } from "./index-b77xqap3.js";
4
4
 
5
5
  // src/rag/retriever.ts
6
6
  import MiniSearch from "minisearch";
@@ -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,7 +394,38 @@ class ChatSession {
296
394
  get mode() {
297
395
  if (this.options.model)
298
396
  return "custom";
299
- return this.options.llm?.endpoint ? "proxy" : "visitor";
397
+ if (this.options.llm?.endpoint)
398
+ return "proxy";
399
+ if (!this.localAllowed)
400
+ return "visitor";
401
+ return readLocal(`${this.key}-local`) === "0" ? "visitor" : "local";
402
+ }
403
+ get localAllowed() {
404
+ return !this.options.model && !this.options.llm?.endpoint && this.options.local !== false;
405
+ }
406
+ async settleChoice() {
407
+ if (!this.localAllowed || readLocal(`${this.key}-local`) !== null)
408
+ return;
409
+ const [{ isConfigured }, store] = await Promise.all([(__preload("vb9smwta"), import("../llm/index.js")), this.store()]);
410
+ const saved = await store.load();
411
+ if (saved && isConfigured(saved))
412
+ writeLocal(`${this.key}-local`, "0");
413
+ }
414
+ async localSpec() {
415
+ const { local } = this.options;
416
+ if (local && typeof local === "object")
417
+ return local;
418
+ return (await (__preload("nb4hdy2t"), import("../local/index.js"))).DEFAULT_LOCAL_MODEL;
419
+ }
420
+ useLocal(on) {
421
+ writeLocal(`${this.key}-local`, on ? "1" : "0");
422
+ this.engine = null;
423
+ }
424
+ get needsDownloadConsent() {
425
+ return this.mode === "local" && readLocal(`${this.key}-local-consent`) !== "1";
426
+ }
427
+ agreeToDownload() {
428
+ writeLocal(`${this.key}-local-consent`, "1");
300
429
  }
301
430
  persist() {
302
431
  try {
@@ -327,6 +456,9 @@ class ChatSession {
327
456
  async needsSetup() {
328
457
  return this.mode === "visitor" && !await this.settings();
329
458
  }
459
+ async canRunLocally() {
460
+ return (await (__preload("nb4hdy2t"), import("../local/index.js"))).hasWebGPU();
461
+ }
330
462
  async loadIndex() {
331
463
  const { index } = this.options;
332
464
  if (index && typeof index !== "string")
@@ -341,23 +473,34 @@ class ChatSession {
341
473
  throw new Error("murmur: not a murmur index");
342
474
  return data;
343
475
  }
344
- const { indexDocument } = await (__preload("gp7czjd1"), import("../indexer/index.js"));
476
+ const { indexDocument } = await (__preload("dj4321kh"), import("../indexer/index.js"));
345
477
  return indexDocument(document, location.pathname + location.search);
346
478
  }
347
479
  createEngine() {
348
480
  return (async () => {
349
481
  const [{ createMurmur }, retriever] = await Promise.all([
350
- (__preload("c1c79eh8"), import("./murmur-c1c79eh8.js")),
351
- this.options.retriever ?? (__preload("h8nz6jsc"), import("./retriever-h8nz6jsc.js")).then(async (r) => r.bm25Retriever(await this.loadIndex()))
482
+ (__preload("t36hd4vt"), import("./murmur-t36hd4vt.js")),
483
+ this.options.retriever ?? (__preload("wxshtnm0"), import("./retriever-wxshtnm0.js")).then(async (r) => r.bm25Retriever(await this.loadIndex()))
352
484
  ]);
353
- const model = this.options.model ?? (async () => {
485
+ const local = this.mode === "local";
486
+ const model = this.options.model ?? (local ? async () => {
487
+ const { localModel } = await (__preload("nb4hdy2t"), import("../local/index.js"));
488
+ return localModel({
489
+ spec: await this.localSpec(),
490
+ onProgress: (p) => this.onProgress?.(p)
491
+ });
492
+ } : async () => {
354
493
  const settings = await this.settings();
355
494
  if (!settings)
356
495
  throw new SetupNeeded2;
357
496
  const { loadModel } = await (__preload("vb9smwta"), import("../llm/index.js"));
358
497
  return loadModel(settings);
359
498
  });
360
- const opts = { retriever, model };
499
+ const opts = {
500
+ retriever,
501
+ model,
502
+ ...local ? LOCAL_SEARCH : {}
503
+ };
361
504
  if (this.options.instructions)
362
505
  opts.instructions = this.options.instructions;
363
506
  if (this.options.lang)
@@ -388,7 +531,9 @@ class ChatSession {
388
531
  turn.content += part;
389
532
  onText(turn.content, turn);
390
533
  }
391
- turn.cited = (await answer.done).cited;
534
+ const done = await answer.done;
535
+ turn.content = done.text;
536
+ turn.cited = done.cited;
392
537
  } catch (error) {
393
538
  if (signal.aborted) {
394
539
  turn.cited = [];
@@ -404,9 +549,12 @@ class ChatSession {
404
549
  return turn;
405
550
  }
406
551
  async describe(error) {
552
+ if (error instanceof Error && error.name === "LocalModelError") {
553
+ return `local-${error.code ?? "load"}|${error.message}`;
554
+ }
407
555
  try {
408
556
  const { describeError } = await (__preload("vb9smwta"), import("../llm/index.js"));
409
- const settings = this.mode === "custom" ? null : await this.settings();
557
+ const settings = this.mode === "custom" || this.mode === "local" ? null : await this.settings();
410
558
  const info = describeError(error, settings?.apiKey);
411
559
  return `${info.code}|${info.message}`;
412
560
  } catch {
@@ -414,6 +562,21 @@ class ChatSession {
414
562
  }
415
563
  }
416
564
  }
565
+ function readLocal(key) {
566
+ try {
567
+ return localStorage.getItem(key);
568
+ } catch {
569
+ return null;
570
+ }
571
+ }
572
+ function writeLocal(key, value) {
573
+ try {
574
+ if (value === null)
575
+ localStorage.removeItem(key);
576
+ else
577
+ localStorage.setItem(key, value);
578
+ } catch {}
579
+ }
417
580
 
418
581
  class SetupNeeded2 extends Error {
419
582
  constructor() {
@@ -528,6 +691,13 @@ header .icon-btn:focus-visible { outline-color: var(--accent-ink); }
528
691
  .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
692
  .msg pre { margin: 0 0 0.6em; padding: 10px 12px; background: var(--raised); border-radius: 10px; overflow-x: auto; }
530
693
  .msg pre code { background: none; padding: 0; }
694
+ .msg .table { margin: 0 0 0.6em; overflow-x: auto; border-radius: 10px; box-shadow: inset 0 0 0 1px var(--line); }
695
+ .msg table { width: 100%; border-collapse: collapse; font-size: 14px; }
696
+ .msg th, .msg td { padding: 6px 10px; text-align: left; vertical-align: top; border-bottom: 1px solid var(--line); }
697
+ .msg tr:last-child td { border-bottom: 0; }
698
+ .msg th { font-weight: 650; background: var(--raised); }
699
+ .msg blockquote { margin: 0 0 0.6em; padding: 2px 0 2px 12px; color: var(--muted); box-shadow: inset 1px 0 0 var(--line); }
700
+ .msg hr { margin: 0.8em 0; border: 0; border-top: 1px solid var(--line); }
531
701
  .user {
532
702
  align-self: flex-end; max-width: 85%;
533
703
  padding: 9px 14px; border-radius: 16px 16px 4px 16px;
@@ -614,6 +784,17 @@ footer a:hover { text-decoration: underline; }
614
784
  .result.ok { color: inherit; }
615
785
  .result.ok::before { content: ""; display: inline-block; width: 8px; height: 8px; margin-right: 8px; border-radius: 50%; background: var(--lamp); }
616
786
  .result.fail { color: var(--danger); }
787
+ .local-choice { display: grid; gap: 6px; padding: 14px; border-radius: 12px; background: var(--raised); }
788
+ .local-choice small { color: var(--muted); font-size: 12.5px; }
789
+ .local-choice .link-btn { margin-top: 2px; }
790
+ p.or { margin: 0; color: var(--muted); font-size: 13px; text-align: center; }
791
+ .consent { display: grid; gap: 10px; padding: 14px; border-radius: 12px; background: var(--raised); }
792
+ .consent p { margin: 0; }
793
+ .status .grow { display: grid; gap: 6px; flex: 1; }
794
+ progress { width: 100%; height: 6px; appearance: none; border: 0; border-radius: 3px; background: var(--line); overflow: hidden; }
795
+ progress::-webkit-progress-bar { background: var(--line); }
796
+ progress::-webkit-progress-value { background: var(--lamp); }
797
+ progress::-moz-progress-bar { background: var(--lamp); }
617
798
  .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
799
 
619
800
  .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@@ -628,8 +809,17 @@ var ERROR_KEYS = {
628
809
  auth: "errorAuth",
629
810
  "auth-or-network": "errorNetwork",
630
811
  "rate-limit": "errorRate",
631
- overloaded: "errorRate"
812
+ overloaded: "errorRate",
813
+ "local-webgpu": "localNoWebGPU",
814
+ "local-load": "localLoadFailed"
632
815
  };
816
+ function size(megabytes, lang, t) {
817
+ if (megabytes >= 1000) {
818
+ const n = (megabytes / 1000).toLocaleString(lang, { maximumFractionDigits: 1 });
819
+ return `${n} ${t.gigabytes}`;
820
+ }
821
+ return `${Math.round(megabytes).toLocaleString(lang)} ${t.megabytes}`;
822
+ }
633
823
  var Base = typeof HTMLElement === "undefined" ? class {
634
824
  } : HTMLElement;
635
825
 
@@ -637,6 +827,7 @@ class MurmurChat2 extends Base {
637
827
  options = {};
638
828
  session = null;
639
829
  t = messagesFor2("en");
830
+ uiLang = "en";
640
831
  root;
641
832
  launcher;
642
833
  panel;
@@ -698,6 +889,7 @@ class MurmurChat2 extends Base {
698
889
  build() {
699
890
  const o = this.options;
700
891
  const lang = pickLanguage2(o.lang, document.documentElement.lang, navigator.language);
892
+ this.uiLang = lang;
701
893
  const overrides = { ...o.messages };
702
894
  this.t = messagesFor2(lang, overrides);
703
895
  if (o.position)
@@ -861,7 +1053,7 @@ class MurmurChat2 extends Base {
861
1053
  }
862
1054
  const passages = turn.passages ?? [];
863
1055
  const body = el(doc, "div");
864
- renderMarkdown2(body, turn.content, (n) => {
1056
+ renderMarkdown2(body, cleanAnswer2(turn.content), (n) => {
865
1057
  const p = passages[n - 1];
866
1058
  if (!p)
867
1059
  return null;
@@ -904,6 +1096,11 @@ class MurmurChat2 extends Base {
904
1096
  const question = this.input.value.trim();
905
1097
  if (!question || this.controller || !this.session)
906
1098
  return;
1099
+ await this.session.settleChoice();
1100
+ if (this.session.needsDownloadConsent) {
1101
+ await this.showConsent(question);
1102
+ return;
1103
+ }
907
1104
  if (await this.session.needsSetup()) {
908
1105
  await this.showSetup(question);
909
1106
  return;
@@ -919,6 +1116,15 @@ class MurmurChat2 extends Base {
919
1116
  this.log.append(node);
920
1117
  this.log.scrollTop = this.log.scrollHeight;
921
1118
  this.log.setAttribute("aria-busy", "true");
1119
+ this.session.onProgress = (p) => {
1120
+ if (!status.isConnected)
1121
+ return;
1122
+ const bar = el(doc, "progress", { max: "1", value: String(p.progress) });
1123
+ bar.setAttribute("aria-label", this.t.localLoadingLabel);
1124
+ 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)}`;
1125
+ status.replaceChildren(el(doc, "span", { class: "lamp busy", "aria-hidden": "true" }), el(doc, "span", { class: "grow" }));
1126
+ status.lastElementChild?.append(doc.createTextNode(label), bar);
1127
+ };
922
1128
  const controller = new AbortController;
923
1129
  this.controller = controller;
924
1130
  this.setSendMode("stop");
@@ -952,6 +1158,47 @@ class MurmurChat2 extends Base {
952
1158
  this.log.scrollTop = this.log.scrollHeight;
953
1159
  }
954
1160
  }
1161
+ async showConsent(question) {
1162
+ if (!this.session)
1163
+ return;
1164
+ const doc = this.ownerDocument;
1165
+ const t = this.t;
1166
+ const session = this.session;
1167
+ const spec = await session.localSpec();
1168
+ const card = el(doc, "div", { class: "msg bot consent" });
1169
+ const row = el(doc, "div", { class: "row" });
1170
+ const useKey = () => {
1171
+ card.remove();
1172
+ session.useLocal(false);
1173
+ this.showSetup(question);
1174
+ };
1175
+ if (!await session.canRunLocally()) {
1176
+ card.append(text(doc, "p", t.localNoWebGPU));
1177
+ const key = el(doc, "button", { class: "primary", type: "button" });
1178
+ key.textContent = t.useKey;
1179
+ key.addEventListener("click", useKey);
1180
+ row.append(key);
1181
+ } else {
1182
+ card.append(text(doc, "p", t.localConsent.replace("{name}", spec.name).replace("{size}", size(spec.sizeMB, this.uiLang, t))));
1183
+ const go = el(doc, "button", { class: "primary", type: "button" });
1184
+ go.textContent = t.localDownload;
1185
+ go.addEventListener("click", () => {
1186
+ session.agreeToDownload();
1187
+ card.remove();
1188
+ this.input.value = question;
1189
+ this.submit();
1190
+ });
1191
+ row.append(go);
1192
+ const key = el(doc, "button", { class: "link-btn", type: "button" });
1193
+ key.textContent = t.useKey;
1194
+ key.addEventListener("click", useKey);
1195
+ row.append(key);
1196
+ }
1197
+ card.append(row);
1198
+ this.log.append(card);
1199
+ this.log.scrollTop = this.log.scrollHeight;
1200
+ row.querySelector("button")?.focus();
1201
+ }
955
1202
  async showSetup(pending) {
956
1203
  if (!this.session)
957
1204
  return;
@@ -968,6 +1215,44 @@ class MurmurChat2 extends Base {
968
1215
  const heading = text(doc, "h3", t.setupTitle);
969
1216
  heading.style.cssText = "margin:0;font-size:17px;font-weight:650";
970
1217
  const intro = text(doc, "p", t.setupIntro, "intro");
1218
+ const session = this.session;
1219
+ const spec = await session.localSpec();
1220
+ const inBrowser = el(doc, "div", { class: "local-choice" });
1221
+ const localBtn = el(doc, "button", { class: "primary", type: "button" });
1222
+ localBtn.textContent = t.localOption;
1223
+ const localHint = text(doc, "small", t.localOptionHint.replace("{name}", spec.name).replace("{size}", size(spec.sizeMB, this.uiLang, t)));
1224
+ inBrowser.append(localBtn, localHint);
1225
+ localBtn.addEventListener("click", () => {
1226
+ session.useLocal(true);
1227
+ this.hideSetup();
1228
+ if (pending) {
1229
+ this.input.value = pending;
1230
+ this.submit();
1231
+ }
1232
+ });
1233
+ const switchToKey = el(doc, "button", { class: "link-btn", type: "button" });
1234
+ switchToKey.textContent = t.useKey;
1235
+ switchToKey.addEventListener("click", () => {
1236
+ session.useLocal(false);
1237
+ localBtn.textContent = t.localOption;
1238
+ localBtn.disabled = false;
1239
+ switchToKey.remove();
1240
+ });
1241
+ if (session.mode === "local") {
1242
+ localBtn.textContent = t.localActive;
1243
+ localBtn.disabled = true;
1244
+ inBrowser.append(switchToKey);
1245
+ }
1246
+ session.canRunLocally().then((ok) => {
1247
+ if (ok)
1248
+ return;
1249
+ localBtn.textContent = t.localOption;
1250
+ localBtn.disabled = true;
1251
+ localHint.textContent = t.localNoWebGPU;
1252
+ switchToKey.remove();
1253
+ });
1254
+ const or = text(doc, "p", t.orKey, "or");
1255
+ inBrowser.hidden = or.hidden = !session.localAllowed;
971
1256
  const local = checkbox(doc, t.local);
972
1257
  const providerField = field(doc, t.provider, el(doc, "select"));
973
1258
  const providerSelect = providerField.control;
@@ -1014,6 +1299,7 @@ class MurmurChat2 extends Base {
1014
1299
  const saved = await store.save();
1015
1300
  if (!saved.ok)
1016
1301
  return;
1302
+ session.useLocal(false);
1017
1303
  this.hideSetup();
1018
1304
  if (pending) {
1019
1305
  this.input.value = pending;
@@ -1023,7 +1309,7 @@ class MurmurChat2 extends Base {
1023
1309
  forget.addEventListener("click", () => void store.clear());
1024
1310
  const testRow = el(doc, "div", { class: "row" });
1025
1311
  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);
1312
+ 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
1313
  const draw = (s) => {
1028
1314
  const custom = s.draft.custom;
1029
1315
  local.input.checked = custom;
@@ -1055,7 +1341,8 @@ class MurmurChat2 extends Base {
1055
1341
  };
1056
1342
  this.unsubscribe?.();
1057
1343
  this.unsubscribe = store.subscribe(draw);
1058
- (store.getState().draft.custom ? endpointInput : providerSelect).focus();
1344
+ this.setupView.scrollTop = 0;
1345
+ back.focus();
1059
1346
  }
1060
1347
  hideSetup() {
1061
1348
  this.unsubscribe?.();
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  searchQuery2,
3
3
  createMurmur2
4
- } from "./index-ynjdey8j.js";
5
- import"./index-10g918kt.js";
4
+ } from "./index-8h9c8ekq.js";
5
+ import"./index-p86sy3y4.js";
6
6
  export {
7
7
  createMurmur2 as createMurmur,
8
8
  searchQuery2 as searchQuery
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  passagesOf2,
3
3
  bm25Retriever2
4
- } from "./index-10g918kt.js";
4
+ } from "./index-p86sy3y4.js";
5
5
  export {
6
6
  bm25Retriever2 as bm25Retriever,
7
7
  passagesOf2 as passagesOf