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.
@@ -2,6 +2,7 @@ import React from 'react';
2
2
 
3
3
  type SageDeskModel = 'all-MiniLM-L6-v2' | 'bge-small-en-v1-5' | 'paraphrase-multilingual-MiniLM-L12-v2' | 'all-mpnet-base-v2';
4
4
  type Theme = 'classic' | 'light' | 'dark';
5
+ type SageDeskMode = 'local' | 'llm';
5
6
  interface AgentConfig {
6
7
  name: string;
7
8
  model?: SageDeskModel;
@@ -13,7 +14,6 @@ interface AgentConfig {
13
14
  position?: 'bottom-right' | 'bottom-left';
14
15
  avatarUrl?: string;
15
16
  contactUrl?: string;
16
- poweredBy?: boolean;
17
17
  suggestedChips?: string[];
18
18
  }
19
19
  interface SearchConfig {
@@ -21,25 +21,34 @@ interface SearchConfig {
21
21
  topK?: number;
22
22
  }
23
23
  interface SageDeskConfig {
24
- indexUrl: string;
24
+ mode?: SageDeskMode;
25
+ indexUrl?: string;
26
+ endpoint?: string;
25
27
  agent: AgentConfig;
26
28
  search?: SearchConfig;
27
29
  }
30
+ type FallbackReason = 'auth-error' | 'quota-exceeded' | 'timeout' | 'api-error' | 'malformed-response';
28
31
  interface ChatMessage {
29
32
  id: string;
30
33
  role: 'user' | 'bot';
31
34
  text: string;
32
35
  isFallback?: boolean;
36
+ fallbackReason?: FallbackReason;
33
37
  timestamp: Date;
34
38
  }
35
39
  type EngineStatus = 'idle' | 'loading-index' | 'loading-model' | 'ready' | 'error-index' | 'error-model' | 'degraded';
36
40
 
37
41
  interface SageDeskWidgetProps {
38
- indexUrl: string;
42
+ /** Operating mode. 'local' (default) runs entirely in the browser via WASM. 'llm' posts to the consumer's own backend. */
43
+ mode?: SageDeskMode;
44
+ /** URL to the pre-built vector index. Required in local mode. */
45
+ indexUrl?: string;
46
+ /** Consumer's backend endpoint that accepts POST { query }. Required in llm mode. */
47
+ endpoint?: string;
39
48
  agent: SageDeskConfig['agent'];
40
49
  search?: SageDeskConfig['search'];
41
50
  }
42
- declare function SageDeskWidget({ indexUrl, agent, search }: SageDeskWidgetProps): React.ReactPortal | null;
51
+ declare function SageDeskWidget({ mode, indexUrl, endpoint, agent, search }: SageDeskWidgetProps): React.ReactPortal | null;
43
52
 
44
53
  interface State {
45
54
  messages: ChatMessage[];
@@ -58,4 +67,4 @@ interface UseSageDeskReturn {
58
67
  }
59
68
  declare function useSageDesk(config: SageDeskConfig): UseSageDeskReturn;
60
69
 
61
- export { type AgentConfig, type ChatMessage, type SageDeskConfig, SageDeskWidget, type SageDeskWidgetProps, type SearchConfig, type UseSageDeskReturn, useSageDesk };
70
+ export { type AgentConfig, type ChatMessage, type SageDeskConfig, type SageDeskMode, SageDeskWidget, type SageDeskWidgetProps, type SearchConfig, type UseSageDeskReturn, useSageDesk };
@@ -226,6 +226,17 @@ function getFallback(config) {
226
226
  }
227
227
 
228
228
  // src/react/useSageDesk.ts
229
+ function logFallbackWarning(reason) {
230
+ if (!reason) return;
231
+ const messages = {
232
+ "auth-error": "[sagedesk] Support service authentication failed. Showing relevant knowledge instead.",
233
+ "quota-exceeded": "[sagedesk] Support service quota exhausted. Showing relevant knowledge instead.",
234
+ "timeout": "[sagedesk] Support service took too long to respond. Showing relevant knowledge instead.",
235
+ "api-error": "[sagedesk] Support service error. Showing relevant knowledge instead.",
236
+ "malformed-response": "[sagedesk] Support service returned invalid response. Showing relevant knowledge instead."
237
+ };
238
+ console.warn(messages[reason] || "[sagedesk] Support service unavailable. Showing relevant knowledge instead.");
239
+ }
229
240
  var initialState = {
230
241
  messages: [],
231
242
  isOpen: false,
@@ -278,6 +289,11 @@ function useSageDesk(config) {
278
289
  const startEngine = useCallback(async () => {
279
290
  if (engineStartedRef.current) return;
280
291
  engineStartedRef.current = true;
292
+ if (config.mode === "llm") {
293
+ setChips(config.agent.suggestedChips ?? []);
294
+ dispatch({ type: "SET_ENGINE_STATUS", payload: { status: "ready" } });
295
+ return;
296
+ }
281
297
  dispatch({ type: "SET_ENGINE_STATUS", payload: { status: "loading-index" } });
282
298
  try {
283
299
  indexRef.current = await fetchIndex(config.indexUrl);
@@ -304,7 +320,7 @@ function useSageDesk(config) {
304
320
  embedderRef.current = new EmbedderRuntime();
305
321
  dispatch({ type: "SET_ENGINE_STATUS", payload: { status: "degraded" } });
306
322
  }
307
- }, [config.indexUrl, config.agent.suggestedChips, addMessage]);
323
+ }, [config.mode, config.indexUrl, config.agent.suggestedChips, addMessage]);
308
324
  const greetingShownRef = useRef(false);
309
325
  const open = useCallback(() => {
310
326
  dispatch({ type: "OPEN" });
@@ -347,8 +363,37 @@ function useSageDesk(config) {
347
363
  }
348
364
  let botText;
349
365
  let isFallback = false;
350
- let mode = "keyword";
351
- if (!indexRef.current) {
366
+ let fallbackReason;
367
+ let retrievalMode = "keyword";
368
+ if (config.mode === "llm") {
369
+ if (!config.endpoint) {
370
+ console.warn('[sagedesk] LLM mode requires an "endpoint" prop.');
371
+ botText = getFallback(config.agent);
372
+ isFallback = true;
373
+ } else {
374
+ try {
375
+ const res = await fetch(config.endpoint, {
376
+ method: "POST",
377
+ headers: { "Content-Type": "application/json" },
378
+ body: JSON.stringify({ query: trimmed })
379
+ });
380
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
381
+ const data = await res.json();
382
+ if (data.isFallback || !data.answer) {
383
+ fallbackReason = data.fallbackReason;
384
+ logFallbackWarning(fallbackReason);
385
+ botText = getFallback(config.agent);
386
+ isFallback = true;
387
+ } else {
388
+ botText = data.answer;
389
+ }
390
+ } catch (err) {
391
+ console.warn("[sagedesk] Support service unavailable. Using cached knowledge instead.");
392
+ botText = getFallback(config.agent);
393
+ isFallback = true;
394
+ }
395
+ }
396
+ } else if (!indexRef.current) {
352
397
  botText = getFallback(config.agent);
353
398
  isFallback = true;
354
399
  } else {
@@ -359,7 +404,7 @@ function useSageDesk(config) {
359
404
  embedderRef.current,
360
405
  config.search
361
406
  );
362
- mode = res.mode;
407
+ retrievalMode = res.mode;
363
408
  if (res.results.length > 0) {
364
409
  botText = buildAnswer(res.results);
365
410
  } else {
@@ -372,11 +417,13 @@ function useSageDesk(config) {
372
417
  isFallback = true;
373
418
  }
374
419
  }
375
- const elapsed = Date.now() - typingStart;
376
- const delayBase = mode === "keyword" || isFallback ? 800 : 3e3;
377
- const minTypingMs = delayBase + Math.random() * 2e3;
378
- const remaining = minTypingMs - elapsed;
379
- if (remaining > 0) await new Promise((r) => setTimeout(r, remaining));
420
+ if (config.mode !== "llm") {
421
+ const elapsed = Date.now() - typingStart;
422
+ const delayBase = retrievalMode === "keyword" || isFallback ? 800 : 3e3;
423
+ const minTypingMs = delayBase + Math.random() * 2e3;
424
+ const remaining = minTypingMs - elapsed;
425
+ if (remaining > 0) await new Promise((r) => setTimeout(r, remaining));
426
+ }
380
427
  dispatch({ type: "SET_TYPING", payload: false });
381
428
  addMessage({ role: "bot", text: botText, isFallback });
382
429
  },
@@ -399,6 +446,54 @@ function useSageDesk(config) {
399
446
  return { state, chips: activeChips, open, close, submit };
400
447
  }
401
448
 
449
+ // src/react/markdownUtils.ts
450
+ import { marked } from "marked";
451
+ import DOMPurify from "dompurify";
452
+ marked.setOptions({
453
+ breaks: true,
454
+ gfm: true,
455
+ pedantic: false
456
+ });
457
+ var PURIFY_CONFIG = {
458
+ ALLOWED_TAGS: [
459
+ "p",
460
+ "br",
461
+ "strong",
462
+ "em",
463
+ "u",
464
+ "h1",
465
+ "h2",
466
+ "h3",
467
+ "h4",
468
+ "h5",
469
+ "h6",
470
+ "ul",
471
+ "ol",
472
+ "li",
473
+ "blockquote",
474
+ "code",
475
+ "pre",
476
+ "a",
477
+ "hr"
478
+ ],
479
+ ALLOWED_ATTR: ["href", "title", "target", "rel"],
480
+ ALLOW_DATA_ATTR: false
481
+ };
482
+ var HOOK_ALLOWLIST = ["a"];
483
+ DOMPurify.addHook("afterSanitizeAttributes", (node) => {
484
+ if (HOOK_ALLOWLIST.includes(node.tagName.toLowerCase())) {
485
+ if (node.tagName.toLowerCase() === "a") {
486
+ node.setAttribute("target", "_blank");
487
+ node.setAttribute("rel", "noopener noreferrer");
488
+ }
489
+ }
490
+ });
491
+ function parseMarkdown(markdown) {
492
+ const html = marked.parse(markdown);
493
+ const sanitized = DOMPurify.sanitize(html, PURIFY_CONFIG);
494
+ return sanitized;
495
+ }
496
+
402
497
  // src/react/SageDeskWidget.tsx
403
498
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
404
499
  var STYLE_ID = "sagedesk-widget-styles";
@@ -428,6 +523,28 @@ var SHARED = `
428
523
  }
429
524
  .sd-r-scrollable::-webkit-scrollbar { display: none !important; }
430
525
  .sd-r-scrollable > * { flex-shrink: 0 !important; }
526
+ .sd-r-markdown h1, .sd-r-markdown h2, .sd-r-markdown h3, .sd-r-markdown h4, .sd-r-markdown h5, .sd-r-markdown h6 {
527
+ margin: 12px 0 8px 0 !important; font-weight: 600 !important; line-height: 1.3 !important;
528
+ }
529
+ .sd-r-markdown h1 { font-size: 1.3em !important; }
530
+ .sd-r-markdown h2 { font-size: 1.2em !important; }
531
+ .sd-r-markdown h3 { font-size: 1.1em !important; }
532
+ .sd-r-markdown h4, .sd-r-markdown h5, .sd-r-markdown h6 { font-size: 1em !important; }
533
+ .sd-r-markdown strong { font-weight: 600 !important; }
534
+ .sd-r-markdown em { font-style: italic !important; }
535
+ .sd-r-markdown u { text-decoration: underline !important; }
536
+ .sd-r-markdown ul, .sd-r-markdown ol { margin: 8px 0 !important; padding-left: 20px !important; }
537
+ .sd-r-markdown li { margin: 4px 0 !important; }
538
+ .sd-r-markdown blockquote { margin: 8px 0 !important; padding-left: 12px !important; border-left: 3px solid currentColor !important; opacity: 0.8 !important; }
539
+ .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; }
540
+ .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; }
541
+ .sd-r-markdown pre code { background: none !important; padding: 0 !important; }
542
+ .sd-r-markdown hr { border: none !important; border-top: 1px solid currentColor !important; opacity: 0.3 !important; margin: 10px 0 !important; }
543
+ .sd-r-markdown a { text-decoration: underline !important; opacity: 0.9 !important; }
544
+ .sd-r-markdown a:hover { opacity: 1 !important; }
545
+ .sd-r-markdown p { margin: 6px 0 !important; }
546
+ .sd-r-markdown > *:first-child { margin-top: 0 !important; }
547
+ .sd-r-markdown > *:last-child { margin-bottom: 0 !important; }
431
548
  @media (max-width: 420px) {
432
549
  .sd-r-panel {
433
550
  bottom: 0 !important; right: 0 !important; left: 0 !important;
@@ -514,7 +631,7 @@ var PoweredBy = ({ dark = false }) => /* @__PURE__ */ jsxs("div", { style: {
514
631
  {
515
632
  href: "https://github.com/mzeeshanwahid/sagedesk",
516
633
  target: "_blank",
517
- rel: "noopener noreferrer",
634
+ rel: "noopener",
518
635
  style: {
519
636
  color: dark ? "rgba(255,255,255,0.7)" : "#5a5a64",
520
637
  fontWeight: 500,
@@ -526,6 +643,7 @@ var PoweredBy = ({ dark = false }) => /* @__PURE__ */ jsxs("div", { style: {
526
643
  ] });
527
644
  function ClassicMessageBubble({ msg, accent }) {
528
645
  const isBot = msg.role === "bot";
646
+ const renderedHtml = isBot ? parseMarkdown(msg.text) : msg.text;
529
647
  return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: isBot ? "flex-start" : "flex-end", gap: "4px" }, children: [
530
648
  msg.isFallback && /* @__PURE__ */ jsx("p", { style: { fontSize: "11px", fontWeight: 500, color: "#9b9aa3", margin: 0, padding: "0 4px", fontFamily: "inherit" }, children: "Not sure about that one" }),
531
649
  /* @__PURE__ */ jsx("div", { style: {
@@ -538,10 +656,9 @@ function ClassicMessageBubble({ msg, accent }) {
538
656
  color: isBot ? "#1a1a2e" : "#fff",
539
657
  border: isBot ? "1px solid rgba(20,20,40,0.06)" : "none",
540
658
  boxShadow: isBot ? "0 1px 2px rgba(20,20,40,0.04)" : `0 6px 16px -6px color-mix(in oklab, ${accent} 60%, transparent)`,
541
- whiteSpace: "pre-wrap",
542
659
  wordBreak: "break-word",
543
660
  fontFamily: "inherit"
544
- }, children: msg.text }),
661
+ }, className: "sd-r-markdown", children: isBot ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderedHtml } }) : /* @__PURE__ */ jsx("div", { style: { whiteSpace: "pre-wrap" }, children: msg.text }) }),
545
662
  /* @__PURE__ */ jsx("span", { style: {
546
663
  fontSize: "11px",
547
664
  color: "#a8a8b0",
@@ -571,6 +688,7 @@ function ClassicTypingIndicator() {
571
688
  }
572
689
  function LightMessageBubble({ msg, accent, agentName }) {
573
690
  const isBot = msg.role === "bot";
691
+ const renderedHtml = isBot ? parseMarkdown(msg.text) : msg.text;
574
692
  if (isBot) {
575
693
  return /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "10px" }, children: [
576
694
  /* @__PURE__ */ jsx("div", { style: {
@@ -587,7 +705,7 @@ function LightMessageBubble({ msg, accent, agentName }) {
587
705
  /* @__PURE__ */ jsx("span", { style: { fontSize: "11px", color: "#a8a89e", fontVariantNumeric: "tabular-nums", fontFamily: "inherit" }, children: "just now" })
588
706
  ] }),
589
707
  msg.isFallback && /* @__PURE__ */ jsx("p", { style: { fontSize: "11px", color: "#9b9aa3", margin: "0 0 4px", fontFamily: "inherit" }, children: "Not sure about that one" }),
590
- /* @__PURE__ */ jsx("div", { style: { fontSize: "14px", lineHeight: 1.55, color: "#2a2a36", fontFamily: "inherit", whiteSpace: "pre-wrap", wordBreak: "break-word" }, children: msg.text })
708
+ /* @__PURE__ */ jsx("div", { style: { fontSize: "14px", lineHeight: 1.55, color: "#2a2a36", fontFamily: "inherit", wordBreak: "break-word" }, className: "sd-r-markdown", children: /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderedHtml } }) })
591
709
  ] })
592
710
  ] });
593
711
  }
@@ -635,6 +753,7 @@ function LightTypingIndicator({ accent }) {
635
753
  }
636
754
  function DarkMessageBubble({ msg, accent }) {
637
755
  const isBot = msg.role === "bot";
756
+ const renderedHtml = isBot ? parseMarkdown(msg.text) : msg.text;
638
757
  return /* @__PURE__ */ jsxs("div", { style: {
639
758
  maxWidth: "85%",
640
759
  padding: "12px 14px",
@@ -646,12 +765,11 @@ function DarkMessageBubble({ msg, accent }) {
646
765
  color: isBot ? "rgba(255,255,255,0.92)" : "#fff",
647
766
  alignSelf: isBot ? "flex-start" : "flex-end",
648
767
  boxShadow: isBot ? "none" : `0 8px 20px -8px color-mix(in oklab, ${accent} 70%, transparent)`,
649
- whiteSpace: "pre-wrap",
650
768
  wordBreak: "break-word",
651
769
  fontFamily: "inherit"
652
- }, children: [
770
+ }, className: isBot ? "sd-r-markdown" : "", children: [
653
771
  msg.isFallback && /* @__PURE__ */ jsx("span", { style: { fontSize: "11px", color: "rgba(255,255,255,0.5)", display: "block", marginBottom: "4px" }, children: "Not sure about that one" }),
654
- msg.text
772
+ isBot ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderedHtml } }) : /* @__PURE__ */ jsx("div", { style: { whiteSpace: "pre-wrap" }, children: msg.text })
655
773
  ] });
656
774
  }
657
775
  function DarkTypingIndicator() {
@@ -1271,16 +1389,22 @@ function renderDark(p) {
1271
1389
  ] })
1272
1390
  ] });
1273
1391
  }
1274
- function SageDeskWidget({ indexUrl, agent, search: search2 }) {
1275
- if (!indexUrl) {
1392
+ function SageDeskWidget({ mode, indexUrl, endpoint, agent, search: search2 }) {
1393
+ const resolvedMode = mode ?? "local";
1394
+ if (!agent?.name) {
1395
+ throw new Error('[sagedesk] Required prop "agent.name" is missing.');
1396
+ }
1397
+ if (resolvedMode === "local" && !indexUrl) {
1276
1398
  throw new Error(
1277
- '[sagedesk] Required prop "indexUrl" is missing. Run `npx sagedesk build` and pass the output path, e.g. indexUrl="/support-index.json".'
1399
+ '[sagedesk] Required prop "indexUrl" is missing for local mode. Run `npx sagedesk build` and pass the output path, e.g. indexUrl="/support-index.json".'
1278
1400
  );
1279
1401
  }
1280
- if (!agent?.name) {
1281
- throw new Error('[sagedesk] Required prop "agent.name" is missing.');
1402
+ if (resolvedMode === "llm" && !endpoint) {
1403
+ throw new Error(
1404
+ '[sagedesk] Required prop "endpoint" is missing for llm mode. Provide your backend route, e.g. endpoint="/api/sagedesk".'
1405
+ );
1282
1406
  }
1283
- const config = { indexUrl, agent, search: search2 };
1407
+ const config = { mode: resolvedMode, indexUrl, endpoint, agent, search: search2 };
1284
1408
  const { state, chips, open, close, submit } = useSageDesk(config);
1285
1409
  const theme = agent.theme ?? "classic";
1286
1410
  const accent = agent.accentColor ?? "#534AB7";
@@ -1293,7 +1417,7 @@ function SageDeskWidget({ indexUrl, agent, search: search2 }) {
1293
1417
  const triggerRef = useRef2(null);
1294
1418
  const [mounted, setMounted] = useState2(false);
1295
1419
  useEffect2(() => {
1296
- if (!indexUrl.startsWith("/") && !indexUrl.startsWith("http")) {
1420
+ if (resolvedMode === "local" && indexUrl && !indexUrl.startsWith("/") && !indexUrl.startsWith("http")) {
1297
1421
  console.warn(
1298
1422
  `[sagedesk] indexUrl "${indexUrl}" looks like a relative path. It should start with "/" so it resolves correctly from any page.`
1299
1423
  );
@@ -1335,7 +1459,7 @@ function SageDeskWidget({ indexUrl, agent, search: search2 }) {
1335
1459
  [handleSubmit]
1336
1460
  );
1337
1461
  if (!mounted || typeof document === "undefined") return null;
1338
- const showPoweredBy = agent.poweredBy !== false;
1462
+ const showPoweredBy = true;
1339
1463
  const showChips = chips.length > 0;
1340
1464
  const panelClass = isClosing ? "sd-r-closing" : state.isOpen ? "sd-r-opening" : "";
1341
1465
  const props = {