sagedesk 1.0.0 → 2.0.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.
@@ -4,7 +4,7 @@
4
4
  import { useState, useEffect, Suspense, lazy } from "react";
5
5
  import { jsx } from "react/jsx-runtime";
6
6
  var LazyWidget = lazy(
7
- () => import("./SageDeskWidget-P3H2VJR5.js").then((mod) => ({ default: mod.SageDeskWidget })).catch((err) => {
7
+ () => import("./SageDeskWidget-R3XJ5OUY.js").then((mod) => ({ default: mod.SageDeskWidget })).catch((err) => {
8
8
  console.warn("[sagedesk] Failed to load widget bundle:", err);
9
9
  const Empty = () => null;
10
10
  return { default: Empty };
@@ -13,17 +13,23 @@ var LazyWidget = lazy(
13
13
  function SageDeskNext(props) {
14
14
  const [mounted, setMounted] = useState(false);
15
15
  useEffect(() => {
16
- if (!props.indexUrl) {
16
+ const mode = props.mode ?? "local";
17
+ if (mode === "local" && !props.indexUrl) {
17
18
  console.warn(
18
19
  '[sagedesk] Missing required prop: indexUrl. The widget will not load.\nMake sure you ran `npx sagedesk build` and are passing indexUrl="/support-index.json" (or wherever you placed the output file in public/).'
19
20
  );
20
- } else if (!props.indexUrl.startsWith("/") && !props.indexUrl.startsWith("http")) {
21
+ } else if (mode === "llm" && !props.endpoint) {
22
+ console.warn(
23
+ '[sagedesk] Missing required prop: endpoint for LLM mode. The widget will not load.\nProvide your backend route, e.g. endpoint="/api/sagedesk".'
24
+ );
25
+ }
26
+ if (props.indexUrl && !props.indexUrl.startsWith("/") && !props.indexUrl.startsWith("http")) {
21
27
  console.warn(
22
28
  `[sagedesk] indexUrl "${props.indexUrl}" looks like a relative path. It should start with "/" (e.g. "/support-index.json") so it resolves correctly from any page.`
23
29
  );
24
30
  }
25
31
  setMounted(true);
26
- }, []);
32
+ }, [props.mode, props.indexUrl, props.endpoint]);
27
33
  if (!mounted) return null;
28
34
  return /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(LazyWidget, { ...props }) });
29
35
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/next/SageDeskNext.tsx"],"sourcesContent":["'use client';\n\nimport { useState, useEffect, Suspense, lazy } from 'react';\nimport type { SageDeskWidgetProps } from '../react/SageDeskWidget.js';\n\nconst LazyWidget = lazy(() =>\n import('../react/SageDeskWidget.js')\n .then((mod) => ({ default: mod.SageDeskWidget }))\n .catch((err) => {\n console.warn('[sagedesk] Failed to load widget bundle:', err);\n const Empty = () => null;\n return { default: Empty };\n })\n);\n\nexport function SageDeskNext(props: SageDeskWidgetProps) {\n // useState + useEffect ensures server render and initial client hydration\n // both return null (no mismatch). The widget appears only after hydration.\n // Using typeof window directly in render body causes a hydration mismatch\n // in Next.js App Router because 'use client' components still SSR.\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n if (!props.indexUrl) {\n console.warn(\n '[sagedesk] Missing required prop: indexUrl. The widget will not load.\\n' +\n 'Make sure you ran `npx sagedesk build` and are passing indexUrl=\"/support-index.json\" (or wherever you placed the output file in public/).'\n );\n } else if (!props.indexUrl.startsWith('/') && !props.indexUrl.startsWith('http')) {\n console.warn(\n `[sagedesk] indexUrl \"${props.indexUrl}\" looks like a relative path. ` +\n 'It should start with \"/\" (e.g. \"/support-index.json\") so it resolves correctly from any page.'\n );\n }\n setMounted(true);\n }, []);\n\n if (!mounted) return null;\n\n return (\n <Suspense fallback={null}>\n <LazyWidget {...props} />\n </Suspense>\n );\n}\n"],"mappings":";;;AAEA,SAAS,UAAU,WAAW,UAAU,YAAY;AAuC9C;AApCN,IAAM,aAAa;AAAA,EAAK,MACtB,OAAO,8BAA4B,EAChC,KAAK,CAAC,SAAS,EAAE,SAAS,IAAI,eAAe,EAAE,EAC/C,MAAM,CAAC,QAAQ;AACd,YAAQ,KAAK,4CAA4C,GAAG;AAC5D,UAAM,QAAQ,MAAM;AACpB,WAAO,EAAE,SAAS,MAAM;AAAA,EAC1B,CAAC;AACL;AAEO,SAAS,aAAa,OAA4B;AAKvD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAE5C,YAAU,MAAM;AACd,QAAI,CAAC,MAAM,UAAU;AACnB,cAAQ;AAAA,QACN;AAAA,MAEF;AAAA,IACF,WAAW,CAAC,MAAM,SAAS,WAAW,GAAG,KAAK,CAAC,MAAM,SAAS,WAAW,MAAM,GAAG;AAChF,cAAQ;AAAA,QACN,wBAAwB,MAAM,QAAQ;AAAA,MAExC;AAAA,IACF;AACA,eAAW,IAAI;AAAA,EACjB,GAAG,CAAC,CAAC;AAEL,MAAI,CAAC,QAAS,QAAO;AAErB,SACE,oBAAC,YAAS,UAAU,MAClB,8BAAC,cAAY,GAAG,OAAO,GACzB;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/next/SageDeskNext.tsx"],"sourcesContent":["'use client';\n\nimport { useState, useEffect, Suspense, lazy } from 'react';\nimport type { SageDeskWidgetProps } from '../react/SageDeskWidget.js';\n\nconst LazyWidget = lazy(() =>\n import('../react/SageDeskWidget.js')\n .then((mod) => ({ default: mod.SageDeskWidget }))\n .catch((err) => {\n console.warn('[sagedesk] Failed to load widget bundle:', err);\n const Empty = () => null;\n return { default: Empty };\n })\n);\n\nexport function SageDeskNext(props: SageDeskWidgetProps) {\n // useState + useEffect ensures server render and initial client hydration\n // both return null (no mismatch). The widget appears only after hydration.\n // Using typeof window directly in render body causes a hydration mismatch\n // in Next.js App Router because 'use client' components still SSR.\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n const mode = props.mode ?? 'local';\n\n if (mode === 'local' && !props.indexUrl) {\n console.warn(\n '[sagedesk] Missing required prop: indexUrl. The widget will not load.\\n' +\n 'Make sure you ran `npx sagedesk build` and are passing indexUrl=\"/support-index.json\" (or wherever you placed the output file in public/).'\n );\n } else if (mode === 'llm' && !props.endpoint) {\n console.warn(\n '[sagedesk] Missing required prop: endpoint for LLM mode. The widget will not load.\\n' +\n 'Provide your backend route, e.g. endpoint=\"/api/sagedesk\".'\n );\n }\n\n if (props.indexUrl && !props.indexUrl.startsWith('/') && !props.indexUrl.startsWith('http')) {\n console.warn(\n `[sagedesk] indexUrl \"${props.indexUrl}\" looks like a relative path. ` +\n 'It should start with \"/\" (e.g. \"/support-index.json\") so it resolves correctly from any page.'\n );\n }\n setMounted(true);\n }, [props.mode, props.indexUrl, props.endpoint]);\n\n if (!mounted) return null;\n\n return (\n <Suspense fallback={null}>\n <LazyWidget {...props} />\n </Suspense>\n );\n}\n"],"mappings":";;;AAEA,SAAS,UAAU,WAAW,UAAU,YAAY;AAgD9C;AA7CN,IAAM,aAAa;AAAA,EAAK,MACtB,OAAO,8BAA4B,EAChC,KAAK,CAAC,SAAS,EAAE,SAAS,IAAI,eAAe,EAAE,EAC/C,MAAM,CAAC,QAAQ;AACd,YAAQ,KAAK,4CAA4C,GAAG;AAC5D,UAAM,QAAQ,MAAM;AACpB,WAAO,EAAE,SAAS,MAAM;AAAA,EAC1B,CAAC;AACL;AAEO,SAAS,aAAa,OAA4B;AAKvD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAE5C,YAAU,MAAM;AACd,UAAM,OAAO,MAAM,QAAQ;AAE3B,QAAI,SAAS,WAAW,CAAC,MAAM,UAAU;AACvC,cAAQ;AAAA,QACN;AAAA,MAEF;AAAA,IACF,WAAW,SAAS,SAAS,CAAC,MAAM,UAAU;AAC5C,cAAQ;AAAA,QACN;AAAA,MAEF;AAAA,IACF;AAEA,QAAI,MAAM,YAAY,CAAC,MAAM,SAAS,WAAW,GAAG,KAAK,CAAC,MAAM,SAAS,WAAW,MAAM,GAAG;AAC3F,cAAQ;AAAA,QACN,wBAAwB,MAAM,QAAQ;AAAA,MAExC;AAAA,IACF;AACA,eAAW,IAAI;AAAA,EACjB,GAAG,CAAC,MAAM,MAAM,MAAM,UAAU,MAAM,QAAQ,CAAC;AAE/C,MAAI,CAAC,QAAS,QAAO;AAErB,SACE,oBAAC,YAAS,UAAU,MAClB,8BAAC,cAAY,GAAG,OAAO,GACzB;AAEJ;","names":[]}
@@ -258,6 +258,17 @@ function getFallback(config) {
258
258
  }
259
259
 
260
260
  // src/react/useSageDesk.ts
261
+ function logFallbackWarning(reason) {
262
+ if (!reason) return;
263
+ const messages = {
264
+ "auth-error": "[sagedesk] Support service authentication failed. Showing relevant knowledge instead.",
265
+ "quota-exceeded": "[sagedesk] Support service quota exhausted. Showing relevant knowledge instead.",
266
+ "timeout": "[sagedesk] Support service took too long to respond. Showing relevant knowledge instead.",
267
+ "api-error": "[sagedesk] Support service error. Showing relevant knowledge instead.",
268
+ "malformed-response": "[sagedesk] Support service returned invalid response. Showing relevant knowledge instead."
269
+ };
270
+ console.warn(messages[reason] || "[sagedesk] Support service unavailable. Showing relevant knowledge instead.");
271
+ }
261
272
  var initialState = {
262
273
  messages: [],
263
274
  isOpen: false,
@@ -310,6 +321,11 @@ function useSageDesk(config) {
310
321
  const startEngine = (0, import_react.useCallback)(async () => {
311
322
  if (engineStartedRef.current) return;
312
323
  engineStartedRef.current = true;
324
+ if (config.mode === "llm") {
325
+ setChips(config.agent.suggestedChips ?? []);
326
+ dispatch({ type: "SET_ENGINE_STATUS", payload: { status: "ready" } });
327
+ return;
328
+ }
313
329
  dispatch({ type: "SET_ENGINE_STATUS", payload: { status: "loading-index" } });
314
330
  try {
315
331
  indexRef.current = await fetchIndex(config.indexUrl);
@@ -336,7 +352,7 @@ function useSageDesk(config) {
336
352
  embedderRef.current = new EmbedderRuntime();
337
353
  dispatch({ type: "SET_ENGINE_STATUS", payload: { status: "degraded" } });
338
354
  }
339
- }, [config.indexUrl, config.agent.suggestedChips, addMessage]);
355
+ }, [config.mode, config.indexUrl, config.agent.suggestedChips, addMessage]);
340
356
  const greetingShownRef = (0, import_react.useRef)(false);
341
357
  const open = (0, import_react.useCallback)(() => {
342
358
  dispatch({ type: "OPEN" });
@@ -379,8 +395,37 @@ function useSageDesk(config) {
379
395
  }
380
396
  let botText;
381
397
  let isFallback = false;
382
- let mode = "keyword";
383
- if (!indexRef.current) {
398
+ let fallbackReason;
399
+ let retrievalMode = "keyword";
400
+ if (config.mode === "llm") {
401
+ if (!config.endpoint) {
402
+ console.warn('[sagedesk] LLM mode requires an "endpoint" prop.');
403
+ botText = getFallback(config.agent);
404
+ isFallback = true;
405
+ } else {
406
+ try {
407
+ const res = await fetch(config.endpoint, {
408
+ method: "POST",
409
+ headers: { "Content-Type": "application/json" },
410
+ body: JSON.stringify({ query: trimmed })
411
+ });
412
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
413
+ const data = await res.json();
414
+ if (data.isFallback || !data.answer) {
415
+ fallbackReason = data.fallbackReason;
416
+ logFallbackWarning(fallbackReason);
417
+ botText = getFallback(config.agent);
418
+ isFallback = true;
419
+ } else {
420
+ botText = data.answer;
421
+ }
422
+ } catch (err) {
423
+ console.warn("[sagedesk] Support service unavailable. Using cached knowledge instead.");
424
+ botText = getFallback(config.agent);
425
+ isFallback = true;
426
+ }
427
+ }
428
+ } else if (!indexRef.current) {
384
429
  botText = getFallback(config.agent);
385
430
  isFallback = true;
386
431
  } else {
@@ -391,7 +436,7 @@ function useSageDesk(config) {
391
436
  embedderRef.current,
392
437
  config.search
393
438
  );
394
- mode = res.mode;
439
+ retrievalMode = res.mode;
395
440
  if (res.results.length > 0) {
396
441
  botText = buildAnswer(res.results);
397
442
  } else {
@@ -404,11 +449,13 @@ function useSageDesk(config) {
404
449
  isFallback = true;
405
450
  }
406
451
  }
407
- const elapsed = Date.now() - typingStart;
408
- const delayBase = mode === "keyword" || isFallback ? 800 : 3e3;
409
- const minTypingMs = delayBase + Math.random() * 2e3;
410
- const remaining = minTypingMs - elapsed;
411
- if (remaining > 0) await new Promise((r) => setTimeout(r, remaining));
452
+ if (config.mode !== "llm") {
453
+ const elapsed = Date.now() - typingStart;
454
+ const delayBase = retrievalMode === "keyword" || isFallback ? 800 : 3e3;
455
+ const minTypingMs = delayBase + Math.random() * 2e3;
456
+ const remaining = minTypingMs - elapsed;
457
+ if (remaining > 0) await new Promise((r) => setTimeout(r, remaining));
458
+ }
412
459
  dispatch({ type: "SET_TYPING", payload: false });
413
460
  addMessage({ role: "bot", text: botText, isFallback });
414
461
  },
@@ -431,6 +478,54 @@ function useSageDesk(config) {
431
478
  return { state, chips: activeChips, open, close, submit };
432
479
  }
433
480
 
481
+ // src/react/markdownUtils.ts
482
+ var import_marked = require("marked");
483
+ var import_dompurify = __toESM(require("dompurify"), 1);
484
+ import_marked.marked.setOptions({
485
+ breaks: true,
486
+ gfm: true,
487
+ pedantic: false
488
+ });
489
+ var PURIFY_CONFIG = {
490
+ ALLOWED_TAGS: [
491
+ "p",
492
+ "br",
493
+ "strong",
494
+ "em",
495
+ "u",
496
+ "h1",
497
+ "h2",
498
+ "h3",
499
+ "h4",
500
+ "h5",
501
+ "h6",
502
+ "ul",
503
+ "ol",
504
+ "li",
505
+ "blockquote",
506
+ "code",
507
+ "pre",
508
+ "a",
509
+ "hr"
510
+ ],
511
+ ALLOWED_ATTR: ["href", "title", "target", "rel"],
512
+ ALLOW_DATA_ATTR: false
513
+ };
514
+ var HOOK_ALLOWLIST = ["a"];
515
+ import_dompurify.default.addHook("afterSanitizeAttributes", (node) => {
516
+ if (HOOK_ALLOWLIST.includes(node.tagName.toLowerCase())) {
517
+ if (node.tagName.toLowerCase() === "a") {
518
+ node.setAttribute("target", "_blank");
519
+ node.setAttribute("rel", "noopener noreferrer");
520
+ }
521
+ }
522
+ });
523
+ function parseMarkdown(markdown) {
524
+ const html = import_marked.marked.parse(markdown);
525
+ const sanitized = import_dompurify.default.sanitize(html, PURIFY_CONFIG);
526
+ return sanitized;
527
+ }
528
+
434
529
  // src/react/SageDeskWidget.tsx
435
530
  var import_jsx_runtime = require("react/jsx-runtime");
436
531
  var STYLE_ID = "sagedesk-widget-styles";
@@ -460,6 +555,28 @@ var SHARED = `
460
555
  }
461
556
  .sd-r-scrollable::-webkit-scrollbar { display: none !important; }
462
557
  .sd-r-scrollable > * { flex-shrink: 0 !important; }
558
+ .sd-r-markdown h1, .sd-r-markdown h2, .sd-r-markdown h3, .sd-r-markdown h4, .sd-r-markdown h5, .sd-r-markdown h6 {
559
+ margin: 12px 0 8px 0 !important; font-weight: 600 !important; line-height: 1.3 !important;
560
+ }
561
+ .sd-r-markdown h1 { font-size: 1.3em !important; }
562
+ .sd-r-markdown h2 { font-size: 1.2em !important; }
563
+ .sd-r-markdown h3 { font-size: 1.1em !important; }
564
+ .sd-r-markdown h4, .sd-r-markdown h5, .sd-r-markdown h6 { font-size: 1em !important; }
565
+ .sd-r-markdown strong { font-weight: 600 !important; }
566
+ .sd-r-markdown em { font-style: italic !important; }
567
+ .sd-r-markdown u { text-decoration: underline !important; }
568
+ .sd-r-markdown ul, .sd-r-markdown ol { margin: 8px 0 !important; padding-left: 20px !important; }
569
+ .sd-r-markdown li { margin: 4px 0 !important; }
570
+ .sd-r-markdown blockquote { margin: 8px 0 !important; padding-left: 12px !important; border-left: 3px solid currentColor !important; opacity: 0.8 !important; }
571
+ .sd-r-markdown code { font-family: 'Monaco', 'Courier New', monospace !important; font-size: 0.9em !important; padding: 2px 4px !important; background: rgba(0,0,0,0.05) !important; border-radius: 3px !important; }
572
+ .sd-r-markdown pre { background: rgba(0,0,0,0.05) !important; padding: 8px 10px !important; border-radius: 6px !important; overflow-x: auto !important; margin: 8px 0 !important; }
573
+ .sd-r-markdown pre code { background: none !important; padding: 0 !important; }
574
+ .sd-r-markdown hr { border: none !important; border-top: 1px solid currentColor !important; opacity: 0.3 !important; margin: 10px 0 !important; }
575
+ .sd-r-markdown a { text-decoration: underline !important; opacity: 0.9 !important; }
576
+ .sd-r-markdown a:hover { opacity: 1 !important; }
577
+ .sd-r-markdown p { margin: 6px 0 !important; }
578
+ .sd-r-markdown > *:first-child { margin-top: 0 !important; }
579
+ .sd-r-markdown > *:last-child { margin-bottom: 0 !important; }
463
580
  @media (max-width: 420px) {
464
581
  .sd-r-panel {
465
582
  bottom: 0 !important; right: 0 !important; left: 0 !important;
@@ -546,7 +663,7 @@ var PoweredBy = ({ dark = false }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx
546
663
  {
547
664
  href: "https://github.com/mzeeshanwahid/sagedesk",
548
665
  target: "_blank",
549
- rel: "noopener noreferrer",
666
+ rel: "noopener",
550
667
  style: {
551
668
  color: dark ? "rgba(255,255,255,0.7)" : "#5a5a64",
552
669
  fontWeight: 500,
@@ -558,6 +675,7 @@ var PoweredBy = ({ dark = false }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx
558
675
  ] });
559
676
  function ClassicMessageBubble({ msg, accent }) {
560
677
  const isBot = msg.role === "bot";
678
+ const renderedHtml = isBot ? parseMarkdown(msg.text) : msg.text;
561
679
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", alignItems: isBot ? "flex-start" : "flex-end", gap: "4px" }, children: [
562
680
  msg.isFallback && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: "11px", fontWeight: 500, color: "#9b9aa3", margin: 0, padding: "0 4px", fontFamily: "inherit" }, children: "Not sure about that one" }),
563
681
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
@@ -570,10 +688,9 @@ function ClassicMessageBubble({ msg, accent }) {
570
688
  color: isBot ? "#1a1a2e" : "#fff",
571
689
  border: isBot ? "1px solid rgba(20,20,40,0.06)" : "none",
572
690
  boxShadow: isBot ? "0 1px 2px rgba(20,20,40,0.04)" : `0 6px 16px -6px color-mix(in oklab, ${accent} 60%, transparent)`,
573
- whiteSpace: "pre-wrap",
574
691
  wordBreak: "break-word",
575
692
  fontFamily: "inherit"
576
- }, children: msg.text }),
693
+ }, className: "sd-r-markdown", children: isBot ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: renderedHtml } }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { whiteSpace: "pre-wrap" }, children: msg.text }) }),
577
694
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: {
578
695
  fontSize: "11px",
579
696
  color: "#a8a8b0",
@@ -603,6 +720,7 @@ function ClassicTypingIndicator() {
603
720
  }
604
721
  function LightMessageBubble({ msg, accent, agentName }) {
605
722
  const isBot = msg.role === "bot";
723
+ const renderedHtml = isBot ? parseMarkdown(msg.text) : msg.text;
606
724
  if (isBot) {
607
725
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "10px" }, children: [
608
726
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
@@ -619,7 +737,7 @@ function LightMessageBubble({ msg, accent, agentName }) {
619
737
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "11px", color: "#a8a89e", fontVariantNumeric: "tabular-nums", fontFamily: "inherit" }, children: "just now" })
620
738
  ] }),
621
739
  msg.isFallback && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: "11px", color: "#9b9aa3", margin: "0 0 4px", fontFamily: "inherit" }, children: "Not sure about that one" }),
622
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: "14px", lineHeight: 1.55, color: "#2a2a36", fontFamily: "inherit", whiteSpace: "pre-wrap", wordBreak: "break-word" }, children: msg.text })
740
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: "14px", lineHeight: 1.55, color: "#2a2a36", fontFamily: "inherit", wordBreak: "break-word" }, className: "sd-r-markdown", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: renderedHtml } }) })
623
741
  ] })
624
742
  ] });
625
743
  }
@@ -667,6 +785,7 @@ function LightTypingIndicator({ accent }) {
667
785
  }
668
786
  function DarkMessageBubble({ msg, accent }) {
669
787
  const isBot = msg.role === "bot";
788
+ const renderedHtml = isBot ? parseMarkdown(msg.text) : msg.text;
670
789
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: {
671
790
  maxWidth: "85%",
672
791
  padding: "12px 14px",
@@ -678,12 +797,11 @@ function DarkMessageBubble({ msg, accent }) {
678
797
  color: isBot ? "rgba(255,255,255,0.92)" : "#fff",
679
798
  alignSelf: isBot ? "flex-start" : "flex-end",
680
799
  boxShadow: isBot ? "none" : `0 8px 20px -8px color-mix(in oklab, ${accent} 70%, transparent)`,
681
- whiteSpace: "pre-wrap",
682
800
  wordBreak: "break-word",
683
801
  fontFamily: "inherit"
684
- }, children: [
802
+ }, className: isBot ? "sd-r-markdown" : "", children: [
685
803
  msg.isFallback && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "11px", color: "rgba(255,255,255,0.5)", display: "block", marginBottom: "4px" }, children: "Not sure about that one" }),
686
- msg.text
804
+ isBot ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: renderedHtml } }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { whiteSpace: "pre-wrap" }, children: msg.text })
687
805
  ] });
688
806
  }
689
807
  function DarkTypingIndicator() {
@@ -1303,16 +1421,22 @@ function renderDark(p) {
1303
1421
  ] })
1304
1422
  ] });
1305
1423
  }
1306
- function SageDeskWidget({ indexUrl, agent, search: search2 }) {
1307
- if (!indexUrl) {
1424
+ function SageDeskWidget({ mode, indexUrl, endpoint, agent, search: search2 }) {
1425
+ const resolvedMode = mode ?? "local";
1426
+ if (!agent?.name) {
1427
+ throw new Error('[sagedesk] Required prop "agent.name" is missing.');
1428
+ }
1429
+ if (resolvedMode === "local" && !indexUrl) {
1308
1430
  throw new Error(
1309
- '[sagedesk] Required prop "indexUrl" is missing. Run `npx sagedesk build` and pass the output path, e.g. indexUrl="/support-index.json".'
1431
+ '[sagedesk] Required prop "indexUrl" is missing for local mode. Run `npx sagedesk build` and pass the output path, e.g. indexUrl="/support-index.json".'
1310
1432
  );
1311
1433
  }
1312
- if (!agent?.name) {
1313
- throw new Error('[sagedesk] Required prop "agent.name" is missing.');
1434
+ if (resolvedMode === "llm" && !endpoint) {
1435
+ throw new Error(
1436
+ '[sagedesk] Required prop "endpoint" is missing for llm mode. Provide your backend route, e.g. endpoint="/api/sagedesk".'
1437
+ );
1314
1438
  }
1315
- const config = { indexUrl, agent, search: search2 };
1439
+ const config = { mode: resolvedMode, indexUrl, endpoint, agent, search: search2 };
1316
1440
  const { state, chips, open, close, submit } = useSageDesk(config);
1317
1441
  const theme = agent.theme ?? "classic";
1318
1442
  const accent = agent.accentColor ?? "#534AB7";
@@ -1325,7 +1449,7 @@ function SageDeskWidget({ indexUrl, agent, search: search2 }) {
1325
1449
  const triggerRef = (0, import_react2.useRef)(null);
1326
1450
  const [mounted, setMounted] = (0, import_react2.useState)(false);
1327
1451
  (0, import_react2.useEffect)(() => {
1328
- if (!indexUrl.startsWith("/") && !indexUrl.startsWith("http")) {
1452
+ if (resolvedMode === "local" && indexUrl && !indexUrl.startsWith("/") && !indexUrl.startsWith("http")) {
1329
1453
  console.warn(
1330
1454
  `[sagedesk] indexUrl "${indexUrl}" looks like a relative path. It should start with "/" so it resolves correctly from any page.`
1331
1455
  );
@@ -1367,7 +1491,7 @@ function SageDeskWidget({ indexUrl, agent, search: search2 }) {
1367
1491
  [handleSubmit]
1368
1492
  );
1369
1493
  if (!mounted || typeof document === "undefined") return null;
1370
- const showPoweredBy = agent.poweredBy !== false;
1494
+ const showPoweredBy = true;
1371
1495
  const showChips = chips.length > 0;
1372
1496
  const panelClass = isClosing ? "sd-r-closing" : state.isOpen ? "sd-r-opening" : "";
1373
1497
  const props = {