@xbrain-swe/nova-chat-widget 0.1.11 → 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 {
@@ -65,6 +66,35 @@ 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((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]);
68
98
  const value = useMemo(
69
99
  () => ({
70
100
  auth,
@@ -72,10 +102,12 @@ function InternalProvider({
72
102
  factsRegistry,
73
103
  contextRegistry,
74
104
  clientActionHandlers: clientActionHandlers ?? emptyHandlers,
75
- visitorId: visitorIdRef.current,
105
+ visitorId,
106
+ sessionId,
107
+ sendEvent,
76
108
  theme
77
109
  }),
78
- [auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, theme]
110
+ [auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, visitorId, sessionId, sendEvent, theme]
79
111
  );
80
112
  return /* @__PURE__ */ jsx(InternalContext.Provider, { value, children });
81
113
  }
@@ -96,7 +128,7 @@ function XbNovaClientActionSync({ actions }) {
96
128
  const { auth } = useXbNovaInternal();
97
129
  const { apiKey, apiBaseUrl, appId } = auth;
98
130
  const lastSyncedRef = useRef2(null);
99
- useEffect(() => {
131
+ useEffect2(() => {
100
132
  if (!actions || Object.keys(actions).length === 0) return;
101
133
  const serialized = JSON.stringify(toSyncPayload(actions));
102
134
  if (serialized === lastSyncedRef.current) return;
@@ -150,7 +182,7 @@ function peekVisitCount(screen) {
150
182
  }
151
183
 
152
184
  // src/nudge-context.tsx
153
- 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";
154
186
  import { jsx as jsx2 } from "react/jsx-runtime";
155
187
  var NudgeContext = createContext2(null);
156
188
  var HINTS_DISABLED_KEY = "xbnova_hints_disabled";
@@ -194,43 +226,43 @@ function NudgeStateProvider({ children }) {
194
226
  const [hintsEnabled, setHintsEnabled] = useState(() => !readFlag(HINTS_DISABLED_KEY));
195
227
  const [barHidden, setBarHidden] = useState(() => readFlag(BAR_HIDDEN_KEY));
196
228
  const streakRef = useRef3(readStreak());
197
- const disableHints = useCallback(() => {
229
+ const disableHints = useCallback2(() => {
198
230
  setHintsEnabled(false);
199
231
  writeFlag(HINTS_DISABLED_KEY, true);
200
232
  }, []);
201
- const enableHints = useCallback(() => {
233
+ const enableHints = useCallback2(() => {
202
234
  setHintsEnabled(true);
203
235
  writeFlag(HINTS_DISABLED_KEY, false);
204
236
  streakRef.current = 0;
205
237
  writeStreak(0);
206
238
  }, []);
207
- const hideBar = useCallback(() => {
239
+ const hideBar = useCallback2(() => {
208
240
  setBarHidden(true);
209
241
  writeFlag(BAR_HIDDEN_KEY, true);
210
242
  }, []);
211
- const showBar = useCallback(() => {
243
+ const showBar = useCallback2(() => {
212
244
  setBarHidden(false);
213
245
  writeFlag(BAR_HIDDEN_KEY, false);
214
246
  }, []);
215
- const offerHint = useCallback((next) => {
247
+ const offerHint = useCallback2((next) => {
216
248
  setHint((current) => current ? current : next);
217
249
  }, []);
218
- useEffect2(() => {
250
+ useEffect3(() => {
219
251
  if (hint) setLastHint(hint);
220
252
  }, [hint]);
221
- const dismissHint = useCallback(() => {
253
+ const dismissHint = useCallback2(() => {
222
254
  const limit = hint?.autoDisableAfterDismissals ?? DEFAULT_DISMISS_STREAK_LIMIT;
223
255
  setHint(null);
224
256
  streakRef.current += 1;
225
257
  writeStreak(streakRef.current);
226
258
  if (streakRef.current >= limit) disableHints();
227
259
  }, [hint, disableHints]);
228
- const openHint = useCallback(() => {
260
+ const openHint = useCallback2(() => {
229
261
  setHint(null);
230
262
  streakRef.current = 0;
231
263
  writeStreak(0);
232
264
  }, []);
233
- const autoHideHint = useCallback(() => {
265
+ const autoHideHint = useCallback2(() => {
234
266
  setHint(null);
235
267
  }, []);
236
268
  return /* @__PURE__ */ jsx2(
@@ -267,10 +299,10 @@ function useXbNovaSettings() {
267
299
  }
268
300
 
269
301
  // src/nudge-loop.tsx
270
- 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";
271
303
 
272
304
  // src/track.tsx
273
- 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";
274
306
  import { jsx as jsx3 } from "react/jsx-runtime";
275
307
  var VISIBILITY_THRESHOLD = 0;
276
308
  function finalizeIfVisible(registry, region) {
@@ -287,7 +319,7 @@ function useXbNovaTrack(region, data, label) {
287
319
  dataRef.current = data;
288
320
  const labelRef = useRef4(label);
289
321
  labelRef.current = label;
290
- useEffect3(() => {
322
+ useEffect4(() => {
291
323
  const entry = trackRegistry.get(region);
292
324
  if (entry) {
293
325
  entry.data = data;
@@ -296,7 +328,7 @@ function useXbNovaTrack(region, data, label) {
296
328
  trackRegistry.set(region, { region, data, label, enteredAt: null, totalMs: 0, node: null });
297
329
  }
298
330
  }, [region, data, label, trackRegistry]);
299
- useEffect3(() => {
331
+ useEffect4(() => {
300
332
  return () => {
301
333
  observerRef.current?.disconnect();
302
334
  finalizeIfVisible(trackRegistry, region);
@@ -304,7 +336,7 @@ function useXbNovaTrack(region, data, label) {
304
336
  if (entry) entry.node = null;
305
337
  };
306
338
  }, [region, trackRegistry]);
307
- return useCallback2(
339
+ return useCallback3(
308
340
  (node) => {
309
341
  observerRef.current?.disconnect();
310
342
  observerRef.current = null;
@@ -378,7 +410,7 @@ function getFocusedTrackEntry(trackRegistry) {
378
410
  }
379
411
 
380
412
  // src/nudge-loop.tsx
381
- var EVAL_INTERVAL_MS = 1e3;
413
+ var EVAL_INTERVAL_MS = 250;
382
414
  var XBNOVA_EVENT_NAME = "xbnova:event";
383
415
  var SESSION_COUNT_KEY_PREFIX = "xbnova_hint_session_count:";
384
416
  var SCROLL_SETTLE_MS = 500;
@@ -571,7 +603,7 @@ function clearDebugOverlay(refs) {
571
603
  refs.bandLinesRef.current.clear();
572
604
  }
573
605
  function XbNovaNudgeLoop({ appId }) {
574
- const { auth, trackRegistry, factsRegistry, visitorId } = useXbNovaInternal();
606
+ const { auth, trackRegistry, factsRegistry, sendEvent } = useXbNovaInternal();
575
607
  const { apiKey, apiBaseUrl, screen } = auth;
576
608
  const { hint, offerHint, autoHideHint, hintsEnabled } = useNudgeState();
577
609
  const graphRef = useRef5(null);
@@ -586,7 +618,7 @@ function XbNovaNudgeLoop({ appId }) {
586
618
  hintRef.current = hint;
587
619
  const hintsEnabledRef = useRef5(hintsEnabled);
588
620
  hintsEnabledRef.current = hintsEnabled;
589
- useEffect4(() => {
621
+ useEffect5(() => {
590
622
  let cancelled = false;
591
623
  fetch(`${apiBaseUrl}/v1/apps/${appId}/nudge-graph`, {
592
624
  headers: { Authorization: `Bearer ${apiKey}` }
@@ -598,14 +630,16 @@ function XbNovaNudgeLoop({ appId }) {
598
630
  cancelled = true;
599
631
  };
600
632
  }, [apiBaseUrl, apiKey, appId]);
601
- const evaluate = useCallback3(
633
+ const evaluate = useCallback4(
602
634
  (firedEventName) => {
603
635
  const graph = graphRef.current;
604
636
  const debugEnabled = isDebugEnabled();
605
637
  const debugRefs = { panelRef: debugPanelRef, boxesRef: debugBoxesRef, bandLinesRef: debugBandLinesRef };
606
638
  if (!debugEnabled && debugPanelRef.current) clearDebugOverlay(debugRefs);
639
+ const tickDebug = debugEnabled ? (msg) => console.debug(msg) : () => {
640
+ };
607
641
  if (!graph) {
608
- 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");
609
643
  return;
610
644
  }
611
645
  const canOffer = !hintRef.current && hintsEnabledRef.current;
@@ -656,18 +690,7 @@ function XbNovaNudgeLoop({ appId }) {
656
690
  autoHideMs: messageNode.auto_hide_ms ?? 2e4,
657
691
  quickReplies: messageNode.quick_replies ?? []
658
692
  });
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
- });
693
+ sendEvent("nudge_impression", { nudge_node_id: messageNode.id, bot_id: botId });
671
694
  };
672
695
  const tryFireFrom = (node) => {
673
696
  const nextEdge = (edgesFrom.get(node.id) ?? [])[0];
@@ -695,7 +718,7 @@ function XbNovaNudgeLoop({ appId }) {
695
718
  const maxPerSession = node.max_per_session === 0 ? Infinity : node.max_per_session ?? 3;
696
719
  const sessionCount = peekSessionCount(node.id);
697
720
  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`);
721
+ tickDebug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id}(${node.trigger_type}) \uAC74\uB108\uB700: \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC`);
699
722
  if (debugEnabled && node.trigger_type === "dwell") {
700
723
  debugLines.push(`- [${node.id}] \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC \u2014 \uB354 \uC548 \uC7BC`);
701
724
  }
@@ -730,7 +753,7 @@ function XbNovaNudgeLoop({ appId }) {
730
753
  overlapPx = status.overlapPx;
731
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}`;
732
755
  }
733
- console.debug(
756
+ tickDebug(
734
757
  `[XbNova] band dwell \uD2B8\uB9AC\uAC70=${node.id} region="${region}" band%=${fromPercent}~${toPercent} | ${statusDetail}`
735
758
  );
736
759
  dwellRecords.push({ node, state, requiredMs, region, found, screenMatches, inBand, overlapPx });
@@ -739,7 +762,7 @@ function XbNovaNudgeLoop({ appId }) {
739
762
  const fires = triggerFires(node, { screen, firedEventName });
740
763
  if (!fires) continue;
741
764
  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`);
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`);
743
766
  continue;
744
767
  }
745
768
  if (tryFireFrom(node)) {
@@ -839,9 +862,9 @@ function XbNovaNudgeLoop({ appId }) {
839
862
  ensureDebugPanel(debugPanelRef, debugCollapsedRef).body.textContent = debugLines.join("\n");
840
863
  }
841
864
  },
842
- [apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, visitorId]
865
+ [apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, sendEvent]
843
866
  );
844
- useEffect4(() => {
867
+ useEffect5(() => {
845
868
  const interval = window.setInterval(() => evaluate(null), EVAL_INTERVAL_MS);
846
869
  const onEvent = (e) => evaluate(e.detail.eventName);
847
870
  window.addEventListener(XBNOVA_EVENT_NAME, onEvent);
@@ -870,24 +893,36 @@ function XbNovaProvider({
870
893
  apiBaseUrl = DEFAULT_API_BASE_URL,
871
894
  clientActions,
872
895
  theme,
896
+ userKey,
897
+ threadKey,
898
+ sessionKey,
873
899
  children
874
900
  }) {
875
- useEffect5(() => {
901
+ useEffect6(() => {
876
902
  if (apiKey.startsWith("sk_")) {
877
903
  console.error(
878
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."
879
905
  );
880
906
  }
881
907
  }, [apiKey]);
882
- useEffect5(() => {
908
+ useEffect6(() => {
883
909
  markSessionStart();
884
910
  }, []);
885
- useEffect5(() => {
911
+ useEffect6(() => {
886
912
  if (screen) bumpVisitCount(screen);
887
913
  }, [screen]);
888
914
  const auth = useMemo2(
889
- () => ({ apiKey, botId, appId, apiBaseUrl, screen }),
890
- [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]
891
926
  );
892
927
  const resolvedTheme = useMemo2(
893
928
  () => ({ ...DEFAULT_THEME, ...theme }),
@@ -916,13 +951,13 @@ function XbNovaProvider({
916
951
  }
917
952
 
918
953
  // src/chat.tsx
919
- 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";
920
955
  import ReactMarkdown from "react-markdown";
921
956
  import remarkBreaks from "remark-breaks";
922
957
  import remarkGfm from "remark-gfm";
923
958
 
924
959
  // src/shadow-root.tsx
925
- 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";
926
961
  import { createPortal } from "react-dom";
927
962
  import { jsx as jsx5 } from "react/jsx-runtime";
928
963
  var cssPromise = null;
@@ -938,7 +973,7 @@ function loadCss() {
938
973
  }
939
974
  function useResolvedScheme(scheme) {
940
975
  const [systemDark, setSystemDark] = useState2(false);
941
- useEffect6(() => {
976
+ useEffect7(() => {
942
977
  if (scheme !== "system") return;
943
978
  const mq = window.matchMedia("(prefers-color-scheme: dark)");
944
979
  setSystemDark(mq.matches);
@@ -958,10 +993,10 @@ function XbNovaShadowRoot({
958
993
  const hostRef = useRef6(null);
959
994
  const [mountNode, setMountNode] = useState2(null);
960
995
  const [portalTarget, setPortalTarget] = useState2(null);
961
- useEffect6(() => {
996
+ useEffect7(() => {
962
997
  setPortalTarget(document.body);
963
998
  }, []);
964
- useEffect6(() => {
999
+ useEffect7(() => {
965
1000
  const host = hostRef.current;
966
1001
  if (!host || host.shadowRoot) return;
967
1002
  const shadow = host.attachShadow({ mode: "open" });
@@ -976,7 +1011,7 @@ function XbNovaShadowRoot({
976
1011
  shadow.appendChild(mount);
977
1012
  setMountNode(mount);
978
1013
  }, [portalTarget]);
979
- useEffect6(() => {
1014
+ useEffect7(() => {
980
1015
  if (!mountNode) return;
981
1016
  mountNode.dataset.xbnovaScheme = scheme;
982
1017
  mountNode.style.colorScheme = scheme;
@@ -989,15 +1024,44 @@ function XbNovaShadowRoot({
989
1024
  }
990
1025
 
991
1026
  // src/use-chat.ts
992
- 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";
993
1028
 
994
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
+ }
995
1058
  async function consumeSse(response, handlers) {
996
1059
  if (!response.ok || !response.body) {
997
1060
  handlers.onError?.(`\uC694\uCCAD \uC2E4\uD328 (${response.status})`);
998
1061
  return null;
999
1062
  }
1000
1063
  const conversationId = response.headers.get("x-conversation-id");
1064
+ if (conversationId) handlers.onConversationId?.(conversationId);
1001
1065
  const reader = response.body.getReader();
1002
1066
  const decoder = new TextDecoder();
1003
1067
  let buffer = "";
@@ -1102,12 +1166,19 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
1102
1166
  // src/use-chat.ts
1103
1167
  var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
1104
1168
  function useXbNovaChat(options) {
1105
- const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId } = useXbNovaInternal();
1169
+ const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId, sessionId, sendEvent } = useXbNovaInternal();
1106
1170
  const botId = options?.botId ?? auth.botId;
1107
1171
  const nudgeNodeId = options?.nudgeNodeId;
1108
- const { apiKey, apiBaseUrl } = auth;
1172
+ const { apiKey, apiBaseUrl, threadKey } = auth;
1173
+ const userKey = visitorId;
1109
1174
  const [onboarding, setOnboarding] = useState3(null);
1110
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;
1111
1182
  const [busy, setBusy] = useState3(false);
1112
1183
  const [error, setError] = useState3(null);
1113
1184
  const [clientResults, setClientResults] = useState3({});
@@ -1117,19 +1188,60 @@ function useXbNovaChat(options) {
1117
1188
  const dispatchedClientActionsRef = useRef7(/* @__PURE__ */ new Set());
1118
1189
  const nudgeNodeIdRef = useRef7(nudgeNodeId);
1119
1190
  nudgeNodeIdRef.current = nudgeNodeId;
1120
- useEffect7(() => {
1191
+ useEffect8(() => {
1192
+ let cancelled = false;
1121
1193
  setOnboarding(null);
1122
1194
  setMessages([]);
1195
+ setResumed(false);
1196
+ setSentOpening(null);
1123
1197
  setError(null);
1124
1198
  setClientResults({});
1125
1199
  setFeedbackState({});
1126
1200
  dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
1201
+ conversationIdRef.current = null;
1202
+ pendingOpeningRef.current = null;
1127
1203
  fetch(`${apiBaseUrl}/v1/conversations`, {
1128
1204
  method: "POST",
1129
1205
  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]);
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]);
1133
1245
  const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
1134
1246
  const next = [...prev];
1135
1247
  next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
@@ -1142,7 +1254,7 @@ function useXbNovaChat(options) {
1142
1254
  return next;
1143
1255
  });
1144
1256
  async function setFeedback(messageId, rating, reason) {
1145
- const conversationId = onboarding?.conversation_id;
1257
+ const conversationId = conversationIdRef.current;
1146
1258
  if (!conversationId || !messageId) return;
1147
1259
  const prevRating = feedbackRef.current[messageId] ?? null;
1148
1260
  const apply = (r) => setFeedbackState((prev) => {
@@ -1168,6 +1280,14 @@ function useXbNovaChat(options) {
1168
1280
  apply(prevRating);
1169
1281
  }
1170
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
+ }
1171
1291
  async function runClientAction(spec, part, existingParts) {
1172
1292
  if (spec.dismissSheet) options?.onDismissSheet?.();
1173
1293
  let result;
@@ -1191,7 +1311,7 @@ function useXbNovaChat(options) {
1191
1311
  result = e instanceof Error ? `\uC624\uB958: ${e.message}` : "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
1192
1312
  }
1193
1313
  if (!spec.awaitResult) return;
1194
- const conversationId = onboarding?.conversation_id;
1314
+ const conversationId = conversationIdRef.current;
1195
1315
  if (!conversationId) return;
1196
1316
  const structured = result && typeof result === "object" ? result : null;
1197
1317
  if (structured?.view) {
@@ -1225,7 +1345,7 @@ function useXbNovaChat(options) {
1225
1345
  setBusy(false);
1226
1346
  }
1227
1347
  }
1228
- useEffect7(() => {
1348
+ useEffect8(() => {
1229
1349
  if (clientActionHandlers.size === 0) return;
1230
1350
  const last = messages[messages.length - 1];
1231
1351
  if (!last || last.role !== "assistant") return;
@@ -1235,6 +1355,7 @@ function useXbNovaChat(options) {
1235
1355
  if (!spec) continue;
1236
1356
  if (dispatchedClientActionsRef.current.has(part.toolCallId)) continue;
1237
1357
  dispatchedClientActionsRef.current.add(part.toolCallId);
1358
+ if (spec.render) postClientActionEvent("cta_impression", part.toolName, null);
1238
1359
  void runClientAction(spec, part, last.parts);
1239
1360
  }
1240
1361
  }, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
@@ -1257,6 +1378,14 @@ function useXbNovaChat(options) {
1257
1378
  const notes = collectContextEntries(contextRegistry);
1258
1379
  const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
1259
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;
1260
1389
  try {
1261
1390
  await streamChat(
1262
1391
  apiBaseUrl,
@@ -1264,12 +1393,22 @@ function useXbNovaChat(options) {
1264
1393
  {
1265
1394
  bot_id: botId,
1266
1395
  message: trimmed,
1267
- conversation_id: onboarding?.conversation_id ?? null,
1396
+ conversation_id: conversationIdRef.current,
1268
1397
  context,
1269
1398
  nudge_node_id: nudgeNodeId ?? null,
1270
- opening_message: messages.length === 0 ? (options?.openingMessage ?? onboarding?.greeting) || null : null
1399
+ opening_message: openingMessage2,
1400
+ visitor_id: userKey || null,
1401
+ thread_key: threadKey ?? null,
1402
+ session_id: sessionId
1271
1403
  },
1272
- { 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
+ }
1273
1412
  );
1274
1413
  } catch (e) {
1275
1414
  setError(e instanceof Error ? e.message : String(e));
@@ -1277,7 +1416,19 @@ function useXbNovaChat(options) {
1277
1416
  setBusy(false);
1278
1417
  }
1279
1418
  }
1280
- 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
+ };
1281
1432
  }
1282
1433
 
1283
1434
  // src/views.tsx
@@ -1410,6 +1561,13 @@ function hasVisiblePart(parts, clientActionHandlers, clientResults) {
1410
1561
  }
1411
1562
  var DISCLOSURE_SEEN_KEY = "xbnova_disclosure_seen";
1412
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
+ }
1413
1571
  function readDisclosureSeen() {
1414
1572
  if (typeof window === "undefined") return true;
1415
1573
  try {
@@ -1428,7 +1586,7 @@ function markDisclosureSeen() {
1428
1586
  var DESKTOP_MEDIA_QUERY = "(min-width: 640px)";
1429
1587
  function useIsDesktopViewport() {
1430
1588
  const [isDesktop, setIsDesktop] = useState4(false);
1431
- useEffect8(() => {
1589
+ useEffect9(() => {
1432
1590
  if (typeof window === "undefined") return;
1433
1591
  const mql = window.matchMedia(DESKTOP_MEDIA_QUERY);
1434
1592
  setIsDesktop(mql.matches);
@@ -1452,7 +1610,7 @@ function backdropBaseStyle(isDesktop) {
1452
1610
  }
1453
1611
  function useTypewriter(text, key, speedMs = 28) {
1454
1612
  const [shown, setShown] = useState4("");
1455
- useEffect8(() => {
1613
+ useEffect9(() => {
1456
1614
  setShown("");
1457
1615
  if (!text) return;
1458
1616
  let i = 0;
@@ -1539,9 +1697,10 @@ function XbNovaChat({
1539
1697
  avatarSize = 28,
1540
1698
  onVerdictAction,
1541
1699
  feedback: feedbackEnabled = true,
1542
- onFeedback
1700
+ onFeedback,
1701
+ onOpen
1543
1702
  }) {
1544
- const { auth, trackRegistry, factsRegistry, visitorId, clientActionHandlers, theme } = useXbNovaInternal();
1703
+ const { auth, trackRegistry, factsRegistry, clientActionHandlers, theme, sendEvent } = useXbNovaInternal();
1545
1704
  const themeVars = {
1546
1705
  "--xbnova-accent-1": theme.accentFrom,
1547
1706
  "--xbnova-accent-2": theme.accentTo
@@ -1562,7 +1721,7 @@ function XbNovaChat({
1562
1721
  const [input, setInput] = useState4("");
1563
1722
  const bottomRef = useRef8(null);
1564
1723
  const typedHintText = useTypewriter(hint?.text ?? "", hint?.triggerNodeId ?? "");
1565
- const { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback } = useXbNovaChat({
1724
+ const { onboarding, opening, messages, busy, error, send, clientResults, feedback, setFeedback, trackClientActionClick } = useXbNovaChat({
1566
1725
  onFeedback,
1567
1726
  botId: activeBotId ?? botId,
1568
1727
  nudgeNodeId: activeNudgeNodeId,
@@ -1571,7 +1730,7 @@ function XbNovaChat({
1571
1730
  });
1572
1731
  const effectiveBotId = activeBotId ?? botId;
1573
1732
  const [cachedBranding, setCachedBranding] = useState4(null);
1574
- useEffect8(() => {
1733
+ useEffect9(() => {
1575
1734
  try {
1576
1735
  const raw = window.localStorage.getItem(`xbnova_branding:${effectiveBotId}`);
1577
1736
  setCachedBranding(raw ? JSON.parse(raw) : null);
@@ -1579,22 +1738,29 @@ function XbNovaChat({
1579
1738
  setCachedBranding(null);
1580
1739
  }
1581
1740
  }, [effectiveBotId]);
1582
- useEffect8(() => {
1741
+ useEffect9(() => {
1583
1742
  if (!onboarding?.branding) return;
1584
1743
  try {
1585
1744
  window.localStorage.setItem(
1586
1745
  `xbnova_branding:${effectiveBotId}`,
1587
- 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
+ })
1588
1752
  );
1589
1753
  } catch {
1590
1754
  }
1591
1755
  }, [onboarding?.branding, effectiveBotId]);
1592
1756
  const branding = onboarding?.branding ?? cachedBranding ?? void 0;
1757
+ const badgeText = branding?.badge_text?.trim() ?? "";
1758
+ const disclaimer = branding?.disclaimer?.trim() ?? "";
1593
1759
  const brandingReady = Boolean(onboarding) || cachedBranding !== null || Boolean(error);
1594
- useEffect8(() => {
1760
+ useEffect9(() => {
1595
1761
  bottomRef.current?.scrollIntoView({ behavior: "smooth" });
1596
1762
  }, [messages]);
1597
- useEffect8(() => {
1763
+ useEffect9(() => {
1598
1764
  if (!open) {
1599
1765
  setEntered(false);
1600
1766
  return;
@@ -1602,7 +1768,7 @@ function XbNovaChat({
1602
1768
  const raf = requestAnimationFrame(() => setEntered(true));
1603
1769
  return () => cancelAnimationFrame(raf);
1604
1770
  }, [open]);
1605
- useEffect8(() => {
1771
+ useEffect9(() => {
1606
1772
  if (!hint || hint.autoHideMs <= 0) return;
1607
1773
  console.debug(`[XbNova] \uD78C\uD2B8 "${hint.text}" \uD45C\uC2DC \u2014 ${hint.autoHideMs}ms \uD6C4 \uC790\uB3D9\uC73C\uB85C \uC0AC\uB77C\uC9D0`);
1608
1774
  const timer = window.setTimeout(() => {
@@ -1619,18 +1785,7 @@ function XbNovaChat({
1619
1785
  setOpeningLine(consumed.text);
1620
1786
  setActiveNudgeNodeId(consumed.messageNodeId);
1621
1787
  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
- });
1788
+ sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1634
1789
  } else {
1635
1790
  setNudgeQuickReplies([]);
1636
1791
  setActiveNudgeNodeId(void 0);
@@ -1644,6 +1799,11 @@ function XbNovaChat({
1644
1799
  markDisclosureSeen();
1645
1800
  }
1646
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
+ }
1647
1807
  }
1648
1808
  if (!open) {
1649
1809
  if (barHidden) return null;
@@ -1739,7 +1899,6 @@ function XbNovaChat({
1739
1899
  const summary = resolveFactValue(factsRegistry.get(SUMMARY_FACT_KEY));
1740
1900
  const focusedLabel = getFocusedTrackEntry(trackRegistry)?.label;
1741
1901
  const contextLine = summary ?? (focusedLabel ? `${focusedLabel} \uBCF4\uB294 \uC911` : void 0);
1742
- const firstBubble = openingLine ?? onboarding?.greeting;
1743
1902
  return /* @__PURE__ */ jsxs3(Fragment, { children: [
1744
1903
  !isDesktop ? /* @__PURE__ */ jsx7(
1745
1904
  XbNovaShadowRoot,
@@ -1776,7 +1935,10 @@ function XbNovaChat({
1776
1935
  }
1777
1936
  ),
1778
1937
  /* @__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" }),
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
+ ] }),
1780
1942
  contextLine ? /* @__PURE__ */ jsx7("div", { className: "truncate text-[11px] text-neutral-400", children: contextLine }) : null
1781
1943
  ] }),
1782
1944
  /* @__PURE__ */ jsx7(
@@ -1792,7 +1954,7 @@ function XbNovaChat({
1792
1954
  ] }),
1793
1955
  /* @__PURE__ */ jsxs3("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [
1794
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,
1795
- 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,
1796
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(
1797
1959
  AnswerActionsFrame,
1798
1960
  {
@@ -1820,7 +1982,17 @@ function XbNovaChat({
1820
1982
  );
1821
1983
  }
1822
1984
  const spec = clientActionHandlers.get(part.toolName);
1823
- 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;
1824
1996
  }
1825
1997
  return /* @__PURE__ */ jsx7(ToolView, { part, viewRenderers, onVerdictAction }, j);
1826
1998
  }),
@@ -1891,7 +2063,8 @@ function XbNovaChat({
1891
2063
  )
1892
2064
  ]
1893
2065
  }
1894
- )
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
1895
2068
  ] })
1896
2069
  }
1897
2070
  )
@@ -1993,8 +2166,8 @@ function AnswerActions({
1993
2166
  if (timerRef.current) clearTimeout(timerRef.current);
1994
2167
  timerRef.current = null;
1995
2168
  };
1996
- useEffect8(() => clearTimer, []);
1997
- useEffect8(() => {
2169
+ useEffect9(() => clearTimer, []);
2170
+ useEffect9(() => {
1998
2171
  if (!menuOpen) {
1999
2172
  setEntered(false);
2000
2173
  return;
@@ -2141,13 +2314,13 @@ function AnswerActionsFrame({
2141
2314
  }
2142
2315
 
2143
2316
  // src/facts.ts
2144
- import { useEffect as useEffect9, useId, useRef as useRef9 } from "react";
2317
+ import { useEffect as useEffect10, useId, useRef as useRef9 } from "react";
2145
2318
  function useXbNovaFact(key, value) {
2146
2319
  const { factsRegistry } = useXbNovaInternal();
2147
- useEffect9(() => {
2320
+ useEffect10(() => {
2148
2321
  factsRegistry.set(key, value);
2149
2322
  });
2150
- useEffect9(() => {
2323
+ useEffect10(() => {
2151
2324
  return () => {
2152
2325
  factsRegistry.delete(key);
2153
2326
  };
@@ -2156,13 +2329,13 @@ function useXbNovaFact(key, value) {
2156
2329
  function useXbNovaFacts(values) {
2157
2330
  const { factsRegistry } = useXbNovaInternal();
2158
2331
  const keysRef = useRef9([]);
2159
- useEffect9(() => {
2332
+ useEffect10(() => {
2160
2333
  for (const [key, value] of Object.entries(values)) {
2161
2334
  factsRegistry.set(key, value);
2162
2335
  }
2163
2336
  keysRef.current = Object.keys(values);
2164
2337
  });
2165
- useEffect9(() => {
2338
+ useEffect10(() => {
2166
2339
  return () => {
2167
2340
  for (const key of keysRef.current) {
2168
2341
  factsRegistry.delete(key);
@@ -2173,10 +2346,10 @@ function useXbNovaFacts(values) {
2173
2346
  function useXbNovaContext(entries) {
2174
2347
  const { contextRegistry } = useXbNovaInternal();
2175
2348
  const id = useId();
2176
- useEffect9(() => {
2349
+ useEffect10(() => {
2177
2350
  contextRegistry.set(id, entries);
2178
2351
  });
2179
- useEffect9(() => {
2352
+ useEffect10(() => {
2180
2353
  return () => {
2181
2354
  contextRegistry.delete(id);
2182
2355
  };