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.
@@ -275,6 +275,17 @@ var init_fallback = __esm({
275
275
  });
276
276
 
277
277
  // src/react/useSageDesk.ts
278
+ function logFallbackWarning(reason) {
279
+ if (!reason) return;
280
+ const messages = {
281
+ "auth-error": "[sagedesk] Support service authentication failed. Showing relevant knowledge instead.",
282
+ "quota-exceeded": "[sagedesk] Support service quota exhausted. Showing relevant knowledge instead.",
283
+ "timeout": "[sagedesk] Support service took too long to respond. Showing relevant knowledge instead.",
284
+ "api-error": "[sagedesk] Support service error. Showing relevant knowledge instead.",
285
+ "malformed-response": "[sagedesk] Support service returned invalid response. Showing relevant knowledge instead."
286
+ };
287
+ console.warn(messages[reason] || "[sagedesk] Support service unavailable. Showing relevant knowledge instead.");
288
+ }
278
289
  function reducer(state, action) {
279
290
  switch (action.type) {
280
291
  case "OPEN":
@@ -319,6 +330,11 @@ function useSageDesk(config) {
319
330
  const startEngine = (0, import_react.useCallback)(async () => {
320
331
  if (engineStartedRef.current) return;
321
332
  engineStartedRef.current = true;
333
+ if (config.mode === "llm") {
334
+ setChips(config.agent.suggestedChips ?? []);
335
+ dispatch({ type: "SET_ENGINE_STATUS", payload: { status: "ready" } });
336
+ return;
337
+ }
322
338
  dispatch({ type: "SET_ENGINE_STATUS", payload: { status: "loading-index" } });
323
339
  try {
324
340
  indexRef.current = await fetchIndex(config.indexUrl);
@@ -345,7 +361,7 @@ function useSageDesk(config) {
345
361
  embedderRef.current = new EmbedderRuntime();
346
362
  dispatch({ type: "SET_ENGINE_STATUS", payload: { status: "degraded" } });
347
363
  }
348
- }, [config.indexUrl, config.agent.suggestedChips, addMessage]);
364
+ }, [config.mode, config.indexUrl, config.agent.suggestedChips, addMessage]);
349
365
  const greetingShownRef = (0, import_react.useRef)(false);
350
366
  const open = (0, import_react.useCallback)(() => {
351
367
  dispatch({ type: "OPEN" });
@@ -388,8 +404,37 @@ function useSageDesk(config) {
388
404
  }
389
405
  let botText;
390
406
  let isFallback = false;
391
- let mode = "keyword";
392
- if (!indexRef.current) {
407
+ let fallbackReason;
408
+ let retrievalMode = "keyword";
409
+ if (config.mode === "llm") {
410
+ if (!config.endpoint) {
411
+ console.warn('[sagedesk] LLM mode requires an "endpoint" prop.');
412
+ botText = getFallback(config.agent);
413
+ isFallback = true;
414
+ } else {
415
+ try {
416
+ const res = await fetch(config.endpoint, {
417
+ method: "POST",
418
+ headers: { "Content-Type": "application/json" },
419
+ body: JSON.stringify({ query: trimmed })
420
+ });
421
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
422
+ const data = await res.json();
423
+ if (data.isFallback || !data.answer) {
424
+ fallbackReason = data.fallbackReason;
425
+ logFallbackWarning(fallbackReason);
426
+ botText = getFallback(config.agent);
427
+ isFallback = true;
428
+ } else {
429
+ botText = data.answer;
430
+ }
431
+ } catch (err) {
432
+ console.warn("[sagedesk] Support service unavailable. Using cached knowledge instead.");
433
+ botText = getFallback(config.agent);
434
+ isFallback = true;
435
+ }
436
+ }
437
+ } else if (!indexRef.current) {
393
438
  botText = getFallback(config.agent);
394
439
  isFallback = true;
395
440
  } else {
@@ -400,7 +445,7 @@ function useSageDesk(config) {
400
445
  embedderRef.current,
401
446
  config.search
402
447
  );
403
- mode = res.mode;
448
+ retrievalMode = res.mode;
404
449
  if (res.results.length > 0) {
405
450
  botText = buildAnswer(res.results);
406
451
  } else {
@@ -413,11 +458,13 @@ function useSageDesk(config) {
413
458
  isFallback = true;
414
459
  }
415
460
  }
416
- const elapsed = Date.now() - typingStart;
417
- const delayBase = mode === "keyword" || isFallback ? 800 : 3e3;
418
- const minTypingMs = delayBase + Math.random() * 2e3;
419
- const remaining = minTypingMs - elapsed;
420
- if (remaining > 0) await new Promise((r) => setTimeout(r, remaining));
461
+ if (config.mode !== "llm") {
462
+ const elapsed = Date.now() - typingStart;
463
+ const delayBase = retrievalMode === "keyword" || isFallback ? 800 : 3e3;
464
+ const minTypingMs = delayBase + Math.random() * 2e3;
465
+ const remaining = minTypingMs - elapsed;
466
+ if (remaining > 0) await new Promise((r) => setTimeout(r, remaining));
467
+ }
421
468
  dispatch({ type: "SET_TYPING", payload: false });
422
469
  addMessage({ role: "bot", text: botText, isFallback });
423
470
  },
@@ -460,6 +507,60 @@ var init_useSageDesk = __esm({
460
507
  }
461
508
  });
462
509
 
510
+ // src/react/markdownUtils.ts
511
+ function parseMarkdown(markdown) {
512
+ const html = import_marked.marked.parse(markdown);
513
+ const sanitized = import_dompurify.default.sanitize(html, PURIFY_CONFIG);
514
+ return sanitized;
515
+ }
516
+ var import_marked, import_dompurify, PURIFY_CONFIG, HOOK_ALLOWLIST;
517
+ var init_markdownUtils = __esm({
518
+ "src/react/markdownUtils.ts"() {
519
+ "use strict";
520
+ import_marked = require("marked");
521
+ import_dompurify = __toESM(require("dompurify"), 1);
522
+ import_marked.marked.setOptions({
523
+ breaks: true,
524
+ gfm: true,
525
+ pedantic: false
526
+ });
527
+ PURIFY_CONFIG = {
528
+ ALLOWED_TAGS: [
529
+ "p",
530
+ "br",
531
+ "strong",
532
+ "em",
533
+ "u",
534
+ "h1",
535
+ "h2",
536
+ "h3",
537
+ "h4",
538
+ "h5",
539
+ "h6",
540
+ "ul",
541
+ "ol",
542
+ "li",
543
+ "blockquote",
544
+ "code",
545
+ "pre",
546
+ "a",
547
+ "hr"
548
+ ],
549
+ ALLOWED_ATTR: ["href", "title", "target", "rel"],
550
+ ALLOW_DATA_ATTR: false
551
+ };
552
+ HOOK_ALLOWLIST = ["a"];
553
+ import_dompurify.default.addHook("afterSanitizeAttributes", (node) => {
554
+ if (HOOK_ALLOWLIST.includes(node.tagName.toLowerCase())) {
555
+ if (node.tagName.toLowerCase() === "a") {
556
+ node.setAttribute("target", "_blank");
557
+ node.setAttribute("rel", "noopener noreferrer");
558
+ }
559
+ }
560
+ });
561
+ }
562
+ });
563
+
463
564
  // src/react/SageDeskWidget.tsx
464
565
  var SageDeskWidget_exports = {};
465
566
  __export(SageDeskWidget_exports, {
@@ -476,6 +577,7 @@ function injectStyles(theme) {
476
577
  }
477
578
  function ClassicMessageBubble({ msg, accent }) {
478
579
  const isBot = msg.role === "bot";
580
+ const renderedHtml = isBot ? parseMarkdown(msg.text) : msg.text;
479
581
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", alignItems: isBot ? "flex-start" : "flex-end", gap: "4px" }, children: [
480
582
  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" }),
481
583
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
@@ -488,10 +590,9 @@ function ClassicMessageBubble({ msg, accent }) {
488
590
  color: isBot ? "#1a1a2e" : "#fff",
489
591
  border: isBot ? "1px solid rgba(20,20,40,0.06)" : "none",
490
592
  boxShadow: isBot ? "0 1px 2px rgba(20,20,40,0.04)" : `0 6px 16px -6px color-mix(in oklab, ${accent} 60%, transparent)`,
491
- whiteSpace: "pre-wrap",
492
593
  wordBreak: "break-word",
493
594
  fontFamily: "inherit"
494
- }, children: msg.text }),
595
+ }, 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 }) }),
495
596
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: {
496
597
  fontSize: "11px",
497
598
  color: "#a8a8b0",
@@ -521,6 +622,7 @@ function ClassicTypingIndicator() {
521
622
  }
522
623
  function LightMessageBubble({ msg, accent, agentName }) {
523
624
  const isBot = msg.role === "bot";
625
+ const renderedHtml = isBot ? parseMarkdown(msg.text) : msg.text;
524
626
  if (isBot) {
525
627
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "10px" }, children: [
526
628
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
@@ -537,7 +639,7 @@ function LightMessageBubble({ msg, accent, agentName }) {
537
639
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "11px", color: "#a8a89e", fontVariantNumeric: "tabular-nums", fontFamily: "inherit" }, children: "just now" })
538
640
  ] }),
