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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,44 @@ 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)((event, properties = {}) => {
121
+ const { auth: a, visitorId: v, sessionId: sid } = latestRef.current;
122
+ if (!v) return;
123
+ const { conversation_id: conversationId, ...rest } = properties;
124
+ fetch(`${a.apiBaseUrl}/v1/events`, {
125
+ method: "POST",
126
+ keepalive: true,
127
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${a.apiKey}` },
128
+ body: JSON.stringify({
129
+ app_id: a.appId,
130
+ event,
131
+ visitor_id: v,
132
+ session_id: sid,
133
+ conversation_id: conversationId ?? null,
134
+ properties: { screen: a.screen, ...rest }
135
+ })
136
+ }).catch(() => {
137
+ });
138
+ }, []);
139
+ const widgetViewSentRef = (0, import_react.useRef)(false);
140
+ (0, import_react.useEffect)(() => {
141
+ if (widgetViewSentRef.current) return;
142
+ const send = () => {
143
+ if (widgetViewSentRef.current) return;
144
+ widgetViewSentRef.current = true;
145
+ sendEvent("widget_view");
146
+ };
147
+ if (sessionId) {
148
+ send();
149
+ return;
150
+ }
151
+ const timer = setTimeout(send, WIDGET_VIEW_WAIT_MS);
152
+ return () => clearTimeout(timer);
153
+ }, [sessionId, sendEvent]);
115
154
  const value = (0, import_react.useMemo)(
116
155
  () => ({
117
156
  auth,
@@ -119,10 +158,12 @@ function InternalProvider({
119
158
  factsRegistry,
120
159
  contextRegistry,
121
160
  clientActionHandlers: clientActionHandlers ?? emptyHandlers,
122
- visitorId: visitorIdRef.current,
161
+ visitorId,
162
+ sessionId,
163
+ sendEvent,
123
164
  theme
124
165
  }),
125
- [auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, theme]
166
+ [auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, visitorId, sessionId, sendEvent, theme]
126
167
  );
127
168
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InternalContext.Provider, { value, children });
128
169
  }
@@ -425,7 +466,7 @@ function getFocusedTrackEntry(trackRegistry) {
425
466
  }
426
467
 
427
468
  // src/nudge-loop.tsx
428
- var EVAL_INTERVAL_MS = 1e3;
469
+ var EVAL_INTERVAL_MS = 250;
429
470
  var XBNOVA_EVENT_NAME = "xbnova:event";
430
471
  var SESSION_COUNT_KEY_PREFIX = "xbnova_hint_session_count:";
431
472
  var SCROLL_SETTLE_MS = 500;
@@ -618,7 +659,7 @@ function clearDebugOverlay(refs) {
618
659
  refs.bandLinesRef.current.clear();
619
660
  }
620
661
  function XbNovaNudgeLoop({ appId }) {
621
- const { auth, trackRegistry, factsRegistry, visitorId } = useXbNovaInternal();
662
+ const { auth, trackRegistry, factsRegistry, sendEvent } = useXbNovaInternal();
622
663
  const { apiKey, apiBaseUrl, screen } = auth;
623
664
  const { hint, offerHint, autoHideHint, hintsEnabled } = useNudgeState();
624
665
  const graphRef = (0, import_react5.useRef)(null);
@@ -651,8 +692,10 @@ function XbNovaNudgeLoop({ appId }) {
651
692
  const debugEnabled = isDebugEnabled();
652
693
  const debugRefs = { panelRef: debugPanelRef, boxesRef: debugBoxesRef, bandLinesRef: debugBandLinesRef };
653
694
  if (!debugEnabled && debugPanelRef.current) clearDebugOverlay(debugRefs);
695
+ const tickDebug = debugEnabled ? (msg) => console.debug(msg) : () => {
696
+ };
654
697
  if (!graph) {
655
- console.debug("[XbNova] nudge \uD3C9\uAC00 \uAC74\uB108\uB700: \uADF8\uB798\uD504\uB97C \uC544\uC9C1 \uBABB \uBD88\uB7EC\uC634");
698
+ tickDebug("[XbNova] nudge \uD3C9\uAC00 \uAC74\uB108\uB700: \uADF8\uB798\uD504\uB97C \uC544\uC9C1 \uBABB \uBD88\uB7EC\uC634");
656
699
  return;
657
700
  }
658
701
  const canOffer = !hintRef.current && hintsEnabledRef.current;
@@ -703,18 +746,7 @@ function XbNovaNudgeLoop({ appId }) {
703
746
  autoHideMs: messageNode.auto_hide_ms ?? 2e4,
704
747
  quickReplies: messageNode.quick_replies ?? []
705
748
  });
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
- });
749
+ sendEvent("nudge_impression", { nudge_node_id: messageNode.id, bot_id: botId });
718
750
  };
719
751
  const tryFireFrom = (node) => {
720
752
  const nextEdge = (edgesFrom.get(node.id) ?? [])[0];
@@ -742,7 +774,7 @@ function XbNovaNudgeLoop({ appId }) {
742
774
  const maxPerSession = node.max_per_session === 0 ? Infinity : node.max_per_session ?? 3;
743
775
  const sessionCount = peekSessionCount(node.id);
744
776
  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`);
777
+ tickDebug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id}(${node.trigger_type}) \uAC74\uB108\uB700: \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC`);
746
778
  if (debugEnabled && node.trigger_type === "dwell") {
747
779
  debugLines.push(`- [${node.id}] \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC \u2014 \uB354 \uC548 \uC7BC`);
748
780
  }
@@ -777,7 +809,7 @@ function XbNovaNudgeLoop({ appId }) {
777
809
  overlapPx = status.overlapPx;
778
810
  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
811
  }
780
- console.debug(
812
+ tickDebug(
781
813
  `[XbNova] band dwell \uD2B8\uB9AC\uAC70=${node.id} region="${region}" band%=${fromPercent}~${toPercent} | ${statusDetail}`
782
814
  );
783
815
  dwellRecords.push({ node, state, requiredMs, region, found, screenMatches, inBand, overlapPx });
@@ -786,7 +818,7 @@ function XbNovaNudgeLoop({ appId }) {
786
818
  const fires = triggerFires(node, { screen, firedEventName });
787
819
  if (!fires) continue;
788
820
  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`);
821
+ 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
822
  continue;
791
823
  }
