@xbrain-swe/nova-chat-widget 0.1.10 → 0.2.4-dev.15

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.
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  // src/provider.tsx
4
- import { useEffect as useEffect5, useMemo as useMemo2 } from "react";
4
+ import { useEffect as useEffect6, useMemo as useMemo2 } from "react";
5
5
 
6
6
  // src/client-actions.tsx
7
- import { useEffect, useRef as useRef2 } from "react";
7
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
8
8
 
9
9
  // src/internal-context.tsx
10
- import { createContext, useContext, useMemo, useRef } from "react";
10
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef } from "react";
11
11
  import { jsx } from "react/jsx-runtime";
12
12
  var DEFAULT_THEME = {
13
13
  accentFrom: "#4f46e5",
@@ -25,6 +25,7 @@ function useXbNovaInternal() {
25
25
  return ctx;
26
26
  }
27
27
  var VISITOR_STORAGE_KEY = "xbnova_visitor_id";
28
+ var WIDGET_VIEW_WAIT_MS = 1e4;
28
29
  function resolveVisitorId() {
29
30
  if (typeof window === "undefined") return "";
30
31
  try {
@@ -40,6 +41,17 @@ function resolveVisitorId() {
40
41
  function resolveFactValue(value) {
41
42
  return typeof value === "function" ? value() : value;
42
43
  }
44
+ function collectContextEntries(registry) {
45
+ const out = [];
46
+ for (const source of registry.values()) {
47
+ const entries = typeof source === "function" ? source() : source;
48
+ if (!Array.isArray(entries)) continue;
49
+ for (const e of entries) {
50
+ if (e && typeof e.title === "string" && typeof e.description === "string") out.push(e);
51
+ }
52
+ }
53
+ return out;
54
+ }
43
55
  function InternalProvider({
44
56
  auth,
45
57
  clientActionHandlers,
@@ -48,21 +60,54 @@ function InternalProvider({
48
60
  }) {
49
61
  const trackRegistry = useRef(/* @__PURE__ */ new Map()).current;
50
62
  const factsRegistry = useRef(/* @__PURE__ */ new Map()).current;
63
+ const contextRegistry = useRef(/* @__PURE__ */ new Map()).current;
51
64
  const emptyHandlers = useRef(/* @__PURE__ */ new Map()).current;
52
65
  const visitorIdRef = useRef(null);
53
66
  if (visitorIdRef.current === null) {
54
67
  visitorIdRef.current = resolveVisitorId();
55
68
  }
69
+ const visitorId = auth.userKey || visitorIdRef.current;
70
+ const sessionId = auth.sessionKey || null;
71
+ const latestRef = useRef({ auth, visitorId, sessionId });
72
+ latestRef.current = { auth, visitorId, sessionId };
73
+ const sendEvent = useCallback((kind, fields = {}) => {
74
+ const { auth: a, visitorId: v, sessionId: sid } = latestRef.current;
75
+ fetch(`${a.apiBaseUrl}/v1/nudge-events`, {
76
+ method: "POST",
77
+ headers: { "Content-Type": "application/json" },
78
+ body: JSON.stringify({ app_id: a.appId, kind, visitor_id: v || null, session_id: sid, ...fields })
79
+ }).catch(() => {
80
+ });
81
+ }, []);
82
+ const widgetViewSentRef = useRef(false);
83
+ useEffect(() => {
84
+ if (widgetViewSentRef.current) return;
85
+ const send = () => {
86
+ if (widgetViewSentRef.current) return;
87
+ widgetViewSentRef.current = true;
88
+ const screen = latestRef.current.auth.screen;
89
+ sendEvent("widget_view", screen ? { payload: { screen } } : {});
90
+ };
91
+ if (sessionId) {
92
+ send();
93
+ return;
94
+ }
95
+ const timer = setTimeout(send, WIDGET_VIEW_WAIT_MS);
96
+ return () => clearTimeout(timer);
97
+ }, [sessionId, sendEvent]);
56
98
  const value = useMemo(
57
99
  () => ({
58
100
  auth,
59
101
  trackRegistry,
60
102
  factsRegistry,
103
+ contextRegistry,
61
104
  clientActionHandlers: clientActionHandlers ?? emptyHandlers,
62
- visitorId: visitorIdRef.current,
105
+ visitorId,
106
+ sessionId,
107
+ sendEvent,
63
108
  theme
64
109
  }),
65
- [auth, trackRegistry, factsRegistry, clientActionHandlers, emptyHandlers, theme]
110
+ [auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, visitorId, sessionId, sendEvent, theme]
66
111
  );
67
112
  return /* @__PURE__ */ jsx(InternalContext.Provider, { value, children });
68
113
  }
@@ -83,7 +128,7 @@ function XbNovaClientActionSync({ actions }) {
83
128
  const { auth } = useXbNovaInternal();
84
129
  const { apiKey, apiBaseUrl, appId } = auth;
85
130
  const lastSyncedRef = useRef2(null);
86
- useEffect(() => {
131
+ useEffect2(() => {
87
132
  if (!actions || Object.keys(actions).length === 0) return;
88
133
  const serialized = JSON.stringify(toSyncPayload(actions));
89
134
  if (serialized === lastSyncedRef.current) return;
@@ -137,7 +182,7 @@ function peekVisitCount(screen) {
137
182
  }
138
183
 
139
184
  // src/nudge-context.tsx
140
- import { createContext as createContext2, useCallback, useContext as useContext2, useEffect as useEffect2, useRef as useRef3, useState } from "react";
185
+ import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useEffect as useEffect3, useRef as useRef3, useState } from "react";
141
186
  import { jsx as jsx2 } from "react/jsx-runtime";
142
187
  var NudgeContext = createContext2(null);
143
188
  var HINTS_DISABLED_KEY = "xbnova_hints_disabled";
@@ -181,43 +226,43 @@ function NudgeStateProvider({ children }) {
181
226
  const [hintsEnabled, setHintsEnabled] = useState(() => !readFlag(HINTS_DISABLED_KEY));
182
227
  const [barHidden, setBarHidden] = useState(() => readFlag(BAR_HIDDEN_KEY));
183
228
  const streakRef = useRef3(readStreak());
184
- const disableHints = useCallback(() => {
229
+ const disableHints = useCallback2(() => {
185
230
  setHintsEnabled(false);
186
231
  writeFlag(HINTS_DISABLED_KEY, true);
187
232
  }, []);
188
- const enableHints = useCallback(() => {
233
+ const enableHints = useCallback2(() => {
189
234
  setHintsEnabled(true);
190
235
  writeFlag(HINTS_DISABLED_KEY, false);
191
236
  streakRef.current = 0;
192
237
  writeStreak(0);
193
238
  }, []);
194
- const hideBar = useCallback(() => {
239
+ const hideBar = useCallback2(() => {
195
240
  setBarHidden(true);
196
241
  writeFlag(BAR_HIDDEN_KEY, true);
197
242
  }, []);
198
- const showBar = useCallback(() => {
243
+ const showBar = useCallback2(() => {
199
244
  setBarHidden(false);
200
245
  writeFlag(BAR_HIDDEN_KEY, false);
201
246
  }, []);
202
- const offerHint = useCallback((next) => {
247
+ const offerHint = useCallback2((next) => {
203
248
  setHint((current) => current ? current : next);
204
249
  }, []);
205
- useEffect2(() => {
250
+ useEffect3(() => {
206
251
  if (hint) setLastHint(hint);
207
252
  }, [hint]);
208
- const dismissHint = useCallback(() => {
253
+ const dismissHint = useCallback2(() => {
209
254
  const limit = hint?.autoDisableAfterDismissals ?? DEFAULT_DISMISS_STREAK_LIMIT;
210
255
  setHint(null);
211
256
  streakRef.current += 1;
212
257
  writeStreak(streakRef.current);
213
258
  if (streakRef.current >= limit) disableHints();
214
259
  }, [hint, disableHints]);
215
- const openHint = useCallback(() => {
260
+ const openHint = useCallback2(() => {
216
261
  setHint(null);
217
262
  streakRef.current = 0;
218
263
  writeStreak(0);
219
264
  }, []);
220
- const autoHideHint = useCallback(() => {
265
+ const autoHideHint = useCallback2(() => {
221
266
  setHint(null);
222
267
  }, []);
223
268
  return /* @__PURE__ */ jsx2(
@@ -254,10 +299,10 @@ function useXbNovaSettings() {
254
299
  }
255
300
 
256
301
  // src/nudge-loop.tsx
257
- import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef5 } from "react";
302
+ import { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef5 } from "react";
258
303
 
259
304
  // src/track.tsx
260
- import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef4 } from "react";
305
+ import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef4 } from "react";
261
306
  import { jsx as jsx3 } from "react/jsx-runtime";
262
307
  var VISIBILITY_THRESHOLD = 0;
263
308
  function finalizeIfVisible(registry, region) {
@@ -274,7 +319,7 @@ function useXbNovaTrack(region, data, label) {
274
319
  dataRef.current = data;
275
320
  const labelRef = useRef4(label);
276
321
  labelRef.current = label;
277
- useEffect3(() => {
322
+ useEffect4(() => {
278
323
  const entry = trackRegistry.get(region);
279
324
  if (entry) {
280
325
  entry.data = data;
@@ -283,7 +328,7 @@ function useXbNovaTrack(region, data, label) {
283
328
  trackRegistry.set(region, { region, data, label, enteredAt: null, totalMs: 0, node: null });
284
329
  }
285
330
  }, [region, data, label, trackRegistry]);
286
- useEffect3(() => {
331
+ useEffect4(() => {
287
332
  return () => {
288
333
  observerRef.current?.disconnect();
289
334
  finalizeIfVisible(trackRegistry, region);
@@ -291,7 +336,7 @@ function useXbNovaTrack(region, data, label) {
291
336
  if (entry) entry.node = null;
292
337
  };
293
338
  }, [region, trackRegistry]);
294
- return useCallback2(
339
+ return useCallback3(
295
340
  (node) => {
296
341
  observerRef.current?.disconnect();
297
342
  observerRef.current = null;
@@ -365,7 +410,7 @@ function getFocusedTrackEntry(trackRegistry) {
365
410
  }
366
411
 
367
412
  // src/nudge-loop.tsx
368
- var EVAL_INTERVAL_MS = 1e3;
413
+ var EVAL_INTERVAL_MS = 250;
369
414
  var XBNOVA_EVENT_NAME = "xbnova:event";
370
415
  var SESSION_COUNT_KEY_PREFIX = "xbnova_hint_session_count:";
371
416
  var SCROLL_SETTLE_MS = 500;
@@ -558,7 +603,7 @@ function clearDebugOverlay(refs) {
558
603
  refs.bandLinesRef.current.clear();
559
604
  }
560
605
  function XbNovaNudgeLoop({ appId }) {
561
- const { auth, trackRegistry, factsRegistry, visitorId } = useXbNovaInternal();
606
+ const { auth, trackRegistry, factsRegistry, sendEvent } = useXbNovaInternal();
562
607
  const { apiKey, apiBaseUrl, screen } = auth;
563
608
  const { hint, offerHint, autoHideHint, hintsEnabled } = useNudgeState();
564
609
  const graphRef = useRef5(null);
@@ -573,7 +618,7 @@ function XbNovaNudgeLoop({ appId }) {
573
618
  hintRef.current = hint;
574
619
  const hintsEnabledRef = useRef5(hintsEnabled);
575
620
  hintsEnabledRef.current = hintsEnabled;
576
- useEffect4(() => {
621
+ useEffect5(() => {
577
622
  let cancelled = false;
578
623
  fetch(`${apiBaseUrl}/v1/apps/${appId}/nudge-graph`, {
579
624
  headers: { Authorization: `Bearer ${apiKey}` }
@@ -585,14 +630,16 @@ function XbNovaNudgeLoop({ appId }) {
585
630
  cancelled = true;
586
631
  };
587
632
  }, [apiBaseUrl, apiKey, appId]);
588
- const evaluate = useCallback3(
633
+ const evaluate = useCallback4(
589
634
  (firedEventName) => {
590
635
  const graph = graphRef.current;
591
636
  const debugEnabled = isDebugEnabled();
592
637
  const debugRefs = { panelRef: debugPanelRef, boxesRef: debugBoxesRef, bandLinesRef: debugBandLinesRef };
593
638
  if (!debugEnabled && debugPanelRef.current) clearDebugOverlay(debugRefs);
639
+ const tickDebug = debugEnabled ? (msg) => console.debug(msg) : () => {
640
+ };
594
641
  if (!graph) {
595
- console.debug("[XbNova] nudge \uD3C9\uAC00 \uAC74\uB108\uB700: \uADF8\uB798\uD504\uB97C \uC544\uC9C1 \uBABB \uBD88\uB7EC\uC634");
642
+ tickDebug("[XbNova] nudge \uD3C9\uAC00 \uAC74\uB108\uB700: \uADF8\uB798\uD504\uB97C \uC544\uC9C1 \uBABB \uBD88\uB7EC\uC634");
596
643
  return;
597
644
  }
598
645
  const canOffer = !hintRef.current && hintsEnabledRef.current;
@@ -643,18 +690,7 @@ function XbNovaNudgeLoop({ appId }) {
643
690
  autoHideMs: messageNode.auto_hide_ms ?? 2e4,
644
691
  quickReplies: messageNode.quick_replies ?? []
645
692
  });
646
- fetch(`${apiBaseUrl}/v1/nudge-events`, {
647
- method: "POST",
648
- headers: { "Content-Type": "application/json" },
649
- body: JSON.stringify({
650
- app_id: appId,
651
- nudge_node_id: messageNode.id,
652
- kind: "nudge_impression",
653
- bot_id: botId,
654
- visitor_id: visitorId
655
- })
656
- }).catch(() => {
657
- });
693
+ sendEvent("nudge_impression", { nudge_node_id: messageNode.id, bot_id: botId });
658
694
  };
659
695
  const tryFireFrom = (node) => {
660
696
  const nextEdge = (edgesFrom.get(node.id) ?? [])[0];
@@ -682,7 +718,7 @@ function XbNovaNudgeLoop({ appId }) {
682
718
  const maxPerSession = node.max_per_session === 0 ? Infinity : node.max_per_session ?? 3;
683
719
  const sessionCount = peekSessionCount(node.id);
684
720
  if (sessionCount >= maxPerSession) {
685
- console.debug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id}(${node.trigger_type}) \uAC74\uB108\uB700: \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC`);
721
+ tickDebug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id}(${node.trigger_type}) \uAC74\uB108\uB700: \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC`);
686
722
  if (debugEnabled && node.trigger_type === "dwell") {
687
723
  debugLines.push(`- [${node.id}] \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC \u2014 \uB354 \uC548 \uC7BC`);
688
724
  }
@@ -717,7 +753,7 @@ function XbNovaNudgeLoop({ appId }) {
717
753
  overlapPx = status.overlapPx;
718
754
  statusDetail = `rect=[top=${status.rectTop.toFixed(0)}, bottom=${status.rectBottom.toFixed(0)}] band=[top=${status.bandTop.toFixed(0)}, bottom=${status.bandBottom.toFixed(0)}] overlapPx=${overlapPx.toFixed(0)} inBand=${inBand}`;
719
755
  }
720
- console.debug(
756
+ tickDebug(
721
757
  `[XbNova] band dwell \uD2B8\uB9AC\uAC70=${node.id} region="${region}" band%=${fromPercent}~${toPercent} | ${statusDetail}`
722
758
  );
723
759
  dwellRecords.push({ node, state, requiredMs, region, found, screenMatches, inBand, overlapPx });
@@ -726,7 +762,7 @@ function XbNovaNudgeLoop({ appId }) {
726
762
  const fires = triggerFires(node, { screen, firedEventName });
727
763
  if (!fires) continue;
728
764
  if (!canOffer) {
729
- console.debug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id} \uC870\uAC74\uC740 \uCDA9\uC871\uD588\uC9C0\uB9CC \uC0C8 \uD78C\uD2B8 \uD45C\uC2DC\uB294 \uC7A0\uAE50 \uB9C9\uD798`);
765
+ tickDebug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id} \uC870\uAC74\uC740 \uCDA9\uC871\uD588\uC9C0\uB9CC \uC0C8 \uD78C\uD2B8 \uD45C\uC2DC\uB294 \uC7A0\uAE50 \uB9C9\uD798`);
730
766
  continue;
731
767
  }
732
768
  if (tryFireFrom(node)) {
@@ -826,9 +862,9 @@ function XbNovaNudgeLoop({ appId }) {
826
862
  ensureDebugPanel(debugPanelRef, debugCollapsedRef).body.textContent = debugLines.join("\n");
827
863
  }
828
864
  },
829
- [apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, visitorId]
865
+ [apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, sendEvent]
830
866
  );
831
- useEffect4(() => {
867
+ useEffect5(() => {
832
868
  const interval = window.setInterval(() => evaluate(null), EVAL_INTERVAL_MS);
833
869
  const onEvent = (e) => evaluate(e.detail.eventName);
834
870
  window.addEventListener(XBNOVA_EVENT_NAME, onEvent);
@@ -857,24 +893,36 @@ function XbNovaProvider({
857
893
  apiBaseUrl = DEFAULT_API_BASE_URL,
858
894
  clientActions,
859
895
  theme,
896
+ userKey,
897
+ threadKey,
898
+ sessionKey,
860
899
  children
861
900
  }) {
862
- useEffect5(() => {
901
+ useEffect6(() => {
863
902
  if (apiKey.startsWith("sk_")) {
864
903
  console.error(
865
904
  "[XbNova] sk_ \uD0A4\uB97C \uBE0C\uB77C\uC6B0\uC800\uC5D0 \uB178\uCD9C\uD588\uC2B5\uB2C8\uB2E4 \u2014 pk_(publishable) \uD0A4\uB97C \uC4F0\uC138\uC694. sk_\uB294 \uC11C\uBC84 \uC804\uC6A9\uC785\uB2C8\uB2E4."
866
905
  );
867
906
  }
868
907
  }, [apiKey]);
869
- useEffect5(() => {
908
+ useEffect6(() => {
870
909
  markSessionStart();
871
910
  }, []);
872
- useEffect5(() => {
911
+ useEffect6(() => {
873
912
  if (screen) bumpVisitCount(screen);
874
913
  }, [screen]);
875
914
  const auth = useMemo2(
876
- () => ({ apiKey, botId, appId, apiBaseUrl, screen }),
877
- [apiKey, botId, appId, apiBaseUrl, screen]
915
+ () => ({
916
+ apiKey,
917
+ botId,
918
+ appId,
919
+ apiBaseUrl,
920
+ screen,
921
+ userKey: userKey || void 0,
922
+ threadKey: threadKey || void 0,
923
+ sessionKey: sessionKey || void 0
924
+ }),
925
+ [apiKey, botId, appId, apiBaseUrl, screen, userKey, threadKey, sessionKey]
878
926
  );
879
927
  const resolvedTheme = useMemo2(
880
928
  () => ({ ...DEFAULT_THEME, ...theme }),
@@ -903,13 +951,13 @@ function XbNovaProvider({
903
951
  }
904
952
 
905
953
  // src/chat.tsx
906
- import { useEffect as useEffect8, useRef as useRef8, useState as useState4 } from "react";
954
+ import { useEffect as useEffect9, useRef as useRef8, useState as useState4 } from "react";
907
955
  import ReactMarkdown from "react-markdown";
908
956
  import remarkBreaks from "remark-breaks";
909
957
  import remarkGfm from "remark-gfm";
910
958
 
911
959
  // src/shadow-root.tsx
912
- import { useEffect as useEffect6, useRef as useRef6, useState as useState2 } from "react";
960
+ import { useEffect as useEffect7, useRef as useRef6, useState as useState2 } from "react";
913
961
  import { createPortal } from "react-dom";
914
962
  import { jsx as jsx5 } from "react/jsx-runtime";
915
963
  var cssPromise = null;
@@ -925,7 +973,7 @@ function loadCss() {
925
973
  }
926
974
  function useResolvedScheme(scheme) {
927
975
  const [systemDark, setSystemDark] = useState2(false);
928
- useEffect6(() => {
976
+ useEffect7(() => {
929
977
  if (scheme !== "system") return;
930
978
  const mq = window.matchMedia("(prefers-color-scheme: dark)");
931
979
  setSystemDark(mq.matches);
@@ -945,10 +993,10 @@ function XbNovaShadowRoot({
945
993
  const hostRef = useRef6(null);
946
994
  const [mountNode, setMountNode] = useState2(null);
947
995
  const [portalTarget, setPortalTarget] = useState2(null);
948
- useEffect6(() => {
996
+ useEffect7(() => {
949
997
  setPortalTarget(document.body);
950
998
  }, []);
951
- useEffect6(() => {
999
+ useEffect7(() => {
952
1000
  const host = hostRef.current;
953
1001
  if (!host || host.shadowRoot) return;
954
1002
  const shadow = host.attachShadow({ mode: "open" });
@@ -963,7 +1011,7 @@ function XbNovaShadowRoot({
963
1011
  shadow.appendChild(mount);
964
1012
  setMountNode(mount);
965
1013
  }, [portalTarget]);
966
- useEffect6(() => {
1014
+ useEffect7(() => {
967
1015
  if (!mountNode) return;
968
1016
  mountNode.dataset.xbnovaScheme = scheme;
969
1017
  mountNode.style.colorScheme = scheme;
@@ -976,15 +1024,44 @@ function XbNovaShadowRoot({
976
1024
  }
977
1025
 
978
1026
  // src/use-chat.ts
979
- import { useEffect as useEffect7, useRef as useRef7, useState as useState3 } from "react";
1027
+ import { useEffect as useEffect8, useRef as useRef7, useState as useState3 } from "react";
980
1028
 
981
1029
  // src/stream.ts
1030
+ function fromStoredMessages(stored) {
1031
+ return stored.map((m) => ({
1032
+ id: m.role === "assistant" ? m.id : void 0,
1033
+ role: m.role === "assistant" ? "assistant" : "user",
1034
+ parts: m.parts.flatMap((p, i) => {
1035
+ const type = String(p.type ?? "");
1036
+ if (type === "text") {
1037
+ const text = typeof p.text === "string" ? p.text : "";
1038
+ return text ? [{ kind: "text", id: `${m.id}:${i}`, text }] : [];
1039
+ }
1040
+ if (type.startsWith("tool-")) {
1041
+ const meta = p.providerMetadata;
1042
+ return [
1043
+ {
1044
+ kind: "tool",
1045
+ toolCallId: String(p.toolCallId ?? `${m.id}:${i}`),
1046
+ toolName: type.slice("tool-".length),
1047
+ state: p.state === "output-error" ? "error" : "output",
1048
+ input: p.input,
1049
+ output: p.output,
1050
+ view: meta?.["kr.dsauto.chat"]?.view ?? null
1051
+ }
1052
+ ];
1053
+ }
1054
+ return [];
1055
+ })
1056
+ }));
1057
+ }
982
1058
  async function consumeSse(response, handlers) {
983
1059
  if (!response.ok || !response.body) {
984
1060
  handlers.onError?.(`\uC694\uCCAD \uC2E4\uD328 (${response.status})`);
985
1061
  return null;
986
1062
  }
987
1063
  const conversationId = response.headers.get("x-conversation-id");
1064
+ if (conversationId) handlers.onConversationId?.(conversationId);
988
1065
  const reader = response.body.getReader();
989
1066
  const decoder = new TextDecoder();
990
1067
  let buffer = "";
@@ -1089,12 +1166,19 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
1089
1166
  // src/use-chat.ts
1090
1167
  var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
1091
1168
  function useXbNovaChat(options) {
1092
- const { auth, trackRegistry, clientActionHandlers, visitorId } = useXbNovaInternal();
1169
+ const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId, sessionId, sendEvent } = useXbNovaInternal();
1093
1170
  const botId = options?.botId ?? auth.botId;
1094
1171
  const nudgeNodeId = options?.nudgeNodeId;
1095
- const { apiKey, apiBaseUrl } = auth;
1172
+ const { apiKey, apiBaseUrl, threadKey } = auth;
1173
+ const userKey = visitorId;
1096
1174
  const [onboarding, setOnboarding] = useState3(null);
1097
1175
  const [messages, setMessages] = useState3([]);
1176
+ const [resumed, setResumed] = useState3(false);
1177
+ const [sentOpening, setSentOpening] = useState3(null);
1178
+ const conversationIdRef = useRef7(null);
1179
+ const pendingOpeningRef = useRef7(null);
1180
+ const messagesRef = useRef7(messages);
1181
+ messagesRef.current = messages;
1098
1182
  const [busy, setBusy] = useState3(false);
1099
1183
  const [error, setError] = useState3(null);
1100
1184
  const [clientResults, setClientResults] = useState3({});
@@ -1104,19 +1188,60 @@ function useXbNovaChat(options) {
1104
1188
  const dispatchedClientActionsRef = useRef7(/* @__PURE__ */ new Set());
1105
1189
  const nudgeNodeIdRef = useRef7(nudgeNodeId);
1106
1190
  nudgeNodeIdRef.current = nudgeNodeId;
1107
- useEffect7(() => {
1191
+ useEffect8(() => {
1192
+ let cancelled = false;
1108
1193
  setOnboarding(null);
1109
1194
  setMessages([]);
1195
+ setResumed(false);
1196
+ setSentOpening(null);
1110
1197
  setError(null);
1111
1198
  setClientResults({});
1112
1199
  setFeedbackState({});
1113
1200
  dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
1201
+ conversationIdRef.current = null;
1202
+ pendingOpeningRef.current = null;
1114
1203
  fetch(`${apiBaseUrl}/v1/conversations`, {
1115
1204
  method: "POST",
1116
1205
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
1117
- body: JSON.stringify({ bot_id: botId, nudge_node_id: nudgeNodeIdRef.current ?? null })
1118
- }).then((r) => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))).then(setOnboarding).catch((e) => setError(`\uBD07\uC744 \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`));
1119
- }, [apiBaseUrl, apiKey, botId]);
1206
+ body: JSON.stringify({
1207
+ bot_id: botId,
1208
+ nudge_node_id: nudgeNodeIdRef.current ?? null,
1209
+ visitor_id: userKey || null,
1210
+ thread_key: threadKey ?? null,
1211
+ session_id: sessionId,
1212
+ defer: true
1213
+ })
1214
+ }).then((r) => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))).then((data) => {
1215
+ if (cancelled) return;
1216
+ if (conversationIdRef.current || messagesRef.current.length > 0) {
1217
+ setOnboarding(data);
1218
+ return;
1219
+ }
1220
+ conversationIdRef.current = data.conversation_id;
1221
+ const restored = fromStoredMessages(data.messages ?? []);
1222
+ for (const m of restored) {
1223
+ for (const p of m.parts) if (p.kind === "tool") dispatchedClientActionsRef.current.add(p.toolCallId);
1224
+ }
1225
+ setMessages(restored);
1226
+ setResumed(restored.length > 0);
1227
+ setOnboarding(data);
1228
+ }).catch((e) => {
1229
+ if (!cancelled) setError(`\uBD07\uC744 \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`);
1230
+ });
1231
+ return () => {
1232
+ cancelled = true;
1233
+ };
1234
+ }, [apiBaseUrl, apiKey, botId, userKey, threadKey]);
1235
+ const openingMessage = options?.openingMessage;
1236
+ useEffect8(() => {
1237
+ if (!onboarding || !nudgeNodeId || !openingMessage) return;
1238
+ if (messagesRef.current.length === 0) return;
1239
+ setMessages((prev) => [
1240
+ ...prev,
1241
+ { role: "assistant", parts: [{ kind: "text", id: `nudge:${nudgeNodeId}:${prev.length}`, text: openingMessage }] }
1242
+ ]);
1243
+ pendingOpeningRef.current = openingMessage;
1244
+ }, [onboarding, nudgeNodeId, openingMessage]);
1120
1245
  const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
1121
1246
  const next = [...prev];
1122
1247
  next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
@@ -1129,7 +1254,7 @@ function useXbNovaChat(options) {
1129
1254
  return next;
1130
1255
  });
1131
1256
  async function setFeedback(messageId, rating, reason) {
1132
- const conversationId = onboarding?.conversation_id;
1257
+ const conversationId = conversationIdRef.current;
1133
1258
  if (!conversationId || !messageId) return;
1134
1259
  const prevRating = feedbackRef.current[messageId] ?? null;
1135
1260
  const apply = (r) => setFeedbackState((prev) => {
@@ -1155,6 +1280,14 @@ function useXbNovaChat(options) {
1155
1280
  apply(prevRating);
1156
1281
  }
1157
1282
  }
1283
+ function postClientActionEvent(kind, action, label) {
1284
+ const conversationId = conversationIdRef.current;
1285
+ if (!conversationId) return;
1286
+ sendEvent(kind, { conversation_id: conversationId, payload: label ? { action, label } : { action } });
1287
+ }
1288
+ function trackClientActionClick(action, label) {
1289
+ postClientActionEvent("cta_click", action, label);
1290
+ }
1158
1291
  async function runClientAction(spec, part, existingParts) {
1159
1292
  if (spec.dismissSheet) options?.onDismissSheet?.();
1160
1293
  let result;
@@ -1178,7 +1311,7 @@ function useXbNovaChat(options) {
1178
1311
  result = e instanceof Error ? `\uC624\uB958: ${e.message}` : "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
1179
1312
  }
1180
1313
  if (!spec.awaitResult) return;
1181
- const conversationId = onboarding?.conversation_id;
1314
+ const conversationId = conversationIdRef.current;
1182
1315
  if (!conversationId) return;
1183
1316
  const structured = result && typeof result === "object" ? result : null;
1184
1317
  if (structured?.view) {
@@ -1212,7 +1345,7 @@ function useXbNovaChat(options) {
1212
1345
  setBusy(false);
1213
1346
  }
1214
1347
  }
1215
- useEffect7(() => {
1348
+ useEffect8(() => {
1216
1349
  if (clientActionHandlers.size === 0) return;
1217
1350
  const last = messages[messages.length - 1];
1218
1351
  if (!last || last.role !== "assistant") return;
@@ -1222,6 +1355,7 @@ function useXbNovaChat(options) {
1222
1355
  if (!spec) continue;
1223
1356
  if (dispatchedClientActionsRef.current.has(part.toolCallId)) continue;
1224
1357
  dispatchedClientActionsRef.current.add(part.toolCallId);
1358
+ if (spec.render) postClientActionEvent("cta_impression", part.toolName, null);
1225
1359
  void runClientAction(spec, part, last.parts);
1226
1360
  }
1227
1361
  }, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
@@ -1241,7 +1375,17 @@ function useXbNovaChat(options) {
1241
1375
  return next;
1242
1376
  });
1243
1377
  const focused = getFocusedTrackEntry(trackRegistry);
1244
- const context = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : void 0;
1378
+ const notes = collectContextEntries(contextRegistry);
1379
+ const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
1380
+ const context = focused || notes.length ? { ...screen, ...notes.length ? { notes } : {} } : void 0;
1381
+ let openingMessage2;
1382
+ if (messages.length === 0) {
1383
+ openingMessage2 = (options?.openingMessage ?? onboarding?.greeting) || null;
1384
+ setSentOpening(openingMessage2);
1385
+ } else {
1386
+ openingMessage2 = pendingOpeningRef.current;
1387
+ }
1388
+ pendingOpeningRef.current = null;
1245
1389
  try {
1246
1390
  await streamChat(
1247
1391
  apiBaseUrl,
@@ -1249,11 +1393,22 @@ function useXbNovaChat(options) {
1249
1393
  {
1250
1394
  bot_id: botId,
1251
1395
  message: trimmed,
1252
- conversation_id: onboarding?.conversation_id ?? null,
1396
+ conversation_id: conversationIdRef.current,
1253
1397
  context,
1254
- nudge_node_id: nudgeNodeId ?? null
1398
+ nudge_node_id: nudgeNodeId ?? null,
1399
+ opening_message: openingMessage2,
1400
+ visitor_id: userKey || null,
1401
+ thread_key: threadKey ?? null,
1402
+ session_id: sessionId
1255
1403
  },
1256
- { onPart: replaceLast, onMessageId: setLastAssistantId, onError: setError }
1404
+ {
1405
+ onPart: replaceLast,
1406
+ onMessageId: setLastAssistantId,
1407
+ onConversationId: (id) => {
1408
+ conversationIdRef.current = id;
1409
+ },
1410
+ onError: setError
1411
+ }
1257
1412
  );
1258
1413
  } catch (e) {
1259
1414
  setError(e instanceof Error ? e.message : String(e));
@@ -1261,7 +1416,19 @@ function useXbNovaChat(options) {
1261
1416
  setBusy(false);
1262
1417
  }
1263
1418
  }
1264
- return { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback };
1419
+ const opening = resumed ? null : sentOpening ?? options?.openingMessage ?? onboarding?.greeting ?? null;
1420
+ return {
1421
+ onboarding,
1422
+ opening,
1423
+ messages,
1424
+ busy,
1425
+ error,
1426
+ send,
1427
+ clientResults,
1428
+ feedback,
1429
+ setFeedback,
1430
+ trackClientActionClick
1431
+ };
1265
1432
  }
1266
1433
 
1267
1434
  // src/views.tsx
@@ -1394,6 +1561,13 @@ function hasVisiblePart(parts, clientActionHandlers, clientResults) {
1394
1561
  }
1395
1562
  var DISCLOSURE_SEEN_KEY = "xbnova_disclosure_seen";
1396
1563
  var SUMMARY_FACT_KEY = "xbnova_summary";
1564
+ function clickedControl(e) {
1565
+ const target = e.target instanceof Element ? e.target : null;
1566
+ const control = target?.closest("button, a, [role='button']");
1567
+ if (!control || !e.currentTarget.contains(control)) return null;
1568
+ const text = (control.textContent ?? "").replace(/\s+/g, " ").trim();
1569
+ return { label: text || control.getAttribute("aria-label") || null };
1570
+ }
1397
1571
  function readDisclosureSeen() {
1398
1572
  if (typeof window === "undefined") return true;
1399
1573
  try {
@@ -1412,7 +1586,7 @@ function markDisclosureSeen() {
1412
1586
  var DESKTOP_MEDIA_QUERY = "(min-width: 640px)";
1413
1587
  function useIsDesktopViewport() {
1414
1588
  const [isDesktop, setIsDesktop] = useState4(false);
1415
- useEffect8(() => {
1589
+ useEffect9(() => {
1416
1590
  if (typeof window === "undefined") return;
1417
1591
  const mql = window.matchMedia(DESKTOP_MEDIA_QUERY);
1418
1592
  setIsDesktop(mql.matches);
@@ -1436,7 +1610,7 @@ function backdropBaseStyle(isDesktop) {
1436
1610
  }
1437
1611
  function useTypewriter(text, key, speedMs = 28) {
1438
1612
  const [shown, setShown] = useState4("");
1439
- useEffect8(() => {
1613
+ useEffect9(() => {
1440
1614
  setShown("");
1441
1615
  if (!text) return;
1442
1616
  let i = 0;
@@ -1523,9 +1697,10 @@ function XbNovaChat({
1523
1697
  avatarSize = 28,
1524
1698
  onVerdictAction,
1525
1699
  feedback: feedbackEnabled = true,
1526
- onFeedback
1700
+ onFeedback,
1701
+ onOpen
1527
1702
  }) {
1528
- const { auth, trackRegistry, factsRegistry, visitorId, clientActionHandlers, theme } = useXbNovaInternal();
1703
+ const { auth, trackRegistry, factsRegistry, clientActionHandlers, theme, sendEvent } = useXbNovaInternal();
1529
1704
  const themeVars = {
1530
1705
  "--xbnova-accent-1": theme.accentFrom,
1531
1706
  "--xbnova-accent-2": theme.accentTo
@@ -1546,15 +1721,16 @@ function XbNovaChat({
1546
1721
  const [input, setInput] = useState4("");
1547
1722
  const bottomRef = useRef8(null);
1548
1723
  const typedHintText = useTypewriter(hint?.text ?? "", hint?.triggerNodeId ?? "");
1549
- const { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback } = useXbNovaChat({
1724
+ const { onboarding, opening, messages, busy, error, send, clientResults, feedback, setFeedback, trackClientActionClick } = useXbNovaChat({
1550
1725
  onFeedback,
1551
1726
  botId: activeBotId ?? botId,
1552
1727
  nudgeNodeId: activeNudgeNodeId,
1728
+ openingMessage: openingLine ?? void 0,
1553
1729
  onDismissSheet: () => setOpen(false)
1554
1730
  });
1555
1731
  const effectiveBotId = activeBotId ?? botId;
1556
1732
  const [cachedBranding, setCachedBranding] = useState4(null);
1557
- useEffect8(() => {
1733
+ useEffect9(() => {
1558
1734
  try {
1559
1735
  const raw = window.localStorage.getItem(`xbnova_branding:${effectiveBotId}`);
1560
1736
  setCachedBranding(raw ? JSON.parse(raw) : null);
@@ -1562,22 +1738,29 @@ function XbNovaChat({
1562
1738
  setCachedBranding(null);
1563
1739
  }
1564
1740
  }, [effectiveBotId]);
1565
- useEffect8(() => {
1741
+ useEffect9(() => {
1566
1742
  if (!onboarding?.branding) return;
1567
1743
  try {
1568
1744
  window.localStorage.setItem(
1569
1745
  `xbnova_branding:${effectiveBotId}`,
1570
- JSON.stringify({ logo_url: onboarding.branding.logo_url, avatar_text: onboarding.branding.avatar_text })
1746
+ JSON.stringify({
1747
+ logo_url: onboarding.branding.logo_url,
1748
+ avatar_text: onboarding.branding.avatar_text,
1749
+ badge_text: onboarding.branding.badge_text,
1750
+ disclaimer: onboarding.branding.disclaimer
1751
+ })
1571
1752
  );
1572
1753
  } catch {
1573
1754
  }
1574
1755
  }, [onboarding?.branding, effectiveBotId]);
1575
1756
  const branding = onboarding?.branding ?? cachedBranding ?? void 0;
1757
+ const badgeText = branding?.badge_text?.trim() ?? "";
1758
+ const disclaimer = branding?.disclaimer?.trim() ?? "";
1576
1759
  const brandingReady = Boolean(onboarding) || cachedBranding !== null || Boolean(error);
1577
- useEffect8(() => {
1760
+ useEffect9(() => {
1578
1761
  bottomRef.current?.scrollIntoView({ behavior: "smooth" });
1579
1762
  }, [messages]);
1580
- useEffect8(() => {
1763
+ useEffect9(() => {
1581
1764
  if (!open) {
1582
1765
  setEntered(false);
1583
1766
  return;
@@ -1585,7 +1768,7 @@ function XbNovaChat({
1585
1768
  const raf = requestAnimationFrame(() => setEntered(true));
1586
1769
  return () => cancelAnimationFrame(raf);
1587
1770
  }, [open]);
1588
- useEffect8(() => {
1771
+ useEffect9(() => {
1589
1772
  if (!hint || hint.autoHideMs <= 0) return;
1590
1773
  console.debug(`[XbNova] \uD78C\uD2B8 "${hint.text}" \uD45C\uC2DC \u2014 ${hint.autoHideMs}ms \uD6C4 \uC790\uB3D9\uC73C\uB85C \uC0AC\uB77C\uC9D0`);
1591
1774
  const timer = window.setTimeout(() => {
@@ -1602,18 +1785,7 @@ function XbNovaChat({
1602
1785
  setOpeningLine(consumed.text);
1603
1786
  setActiveNudgeNodeId(consumed.messageNodeId);
1604
1787
  setNudgeQuickReplies(consumed.quickReplies);
1605
- fetch(`${auth.apiBaseUrl}/v1/nudge-events`, {
1606
- method: "POST",
1607
- headers: { "Content-Type": "application/json" },
1608
- body: JSON.stringify({
1609
- app_id: auth.appId,
1610
- nudge_node_id: consumed.messageNodeId,
1611
- kind: "nudge_open",
1612
- bot_id: consumed.botId,
1613
- visitor_id: visitorId
1614
- })
1615
- }).catch(() => {
1616
- });
1788
+ sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1617
1789
  } else {
1618
1790
  setNudgeQuickReplies([]);
1619
1791
  setActiveNudgeNodeId(void 0);
@@ -1627,6 +1799,11 @@ function XbNovaChat({
1627
1799
  markDisclosureSeen();
1628
1800
  }
1629
1801
  setOpen(true);
1802
+ try {
1803
+ onOpen?.({ fromNudge: Boolean(consumed), hintText: consumed?.text });
1804
+ } catch (err) {
1805
+ console.warn("[XbNova] onOpen \uCF5C\uBC31 \uC624\uB958", err);
1806
+ }
1630
1807
  }
1631
1808
  if (!open) {
1632
1809
  if (barHidden) return null;
@@ -1722,7 +1899,6 @@ function XbNovaChat({
1722
1899
  const summary = resolveFactValue(factsRegistry.get(SUMMARY_FACT_KEY));
1723
1900
  const focusedLabel = getFocusedTrackEntry(trackRegistry)?.label;
1724
1901
  const contextLine = summary ?? (focusedLabel ? `${focusedLabel} \uBCF4\uB294 \uC911` : void 0);
1725
- const firstBubble = openingLine ?? onboarding?.greeting;
1726
1902
  return /* @__PURE__ */ jsxs3(Fragment, { children: [
1727
1903
  !isDesktop ? /* @__PURE__ */ jsx7(
1728
1904
  XbNovaShadowRoot,
@@ -1759,7 +1935,10 @@ function XbNovaChat({
1759
1935
  }
1760
1936
  ),
1761
1937
  /* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
1762
- /* @__PURE__ */ jsx7("div", { className: "text-sm font-semibold", children: onboarding?.branding?.title ?? "AI \uB3D9\uD589" }),
1938
+ /* @__PURE__ */ jsxs3("div", { className: "flex min-w-0 items-center gap-1.5", children: [
1939
+ /* @__PURE__ */ jsx7("span", { className: "truncate text-sm font-semibold", children: onboarding?.branding?.title ?? "AI \uB3D9\uD589" }),
1940
+ badgeText ? /* @__PURE__ */ jsx7("span", { className: "shrink-0 rounded-full border border-neutral-300 px-1.5 py-px text-[10px] font-semibold leading-4 text-neutral-500 dark:border-neutral-600 dark:text-neutral-400", children: badgeText }) : null
1941
+ ] }),
1763
1942
  contextLine ? /* @__PURE__ */ jsx7("div", { className: "truncate text-[11px] text-neutral-400", children: contextLine }) : null
1764
1943
  ] }),
1765
1944
  /* @__PURE__ */ jsx7(
@@ -1775,7 +1954,7 @@ function XbNovaChat({
1775
1954
  ] }),
1776
1955
  /* @__PURE__ */ jsxs3("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [
1777
1956
  showDisclosure ? /* @__PURE__ */ jsx7("div", { className: "rounded-lg bg-neutral-50 px-3 py-2 text-[11px] text-neutral-400 dark:bg-neutral-800", children: "\uC9C0\uAE08 \uBCF4\uACE0 \uACC4\uC2E0 \uD654\uBA74\uACFC \uB9E4\uBB3C \uC815\uBCF4\uB97C \uAE30\uC900\uC73C\uB85C \uC548\uB0B4\uD574 \uB4DC\uB824\uC694." }) : null,
1778
- firstBubble ? /* @__PURE__ */ jsx7(Bubble, { role: "assistant", children: /* @__PURE__ */ jsx7(AssistantMarkdown, { text: firstBubble }) }) : null,
1957
+ opening ? /* @__PURE__ */ jsx7(Bubble, { role: "assistant", children: /* @__PURE__ */ jsx7(AssistantMarkdown, { text: opening }) }) : null,
1779
1958
  messages.map((message, i) => /* @__PURE__ */ jsx7("div", { children: message.role === "user" ? /* @__PURE__ */ jsx7(Bubble, { role: "user", children: message.parts.map((p) => p.kind === "text" ? p.text : "").join("") }) : /* @__PURE__ */ jsxs3(
1780
1959
  AnswerActionsFrame,
1781
1960
  {
@@ -1803,7 +1982,17 @@ function XbNovaChat({
1803
1982
  );
1804
1983
  }
1805
1984
  const spec = clientActionHandlers.get(part.toolName);
1806
- return spec?.render ? /* @__PURE__ */ jsx7("div", { children: spec.render(part.input) }, j) : null;
1985
+ return spec?.render ? /* @__PURE__ */ jsx7(
1986
+ "div",
1987
+ {
1988
+ onClickCapture: (e) => {
1989
+ const control = clickedControl(e);
1990
+ if (control) trackClientActionClick(part.toolName, control.label);
1991
+ },
1992
+ children: spec.render(part.input)
1993
+ },
1994
+ j
1995
+ ) : null;
1807
1996
  }
1808
1997
  return /* @__PURE__ */ jsx7(ToolView, { part, viewRenderers, onVerdictAction }, j);
1809
1998
  }),
@@ -1874,7 +2063,8 @@ function XbNovaChat({
1874
2063
  )
1875
2064
  ]
1876
2065
  }
1877
- )
2066
+ ),
2067
+ disclaimer ? /* @__PURE__ */ jsx7("p", { className: "-mt-1 px-4 pb-2.5 text-center text-[11px] leading-snug text-neutral-400 dark:text-neutral-500", children: disclaimer }) : null
1878
2068
  ] })
1879
2069
  }
1880
2070
  )
@@ -1976,8 +2166,8 @@ function AnswerActions({
1976
2166
  if (timerRef.current) clearTimeout(timerRef.current);
1977
2167
  timerRef.current = null;
1978
2168
  };
1979
- useEffect8(() => clearTimer, []);
1980
- useEffect8(() => {
2169
+ useEffect9(() => clearTimer, []);
2170
+ useEffect9(() => {
1981
2171
  if (!menuOpen) {
1982
2172
  setEntered(false);
1983
2173
  return;
@@ -2124,13 +2314,13 @@ function AnswerActionsFrame({
2124
2314
  }
2125
2315
 
2126
2316
  // src/facts.ts
2127
- import { useEffect as useEffect9, useRef as useRef9 } from "react";
2317
+ import { useEffect as useEffect10, useId, useRef as useRef9 } from "react";
2128
2318
  function useXbNovaFact(key, value) {
2129
2319
  const { factsRegistry } = useXbNovaInternal();
2130
- useEffect9(() => {
2320
+ useEffect10(() => {
2131
2321
  factsRegistry.set(key, value);
2132
2322
  });
2133
- useEffect9(() => {
2323
+ useEffect10(() => {
2134
2324
  return () => {
2135
2325
  factsRegistry.delete(key);
2136
2326
  };
@@ -2139,13 +2329,13 @@ function useXbNovaFact(key, value) {
2139
2329
  function useXbNovaFacts(values) {
2140
2330
  const { factsRegistry } = useXbNovaInternal();
2141
2331
  const keysRef = useRef9([]);
2142
- useEffect9(() => {
2332
+ useEffect10(() => {
2143
2333
  for (const [key, value] of Object.entries(values)) {
2144
2334
  factsRegistry.set(key, value);
2145
2335
  }
2146
2336
  keysRef.current = Object.keys(values);
2147
2337
  });
2148
- useEffect9(() => {
2338
+ useEffect10(() => {
2149
2339
  return () => {
2150
2340
  for (const key of keysRef.current) {
2151
2341
  factsRegistry.delete(key);
@@ -2153,6 +2343,18 @@ function useXbNovaFacts(values) {
2153
2343
  };
2154
2344
  }, [factsRegistry]);
2155
2345
  }
2346
+ function useXbNovaContext(entries) {
2347
+ const { contextRegistry } = useXbNovaInternal();
2348
+ const id = useId();
2349
+ useEffect10(() => {
2350
+ contextRegistry.set(id, entries);
2351
+ });
2352
+ useEffect10(() => {
2353
+ return () => {
2354
+ contextRegistry.delete(id);
2355
+ };
2356
+ }, [id, contextRegistry]);
2357
+ }
2156
2358
  function useXbNovaVisitor() {
2157
2359
  const { visitorId } = useXbNovaInternal();
2158
2360
  return visitorId;
@@ -2173,6 +2375,7 @@ export {
2173
2375
  XbNovaTrack,
2174
2376
  trackXbNovaEvent,
2175
2377
  useXbNovaChat,
2378
+ useXbNovaContext,
2176
2379
  useXbNovaFact,
2177
2380
  useXbNovaFacts,
2178
2381
  useXbNovaSettings,