539
641
  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" }),
540
- /* @__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 })
642
+ /* @__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 } }) })
541
643
  ] })
542
644
  ] });
543
645
  }
@@ -585,6 +687,7 @@ function LightTypingIndicator({ accent }) {
585
687
  }
586
688
  function DarkMessageBubble({ msg, accent }) {
587
689
  const isBot = msg.role === "bot";
690
+ const renderedHtml = isBot ? parseMarkdown(msg.text) : msg.text;
588
691
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: {
589
692
  maxWidth: "85%",
590
693
  padding: "12px 14px",
@@ -596,12 +699,11 @@ function DarkMessageBubble({ msg, accent }) {
596
699
  color: isBot ? "rgba(255,255,255,0.92)" : "#fff",
597
700
  alignSelf: isBot ? "flex-start" : "flex-end",
598
701
  boxShadow: isBot ? "none" : `0 8px 20px -8px color-mix(in oklab, ${accent} 70%, transparent)`,
599
- whiteSpace: "pre-wrap",
600
702
  wordBreak: "break-word",
601
703
  fontFamily: "inherit"
602
- }, children: [
704
+ }, className: isBot ? "sd-r-markdown" : "", children: [
603
705
  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" }),
604
- msg.text
706
+ 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 })
605
707
  ] });
606
708
  }
607
709
  function DarkTypingIndicator() {
@@ -1221,16 +1323,22 @@ function renderDark(p) {
1221
1323
  ] })
1222
1324
  ] });
1223
1325
  }
1224
- function SageDeskWidget({ indexUrl, agent, search: search2 }) {
1225
- if (!indexUrl) {
1326
+ function SageDeskWidget({ mode, indexUrl, endpoint, agent, search: search2 }) {
1327
+ const resolvedMode = mode ?? "local";
1328
+ if (!agent?.name) {
1329
+ throw new Error('[sagedesk] Required prop "agent.name" is missing.');
1330
+ }
1331
+ if (resolvedMode === "local" && !indexUrl) {
1226
1332
  throw new Error(
1227
- '[sagedesk] Required prop "indexUrl" is missing. Run `npx sagedesk build` and pass the output path, e.g. indexUrl="/support-index.json".'
1333
+ '[sagedesk] Required prop "indexUrl" is missing for local mode. Run `npx sagedesk build` and pass the output path, e.g. indexUrl="/support-index.json".'
1228
1334
  );
1229
1335
  }
1230
- if (!agent?.name) {
1231
- throw new Error('[sagedesk] Required prop "agent.name" is missing.');
1336
+ if (resolvedMode === "llm" && !endpoint) {
1337
+ throw new Error(
1338
+ '[sagedesk] Required prop "endpoint" is missing for llm mode. Provide your backend route, e.g. endpoint="/api/sagedesk".'
1339
+ );
1232
1340
  }
1233
- const config = { indexUrl, agent, search: search2 };
1341
+ const config = { mode: resolvedMode, indexUrl, endpoint, agent, search: search2 };
1234
1342
  const { state, chips, open, close, submit } = useSageDesk(config);
1235
1343
  const theme = agent.theme ?? "classic";
1236
1344
  const accent = agent.accentColor ?? "#534AB7";
@@ -1243,7 +1351,7 @@ function SageDeskWidget({ indexUrl, agent, search: search2 }) {
1243
1351
  const triggerRef = (0, import_react2.useRef)(null);
1244
1352
  const [mounted, setMounted] = (0, import_react2.useState)(false);
1245
1353
  (0, import_react2.useEffect)(() => {
1246
- if (!indexUrl.startsWith("/") && !indexUrl.startsWith("http")) {
1354
+ if (resolvedMode === "local" && indexUrl && !indexUrl.startsWith("/") && !indexUrl.startsWith("http")) {
1247
1355
  console.warn(
1248
1356
  `[sagedesk] indexUrl "${indexUrl}" looks like a relative path. It should start with "/" so it resolves correctly from any page.`
1249
1357
  );
@@ -1285,7 +1393,7 @@ function SageDeskWidget({ indexUrl, agent, search: search2 }) {
1285
1393
  [handleSubmit]
1286
1394
  );
1287
1395
  if (!mounted || typeof document === "undefined") return null;
1288
- const showPoweredBy = agent.poweredBy !== false;
1396
+ const showPoweredBy = true;
1289
1397
  const showChips = chips.length > 0;
1290
1398
  const panelClass = isClosing ? "sd-r-closing" : state.isOpen ? "sd-r-opening" : "";
1291
1399
  const props = {
@@ -1319,6 +1427,7 @@ var init_SageDeskWidget = __esm({
1319
1427
  import_react2 = require("react");
1320
1428
  import_react_dom = require("react-dom");
1321
1429
  init_useSageDesk();
1430
+ init_markdownUtils();
1322
1431
  import_jsx_runtime = require("react/jsx-runtime");
1323
1432
  STYLE_ID = "sagedesk-widget-styles";
1324
1433
  SHARED = `
@@ -1347,6 +1456,28 @@ var init_SageDeskWidget = __esm({
1347
1456
  }
1348
1457
  .sd-r-scrollable::-webkit-scrollbar { display: none !important; }
1349
1458
  .sd-r-scrollable > * { flex-shrink: 0 !important; }
1459
+ .sd-r-markdown h1, .sd-r-markdown h2, .sd-r-markdown h3, .sd-r-markdown h4, .sd-r-markdown h5, .sd-r-markdown h6 {
1460
+ margin: 12px 0 8px 0 !important; font-weight: 600 !important; line-height: 1.3 !important;
1461
+ }
1462
+ .sd-r-markdown h1 { font-size: 1.3em !important; }
1463
+ .sd-r-markdown h2 { font-size: 1.2em !important; }
1464
+ .sd-r-markdown h3 { font-size: 1.1em !important; }
1465
+ .sd-r-markdown h4, .sd-r-markdown h5, .sd-r-markdown h6 { font-size: 1em !important; }
1466
+ .sd-r-markdown strong { font-weight: 600 !important; }
1467
+ .sd-r-markdown em { font-style: italic !important; }
1468
+ .sd-r-markdown u { text-decoration: underline !important; }
1469
+ .sd-r-markdown ul, .sd-r-markdown ol { margin: 8px 0 !important; padding-left: 20px !important; }
1470
+ .sd-r-markdown li { margin: 4px 0 !important; }
1471
+ .sd-r-markdown blockquote { margin: 8px 0 !important; padding-left: 12px !important; border-left: 3px solid currentColor !important; opacity: 0.8 !important; }
1472
+ .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; }
1473
+ .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; }
1474
+ .sd-r-markdown pre code { background: none !important; padding: 0 !important; }
1475
+ .sd-r-markdown hr { border: none !important; border-top: 1px solid currentColor !important; opacity: 0.3 !important; margin: 10px 0 !important; }
1476
+ .sd-r-markdown a { text-decoration: underline !important; opacity: 0.9 !important; }
1477
+ .sd-r-markdown a:hover { opacity: 1 !important; }
1478
+ .sd-r-markdown p { margin: 6px 0 !important; }
1479
+ .sd-r-markdown > *:first-child { margin-top: 0 !important; }
1480
+ .sd-r-markdown > *:last-child { margin-bottom: 0 !important; }
1350
1481
  @media (max-width: 420px) {
1351
1482
  .sd-r-panel {
1352
1483
  bottom: 0 !important; right: 0 !important; left: 0 !important;
@@ -1424,7 +1555,7 @@ var init_SageDeskWidget = __esm({
1424
1555
  {
1425
1556
  href: "https://github.com/mzeeshanwahid/sagedesk",
1426
1557
  target: "_blank",
1427
- rel: "noopener noreferrer",
1558
+ rel: "noopener",
1428
1559
  style: {
1429
1560
  color: dark ? "rgba(255,255,255,0.7)" : "#5a5a64",
1430
1561
  fontWeight: 500,
@@ -1457,17 +1588,23 @@ var LazyWidget = (0, import_react3.lazy)(
1457
1588
  function SageDeskNext(props) {
1458
1589
  const [mounted, setMounted] = (0, import_react3.useState)(false);
1459
1590
  (0, import_react3.useEffect)(() => {
1460
- if (!props.indexUrl) {
1591
+ const mode = props.mode ?? "local";
1592
+ if (mode === "local" && !props.indexUrl) {
1461
1593
  console.warn(
1462
1594
  '[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/).'
1463
1595
  );
1464
- } else if (!props.indexUrl.startsWith("/") && !props.indexUrl.startsWith("http")) {
1596
+ } else if (mode === "llm" && !props.endpoint) {
1597
+ console.warn(
1598
+ '[sagedesk] Missing required prop: endpoint for LLM mode. The widget will not load.\nProvide your backend route, e.g. endpoint="/api/sagedesk".'
1599
+ );
1600
+ }
1601
+ if (props.indexUrl && !props.indexUrl.startsWith("/") && !props.indexUrl.startsWith("http")) {
1465
1602
  console.warn(
1466
1603
  `[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.`
1467
1604
  );
1468
1605
  }
1469
1606
  setMounted(true);
1470
- }, []);
1607
+ }, [props.mode, props.indexUrl, props.endpoint]);
1471
1608
  if (!mounted) return null;
1472
1609
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react3.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LazyWidget, { ...props }) });
1473
1610
  }