sagedesk 2.1.1 → 2.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.
@@ -1,9 +1,10 @@
1
1
  // src/react/SageDeskWidget.tsx
2
- import {
2
+ import React, {
3
3
  useState as useState2,
4
4
  useRef as useRef2,
5
5
  useEffect as useEffect2,
6
- useCallback as useCallback2
6
+ useCallback as useCallback2,
7
+ useMemo as useMemo2
7
8
  } from "react";
8
9
  import { createPortal } from "react-dom";
9
10
 
@@ -263,6 +264,7 @@ function logFallbackWarning(reason) {
263
264
  }
264
265
  var initialState = {
265
266
  messages: [],
267
+ userMessages: [],
266
268
  isOpen: false,
267
269
  isTyping: false,
268
270
  engineStatus: "idle",
@@ -275,8 +277,11 @@ function reducer(state, action) {
275
277
  return { ...state, isOpen: true };
276
278
  case "CLOSE":
277
279
  return { ...state, isOpen: false };
278
- case "ADD_MESSAGE":
279
- return { ...state, messages: [...state.messages, action.payload] };
280
+ case "ADD_MESSAGE": {
281
+ const newMessages = [...state.messages, action.payload];
282
+ const newUserMessages = action.payload.role === "user" ? [...state.userMessages, action.payload] : state.userMessages;
283
+ return { ...state, messages: newMessages, userMessages: newUserMessages };
284
+ }
280
285
  case "SET_TYPING":
281
286
  return { ...state, isTyping: action.payload };
282
287
  case "SET_ENGINE_STATUS":
@@ -299,6 +304,7 @@ function useSageDesk(config) {
299
304
  const embedderRef = useRef(null);
300
305
  const engineStartedRef = useRef(false);
301
306
  const msgCounterRef = useRef(0);
307
+ const engineReadyCallbacksRef = useRef([]);
302
308
  const [chips, setChips] = useState([]);
303
309
  const makeId = () => `msg-${++msgCounterRef.current}`;
304
310
  const addMessage = useCallback(
@@ -310,12 +316,27 @@ function useSageDesk(config) {
310
316
  },
311
317
  []
312
318
  );
319
+ const notifyEngineReady = useCallback(() => {
320
+ const callbacks = engineReadyCallbacksRef.current;
321
+ engineReadyCallbacksRef.current = [];
322
+ callbacks.forEach((cb) => cb());
323
+ }, []);
313
324
  const startEngine = useCallback(async () => {
314
325
  if (engineStartedRef.current) return;
315
326
  engineStartedRef.current = true;
316
327
  if (config.mode === "llm") {
317
328
  setChips(config.agent.suggestedChips ?? []);
318
- dispatch({ type: "SET_ENGINE_STATUS", payload: { status: "ready" } });
329
+ dispatch({ type: "SET_ENGINE_STATUS", payload: { status: "loading-model" } });
330
+ try {
331
+ embedderRef.current = new EmbedderRuntime();
332
+ await embedderRef.current.load(config.agent.model);
333
+ dispatch({ type: "SET_ENGINE_STATUS", payload: { status: "ready" } });
334
+ notifyEngineReady();
335
+ } catch (err) {
336
+ console.warn("[sagedesk] WASM embedder failed to load - LLM mode will use fallback messages.", err);
337
+ dispatch({ type: "SET_ENGINE_STATUS", payload: { status: "error-model", error: String(err) } });
338
+ notifyEngineReady();
339
+ }
319
340
  return;
320
341
  }
321
342
  dispatch({ type: "SET_ENGINE_STATUS", payload: { status: "loading-index" } });
@@ -327,6 +348,7 @@ function useSageDesk(config) {
327
348
  type: "SET_ENGINE_STATUS",
328
349
  payload: { status: "error-index", error: String(err) }
329
350
  });
351
+ notifyEngineReady();
330
352
  addMessage({
331
353
  role: "bot",
332
354
  text: "I'm having trouble loading right now. Please try again in a moment."
@@ -339,12 +361,14 @@ function useSageDesk(config) {
339
361
  embedderRef.current = new EmbedderRuntime();
340
362
  await embedderRef.current.load(config.agent.model);
341
363
  dispatch({ type: "SET_ENGINE_STATUS", payload: { status: "ready" } });
364
+ notifyEngineReady();
342
365
  } catch (err) {
343
366
  console.warn("[sagedesk] WASM model failed to load, falling back to keyword search -", err);
344
367
  embedderRef.current = new EmbedderRuntime();
345
368
  dispatch({ type: "SET_ENGINE_STATUS", payload: { status: "degraded" } });
369
+ notifyEngineReady();
346
370
  }
347
- }, [config.mode, config.indexUrl, config.agent.suggestedChips, addMessage]);
371
+ }, [config.mode, config.indexUrl, config.agent.model, config.agent.suggestedChips, addMessage, notifyEngineReady]);
348
372
  const greetingShownRef = useRef(false);
349
373
  const open = useCallback(() => {
350
374
  dispatch({ type: "OPEN" });
@@ -361,16 +385,12 @@ function useSageDesk(config) {
361
385
  dispatch({ type: "CLOSE" });
362
386
  }, []);
363
387
  const waitForEngine = useCallback(() => {
388
+ const s = engineStatusRef.current;
389
+ if (s === "ready" || s === "degraded" || s === "error-index" || s === "error-model") {
390
+ return Promise.resolve();
391
+ }
364
392
  return new Promise((resolve) => {
365
- const check = () => {
366
- const s = engineStatusRef.current;
367
- if (s === "ready" || s === "degraded" || s === "error-index" || s === "error-model") {
368
- resolve();
369
- } else {
370
- setTimeout(check, 100);
371
- }
372
- };
373
- check();
393
+ engineReadyCallbacksRef.current.push(resolve);
374
394
  });
375
395
  }, []);
376
396
  const submit = useCallback(
@@ -394,12 +414,20 @@ function useSageDesk(config) {
394
414
  console.warn('[sagedesk] LLM mode requires an "endpoint" prop.');
395
415
  botText = getFallback(config.agent);
396
416
  isFallback = true;
417
+ } else if (!embedderRef.current?.isReady) {
418
+ console.warn("[sagedesk] Embedder not ready - showing fallback.");
419
+ botText = getFallback(config.agent);
420
+ isFallback = true;
397
421
  } else {
398
422
  try {
423
+ const vector = await embedderRef.current.embed(trimmed);
399
424
  const res = await fetch(config.endpoint, {
400
425
  method: "POST",
401
426
  headers: { "Content-Type": "application/json" },
402
- body: JSON.stringify({ query: trimmed })
427
+ body: JSON.stringify({
428
+ query: trimmed,
429
+ queryVector: Array.from(vector)
430
+ })
403
431
  });
404
432
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
405
433
  const data = await res.json();
@@ -443,8 +471,8 @@ function useSageDesk(config) {
443
471
  }
444
472
  if (config.mode !== "llm") {
445
473
  const elapsed = Date.now() - typingStart;
446
- const delayBase = retrievalMode === "keyword" || isFallback ? 800 : 3e3;
447
- const minTypingMs = delayBase + Math.random() * 2e3;
474
+ const delayBase = retrievalMode === "keyword" || isFallback ? 400 : 800;
475
+ const minTypingMs = delayBase + Math.random() * 400;
448
476
  const remaining = minTypingMs - elapsed;
449
477
  if (remaining > 0) await new Promise((r) => setTimeout(r, remaining));
450
478
  }
@@ -462,11 +490,9 @@ function useSageDesk(config) {
462
490
  return () => document.removeEventListener("keydown", handler);
463
491
  }, [state.isOpen, close]);
464
492
  const activeChips = useMemo(() => {
465
- const askedTexts = new Set(
466
- state.messages.filter((m) => m.role === "user").map((m) => m.text.toLowerCase().trim())
467
- );
493
+ const askedTexts = new Set(state.userMessages.map((m) => m.text.toLowerCase().trim()));
468
494
  return chips.filter((chip) => !askedTexts.has(chip.toLowerCase().trim()));
469
- }, [chips, state.messages]);
495
+ }, [chips, state.userMessages]);
470
496
  return { state, chips: activeChips, open, close, submit };
471
497
  }
472
498
 
@@ -662,9 +688,9 @@ var PoweredBy = ({ dark = false }) => /* @__PURE__ */ jsxs("div", { style: {
662
688
  }
663
689
  )
664
690
  ] });
665
- function ClassicMessageBubble({ msg, accent }) {
691
+ var ClassicMessageBubble = React.memo(function ClassicMessageBubble2({ msg, accent }) {
666
692
  const isBot = msg.role === "bot";
667
- const renderedHtml = isBot ? parseMarkdown(msg.text) : msg.text;
693
+ const renderedHtml = useMemo2(() => isBot ? parseMarkdown(msg.text) : msg.text, [isBot, msg.text]);
668
694
  return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: isBot ? "flex-start" : "flex-end", gap: "4px" }, children: [
669
695
  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" }),
670
696
  /* @__PURE__ */ jsx("div", { style: {
@@ -689,7 +715,7 @@ function ClassicMessageBubble({ msg, accent }) {
689
715
  fontFamily: "inherit"
690
716
  }, children: "just now" })
691
717
  ] });
692
- }
718
+ });
693
719
  function ClassicTypingIndicator() {
694
720
  const dot = { width: 6, height: 6, borderRadius: "50%", background: "#c8c8ce", display: "inline-block" };
695
721
  return /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", alignItems: "flex-start", gap: "4px" }, children: /* @__PURE__ */ jsxs("div", { style: {
@@ -707,9 +733,9 @@ function ClassicTypingIndicator() {
707
733
  /* @__PURE__ */ jsx("span", { style: dot, className: "sd-r-dot-3" })
708
734
  ] }) });
