@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.cjs CHANGED
@@ -38,6 +38,7 @@ __export(index_exports, {
38
38
  XbNovaTrack: () => XbNovaTrack,
39
39
  trackXbNovaEvent: () => trackXbNovaEvent,
40
40
  useXbNovaChat: () => useXbNovaChat,
41
+ useXbNovaContext: () => useXbNovaContext,
41
42
  useXbNovaFact: () => useXbNovaFact,
42
43
  useXbNovaFacts: () => useXbNovaFacts,
43
44
  useXbNovaSettings: () => useXbNovaSettings,
@@ -71,6 +72,7 @@ function useXbNovaInternal() {
71
72
  return ctx;
72
73
  }
73
74
  var VISITOR_STORAGE_KEY = "xbnova_visitor_id";
75
+ var WIDGET_VIEW_WAIT_MS = 1e4;
74
76
  function resolveVisitorId() {
75
77
  if (typeof window === "undefined") return "";
76
78
  try {
@@ -86,6 +88,17 @@ function resolveVisitorId() {
86
88
  function resolveFactValue(value) {
87
89
  return typeof value === "function" ? value() : value;
88
90
  }
91
+ function collectContextEntries(registry) {
92
+ const out = [];
93
+ for (const source of registry.values()) {
94
+ const entries = typeof source === "function" ? source() : source;
95
+ if (!Array.isArray(entries)) continue;
96
+ for (const e of entries) {
97
+ if (e && typeof e.title === "string" && typeof e.description === "string") out.push(e);
98
+ }
99
+ }
100
+ return out;
101
+ }
89
102
  function InternalProvider({
90
103
  auth,
91
104
  clientActionHandlers,
@@ -94,21 +107,54 @@ function InternalProvider({
94
107
  }) {
95
108
  const trackRegistry = (0, import_react.useRef)(/* @__PURE__ */ new Map()).current;
96
109
  const factsRegistry = (0, import_react.useRef)(/* @__PURE__ */ new Map()).current;
110
+ const contextRegistry = (0, import_react.useRef)(/* @__PURE__ */ new Map()).current;
97
111
  const emptyHandlers = (0, import_react.useRef)(/* @__PURE__ */ new Map()).current;
98
112
  const visitorIdRef = (0, import_react.useRef)(null);
99
113
  if (visitorIdRef.current === null) {
100
114
  visitorIdRef.current = resolveVisitorId();
101
115
  }
116
+ const visitorId = auth.userKey || visitorIdRef.current;
117
+ const sessionId = auth.sessionKey || null;
118
+ const latestRef = (0, import_react.useRef)({ auth, visitorId, sessionId });
119
+ latestRef.current = { auth, visitorId, sessionId };
120
+ const sendEvent = (0, import_react.useCallback)((kind, fields = {}) => {
121
+ const { auth: a, visitorId: v, sessionId: sid } = latestRef.current;
122
+ fetch(`${a.apiBaseUrl}/v1/nudge-events`, {
123
+ method: "POST",
124
+ headers: { "Content-Type": "application/json" },
125
+ body: JSON.stringify({ app_id: a.appId, kind, visitor_id: v || null, session_id: sid, ...fields })
126
+ }).catch(() => {
127
+ });
128
+ }, []);
129
+ const widgetViewSentRef = (0, import_react.useRef)(false);
130
+ (0, import_react.useEffect)(() => {
131
+ if (widgetViewSentRef.current) return;
132
+ const send = () => {
133
+ if (widgetViewSentRef.current) return;
134
+ widgetViewSentRef.current = true;
135
+ const screen = latestRef.current.auth.screen;
136
+ sendEvent("widget_view", screen ? { payload: { screen } } : {});
137
+ };
138
+ if (sessionId) {
139
+ send();
140
+ return;
141
+ }
142
+ const timer = setTimeout(send, WIDGET_VIEW_WAIT_MS);
143
+ return () => clearTimeout(timer);
144
+ }, [sessionId, sendEvent]);
102
145
  const value = (0, import_react.useMemo)(
103
146
  () => ({
104
147
  auth,
105
148
  trackRegistry,
106
149
  factsRegistry,
150
+ contextRegistry,
107
151
  clientActionHandlers: clientActionHandlers ?? emptyHandlers,
108
- visitorId: visitorIdRef.current,
152
+ visitorId,
153
+ sessionId,
154
+ sendEvent,
109
155
  theme
110
156
  }),
111
- [auth, trackRegistry, factsRegistry, clientActionHandlers, emptyHandlers, theme]
157
+ [auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, visitorId, sessionId, sendEvent, theme]
112
158
  );
113
159
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InternalContext.Provider, { value, children });
114
160
  }
@@ -411,7 +457,7 @@ function getFocusedTrackEntry(trackRegistry) {
411
457
  }
412
458
 
413
459
  // src/nudge-loop.tsx
414
- var EVAL_INTERVAL_MS = 1e3;
460
+ var EVAL_INTERVAL_MS = 250;
415
461
  var XBNOVA_EVENT_NAME = "xbnova:event";
416
462
  var SESSION_COUNT_KEY_PREFIX = "xbnova_hint_session_count:";
417
463
  var SCROLL_SETTLE_MS = 500;
@@ -604,7 +650,7 @@ function clearDebugOverlay(refs) {
604
650
  refs.bandLinesRef.current.clear();
605
651
  }
606
652
  function XbNovaNudgeLoop({ appId }) {
607
- const { auth, trackRegistry, factsRegistry, visitorId } = useXbNovaInternal();
653
+ const { auth, trackRegistry, factsRegistry, sendEvent } = useXbNovaInternal();
608
654
  const { apiKey, apiBaseUrl, screen } = auth;
609
655
  const { hint, offerHint, autoHideHint, hintsEnabled } = useNudgeState();
610
656
  const graphRef = (0, import_react5.useRef)(null);
@@ -637,8 +683,10 @@ function XbNovaNudgeLoop({ appId }) {
637
683
  const debugEnabled = isDebugEnabled();
638
684
  const debugRefs = { panelRef: debugPanelRef, boxesRef: debugBoxesRef, bandLinesRef: debugBandLinesRef };
639
685
  if (!debugEnabled && debugPanelRef.current) clearDebugOverlay(debugRefs);
686
+ const tickDebug = debugEnabled ? (msg) => console.debug(msg) : () => {
687
+ };
640
688
  if (!graph) {
641
- console.debug("[XbNova] nudge \uD3C9\uAC00 \uAC74\uB108\uB700: \uADF8\uB798\uD504\uB97C \uC544\uC9C1 \uBABB \uBD88\uB7EC\uC634");
689
+ tickDebug("[XbNova] nudge \uD3C9\uAC00 \uAC74\uB108\uB700: \uADF8\uB798\uD504\uB97C \uC544\uC9C1 \uBABB \uBD88\uB7EC\uC634");
642
690
  return;
643
691
  }
644
692
  const canOffer = !hintRef.current && hintsEnabledRef.current;
@@ -689,18 +737,7 @@ function XbNovaNudgeLoop({ appId }) {
689
737
  autoHideMs: messageNode.auto_hide_ms ?? 2e4,
690
738
  quickReplies: messageNode.quick_replies ?? []
691
739
  });
692
- fetch(`${apiBaseUrl}/v1/nudge-events`, {
693
- method: "POST",
694
- headers: { "Content-Type": "application/json" },
695
- body: JSON.stringify({
696
- app_id: appId,
697
- nudge_node_id: messageNode.id,
698
- kind: "nudge_impression",
699
- bot_id: botId,
700
- visitor_id: visitorId
701
- })
702
- }).catch(() => {
703
- });
740
+ sendEvent("nudge_impression", { nudge_node_id: messageNode.id, bot_id: botId });
704
741
  };
705
742
  const tryFireFrom = (node) => {
706
743
  const nextEdge = (edgesFrom.get(node.id) ?? [])[0];
@@ -728,7 +765,7 @@ function XbNovaNudgeLoop({ appId }) {
728
765
  const maxPerSession = node.max_per_session === 0 ? Infinity : node.max_per_session ?? 3;
729
766
  const sessionCount = peekSessionCount(node.id);
730
767
  if (sessionCount >= maxPerSession) {
731
- console.debug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id}(${node.trigger_type}) \uAC74\uB108\uB700: \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC`);
768
+ tickDebug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id}(${node.trigger_type}) \uAC74\uB108\uB700: \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC`);
732
769
  if (debugEnabled && node.trigger_type === "dwell") {
733
770
  debugLines.push(`- [${node.id}] \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC \u2014 \uB354 \uC548 \uC7BC`);
734
771
  }
@@ -763,7 +800,7 @@ function XbNovaNudgeLoop({ appId }) {
763
800
  overlapPx = status.overlapPx;
764
801
  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}`;
765
802
  }
766
- console.debug(
803
+ tickDebug(
767
804
  `[XbNova] band dwell \uD2B8\uB9AC\uAC70=${node.id} region="${region}" band%=${fromPercent}~${toPercent} | ${statusDetail}`
768
805
  );
769
806
  dwellRecords.push({ node, state, requiredMs, region, found, screenMatches, inBand, overlapPx });
@@ -772,7 +809,7 @@ function XbNovaNudgeLoop({ appId }) {
772
809
  const fires = triggerFires(node, { screen, firedEventName });
773
810
  if (!fires) continue;
774
811
  if (!canOffer) {
775
- 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`);
812
+ 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`);
776
813
  continue;
777
814
  }
778
815
  if (tryFireFrom(node)) {
@@ -872,7 +909,7 @@ function XbNovaNudgeLoop({ appId }) {
872
909
  ensureDebugPanel(debugPanelRef, debugCollapsedRef).body.textContent = debugLines.join("\n");
873
910
  }
874
911
  },
875
- [apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, visitorId]
912
+ [apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, sendEvent]
876
913
  );
877
914
  (0, import_react5.useEffect)(() => {
878
915
  const interval = window.setInterval(() => evaluate(null), EVAL_INTERVAL_MS);
@@ -903,6 +940,9 @@ function XbNovaProvider({
903
940
  apiBaseUrl = DEFAULT_API_BASE_URL,
904
941
  clientActions,
905
942
  theme,
943
+ userKey,
944
+ threadKey,
945
+ sessionKey,
906
946
  children
907
947
  }) {
908
948
  (0, import_react6.useEffect)(() => {
@@ -919,8 +959,17 @@ function XbNovaProvider({
919
959
  if (screen) bumpVisitCount(screen);
920
960
  }, [screen]);
921
961
  const auth = (0, import_react6.useMemo)(
922
- () => ({ apiKey, botId, appId, apiBaseUrl, screen }),
923
- [apiKey, botId, appId, apiBaseUrl, screen]
962
+ () => ({
963
+ apiKey,
964
+ botId,
965
+ appId,
966
+ apiBaseUrl,
967
+ screen,
968
+ userKey: userKey || void 0,
969
+ threadKey: threadKey || void 0,
970
+ sessionKey: sessionKey || void 0
971
+ }),
972
+ [apiKey, botId, appId, apiBaseUrl, screen, userKey, threadKey, sessionKey]
924
973
  );
925
974
  const resolvedTheme = (0, import_react6.useMemo)(
926
975
  () => ({ ...DEFAULT_THEME, ...theme }),
@@ -1026,12 +1075,41 @@ function XbNovaShadowRoot({
1026
1075
  var import_react8 = require("react");
1027
1076
 
1028
1077
  // src/stream.ts
1078
+ function fromStoredMessages(stored) {
1079
+ return stored.map((m) => ({
1080
+ id: m.role === "assistant" ? m.id : void 0,
1081
+ role: m.role === "assistant" ? "assistant" : "user",
1082
+ parts: m.parts.flatMap((p, i) => {
1083
+ const type = String(p.type ?? "");
1084
+ if (type === "text") {
1085
+ const text = typeof p.text === "string" ? p.text : "";
1086
+ return text ? [{ kind: "text", id: `${m.id}:${i}`, text }] : [];
1087
+ }
1088
+ if (type.startsWith("tool-")) {
1089
+ const meta = p.providerMetadata;
1090
+ return [
1091
+ {
1092
+ kind: "tool",
1093
+ toolCallId: String(p.toolCallId ?? `${m.id}:${i}`),
1094
+ toolName: type.slice("tool-".length),
1095
+ state: p.state === "output-error" ? "error" : "output",
1096
+ input: p.input,
1097
+ output: p.output,
1098
+ view: meta?.["kr.dsauto.chat"]?.view ?? null
1099
+ }
1100
+ ];
1101
+ }
1102
+ return [];
1103
+ })
1104
+ }));
1105
+ }
1029
1106
  async function consumeSse(response, handlers) {
1030
1107
  if (!response.ok || !response.body) {
1031
1108
  handlers.onError?.(`\uC694\uCCAD \uC2E4\uD328 (${response.status})`);
1032
1109
  return null;
1033
1110
  }
1034
1111
  const conversationId = response.headers.get("x-conversation-id");
1112
+ if (conversationId) handlers.onConversationId?.(conversationId);
1035
1113
  const reader = response.body.getReader();
1036
1114
  const decoder = new TextDecoder();
1037
1115
  let buffer = "";
@@ -1136,12 +1214,19 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
1136
1214
  // src/use-chat.ts
1137
1215
  var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
1138
1216
  function useXbNovaChat(options) {
1139
- const { auth, trackRegistry, clientActionHandlers, visitorId } = useXbNovaInternal();
1217
+ const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId, sessionId, sendEvent } = useXbNovaInternal();
1140
1218
  const botId = options?.botId ?? auth.botId;
1141
1219
  const nudgeNodeId = options?.nudgeNodeId;
1142
- const { apiKey, apiBaseUrl } = auth;
1220
+ const { apiKey, apiBaseUrl, threadKey } = auth;
1221
+ const userKey = visitorId;
1143
1222
  const [onboarding, setOnboarding] = (0, import_react8.useState)(null);
1144
1223
  const [messages, setMessages] = (0, import_react8.useState)([]);
1224
+ const [resumed, setResumed] = (0, import_react8.useState)(false);
1225
+ const [sentOpening, setSentOpening] = (0, import_react8.useState)(null);
1226
+ const conversationIdRef = (0, import_react8.useRef)(null);
1227
+ const pendingOpeningRef = (0, import_react8.useRef)(null);
1228
+ const messagesRef = (0, import_react8.useRef)(messages);
1229
+ messagesRef.current = messages;
1145
1230
  const [busy, setBusy] = (0, import_react8.useState)(false);
1146
1231
  const [error, setError] = (0, import_react8.useState)(null);
1147
1232
  const [clientResults, setClientResults] = (0, import_react8.useState)({});
@@ -1152,18 +1237,59 @@ function useXbNovaChat(options) {
1152
1237
  const nudgeNodeIdRef = (0, import_react8.useRef)(nudgeNodeId);
1153
1238
  nudgeNodeIdRef.current = nudgeNodeId;
1154
1239
  (0, import_react8.useEffect)(() => {
1240
+ let cancelled = false;
1155
1241
  setOnboarding(null);
1156
1242
  setMessages([]);
1243
+ setResumed(false);
1244
+ setSentOpening(null);
1157
1245
  setError(null);
1158
1246
  setClientResults({});
1159
1247
  setFeedbackState({});
1160
1248
  dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
1249
+ conversationIdRef.current = null;
1250
+ pendingOpeningRef.current = null;
1161
1251
  fetch(`${apiBaseUrl}/v1/conversations`, {
1162
1252
  method: "POST",
1163
1253
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
1164
- body: JSON.stringify({ bot_id: botId, nudge_node_id: nudgeNodeIdRef.current ?? null })
1165
- }).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}`));
1166
- }, [apiBaseUrl, apiKey, botId]);
1254
+ body: JSON.stringify({
1255
+ bot_id: botId,
1256
+ nudge_node_id: nudgeNodeIdRef.current ?? null,
1257
+ visitor_id: userKey || null,
1258
+ thread_key: threadKey ?? null,
1259
+ session_id: sessionId,
1260
+ defer: true
1261
+ })
1262
+ }).then((r) => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))).then((data) => {
1263
+ if (cancelled) return;
1264
+ if (conversationIdRef.current || messagesRef.current.length > 0) {
1265
+ setOnboarding(data);
1266
+ return;
1267
+ }
1268
+ conversationIdRef.current = data.conversation_id;
1269
+ const restored = fromStoredMessages(data.messages ?? []);
1270
+ for (const m of restored) {
1271
+ for (const p of m.parts) if (p.kind === "tool") dispatchedClientActionsRef.current.add(p.toolCallId);
1272
+ }
1273
+ setMessages(restored);
1274
+ setResumed(restored.length > 0);
1275
+ setOnboarding(data);
1276
+ }).catch((e) => {
1277
+ if (!cancelled) setError(`\uBD07\uC744 \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`);
1278
+ });
1279
+ return () => {
1280
+ cancelled = true;
1281
+ };
1282
+ }, [apiBaseUrl, apiKey, botId, userKey, threadKey]);
1283
+ const openingMessage = options?.openingMessage;
1284
+ (0, import_react8.useEffect)(() => {
1285
+ if (!onboarding || !nudgeNodeId || !openingMessage) return;
1286
+ if (messagesRef.current.length === 0) return;
1287
+ setMessages((prev) => [
1288
+ ...prev,
1289
+ { role: "assistant", parts: [{ kind: "text", id: `nudge:${nudgeNodeId}:${prev.length}`, text: openingMessage }] }
1290
+ ]);
1291
+ pendingOpeningRef.current = openingMessage;
1292
+ }, [onboarding, nudgeNodeId, openingMessage]);
1167
1293
  const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
