@xbrain-swe/nova-chat-widget 0.1.11 → 0.3.0-dev.16
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 +250 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -5
- package/dist/index.d.ts +42 -5
- package/dist/index.js +300 -107
- package/dist/index.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -72,6 +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
76
|
function resolveVisitorId() {
|
|
76
77
|
if (typeof window === "undefined") return "";
|
|
77
78
|
try {
|
|
@@ -112,6 +113,44 @@ function InternalProvider({
|
|
|
112
113
|
if (visitorIdRef.current === null) {
|
|
113
114
|
visitorIdRef.current = resolveVisitorId();
|
|
114
115
|
}
|
|
116
|
+
const visitorId = auth.userKey || visitorIdRef.current;
|
|
117
|
+
const sessionId = auth.sessionKey || null;
|
|
118
|
+
const latestRef = (0, import_react.useRef)({ auth, visitorId, sessionId });
|
|
119
|
+
latestRef.current = { auth, visitorId, sessionId };
|
|
120
|
+
const sendEvent = (0, import_react.useCallback)((event, properties = {}) => {
|
|
121
|
+
const { auth: a, visitorId: v, sessionId: sid } = latestRef.current;
|
|
122
|
+
if (!v) return;
|
|
123
|
+
const { conversation_id: conversationId, ...rest } = properties;
|
|
124
|
+
fetch(`${a.apiBaseUrl}/v1/events`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
keepalive: true,
|
|
127
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${a.apiKey}` },
|
|
128
|
+
body: JSON.stringify({
|
|
129
|
+
app_id: a.appId,
|
|
130
|
+
event,
|
|
131
|
+
visitor_id: v,
|
|
132
|
+
session_id: sid,
|
|
133
|
+
conversation_id: conversationId ?? null,
|
|
134
|
+
properties: { screen: a.screen, ...rest }
|
|
135
|
+
})
|
|
136
|
+
}).catch(() => {
|
|
137
|
+
});
|
|
138
|
+
}, []);
|
|
139
|
+
const widgetViewSentRef = (0, import_react.useRef)(false);
|
|
140
|
+
(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();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const timer = setTimeout(send, WIDGET_VIEW_WAIT_MS);
|
|
152
|
+
return () => clearTimeout(timer);
|
|
153
|
+
}, [sessionId, sendEvent]);
|
|
115
154
|
const value = (0, import_react.useMemo)(
|
|
116
155
|
() => ({
|
|
117
156
|
auth,
|
|
@@ -119,10 +158,12 @@ function InternalProvider({
|
|
|
119
158
|
factsRegistry,
|
|
120
159
|
contextRegistry,
|
|
121
160
|
clientActionHandlers: clientActionHandlers ?? emptyHandlers,
|
|
122
|
-
visitorId
|
|
161
|
+
visitorId,
|
|
162
|
+
sessionId,
|
|
163
|
+
sendEvent,
|
|
123
164
|
theme
|
|
124
165
|
}),
|
|
125
|
-
[auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, theme]
|
|
166
|
+
[auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, visitorId, sessionId, sendEvent, theme]
|
|
126
167
|
);
|
|
127
168
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InternalContext.Provider, { value, children });
|
|
128
169
|
}
|
|
@@ -425,7 +466,7 @@ function getFocusedTrackEntry(trackRegistry) {
|
|
|
425
466
|
}
|
|
426
467
|
|
|
427
468
|
// src/nudge-loop.tsx
|
|
428
|
-
var EVAL_INTERVAL_MS =
|
|
469
|
+
var EVAL_INTERVAL_MS = 250;
|
|
429
470
|
var XBNOVA_EVENT_NAME = "xbnova:event";
|
|
430
471
|
var SESSION_COUNT_KEY_PREFIX = "xbnova_hint_session_count:";
|
|
431
472
|
var SCROLL_SETTLE_MS = 500;
|
|
@@ -618,7 +659,7 @@ function clearDebugOverlay(refs) {
|
|
|
618
659
|
refs.bandLinesRef.current.clear();
|
|
619
660
|
}
|
|
620
661
|
function XbNovaNudgeLoop({ appId }) {
|
|
621
|
-
const { auth, trackRegistry, factsRegistry,
|
|
662
|
+
const { auth, trackRegistry, factsRegistry, sendEvent } = useXbNovaInternal();
|
|
622
663
|
const { apiKey, apiBaseUrl, screen } = auth;
|
|
623
664
|
const { hint, offerHint, autoHideHint, hintsEnabled } = useNudgeState();
|
|
624
665
|
const graphRef = (0, import_react5.useRef)(null);
|
|
@@ -651,8 +692,10 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
651
692
|
const debugEnabled = isDebugEnabled();
|
|
652
693
|
const debugRefs = { panelRef: debugPanelRef, boxesRef: debugBoxesRef, bandLinesRef: debugBandLinesRef };
|
|
653
694
|
if (!debugEnabled && debugPanelRef.current) clearDebugOverlay(debugRefs);
|
|
695
|
+
const tickDebug = debugEnabled ? (msg) => console.debug(msg) : () => {
|
|
696
|
+
};
|
|
654
697
|
if (!graph) {
|
|
655
|
-
|
|
698
|
+
tickDebug("[XbNova] nudge \uD3C9\uAC00 \uAC74\uB108\uB700: \uADF8\uB798\uD504\uB97C \uC544\uC9C1 \uBABB \uBD88\uB7EC\uC634");
|
|
656
699
|
return;
|
|
657
700
|
}
|
|
658
701
|
const canOffer = !hintRef.current && hintsEnabledRef.current;
|
|
@@ -703,18 +746,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
703
746
|
autoHideMs: messageNode.auto_hide_ms ?? 2e4,
|
|
704
747
|
quickReplies: messageNode.quick_replies ?? []
|
|
705
748
|
});
|
|
706
|
-
|
|
707
|
-
method: "POST",
|
|
708
|
-
headers: { "Content-Type": "application/json" },
|
|
709
|
-
body: JSON.stringify({
|
|
710
|
-
app_id: appId,
|
|
711
|
-
nudge_node_id: messageNode.id,
|
|
712
|
-
kind: "nudge_impression",
|
|
713
|
-
bot_id: botId,
|
|
714
|
-
visitor_id: visitorId
|
|
715
|
-
})
|
|
716
|
-
}).catch(() => {
|
|
717
|
-
});
|
|
749
|
+
sendEvent("nudge_impression", { nudge_node_id: messageNode.id, bot_id: botId });
|
|
718
750
|
};
|
|
719
751
|
const tryFireFrom = (node) => {
|
|
720
752
|
const nextEdge = (edgesFrom.get(node.id) ?? [])[0];
|
|
@@ -742,7 +774,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
742
774
|
const maxPerSession = node.max_per_session === 0 ? Infinity : node.max_per_session ?? 3;
|
|
743
775
|
const sessionCount = peekSessionCount(node.id);
|
|
744
776
|
if (sessionCount >= maxPerSession) {
|
|
745
|
-
|
|
777
|
+
tickDebug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id}(${node.trigger_type}) \uAC74\uB108\uB700: \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC`);
|
|
746
778
|
if (debugEnabled && node.trigger_type === "dwell") {
|
|
747
779
|
debugLines.push(`- [${node.id}] \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC \u2014 \uB354 \uC548 \uC7BC`);
|
|
748
780
|
}
|
|
@@ -777,7 +809,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
777
809
|
overlapPx = status.overlapPx;
|
|
778
810
|
statusDetail = `rect=[top=${status.rectTop.toFixed(0)}, bottom=${status.rectBottom.toFixed(0)}] band=[top=${status.bandTop.toFixed(0)}, bottom=${status.bandBottom.toFixed(0)}] overlapPx=${overlapPx.toFixed(0)} inBand=${inBand}`;
|
|
779
811
|
}
|
|
780
|
-
|
|
812
|
+
tickDebug(
|
|
781
813
|
`[XbNova] band dwell \uD2B8\uB9AC\uAC70=${node.id} region="${region}" band%=${fromPercent}~${toPercent} | ${statusDetail}`
|
|
782
814
|
);
|
|
783
815
|
dwellRecords.push({ node, state, requiredMs, region, found, screenMatches, inBand, overlapPx });
|
|
@@ -786,7 +818,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
786
818
|
const fires = triggerFires(node, { screen, firedEventName });
|
|
787
819
|
if (!fires) continue;
|
|
788
820
|
if (!canOffer) {
|
|
789
|
-
|
|
821
|
+
tickDebug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id} \uC870\uAC74\uC740 \uCDA9\uC871\uD588\uC9C0\uB9CC \uC0C8 \uD78C\uD2B8 \uD45C\uC2DC\uB294 \uC7A0\uAE50 \uB9C9\uD798`);
|
|
790
822
|
continue;
|
|
791
823
|
}
|
|
792
824
|
if (tryFireFrom(node)) {
|
|
@@ -886,7 +918,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
886
918
|
ensureDebugPanel(debugPanelRef, debugCollapsedRef).body.textContent = debugLines.join("\n");
|
|
887
919
|
}
|
|
888
920
|
},
|
|
889
|
-
[apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry,
|
|
921
|
+
[apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, sendEvent]
|
|
890
922
|
);
|
|
891
923
|
(0, import_react5.useEffect)(() => {
|
|
892
924
|
const interval = window.setInterval(() => evaluate(null), EVAL_INTERVAL_MS);
|
|
@@ -917,6 +949,9 @@ function XbNovaProvider({
|
|
|
917
949
|
apiBaseUrl = DEFAULT_API_BASE_URL,
|
|
918
950
|
clientActions,
|
|
919
951
|
theme,
|
|
952
|
+
userKey,
|
|
953
|
+
threadKey,
|
|
954
|
+
sessionKey,
|
|
920
955
|
children
|
|
921
956
|
}) {
|
|
922
957
|
(0, import_react6.useEffect)(() => {
|
|
@@ -933,8 +968,17 @@ function XbNovaProvider({
|
|
|
933
968
|
if (screen) bumpVisitCount(screen);
|
|
934
969
|
}, [screen]);
|
|
935
970
|
const auth = (0, import_react6.useMemo)(
|
|
936
|
-
() => ({
|
|
937
|
-
|
|
971
|
+
() => ({
|
|
972
|
+
apiKey,
|
|
973
|
+
botId,
|
|
974
|
+
appId,
|
|
975
|
+
apiBaseUrl,
|
|
976
|
+
screen,
|
|
977
|
+
userKey: userKey || void 0,
|
|
978
|
+
threadKey: threadKey || void 0,
|
|
979
|
+
sessionKey: sessionKey || void 0
|
|
980
|
+
}),
|
|
981
|
+
[apiKey, botId, appId, apiBaseUrl, screen, userKey, threadKey, sessionKey]
|
|
938
982
|
);
|
|
939
983
|
const resolvedTheme = (0, import_react6.useMemo)(
|
|
940
984
|
() => ({ ...DEFAULT_THEME, ...theme }),
|
|
@@ -1040,12 +1084,41 @@ function XbNovaShadowRoot({
|
|
|
1040
1084
|
var import_react8 = require("react");
|
|
1041
1085
|
|
|
1042
1086
|
// src/stream.ts
|
|
1087
|
+
function fromStoredMessages(stored) {
|
|
1088
|
+
return stored.map((m) => ({
|
|
1089
|
+
id: m.role === "assistant" ? m.id : void 0,
|
|
1090
|
+
role: m.role === "assistant" ? "assistant" : "user",
|
|
1091
|
+
parts: m.parts.flatMap((p, i) => {
|
|
1092
|
+
const type = String(p.type ?? "");
|
|
1093
|
+
if (type === "text") {
|
|
1094
|
+
const text = typeof p.text === "string" ? p.text : "";
|
|
1095
|
+
return text ? [{ kind: "text", id: `${m.id}:${i}`, text }] : [];
|
|
1096
|
+
}
|
|
1097
|
+
if (type.startsWith("tool-")) {
|
|
1098
|
+
const meta = p.providerMetadata;
|
|
1099
|
+
return [
|
|
1100
|
+
{
|
|
1101
|
+
kind: "tool",
|
|
1102
|
+
toolCallId: String(p.toolCallId ?? `${m.id}:${i}`),
|
|
1103
|
+
toolName: type.slice("tool-".length),
|
|
1104
|
+
state: p.state === "output-error" ? "error" : "output",
|
|
1105
|
+
input: p.input,
|
|
1106
|
+
output: p.output,
|
|
1107
|
+
view: meta?.["kr.dsauto.chat"]?.view ?? null
|
|
1108
|
+
}
|
|
1109
|
+
];
|
|
1110
|
+
}
|
|
1111
|
+
return [];
|
|
1112
|
+
})
|
|
1113
|
+
}));
|
|
1114
|
+
}
|
|
1043
1115
|
async function consumeSse(response, handlers) {
|
|
1044
1116
|
if (!response.ok || !response.body) {
|
|
1045
1117
|
handlers.onError?.(`\uC694\uCCAD \uC2E4\uD328 (${response.status})`);
|
|
1046
1118
|
return null;
|
|
1047
1119
|
}
|
|
1048
1120
|
const conversationId = response.headers.get("x-conversation-id");
|
|
1121
|
+
if (conversationId) handlers.onConversationId?.(conversationId);
|
|
1049
1122
|
const reader = response.body.getReader();
|
|
1050
1123
|
const decoder = new TextDecoder();
|
|
1051
1124
|
let buffer = "";
|
|
@@ -1150,12 +1223,19 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
|
|
|
1150
1223
|
// src/use-chat.ts
|
|
1151
1224
|
var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
|
|
1152
1225
|
function useXbNovaChat(options) {
|
|
1153
|
-
const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId } = useXbNovaInternal();
|
|
1226
|
+
const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId, sessionId, sendEvent } = useXbNovaInternal();
|
|
1154
1227
|
const botId = options?.botId ?? auth.botId;
|
|
1155
1228
|
const nudgeNodeId = options?.nudgeNodeId;
|
|
1156
|
-
const { apiKey, apiBaseUrl } = auth;
|
|
1229
|
+
const { apiKey, apiBaseUrl, threadKey } = auth;
|
|
1230
|
+
const userKey = visitorId;
|
|
1157
1231
|
const [onboarding, setOnboarding] = (0, import_react8.useState)(null);
|
|
1158
1232
|
const [messages, setMessages] = (0, import_react8.useState)([]);
|
|
1233
|
+
const [resumed, setResumed] = (0, import_react8.useState)(false);
|
|
1234
|
+
const [sentOpening, setSentOpening] = (0, import_react8.useState)(null);
|
|
1235
|
+
const conversationIdRef = (0, import_react8.useRef)(null);
|
|
1236
|
+
const pendingOpeningRef = (0, import_react8.useRef)(null);
|
|
1237
|
+
const messagesRef = (0, import_react8.useRef)(messages);
|
|
1238
|
+
messagesRef.current = messages;
|
|
1159
1239
|
const [busy, setBusy] = (0, import_react8.useState)(false);
|
|
1160
1240
|
const [error, setError] = (0, import_react8.useState)(null);
|
|
1161
1241
|
const [clientResults, setClientResults] = (0, import_react8.useState)({});
|
|
@@ -1166,18 +1246,59 @@ function useXbNovaChat(options) {
|
|
|
1166
1246
|
const nudgeNodeIdRef = (0, import_react8.useRef)(nudgeNodeId);
|
|
1167
1247
|
nudgeNodeIdRef.current = nudgeNodeId;
|
|
1168
1248
|
(0, import_react8.useEffect)(() => {
|
|
1249
|
+
let cancelled = false;
|
|
1169
1250
|
setOnboarding(null);
|
|
1170
1251
|
setMessages([]);
|
|
1252
|
+
setResumed(false);
|
|
1253
|
+
setSentOpening(null);
|
|
1171
1254
|
setError(null);
|
|
1172
1255
|
setClientResults({});
|
|
1173
1256
|
setFeedbackState({});
|
|
1174
1257
|
dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
|
|
1258
|
+
conversationIdRef.current = null;
|
|
1259
|
+
pendingOpeningRef.current = null;
|
|
1175
1260
|
fetch(`${apiBaseUrl}/v1/conversations`, {
|
|
1176
1261
|
method: "POST",
|
|
1177
1262
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
1178
|
-
body: JSON.stringify({
|
|
1179
|
-
|
|
1180
|
-
|
|
1263
|
+
body: JSON.stringify({
|
|
1264
|
+
bot_id: botId,
|
|
1265
|
+
nudge_node_id: nudgeNodeIdRef.current ?? null,
|
|
1266
|
+
visitor_id: userKey || null,
|
|
1267
|
+
thread_key: threadKey ?? null,
|
|
1268
|
+
session_id: sessionId,
|
|
1269
|
+
defer: true
|
|
1270
|
+
})
|
|
1271
|
+
}).then((r) => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))).then((data) => {
|
|
1272
|
+
if (cancelled) return;
|
|
1273
|
+
if (conversationIdRef.current || messagesRef.current.length > 0) {
|
|
1274
|
+
setOnboarding(data);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
conversationIdRef.current = data.conversation_id;
|
|
1278
|
+
const restored = fromStoredMessages(data.messages ?? []);
|
|
1279
|
+
for (const m of restored) {
|
|
1280
|
+
for (const p of m.parts) if (p.kind === "tool") dispatchedClientActionsRef.current.add(p.toolCallId);
|
|
1281
|
+
}
|
|
1282
|
+
setMessages(restored);
|
|
1283
|
+
setResumed(restored.length > 0);
|
|
1284
|
+
setOnboarding(data);
|
|
1285
|
+
}).catch((e) => {
|
|
1286
|
+
if (!cancelled) setError(`\uBD07\uC744 \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`);
|
|
1287
|
+
});
|
|
1288
|
+
return () => {
|
|
1289
|
+
cancelled = true;
|
|
1290
|
+
};
|
|
1291
|
+
}, [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]);
|
|
1181
1302
|
const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
|
|
1182
1303
|
const next = [...prev];
|
|
1183
1304
|
next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
|
|
@@ -1190,7 +1311,7 @@ function useXbNovaChat(options) {
|
|
|
1190
1311
|
return next;
|
|
1191
1312
|
});
|
|
1192
1313
|
async function setFeedback(messageId, rating, reason) {
|
|
1193
|
-
const conversationId =
|
|
1314
|
+
const conversationId = conversationIdRef.current;
|
|
1194
1315
|
if (!conversationId || !messageId) return;
|
|
1195
1316
|
const prevRating = feedbackRef.current[messageId] ?? null;
|
|
1196
1317
|
const apply = (r) => setFeedbackState((prev) => {
|
|
@@ -1210,12 +1331,21 @@ function useXbNovaChat(options) {
|
|
|
1210
1331
|
}
|
|
1211
1332
|
);
|
|
1212
1333
|
if (!res.ok) throw new Error(`${res.status}`);
|
|
1334
|
+
sendEvent("feedback_submit", { conversation_id: conversationId, message_id: messageId, rating, reason });
|
|
1213
1335
|
options?.onFeedback?.({ conversationId, messageId, rating, reason });
|
|
1214
1336
|
} catch (e) {
|
|
1215
1337
|
console.warn("[xbnova] \uD53C\uB4DC\uBC31 \uC800\uC7A5 \uC2E4\uD328", e);
|
|
1216
1338
|
apply(prevRating);
|
|
1217
1339
|
}
|
|
1218
1340
|
}
|
|
1341
|
+
function postClientActionEvent(kind, action, label) {
|
|
1342
|
+
const conversationId = conversationIdRef.current;
|
|
1343
|
+
if (!conversationId) return;
|
|
1344
|
+
sendEvent(kind, { conversation_id: conversationId, action, label });
|
|
1345
|
+
}
|
|
1346
|
+
function trackClientActionClick(action, label) {
|
|
1347
|
+
postClientActionEvent("cta_click", action, label);
|
|
1348
|
+
}
|
|
1219
1349
|
async function runClientAction(spec, part, existingParts) {
|
|
1220
1350
|
if (spec.dismissSheet) options?.onDismissSheet?.();
|
|
1221
1351
|
let result;
|
|
@@ -1239,7 +1369,7 @@ function useXbNovaChat(options) {
|
|
|
1239
1369
|
result = e instanceof Error ? `\uC624\uB958: ${e.message}` : "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
|
|
1240
1370
|
}
|
|
1241
1371
|
if (!spec.awaitResult) return;
|
|
1242
|
-
const conversationId =
|
|
1372
|
+
const conversationId = conversationIdRef.current;
|
|
1243
1373
|
if (!conversationId) return;
|
|
1244
1374
|
const structured = result && typeof result === "object" ? result : null;
|
|
1245
1375
|
if (structured?.view) {
|
|
@@ -1283,12 +1413,18 @@ function useXbNovaChat(options) {
|
|
|
1283
1413
|
if (!spec) continue;
|
|
1284
1414
|
if (dispatchedClientActionsRef.current.has(part.toolCallId)) continue;
|
|
1285
1415
|
dispatchedClientActionsRef.current.add(part.toolCallId);
|
|
1416
|
+
if (spec.render) postClientActionEvent("cta_impression", part.toolName, null);
|
|
1286
1417
|
void runClientAction(spec, part, last.parts);
|
|
1287
1418
|
}
|
|
1288
1419
|
}, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
|
|
1289
1420
|
async function send(text) {
|
|
1290
1421
|
const trimmed = text.trim();
|
|
1291
1422
|
if (!trimmed || busy) return;
|
|
1423
|
+
sendEvent("message_send", {
|
|
1424
|
+
conversation_id: conversationIdRef.current,
|
|
1425
|
+
is_first: messages.length === 0,
|
|
1426
|
+
length: trimmed.length
|
|
1427
|
+
});
|
|
1292
1428
|
setError(null);
|
|
1293
1429
|
setBusy(true);
|
|
1294
1430
|
setMessages((prev) => [
|
|
@@ -1305,6 +1441,14 @@ function useXbNovaChat(options) {
|
|
|
1305
1441
|
const notes = collectContextEntries(contextRegistry);
|
|
1306
1442
|
const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
|
|
1307
1443
|
const context = focused || notes.length ? { ...screen, ...notes.length ? { notes } : {} } : void 0;
|
|
1444
|
+
let openingMessage2;
|
|
1445
|
+
if (messages.length === 0) {
|
|
1446
|
+
openingMessage2 = (options?.openingMessage ?? onboarding?.greeting) || null;
|
|
1447
|
+
setSentOpening(openingMessage2);
|
|
1448
|
+
} else {
|
|
1449
|
+
openingMessage2 = pendingOpeningRef.current;
|
|
1450
|
+
}
|
|
1451
|
+
pendingOpeningRef.current = null;
|
|
1308
1452
|
try {
|
|
1309
1453
|
await streamChat(
|
|
1310
1454
|
apiBaseUrl,
|
|
@@ -1312,12 +1456,22 @@ function useXbNovaChat(options) {
|
|
|
1312
1456
|
{
|
|
1313
1457
|
bot_id: botId,
|
|
1314
1458
|
message: trimmed,
|
|
1315
|
-
conversation_id:
|
|
1459
|
+
conversation_id: conversationIdRef.current,
|
|
1316
1460
|
context,
|
|
1317
1461
|
nudge_node_id: nudgeNodeId ?? null,
|
|
1318
|
-
opening_message:
|
|
1462
|
+
opening_message: openingMessage2,
|
|
1463
|
+
visitor_id: userKey || null,
|
|
1464
|
+
thread_key: threadKey ?? null,
|
|
1465
|
+
session_id: sessionId
|
|
1319
1466
|
},
|
|
1320
|
-
{
|
|
1467
|
+
{
|
|
1468
|
+
onPart: replaceLast,
|
|
1469
|
+
onMessageId: setLastAssistantId,
|
|
1470
|
+
onConversationId: (id) => {
|
|
1471
|
+
conversationIdRef.current = id;
|
|
1472
|
+
},
|
|
1473
|
+
onError: setError
|
|
1474
|
+
}
|
|
1321
1475
|
);
|
|
1322
1476
|
} catch (e) {
|
|
1323
1477
|
setError(e instanceof Error ? e.message : String(e));
|
|
@@ -1325,7 +1479,19 @@ function useXbNovaChat(options) {
|
|
|
1325
1479
|
setBusy(false);
|
|
1326
1480
|
}
|
|
1327
1481
|
}
|
|
1328
|
-
|
|
1482
|
+
const opening = resumed ? null : sentOpening ?? options?.openingMessage ?? onboarding?.greeting ?? null;
|
|
1483
|
+
return {
|
|
1484
|
+
onboarding,
|
|
1485
|
+
opening,
|
|
1486
|
+
messages,
|
|
1487
|
+
busy,
|
|
1488
|
+
error,
|
|
1489
|
+
send,
|
|
1490
|
+
clientResults,
|
|
1491
|
+
feedback,
|
|
1492
|
+
setFeedback,
|
|
1493
|
+
trackClientActionClick
|
|
1494
|
+
};
|
|
1329
1495
|
}
|
|
1330
1496
|
|
|
1331
1497
|
// src/views.tsx
|
|
@@ -1458,6 +1624,13 @@ function hasVisiblePart(parts, clientActionHandlers, clientResults) {
|
|
|
1458
1624
|
}
|
|
1459
1625
|
var DISCLOSURE_SEEN_KEY = "xbnova_disclosure_seen";
|
|
1460
1626
|
var SUMMARY_FACT_KEY = "xbnova_summary";
|
|
1627
|
+
function clickedControl(e) {
|
|
1628
|
+
const target = e.target instanceof Element ? e.target : null;
|
|
1629
|
+
const control = target?.closest("button, a, [role='button']");
|
|
1630
|
+
if (!control || !e.currentTarget.contains(control)) return null;
|
|
1631
|
+
const text = (control.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
1632
|
+
return { label: text || control.getAttribute("aria-label") || null };
|
|
1633
|
+
}
|
|
1461
1634
|
function readDisclosureSeen() {
|
|
1462
1635
|
if (typeof window === "undefined") return true;
|
|
1463
1636
|
try {
|
|
@@ -1587,9 +1760,10 @@ function XbNovaChat({
|
|
|
1587
1760
|
avatarSize = 28,
|
|
1588
1761
|
onVerdictAction,
|
|
1589
1762
|
feedback: feedbackEnabled = true,
|
|
1590
|
-
onFeedback
|
|
1763
|
+
onFeedback,
|
|
1764
|
+
onOpen
|
|
1591
1765
|
}) {
|
|
1592
|
-
const { auth, trackRegistry, factsRegistry,
|
|
1766
|
+
const { auth, trackRegistry, factsRegistry, clientActionHandlers, theme, sendEvent } = useXbNovaInternal();
|
|
1593
1767
|
const themeVars = {
|
|
1594
1768
|
"--xbnova-accent-1": theme.accentFrom,
|
|
1595
1769
|
"--xbnova-accent-2": theme.accentTo
|
|
@@ -1610,7 +1784,7 @@ function XbNovaChat({
|
|
|
1610
1784
|
const [input, setInput] = (0, import_react9.useState)("");
|
|
1611
1785
|
const bottomRef = (0, import_react9.useRef)(null);
|
|
1612
1786
|
const typedHintText = useTypewriter(hint?.text ?? "", hint?.triggerNodeId ?? "");
|
|
1613
|
-
const { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback } = useXbNovaChat({
|
|
1787
|
+
const { onboarding, opening, messages, busy, error, send, clientResults, feedback, setFeedback, trackClientActionClick } = useXbNovaChat({
|
|
1614
1788
|
onFeedback,
|
|
1615
1789
|
botId: activeBotId ?? botId,
|
|
1616
1790
|
nudgeNodeId: activeNudgeNodeId,
|
|
@@ -1632,12 +1806,19 @@ function XbNovaChat({
|
|
|
1632
1806
|
try {
|
|
1633
1807
|
window.localStorage.setItem(
|
|
1634
1808
|
`xbnova_branding:${effectiveBotId}`,
|
|
1635
|
-
JSON.stringify({
|
|
1809
|
+
JSON.stringify({
|
|
1810
|
+
logo_url: onboarding.branding.logo_url,
|
|
1811
|
+
avatar_text: onboarding.branding.avatar_text,
|
|
1812
|
+
badge_text: onboarding.branding.badge_text,
|
|
1813
|
+
disclaimer: onboarding.branding.disclaimer
|
|
1814
|
+
})
|
|
1636
1815
|
);
|
|
1637
1816
|
} catch {
|
|
1638
1817
|
}
|
|
1639
1818
|
}, [onboarding?.branding, effectiveBotId]);
|
|
1640
1819
|
const branding = onboarding?.branding ?? cachedBranding ?? void 0;
|
|
1820
|
+
const badgeText = branding?.badge_text?.trim() ?? "";
|
|
1821
|
+
const disclaimer = branding?.disclaimer?.trim() ?? "";
|
|
1641
1822
|
const brandingReady = Boolean(onboarding) || cachedBranding !== null || Boolean(error);
|
|
1642
1823
|
(0, import_react9.useEffect)(() => {
|
|
1643
1824
|
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
@@ -1667,18 +1848,7 @@ function XbNovaChat({
|
|
|
1667
1848
|
setOpeningLine(consumed.text);
|
|
1668
1849
|
setActiveNudgeNodeId(consumed.messageNodeId);
|
|
1669
1850
|
setNudgeQuickReplies(consumed.quickReplies);
|
|
1670
|
-
|
|
1671
|
-
method: "POST",
|
|
1672
|
-
headers: { "Content-Type": "application/json" },
|
|
1673
|
-
body: JSON.stringify({
|
|
1674
|
-
app_id: auth.appId,
|
|
1675
|
-
nudge_node_id: consumed.messageNodeId,
|
|
1676
|
-
kind: "nudge_open",
|
|
1677
|
-
bot_id: consumed.botId,
|
|
1678
|
-
visitor_id: visitorId
|
|
1679
|
-
})
|
|
1680
|
-
}).catch(() => {
|
|
1681
|
-
});
|
|
1851
|
+
sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
|
|
1682
1852
|
} else {
|
|
1683
1853
|
setNudgeQuickReplies([]);
|
|
1684
1854
|
setActiveNudgeNodeId(void 0);
|
|
@@ -1692,6 +1862,16 @@ function XbNovaChat({
|
|
|
1692
1862
|
markDisclosureSeen();
|
|
1693
1863
|
}
|
|
1694
1864
|
setOpen(true);
|
|
1865
|
+
sendEvent("chat_open", { from_nudge: Boolean(consumed) });
|
|
1866
|
+
try {
|
|
1867
|
+
onOpen?.({ fromNudge: Boolean(consumed), hintText: consumed?.text });
|
|
1868
|
+
} catch (err) {
|
|
1869
|
+
console.warn("[XbNova] onOpen \uCF5C\uBC31 \uC624\uB958", err);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
function closeSheet() {
|
|
1873
|
+
setOpen(false);
|
|
1874
|
+
sendEvent("chat_close");
|
|
1695
1875
|
}
|
|
1696
1876
|
if (!open) {
|
|
1697
1877
|
if (barHidden) return null;
|
|
@@ -1787,7 +1967,6 @@ function XbNovaChat({
|
|
|
1787
1967
|
const summary = resolveFactValue(factsRegistry.get(SUMMARY_FACT_KEY));
|
|
1788
1968
|
const focusedLabel = getFocusedTrackEntry(trackRegistry)?.label;
|
|
1789
1969
|
const contextLine = summary ?? (focusedLabel ? `${focusedLabel} \uBCF4\uB294 \uC911` : void 0);
|
|
1790
|
-
const firstBubble = openingLine ?? onboarding?.greeting;
|
|
1791
1970
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
1792
1971
|
!isDesktop ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1793
1972
|
XbNovaShadowRoot,
|
|
@@ -1798,7 +1977,7 @@ function XbNovaChat({
|
|
|
1798
1977
|
transition: "opacity 260ms ease-out",
|
|
1799
1978
|
...backdropStyle
|
|
1800
1979
|
},
|
|
1801
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "h-full w-full bg-black/45", onClick:
|
|
1980
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "h-full w-full bg-black/45", onClick: closeSheet })
|
|
1802
1981
|
}
|
|
1803
1982
|
) : null,
|
|
1804
1983
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
@@ -1824,14 +2003,17 @@ function XbNovaChat({
|
|
|
1824
2003
|
}
|
|
1825
2004
|
),
|
|
1826
2005
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
1827
|
-
/* @__PURE__ */ (0, import_jsx_runtime7.
|
|
2006
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex min-w-0 items-center gap-1.5", children: [
|
|
2007
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "truncate text-sm font-semibold", children: onboarding?.branding?.title ?? "AI \uB3D9\uD589" }),
|
|
2008
|
+
badgeText ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "shrink-0 rounded-full border border-neutral-300 px-1.5 py-px text-[10px] font-semibold leading-4 text-neutral-500 dark:border-neutral-600 dark:text-neutral-400", children: badgeText }) : null
|
|
2009
|
+
] }),
|
|
1828
2010
|
contextLine ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "truncate text-[11px] text-neutral-400", children: contextLine }) : null
|
|
1829
2011
|
] }),
|
|
1830
2012
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1831
2013
|
"button",
|
|
1832
2014
|
{
|
|
1833
2015
|
type: "button",
|
|
1834
|
-
onClick:
|
|
2016
|
+
onClick: closeSheet,
|
|
1835
2017
|
"aria-label": "\uCC44\uD305 \uB2EB\uAE30",
|
|
1836
2018
|
className: "shrink-0 text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200",
|
|
1837
2019
|
children: "\xD7"
|
|
@@ -1840,7 +2022,7 @@ function XbNovaChat({
|
|
|
1840
2022
|
] }),
|
|
1841
2023
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [
|
|
1842
2024
|
showDisclosure ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rounded-lg bg-neutral-50 px-3 py-2 text-[11px] text-neutral-400 dark:bg-neutral-800", children: "\uC9C0\uAE08 \uBCF4\uACE0 \uACC4\uC2E0 \uD654\uBA74\uACFC \uB9E4\uBB3C \uC815\uBCF4\uB97C \uAE30\uC900\uC73C\uB85C \uC548\uB0B4\uD574 \uB4DC\uB824\uC694." }) : null,
|
|
1843
|
-
|
|
2025
|
+
opening ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssistantMarkdown, { text: opening }) }) : null,
|
|
1844
2026
|
messages.map((message, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: message.role === "user" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "user", children: message.parts.map((p) => p.kind === "text" ? p.text : "").join("") }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
1845
2027
|
AnswerActionsFrame,
|
|
1846
2028
|
{
|
|
@@ -1868,7 +2050,17 @@ function XbNovaChat({
|
|
|
1868
2050
|
);
|
|
1869
2051
|
}
|
|
1870
2052
|
const spec = clientActionHandlers.get(part.toolName);
|
|
1871
|
-
return spec?.render ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
2053
|
+
return spec?.render ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
2054
|
+
"div",
|
|
2055
|
+
{
|
|
2056
|
+
onClickCapture: (e) => {
|
|
2057
|
+
const control = clickedControl(e);
|
|
2058
|
+
if (control) trackClientActionClick(part.toolName, control.label);
|
|
2059
|
+
},
|
|
2060
|
+
children: spec.render(part.input)
|
|
2061
|
+
},
|
|
2062
|
+
j
|
|
2063
|
+
) : null;
|
|
1872
2064
|
}
|
|
1873
2065
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ToolView, { part, viewRenderers, onVerdictAction }, j);
|
|
1874
2066
|
}),
|
|
@@ -1939,7 +2131,8 @@ function XbNovaChat({
|
|
|
1939
2131
|
)
|
|
1940
2132
|
]
|
|
1941
2133
|
}
|
|
1942
|
-
)
|
|
2134
|
+
),
|
|
2135
|
+
disclaimer ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "-mt-1 px-4 pb-2.5 text-center text-[11px] leading-snug text-neutral-400 dark:text-neutral-500", children: disclaimer }) : null
|
|
1943
2136
|
] })
|
|
1944
2137
|
}
|
|
1945
2138
|
)
|