792
824
  if (tryFireFrom(node)) {
@@ -886,7 +918,7 @@ function XbNovaNudgeLoop({ appId }) {
886
918
  ensureDebugPanel(debugPanelRef, debugCollapsedRef).body.textContent = debugLines.join("\n");
887
919
  }
888
920
  },
889
- [apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, visitorId]
921
+ [apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, sendEvent]
890
922
  );
891
923
  (0, import_react5.useEffect)(() => {
892
924
  const interval = window.setInterval(() => evaluate(null), EVAL_INTERVAL_MS);
@@ -917,6 +949,9 @@ function XbNovaProvider({
917
949
  apiBaseUrl = DEFAULT_API_BASE_URL,
918
950
  clientActions,
919
951
  theme,
952
+ userKey,
953
+ threadKey,
954
+ sessionKey,
920
955
  children
921
956
  }) {
922
957
  (0, import_react6.useEffect)(() => {
@@ -933,8 +968,17 @@ function XbNovaProvider({
933
968
  if (screen) bumpVisitCount(screen);
934
969
  }, [screen]);
935
970
  const auth = (0, import_react6.useMemo)(
936
- () => ({ apiKey, botId, appId, apiBaseUrl, screen }),
937
- [apiKey, botId, appId, apiBaseUrl, screen]
971
+ () => ({
972
+ apiKey,
973
+ botId,
974
+ appId,
975
+ apiBaseUrl,
976
+ screen,
977
+ userKey: userKey || void 0,
978
+ threadKey: threadKey || void 0,
979
+ sessionKey: sessionKey || void 0
980
+ }),
981
+ [apiKey, botId, appId, apiBaseUrl, screen, userKey, threadKey, sessionKey]
938
982
  );
939
983
  const resolvedTheme = (0, import_react6.useMemo)(
940
984
  () => ({ ...DEFAULT_THEME, ...theme }),
@@ -1040,12 +1084,41 @@ function XbNovaShadowRoot({
1040
1084
  var import_react8 = require("react");
1041
1085
 
1042
1086
  // src/stream.ts
1087
+ function fromStoredMessages(stored) {
1088
+ return stored.map((m) => ({
1089
+ id: m.role === "assistant" ? m.id : void 0,
1090
+ role: m.role === "assistant" ? "assistant" : "user",
1091
+ parts: m.parts.flatMap((p, i) => {
1092
+ const type = String(p.type ?? "");
1093
+ if (type === "text") {
1094
+ const text = typeof p.text === "string" ? p.text : "";
1095
+ return text ? [{ kind: "text", id: `${m.id}:${i}`, text }] : [];
1096
+ }
1097
+ if (type.startsWith("tool-")) {
1098
+ const meta = p.providerMetadata;
1099
+ return [
1100
+ {
1101
+ kind: "tool",
1102
+ toolCallId: String(p.toolCallId ?? `${m.id}:${i}`),
1103
+ toolName: type.slice("tool-".length),
1104
+ state: p.state === "output-error" ? "error" : "output",
1105
+ input: p.input,
1106
+ output: p.output,
1107
+ view: meta?.["kr.dsauto.chat"]?.view ?? null
1108
+ }
1109
+ ];
1110
+ }
1111
+ return [];
1112
+ })
1113
+ }));
1114
+ }
1043
1115
  async function consumeSse(response, handlers) {
1044
1116
  if (!response.ok || !response.body) {
1045
1117
  handlers.onError?.(`\uC694\uCCAD \uC2E4\uD328 (${response.status})`);
1046
1118
  return null;
1047
1119
  }
1048
1120
  const conversationId = response.headers.get("x-conversation-id");
1121
+ if (conversationId) handlers.onConversationId?.(conversationId);
1049
1122
  const reader = response.body.getReader();
1050
1123
  const decoder = new TextDecoder();
1051
1124
  let buffer = "";
@@ -1150,12 +1223,19 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
1150
1223
  // src/use-chat.ts
1151
1224
  var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
1152
1225
  function useXbNovaChat(options) {
1153
- const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId } = useXbNovaInternal();
1226
+ const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId, sessionId, sendEvent } = useXbNovaInternal();
1154
1227
  const botId = options?.botId ?? auth.botId;
1155
1228
  const nudgeNodeId = options?.nudgeNodeId;
1156
- const { apiKey, apiBaseUrl } = auth;
1229
+ const { apiKey, apiBaseUrl, threadKey } = auth;
1230
+ const userKey = visitorId;
1157
1231
  const [onboarding, setOnboarding] = (0, import_react8.useState)(null);
1158
1232
  const [messages, setMessages] = (0, import_react8.useState)([]);
1233
+ const [resumed, setResumed] = (0, import_react8.useState)(false);
1234
+ const [sentOpening, setSentOpening] = (0, import_react8.useState)(null);
1235
+ const conversationIdRef = (0, import_react8.useRef)(null);
1236
+ const pendingOpeningRef = (0, import_react8.useRef)(null);
1237
+ const messagesRef = (0, import_react8.useRef)(messages);
1238
+ messagesRef.current = messages;
1159
1239
  const [busy, setBusy] = (0, import_react8.useState)(false);
1160
1240
  const [error, setError] = (0, import_react8.useState)(null);
1161
1241
  const [clientResults, setClientResults] = (0, import_react8.useState)({});
@@ -1166,18 +1246,59 @@ function useXbNovaChat(options) {
1166
1246
  const nudgeNodeIdRef = (0, import_react8.useRef)(nudgeNodeId);
1167
1247
  nudgeNodeIdRef.current = nudgeNodeId;
1168
1248
  (0, import_react8.useEffect)(() => {
1249
+ let cancelled = false;
1169
1250
  setOnboarding(null);
1170
1251
  setMessages([]);
1252
+ setResumed(false);
1253
+ setSentOpening(null);
1171
1254
  setError(null);
1172
1255
  setClientResults({});
1173
1256
  setFeedbackState({});
1174
1257
  dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
1258
+ conversationIdRef.current = null;
1259
+ pendingOpeningRef.current = null;
1175
1260
  fetch(`${apiBaseUrl}/v1/conversations`, {
1176
1261
  method: "POST",
1177
1262
  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]);
1263
+ body: JSON.stringify({
1264
+ bot_id: botId,
1265
+ nudge_node_id: nudgeNodeIdRef.current ?? null,
1266
+ visitor_id: userKey || null,
1267
+ thread_key: threadKey ?? null,
1268
+ session_id: sessionId,
1269
+ defer: true
1270
+ })
1271
+ }).then((r) => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))).then((data) => {
1272
+ if (cancelled) return;
1273
+ if (conversationIdRef.current || messagesRef.current.length > 0) {
1274
+ setOnboarding(data);
1275
+ return;
1276
+ }
1277
+ conversationIdRef.current = data.conversation_id;
1278
+ const restored = fromStoredMessages(data.messages ?? []);
1279
+ for (const m of restored) {
1280
+ for (const p of m.parts) if (p.kind === "tool") dispatchedClientActionsRef.current.add(p.toolCallId);
1281
+ }
1282
+ setMessages(restored);
1283
+ setResumed(restored.length > 0);
1284
+ setOnboarding(data);
1285
+ }).catch((e) => {
1286
+ if (!cancelled) setError(`\uBD07\uC744 \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`);
1287
+ });
1288
+ return () => {
1289
+ cancelled = true;
1290
+ };
1291
+ }, [apiBaseUrl, apiKey, botId, userKey, threadKey]);
1292
+ const openingMessage = options?.openingMessage;
1293
+ (0, import_react8.useEffect)(() => {
1294
+ if (!onboarding || !nudgeNodeId || !openingMessage) return;
1295
+ if (messagesRef.current.length === 0) return;
1296
+ setMessages((prev) => [
1297
+ ...prev,
1298
+ { role: "assistant", parts: [{ kind: "text", id: `nudge:${nudgeNodeId}:${prev.length}`, text: openingMessage }] }
1299
+ ]);
1300
+ pendingOpeningRef.current = openingMessage;
1301
+ }, [onboarding, nudgeNodeId, openingMessage]);
1181
1302
  const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
1182
1303
  const next = [...prev];
1183
1304
  next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
@@ -1190,7 +1311,7 @@ function useXbNovaChat(options) {
1190
1311
  return next;
1191
1312
  });
1192
1313
  async function setFeedback(messageId, rating, reason) {
1193
- const conversationId = onboarding?.conversation_id;
1314
+ const conversationId = conversationIdRef.current;
1194
1315
  if (!conversationId || !messageId) return;
1195
1316
  const prevRating = feedbackRef.current[messageId] ?? null;
1196
1317
  const apply = (r) => setFeedbackState((prev) => {
@@ -1210,12 +1331,21 @@ function useXbNovaChat(options) {
1210
1331
  }
1211
1332
  );
1212
1333
  if (!res.ok) throw new Error(`${res.status}`);
1334
+ sendEvent("feedback_submit", { conversation_id: conversationId, message_id: messageId, rating, reason });
1213
1335
  options?.onFeedback?.({ conversationId, messageId, rating, reason });
1214
1336
  } catch (e) {
1215
1337
  console.warn("[xbnova] \uD53C\uB4DC\uBC31 \uC800\uC7A5 \uC2E4\uD328", e);
1216
1338
  apply(prevRating);
1217
1339
  }
1218
1340
  }
1341
+ function postClientActionEvent(kind, action, label) {
1342
+ const conversationId = conversationIdRef.current;
1343
+ if (!conversationId) return;
1344
+ sendEvent(kind, { conversation_id: conversationId, action, label });
1345
+ }
1346
+ function trackClientActionClick(action, label) {
1347
+ postClientActionEvent("cta_click", action, label);
1348
+ }
1219
1349
  async function runClientAction(spec, part, existingParts) {
1220
1350
  if (spec.dismissSheet) options?.onDismissSheet?.();
1221
1351
  let result;
@@ -1239,7 +1369,7 @@ function useXbNovaChat(options) {
1239
1369
  result = e instanceof Error ? `\uC624\uB958: ${e.message}` : "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
1240
1370
  }
1241
1371
  if (!spec.awaitResult) return;
1242
- const conversationId = onboarding?.conversation_id;
1372
+ const conversationId = conversationIdRef.current;
1243
1373
  if (!conversationId) return;
1244
1374
  const structured = result && typeof result === "object" ? result : null;
1245
1375
  if (structured?.view) {
@@ -1283,12 +1413,18 @@ function useXbNovaChat(options) {
1283
1413
  if (!spec) continue;
1284
1414
  if (dispatchedClientActionsRef.current.has(part.toolCallId)) continue;
1285
1415
  dispatchedClientActionsRef.current.add(part.toolCallId);
1416
+ if (spec.render) postClientActionEvent("cta_impression", part.toolName, null);
1286
1417
  void runClientAction(spec, part, last.parts);
1287
1418
  }
1288
1419
  }, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
1289
1420
  async function send(text) {
1290
1421
  const trimmed = text.trim();
1291
1422
  if (!trimmed || busy) return;
1423
+ sendEvent("message_send", {
1424
+ conversation_id: conversationIdRef.current,
1425
+ is_first: messages.length === 0,
1426
+ length: trimmed.length
1427
+ });
1292
1428
  setError(null);
1293
1429
  setBusy(true);
1294
1430
  setMessages((prev) => [
@@ -1305,6 +1441,14 @@ function useXbNovaChat(options) {
1305
1441
  const notes = collectContextEntries(contextRegistry);
1306
1442
  const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
1307
1443
  const context = focused || notes.length ? { ...screen, ...notes.length ? { notes } : {} } : void 0;
1444
+ let openingMessage2;
1445
+ if (messages.length === 0) {
1446
+ openingMessage2 = (options?.openingMessage ?? onboarding?.greeting) || null;
1447
+ setSentOpening(openingMessage2);
1448
+ } else {
1449
+ openingMessage2 = pendingOpeningRef.current;
1450
+ }
1451
+ pendingOpeningRef.current = null;
1308
1452
  try {
1309
1453
  await streamChat(
1310
1454
  apiBaseUrl,
@@ -1312,12 +1456,22 @@ function useXbNovaChat(options) {
1312
1456
  {
1313
1457
  bot_id: botId,
1314
1458
  message: trimmed,
1315
- conversation_id: onboarding?.conversation_id ?? null,
1459
+ conversation_id: conversationIdRef.current,
1316
1460
  context,
1317
1461
  nudge_node_id: nudgeNodeId ?? null,
1318
- opening_message: messages.length === 0 ? (options?.openingMessage ?? onboarding?.greeting) || null : null
1462
+ opening_message: openingMessage2,
1463
+ visitor_id: userKey || null,
1464
+ thread_key: threadKey ?? null,
1465
+ session_id: sessionId
1319
1466
  },
1320
- { onPart: replaceLast, onMessageId: setLastAssistantId, onError: setError }
1467
+ {
1468
+ onPart: replaceLast,
1469
+ onMessageId: setLastAssistantId,
1470
+ onConversationId: (id) => {
1471
+ conversationIdRef.current = id;
1472
+ },
1473
+ onError: setError
1474
+ }
1321
1475
  );
1322
1476
  } catch (e) {
1323
1477
  setError(e instanceof Error ? e.message : String(e));
@@ -1325,7 +1479,19 @@ function useXbNovaChat(options) {
1325
1479
  setBusy(false);
1326
1480
  }
1327
1481
  }
1328
- return { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback };
1482
+ const opening = resumed ? null : sentOpening ?? options?.openingMessage ?? onboarding?.greeting ?? null;
1483
+ return {
1484
+ onboarding,
1485
+ opening,
1486
+ messages,
1487
+ busy,
1488
+ error,
1489
+ send,
1490
+ clientResults,
1491
+ feedback,
1492
+ setFeedback,
1493
+ trackClientActionClick
1494
+ };
1329
1495
  }
1330
1496
 
1331
1497
  // src/views.tsx
@@ -1458,6 +1624,13 @@ function hasVisiblePart(parts, clientActionHandlers, clientResults) {
1458
1624
  }
1459
1625
  var DISCLOSURE_SEEN_KEY = "xbnova_disclosure_seen";
1460
1626
  var SUMMARY_FACT_KEY = "xbnova_summary";
1627
+ function clickedControl(e) {
1628
+ const target = e.target instanceof Element ? e.target : null;
1629
+ const control = target?.closest("button, a, [role='button']");
1630
+ if (!control || !e.currentTarget.contains(control)) return null;
1631
+ const text = (control.textContent ?? "").replace(/\s+/g, " ").trim();
1632
+ return { label: text || control.getAttribute("aria-label") || null };
1633
+ }
1461
1634
  function readDisclosureSeen() {
1462
1635
  if (typeof window === "undefined") return true;
1463
1636
  try {
@@ -1587,9 +1760,10 @@ function XbNovaChat({
1587
1760
  avatarSize = 28,
1588
1761
  onVerdictAction,
1589
1762
  feedback: feedbackEnabled = true,
1590
- onFeedback
1763
+ onFeedback,
1764
+ onOpen
1591
1765
  }) {
1592
- const { auth, trackRegistry, factsRegistry, visitorId, clientActionHandlers, theme } = useXbNovaInternal();
1766
+ const { auth, trackRegistry, factsRegistry, clientActionHandlers, theme, sendEvent } = useXbNovaInternal();
1593
1767
  const themeVars = {
1594
1768
  "--xbnova-accent-1": theme.accentFrom,
1595
1769
  "--xbnova-accent-2": theme.accentTo
@@ -1610,7 +1784,7 @@ function XbNovaChat({
1610
1784
  const [input, setInput] = (0, import_react9.useState)("");
1611
1785
  const bottomRef = (0, import_react9.useRef)(null);
1612
1786
  const typedHintText = useTypewriter(hint?.text ?? "", hint?.triggerNodeId ?? "");
1613
- const { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback } = useXbNovaChat({
1787
+ const { onboarding, opening, messages, busy, error, send, clientResults, feedback, setFeedback, trackClientActionClick } = useXbNovaChat({
1614
1788
  onFeedback,
1615
1789
  botId: activeBotId ?? botId,
1616
1790
  nudgeNodeId: activeNudgeNodeId,
@@ -1632,12 +1806,19 @@ function XbNovaChat({
1632
1806
  try {
1633
1807
  window.localStorage.setItem(
1634
1808
  `xbnova_branding:${effectiveBotId}`,
1635
- JSON.stringify({ logo_url: onboarding.branding.logo_url, avatar_text: onboarding.branding.avatar_text })
1809
+ JSON.stringify({
1810
+ logo_url: onboarding.branding.logo_url,
1811
+ avatar_text: onboarding.branding.avatar_text,
1812
+ badge_text: onboarding.branding.badge_text,
1813
+ disclaimer: onboarding.branding.disclaimer
1814
+ })
1636
1815
  );
1637
1816
  } catch {
1638
1817
  }
1639
1818
  }, [onboarding?.branding, effectiveBotId]);
1640
1819
  const branding = onboarding?.branding ?? cachedBranding ?? void 0;
1820
+ const badgeText = branding?.badge_text?.trim() ?? "";
1821
+ const disclaimer = branding?.disclaimer?.trim() ?? "";
1641
1822
  const brandingReady = Boolean(onboarding) || cachedBranding !== null || Boolean(error);
1642
1823
  (0, import_react9.useEffect)(() => {
1643
1824
  bottomRef.current?.scrollIntoView({ behavior: "smooth" });
@@ -1667,18 +1848,7 @@ function XbNovaChat({
1667
1848
  setOpeningLine(consumed.text);
1668
1849
  setActiveNudgeNodeId(consumed.messageNodeId);
1669
1850
  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
- });
1851
+ sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1682
1852
  } else {
1683
1853
  setNudgeQuickReplies([]);
1684
1854
  setActiveNudgeNodeId(void 0);
@@ -1692,6 +1862,16 @@ function XbNovaChat({
1692
1862
  markDisclosureSeen();
1693
1863
  }
1694
1864
  setOpen(true);
1865
+ sendEvent("chat_open", { from_nudge: Boolean(consumed) });
1866
+ try {
1867
+ onOpen?.({ fromNudge: Boolean(consumed), hintText: consumed?.text });
1868
+ } catch (err) {
1869
+ console.warn("[XbNova] onOpen \uCF5C\uBC31 \uC624\uB958", err);
1870
+ }
1871
+ }
1872
+ function closeSheet() {
1873
+ setOpen(false);
1874
+ sendEvent("chat_close");
1695
1875
  }
1696
1876
  if (!open) {
1697
1877
  if (barHidden) return null;
@@ -1787,7 +1967,6 @@ function XbNovaChat({
1787
1967
  const summary = resolveFactValue(factsRegistry.get(SUMMARY_FACT_KEY));
1788
1968
  const focusedLabel = getFocusedTrackEntry(trackRegistry)?.label;
1789
1969
  const contextLine = summary ?? (focusedLabel ? `${focusedLabel} \uBCF4\uB294 \uC911` : void 0);
1790
- const firstBubble = openingLine ?? onboarding?.greeting;
1791
1970
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1792
1971
  !isDesktop ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1793
1972
  XbNovaShadowRoot,
@@ -1798,7 +1977,7 @@ function XbNovaChat({
1798
1977
  transition: "opacity 260ms ease-out",
1799
1978
  ...backdropStyle
1800
1979
  },
1801
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "h-full w-full bg-black/45", onClick: () => setOpen(false) })
1980
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "h-full w-full bg-black/45", onClick: closeSheet })
1802
1981
  }
1803
1982
  ) : null,
1804
1983
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
@@ -1824,14 +2003,17 @@ function XbNovaChat({
1824
2003
  }
1825
2004
  ),
1826
2005
  /* @__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" }),
2006
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex min-w-0 items-center gap-1.5", children: [
2007
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "truncate text-sm font-semibold", children: onboarding?.branding?.title ?? "AI \uB3D9\uD589" }),
2008
+ 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
2009
+ ] }),
1828
2010
  contextLine ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "truncate text-[11px] text-neutral-400", children: contextLine }) : null
1829
2011
  ] }),
1830
2012
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1831
2013
  "button",
1832
2014
  {
1833
2015
  type: "button",
1834
- onClick: () => setOpen(false),
2016
+ onClick: closeSheet,
1835
2017
  "aria-label": "\uCC44\uD305 \uB2EB\uAE30",
1836
2018
  className: "shrink-0 text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200",
1837
2019
  children: "\xD7"
@@ -1840,7 +2022,7 @@ function XbNovaChat({
1840
2022
  ] }),
1841
2023
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [
1842
2024
  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,
2025
+ opening ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssistantMarkdown, { text: opening }) }) : null,
1844
2026
  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
2027
  AnswerActionsFrame,
1846
2028
  {
@@ -1868,7 +2050,17 @@ function XbNovaChat({
1868
2050
  );
1869
2051
  }
1870
2052
  const spec = clientActionHandlers.get(part.toolName);
1871
- return spec?.render ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: spec.render(part.input) }, j) : null;
2053
+ return spec?.render ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2054
+ "div",
2055
+ {
2056
+ onClickCapture: (e) => {
2057
+ const control = clickedControl(e);
2058
+ if (control) trackClientActionClick(part.toolName, control.label);
2059
+ },
2060
+ children: spec.render(part.input)
2061
+ },
2062
+ j
2063
+ ) : null;
1872
2064
  }
1873
2065
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ToolView, { part, viewRenderers, onVerdictAction }, j);
1874
2066
  }),
@@ -1939,7 +2131,8 @@ function XbNovaChat({
1939
2131
  )
1940
2132
  ]
1941
2133
  }
1942
- )
2134
+ ),
2135
+ 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
2136
  ] })
1944
2137
  }
1945
2138
  )