@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 +91 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.js +91 -58
- package/dist/index.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
134
|
+
occurred_at: occurredAt,
|
|
135
|
+
properties: { screen: a.screen, ...a.eventProperties, ...rest }
|
|
135
136
|
})
|
|
136
137
|
}).catch(() => {
|
|
137
138
|
});
|
|
138
139
|
}, []);
|
|
139
|
-
const
|
|
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 (
|
|
142
|
-
|
|
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(
|
|
152
|
-
|
|
153
|
-
|
|
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("
|
|
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(
|
|
1355
|
+
function postClientActionEvent(event, action, label, ctaType) {
|
|
1342
1356
|
const conversationId = conversationIdRef.current;
|
|
1343
1357
|
if (!conversationId) return;
|
|
1344
|
-
sendEvent(
|
|
1358
|
+
sendEvent(event, { conversation_id: conversationId, action, label, cta_type: ctaType });
|
|
1345
1359
|
}
|
|
1346
1360
|
function trackClientActionClick(action, label) {
|
|
1347
|
-
|
|
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
|
|
1468
|
+
let openingMessage;
|
|
1445
1469
|
if (messages.length === 0) {
|
|
1446
|
-
|
|
1447
|
-
setSentOpening(
|
|
1470
|
+
openingMessage = (options?.openingMessage ?? onboarding?.greeting) || null;
|
|
1471
|
+
setSentOpening(openingMessage);
|
|
1448
1472
|
} else {
|
|
1449
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
{
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
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
|
-
|
|
2084
|
-
|
|
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
|
-
|
|
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",
|