@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.cjs CHANGED
@@ -72,6 +72,7 @@ function useXbNovaInternal() {
72
72
  return ctx;
73
73
  }
74
74
  var VISITOR_STORAGE_KEY = "xbnova_visitor_id";
75
+ var WIDGET_VIEW_WAIT_MS = 1e4;
75
76
  function resolveVisitorId() {
76
77
  if (typeof window === "undefined") return "";
77
78
  try {
@@ -112,6 +113,35 @@ function InternalProvider({
112
113
  if (visitorIdRef.current === null) {
113
114
  visitorIdRef.current = resolveVisitorId();
114
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]);
115
145
  const value = (0, import_react.useMemo)(
116
146
  () => ({
117
147
  auth,
@@ -119,10 +149,12 @@ function InternalProvider({
119
149
  factsRegistry,
120
150
  contextRegistry,
121
151
  clientActionHandlers: clientActionHandlers ?? emptyHandlers,
122
- visitorId: visitorIdRef.current,
152
+ visitorId,
153
+ sessionId,
154
+ sendEvent,
123
155
  theme
124
156
  }),
125
- [auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, theme]
157
+ [auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, visitorId, sessionId, sendEvent, theme]
126
158
  );
127
159
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InternalContext.Provider, { value, children });
128
160
  }
@@ -425,7 +457,7 @@ function getFocusedTrackEntry(trackRegistry) {
425
457
  }
426
458
 
427
459
  // src/nudge-loop.tsx
428
- var EVAL_INTERVAL_MS = 1e3;
460
+ var EVAL_INTERVAL_MS = 250;
429
461
  var XBNOVA_EVENT_NAME = "xbnova:event";
430
462
  var SESSION_COUNT_KEY_PREFIX = "xbnova_hint_session_count:";
431
463
  var SCROLL_SETTLE_MS = 500;
@@ -618,7 +650,7 @@ function clearDebugOverlay(refs) {
618
650
  refs.bandLinesRef.current.clear();
619
651
  }
620
652
  function XbNovaNudgeLoop({ appId }) {
621
- const { auth, trackRegistry, factsRegistry, visitorId } = useXbNovaInternal();
653
+ const { auth, trackRegistry, factsRegistry, sendEvent } = useXbNovaInternal();
622
654
  const { apiKey, apiBaseUrl, screen } = auth;
623
655
  const { hint, offerHint, autoHideHint, hintsEnabled } = useNudgeState();
624
656
  const graphRef = (0, import_react5.useRef)(null);
@@ -651,8 +683,10 @@ function XbNovaNudgeLoop({ appId }) {
651
683
  const debugEnabled = isDebugEnabled();
652
684
  const debugRefs = { panelRef: debugPanelRef, boxesRef: debugBoxesRef, bandLinesRef: debugBandLinesRef };
653
685
  if (!debugEnabled && debugPanelRef.current) clearDebugOverlay(debugRefs);
686
+ const tickDebug = debugEnabled ? (msg) => console.debug(msg) : () => {
687
+ };
654
688
  if (!graph) {
655
- 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");
656
690
  return;
657
691
  }
658
692
  const canOffer = !hintRef.current && hintsEnabledRef.current;
@@ -703,18 +737,7 @@ function XbNovaNudgeLoop({ appId }) {
703
737
  autoHideMs: messageNode.auto_hide_ms ?? 2e4,
704
738
  quickReplies: messageNode.quick_replies ?? []
705
739
  });
706
- fetch(`${apiBaseUrl}/v1/nudge-events`, {
707
- method: "POST",
708
- headers: { "Content-Type": "application/json" },
709
- body: JSON.stringify({
710
- app_id: appId,
711
- nudge_node_id: messageNode.id,
712
- kind: "nudge_impression",
713
- bot_id: botId,
714
- visitor_id: visitorId
715
- })
716
- }).catch(() => {
717
- });
740
+ sendEvent("nudge_impression", { nudge_node_id: messageNode.id, bot_id: botId });
718
741
  };
719
742
  const tryFireFrom = (node) => {
720
743
  const nextEdge = (edgesFrom.get(node.id) ?? [])[0];
@@ -742,7 +765,7 @@ function XbNovaNudgeLoop({ appId }) {
742
765
  const maxPerSession = node.max_per_session === 0 ? Infinity : node.max_per_session ?? 3;
743
766
  const sessionCount = peekSessionCount(node.id);
744
767
  if (sessionCount >= maxPerSession) {
745
- 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`);
746
769
  if (debugEnabled && node.trigger_type === "dwell") {
747
770
  debugLines.push(`- [${node.id}] \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC \u2014 \uB354 \uC548 \uC7BC`);
748
771
  }
@@ -777,7 +800,7 @@ function XbNovaNudgeLoop({ appId }) {
777
800
  overlapPx = status.overlapPx;
778
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}`;
779
802
  }
780
- console.debug(
803
+ tickDebug(
781
804
  `[XbNova] band dwell \uD2B8\uB9AC\uAC70=${node.id} region="${region}" band%=${fromPercent}~${toPercent} | ${statusDetail}`
782
805
  );
783
806
  dwellRecords.push({ node, state, requiredMs, region, found, screenMatches, inBand, overlapPx });
@@ -786,7 +809,7 @@ function XbNovaNudgeLoop({ appId }) {
786
809
  const fires = triggerFires(node, { screen, firedEventName });
787
810
  if (!fires) continue;
788
811
  if (!canOffer) {
789
- 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`);
790
813
  continue;
791
814
  }
792
815
  if (tryFireFrom(node)) {
@@ -886,7 +909,7 @@ function XbNovaNudgeLoop({ appId }) {
886
909
  ensureDebugPanel(debugPanelRef, debugCollapsedRef).body.textContent = debugLines.join("\n");
887
910
  }
888
911
  },
889
- [apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, visitorId]
912
+ [apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, sendEvent]
890
913
  );
891
914
  (0, import_react5.useEffect)(() => {
892
915
  const interval = window.setInterval(() => evaluate(null), EVAL_INTERVAL_MS);
@@ -917,6 +940,9 @@ function XbNovaProvider({
917
940
  apiBaseUrl = DEFAULT_API_BASE_URL,
918
941
  clientActions,
919
942
  theme,
943
+ userKey,
944
+ threadKey,
945
+ sessionKey,
920
946
  children
921
947
  }) {
922
948
  (0, import_react6.useEffect)(() => {
@@ -933,8 +959,17 @@ function XbNovaProvider({
933
959
  if (screen) bumpVisitCount(screen);
934
960
  }, [screen]);
935
961
  const auth = (0, import_react6.useMemo)(
936
- () => ({ apiKey, botId, appId, apiBaseUrl, screen }),
937
- [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]
938
973
  );
939
974
  const resolvedTheme = (0, import_react6.useMemo)(
940
975
  () => ({ ...DEFAULT_THEME, ...theme }),
@@ -1040,12 +1075,41 @@ function XbNovaShadowRoot({
1040
1075
  var import_react8 = require("react");
1041
1076
 
1042
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
+ }
1043
1106
  async function consumeSse(response, handlers) {
1044
1107
  if (!response.ok || !response.body) {
1045
1108
  handlers.onError?.(`\uC694\uCCAD \uC2E4\uD328 (${response.status})`);
1046
1109
  return null;
1047
1110
  }
1048
1111
  const conversationId = response.headers.get("x-conversation-id");
1112
+ if (conversationId) handlers.onConversationId?.(conversationId);
1049
1113
  const reader = response.body.getReader();
1050
1114
  const decoder = new TextDecoder();
1051
1115
  let buffer = "";
@@ -1150,12 +1214,19 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
1150
1214
  // src/use-chat.ts
1151
1215
  var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
1152
1216
  function useXbNovaChat(options) {
1153
- const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId } = useXbNovaInternal();
1217
+ const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId, sessionId, sendEvent } = useXbNovaInternal();
1154
1218
  const botId = options?.botId ?? auth.botId;
1155
1219
  const nudgeNodeId = options?.nudgeNodeId;
1156
- const { apiKey, apiBaseUrl } = auth;
1220
+ const { apiKey, apiBaseUrl, threadKey } = auth;
1221
+ const userKey = visitorId;
1157
1222
  const [onboarding, setOnboarding] = (0, import_react8.useState)(null);
1158
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;
1159
1230
  const [busy, setBusy] = (0, import_react8.useState)(false);
1160
1231
  const [error, setError] = (0, import_react8.useState)(null);
1161
1232
  const [clientResults, setClientResults] = (0, import_react8.useState)({});
@@ -1166,18 +1237,59 @@ function useXbNovaChat(options) {
1166
1237
  const nudgeNodeIdRef = (0, import_react8.useRef)(nudgeNodeId);
1167
1238
  nudgeNodeIdRef.current = nudgeNodeId;
1168
1239
  (0, import_react8.useEffect)(() => {
1240
+ let cancelled = false;
1169
1241
  setOnboarding(null);
1170
1242
  setMessages([]);
1243
+ setResumed(false);
1244
+ setSentOpening(null);
1171
1245
  setError(null);
1172
1246
  setClientResults({});
1173
1247
  setFeedbackState({});
1174
1248
  dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
1249
+ conversationIdRef.current = null;
1250
+ pendingOpeningRef.current = null;
1175
1251
  fetch(`${apiBaseUrl}/v1/conversations`, {
1176
1252
  method: "POST",
1177
1253
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
1178
- body: JSON.stringify({ bot_id: botId, nudge_node_id: nudgeNodeIdRef.current ?? null })
1179
- }).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}`));
1180
- }, [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]);
1181
1293
  const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
1182
1294
  const next = [...prev];
1183
1295
  next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
@@ -1190,7 +1302,7 @@ function useXbNovaChat(options) {
1190
1302
  return next;
1191
1303
  });
1192
1304
  async function setFeedback(messageId, rating, reason) {
1193
- const conversationId = onboarding?.conversation_id;
1305
+ const conversationId = conversationIdRef.current;
1194
1306
  if (!conversationId || !messageId) return;
1195
1307
  const prevRating = feedbackRef.current[messageId] ?? null;
1196
1308
  const apply = (r) => setFeedbackState((prev) => {
@@ -1216,6 +1328,14 @@ function useXbNovaChat(options) {
1216
1328
  apply(prevRating);
1217
1329
  }
1218
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
+ }
1219
1339
  async function runClientAction(spec, part, existingParts) {
1220
1340
  if (spec.dismissSheet) options?.onDismissSheet?.();
1221
1341
  let result;
@@ -1239,7 +1359,7 @@ function useXbNovaChat(options) {
1239
1359
  result = e instanceof Error ? `\uC624\uB958: ${e.message}` : "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
1240
1360
  }
1241
1361
  if (!spec.awaitResult) return;
1242
- const conversationId = onboarding?.conversation_id;
1362
+ const conversationId = conversationIdRef.current;
1243
1363
  if (!conversationId) return;
1244
1364
  const structured = result && typeof result === "object" ? result : null;
1245
1365
  if (structured?.view) {
@@ -1283,6 +1403,7 @@ function useXbNovaChat(options) {
1283
1403
  if (!spec) continue;
1284
1404
  if (dispatchedClientActionsRef.current.has(part.toolCallId)) continue;
1285
1405
  dispatchedClientActionsRef.current.add(part.toolCallId);
1406
+ if (spec.render) postClientActionEvent("cta_impression", part.toolName, null);
1286
1407
  void runClientAction(spec, part, last.parts);
1287
1408
  }
1288
1409
  }, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
@@ -1305,6 +1426,14 @@ function useXbNovaChat(options) {
1305
1426
  const notes = collectContextEntries(contextRegistry);
1306
1427
  const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
1307
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;
1308
1437
  try {
1309
1438
  await streamChat(
1310
1439
  apiBaseUrl,
@@ -1312,12 +1441,22 @@ function useXbNovaChat(options) {
1312
1441
  {
1313
1442
  bot_id: botId,
1314
1443
  message: trimmed,
1315
- conversation_id: onboarding?.conversation_id ?? null,
1444
+ conversation_id: conversationIdRef.current,
1316
1445
  context,
1317
1446
  nudge_node_id: nudgeNodeId ?? null,
1318
- opening_message: messages.length === 0 ? (options?.openingMessage ?? onboarding?.greeting) || null : null
1447
+ opening_message: openingMessage2,
1448
+ visitor_id: userKey || null,
1449
+ thread_key: threadKey ?? null,
1450
+ session_id: sessionId
1319
1451
  },
1320
- { 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
+ }
1321
1460
  );
1322
1461
  } catch (e) {
1323
1462
  setError(e instanceof Error ? e.message : String(e));
@@ -1325,7 +1464,19 @@ function useXbNovaChat(options) {
1325
1464
  setBusy(false);
1326
1465
  }
1327
1466
  }
1328
- 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
+ };
1329
1480
  }
1330
1481
 
1331
1482
  // src/views.tsx
@@ -1458,6 +1609,13 @@ function hasVisiblePart(parts, clientActionHandlers, clientResults) {
1458
1609
  }
1459
1610
  var DISCLOSURE_SEEN_KEY = "xbnova_disclosure_seen";
1460
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
+ }
1461
1619
  function readDisclosureSeen() {
1462
1620
  if (typeof window === "undefined") return true;
1463
1621
  try {
@@ -1587,9 +1745,10 @@ function XbNovaChat({
1587
1745
  avatarSize = 28,
1588
1746
  onVerdictAction,
1589
1747
  feedback: feedbackEnabled = true,
1590
- onFeedback
1748
+ onFeedback,
1749
+ onOpen
1591
1750
  }) {
1592
- const { auth, trackRegistry, factsRegistry, visitorId, clientActionHandlers, theme } = useXbNovaInternal();
1751
+ const { auth, trackRegistry, factsRegistry, clientActionHandlers, theme, sendEvent } = useXbNovaInternal();
1593
1752
  const themeVars = {
1594
1753
  "--xbnova-accent-1": theme.accentFrom,
1595
1754
  "--xbnova-accent-2": theme.accentTo
@@ -1610,7 +1769,7 @@ function XbNovaChat({
1610
1769
  const [input, setInput] = (0, import_react9.useState)("");
1611
1770
  const bottomRef = (0, import_react9.useRef)(null);
1612
1771
  const typedHintText = useTypewriter(hint?.text ?? "", hint?.triggerNodeId ?? "");
1613
- const { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback } = useXbNovaChat({
1772
+ const { onboarding, opening, messages, busy, error, send, clientResults, feedback, setFeedback, trackClientActionClick } = useXbNovaChat({
1614
1773
  onFeedback,
1615
1774
  botId: activeBotId ?? botId,
1616
1775
  nudgeNodeId: activeNudgeNodeId,
@@ -1632,12 +1791,19 @@ function XbNovaChat({
1632
1791
  try {
1633
1792
  window.localStorage.setItem(
1634
1793
  `xbnova_branding:${effectiveBotId}`,
1635
- 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
+ })
1636
1800
  );
1637
1801
  } catch {
1638
1802
  }
1639
1803
  }, [onboarding?.branding, effectiveBotId]);
1640
1804
  const branding = onboarding?.branding ?? cachedBranding ?? void 0;
1805
+ const badgeText = branding?.badge_text?.trim() ?? "";
1806
+ const disclaimer = branding?.disclaimer?.trim() ?? "";
1641
1807
  const brandingReady = Boolean(onboarding) || cachedBranding !== null || Boolean(error);
1642
1808
  (0, import_react9.useEffect)(() => {
1643
1809
  bottomRef.current?.scrollIntoView({ behavior: "smooth" });
@@ -1667,18 +1833,7 @@ function XbNovaChat({
1667
1833
  setOpeningLine(consumed.text);
1668
1834
  setActiveNudgeNodeId(consumed.messageNodeId);
1669
1835
  setNudgeQuickReplies(consumed.quickReplies);
1670
- fetch(`${auth.apiBaseUrl}/v1/nudge-events`, {
1671
- method: "POST",
1672
- headers: { "Content-Type": "application/json" },
1673
- body: JSON.stringify({
1674
- app_id: auth.appId,
1675
- nudge_node_id: consumed.messageNodeId,
1676
- kind: "nudge_open",
1677
- bot_id: consumed.botId,
1678
- visitor_id: visitorId
1679
- })
1680
- }).catch(() => {
1681
- });
1836
+ sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1682
1837
  } else {
1683
1838
  setNudgeQuickReplies([]);
1684
1839
  setActiveNudgeNodeId(void 0);
@@ -1692,6 +1847,11 @@ function XbNovaChat({
1692
1847
  markDisclosureSeen();
1693
1848
  }
1694
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
+ }
1695
1855
  }
1696
1856
  if (!open) {
1697
1857
  if (barHidden) return null;
@@ -1787,7 +1947,6 @@ function XbNovaChat({
1787
1947
  const summary = resolveFactValue(factsRegistry.get(SUMMARY_FACT_KEY));
1788
1948
  const focusedLabel = getFocusedTrackEntry(trackRegistry)?.label;
1789
1949
  const contextLine = summary ?? (focusedLabel ? `${focusedLabel} \uBCF4\uB294 \uC911` : void 0);
1790
- const firstBubble = openingLine ?? onboarding?.greeting;
1791
1950
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1792
1951
  !isDesktop ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1793
1952
  XbNovaShadowRoot,
@@ -1824,7 +1983,10 @@ function XbNovaChat({
1824
1983
  }
1825
1984
  ),
1826
1985
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "min-w-0 flex-1", children: [
1827
- /* @__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
+ ] }),
1828
1990
  contextLine ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "truncate text-[11px] text-neutral-400", children: contextLine }) : null
1829
1991
  ] }),
1830
1992
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
@@ -1840,7 +2002,7 @@ function XbNovaChat({
1840
2002
  ] }),
1841
2003
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [
1842
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,
1843
- 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,
1844
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)(
1845
2007
  AnswerActionsFrame,
1846
2008
  {
@@ -1868,7 +2030,17 @@ function XbNovaChat({
1868
2030
  );
1869
2031
  }
1870
2032
  const spec = clientActionHandlers.get(part.toolName);
1871
- 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;
1872
2044
  }
1873
2045
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ToolView, { part, viewRenderers, onVerdictAction }, j);
1874
2046
  }),
@@ -1939,7 +2111,8 @@ function XbNovaChat({
1939
2111
  )
1940
2112
  ]
1941
2113
  }
1942
- )
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
1943
2116
  ] })
1944
2117
  }
1945
2118
  )