1168
1294
  const next = [...prev];
1169
1295
  next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
@@ -1176,7 +1302,7 @@ function useXbNovaChat(options) {
1176
1302
  return next;
1177
1303
  });
1178
1304
  async function setFeedback(messageId, rating, reason) {
1179
- const conversationId = onboarding?.conversation_id;
1305
+ const conversationId = conversationIdRef.current;
1180
1306
  if (!conversationId || !messageId) return;
1181
1307
  const prevRating = feedbackRef.current[messageId] ?? null;
1182
1308
  const apply = (r) => setFeedbackState((prev) => {
@@ -1202,6 +1328,14 @@ function useXbNovaChat(options) {
1202
1328
  apply(prevRating);
1203
1329
  }
1204
1330
  }
1331
+ function postClientActionEvent(kind, action, label) {
1332
+ const conversationId = conversationIdRef.current;
1333
+ if (!conversationId) return;
1334
+ sendEvent(kind, { conversation_id: conversationId, payload: label ? { action, label } : { action } });
1335
+ }
1336
+ function trackClientActionClick(action, label) {
1337
+ postClientActionEvent("cta_click", action, label);
1338
+ }
1205
1339
  async function runClientAction(spec, part, existingParts) {
1206
1340
  if (spec.dismissSheet) options?.onDismissSheet?.();
1207
1341
  let result;
@@ -1225,7 +1359,7 @@ function useXbNovaChat(options) {
1225
1359
  result = e instanceof Error ? `\uC624\uB958: ${e.message}` : "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
1226
1360
  }
1227
1361
  if (!spec.awaitResult) return;
1228
- const conversationId = onboarding?.conversation_id;
1362
+ const conversationId = conversationIdRef.current;
1229
1363
  if (!conversationId) return;
1230
1364
  const structured = result && typeof result === "object" ? result : null;
1231
1365
  if (structured?.view) {
@@ -1269,6 +1403,7 @@ function useXbNovaChat(options) {
1269
1403
  if (!spec) continue;
1270
1404
  if (dispatchedClientActionsRef.current.has(part.toolCallId)) continue;
1271
1405
  dispatchedClientActionsRef.current.add(part.toolCallId);
1406
+ if (spec.render) postClientActionEvent("cta_impression", part.toolName, null);
1272
1407
  void runClientAction(spec, part, last.parts);
1273
1408
  }
1274
1409
  }, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
@@ -1288,7 +1423,17 @@ function useXbNovaChat(options) {
1288
1423
  return next;
1289
1424
  });
1290
1425
  const focused = getFocusedTrackEntry(trackRegistry);
1291
- const context = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : void 0;
1426
+ const notes = collectContextEntries(contextRegistry);
1427
+ const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
1428
+ const context = focused || notes.length ? { ...screen, ...notes.length ? { notes } : {} } : void 0;
1429
+ let openingMessage2;
1430
+ if (messages.length === 0) {
1431
+ openingMessage2 = (options?.openingMessage ?? onboarding?.greeting) || null;
1432
+ setSentOpening(openingMessage2);
1433
+ } else {
1434
+ openingMessage2 = pendingOpeningRef.current;
1435
+ }
1436
+ pendingOpeningRef.current = null;
1292
1437
  try {
1293
1438
  await streamChat(
1294
1439
  apiBaseUrl,
@@ -1296,11 +1441,22 @@ function useXbNovaChat(options) {
1296
1441
  {
1297
1442
  bot_id: botId,
1298
1443
  message: trimmed,
1299
- conversation_id: onboarding?.conversation_id ?? null,
1444
+ conversation_id: conversationIdRef.current,
1300
1445
  context,
1301
- nudge_node_id: nudgeNodeId ?? null
1446
+ nudge_node_id: nudgeNodeId ?? null,
1447
+ opening_message: openingMessage2,
1448
+ visitor_id: userKey || null,
1449
+ thread_key: threadKey ?? null,
1450
+ session_id: sessionId
1302
1451
  },
1303
- { onPart: replaceLast, onMessageId: setLastAssistantId, onError: setError }
1452
+ {
1453
+ onPart: replaceLast,
1454
+ onMessageId: setLastAssistantId,
1455
+ onConversationId: (id) => {
1456
+ conversationIdRef.current = id;
1457
+ },
1458
+ onError: setError
1459
+ }
1304
1460
  );
1305
1461
  } catch (e) {
1306
1462
  setError(e instanceof Error ? e.message : String(e));
@@ -1308,7 +1464,19 @@ function useXbNovaChat(options) {
1308
1464
  setBusy(false);
1309
1465
  }
1310
1466
  }
1311
- return { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback };
1467
+ const opening = resumed ? null : sentOpening ?? options?.openingMessage ?? onboarding?.greeting ?? null;
1468
+ return {
1469
+ onboarding,
1470
+ opening,
1471
+ messages,
1472
+ busy,
1473
+ error,
1474
+ send,
1475
+ clientResults,
1476
+ feedback,
1477
+ setFeedback,
1478
+ trackClientActionClick
1479
+ };
1312
1480
  }
1313
1481
 
1314
1482
  // src/views.tsx
@@ -1441,6 +1609,13 @@ function hasVisiblePart(parts, clientActionHandlers, clientResults) {
1441
1609
  }
1442
1610
  var DISCLOSURE_SEEN_KEY = "xbnova_disclosure_seen";
1443
1611
  var SUMMARY_FACT_KEY = "xbnova_summary";
1612
+ function clickedControl(e) {
1613
+ const target = e.target instanceof Element ? e.target : null;
1614
+ const control = target?.closest("button, a, [role='button']");
1615
+ if (!control || !e.currentTarget.contains(control)) return null;
1616
+ const text = (control.textContent ?? "").replace(/\s+/g, " ").trim();
1617
+ return { label: text || control.getAttribute("aria-label") || null };
1618
+ }
1444
1619
  function readDisclosureSeen() {
1445
1620
  if (typeof window === "undefined") return true;
1446
1621
  try {
@@ -1570,9 +1745,10 @@ function XbNovaChat({
1570
1745
  avatarSize = 28,
1571
1746
  onVerdictAction,
1572
1747
  feedback: feedbackEnabled = true,
1573
- onFeedback
1748
+ onFeedback,
1749
+ onOpen
1574
1750
  }) {
1575
- const { auth, trackRegistry, factsRegistry, visitorId, clientActionHandlers, theme } = useXbNovaInternal();
1751
+ const { auth, trackRegistry, factsRegistry, clientActionHandlers, theme, sendEvent } = useXbNovaInternal();
1576
1752
  const themeVars = {
1577
1753
  "--xbnova-accent-1": theme.accentFrom,
1578
1754
  "--xbnova-accent-2": theme.accentTo
@@ -1593,10 +1769,11 @@ function XbNovaChat({
1593
1769
  const [input, setInput] = (0, import_react9.useState)("");
1594
1770
  const bottomRef = (0, import_react9.useRef)(null);
1595
1771
  const typedHintText = useTypewriter(hint?.text ?? "", hint?.triggerNodeId ?? "");
1596
- const { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback } = useXbNovaChat({
1772
+ const { onboarding, opening, messages, busy, error, send, clientResults, feedback, setFeedback, trackClientActionClick } = useXbNovaChat({
1597
1773
  onFeedback,
1598
1774
  botId: activeBotId ?? botId,
1599
1775
  nudgeNodeId: activeNudgeNodeId,
1776
+ openingMessage: openingLine ?? void 0,
1600
1777
  onDismissSheet: () => setOpen(false)
1601
1778
  });
1602
1779
  const effectiveBotId = activeBotId ?? botId;
@@ -1614,12 +1791,19 @@ function XbNovaChat({
1614
1791
  try {
1615
1792
  window.localStorage.setItem(
1616
1793
  `xbnova_branding:${effectiveBotId}`,
1617
- JSON.stringify({ logo_url: onboarding.branding.logo_url, avatar_text: onboarding.branding.avatar_text })
1794
+ JSON.stringify({
1795
+ logo_url: onboarding.branding.logo_url,
1796
+ avatar_text: onboarding.branding.avatar_text,
1797
+ badge_text: onboarding.branding.badge_text,
1798
+ disclaimer: onboarding.branding.disclaimer
1799
+ })
1618
1800
  );
1619
1801
  } catch {
1620
1802
  }
1621
1803
  }, [onboarding?.branding, effectiveBotId]);
1622
1804
  const branding = onboarding?.branding ?? cachedBranding ?? void 0;
1805
+ const badgeText = branding?.badge_text?.trim() ?? "";
1806
+ const disclaimer = branding?.disclaimer?.trim() ?? "";
1623
1807
  const brandingReady = Boolean(onboarding) || cachedBranding !== null || Boolean(error);
1624
1808
  (0, import_react9.useEffect)(() => {
1625
1809
  bottomRef.current?.scrollIntoView({ behavior: "smooth" });
@@ -1649,18 +1833,7 @@ function XbNovaChat({
1649
1833
  setOpeningLine(consumed.text);
1650
1834
  setActiveNudgeNodeId(consumed.messageNodeId);
1651
1835
  setNudgeQuickReplies(consumed.quickReplies);
1652
- fetch(`${auth.apiBaseUrl}/v1/nudge-events`, {
1653
- method: "POST",
1654
- headers: { "Content-Type": "application/json" },
1655
- body: JSON.stringify({
1656
- app_id: auth.appId,
1657
- nudge_node_id: consumed.messageNodeId,
1658
- kind: "nudge_open",
1659
- bot_id: consumed.botId,
1660
- visitor_id: visitorId
1661
- })
1662
- }).catch(() => {
1663
- });
1836
+ sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1664
1837
  } else {
1665
1838
  setNudgeQuickReplies([]);
1666
1839
  setActiveNudgeNodeId(void 0);
@@ -1674,6 +1847,11 @@ function XbNovaChat({
1674
1847
  markDisclosureSeen();
1675
1848
  }
1676
1849
  setOpen(true);
1850
+ try {
1851
+ onOpen?.({ fromNudge: Boolean(consumed), hintText: consumed?.text });
1852
+ } catch (err) {
1853
+ console.warn("[XbNova] onOpen \uCF5C\uBC31 \uC624\uB958", err);
1854
+ }
1677
1855
  }
1678
1856
  if (!open) {
1679
1857
  if (barHidden) return null;
@@ -1769,7 +1947,6 @@ function XbNovaChat({
1769
1947
  const summary = resolveFactValue(factsRegistry.get(SUMMARY_FACT_KEY));
1770
1948
  const focusedLabel = getFocusedTrackEntry(trackRegistry)?.label;
1771
1949
  const contextLine = summary ?? (focusedLabel ? `${focusedLabel} \uBCF4\uB294 \uC911` : void 0);
1772
- const firstBubble = openingLine ?? onboarding?.greeting;
1773
1950
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1774
1951
  !isDesktop ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1775
1952
  XbNovaShadowRoot,
@@ -1806,7 +1983,10 @@ function XbNovaChat({
1806
1983
  }
1807
1984
  ),
1808
1985
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "min-w-0 flex-1", children: [
1809
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "text-sm font-semibold", children: onboarding?.branding?.title ?? "AI \uB3D9\uD589" }),
1986
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex min-w-0 items-center gap-1.5", children: [
1987
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "truncate text-sm font-semibold", children: onboarding?.branding?.title ?? "AI \uB3D9\uD589" }),
1988
+ badgeText ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("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
1989
+ ] }),
1810
1990
  contextLine ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "truncate text-[11px] text-neutral-400", children: contextLine }) : null
1811
1991
  ] }),
1812
1992
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
@@ -1822,7 +2002,7 @@ function XbNovaChat({
1822
2002
  ] }),
1823
2003
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [
1824
2004
  showDisclosure ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("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,
1825
- firstBubble ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssistantMarkdown, { text: firstBubble }) }) : null,
2005
+ opening ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssistantMarkdown, { text: opening }) }) : null,
1826
2006
  messages.map((message, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: message.role === "user" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "user", children: message.parts.map((p) => p.kind === "text" ? p.text : "").join("") }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1827
2007
  AnswerActionsFrame,
1828
2008
  {
@@ -1850,7 +2030,17 @@ function XbNovaChat({
1850
2030
  );
1851
2031
  }
1852
2032
  const spec = clientActionHandlers.get(part.toolName);
1853
- return spec?.render ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: spec.render(part.input) }, j) : null;
2033
+ return spec?.render ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2034
+ "div",
2035
+ {
2036
+ onClickCapture: (e) => {
2037
+ const control = clickedControl(e);
2038
+ if (control) trackClientActionClick(part.toolName, control.label);
2039
+ },
2040
+ children: spec.render(part.input)
2041
+ },
2042
+ j
2043
+ ) : null;
1854
2044
  }
