murmur-rag 1.2.0 → 1.3.1

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 (43) hide show
  1. package/README.md +27 -16
  2. package/dist/cdn/{anthropic-ynz7717b.js → anthropic-tw416spz.js} +2 -2
  3. package/dist/cdn/chunk-xyp6nm65.js +1 -0
  4. package/dist/cdn/{embed-xtkxyrpe.js → embed-3am76qfd.js} +6 -6
  5. package/dist/cdn/embed-50pfy67v.js +83 -0
  6. package/dist/cdn/{embed-n5nzhrf2.js → embed-cx1r66ch.js} +6 -6
  7. package/dist/cdn/{embed-7pk91zq8.js → embed-g0p6yc29.js} +2 -2
  8. package/dist/cdn/{embed-krrjjyf5.js → embed-q81wdr1z.js} +4 -4
  9. package/dist/cdn/embed-rjpq8psf.js +10 -0
  10. package/dist/cdn/{embed-0m8xctqy.js → embed-yz75savs.js} +3 -3
  11. package/dist/cdn/{google-k5kwa54v.js → google-556tp13p.js} +8 -8
  12. package/dist/cdn/index-8r44b2wt.js +1 -0
  13. package/dist/cdn/index-a5j4aat5.js +1 -0
  14. package/dist/cdn/index-ft8r539a.js +1 -0
  15. package/dist/cdn/index-pjxy71n5.js +1 -0
  16. package/dist/cdn/{index-gyd02jvt.js → index-qpy1k923.js} +2 -2
  17. package/dist/cdn/{mistral-p2qcnn42.js → mistral-25w1nsx5.js} +2 -2
  18. package/dist/cdn/{murmur-d6t6cjc7.js → murmur-1a55et82.js} +3 -3
  19. package/dist/cdn/murmur.js +7 -7
  20. package/dist/cdn/{openai-d3q1d6cz.js → openai-bjrbem49.js} +12 -12
  21. package/dist/cdn/{openai-compatible-psnkrpxy.js → openai-compatible-sg25g0vj.js} +6 -6
  22. package/dist/cdn/retriever-06ybegpd.js +1 -0
  23. package/dist/cdn/{transformers.web-xn16v7pz.js → transformers.web-49m0ht4a.js} +7 -7
  24. package/dist/lib/chunks/{index-y3v46yjp.js → index-4ftk20yq.js} +143 -39
  25. package/dist/lib/embed.js +8 -2
  26. package/dist/lib/llm/index.js +23 -0
  27. package/dist/lib/local/index.js +38 -2
  28. package/dist/lib/widget/index.js +2 -2
  29. package/dist/types/llm/index.d.ts +8 -0
  30. package/dist/types/local/index.d.ts +12 -0
  31. package/dist/types/widget/element.d.ts +4 -0
  32. package/dist/types/widget/i18n.d.ts +1 -0
  33. package/dist/types/widget/session.d.ts +43 -7
  34. package/dist/types/widget/styles.d.ts +1 -1
  35. package/package.json +4 -2
  36. package/dist/cdn/chunk-hpx6rpea.js +0 -1
  37. package/dist/cdn/embed-dbkjhg16.js +0 -83
  38. package/dist/cdn/embed-rp9gb1qq.js +0 -10
  39. package/dist/cdn/index-db4ne93m.js +0 -1
  40. package/dist/cdn/index-qc8kgsdt.js +0 -1
  41. package/dist/cdn/index-t0qn2m4r.js +0 -1
  42. package/dist/cdn/index-y0faj89r.js +0 -1
  43. package/dist/cdn/retriever-6qvn2maq.js +0 -1
@@ -87,6 +87,7 @@ var en = {
87
87
  localPreparing: "Preparing the model on your GPU…",
88
88
  localNoWebGPU: "This browser cannot run the model locally (no WebGPU). Try a recent Chrome, Edge or Safari, or use an API key.",
89
89
  localLoadFailed: "The model could not be loaded in this browser.",
90
+ localNoWebGPUOnly: "This site answers with an AI model that runs in your browser, and this browser cannot run it (no WebGPU). Try a recent Chrome, Edge or Safari.",
90
91
  gigabytes: "GB",
91
92
  megabytes: "MB"
92
93
  };
