murmur-rag 1.1.0 → 1.3.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 (45) hide show
  1. package/README.md +28 -19
  2. package/dist/cdn/{anthropic-dc7h9prc.js → anthropic-tw416spz.js} +2 -2
  3. package/dist/cdn/chunk-xyp6nm65.js +1 -0
  4. package/dist/cdn/{embed-fd96s8qk.js → embed-3am76qfd.js} +9 -9
  5. package/dist/cdn/embed-50pfy67v.js +83 -0
  6. package/dist/cdn/{embed-z32y952f.js → embed-cx1r66ch.js} +6 -6
  7. package/dist/cdn/{embed-ssqsb38w.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-acjpjh6t.js → embed-yz75savs.js} +3 -3
  11. package/dist/cdn/{google-yywkqdph.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-25w1nsx5.js +3 -0
  18. package/dist/cdn/{murmur-j1ky5vmr.js → murmur-1a55et82.js} +3 -3
  19. package/dist/cdn/murmur.js +7 -7
  20. package/dist/cdn/openai-bjrbem49.js +18 -0
  21. package/dist/cdn/{openai-compatible-zhaj1r43.js → openai-compatible-sg25g0vj.js} +6 -6
  22. package/dist/cdn/retriever-06ybegpd.js +1 -0
  23. package/dist/cdn/{transformers.web-3kmbvcbt.js → transformers.web-49m0ht4a.js} +9 -9
  24. package/dist/lib/chunks/{index-92w507gk.js → index-4ftk20yq.js} +164 -50
  25. package/dist/lib/embed.js +10 -4
  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 +56 -8
  34. package/dist/types/widget/styles.d.ts +1 -1
  35. package/package.json +1 -1
  36. package/dist/cdn/chunk-hpx6rpea.js +0 -1
  37. package/dist/cdn/embed-6gef7ym5.js +0 -10
  38. package/dist/cdn/embed-jdraznwg.js +0 -83
  39. package/dist/cdn/index-bq0x71h1.js +0 -1
  40. package/dist/cdn/index-jt6n0d28.js +0 -1
  41. package/dist/cdn/index-nesgnf2n.js +0 -1
  42. package/dist/cdn/index-wgzgvzaf.js +0 -1
  43. package/dist/cdn/mistral-0mg4kk7s.js +0 -3
  44. package/dist/cdn/openai-g1685z9v.js +0 -18
  45. package/dist/cdn/retriever-dqxzjaqk.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,28 +394,93 @@ 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.local || this.visitorChoseLocal)
404
+ if (m === "proxy" || m === "auto" && this.options.llm?.endpoint)
405
+ return "proxy";
406
+ if ((m === "auto" || m === "key") && this.options.llm?.apiKey)
407
+ return "owner";
408
+ if (m === "local")
398
409
  return "local";
399
- return this.options.llm?.endpoint ? "proxy" : "visitor";
410
+ if (m === "key" || !this.localAllowed)
411
+ return "visitor";
412
+ return readLocal(`${this.key}-local`) === "0" ? "visitor" : "local";
413
+ }
414
+ get localAllowed() {
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";
463
+ }
464
+ async settleChoice() {
465
+ if (!this.localAllowed || !this.keyAllowed || readLocal(`${this.key}-local`) !== null)
466
+ return;
467
+ const [{ isConfigured }, store] = await Promise.all([(__preload("xreqcp1t"), import("../llm/index.js")), this.store()]);
468
+ const saved = await store.load();
469
+ if (saved && isConfigured(saved))
470
+ writeLocal(`${this.key}-local`, "0");
400
471
  }
401
472
  async localSpec() {
402
473
  const { local } = this.options;
403
474
  if (local && typeof local === "object")
404
475
  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";
476
+ return (await (__preload("qfx1tgz2"), import("../local/index.js"))).DEFAULT_LOCAL_MODEL;
409
477
  }
410
478
  useLocal(on) {
411
- writeLocal(`${this.key}-local`, on ? "1" : null);
479
+ writeLocal(`${this.key}-local`, on ? "1" : "0");
412
480
  this.engine = null;
413
481
  }
414
482
  get needsDownloadConsent() {
415
- 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";
416
484
  }
