@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.d.cts CHANGED
@@ -107,7 +107,7 @@ type XbNovaTheme = {
107
107
  colorScheme: "light" | "dark" | "system";
108
108
  };
109
109
 
110
- declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clientActions, theme, userKey, threadKey, sessionKey, children, }: {
110
+ declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clientActions, theme, userKey, threadKey, sessionKey, eventProperties, children, }: {
111
111
  apiKey: string;
112
112
  botId: string;
113
113
  appId: string;
@@ -123,6 +123,8 @@ declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clie
123
123
  /** 이 방문의 세션 키(예: GA4 session_id). nova가 남기는 이벤트(위젯 노출·넛지·상담 버튼)와 대화에 실려
124
124
  * 콘솔에서 세션 단위로 집계된다. 값이 바뀌면(세션 갱신) 그 뒤 이벤트부터 새 값으로 실린다. 안 주면 세션은 비운다. */
125
125
  sessionKey?: string;
126
+ /** 모든 분석 이벤트에 함께 실을 값(예: `{ vehicle_id: data.id }`). 평평한 원시값만 넣고 개인정보는 넣지 않는다. */
127
+ eventProperties?: Record<string, string | number | boolean | null | undefined>;
126
128
  /** 이 페이지가 AI에게 내어줄 client action tool — 이름별로 설명·스키마·실행 함수를
127
129
  * 등록한다("SDK 초기화 시점에 등록"). 스펙은 App 레벨 레지스트리로 자동 동기화되고
128
130
  * (`XbNovaClientActionSync`), 콘솔의 tool 목록에 반영된다. 핸들러는 여기서만 산다. */
@@ -172,7 +174,10 @@ type UseXbNovaChatResult = {
172
174
  busy: boolean;
173
175
  error: string | null;
174
176
  /** `nudgeOpening`은 대화 중 넛지를 눌러 띄운 질문이다. 답을 보낼 때 그 앞에 AI 발화로 남긴다. */
175
- send: (text: string, nudgeOpening?: string | null) => Promise<void>;
177
+ send: (text: string, options?: {
178
+ nudgeOpening?: string | null;
179
+ inputType?: "typed" | "quick_reply";
180
+ }) => Promise<void>;
176
181
  /** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */
177
182
  clientResults: ClientResults;
178
183
  /** messageId → 현재 평가. 평가 안 한 메시지는 키가 없다. */
package/dist/index.d.ts CHANGED
@@ -107,7 +107,7 @@ type XbNovaTheme = {
107
107
  colorScheme: "light" | "dark" | "system";
108
108
  };
109
109
 
110
- declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clientActions, theme, userKey, threadKey, sessionKey, children, }: {
110
+ declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clientActions, theme, userKey, threadKey, sessionKey, eventProperties, children, }: {
111
111
  apiKey: string;
112
112
  botId: string;
113
113
  appId: string;
@@ -123,6 +123,8 @@ declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clie
123
123
  /** 이 방문의 세션 키(예: GA4 session_id). nova가 남기는 이벤트(위젯 노출·넛지·상담 버튼)와 대화에 실려
124
124
  * 콘솔에서 세션 단위로 집계된다. 값이 바뀌면(세션 갱신) 그 뒤 이벤트부터 새 값으로 실린다. 안 주면 세션은 비운다. */
125
125
  sessionKey?: string;
126
+ /** 모든 분석 이벤트에 함께 실을 값(예: `{ vehicle_id: data.id }`). 평평한 원시값만 넣고 개인정보는 넣지 않는다. */
127
+ eventProperties?: Record<string, string | number | boolean | null | undefined>;
126
128
  /** 이 페이지가 AI에게 내어줄 client action tool — 이름별로 설명·스키마·실행 함수를
127
129
  * 등록한다("SDK 초기화 시점에 등록"). 스펙은 App 레벨 레지스트리로 자동 동기화되고
128
130
  * (`XbNovaClientActionSync`), 콘솔의 tool 목록에 반영된다. 핸들러는 여기서만 산다. */
@@ -172,7 +174,10 @@ type UseXbNovaChatResult = {
172
174
  busy: boolean;
173
175
  error: string | null;
174
176
  /** `nudgeOpening`은 대화 중 넛지를 눌러 띄운 질문이다. 답을 보낼 때 그 앞에 AI 발화로 남긴다. */
175
- send: (text: string, nudgeOpening?: string | null) => Promise<void>;
177
+ send: (text: string, options?: {
178
+ nudgeOpening?: string | null;
179
+ inputType?: "typed" | "quick_reply";
180
+ }) => Promise<void>;
176
181
  /** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */
177
182
  clientResults: ClientResults;
178
183
  /** messageId → 현재 평가. 평가 안 한 메시지는 키가 없다. */
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ function useXbNovaInternal() {
25
25
  return ctx;
26
26
  }
27
27
  var VISITOR_STORAGE_KEY = "xbnova_visitor_id";
28
- var WIDGET_VIEW_WAIT_MS = 1e4;
28
+ var HOST_IDENTITY_WAIT_MS = 1e4;
29
29
  function resolveVisitorId() {
30
30
  if (typeof window === "undefined") return "";
31
31
  try {
@@ -70,7 +70,7 @@ function InternalProvider({
70
70
  const sessionId = auth.sessionKey || null;
71
71
  const latestRef = useRef({ auth, visitorId, sessionId });
72
72
  latestRef.current = { auth, visitorId, sessionId };
73
- const sendEvent = useCallback((event, properties = {}) => {
73
+ const postEvent = useCallback((event, properties) => {
74
74
  const { auth: a, visitorId: v, sessionId: sid } = latestRef.current;
75
75
  if (!v) return;
76
76
  const { conversation_id: conversationId, occurred_at: occurredAt, ...rest } = properties;
@@ -84,28 +84,45 @@ function InternalProvider({
84
84
  visitor_id: v,
85
85
  session_id: sid,
86
86
  conversation_id: conversationId ?? null,
87
- occurred_at: occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
88
- properties: { screen: a.screen, ...rest }
87
+ occurred_at: occurredAt,
88
+ properties: { screen: a.screen, ...a.eventProperties, ...rest }
89
89
  })
90
90
  }).catch(() => {
91
91
  });
92
92
  }, []);
93
- const widgetViewSentRef = useRef(false);
94
- const mountedAtRef = useRef((/* @__PURE__ */ new Date()).toISOString());
93
+ const eventQueueRef = useRef([]);
94
+ const identitySettledRef = useRef(false);
95
+ const flushEvents = useCallback(() => {
96
+ identitySettledRef.current = true;
97
+ for (const [event, properties] of eventQueueRef.current.splice(0)) postEvent(event, properties);
98
+ }, [postEvent]);
99
+ const sendEvent = useCallback(
100
+ (event, properties = {}) => {
101
+ const stamped = { ...properties, occurred_at: properties.occurred_at ?? (/* @__PURE__ */ new Date()).toISOString() };
102
+ if (identitySettledRef.current) postEvent(event, stamped);
103
+ else eventQueueRef.current.push([event, stamped]);
104
+ },
105
+ [postEvent]
106
+ );
107
+ const hostIdentity = auth.userKey || sessionId;
95
108
  useEffect(() => {
96
- if (widgetViewSentRef.current) return;
97
- const send = () => {
98
- if (widgetViewSentRef.current) return;
99
- widgetViewSentRef.current = true;
100
- sendEvent("widget_view", { occurred_at: mountedAtRef.current });
101
- };
102
- if (sessionId) {
103
- send();
109
+ if (hostIdentity) {
110
+ flushEvents();
104
111
  return;
105
112
  }
106
- const timer = setTimeout(send, WIDGET_VIEW_WAIT_MS);
107
- return () => clearTimeout(timer);
108
- }, [sessionId, sendEvent]);
113
+ const timer = setTimeout(flushEvents, HOST_IDENTITY_WAIT_MS);
114
+ window.addEventListener("pagehide", flushEvents);
115
+ return () => {
116
+ clearTimeout(timer);
117
+ window.removeEventListener("pagehide", flushEvents);
118
+ };
119
+ }, [hostIdentity, flushEvents]);
120
+ const widgetViewSentRef = useRef(false);
121
+ useEffect(() => {
122
+ if (widgetViewSentRef.current) return;
123
+ widgetViewSentRef.current = true;
124
+ sendEvent("view_vehicle");
125
+ }, [sendEvent]);
109
126
  const value = useMemo(
110
127
  () => ({
111
128
  auth,
@@ -701,7 +718,7 @@ function XbNovaNudgeLoop({ appId }) {
701
718
  autoHideMs: messageNode.auto_hide_ms ?? 2e4,
702
719
  quickReplies: messageNode.quick_replies ?? []
703
720
  });
704
- sendEvent("nudge_impression", { nudge_node_id: messageNode.id, bot_id: botId });
721
+ sendEvent("nudge_AI_view", { nudge_node_id: messageNode.id, bot_id: botId });
705
722
  };
706
723
  const tryFireFrom = (node) => {
707
724
  const nextEdge = (edgesFrom.get(node.id) ?? [])[0];
@@ -907,6 +924,7 @@ function XbNovaProvider({
907
924
  userKey,
908
925
  threadKey,
909
926
  sessionKey,
927
+ eventProperties,
910
928
  children
911
929
  }) {
912
930
  useEffect6(() => {
@@ -922,6 +940,7 @@ function XbNovaProvider({
922
940
  useEffect6(() => {
923
941
  if (screen) bumpVisitCount(screen);
924
942
  }, [screen]);
943
+ const eventPropertiesJson = JSON.stringify(eventProperties ?? null);
925
944
  const auth = useMemo2(
926
945
  () => ({
927
946
  apiKey,
@@ -931,9 +950,10 @@ function XbNovaProvider({
931
950
  screen,
932
951
  userKey: userKey || void 0,
933
952
  threadKey: threadKey || void 0,
934
- sessionKey: sessionKey || void 0
953
+ sessionKey: sessionKey || void 0,
954
+ eventProperties: JSON.parse(eventPropertiesJson) ?? void 0
935
955
  }),
936
- [apiKey, botId, appId, apiBaseUrl, screen, userKey, threadKey, sessionKey]
956
+ [apiKey, botId, appId, apiBaseUrl, screen, userKey, threadKey, sessionKey, eventPropertiesJson]
937
957
  );
938
958
  const resolvedTheme = useMemo2(
939
959
  () => ({ ...DEFAULT_THEME, ...theme }),
@@ -1176,6 +1196,10 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
1176
1196
 
1177
1197
  // src/use-chat.ts
1178
1198
  var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
1199
+ var CTA_CLICK_EVENTS = {
1200
+ suggest_consultation: { event: "apply_AI_click", ctaType: "consult" },
1201
+ suggest_finance_limit: { event: "finance_AI_click", ctaType: "finance" }
1202
+ };
1179
1203
  function useXbNovaChat(options) {
1180
1204
  const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId, sessionId, sendEvent } = useXbNovaInternal();
1181
1205
  const botId = options?.botId ?? auth.botId;
@@ -1280,13 +1304,14 @@ function useXbNovaChat(options) {
1280
1304
  apply(prevRating);
1281
1305
  }
1282
1306
  }
1283
- function postClientActionEvent(kind, action, label) {
1307
+ function postClientActionEvent(event, action, label, ctaType) {
1284
1308
  const conversationId = conversationIdRef.current;
1285
1309
  if (!conversationId) return;
1286
- sendEvent(kind, { conversation_id: conversationId, action, label });
1310
+ sendEvent(event, { conversation_id: conversationId, action, label, cta_type: ctaType });
1287
1311
  }
1288
1312
  function trackClientActionClick(action, label) {
1289
- postClientActionEvent("cta_click", action, label);
1313
+ const cta = CTA_CLICK_EVENTS[action];
1314
+ postClientActionEvent(cta?.event ?? "cta_click", action, label, cta?.ctaType);
1290
1315
  }
1291
1316
  async function runClientAction(spec, part, existingParts) {
1292
1317
  if (spec.dismissSheet) options?.onDismissSheet?.();
@@ -1359,7 +1384,7 @@ function useXbNovaChat(options) {
1359
1384
  void runClientAction(spec, part, last.parts);
1360
1385
  }
1361
1386
  }, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
1362
- async function send(text, nudgeOpening) {
1387
+ async function send(text, { nudgeOpening, inputType = "typed" } = {}) {
1363
1388
  const trimmed = text.trim();
1364
1389
  if (!trimmed || busy) return;
1365
1390
  sendEvent("message_send", {
@@ -1367,6 +1392,13 @@ function useXbNovaChat(options) {
1367
1392
  is_first: messages.length === 0,
1368
1393
  length: trimmed.length
1369
1394
  });
1395
+ let firstQuestionPending = messages.length === 0;
1396
+ const reportFirstQuestion = (conversationId) => {
1397
+ if (!firstQuestionPending) return;
1398
+ firstQuestionPending = false;
1399
+ sendEvent("question_AI_first", { conversation_id: conversationId, input_type: inputType });
1400
+ };
1401
+ if (conversationIdRef.current) reportFirstQuestion(conversationIdRef.current);
1370
1402
  setError(null);
1371
1403
  setBusy(true);
1372
1404
  const resumedOpening = messages.length > 0 && nudgeOpening ? nudgeOpening : null;
@@ -1412,6 +1444,7 @@ function useXbNovaChat(options) {
1412
1444
  onMessageId: setLastAssistantId,
1413
1445
  onConversationId: (id) => {
1414
1446
  conversationIdRef.current = id;
1447
+ reportFirstQuestion(id);
1415
1448
  },
1416
1449
  onError: setError
1417
1450
  }
@@ -1793,11 +1826,12 @@ function XbNovaChat({
1793
1826
  setActiveNudgeNodeId(consumed.messageNodeId);
1794
1827
  setNudgeQuickReplies(consumed.quickReplies);
1795
1828
  setNudgePrompt(consumed.text);
1796
- sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1829
+ sendEvent("nudge_AI_click", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1797
1830
  } else {
1798
1831
  setNudgeQuickReplies([]);
1799
1832
  setNudgePrompt(null);
1800
1833
  setActiveNudgeNodeId(void 0);
1834
+ sendEvent("chat_AI_open", { source: "direct" });
1801
1835
  const focused = getFocusedTrackEntry(trackRegistry);
1802
1836
  setOpeningLine(
1803
1837
  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
@@ -1808,7 +1842,6 @@ function XbNovaChat({
1808
1842
  markDisclosureSeen();
1809
1843
  }
1810
1844
  setOpen(true);
1811
- sendEvent("chat_open", { from_nudge: Boolean(consumed) });
1812
1845
  try {
1813
1846
  onOpen?.({ fromNudge: Boolean(consumed), hintText: consumed?.text });
1814
1847
  } catch (err) {
@@ -2028,7 +2061,7 @@ function XbNovaChat({
2028
2061
  onClick: () => {
2029
2062
  setNudgeQuickReplies([]);
2030
2063
  setNudgePrompt(null);
2031
- send(q, prompt);
2064
+ send(q, { nudgeOpening: prompt, inputType: "quick_reply" });
2032
2065
  },
2033
2066
  className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2034
2067
  children: q
@@ -2042,7 +2075,7 @@ function XbNovaChat({
2042
2075
  return /* @__PURE__ */ jsx7("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__ */ jsx7(
2043
2076
  "button",
2044
2077
  {
2045
- onClick: () => send(q),
2078
+ onClick: () => send(q, { inputType: "quick_reply" }),
2046
2079
  className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2047
2080
  children: q
2048
2081
  },
@@ -2056,7 +2089,7 @@ function XbNovaChat({
2056
2089
  e.preventDefault();
2057
2090
  setNudgeQuickReplies([]);
2058
2091
  setNudgePrompt(null);
2059
- send(input, messages.length > 0 ? nudgePrompt : null);
2092
+ send(input, { nudgeOpening: messages.length > 0 ? nudgePrompt : null });
2060
2093
  setInput("");
2061
2094
  },
2062
2095
  className: "flex items-center gap-2 border-t border-neutral-200 px-3 py-2.5 dark:border-neutral-700",