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

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,7 +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
+ var HOST_IDENTITY_WAIT_MS = 1e4;
76
76
  function resolveVisitorId() {
77
77
  if (typeof window === "undefined") return "";
78
78
  try {
@@ -117,10 +117,10 @@ function InternalProvider({
117
117
  const sessionId = auth.sessionKey || null;
118
118
  const latestRef = (0, import_react.useRef)({ auth, visitorId, sessionId });
119
119
  latestRef.current = { auth, visitorId, sessionId };
120
- const sendEvent = (0, import_react.useCallback)((event, properties = {}) => {
120
+ const postEvent = (0, import_react.useCallback)((event, properties) => {
121
121
  const { auth: a, visitorId: v, sessionId: sid } = latestRef.current;
122
122
  if (!v) return;
123
- const { conversation_id: conversationId, ...rest } = properties;
123
+ const { conversation_id: conversationId, occurred_at: occurredAt, ...rest } = properties;
124
124
  fetch(`${a.apiBaseUrl}/v1/events`, {
125
125
  method: "POST",
126
126
  keepalive: true,
@@ -131,26 +131,45 @@ function InternalProvider({
131
131
  visitor_id: v,
132
132
  session_id: sid,
133
133
  conversation_id: conversationId ?? null,
134
- properties: { screen: a.screen, ...rest }
134
+ occurred_at: occurredAt,
135
+ properties: { screen: a.screen, ...a.eventProperties, ...rest }
135
136
  })
136
137
  }).catch(() => {
137
138
  });
138
139
  }, []);
139
- const widgetViewSentRef = (0, import_react.useRef)(false);
140
+ const eventQueueRef = (0, import_react.useRef)([]);
141
+ const identitySettledRef = (0, import_react.useRef)(false);
142
+ const flushEvents = (0, import_react.useCallback)(() => {
143
+ identitySettledRef.current = true;
144
+ for (const [event, properties] of eventQueueRef.current.splice(0)) postEvent(event, properties);
145
+ }, [postEvent]);
146
+ const sendEvent = (0, import_react.useCallback)(
147
+ (event, properties = {}) => {
148
+ const stamped = { ...properties, occurred_at: properties.occurred_at ?? (/* @__PURE__ */ new Date()).toISOString() };
149
+ if (identitySettledRef.current) postEvent(event, stamped);
150
+ else eventQueueRef.current.push([event, stamped]);
151
+ },
152
+ [postEvent]
153
+ );
154
+ const hostIdentity = auth.userKey || sessionId;
140
155
  (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();
156
+ if (hostIdentity) {
157
+ flushEvents();
149
158
  return;
150
159
  }
151
- const timer = setTimeout(send, WIDGET_VIEW_WAIT_MS);
152
- return () => clearTimeout(timer);
153
- }, [sessionId, sendEvent]);
160
+ const timer = setTimeout(flushEvents, HOST_IDENTITY_WAIT_MS);
161
+ window.addEventListener("pagehide", flushEvents);
162
+ return () => {
163
+ clearTimeout(timer);
164
+ window.removeEventListener("pagehide", flushEvents);
165
+ };
166
+ }, [hostIdentity, flushEvents]);
167
+ const widgetViewSentRef = (0, import_react.useRef)(false);
168
+ (0, import_react.useEffect)(() => {
169
+ if (widgetViewSentRef.current) return;
170
+ widgetViewSentRef.current = true;
171
+ sendEvent("view_vehicle");
172
+ }, [sendEvent]);
154
173
  const value = (0, import_react.useMemo)(
155
174
  () => ({
156
175
  auth,
@@ -746,7 +765,7 @@ function XbNovaNudgeLoop({ appId }) {
746
765
  autoHideMs: messageNode.auto_hide_ms ?? 2e4,
747
766
  quickReplies: messageNode.quick_replies ?? []
748
767
  });
749
- sendEvent("nudge_impression", { nudge_node_id: messageNode.id, bot_id: botId });
768
+ sendEvent("nudge_AI_view", { nudge_node_id: messageNode.id, bot_id: botId });
750
769
  };
751
770
  const tryFireFrom = (node) => {
752
771
  const nextEdge = (edgesFrom.get(node.id) ?? [])[0];
@@ -952,6 +971,7 @@ function XbNovaProvider({
952
971
  userKey,
953
972
  threadKey,
954
973
  sessionKey,
974
+ eventProperties,
955
975
  children
956
976
  }) {
957
977
  (0, import_react6.useEffect)(() => {
@@ -967,6 +987,7 @@ function XbNovaProvider({
967
987
  (0, import_react6.useEffect)(() => {
968
988
  if (screen) bumpVisitCount(screen);
969
989
  }, [screen]);
990
+ const eventPropertiesJson = JSON.stringify(eventProperties ?? null);
970
991
  const auth = (0, import_react6.useMemo)(
971
992
  () => ({
972
993
  apiKey,
@@ -976,9 +997,10 @@ function XbNovaProvider({
976
997
  screen,
977
998
  userKey: userKey || void 0,
978
999
  threadKey: threadKey || void 0,
979
- sessionKey: sessionKey || void 0
1000
+ sessionKey: sessionKey || void 0,
1001
+ eventProperties: JSON.parse(eventPropertiesJson) ?? void 0
980
1002
  }),
981
- [apiKey, botId, appId, apiBaseUrl, screen, userKey, threadKey, sessionKey]
1003
+ [apiKey, botId, appId, apiBaseUrl, screen, userKey, threadKey, sessionKey, eventPropertiesJson]
982
1004
  );
983
1005
  const resolvedTheme = (0, import_react6.useMemo)(
984
1006
  () => ({ ...DEFAULT_THEME, ...theme }),
@@ -1222,6 +1244,10 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
1222
1244
 
1223
1245
  // src/use-chat.ts
1224
1246
  var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
1247
+ var CTA_CLICK_EVENTS = {
1248
+ suggest_consultation: { event: "apply_AI_click", ctaType: "consult" },
1249
+ suggest_finance_limit: { event: "finance_AI_click", ctaType: "finance" }
1250
+ };
1225
1251
  function useXbNovaChat(options) {
1226
1252
  const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId, sessionId, sendEvent } = useXbNovaInternal();
1227
1253
  const botId = options?.botId ?? auth.botId;
@@ -1233,7 +1259,6 @@ function useXbNovaChat(options) {
1233
1259
  const [resumed, setResumed] = (0, import_react8.useState)(false);
1234
1260
  const [sentOpening, setSentOpening] = (0, import_react8.useState)(null);
1235
1261
  const conversationIdRef = (0, import_react8.useRef)(null);
1236
- const pendingOpeningRef = (0, import_react8.useRef)(null);
1237
1262
  const messagesRef = (0, import_react8.useRef)(messages);
1238
1263
  messagesRef.current = messages;
1239
1264
  const [busy, setBusy] = (0, import_react8.useState)(false);
@@ -1256,7 +1281,6 @@ function useXbNovaChat(options) {
1256
1281
  setFeedbackState({});
1257
1282
  dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
1258
1283
  conversationIdRef.current = null;
1259
- pendingOpeningRef.current = null;
1260
1284
  fetch(`${apiBaseUrl}/v1/conversations`, {
1261
1285
  method: "POST",
1262
1286
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
@@ -1289,16 +1313,6 @@ function useXbNovaChat(options) {
1289
1313
  cancelled = true;
1290
1314
  };
1291
1315
  }, [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]);
1302
1316
  const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
1303
1317
  const next = [...prev];
1304
1318
  next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
@@ -1338,13 +1352,14 @@ function useXbNovaChat(options) {
1338
1352
  apply(prevRating);
1339
1353
  }
1340
1354
  }
1341
- function postClientActionEvent(kind, action, label) {
1355
+ function postClientActionEvent(event, action, label, ctaType) {
1342
1356
  const conversationId = conversationIdRef.current;
1343
1357
  if (!conversationId) return;
1344
- sendEvent(kind, { conversation_id: conversationId, action, label });
1358
+ sendEvent(event, { conversation_id: conversationId, action, label, cta_type: ctaType });
1345
1359
  }
1346
1360
  function trackClientActionClick(action, label) {
1347
- postClientActionEvent("cta_click", action, label);
1361
+ const cta = CTA_CLICK_EVENTS[action];
1362
+ postClientActionEvent(cta?.event ?? "cta_click", action, label, cta?.ctaType);
1348
1363
  }
1349
1364
  async function runClientAction(spec, part, existingParts) {
1350
1365
  if (spec.dismissSheet) options?.onDismissSheet?.();
@@ -1417,7 +1432,7 @@ function useXbNovaChat(options) {
1417
1432
  void runClientAction(spec, part, last.parts);
1418
1433
  }
1419
1434
  }, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
1420
- async function send(text) {
1435
+ async function send(text, { nudgeOpening, inputType = "typed" } = {}) {
1421
1436
  const trimmed = text.trim();
1422
1437
  if (!trimmed || busy) return;
1423
1438
  sendEvent("message_send", {
@@ -1425,10 +1440,19 @@ function useXbNovaChat(options) {
1425
1440
  is_first: messages.length === 0,
1426
1441
  length: trimmed.length
1427
1442
  });
1443
+ let firstQuestionPending = messages.length === 0;
1444
+ const reportFirstQuestion = (conversationId) => {
1445
+ if (!firstQuestionPending) return;
1446
+ firstQuestionPending = false;
1447
+ sendEvent("question_AI_first", { conversation_id: conversationId, input_type: inputType });
1448
+ };
1449
+ if (conversationIdRef.current) reportFirstQuestion(conversationIdRef.current);
1428
1450
  setError(null);
1429
1451
  setBusy(true);
1452
+ const resumedOpening = messages.length > 0 && nudgeOpening ? nudgeOpening : null;
1430
1453
  setMessages((prev) => [
1431
1454
  ...prev,
1455
+ ...resumedOpening ? [{ role: "assistant", parts: [{ kind: "text", id: `nudge:${prev.length}`, text: resumedOpening }] }] : [],
1432
1456
  { role: "user", parts: [{ kind: "text", id: "u", text: trimmed }] },
1433
1457
  { role: "assistant", parts: [] }
1434
1458
  ]);
@@ -1441,14 +1465,13 @@ function useXbNovaChat(options) {
1441
1465
  const notes = collectContextEntries(contextRegistry);
1442
1466
  const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
1443
1467
  const context = focused || notes.length ? { ...screen, ...notes.length ? { notes } : {} } : void 0;
1444
- let openingMessage2;
1468
+ let openingMessage;
1445
1469
  if (messages.length === 0) {
1446
- openingMessage2 = (options?.openingMessage ?? onboarding?.greeting) || null;
1447
- setSentOpening(openingMessage2);
1470
+ openingMessage = (options?.openingMessage ?? onboarding?.greeting) || null;
1471
+ setSentOpening(openingMessage);
1448
1472
  } else {
1449
- openingMessage2 = pendingOpeningRef.current;
1473
+ openingMessage = resumedOpening;
1450
1474
  }
1451
- pendingOpeningRef.current = null;
1452
1475
  try {
1453
1476
  await streamChat(
1454
1477
  apiBaseUrl,
@@ -1459,7 +1482,7 @@ function useXbNovaChat(options) {
1459
1482
  conversation_id: conversationIdRef.current,
1460
1483
  context,
1461
1484
  nudge_node_id: nudgeNodeId ?? null,
1462
- opening_message: openingMessage2,
1485
+ opening_message: openingMessage,
1463
1486
  visitor_id: userKey || null,
1464
1487
  thread_key: threadKey ?? null,
1465
1488
  session_id: sessionId
@@ -1469,6 +1492,7 @@ function useXbNovaChat(options) {
1469
1492
  onMessageId: setLastAssistantId,
1470
1493
  onConversationId: (id) => {
1471
1494
  conversationIdRef.current = id;
1495
+ reportFirstQuestion(id);
1472
1496
  },
1473
1497
  onError: setError
1474
1498
  }
@@ -1479,7 +1503,7 @@ function useXbNovaChat(options) {
1479
1503
  setBusy(false);
1480
1504
  }
1481
1505
  }
1482
- const opening = resumed ? null : sentOpening ?? options?.openingMessage ?? onboarding?.greeting ?? null;
1506
+ const opening = resumed ? null : messages.length > 0 ? sentOpening : options?.openingMessage ?? onboarding?.greeting ?? null;
1483
1507
  return {
1484
1508
  onboarding,
1485
1509
  opening,
@@ -1779,6 +1803,7 @@ function XbNovaChat({
1779
1803
  const [activeBotId, setActiveBotId] = (0, import_react9.useState)(void 0);
1780
1804
  const [activeNudgeNodeId, setActiveNudgeNodeId] = (0, import_react9.useState)(void 0);
1781
1805
  const [nudgeQuickReplies, setNudgeQuickReplies] = (0, import_react9.useState)([]);
1806
+ const [nudgePrompt, setNudgePrompt] = (0, import_react9.useState)(null);
1782
1807
  const [openingLine, setOpeningLine] = (0, import_react9.useState)(null);
1783
1808
  const [showDisclosure, setShowDisclosure] = (0, import_react9.useState)(false);
1784
1809
  const [input, setInput] = (0, import_react9.useState)("");
@@ -1848,10 +1873,13 @@ function XbNovaChat({
1848
1873
  setOpeningLine(consumed.text);
1849
1874
  setActiveNudgeNodeId(consumed.messageNodeId);
1850
1875
  setNudgeQuickReplies(consumed.quickReplies);
1851
- sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1876
+ setNudgePrompt(consumed.text);
1877
+ sendEvent("nudge_AI_click", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1852
1878
  } else {
1853
1879
  setNudgeQuickReplies([]);
1880
+ setNudgePrompt(null);
1854
1881
  setActiveNudgeNodeId(void 0);
1882
+ sendEvent("chat_AI_open", { source: "direct" });
1855
1883
  const focused = getFocusedTrackEntry(trackRegistry);
1856
1884
  setOpeningLine(
1857
1885
  focused ? `${focused.label ?? focused.region}\uC744(\uB97C) \uBCF4\uACE0 \uACC4\uC2DC\uB124\uC694. \uAD81\uAE08\uD558\uC2E0 \uC810\uC744 \uD3B8\uD558\uAC8C \uBB3C\uC5B4\uBCF4\uC138\uC694.` : null
@@ -1862,7 +1890,6 @@ function XbNovaChat({
1862
1890
  markDisclosureSeen();
1863
1891
  }
1864
1892
  setOpen(true);
1865
- sendEvent("chat_open", { from_nudge: Boolean(consumed) });
1866
1893
  try {
1867
1894
  onOpen?.({ fromNudge: Boolean(consumed), hintText: consumed?.text });
1868
1895
  } catch (err) {
@@ -2072,26 +2099,31 @@ function XbNovaChat({
2072
2099
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: bottomRef })
2073
2100
  ] }),
2074
2101
  (() => {
2075
- if (nudgeQuickReplies.length > 0) {
2076
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "flex flex-wrap gap-1.5 border-t border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700", children: nudgeQuickReplies.map((q) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2077
- "button",
2078
- {
2079
- onClick: () => {
2080
- setNudgeQuickReplies([]);
2081
- send(q);
2102
+ const prompt = messages.length > 0 ? nudgePrompt : null;
2103
+ if (prompt || nudgeQuickReplies.length > 0) {
2104
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "space-y-2 border-t border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700", children: [
2105
+ prompt ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "text-sm font-medium", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssistantMarkdown, { text: prompt }) }) : null,
2106
+ nudgeQuickReplies.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "flex flex-wrap gap-1.5", children: nudgeQuickReplies.map((q) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2107
+ "button",
2108
+ {
2109
+ onClick: () => {
2110
+ setNudgeQuickReplies([]);
2111
+ setNudgePrompt(null);
2112
+ send(q, { nudgeOpening: prompt, inputType: "quick_reply" });
2113
+ },
2114
+ className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2115
+ children: q
2082
2116
  },
2083
- className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2084
- children: q
2085
- },
2086
- q
2087
- )) });
2117
+ q
2118
+ )) }) : null
2119
+ ] });
2088
2120
  }
2089
2121
  const quickReplies = onboarding?.quick_replies ?? [];
2090
2122
  if (messages.length !== 0 || quickReplies.length === 0) return null;
2091
2123
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "flex flex-wrap gap-1.5 border-t border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700", children: quickReplies.map((q) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2092
2124
  "button",
2093
2125
  {
2094
- onClick: () => send(q),
2126
+ onClick: () => send(q, { inputType: "quick_reply" }),
2095
2127
  className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2096
2128
  children: q
2097
2129
  },
@@ -2104,7 +2136,8 @@ function XbNovaChat({
2104
2136
  onSubmit: (e) => {
2105
2137
  e.preventDefault();
2106
2138
  setNudgeQuickReplies([]);
2107
- send(input);
2139
+ setNudgePrompt(null);
2140
+ send(input, { nudgeOpening: messages.length > 0 ? nudgePrompt : null });
2108
2141
  setInput("");
2109
2142
  },
2110
2143
  className: "flex items-center gap-2 border-t border-neutral-200 px-3 py-2.5 dark:border-neutral-700",