1855
2045
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ToolView, { part, viewRenderers, onVerdictAction }, j);
1856
2046
  }),
@@ -1921,7 +2111,8 @@ function XbNovaChat({
1921
2111
  )
1922
2112
  ]
1923
2113
  }
1924
- )
2114
+ ),
2115
+ disclaimer ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("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
1925
2116
  ] })
1926
2117
  }
1927
2118
  )
@@ -2200,6 +2391,18 @@ function useXbNovaFacts(values) {
2200
2391
  };
2201
2392
  }, [factsRegistry]);
2202
2393
  }
2394
+ function useXbNovaContext(entries) {
2395
+ const { contextRegistry } = useXbNovaInternal();
2396
+ const id = (0, import_react10.useId)();
2397
+ (0, import_react10.useEffect)(() => {
2398
+ contextRegistry.set(id, entries);
2399
+ });
2400
+ (0, import_react10.useEffect)(() => {
2401
+ return () => {
2402
+ contextRegistry.delete(id);
2403
+ };
2404
+ }, [id, contextRegistry]);
2405
+ }
2203
2406
  function useXbNovaVisitor() {
2204
2407
  const { visitorId } = useXbNovaInternal();
2205
2408
  return visitorId;
@@ -2221,6 +2424,7 @@ function XbNovaFacts({ values }) {
2221
2424
  XbNovaTrack,
2222
2425
  trackXbNovaEvent,
2223
2426
  useXbNovaChat,
2427
+ useXbNovaContext,
2224
2428
  useXbNovaFact,
2225
2429
  useXbNovaFacts,
2226
2430
  useXbNovaSettings,