709
735
  }
710
- function LightMessageBubble({ msg, accent, agentName }) {
736
+ var LightMessageBubble = React.memo(function LightMessageBubble2({ msg, accent, agentName }) {
711
737
  const isBot = msg.role === "bot";
712
- const renderedHtml = isBot ? parseMarkdown(msg.text) : msg.text;
738
+ const renderedHtml = useMemo2(() => isBot ? parseMarkdown(msg.text) : msg.text, [isBot, msg.text]);
713
739
  if (isBot) {
714
740
  return /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "10px" }, children: [
715
741
  /* @__PURE__ */ jsx("div", { style: {
@@ -744,7 +770,7 @@ function LightMessageBubble({ msg, accent, agentName }) {
744
770
  wordBreak: "break-word",
745
771
  fontFamily: "inherit"
746
772
  }, children: msg.text }) });
747
- }
773
+ });
748
774
  function LightTypingIndicator({ accent }) {
749
775
  const dot = { width: 6, height: 6, borderRadius: "50%", background: "#c4c4be", display: "inline-block" };
750
776
  return /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "10px" }, children: [
@@ -772,9 +798,9 @@ function LightTypingIndicator({ accent }) {
772
798
  ] })
773
799
  ] });
774
800
  }
775
- function DarkMessageBubble({ msg, accent }) {
801
+ var DarkMessageBubble = React.memo(function DarkMessageBubble2({ msg, accent }) {
776
802
  const isBot = msg.role === "bot";
777
- const renderedHtml = isBot ? parseMarkdown(msg.text) : msg.text;
803
+ const renderedHtml = useMemo2(() => isBot ? parseMarkdown(msg.text) : msg.text, [isBot, msg.text]);
778
804
  return /* @__PURE__ */ jsxs("div", { style: {
779
805
  maxWidth: "85%",
780
806
  padding: "12px 14px",
@@ -792,7 +818,7 @@ function DarkMessageBubble({ msg, accent }) {
792
818
  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" }),
793
819
  isBot ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderedHtml } }) : /* @__PURE__ */ jsx("div", { style: { whiteSpace: "pre-wrap" }, children: msg.text })
794
820
  ] });
795
- }
821
+ });
796
822
  function DarkTypingIndicator() {
797
823
  const dot = { width: 6, height: 6, borderRadius: "50%", background: "rgba(255,255,255,0.4)", display: "inline-block" };
798
824
  return /* @__PURE__ */ jsxs("div", { style: {
@@ -811,7 +837,7 @@ function DarkTypingIndicator() {
811
837
  /* @__PURE__ */ jsx("span", { style: dot, className: "sd-r-dot-3" })
812
838
  ] });
813
839
  }
814
- function renderClassic(p) {
840
+ function ClassicTheme(p) {
815
841
  const {
816
842
  agent,
817
843
  state,
@@ -1000,7 +1026,7 @@ function renderClassic(p) {
1000
1026
  ] })
1001
1027
  ] });
