@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 +62 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +62 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
94
|
-
const
|
|
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 (
|
|
97
|
-
|
|
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(
|
|
107
|
-
|
|
108
|
-
|
|
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("
|
|
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(
|
|
1307
|
+
function postClientActionEvent(event, action, label, ctaType) {
|
|
1284
1308
|
const conversationId = conversationIdRef.current;
|
|
1285
1309
|
if (!conversationId) return;
|
|
1286
|
-
sendEvent(
|
|
1310
|
+
sendEvent(event, { conversation_id: conversationId, action, label, cta_type: ctaType });
|
|
1287
1311
|
}
|
|
1288
1312
|
function trackClientActionClick(action, label) {
|
|
1289
|
-
|
|
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("
|
|
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",
|