417
485
  agreeToDownload() {
418
486
  writeLocal(`${this.key}-local-consent`, "1");
@@ -427,7 +495,7 @@ class ChatSession {
427
495
  this.persist();
428
496
  }
429
497
  store() {
430
- this.storePromise ??= (__preload("vb9smwta"), import("../llm/index.js")).then((llm) => {
498
+ this.storePromise ??= (__preload("xreqcp1t"), import("../llm/index.js")).then((llm) => {
431
499
  const opts = {};
432
500
  if (this.options.catalogUrl)
433
501
  opts.catalogUrl = this.options.catalogUrl;
@@ -438,16 +506,22 @@ class ChatSession {
438
506
  return this.storePromise;
439
507
  }
440
508
  async settings() {
441
- if (this.mode === "custom")
509
+ const mode = this.mode;
510
+ if (mode === "custom" || mode === "local")
442
511
  return null;
443
- const llm = await (__preload("vb9smwta"), import("../llm/index.js"));
444
- 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());
445
519
  }
446
520
  async needsSetup() {
447
521
  return this.mode === "visitor" && !await this.settings();
448
522
  }
449
523
  async canRunLocally() {
450
- return (await (__preload("nb4hdy2t"), import("../local/index.js"))).hasWebGPU();
524
+ return (await (__preload("qfx1tgz2"), import("../local/index.js"))).hasWebGPU();
451
525
  }
452
526
  async loadIndex() {
453
527
  const { index } = this.options;
@@ -474,16 +548,16 @@ class ChatSession {
474
548
  ]);
475
549
  const local = this.mode === "local";
476
550
  const model = this.options.model ?? (local ? async () => {
477
- const { localModel } = await (__preload("nb4hdy2t"), import("../local/index.js"));
551
+ const { localModel } = await (__preload("qfx1tgz2"), import("../local/index.js"));
478
552
  return localModel({
479
553
  spec: await this.localSpec(),
480
- onProgress: (p) => this.onProgress?.(p)
554
+ onProgress: (progress) => this.setLocal({ status: "loading", progress })
481
555
  });
482
556
  } : async () => {
483
557
  const settings = await this.settings();
484
558
  if (!settings)
485
559
  throw new SetupNeeded2;
486
- const { loadModel } = await (__preload("vb9smwta"), import("../llm/index.js"));
560
+ const { loadModel } = await (__preload("xreqcp1t"), import("../llm/index.js"));
487
561
  return loadModel(settings);
488
562
  });
489
563
  const opts = {
@@ -505,6 +579,8 @@ class ChatSession {
505
579
  this.turns.push(turn);
506
580
  this.persist();
507
581
  try {
582
+ if (this.mode === "local")
583
+ this.warmUp();
508
584
  this.engine ??= this.createEngine();
509
585
  const engine = await this.engine.catch((error) => {
510
586
  this.engine = null;
@@ -522,6 +598,8 @@ class ChatSession {
522
598
  onText(turn.content, turn);
523
599
  }
524
600
  const done = await answer.done;
601
+ if (this.mode === "local")
602
+ this.setLocal({ status: "ready" });
525
603
  turn.content = done.text;
526
604
  turn.cited = done.cited;
527
605
  } catch (error) {
@@ -543,7 +621,7 @@ class ChatSession {
543
621
  return `local-${error.code ?? "load"}|${error.message}`;
544
622
  }
545
623
  try {
546
- const { describeError } = await (__preload("vb9smwta"), import("../llm/index.js"));
624
+ const { describeError } = await (__preload("xreqcp1t"), import("../llm/index.js"));
547
625
  const settings = this.mode === "custom" || this.mode === "local" ? null : await this.settings();
548
626
  const info = describeError(error, settings?.apiKey);
549
627
  return `${info.code}|${info.message}`;
@@ -780,7 +858,7 @@ footer a:hover { text-decoration: underline; }
780
858
  p.or { margin: 0; color: var(--muted); font-size: 13px; text-align: center; }
781
859
  .consent { display: grid; gap: 10px; padding: 14px; border-radius: 12px; background: var(--raised); }
782
860
  .consent p { margin: 0; }
783
- .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; }
784
862
  progress { width: 100%; height: 6px; appearance: none; border: 0; border-radius: 3px; background: var(--line); overflow: hidden; }
785
863
  progress::-webkit-progress-bar { background: var(--line); }
786
864
  progress::-webkit-progress-value { background: var(--lamp); }
@@ -830,6 +908,8 @@ class MurmurChat2 extends Base {
830
908
  controller = null;
831
909
  built = false;
832
910
  unsubscribe = null;
911
+ unsubscribeLocal = null;
912
+ loadbar;
833
913
  constructor() {
834
914
  super();
835
915
  this.root = this.attachShadow({ mode: "open" });
@@ -849,12 +929,16 @@ class MurmurChat2 extends Base {
849
929
  disconnectedCallback() {
850
930
  this.controller?.abort();
851
931
  this.unsubscribe?.();
932
+ this.unsubscribeLocal?.();
933
+ this.unsubscribeLocal?.();
852
934
  }
853
935
  async open() {
854
936
  if (!this.built)
855
937
  this.build();
856
938
  this.launcher.setAttribute("aria-expanded", "true");
857
939
  this.panel.hidden = false;
940
+ if (this.session?.preload === "open")
941
+ this.session.warmUp();
858
942
  this.session?.persist();
859
943
  tabStorage("set", this.openKey);
860
944
  this.input.focus();
@@ -918,7 +1002,7 @@ class MurmurChat2 extends Base {
918
1002
  this.input.focus();
919
1003
  });
920
1004
  this.settingsBtn = iconButton(doc, "settings", this.t.settings);
921
- this.settingsBtn.hidden = !!o.model || !!o.llm?.endpoint || !!o.local;
1005
+ this.settingsBtn.hidden = !!o.model || !!o.llm?.endpoint;
922
1006
  this.settingsBtn.addEventListener("click", () => void this.showSetup());
923
1007
  const closeBtn = iconButton(doc, "close", this.t.close);
924
1008
  closeBtn.hidden = this.hasAttribute("inline");
@@ -955,6 +1039,8 @@ class MurmurChat2 extends Base {
955
1039
  this.submit();
956
1040
  });
957
1041
  form.append(this.input, this.sendBtn);
1042
+ this.loadbar = el(doc, "div", { class: "loadbar", role: "status" });
1043
+ this.loadbar.hidden = true;
958
1044
  const footer = el(doc, "footer");
959
1045
  const by = el(doc, "a", {
960
1046
  href: "https://maxgfr.github.io/murmur/",
@@ -963,7 +1049,7 @@ class MurmurChat2 extends Base {
963
1049
  });
964
1050
  by.textContent = this.t.poweredBy;
965
1051
  footer.append(by);
966
- this.chatView.append(this.log, form, footer);
1052
+ this.chatView.append(this.log, this.loadbar, form, footer);
967
1053
  this.setupView = el(doc, "div", { class: "setup" });
968
1054
  this.setupView.hidden = true;
969
1055
  this.panel.append(header, this.chatView, this.setupView);
@@ -971,11 +1057,37 @@ class MurmurChat2 extends Base {
971
1057
  if (this.hasAttribute("inline"))
972
1058
  this.launcher.hidden = true;
973
1059
  this.built = true;
974
- 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
+ });
975
1070
  this.renderChat();
976
1071
  if (o.open || !this.hasAttribute("inline") && tabStorage("get", this.openKey))
977
1072
  this.open();
978
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
+ }
979
1091
  autosize() {
980
1092
  this.input.style.height = "auto";
981
1093
  this.input.style.height = `${Math.min(this.input.scrollHeight, 140)}px`;
@@ -1086,7 +1198,8 @@ class MurmurChat2 extends Base {
1086
1198
  const question = this.input.value.trim();
1087
1199
  if (!question || this.controller || !this.session)
1088
1200
  return;
1089
- if (this.session.needsDownloadConsent) {
1201
+ await this.session.settleChoice();
1202
+ if (this.session.needsDownloadConsent || this.session.mode === "local" && !await this.session.canRunLocally()) {
1090
1203
  await this.showConsent(question);
1091
1204
  return;
1092
1205
  }
@@ -1105,15 +1218,6 @@ class MurmurChat2 extends Base {
1105
1218
  this.log.append(node);
1106
1219
  this.log.scrollTop = this.log.scrollHeight;
1107
1220
  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
- };
1117
1221
  const controller = new AbortController;
1118
1222
  this.controller = controller;
1119
1223
  this.setSendMode("stop");
@@ -1156,16 +1260,15 @@ class MurmurChat2 extends Base {
1156
1260
  const spec = await session.localSpec();
1157
1261
  const card = el(doc, "div", { class: "msg bot consent" });
1158
1262
  const row = el(doc, "div", { class: "row" });
1159
- const ownerLocal = !!this.options.local;
1160
1263
  const useKey = () => {
1161
1264
  card.remove();
1162
1265
  session.useLocal(false);
1163
1266
  this.showSetup(question);
1164
1267
  };
1165
1268
  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" });
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" });
1169
1272
  key.textContent = t.useKey;
1170
1273
  key.addEventListener("click", useKey);
1171
1274
  row.append(key);
@@ -1181,7 +1284,7 @@ class MurmurChat2 extends Base {
1181
1284
  this.submit();
1182
1285
  });
1183
1286
  row.append(go);
1184
- if (!ownerLocal) {
1287
+ if (session.keyAllowed) {
1185
1288
  const key = el(doc, "button", { class: "link-btn", type: "button" });
1186
1289
  key.textContent = t.useKey;
1187
1290
  key.addEventListener("click", useKey);
@@ -1224,25 +1327,29 @@ class MurmurChat2 extends Base {
1224
1327
  this.submit();
1225
1328
  }
1226
1329
  });
1227
- session.canRunLocally().then((ok) => {
1228
- localBtn.disabled = !ok;
1229
- if (!ok)
1230
- localHint.textContent = t.localNoWebGPU;
1330
+ const switchToKey = el(doc, "button", { class: "link-btn", type: "button" });
1331
+ switchToKey.textContent = t.useKey;
1332
+ switchToKey.addEventListener("click", () => {
1333
+ session.useLocal(false);
1334
+ localBtn.textContent = t.localOption;
1335
+ localBtn.disabled = false;
1336
+ switchToKey.remove();
1231
1337
  });
1232
1338
  if (session.mode === "local") {
1233
1339
  localBtn.textContent = t.localActive;
1234
1340
  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);
1341
+ inBrowser.append(switchToKey);
1244
1342
  }
1343
+ session.canRunLocally().then((ok) => {
1344
+ if (ok)
1345
+ return;
1346
+ localBtn.textContent = t.localOption;
1347
+ localBtn.disabled = true;
1348
+ localHint.textContent = t.localNoWebGPU;
1349
+ switchToKey.remove();
1350
+ });
1245
1351
  const or = text(doc, "p", t.orKey, "or");
1352
+ inBrowser.hidden = or.hidden = !session.localAllowed;
1246
1353
  const local = checkbox(doc, t.local);
1247
1354
  const providerField = field(doc, t.provider, el(doc, "select"));
1248
1355
  const providerSelect = providerField.control;
@@ -1379,6 +1486,13 @@ function tabStorage(op, key) {
1379
1486
  } catch {}
1380
1487
  return false;
1381
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
+ }
1382
1496
  function el(doc, tag, attrs = {}) {
1383
1497
  const node = doc.createElement(tag);
1384
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-92w507gk.js";
5
- __chunks(import.meta.url,["rv6357f7","92w507gk","t36hd4vt","wxshtnm0","dj4321kh","vb9smwta","nb4hdy2t","kaeec41p","b77xqap3","8h9c8ekq","p86sy3y4","1zk822m5"],[["./embed.js",1],["./chunks/index-92w507gk.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)
@@ -45,8 +47,12 @@ function optionsFromDataset(data, base) {
45
47
  o.storageKey = data.storageKey;
46
48
  if (data.open === "true")
47
49
  o.open = true;
48
- if (data.local === "true")
49
- o.local = true;
50
+ if (data.local === "false")
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-92w507gk.js";
12
- __chunks(import.meta.url,["f9340r29","92w507gk","t36hd4vt","wxshtnm0","dj4321kh","vb9smwta","nb4hdy2t","kaeec41p","b77xqap3","8h9c8ekq","p86sy3y4","1zk822m5"],[["./index.js",1],["../chunks/index-92w507gk.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
  }