@xbrain-swe/nova-chat-widget 0.3.0-dev.17 → 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,7 +117,7 @@ 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
123
  const { conversation_id: conversationId, occurred_at: occurredAt, ...rest } = properties;
@@ -131,28 +131,45 @@ function InternalProvider({
131
131
  visitor_id: v,
132
132
  session_id: sid,
133
133
  conversation_id: conversationId ?? null,
134
- occurred_at: occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
135
- properties: { screen: a.screen, ...rest }
134
+ occurred_at: occurredAt,
135
+ properties: { screen: a.screen, ...a.eventProperties, ...rest }
136
136
  })
137
137
  }).catch(() => {
138
138
  });
139
139
  }, []);
140
- const widgetViewSentRef = (0, import_react.useRef)(false);
141
- const mountedAtRef = (0, import_react.useRef)((/* @__PURE__ */ new Date()).toISOString());
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;
142
155
  (0, import_react.useEffect)(() => {
143
- if (widgetViewSentRef.current) return;
144
- const send = () => {
145
- if (widgetViewSentRef.current) return;
146
- widgetViewSentRef.current = true;
147
- sendEvent("widget_view", { occurred_at: mountedAtRef.current });
148
- };
149
- if (sessionId) {
150
- send();
156
+ if (hostIdentity) {
157
+ flushEvents();
151
158
  return;
152
159
  }
153
- const timer = setTimeout(send, WIDGET_VIEW_WAIT_MS);
154
- return () => clearTimeout(timer);
155
- }, [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]);
156
173
  const value = (0, import_react.useMemo)(
157
174
  () => ({
158
175
  auth,
@@ -748,7 +765,7 @@ function XbNovaNudgeLoop({ appId }) {
748
765
  autoHideMs: messageNode.auto_hide_ms ?? 2e4,
749
766
  quickReplies: messageNode.quick_replies ?? []
750
767
  });
751
- sendEvent("nudge_impression", { nudge_node_id: messageNode.id, bot_id: botId });
768
+ sendEvent("nudge_AI_view", { nudge_node_id: messageNode.id, bot_id: botId });
752
769
  };
753
770
  const tryFireFrom = (node) => {
754
771
  const nextEdge = (edgesFrom.get(node.id) ?? [])[0];
@@ -954,6 +971,7 @@ function XbNovaProvider({
954
971
  userKey,
955
972
  threadKey,
956
973
  sessionKey,
974
+ eventProperties,
957
975
  children
958
976
  }) {
959
977
  (0, import_react6.useEffect)(() => {
@@ -969,6 +987,7 @@ function XbNovaProvider({
969
987
  (0, import_react6.useEffect)(() => {
970
988
  if (screen) bumpVisitCount(screen);
971
989
  }, [screen]);
990
+ const eventPropertiesJson = JSON.stringify(eventProperties ?? null);
972
991
  const auth = (0, import_react6.useMemo)(
973
992
  () => ({
974
993
  apiKey,
@@ -978,9 +997,10 @@ function XbNovaProvider({
978
997
  screen,
979
998
  userKey: userKey || void 0,
980
999
  threadKey: threadKey || void 0,
981
- sessionKey: sessionKey || void 0
1000
+ sessionKey: sessionKey || void 0,
1001
+ eventProperties: JSON.parse(eventPropertiesJson) ?? void 0
982
1002
  }),
983
- [apiKey, botId, appId, apiBaseUrl, screen, userKey, threadKey, sessionKey]
1003
+ [apiKey, botId, appId, apiBaseUrl, screen, userKey, threadKey, sessionKey, eventPropertiesJson]
984
1004
  );
985
1005
  const resolvedTheme = (0, import_react6.useMemo)(
986
1006
  () => ({ ...DEFAULT_THEME, ...theme }),
@@ -1224,6 +1244,10 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
1224
1244
 
1225
1245
  // src/use-chat.ts
1226
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
+ };
1227
1251
  function useXbNovaChat(options) {
1228
1252
  const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId, sessionId, sendEvent } = useXbNovaInternal();
1229
1253
  const botId = options?.botId ?? auth.botId;
@@ -1328,13 +1352,14 @@ function useXbNovaChat(options) {
1328
1352
  apply(prevRating);
1329
1353
  }
1330
1354
  }
1331
- function postClientActionEvent(kind, action, label) {
1355
+ function postClientActionEvent(event, action, label, ctaType) {
1332
1356
  const conversationId = conversationIdRef.current;
1333
1357
  if (!conversationId) return;
1334
- sendEvent(kind, { conversation_id: conversationId, action, label });
1358
+ sendEvent(event, { conversation_id: conversationId, action, label, cta_type: ctaType });
1335
1359
  }
1336
1360
  function trackClientActionClick(action, label) {
1337
- postClientActionEvent("cta_click", action, label);
1361
+ const cta = CTA_CLICK_EVENTS[action];
1362
+ postClientActionEvent(cta?.event ?? "cta_click", action, label, cta?.ctaType);
1338
1363
  }
1339
1364
  async function runClientAction(spec, part, existingParts) {
1340
1365
  if (spec.dismissSheet) options?.onDismissSheet?.();
@@ -1407,7 +1432,7 @@ function useXbNovaChat(options) {
1407
1432
  void runClientAction(spec, part, last.parts);
1408
1433
  }
1409
1434
  }, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
1410
- async function send(text, nudgeOpening) {
1435
+ async function send(text, { nudgeOpening, inputType = "typed" } = {}) {
1411
1436
  const trimmed = text.trim();
1412
1437
  if (!trimmed || busy) return;
1413
1438
  sendEvent("message_send", {
@@ -1415,6 +1440,13 @@ function useXbNovaChat(options) {
1415
1440
  is_first: messages.length === 0,
1416
1441
  length: trimmed.length
1417
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);
1418
1450
  setError(null);
1419
1451
  setBusy(true);
1420
1452
  const resumedOpening = messages.length > 0 && nudgeOpening ? nudgeOpening : null;
@@ -1460,6 +1492,7 @@ function useXbNovaChat(options) {
1460
1492
  onMessageId: setLastAssistantId,
1461
1493
  onConversationId: (id) => {
1462
1494
  conversationIdRef.current = id;
1495
+ reportFirstQuestion(id);
1463
1496
  },
1464
1497
  onError: setError
1465
1498
  }
@@ -1841,11 +1874,12 @@ function XbNovaChat({
1841
1874
  setActiveNudgeNodeId(consumed.messageNodeId);
1842
1875
  setNudgeQuickReplies(consumed.quickReplies);
1843
1876
  setNudgePrompt(consumed.text);
1844
- sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1877
+ sendEvent("nudge_AI_click", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1845
1878
  } else {
1846
1879
  setNudgeQuickReplies([]);
1847
1880
  setNudgePrompt(null);
1848
1881
  setActiveNudgeNodeId(void 0);
1882
+ sendEvent("chat_AI_open", { source: "direct" });
1849
1883
  const focused = getFocusedTrackEntry(trackRegistry);
1850
1884
  setOpeningLine(
1851
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
@@ -1856,7 +1890,6 @@ function XbNovaChat({
1856
1890
  markDisclosureSeen();
1857
1891
  }
1858
1892
  setOpen(true);
1859
- sendEvent("chat_open", { from_nudge: Boolean(consumed) });
1860
1893
  try {
1861
1894
  onOpen?.({ fromNudge: Boolean(consumed), hintText: consumed?.text });
1862
1895
  } catch (err) {
@@ -2076,7 +2109,7 @@ function XbNovaChat({
2076
2109
  onClick: () => {
2077
2110
  setNudgeQuickReplies([]);
2078
2111
  setNudgePrompt(null);
2079
- send(q, prompt);
2112
+ send(q, { nudgeOpening: prompt, inputType: "quick_reply" });
2080
2113
  },
2081
2114
  className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2082
2115
  children: q
@@ -2090,7 +2123,7 @@ function XbNovaChat({
2090
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)(
2091
2124
  "button",
2092
2125
  {
2093
- onClick: () => send(q),
2126
+ onClick: () => send(q, { inputType: "quick_reply" }),
2094
2127
  className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2095
2128
  children: q
2096
2129
  },
@@ -2104,7 +2137,7 @@ function XbNovaChat({
2104
2137
  e.preventDefault();
2105
2138
  setNudgeQuickReplies([]);
2106
2139
  setNudgePrompt(null);
2107
- send(input, messages.length > 0 ? nudgePrompt : 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",