@@ -143,6 +144,7 @@ var fr = {
143
144
  localPreparing: "Préparation du modèle sur votre GPU…",
144
145
  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
146
  localLoadFailed: "Le modèle n'a pas pu être chargé dans ce navigateur.",
147
+ localNoWebGPUOnly: "Ce site répond avec un modèle d'IA qui tourne dans votre navigateur, et ce navigateur ne peut pas le faire tourner (pas de WebGPU). Essayez un Chrome, Edge ou Safari récent.",
146
148
  gigabytes: "Go",
147
149
  megabytes: "Mo"
148
150
  };
@@ -378,7 +380,8 @@ class ChatSession {
378
380
  storePromise = null;
379
381
  historyKey;
380
382
  key;
381
- onProgress = null;
383
+ local = { status: "idle" };
384
+ localListeners = new Set;
382
385
  constructor(options) {
383
386
  this.options = options;
384
387
  this.key = options.storageKey ?? "murmur";
@@ -391,22 +394,77 @@ class ChatSession {
391
394
  this.turns = [];
392
395
  }
393
396
  }
397
+ get configured() {
398
+ return this.options.mode ?? "auto";
399
+ }
394
400
  get mode() {
401
+ const m = this.configured;
395
402
  if (this.options.model)
396
403
  return "custom";
397
- if (this.options.llm?.endpoint)
404
+ if (m === "proxy" || m === "auto" && this.options.llm?.endpoint)
398
405
  return "proxy";
399
- if (!this.localAllowed)
406
+ if ((m === "auto" || m === "key") && this.options.llm?.apiKey)
407
+ return "owner";
408
+ if (m === "local")
409
+ return "local";
410
+ if (m === "key" || !this.localAllowed)
400
411
  return "visitor";
401
412
  return readLocal(`${this.key}-local`) === "0" ? "visitor" : "local";
402
413
  }
403
414
  get localAllowed() {
404
- return !this.options.model && !this.options.llm?.endpoint && this.options.local !== false;
415
+ const m = this.configured;
416
+ if (this.options.model || m === "proxy" || m === "key")
417
+ return false;
418
+ if (m === "local")
419
+ return true;
420
+ return !this.options.llm?.endpoint && !this.options.llm?.apiKey && this.options.local !== false;
421
+ }
422
+ get keyAllowed() {
423
+ const m = this.configured;
424
+ if (this.options.model || m === "proxy" || m === "local")
425
+ return false;
426
+ return !this.options.llm?.endpoint && !this.options.llm?.apiKey;
427
+ }
428
+ get localState() {
429
+ return this.local;
430
+ }
431
+ onLocalState(listener) {
432
+ this.localListeners.add(listener);
433
+ return () => this.localListeners.delete(listener);
434
+ }
435
+ setLocal(state) {
436
+ this.local = state;
437
+ for (const listener of this.localListeners)
438
+ listener(state);
439
+ }
440
+ async warmUp() {
441
+ if (this.mode !== "local" || this.local.status !== "idle")
442
+ return;
443
+ if (!await this.canRunLocally())
444
+ return;
445
+ const { loadLocalModel } = await (__preload("qfx1tgz2"), import("../local/index.js"));
446
+ this.setLocal({ status: "loading" });
447
+ try {
448
+ await loadLocalModel(await this.localSpec(), (progress) => this.setLocal({ status: "loading", progress }));
449
+ this.setLocal({ status: "ready" });
450
+ } catch {
451
+ this.setLocal({ status: "error" });
452
+ }
453
+ }
454
+ async warmUpIfCached() {
455
+ if (this.mode !== "local" || this.preload === "consent")
456
+ return;
457
+ const { isLocalModelCached } = await (__preload("qfx1tgz2"), import("../local/index.js"));
458
+ if (await isLocalModelCached(await this.localSpec()))
459
+ await this.warmUp();
460
+ }
461
+ get preload() {
462
+ return this.options.preload ?? "question";
405
463
  }
406
464
  async settleChoice() {
407
- if (!this.localAllowed || readLocal(`${this.key}-local`) !== null)
465
+ if (!this.localAllowed || !this.keyAllowed || readLocal(`${this.key}-local`) !== null)
408
466
  return;
409
- const [{ isConfigured }, store] = await Promise.all([(__preload("vb9smwta"), import("../llm/index.js")), this.store()]);
467
+ const [{ isConfigured }, store] = await Promise.all([(__preload("xreqcp1t"), import("../llm/index.js")), this.store()]);
410
468
  const saved = await store.load();
411
469
  if (saved && isConfigured(saved))
412
470
  writeLocal(`${this.key}-local`, "0");
@@ -415,14 +473,14 @@ class ChatSession {
415
473
  const { local } = this.options;
416
474
  if (local && typeof local === "object")
417
475
  return local;
418
- return (await (__preload("nb4hdy2t"), import("../local/index.js"))).DEFAULT_LOCAL_MODEL;
476
+ return (await (__preload("qfx1tgz2"), import("../local/index.js"))).DEFAULT_LOCAL_MODEL;
419
477
  }
420
478
  useLocal(on) {
421
479
  writeLocal(`${this.key}-local`, on ? "1" : "0");
422
480
  this.engine = null;
423
481
  }
424
482
  get needsDownloadConsent() {
425
- return this.mode === "local" && readLocal(`${this.key}-local-consent`) !== "1";
483
+ return this.mode === "local" && this.preload === "consent" && this.local.status !== "ready" && readLocal(`${this.key}-local-consent`) !== "1";
426
484
  }
427
485
  agreeToDownload() {
428
486
  writeLocal(`${this.key}-local-consent`, "1");
@@ -437,7 +495,7 @@ class ChatSession {
437
495
  this.persist();
438
496
  }
439
497
  store() {
440
- this.storePromise ??= (__preload("vb9smwta"), import("../llm/index.js")).then((llm) => {
498
+ this.storePromise ??= (__preload("xreqcp1t"), import("../llm/index.js")).then((llm) => {
441
499
  const opts = {};
442
500
  if (this.options.catalogUrl)
443
501
  opts.catalogUrl = this.options.catalogUrl;
@@ -448,16 +506,22 @@ class ChatSession {
448
506
  return this.storePromise;
449
507
  }
450
508
  async settings() {
451
- if (this.mode === "custom")
509
+ const mode = this.mode;
510
+ if (mode === "custom" || mode === "local")
452
511
  return null;
453
- const llm = await (__preload("vb9smwta"), import("../llm/index.js"));
454
- return llm.resolveSettings(this.options.llm ?? {}, this.mode === "visitor" ? await this.store() : null);
512
+ const llm = await (__preload("xreqcp1t"), import("../llm/index.js"));
513
+ const config = this.options.llm ?? {};
514
+ if (mode === "proxy")
515
+ return llm.proxySettings(config);
516
+ if (mode === "owner")
517
+ return llm.ownerKeySettings(config, this.options.catalogUrl);
518
+ return llm.resolveSettings({ ...config, endpoint: "" }, await this.store());
455
519
  }
456
520
  async needsSetup() {
457
521
  return this.mode === "visitor" && !await this.settings();
458
522
  }
459
523
  async canRunLocally() {
460
- return (await (__preload("nb4hdy2t"), import("../local/index.js"))).hasWebGPU();
524
+ return (await (__preload("qfx1tgz2"), import("../local/index.js"))).hasWebGPU();
461
525
  }
462
526
  async loadIndex() {
463
527
  const { index } = this.options;
@@ -484,16 +548,16 @@ class ChatSession {
484
548
  ]);
485
549
  const local = this.mode === "local";
486
550
  const model = this.options.model ?? (local ? async () => {
487
- const { localModel } = await (__preload("nb4hdy2t"), import("../local/index.js"));
551
+ const { localModel } = await (__preload("qfx1tgz2"), import("../local/index.js"));
488
552
  return localModel({
489
553
  spec: await this.localSpec(),
490
- onProgress: (p) => this.onProgress?.(p)
554
+ onProgress: (progress) => this.setLocal({ status: "loading", progress })
491
555
  });
492
556
  } : async () => {
493
557
  const settings = await this.settings();
494
558
  if (!settings)
495
559
  throw new SetupNeeded2;
496
- const { loadModel } = await (__preload("vb9smwta"), import("../llm/index.js"));
560
+ const { loadModel } = await (__preload("xreqcp1t"), import("../llm/index.js"));
497
561
  return loadModel(settings);
498
562
  });
499
563
  const opts = {
@@ -515,6 +579,8 @@ class ChatSession {
515
579
  this.turns.push(turn);
516
580
  this.persist();
517
581
  try {
582
+ if (this.mode === "local")
583
+ this.warmUp();
518
584
  this.engine ??= this.createEngine();
519
585
  const engine = await this.engine.catch((error) => {
520
586
  this.engine = null;
@@ -532,6 +598,8 @@ class ChatSession {
532
598
  onText(turn.content, turn);
533
599
  }
534
600
  const done = await answer.done;
601
+ if (this.mode === "local")
602
+ this.setLocal({ status: "ready" });
535
603
  turn.content = done.text;
536
604
  turn.cited = done.cited;
537
605
  } catch (error) {
@@ -553,7 +621,7 @@ class ChatSession {
553
621
  return `local-${error.code ?? "load"}|${error.message}`;
554
622
  }
555
623
  try {
556
- const { describeError } = await (__preload("vb9smwta"), import("../llm/index.js"));
624
+ const { describeError } = await (__preload("xreqcp1t"), import("../llm/index.js"));
557
625
  const settings = this.mode === "custom" || this.mode === "local" ? null : await this.settings();
558
626
  const info = describeError(error, settings?.apiKey);
559
627
  return `${info.code}|${info.message}`;
@@ -790,7 +858,7 @@ footer a:hover { text-decoration: underline; }
790
858
  p.or { margin: 0; color: var(--muted); font-size: 13px; text-align: center; }
791
859
  .consent { display: grid; gap: 10px; padding: 14px; border-radius: 12px; background: var(--raised); }
792
860
  .consent p { margin: 0; }
793
- .status .grow { display: grid; gap: 6px; flex: 1; }
861
+ .loadbar { display: grid; gap: 6px; margin: 4px 12px 0; padding: 8px 12px; border-radius: 10px; background: var(--raised); color: var(--muted); font-size: 13px; }
794
862
  progress { width: 100%; height: 6px; appearance: none; border: 0; border-radius: 3px; background: var(--line); overflow: hidden; }
795
863
  progress::-webkit-progress-bar { background: var(--line); }
796
864
  progress::-webkit-progress-value { background: var(--lamp); }
@@ -840,6 +908,8 @@ class MurmurChat2 extends Base {
840
908
  controller = null;
841
909
  built = false;
842
910
  unsubscribe = null;
911
+ unsubscribeLocal = null;
912
+ loadbar;
843
913
  constructor() {
844
914
  super();
845
915
  this.root = this.attachShadow({ mode: "open" });
@@ -859,12 +929,16 @@ class MurmurChat2 extends Base {
859
929
  disconnectedCallback() {
860
930
  this.controller?.abort();
861
931
  this.unsubscribe?.();
932
+ this.unsubscribeLocal?.();
933
+ this.unsubscribeLocal?.();
862
934
  }
863
935
  async open() {
864
936
  if (!this.built)
865
937
  this.build();
866
938
  this.launcher.setAttribute("aria-expanded", "true");
867
939
  this.panel.hidden = false;
940
+ if (this.session?.preload === "open")
941
+ this.session.warmUp();
868
942
  this.session?.persist();
869
943
  tabStorage("set", this.openKey);
870
944
  this.input.focus();
@@ -965,6 +1039,8 @@ class MurmurChat2 extends Base {
965
1039
  this.submit();
966
1040
  });
967
1041
  form.append(this.input, this.sendBtn);
1042
+ this.loadbar = el(doc, "div", { class: "loadbar", role: "status" });
1043
+ this.loadbar.hidden = true;
968
1044
  const footer = el(doc, "footer");
969
1045
  const by = el(doc, "a", {
970
1046
  href: "https://maxgfr.github.io/murmur/",
@@ -973,7 +1049,7 @@ class MurmurChat2 extends Base {
973
1049
  });
974
1050
  by.textContent = this.t.poweredBy;
975
1051
  footer.append(by);
976
- this.chatView.append(this.log, form, footer);
1052
+ this.chatView.append(this.log, this.loadbar, form, footer);
977
1053
  this.setupView = el(doc, "div", { class: "setup" });
978
1054
  this.setupView.hidden = true;
979
1055
  this.panel.append(header, this.chatView, this.setupView);
@@ -981,11 +1057,37 @@ class MurmurChat2 extends Base {
981
1057
  if (this.hasAttribute("inline"))
982
1058
  this.launcher.hidden = true;
983
1059
  this.built = true;
984
- this.session = new ChatSession({ ...o, lang });
1060
+ const session = new ChatSession({ ...o, lang });
1061
+ this.session = session;
1062
+ this.settingsBtn.hidden = !session.keyAllowed;
1063
+ this.unsubscribeLocal = session.onLocalState((state) => this.drawLocal(state));
1064
+ whenIdle(() => {
1065
+ if (session.preload === "idle")
1066
+ session.warmUp();
1067
+ else
1068
+ session.warmUpIfCached();
1069
+ });
985
1070
  this.renderChat();
986
1071
  if (o.open || !this.hasAttribute("inline") && tabStorage("get", this.openKey))
987
1072
  this.open();
988
1073
  }
1074
+ drawLocal(state) {
1075
+ const doc = this.ownerDocument;
1076
+ const lamp = this.launcher.querySelector(".lamp");
1077
+ lamp?.classList.toggle("busy", state.status === "loading");
1078
+ if (state.status !== "loading") {
1079
+ this.loadbar.hidden = true;
1080
+ return;
1081
+ }
1082
+ const p = state.progress;
1083
+ const label = !p ? this.t.localLoading : 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)}`;
1084
+ const bar = el(doc, "progress", { max: "1" });
1085
+ if (p && p.progress < 1)
1086
+ bar.setAttribute("value", String(p.progress));
1087
+ bar.setAttribute("aria-label", this.t.localLoadingLabel);
1088
+ this.loadbar.replaceChildren(text(doc, "span", label), bar);
1089
+ this.loadbar.hidden = false;
1090
+ }
989
1091
  autosize() {
990
1092
  this.input.style.height = "auto";
991
1093
  this.input.style.height = `${Math.min(this.input.scrollHeight, 140)}px`;
@@ -1097,7 +1199,7 @@ class MurmurChat2 extends Base {
1097
1199
  if (!question || this.controller || !this.session)
1098
1200
  return;
1099
1201
  await this.session.settleChoice();
1100
- if (this.session.needsDownloadConsent) {
1202
+ if (this.session.needsDownloadConsent || this.session.mode === "local" && !await this.session.canRunLocally()) {
1101
1203
  await this.showConsent(question);
1102
1204
  return;
1103
1205
  }
@@ -1116,15 +1218,6 @@ class MurmurChat2 extends Base {
1116
1218
  this.log.append(node);
1117
1219
  this.log.scrollTop = this.log.scrollHeight;
1118
1220
  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
- };
1128
1221
  const controller = new AbortController;
1129
1222
  this.controller = controller;
1130
1223
  this.setSendMode("stop");
@@ -1173,11 +1266,13 @@ class MurmurChat2 extends Base {
1173
1266
  this.showSetup(question);
1174
1267
  };
1175
1268
  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);
1269
+ card.append(text(doc, "p", session.keyAllowed ? t.localNoWebGPU : t.localNoWebGPUOnly));
1270
+ if (session.keyAllowed) {
1271
+ const key = el(doc, "button", { class: "primary", type: "button" });
1272
+ key.textContent = t.useKey;
1273
+ key.addEventListener("click", useKey);
1274
+ row.append(key);
1275
+ }
1181
1276
  } else {
1182
1277
  card.append(text(doc, "p", t.localConsent.replace("{name}", spec.name).replace("{size}", size(spec.sizeMB, this.uiLang, t))));
1183
1278
  const go = el(doc, "button", { class: "primary", type: "button" });
@@ -1189,10 +1284,12 @@ class MurmurChat2 extends Base {
1189
1284
  this.submit();
1190
1285
  });
1191
1286
  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);
1287
+ if (session.keyAllowed) {
1288
+ const key = el(doc, "button", { class: "link-btn", type: "button" });
1289
+ key.textContent = t.useKey;
1290
+ key.addEventListener("click", useKey);
1291
+ row.append(key);
1292
+ }
1196
1293
  }
1197
1294
  card.append(row);
1198
1295
  this.log.append(card);
@@ -1389,6 +1486,13 @@ function tabStorage(op, key) {
1389
1486
  } catch {}
1390
1487
  return false;
1391
1488
  }
1489
+ function whenIdle(work) {
1490
+ const idle = globalThis.requestIdleCallback;
1491
+ if (idle)
1492
+ idle(work, { timeout: 4000 });
1493
+ else
1494
+ setTimeout(work, 1500);
1495
+ }
1392
1496
  function el(doc, tag, attrs = {}) {
1393
1497
  const node = doc.createElement(tag);
1394
1498
  for (const [k, v] of Object.entries(attrs))
package/dist/lib/embed.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  __chunks,
3
3
  mountMurmur2
4
- } from "./chunks/index-y3v46yjp.js";
5
- __chunks(import.meta.url,["jc4f9310","y3v46yjp","t36hd4vt","wxshtnm0","dj4321kh","vb9smwta","nb4hdy2t","kaeec41p","b77xqap3","8h9c8ekq","p86sy3y4","1zk822m5"],[["./embed.js",1],["./chunks/index-y3v46yjp.js",8],["./chunks/murmur-t36hd4vt.js",9,10],["./chunks/retriever-wxshtnm0.js",10],["./indexer/index.js",11],["./llm/index.js"],["./local/index.js"],["./chunks/chunk-kaeec41p.js",11],["./chunks/index-b77xqap3.js"],["./chunks/index-8h9c8ekq.js",10,8],["./chunks/index-p86sy3y4.js",8],["./chunks/index-1zk822m5.js"]],0);
4
+ } from "./chunks/index-4ftk20yq.js";
5
+ __chunks(import.meta.url,["n035jphv","4ftk20yq","t36hd4vt","wxshtnm0","dj4321kh","xreqcp1t","qfx1tgz2","kaeec41p","b77xqap3","8h9c8ekq","p86sy3y4","1zk822m5"],[["./embed.js",1],["./chunks/index-4ftk20yq.js",8],["./chunks/murmur-t36hd4vt.js",9,10],["./chunks/retriever-wxshtnm0.js",10],["./indexer/index.js",11],["./llm/index.js"],["./local/index.js"],["./chunks/chunk-kaeec41p.js",11],["./chunks/index-b77xqap3.js"],["./chunks/index-8h9c8ekq.js",10,8],["./chunks/index-p86sy3y4.js",8],["./chunks/index-1zk822m5.js"]],0);
6
6
 
7
7
  // src/embed.ts
8
8
  function optionsFromDataset(data, base) {
@@ -16,6 +16,8 @@ function optionsFromDataset(data, base) {
16
16
  llm.model = data.model;
17
17
  if (data.endpoint)
18
18
  llm.endpoint = data.endpoint;
19
+ if (data.apiKey)
20
+ llm.apiKey = data.apiKey;
19
21
  if (Object.keys(llm).length)
20
22
  o.llm = llm;
21
23
  if (data.instructions)
@@ -47,6 +49,10 @@ function optionsFromDataset(data, base) {
47
49
  o.open = true;
48
50
  if (data.local === "false")
49
51
  o.local = false;
52
+ if (data.mode === "auto" || data.mode === "proxy" || data.mode === "local" || data.mode === "key")
53
+ o.mode = data.mode;
54
+ if (data.preload === "question" || data.preload === "open" || data.preload === "idle" || data.preload === "consent")
55
+ o.preload = data.preload;
50
56
  return o;
51
57
  }
52
58
  function start() {
@@ -2,8 +2,11 @@
2
2
  import {
3
3
  createLlmSettingsStore,
4
4
  DEFAULT_RECOMMENDED,
5
+ defaultModelOf,
5
6
  isConfigured,
6
7
  liveCatalog,
8
+ loadCatalog,
9
+ providerMeta,
7
10
  secureStorage,
8
11
  validateEndpoint
9
12
  } from "llm-connect";
@@ -23,6 +26,25 @@ function proxySettings(config) {
23
26
  settings.headers = config.headers;
24
27
  return settings;
25
28
  }
29
+ var warned = false;
30
+ async function ownerKeySettings(config, catalogUrl = DEFAULT_CATALOG_URL) {
31
+ if (!config.apiKey)
32
+ return null;
33
+ if (!warned && typeof console !== "undefined") {
34
+ warned = true;
35
+ console.warn("murmur: the API key in this page can be read by anyone. Use a key with a spending limit, or a proxy (data-endpoint).");
36
+ }
37
+ const providerId = config.provider || "openai";
38
+ const catalog = await loadCatalog(catalogUrl);
39
+ const meta = providerMeta(catalog, providerId);
40
+ if (!meta)
41
+ throw new Error(`murmur: unknown provider "${providerId}"`);
42
+ const model = config.model || defaultModelOf(catalog[providerId]) || "";
43
+ const settings = { apiKey: config.apiKey, model, ...meta };
44
+ if (config.headers)
45
+ settings.headers = config.headers;
46
+ return settings;
47
+ }
26
48
  function visitorStore(config, options = {}) {
27
49
  const key = options.storageKey ?? "murmur";
28
50
  const defaults = {};
@@ -49,6 +71,7 @@ export {
49
71
  describeError,
50
72
  isConfigured2 as isConfigured,
51
73
  loadModel,
74
+ ownerKeySettings,
52
75
  proxySettings,
53
76
  resolveSettings,
54
77
  visitorStore
@@ -26,9 +26,37 @@ function chatMessages(prompt) {
26
26
  });
27
27
  }
28
28
  var loaded = new Map;
29
+ var ready = new Set;
29
30
  var listeners = new Map;
31
+ var keyOf = (spec) => `${spec.id}@${spec.dtype}`;
32
+ var CACHE_NAME = "transformers-cache";
33
+ async function isLocalModelCached(spec = DEFAULT_LOCAL_MODEL) {
34
+ try {
35
+ if (typeof caches === "undefined" || !await caches.has(CACHE_NAME))
36
+ return false;
37
+ const cache = await caches.open(CACHE_NAME);
38
+ const weights = `/${spec.id}/resolve/`;
39
+ const file = `model_${spec.dtype}.onnx`;
40
+ return (await cache.keys()).some((r) => r.url.includes(weights) && r.url.includes(file));
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+ function isLocalModelReady(spec = DEFAULT_LOCAL_MODEL) {
46
+ return ready.has(keyOf(spec));
47
+ }
48
+ async function persistStorage() {
49
+ try {
50
+ const storage = globalThis.navigator?.storage;
51
+ if (!storage?.persist)
52
+ return false;
53
+ return await storage.persisted?.() || await storage.persist();
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
30
58
  function loadLocalModel(spec = DEFAULT_LOCAL_MODEL, onProgress) {
31
- const key = `${spec.id}@${spec.dtype}`;
59
+ const key = keyOf(spec);
32
60
  const set = listeners.get(key) ?? new Set;
33
61
  listeners.set(key, set);
34
62
  if (onProgress)
@@ -38,6 +66,8 @@ function loadLocalModel(spec = DEFAULT_LOCAL_MODEL, onProgress) {
38
66
  pending = (async () => {
39
67
  if (!await hasWebGPU())
40
68
  throw new LocalModelError("webgpu", "This browser has no WebGPU.");
69
+ if (!await isLocalModelCached(spec))
70
+ persistStorage();
41
71
  const { pipeline } = await import("@huggingface/transformers");
42
72
  const generator = await pipeline("text-generation", spec.id, {
43
73
  dtype: spec.dtype,
@@ -53,7 +83,10 @@ function loadLocalModel(spec = DEFAULT_LOCAL_MODEL, onProgress) {
53
83
  for (const listener of set)
54
84
  listener(p);
55
85
  }
86
+ }).catch((error) => {
87
+ throw new LocalModelError("load", error instanceof Error ? error.message : String(error));
56
88
  });
89
+ ready.add(key);
57
90
  return generator;
58
91
  })();
59
92
  pending.catch(() => loaded.delete(key));
@@ -158,6 +191,9 @@ export {
158
191
  LocalModelError,
159
192
  chatMessages,
160
193
  hasWebGPU,
194
+ isLocalModelCached,
195
+ isLocalModelReady,
161
196
  loadLocalModel,
162
- localModel
197
+ localModel,
198
+ persistStorage
163
199
  };
@@ -8,8 +8,8 @@ import {
8
8
  MurmurChat2,
9
9
  defineMurmurChat2,
10
10
  mountMurmur2
11
- } from "../chunks/index-y3v46yjp.js";
12
- __chunks(import.meta.url,["jzqx319b","y3v46yjp","t36hd4vt","wxshtnm0","dj4321kh","vb9smwta","nb4hdy2t","kaeec41p","b77xqap3","8h9c8ekq","p86sy3y4","1zk822m5"],[["./index.js",1],["../chunks/index-y3v46yjp.js",8],["../chunks/murmur-t36hd4vt.js",9,10],["../chunks/retriever-wxshtnm0.js",10],["../indexer/index.js",11],["../llm/index.js"],["../local/index.js"],["../chunks/chunk-kaeec41p.js",11],["../chunks/index-b77xqap3.js"],["../chunks/index-8h9c8ekq.js",10,8],["../chunks/index-p86sy3y4.js",8],["../chunks/index-1zk822m5.js"]],0);
11
+ } from "../chunks/index-4ftk20yq.js";
12
+ __chunks(import.meta.url,["kaf6n93p","4ftk20yq","t36hd4vt","wxshtnm0","dj4321kh","xreqcp1t","qfx1tgz2","kaeec41p","b77xqap3","8h9c8ekq","p86sy3y4","1zk822m5"],[["./index.js",1],["../chunks/index-4ftk20yq.js",8],["../chunks/murmur-t36hd4vt.js",9,10],["../chunks/retriever-wxshtnm0.js",10],["../indexer/index.js",11],["../llm/index.js"],["../local/index.js"],["../chunks/chunk-kaeec41p.js",11],["../chunks/index-b77xqap3.js"],["../chunks/index-8h9c8ekq.js",10,8],["../chunks/index-p86sy3y4.js",8],["../chunks/index-1zk822m5.js"]],0);
13
13
  export {
14
14
  MESSAGES2 as MESSAGES,
15
15
  MurmurChat2 as MurmurChat,
@@ -13,11 +13,19 @@ export interface LlmConfig {
13
13
  endpoint?: string;
14
14
  /** Extra headers sent to the proxy. */
15
15
  headers?: Record<string, string>;
16
+ /**
17
+ * The owner's own key, used for every visitor with `provider` and `model`. It is in the page,
18
+ * so anyone can read it: only a key with a spending limit (OpenRouter, a restricted project
19
+ * key), or prefer a proxy that keeps it secret.
20
+ */
21
+ apiKey?: string;
16
22
  }
17
23
  /** Where the catalog falls back to when models.dev or OpenRouter cannot be reached. */
18
24
  export declare const DEFAULT_CATALOG_URL = "https://maxgfr.github.io/murmur/cdn/models.json";
19
25
  /** Settings calling the owner's proxy, or null when there is no (valid) proxy. */
20
26
  export declare function proxySettings(config: LlmConfig): LlmSettings | null;
27
+ /** Settings that call the provider with the owner's key, or null when there is none. */
28
+ export declare function ownerKeySettings(config: LlmConfig, catalogUrl?: string): Promise<LlmSettings | null>;
21
29
  export interface VisitorStoreOptions {
22
30
  /** Fallback catalog URL. Default {@link DEFAULT_CATALOG_URL}. */
23
31
  catalogUrl?: string;
@@ -42,6 +42,18 @@ export declare function chatMessages(prompt: LanguageModelV4Prompt): {
42
42
  type Generator = ((messages: unknown, options: Record<string, unknown>) => Promise<unknown>) & {
43
43
  tokenizer: unknown;
44
44
  };
45
+ /**
46
+ * Whether the model's weights are already in this browser's cache, so loading it needs no
47
+ * network: the weights file for this dtype is looked up among the cached URLs.
48
+ */
49
+ export declare function isLocalModelCached(spec?: LocalModelSpec): Promise<boolean>;
50
+ /** Whether the model is loaded and compiled in this page. */
51
+ export declare function isLocalModelReady(spec?: LocalModelSpec): boolean;
52
+ /**
53
+ * Asks the browser to keep this site's storage under pressure, so 1 GB of weights is not
54
+ * evicted and downloaded again. Browsers decide on their own (engagement, installed app…).
55
+ */
56
+ export declare function persistStorage(): Promise<boolean>;
45
57
  /** Downloads (or reads from cache) and compiles the model; later calls share the same load. */
46
58
  export declare function loadLocalModel(spec?: LocalModelSpec, onProgress?: (progress: LoadProgress) => void): Promise<Generator>;
47
59
  export declare class LocalModelError extends Error {
@@ -29,6 +29,8 @@ export declare class MurmurChat extends Base {
29
29
  private controller;
30
30
  private built;
31
31
  private unsubscribe;
32
+ private unsubscribeLocal;
33
+ private loadbar;
32
34
  constructor();
33
35
  configure(options: WidgetOptions): this;
34
36
  get isOpen(): boolean;
@@ -41,6 +43,8 @@ export declare class MurmurChat extends Base {
41
43
  ask(question: string): Promise<void>;
42
44
  private get openKey();
43
45
  private build;
46
+ /** The download bar and the bubble's lamp follow the in-browser model. */
47
+ private drawLocal;
44
48
  private autosize;
45
49
  private setSendMode;
46
50
  private emit;
@@ -52,6 +52,7 @@ export interface Messages {
52
52
  localPreparing: string;
53
53
  localNoWebGPU: string;
54
54
  localLoadFailed: string;
55
+ localNoWebGPUOnly: string;
55
56
  gigabytes: string;
56
57
  megabytes: string;
57
58
  }