1002
1028
  }
1003
- function renderLight(p) {
1029
+ function LightTheme(p) {
1004
1030
  const {
1005
1031
  agent,
1006
1032
  state,
@@ -1188,7 +1214,7 @@ function renderLight(p) {
1188
1214
  ] })
1189
1215
  ] });
1190
1216
  }
1191
- function renderDark(p) {
1217
+ function DarkTheme(p) {
1192
1218
  const {
1193
1219
  agent,
1194
1220
  state,
@@ -1425,7 +1451,10 @@ function SageDeskWidget({ mode, indexUrl, endpoint, agent, search: search2 }) {
1425
1451
  '[sagedesk] Required prop "endpoint" is missing for llm mode. Provide your backend route, e.g. endpoint="/api/sagedesk".'
1426
1452
  );
1427
1453
  }
1428
- const config = { mode: resolvedMode, indexUrl, endpoint, agent, search: search2 };
1454
+ const config = useMemo2(
1455
+ () => ({ mode: resolvedMode, indexUrl, endpoint, agent, search: search2 }),
1456
+ [resolvedMode, indexUrl, endpoint, agent, search2]
1457
+ );
1429
1458
  const { state, chips, open, close, submit } = useSageDesk(config);
1430
1459
  const theme = agent.theme ?? "classic";
1431
1460
  const accent = agent.accentColor ?? "#534AB7";
@@ -1504,7 +1533,7 @@ function SageDeskWidget({ mode, indexUrl, endpoint, agent, search: search2 }) {
1504
1533
  open,
1505
1534
  submit
1506
1535
  };
1507
- const content = theme === "dark" ? renderDark(props) : theme === "light" ? renderLight(props) : renderClassic(props);
1536
+ const content = theme === "dark" ? /* @__PURE__ */ jsx(DarkTheme, { ...props }) : theme === "light" ? /* @__PURE__ */ jsx(LightTheme, { ...props }) : /* @__PURE__ */ jsx(ClassicTheme, { ...props });
1508
1537
  return createPortal(content, document.body);
1509
1538
  }
1510
1539
  export {