@xbrain-swe/nova-chat-widget 0.1.11 → 0.3.0-dev.16

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 {
@@ -65,6 +66,44 @@ function InternalProvider({
65
66
  if (visitorIdRef.current === null) {
66
67
  visitorIdRef.current = resolveVisitorId();
67
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((event, properties = {}) => {
74
+ const { auth: a, visitorId: v, sessionId: sid } = latestRef.current;
75
+ if (!v) return;
76
+ const { conversation_id: conversationId, ...rest } = properties;
77
+ fetch(`${a.apiBaseUrl}/v1/events`, {
78
+ method: "POST",
79
+ keepalive: true,
80
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${a.apiKey}` },
81
+ body: JSON.stringify({
82
+ app_id: a.appId,
83
+ event,
84
+ visitor_id: v,
85
+ session_id: sid,
86
+ conversation_id: conversationId ?? null,
87
+ properties: { screen: a.screen, ...rest }
88
+ })
89
+ }).catch(() => {
90
+ });
91
+ }, []);
92
+ const widgetViewSentRef = useRef(false);
93
+ useEffect(() => {
94
+ if (widgetViewSentRef.current) return;
95
+ const send = () => {
96
+ if (widgetViewSentRef.current) return;
97
+ widgetViewSentRef.current = true;
98
+ sendEvent("widget_view");
99
+ };
100
+ if (sessionId) {
101
+ send();
102
+ return;
103
+ }
104
+ const timer = setTimeout(send, WIDGET_VIEW_WAIT_MS);
105
+ return () => clearTimeout(timer);
106
+ }, [sessionId, sendEvent]);
68
107
  const value = useMemo(
69
108
  () => ({
70
109
  auth,
@@ -72,10 +111,12 @@ function InternalProvider({
72
111
  factsRegistry,
73
112
  contextRegistry,
74
113
  clientActionHandlers: clientActionHandlers ?? emptyHandlers,
75
- visitorId: visitorIdRef.current,
114
+ visitorId,
115
+ sessionId,
116
+ sendEvent,
76
117
  theme
77
118
  }),
78
- [auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, theme]
119
+ [auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, visitorId, sessionId, sendEvent, theme]
79
120
  );
80
121
  return /* @__PURE__ */ jsx(InternalContext.Provider, { value, children });
81
122
  }
@@ -96,7 +137,7 @@ function XbNovaClientActionSync({ actions }) {
96
137
  const { auth } = useXbNovaInternal();
97
138
  const { apiKey, apiBaseUrl, appId } = auth;
98
139
  const lastSyncedRef = useRef2(null);
99
- useEffect(() => {
140
+ useEffect2(() => {
100
141
  if (!actions || Object.keys(actions).length === 0) return;
101
142
  const serialized = JSON.stringify(toSyncPayload(actions));
102
143
  if (serialized === lastSyncedRef.current) return;
@@ -150,7 +191,7 @@ function peekVisitCount(screen) {
150
191
  }
151
192
 
152
193
  // src/nudge-context.tsx
153
- import { createContext as createContext2, useCallback, useContext as useContext2, useEffect as useEffect2, useRef as useRef3, useState } from "react";
194
+ import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useEffect as useEffect3, useRef as useRef3, useState } from "react";
154
195
  import { jsx as jsx2 } from "react/jsx-runtime";
155
196
  var NudgeContext = createContext2(null);
156
197
  var HINTS_DISABLED_KEY = "xbnova_hints_disabled";
@@ -194,43 +235,43 @@ function NudgeStateProvider({ children }) {
194
235
  const [hintsEnabled, setHintsEnabled] = useState(() => !readFlag(HINTS_DISABLED_KEY));
195
236
  const [barHidden, setBarHidden] = useState(() => readFlag(BAR_HIDDEN_KEY));
196
237
  const streakRef = useRef3(readStreak());
197
- const disableHints = useCallback(() => {
238
+ const disableHints = useCallback2(() => {
198
239
  setHintsEnabled(false);
199
240
  writeFlag(HINTS_DISABLED_KEY, true);
200
241
  }, []);
201
- const enableHints = useCallback(() => {
242
+ const enableHints = useCallback2(() => {
202
243
  setHintsEnabled(true);
203
244
  writeFlag(HINTS_DISABLED_KEY, false);
204
245
  streakRef.current = 0;
205
246
  writeStreak(0);
206
247
  }, []);
207
- const hideBar = useCallback(() => {
248
+ const hideBar = useCallback2(() => {
208
249
  setBarHidden(true);
209
250
  writeFlag(BAR_HIDDEN_KEY, true);
210
251
  }, []);
211
- const showBar = useCallback(() => {
252
+ const showBar = useCallback2(() => {
212
253
  setBarHidden(false);
213
254
  writeFlag(BAR_HIDDEN_KEY, false);
214
255
  }, []);
215
- const offerHint = useCallback((next) => {
256
+ const offerHint = useCallback2((next) => {
216
257
  setHint((current) => current ? current : next);
217
258
  }, []);
218
- useEffect2(() => {
259
+ useEffect3(() => {
219
260
  if (hint) setLastHint(hint);
220
261
  }, [hint]);
221
- const dismissHint = useCallback(() => {
262
+ const dismissHint = useCallback2(() => {
222
263
  const limit = hint?.autoDisableAfterDismissals ?? DEFAULT_DISMISS_STREAK_LIMIT;
223
264
  setHint(null);
224
265
  streakRef.current += 1;
225
266
  writeStreak(streakRef.current);
226
267
  if (streakRef.current >= limit) disableHints();
227
268
  }, [hint, disableHints]);
228
- const openHint = useCallback(() => {
269
+ const openHint = useCallback2(() => {
229
270
  setHint(null);
230
271
  streakRef.current = 0;
231
272
  writeStreak(0);
232
273
  }, []);
233
- const autoHideHint = useCallback(() => {
274
+ const autoHideHint = useCallback2(() => {
234
275
  setHint(null);
235
276
  }, []);
236
277
  return /* @__PURE__ */ jsx2(
@@ -267,10 +308,10 @@ function useXbNovaSettings() {
267
308
  }
268
309
 
269
310
  // src/nudge-loop.tsx
270
- import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef5 } from "react";
311
+ import { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef5 } from "react";
271
312
 
272
313
  // src/track.tsx
273
- import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef4 } from "react";
314
+ import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef4 } from "react";
274
315
  import { jsx as jsx3 } from "react/jsx-runtime";
275
316
  var VISIBILITY_THRESHOLD = 0;
276
317
  function finalizeIfVisible(registry, region) {
@@ -287,7 +328,7 @@ function useXbNovaTrack(region, data, label) {
287
328
  dataRef.current = data;
288
329
  const labelRef = useRef4(label);
289
330
  labelRef.current = label;
290
- useEffect3(() => {
331
+ useEffect4(() => {
291
332
  const entry = trackRegistry.get(region);
292
333
  if (entry) {
293
334
  entry.data = data;
@@ -296,7 +337,7 @@ function useXbNovaTrack(region, data, label) {
296
337
  trackRegistry.set(region, { region, data, label, enteredAt: null, totalMs: 0, node: null });
297
338
  }
298
339
  }, [region, data, label, trackRegistry]);
299
- useEffect3(() => {
340
+ useEffect4(() => {
300
341
  return () => {
301
342
  observerRef.current?.disconnect();
302
343
  finalizeIfVisible(trackRegistry, region);
@@ -304,7 +345,7 @@ function useXbNovaTrack(region, data, label) {
304
345
  if (entry) entry.node = null;
305
346
  };
306
347
  }, [region, trackRegistry]);
307
- return useCallback2(
348
+ return useCallback3(
308
349
  (node) => {
309
350
  observerRef.current?.disconnect();
310
351
  observerRef.current = null;
@@ -378,7 +419,7 @@ function getFocusedTrackEntry(trackRegistry) {
378
419
  }
379
420
 
380
421
  // src/nudge-loop.tsx
381
- var EVAL_INTERVAL_MS = 1e3;
422
+ var EVAL_INTERVAL_MS = 250;
382
423
  var XBNOVA_EVENT_NAME = "xbnova:event";
383
424
  var SESSION_COUNT_KEY_PREFIX = "xbnova_hint_session_count:";
384
425
  var SCROLL_SETTLE_MS = 500;
@@ -571,7 +612,7 @@ function clearDebugOverlay(refs) {
571
612
  refs.bandLinesRef.current.clear();
572
613
  }
573
614
  function XbNovaNudgeLoop({ appId }) {
574
- const { auth, trackRegistry, factsRegistry, visitorId } = useXbNovaInternal();
615
+ const { auth, trackRegistry, factsRegistry, sendEvent } = useXbNovaInternal();
575
616
  const { apiKey, apiBaseUrl, screen } = auth;
576
617
  const { hint, offerHint, autoHideHint, hintsEnabled } = useNudgeState();
577
618
  const graphRef = useRef5(null);
@@ -586,7 +627,7 @@ function XbNovaNudgeLoop({ appId }) {
586
627
  hintRef.current = hint;
587
628
  const hintsEnabledRef = useRef5(hintsEnabled);
588
629
  hintsEnabledRef.current = hintsEnabled;
589
- useEffect4(() => {
630
+ useEffect5(() => {
590
631
  let cancelled = false;
591
632
  fetch(`${apiBaseUrl}/v1/apps/${appId}/nudge-graph`, {
592
633
  headers: { Authorization: `Bearer ${apiKey}` }
@@ -598,14 +639,16 @@ function XbNovaNudgeLoop({ appId }) {
598
639
  cancelled = true;
599
640
  };
600
641
  }, [apiBaseUrl, apiKey, appId]);
601
- const evaluate = useCallback3(
642
+ const evaluate = useCallback4(
602
643
  (firedEventName) => {
603
644
  const graph = graphRef.current;
604
645
  const debugEnabled = isDebugEnabled();
605
646
  const debugRefs = { panelRef: debugPanelRef, boxesRef: debugBoxesRef, bandLinesRef: debugBandLinesRef };
606
647
  if (!debugEnabled && debugPanelRef.current) clearDebugOverlay(debugRefs);
648
+ const tickDebug = debugEnabled ? (msg) => console.debug(msg) : () => {
649
+ };
607
650
  if (!graph) {
608
- console.debug("[XbNova] nudge \uD3C9\uAC00 \uAC74\uB108\uB700: \uADF8\uB798\uD504\uB97C \uC544\uC9C1 \uBABB \uBD88\uB7EC\uC634");
651
+ tickDebug("[XbNova] nudge \uD3C9\uAC00 \uAC74\uB108\uB700: \uADF8\uB798\uD504\uB97C \uC544\uC9C1 \uBABB \uBD88\uB7EC\uC634");
609
652
  return;
610
653
  }
611
654
  const canOffer = !hintRef.current && hintsEnabledRef.current;
@@ -656,18 +699,7 @@ function XbNovaNudgeLoop({ appId }) {
656
699
  autoHideMs: messageNode.auto_hide_ms ?? 2e4,
657
700
  quickReplies: messageNode.quick_replies ?? []
658
701
  });
659
- fetch(`${apiBaseUrl}/v1/nudge-events`, {
660
- method: "POST",
661
- headers: { "Content-Type": "application/json" },
662
- body: JSON.stringify({
663
- app_id: appId,
664
- nudge_node_id: messageNode.id,
665
- kind: "nudge_impression",
666
- bot_id: botId,
667
- visitor_id: visitorId
668
- })
669
- }).catch(() => {
670
- });
702
+ sendEvent("nudge_impression", { nudge_node_id: messageNode.id, bot_id: botId });
671
703
  };
672
704
  const tryFireFrom = (node) => {
673
705
  const nextEdge = (edgesFrom.get(node.id) ?? [])[0];
@@ -695,7 +727,7 @@ function XbNovaNudgeLoop({ appId }) {
695
727
  const maxPerSession = node.max_per_session === 0 ? Infinity : node.max_per_session ?? 3;
696
728
  const sessionCount = peekSessionCount(node.id);
697
729
  if (sessionCount >= maxPerSession) {
698
- console.debug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id}(${node.trigger_type}) \uAC74\uB108\uB700: \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC`);
730
+ tickDebug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id}(${node.trigger_type}) \uAC74\uB108\uB700: \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC`);
699
731
  if (debugEnabled && node.trigger_type === "dwell") {
700
732
  debugLines.push(`- [${node.id}] \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC \u2014 \uB354 \uC548 \uC7BC`);
701
733
  }
@@ -730,7 +762,7 @@ function XbNovaNudgeLoop({ appId }) {
730
762
  overlapPx = status.overlapPx;
731
763
  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}`;
732
764
  }
733
- console.debug(
765
+ tickDebug(
734
766
  `[XbNova] band dwell \uD2B8\uB9AC\uAC70=${node.id} region="${region}" band%=${fromPercent}~${toPercent} | ${statusDetail}`
735
767
  );
736
768
  dwellRecords.push({ node, state, requiredMs, region, found, screenMatches, inBand, overlapPx });
@@ -739,7 +771,7 @@ function XbNovaNudgeLoop({ appId }) {
739
771
  const fires = triggerFires(node, { screen, firedEventName });
740
772
  if (!fires) continue;
741
773
  if (!canOffer) {
742
- 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`);
774
+ 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`);
743
775
  continue;
744
776
  }
745
777
  if (tryFireFrom(node)) {
@@ -839,9 +871,9 @@ function XbNovaNudgeLoop({ appId }) {
839
871
  ensureDebugPanel(debugPanelRef, debugCollapsedRef).body.textContent = debugLines.join("\n");
840
872
  }
841
873
  },
842
- [apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, visitorId]
874
+ [apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, sendEvent]
843
875
  );
844
- useEffect4(() => {
876
+ useEffect5(() => {
845
877
  const interval = window.setInterval(() => evaluate(null), EVAL_INTERVAL_MS);
846
878
  const onEvent = (e) => evaluate(e.detail.eventName);
847
879
  window.addEventListener(XBNOVA_EVENT_NAME, onEvent);
@@ -870,24 +902,36 @@ function XbNovaProvider({
870
902
  apiBaseUrl = DEFAULT_API_BASE_URL,
871
903
  clientActions,
872
904
  theme,
905
+ userKey,
906
+ threadKey,
907
+ sessionKey,
873
908
  children
874
909
  }) {
875
- useEffect5(() => {
910
+ useEffect6(() => {
876
911
  if (apiKey.startsWith("sk_")) {
877
912
  console.error(
878
913
  "[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."
879
914
  );
880
915
  }
881
916
  }, [apiKey]);
882
- useEffect5(() => {
917
+ useEffect6(() => {
883
918
  markSessionStart();
884
919
  }, []);
885
- useEffect5(() => {
920
+ useEffect6(() => {
886
921
  if (screen) bumpVisitCount(screen);
887
922
  }, [screen]);
888
923
  const auth = useMemo2(
889
- () => ({ apiKey, botId, appId, apiBaseUrl, screen }),
890
- [apiKey, botId, appId, apiBaseUrl, screen]
924
+ () => ({
925
+ apiKey,
926
+ botId,
927
+ appId,
928
+ apiBaseUrl,
929
+ screen,
930
+ userKey: userKey || void 0,
931
+ threadKey: threadKey || void 0,
932
+ sessionKey: sessionKey || void 0
933
+ }),
934
+ [apiKey, botId, appId, apiBaseUrl, screen, userKey, threadKey, sessionKey]
891
935
  );
892
936
  const resolvedTheme = useMemo2(
893
937
  () => ({ ...DEFAULT_THEME, ...theme }),
@@ -916,13 +960,13 @@ function XbNovaProvider({
916
960
  }
917
961
 
918
962
  // src/chat.tsx
919
- import { useEffect as useEffect8, useRef as useRef8, useState as useState4 } from "react";
963
+ import { useEffect as useEffect9, useRef as useRef8, useState as useState4 } from "react";
920
964
  import ReactMarkdown from "react-markdown";
921
965
  import remarkBreaks from "remark-breaks";
922
966
  import remarkGfm from "remark-gfm";
923
967
 
924
968
  // src/shadow-root.tsx
925
- import { useEffect as useEffect6, useRef as useRef6, useState as useState2 } from "react";
969
+ import { useEffect as useEffect7, useRef as useRef6, useState as useState2 } from "react";
926
970
  import { createPortal } from "react-dom";
927
971
  import { jsx as jsx5 } from "react/jsx-runtime";
928
972
  var cssPromise = null;
@@ -938,7 +982,7 @@ function loadCss() {
938
982
  }
939
983
  function useResolvedScheme(scheme) {
940
984
  const [systemDark, setSystemDark] = useState2(false);
941
- useEffect6(() => {
985
+ useEffect7(() => {
942
986
  if (scheme !== "system") return;
943
987
  const mq = window.matchMedia("(prefers-color-scheme: dark)");
944
988
  setSystemDark(mq.matches);
@@ -958,10 +1002,10 @@ function XbNovaShadowRoot({
958
1002
  const hostRef = useRef6(null);
959
1003
  const [mountNode, setMountNode] = useState2(null);
960
1004
  const [portalTarget, setPortalTarget] = useState2(null);
961
- useEffect6(() => {
1005
+ useEffect7(() => {
962
1006
  setPortalTarget(document.body);
963
1007
  }, []);
964
- useEffect6(() => {
1008
+ useEffect7(() => {
965
1009
  const host = hostRef.current;
966
1010
  if (!host || host.shadowRoot) return;
967
1011
  const shadow = host.attachShadow({ mode: "open" });
@@ -976,7 +1020,7 @@ function XbNovaShadowRoot({
976
1020
  shadow.appendChild(mount);
977
1021
  setMountNode(mount);
978
1022
  }, [portalTarget]);
979
- useEffect6(() => {
1023
+ useEffect7(() => {
980
1024
  if (!mountNode) return;
981
1025
  mountNode.dataset.xbnovaScheme = scheme;
982
1026
  mountNode.style.colorScheme = scheme;
@@ -989,15 +1033,44 @@ function XbNovaShadowRoot({
989
1033
  }
990
1034
 
991
1035
  // src/use-chat.ts
992
- import { useEffect as useEffect7, useRef as useRef7, useState as useState3 } from "react";
1036
+ import { useEffect as useEffect8, useRef as useRef7, useState as useState3 } from "react";
993
1037
 
994
1038
  // src/stream.ts
1039
+ function fromStoredMessages(stored) {
1040
+ return stored.map((m) => ({
1041
+ id: m.role === "assistant" ? m.id : void 0,
1042
+ role: m.role === "assistant" ? "assistant" : "user",
1043
+ parts: m.parts.flatMap((p, i) => {
1044
+ const type = String(p.type ?? "");
1045
+ if (type === "text") {
1046
+ const text = typeof p.text === "string" ? p.text : "";
1047
+ return text ? [{ kind: "text", id: `${m.id}:${i}`, text }] : [];
1048
+ }
1049
+ if (type.startsWith("tool-")) {
1050
+ const meta = p.providerMetadata;
1051
+ return [
1052
+ {
1053
+ kind: "tool",
1054
+ toolCallId: String(p.toolCallId ?? `${m.id}:${i}`),
1055
+ toolName: type.slice("tool-".length),
1056
+ state: p.state === "output-error" ? "error" : "output",
1057
+ input: p.input,
1058
+ output: p.output,
1059
+ view: meta?.["kr.dsauto.chat"]?.view ?? null
1060
+ }
1061
+ ];
1062
+ }
1063
+ return [];
1064
+ })
1065
+ }));
1066
+ }
995
1067
  async function consumeSse(response, handlers) {
996
1068
  if (!response.ok || !response.body) {
997
1069
  handlers.onError?.(`\uC694\uCCAD \uC2E4\uD328 (${response.status})`);
998
1070
  return null;
999
1071
  }
1000
1072
  const conversationId = response.headers.get("x-conversation-id");
1073
+ if (conversationId) handlers.onConversationId?.(conversationId);
1001
1074
  const reader = response.body.getReader();
1002
1075
  const decoder = new TextDecoder();
1003
1076
  let buffer = "";
@@ -1102,12 +1175,19 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
1102
1175
  // src/use-chat.ts
1103
1176
  var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
1104
1177
  function useXbNovaChat(options) {
1105
- const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId } = useXbNovaInternal();
1178
+ const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId, sessionId, sendEvent } = useXbNovaInternal();
1106
1179
  const botId = options?.botId ?? auth.botId;
1107
1180
  const nudgeNodeId = options?.nudgeNodeId;
1108
- const { apiKey, apiBaseUrl } = auth;
1181
+ const { apiKey, apiBaseUrl, threadKey } = auth;
1182
+ const userKey = visitorId;
1109
1183
  const [onboarding, setOnboarding] = useState3(null);
1110
1184
  const [messages, setMessages] = useState3([]);
1185
+ const [resumed, setResumed] = useState3(false);
1186
+ const [sentOpening, setSentOpening] = useState3(null);
1187
+ const conversationIdRef = useRef7(null);
1188
+ const pendingOpeningRef = useRef7(null);
1189
+ const messagesRef = useRef7(messages);
1190
+ messagesRef.current = messages;
1111
1191
  const [busy, setBusy] = useState3(false);
1112
1192
  const [error, setError] = useState3(null);
1113
1193
  const [clientResults, setClientResults] = useState3({});
@@ -1117,19 +1197,60 @@ function useXbNovaChat(options) {
1117
1197
  const dispatchedClientActionsRef = useRef7(/* @__PURE__ */ new Set());
1118
1198
  const nudgeNodeIdRef = useRef7(nudgeNodeId);
1119
1199
  nudgeNodeIdRef.current = nudgeNodeId;
1120
- useEffect7(() => {
1200
+ useEffect8(() => {
1201
+ let cancelled = false;
1121
1202
  setOnboarding(null);
1122
1203
  setMessages([]);
1204
+ setResumed(false);
1205
+ setSentOpening(null);
1123
1206
  setError(null);
1124
1207
  setClientResults({});
1125
1208
  setFeedbackState({});
1126
1209
  dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
1210
+ conversationIdRef.current = null;
1211
+ pendingOpeningRef.current = null;
1127
1212
  fetch(`${apiBaseUrl}/v1/conversations`, {
1128
1213
  method: "POST",
1129
1214
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
1130
- body: JSON.stringify({ bot_id: botId, nudge_node_id: nudgeNodeIdRef.current ?? null })
1131
- }).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}`));
1132
- }, [apiBaseUrl, apiKey, botId]);
1215
+ body: JSON.stringify({
1216
+ bot_id: botId,
1217
+ nudge_node_id: nudgeNodeIdRef.current ?? null,
1218
+ visitor_id: userKey || null,
1219
+ thread_key: threadKey ?? null,
1220
+ session_id: sessionId,
1221
+ defer: true
1222
+ })
1223
+ }).then((r) => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))).then((data) => {
1224
+ if (cancelled) return;
1225
+ if (conversationIdRef.current || messagesRef.current.length > 0) {
1226
+ setOnboarding(data);
1227
+ return;
1228
+ }
1229
+ conversationIdRef.current = data.conversation_id;
1230
+ const restored = fromStoredMessages(data.messages ?? []);
1231
+ for (const m of restored) {
1232
+ for (const p of m.parts) if (p.kind === "tool") dispatchedClientActionsRef.current.add(p.toolCallId);
1233
+ }
1234
+ setMessages(restored);
1235
+ setResumed(restored.length > 0);
1236
+ setOnboarding(data);
1237
+ }).catch((e) => {
1238
+ if (!cancelled) setError(`\uBD07\uC744 \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`);
1239
+ });
1240
+ return () => {
1241
+ cancelled = true;
1242
+ };
1243
+ }, [apiBaseUrl, apiKey, botId, userKey, threadKey]);
1244
+ const openingMessage = options?.openingMessage;
1245
+ useEffect8(() => {
1246
+ if (!onboarding || !nudgeNodeId || !openingMessage) return;
1247
+ if (messagesRef.current.length === 0) return;
1248
+ setMessages((prev) => [
1249
+ ...prev,
1250
+ { role: "assistant", parts: [{ kind: "text", id: `nudge:${nudgeNodeId}:${prev.length}`, text: openingMessage }] }
1251
+ ]);
1252
+ pendingOpeningRef.current = openingMessage;
1253
+ }, [onboarding, nudgeNodeId, openingMessage]);
1133
1254
  const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
1134
1255
  const next = [...prev];
1135
1256
  next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
@@ -1142,7 +1263,7 @@ function useXbNovaChat(options) {
1142
1263
  return next;
1143
1264
  });
1144
1265
  async function setFeedback(messageId, rating, reason) {
1145
- const conversationId = onboarding?.conversation_id;
1266
+ const conversationId = conversationIdRef.current;
1146
1267
  if (!conversationId || !messageId) return;
1147
1268
  const prevRating = feedbackRef.current[messageId] ?? null;
1148
1269
  const apply = (r) => setFeedbackState((prev) => {
@@ -1162,12 +1283,21 @@ function useXbNovaChat(options) {
1162
1283
  }
1163
1284
  );
1164
1285
  if (!res.ok) throw new Error(`${res.status}`);
1286
+ sendEvent("feedback_submit", { conversation_id: conversationId, message_id: messageId, rating, reason });
1165
1287
  options?.onFeedback?.({ conversationId, messageId, rating, reason });
1166
1288
  } catch (e) {
1167
1289
  console.warn("[xbnova] \uD53C\uB4DC\uBC31 \uC800\uC7A5 \uC2E4\uD328", e);
1168
1290
  apply(prevRating);
1169
1291
  }
1170
1292
  }
1293
+ function postClientActionEvent(kind, action, label) {
1294
+ const conversationId = conversationIdRef.current;
1295
+ if (!conversationId) return;
1296
+ sendEvent(kind, { conversation_id: conversationId, action, label });
1297
+ }
1298
+ function trackClientActionClick(action, label) {
1299
+ postClientActionEvent("cta_click", action, label);
1300
+ }
1171
1301
  async function runClientAction(spec, part, existingParts) {
1172
1302
  if (spec.dismissSheet) options?.onDismissSheet?.();
1173
1303
  let result;
@@ -1191,7 +1321,7 @@ function useXbNovaChat(options) {
1191
1321
  result = e instanceof Error ? `\uC624\uB958: ${e.message}` : "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
1192
1322
  }
1193
1323
  if (!spec.awaitResult) return;
1194
- const conversationId = onboarding?.conversation_id;
1324
+ const conversationId = conversationIdRef.current;
1195
1325
  if (!conversationId) return;
1196
1326
  const structured = result && typeof result === "object" ? result : null;
1197
1327
  if (structured?.view) {
@@ -1225,7 +1355,7 @@ function useXbNovaChat(options) {
1225
1355
  setBusy(false);
1226
1356
  }
1227
1357
  }
1228
- useEffect7(() => {
1358
+ useEffect8(() => {
1229
1359
  if (clientActionHandlers.size === 0) return;
1230
1360
  const last = messages[messages.length - 1];
1231
1361
  if (!last || last.role !== "assistant") return;
@@ -1235,12 +1365,18 @@ function useXbNovaChat(options) {
1235
1365
  if (!spec) continue;
1236
1366
  if (dispatchedClientActionsRef.current.has(part.toolCallId)) continue;
1237
1367
  dispatchedClientActionsRef.current.add(part.toolCallId);
1368
+ if (spec.render) postClientActionEvent("cta_impression", part.toolName, null);
1238
1369
  void runClientAction(spec, part, last.parts);
1239
1370
  }
1240
1371
  }, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
1241
1372
  async function send(text) {
1242
1373
  const trimmed = text.trim();
1243
1374
  if (!trimmed || busy) return;
1375
+ sendEvent("message_send", {
1376
+ conversation_id: conversationIdRef.current,
1377
+ is_first: messages.length === 0,
1378
+ length: trimmed.length
1379
+ });
1244
1380
  setError(null);
1245
1381
  setBusy(true);
1246
1382
  setMessages((prev) => [
@@ -1257,6 +1393,14 @@ function useXbNovaChat(options) {
1257
1393
  const notes = collectContextEntries(contextRegistry);
1258
1394
  const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
1259
1395
  const context = focused || notes.length ? { ...screen, ...notes.length ? { notes } : {} } : void 0;
1396
+ let openingMessage2;
1397
+ if (messages.length === 0) {
1398
+ openingMessage2 = (options?.openingMessage ?? onboarding?.greeting) || null;
1399
+ setSentOpening(openingMessage2);
1400
+ } else {
1401
+ openingMessage2 = pendingOpeningRef.current;
1402
+ }
1403
+ pendingOpeningRef.current = null;
1260
1404
  try {
1261
1405
  await streamChat(
1262
1406
  apiBaseUrl,
@@ -1264,12 +1408,22 @@ function useXbNovaChat(options) {
1264
1408
  {
1265
1409
  bot_id: botId,
1266
1410
  message: trimmed,
1267
- conversation_id: onboarding?.conversation_id ?? null,
1411
+ conversation_id: conversationIdRef.current,
1268
1412
  context,
1269
1413
  nudge_node_id: nudgeNodeId ?? null,
1270
- opening_message: messages.length === 0 ? (options?.openingMessage ?? onboarding?.greeting) || null : null
1414
+ opening_message: openingMessage2,
1415
+ visitor_id: userKey || null,
1416
+ thread_key: threadKey ?? null,
1417
+ session_id: sessionId
1271
1418
  },
1272
- { onPart: replaceLast, onMessageId: setLastAssistantId, onError: setError }
1419
+ {
1420
+ onPart: replaceLast,
1421
+ onMessageId: setLastAssistantId,
1422
+ onConversationId: (id) => {
1423
+ conversationIdRef.current = id;
1424
+ },
1425
+ onError: setError
1426
+ }
1273
1427
  );
1274
1428
  } catch (e) {
1275
1429
  setError(e instanceof Error ? e.message : String(e));
@@ -1277,7 +1431,19 @@ function useXbNovaChat(options) {
1277
1431
  setBusy(false);
1278
1432
  }
1279
1433
  }
1280
- return { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback };
1434
+ const opening = resumed ? null : sentOpening ?? options?.openingMessage ?? onboarding?.greeting ?? null;
1435
+ return {
1436
+ onboarding,
1437
+ opening,
1438
+ messages,
1439
+ busy,
1440
+ error,
1441
+ send,
1442
+ clientResults,
1443
+ feedback,
1444
+ setFeedback,
1445
+ trackClientActionClick
1446
+ };
1281
1447
  }
1282
1448
 
1283
1449
  // src/views.tsx
@@ -1410,6 +1576,13 @@ function hasVisiblePart(parts, clientActionHandlers, clientResults) {
1410
1576
  }
1411
1577
  var DISCLOSURE_SEEN_KEY = "xbnova_disclosure_seen";
1412
1578
  var SUMMARY_FACT_KEY = "xbnova_summary";
1579
+ function clickedControl(e) {
1580
+ const target = e.target instanceof Element ? e.target : null;
1581
+ const control = target?.closest("button, a, [role='button']");
1582
+ if (!control || !e.currentTarget.contains(control)) return null;
1583
+ const text = (control.textContent ?? "").replace(/\s+/g, " ").trim();
1584
+ return { label: text || control.getAttribute("aria-label") || null };
1585
+ }
1413
1586
  function readDisclosureSeen() {
1414
1587
  if (typeof window === "undefined") return true;
1415
1588
  try {
@@ -1428,7 +1601,7 @@ function markDisclosureSeen() {
1428
1601
  var DESKTOP_MEDIA_QUERY = "(min-width: 640px)";
1429
1602
  function useIsDesktopViewport() {
1430
1603
  const [isDesktop, setIsDesktop] = useState4(false);
1431
- useEffect8(() => {
1604
+ useEffect9(() => {
1432
1605
  if (typeof window === "undefined") return;
1433
1606
  const mql = window.matchMedia(DESKTOP_MEDIA_QUERY);
1434
1607
  setIsDesktop(mql.matches);
@@ -1452,7 +1625,7 @@ function backdropBaseStyle(isDesktop) {
1452
1625
  }
1453
1626
  function useTypewriter(text, key, speedMs = 28) {
1454
1627
  const [shown, setShown] = useState4("");
1455
- useEffect8(() => {
1628
+ useEffect9(() => {
1456
1629
  setShown("");
1457
1630
  if (!text) return;
1458
1631
  let i = 0;
@@ -1539,9 +1712,10 @@ function XbNovaChat({
1539
1712
  avatarSize = 28,
1540
1713
  onVerdictAction,
1541
1714
  feedback: feedbackEnabled = true,
1542
- onFeedback
1715
+ onFeedback,
1716
+ onOpen
1543
1717
  }) {
1544
- const { auth, trackRegistry, factsRegistry, visitorId, clientActionHandlers, theme } = useXbNovaInternal();
1718
+ const { auth, trackRegistry, factsRegistry, clientActionHandlers, theme, sendEvent } = useXbNovaInternal();
1545
1719
  const themeVars = {
1546
1720
  "--xbnova-accent-1": theme.accentFrom,
1547
1721
  "--xbnova-accent-2": theme.accentTo
@@ -1562,7 +1736,7 @@ function XbNovaChat({
1562
1736
  const [input, setInput] = useState4("");
1563
1737
  const bottomRef = useRef8(null);
1564
1738
  const typedHintText = useTypewriter(hint?.text ?? "", hint?.triggerNodeId ?? "");
1565
- const { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback } = useXbNovaChat({
1739
+ const { onboarding, opening, messages, busy, error, send, clientResults, feedback, setFeedback, trackClientActionClick } = useXbNovaChat({
1566
1740
  onFeedback,
1567
1741
  botId: activeBotId ?? botId,
1568
1742
  nudgeNodeId: activeNudgeNodeId,
@@ -1571,7 +1745,7 @@ function XbNovaChat({
1571
1745
  });
1572
1746
  const effectiveBotId = activeBotId ?? botId;
1573
1747
  const [cachedBranding, setCachedBranding] = useState4(null);
1574
- useEffect8(() => {
1748
+ useEffect9(() => {
1575
1749
  try {
1576
1750
  const raw = window.localStorage.getItem(`xbnova_branding:${effectiveBotId}`);
1577
1751
  setCachedBranding(raw ? JSON.parse(raw) : null);
@@ -1579,22 +1753,29 @@ function XbNovaChat({
1579
1753
  setCachedBranding(null);
1580
1754
  }
1581
1755
  }, [effectiveBotId]);
1582
- useEffect8(() => {
1756
+ useEffect9(() => {
1583
1757
  if (!onboarding?.branding) return;
1584
1758
  try {
1585
1759
  window.localStorage.setItem(
1586
1760
  `xbnova_branding:${effectiveBotId}`,
1587
- JSON.stringify({ logo_url: onboarding.branding.logo_url, avatar_text: onboarding.branding.avatar_text })
1761
+ JSON.stringify({
1762
+ logo_url: onboarding.branding.logo_url,
1763
+ avatar_text: onboarding.branding.avatar_text,
1764
+ badge_text: onboarding.branding.badge_text,
1765
+ disclaimer: onboarding.branding.disclaimer
1766
+ })
1588
1767
  );
1589
1768
  } catch {
1590
1769
  }
1591
1770
  }, [onboarding?.branding, effectiveBotId]);
1592
1771
  const branding = onboarding?.branding ?? cachedBranding ?? void 0;
1772
+ const badgeText = branding?.badge_text?.trim() ?? "";
1773
+ const disclaimer = branding?.disclaimer?.trim() ?? "";
1593
1774
  const brandingReady = Boolean(onboarding) || cachedBranding !== null || Boolean(error);
1594
- useEffect8(() => {
1775
+ useEffect9(() => {
1595
1776
  bottomRef.current?.scrollIntoView({ behavior: "smooth" });
1596
1777
  }, [messages]);
1597
- useEffect8(() => {
1778
+ useEffect9(() => {
1598
1779
  if (!open) {
1599
1780
  setEntered(false);
1600
1781
  return;
@@ -1602,7 +1783,7 @@ function XbNovaChat({
1602
1783
  const raf = requestAnimationFrame(() => setEntered(true));
1603
1784
  return () => cancelAnimationFrame(raf);
1604
1785
  }, [open]);
1605
- useEffect8(() => {
1786
+ useEffect9(() => {
1606
1787
  if (!hint || hint.autoHideMs <= 0) return;
1607
1788
  console.debug(`[XbNova] \uD78C\uD2B8 "${hint.text}" \uD45C\uC2DC \u2014 ${hint.autoHideMs}ms \uD6C4 \uC790\uB3D9\uC73C\uB85C \uC0AC\uB77C\uC9D0`);
1608
1789
  const timer = window.setTimeout(() => {
@@ -1619,18 +1800,7 @@ function XbNovaChat({
1619
1800
  setOpeningLine(consumed.text);
1620
1801
  setActiveNudgeNodeId(consumed.messageNodeId);
1621
1802
  setNudgeQuickReplies(consumed.quickReplies);
1622
- fetch(`${auth.apiBaseUrl}/v1/nudge-events`, {
1623
- method: "POST",
1624
- headers: { "Content-Type": "application/json" },
1625
- body: JSON.stringify({
1626
- app_id: auth.appId,
1627
- nudge_node_id: consumed.messageNodeId,
1628
- kind: "nudge_open",
1629
- bot_id: consumed.botId,
1630
- visitor_id: visitorId
1631
- })
1632
- }).catch(() => {
1633
- });
1803
+ sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1634
1804
  } else {
1635
1805
  setNudgeQuickReplies([]);
1636
1806
  setActiveNudgeNodeId(void 0);
@@ -1644,6 +1814,16 @@ function XbNovaChat({
1644
1814
  markDisclosureSeen();
1645
1815
  }
1646
1816
  setOpen(true);
1817
+ sendEvent("chat_open", { from_nudge: Boolean(consumed) });
1818
+ try {
1819
+ onOpen?.({ fromNudge: Boolean(consumed), hintText: consumed?.text });
1820
+ } catch (err) {
1821
+ console.warn("[XbNova] onOpen \uCF5C\uBC31 \uC624\uB958", err);
1822
+ }
1823
+ }
1824
+ function closeSheet() {
1825
+ setOpen(false);
1826
+ sendEvent("chat_close");
1647
1827
  }
1648
1828
  if (!open) {
1649
1829
  if (barHidden) return null;
@@ -1739,7 +1919,6 @@ function XbNovaChat({
1739
1919
  const summary = resolveFactValue(factsRegistry.get(SUMMARY_FACT_KEY));
1740
1920
  const focusedLabel = getFocusedTrackEntry(trackRegistry)?.label;
1741
1921
  const contextLine = summary ?? (focusedLabel ? `${focusedLabel} \uBCF4\uB294 \uC911` : void 0);
1742
- const firstBubble = openingLine ?? onboarding?.greeting;
1743
1922
  return /* @__PURE__ */ jsxs3(Fragment, { children: [
1744
1923
  !isDesktop ? /* @__PURE__ */ jsx7(
1745
1924
  XbNovaShadowRoot,
@@ -1750,7 +1929,7 @@ function XbNovaChat({
1750
1929
  transition: "opacity 260ms ease-out",
1751
1930
  ...backdropStyle
1752
1931
  },
1753
- children: /* @__PURE__ */ jsx7("div", { className: "h-full w-full bg-black/45", onClick: () => setOpen(false) })
1932
+ children: /* @__PURE__ */ jsx7("div", { className: "h-full w-full bg-black/45", onClick: closeSheet })
1754
1933
  }
1755
1934
  ) : null,
1756
1935
  /* @__PURE__ */ jsx7(
@@ -1776,14 +1955,17 @@ function XbNovaChat({
1776
1955
  }
1777
1956
  ),
1778
1957
  /* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
1779
- /* @__PURE__ */ jsx7("div", { className: "text-sm font-semibold", children: onboarding?.branding?.title ?? "AI \uB3D9\uD589" }),
1958
+ /* @__PURE__ */ jsxs3("div", { className: "flex min-w-0 items-center gap-1.5", children: [
1959
+ /* @__PURE__ */ jsx7("span", { className: "truncate text-sm font-semibold", children: onboarding?.branding?.title ?? "AI \uB3D9\uD589" }),
1960
+ 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
1961
+ ] }),
1780
1962
  contextLine ? /* @__PURE__ */ jsx7("div", { className: "truncate text-[11px] text-neutral-400", children: contextLine }) : null
1781
1963
  ] }),
1782
1964
  /* @__PURE__ */ jsx7(
1783
1965
  "button",
1784
1966
  {
1785
1967
  type: "button",
1786
- onClick: () => setOpen(false),
1968
+ onClick: closeSheet,
1787
1969
  "aria-label": "\uCC44\uD305 \uB2EB\uAE30",
1788
1970
  className: "shrink-0 text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200",
1789
1971
  children: "\xD7"
@@ -1792,7 +1974,7 @@ function XbNovaChat({
1792
1974
  ] }),
1793
1975
  /* @__PURE__ */ jsxs3("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [
1794
1976
  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,
1795
- firstBubble ? /* @__PURE__ */ jsx7(Bubble, { role: "assistant", children: /* @__PURE__ */ jsx7(AssistantMarkdown, { text: firstBubble }) }) : null,
1977
+ opening ? /* @__PURE__ */ jsx7(Bubble, { role: "assistant", children: /* @__PURE__ */ jsx7(AssistantMarkdown, { text: opening }) }) : null,
1796
1978
  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(
1797
1979
  AnswerActionsFrame,
1798
1980
  {
@@ -1820,7 +2002,17 @@ function XbNovaChat({
1820
2002
  );
1821
2003
  }
1822
2004
  const spec = clientActionHandlers.get(part.toolName);
1823
- return spec?.render ? /* @__PURE__ */ jsx7("div", { children: spec.render(part.input) }, j) : null;
2005
+ return spec?.render ? /* @__PURE__ */ jsx7(
2006
+ "div",
2007
+ {
2008
+ onClickCapture: (e) => {
2009
+ const control = clickedControl(e);
2010
+ if (control) trackClientActionClick(part.toolName, control.label);
2011
+ },
2012
+ children: spec.render(part.input)
2013
+ },
2014
+ j
2015
+ ) : null;
1824
2016
  }
1825
2017
  return /* @__PURE__ */ jsx7(ToolView, { part, viewRenderers, onVerdictAction }, j);
1826
2018
  }),
@@ -1891,7 +2083,8 @@ function XbNovaChat({
1891
2083
  )
1892
2084
  ]
1893
2085
  }
1894
- )
2086
+ ),
2087
+ 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
1895
2088
  ] })
1896
2089
  }
1897
2090
  )
@@ -1993,8 +2186,8 @@ function AnswerActions({
1993
2186
  if (timerRef.current) clearTimeout(timerRef.current);
1994
2187
  timerRef.current = null;
1995
2188
  };
1996
- useEffect8(() => clearTimer, []);
1997
- useEffect8(() => {
2189
+ useEffect9(() => clearTimer, []);
2190
+ useEffect9(() => {
1998
2191
  if (!menuOpen) {
1999
2192
  setEntered(false);
2000
2193
  return;
@@ -2141,13 +2334,13 @@ function AnswerActionsFrame({
2141
2334
  }
2142
2335
 
2143
2336
  // src/facts.ts
2144
- import { useEffect as useEffect9, useId, useRef as useRef9 } from "react";
2337
+ import { useEffect as useEffect10, useId, useRef as useRef9 } from "react";
2145
2338
  function useXbNovaFact(key, value) {
2146
2339
  const { factsRegistry } = useXbNovaInternal();
2147
- useEffect9(() => {
2340
+ useEffect10(() => {
2148
2341
  factsRegistry.set(key, value);
2149
2342
  });
2150
- useEffect9(() => {
2343
+ useEffect10(() => {
2151
2344
  return () => {
2152
2345
  factsRegistry.delete(key);
2153
2346
  };
@@ -2156,13 +2349,13 @@ function useXbNovaFact(key, value) {
2156
2349
  function useXbNovaFacts(values) {
2157
2350
  const { factsRegistry } = useXbNovaInternal();
2158
2351
  const keysRef = useRef9([]);
2159
- useEffect9(() => {
2352
+ useEffect10(() => {
2160
2353
  for (const [key, value] of Object.entries(values)) {
2161
2354
  factsRegistry.set(key, value);
2162
2355
  }
2163
2356
  keysRef.current = Object.keys(values);
2164
2357
  });
2165
- useEffect9(() => {
2358
+ useEffect10(() => {
2166
2359
  return () => {
2167
2360
  for (const key of keysRef.current) {
2168
2361
  factsRegistry.delete(key);
@@ -2173,10 +2366,10 @@ function useXbNovaFacts(values) {
2173
2366
  function useXbNovaContext(entries) {
2174
2367
  const { contextRegistry } = useXbNovaInternal();
2175
2368
  const id = useId();
2176
- useEffect9(() => {
2369
+ useEffect10(() => {
2177
2370
  contextRegistry.set(id, entries);
2178
2371
  });
2179
- useEffect9(() => {
2372
+ useEffect10(() => {
2180
2373
  return () => {
2181
2374
  contextRegistry.delete(id);
2182
2375
  };