@xbrain-swe/nova-chat-widget 0.1.11 → 0.2.4-dev.15
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 +228 -55
- 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 +278 -105
- 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,35 @@ 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)((kind, fields = {}) => {
|
|
121
|
+
const { auth: a, visitorId: v, sessionId: sid } = latestRef.current;
|
|
122
|
+
fetch(`${a.apiBaseUrl}/v1/nudge-events`, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: { "Content-Type": "application/json" },
|
|
125
|
+
body: JSON.stringify({ app_id: a.appId, kind, visitor_id: v || null, session_id: sid, ...fields })
|
|
126
|
+
}).catch(() => {
|
|
127
|
+
});
|
|
128
|
+
}, []);
|
|
129
|
+
const widgetViewSentRef = (0, import_react.useRef)(false);
|
|
130
|
+
(0, import_react.useEffect)(() => {
|
|
131
|
+
if (widgetViewSentRef.current) return;
|
|
132
|
+
const send = () => {
|
|
133
|
+
if (widgetViewSentRef.current) return;
|
|
134
|
+
widgetViewSentRef.current = true;
|
|
135
|
+
const screen = latestRef.current.auth.screen;
|
|
136
|
+
sendEvent("widget_view", screen ? { payload: { screen } } : {});
|
|
137
|
+
};
|
|
138
|
+
if (sessionId) {
|
|
139
|
+
send();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const timer = setTimeout(send, WIDGET_VIEW_WAIT_MS);
|
|
143
|
+
return () => clearTimeout(timer);
|
|
144
|
+
}, [sessionId, sendEvent]);
|
|
115
145
|
const value = (0, import_react.useMemo)(
|
|
116
146
|
() => ({
|
|
117
147
|
auth,
|
|
@@ -119,10 +149,12 @@ function InternalProvider({
|
|
|
119
149
|
factsRegistry,
|
|
120
150
|
contextRegistry,
|
|
121
151
|
clientActionHandlers: clientActionHandlers ?? emptyHandlers,
|
|
122
|
-
visitorId
|
|
152
|
+
visitorId,
|
|
153
|
+
sessionId,
|
|
154
|
+
sendEvent,
|
|
123
155
|
theme
|
|
124
156
|
}),
|
|
125
|
-
[auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, theme]
|
|
157
|
+
[auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, visitorId, sessionId, sendEvent, theme]
|
|
126
158
|
);
|
|
127
159
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InternalContext.Provider, { value, children });
|
|
128
160
|
}
|
|
@@ -425,7 +457,7 @@ function getFocusedTrackEntry(trackRegistry) {
|
|
|
425
457
|
}
|
|
426
458
|
|
|
427
459
|
// src/nudge-loop.tsx
|
|
428
|
-
var EVAL_INTERVAL_MS =
|
|
460
|
+
var EVAL_INTERVAL_MS = 250;
|
|
429
461
|
var XBNOVA_EVENT_NAME = "xbnova:event";
|
|
430
462
|
var SESSION_COUNT_KEY_PREFIX = "xbnova_hint_session_count:";
|
|
431
463
|
var SCROLL_SETTLE_MS = 500;
|
|
@@ -618,7 +650,7 @@ function clearDebugOverlay(refs) {
|
|
|
618
650
|
refs.bandLinesRef.current.clear();
|
|
619
651
|
}
|
|
620
652
|
function XbNovaNudgeLoop({ appId }) {
|
|
621
|
-
const { auth, trackRegistry, factsRegistry,
|
|
653
|
+
const { auth, trackRegistry, factsRegistry, sendEvent } = useXbNovaInternal();
|
|
622
654
|
const { apiKey, apiBaseUrl, screen } = auth;
|
|
623
655
|
const { hint, offerHint, autoHideHint, hintsEnabled } = useNudgeState();
|
|
624
656
|
const graphRef = (0, import_react5.useRef)(null);
|
|
@@ -651,8 +683,10 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
651
683
|
const debugEnabled = isDebugEnabled();
|
|
652
684
|
const debugRefs = { panelRef: debugPanelRef, boxesRef: debugBoxesRef, bandLinesRef: debugBandLinesRef };
|
|
653
685
|
if (!debugEnabled && debugPanelRef.current) clearDebugOverlay(debugRefs);
|
|
686
|
+
const tickDebug = debugEnabled ? (msg) => console.debug(msg) : () => {
|
|
687
|
+
};
|
|
654
688
|
if (!graph) {
|
|
655
|
-
|
|
689
|
+
tickDebug("[XbNova] nudge \uD3C9\uAC00 \uAC74\uB108\uB700: \uADF8\uB798\uD504\uB97C \uC544\uC9C1 \uBABB \uBD88\uB7EC\uC634");
|
|
656
690
|
return;
|
|
657
691
|
}
|
|
658
692
|
const canOffer = !hintRef.current && hintsEnabledRef.current;
|
|
@@ -703,18 +737,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
703
737
|
autoHideMs: messageNode.auto_hide_ms ?? 2e4,
|
|
704
738
|
quickReplies: messageNode.quick_replies ?? []
|
|
705
739
|
});
|
|
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
|
-
});
|
|
740
|
+
sendEvent("nudge_impression", { nudge_node_id: messageNode.id, bot_id: botId });
|
|
718
741
|
};
|
|
719
742
|
const tryFireFrom = (node) => {
|
|
720
743
|
const nextEdge = (edgesFrom.get(node.id) ?? [])[0];
|
|
@@ -742,7 +765,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
742
765
|
const maxPerSession = node.max_per_session === 0 ? Infinity : node.max_per_session ?? 3;
|
|
743
766
|
const sessionCount = peekSessionCount(node.id);
|
|
744
767
|
if (sessionCount >= maxPerSession) {
|
|
745
|
-
|
|
768
|
+
tickDebug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id}(${node.trigger_type}) \uAC74\uB108\uB700: \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC`);
|
|
746
769
|
if (debugEnabled && node.trigger_type === "dwell") {
|
|
747
770
|
debugLines.push(`- [${node.id}] \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC \u2014 \uB354 \uC548 \uC7BC`);
|
|
748
771
|
}
|
|
@@ -777,7 +800,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
777
800
|
overlapPx = status.overlapPx;
|
|
778
801
|
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
802
|
}
|
|
780
|
-
|
|
803
|
+
tickDebug(
|
|
781
804
|
`[XbNova] band dwell \uD2B8\uB9AC\uAC70=${node.id} region="${region}" band%=${fromPercent}~${toPercent} | ${statusDetail}`
|
|
782
805
|
);
|
|
783
806
|
dwellRecords.push({ node, state, requiredMs, region, found, screenMatches, inBand, overlapPx });
|
|
@@ -786,7 +809,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
786
809
|
const fires = triggerFires(node, { screen, firedEventName });
|
|
787
810
|
if (!fires) continue;
|
|
788
811
|
if (!canOffer) {
|
|
789
|
-
|
|
812
|
+
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
813
|
continue;
|
|
791
814
|
}
|
|
792
815
|
if (tryFireFrom(node)) {
|
|
@@ -886,7 +909,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
886
909
|
ensureDebugPanel(debugPanelRef, debugCollapsedRef).body.textContent = debugLines.join("\n");
|
|
887
910
|
}
|
|
888
911
|
},
|
|
889
|
-
[apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry,
|
|
912
|
+
[apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, sendEvent]
|
|
890
913
|
);
|
|
891
914
|
(0, import_react5.useEffect)(() => {
|
|
892
915
|
const interval = window.setInterval(() => evaluate(null), EVAL_INTERVAL_MS);
|
|
@@ -917,6 +940,9 @@ function XbNovaProvider({
|
|
|
917
940
|
apiBaseUrl = DEFAULT_API_BASE_URL,
|
|
918
941
|
clientActions,
|
|
919
942
|
theme,
|
|
943
|
+
userKey,
|
|
944
|
+
threadKey,
|
|
945
|
+
sessionKey,
|
|
920
946
|
children
|
|
921
947
|
}) {
|
|
922
948
|
(0, import_react6.useEffect)(() => {
|
|
@@ -933,8 +959,17 @@ function XbNovaProvider({
|
|
|
933
959
|
if (screen) bumpVisitCount(screen);
|
|
934
960
|
}, [screen]);
|
|
935
961
|
const auth = (0, import_react6.useMemo)(
|
|
936
|
-
() => ({
|
|
937
|
-
|
|
962
|
+
() => ({
|
|
963
|
+
apiKey,
|
|
964
|
+
botId,
|
|
965
|
+
appId,
|
|
966
|
+
apiBaseUrl,
|
|
967
|
+
screen,
|
|
968
|
+
userKey: userKey || void 0,
|
|
969
|
+
threadKey: threadKey || void 0,
|
|
970
|
+
sessionKey: sessionKey || void 0
|
|
971
|
+
}),
|
|
972
|
+
[apiKey, botId, appId, apiBaseUrl, screen, userKey, threadKey, sessionKey]
|
|
938
973
|
);
|
|
939
974
|
const resolvedTheme = (0, import_react6.useMemo)(
|
|
940
975
|
() => ({ ...DEFAULT_THEME, ...theme }),
|
|
@@ -1040,12 +1075,41 @@ function XbNovaShadowRoot({
|
|
|
1040
1075
|
var import_react8 = require("react");
|
|
1041
1076
|
|
|
1042
1077
|
// src/stream.ts
|
|
1078
|
+
function fromStoredMessages(stored) {
|
|
1079
|
+
return stored.map((m) => ({
|
|
1080
|
+
id: m.role === "assistant" ? m.id : void 0,
|
|
1081
|
+
role: m.role === "assistant" ? "assistant" : "user",
|
|
1082
|
+
parts: m.parts.flatMap((p, i) => {
|
|
1083
|
+
const type = String(p.type ?? "");
|
|
1084
|
+
if (type === "text") {
|
|
1085
|
+
const text = typeof p.text === "string" ? p.text : "";
|
|
1086
|
+
return text ? [{ kind: "text", id: `${m.id}:${i}`, text }] : [];
|
|
1087
|
+
}
|
|
1088
|
+
if (type.startsWith("tool-")) {
|
|
1089
|
+
const meta = p.providerMetadata;
|
|
1090
|
+
return [
|
|
1091
|
+
{
|
|
1092
|
+
kind: "tool",
|
|
1093
|
+
toolCallId: String(p.toolCallId ?? `${m.id}:${i}`),
|
|
1094
|
+
toolName: type.slice("tool-".length),
|
|
1095
|
+
state: p.state === "output-error" ? "error" : "output",
|
|
1096
|
+
input: p.input,
|
|
1097
|
+
output: p.output,
|
|
1098
|
+
view: meta?.["kr.dsauto.chat"]?.view ?? null
|
|
1099
|
+
}
|
|
1100
|
+
];
|
|
1101
|
+
}
|
|
1102
|
+
return [];
|
|
1103
|
+
})
|
|
1104
|
+
}));
|
|
1105
|
+
}
|
|
1043
1106
|
async function consumeSse(response, handlers) {
|
|
1044
1107
|
if (!response.ok || !response.body) {
|
|
1045
1108
|
handlers.onError?.(`\uC694\uCCAD \uC2E4\uD328 (${response.status})`);
|
|
1046
1109
|
return null;
|
|
1047
1110
|
}
|
|
1048
1111
|
const conversationId = response.headers.get("x-conversation-id");
|
|
1112
|
+
if (conversationId) handlers.onConversationId?.(conversationId);
|
|
1049
1113
|
const reader = response.body.getReader();
|
|
1050
1114
|
const decoder = new TextDecoder();
|
|
1051
1115
|
let buffer = "";
|
|
@@ -1150,12 +1214,19 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
|
|
|
1150
1214
|
// src/use-chat.ts
|
|
1151
1215
|
var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
|
|
1152
1216
|
function useXbNovaChat(options) {
|
|
1153
|
-
const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId } = useXbNovaInternal();
|
|
1217
|
+
const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId, sessionId, sendEvent } = useXbNovaInternal();
|
|
1154
1218
|
const botId = options?.botId ?? auth.botId;
|
|
1155
1219
|
const nudgeNodeId = options?.nudgeNodeId;
|
|
1156
|
-
const { apiKey, apiBaseUrl } = auth;
|
|
1220
|
+
const { apiKey, apiBaseUrl, threadKey } = auth;
|
|
1221
|
+
const userKey = visitorId;
|
|
1157
1222
|
const [onboarding, setOnboarding] = (0, import_react8.useState)(null);
|
|
1158
1223
|
const [messages, setMessages] = (0, import_react8.useState)([]);
|
|
1224
|
+
const [resumed, setResumed] = (0, import_react8.useState)(false);
|
|
1225
|
+
const [sentOpening, setSentOpening] = (0, import_react8.useState)(null);
|
|
1226
|
+
const conversationIdRef = (0, import_react8.useRef)(null);
|
|
1227
|
+
const pendingOpeningRef = (0, import_react8.useRef)(null);
|
|
1228
|
+
const messagesRef = (0, import_react8.useRef)(messages);
|
|
1229
|
+
messagesRef.current = messages;
|
|
1159
1230
|
const [busy, setBusy] = (0, import_react8.useState)(false);
|
|
1160
1231
|
const [error, setError] = (0, import_react8.useState)(null);
|
|
1161
1232
|
const [clientResults, setClientResults] = (0, import_react8.useState)({});
|
|
@@ -1166,18 +1237,59 @@ function useXbNovaChat(options) {
|
|
|
1166
1237
|
const nudgeNodeIdRef = (0, import_react8.useRef)(nudgeNodeId);
|
|
1167
1238
|
nudgeNodeIdRef.current = nudgeNodeId;
|
|
1168
1239
|
(0, import_react8.useEffect)(() => {
|
|
1240
|
+
let cancelled = false;
|
|
1169
1241
|
setOnboarding(null);
|
|
1170
1242
|
setMessages([]);
|
|
1243
|
+
setResumed(false);
|
|
1244
|
+
setSentOpening(null);
|
|
1171
1245
|
setError(null);
|
|
1172
1246
|
setClientResults({});
|
|
1173
1247
|
setFeedbackState({});
|
|
1174
1248
|
dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
|
|
1249
|
+
conversationIdRef.current = null;
|
|
1250
|
+
pendingOpeningRef.current = null;
|
|
1175
1251
|
fetch(`${apiBaseUrl}/v1/conversations`, {
|
|
1176
1252
|
method: "POST",
|
|
1177
1253
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
1178
|
-
body: JSON.stringify({
|
|
1179
|
-
|
|
1180
|
-
|
|
1254
|
+
body: JSON.stringify({
|
|
1255
|
+
bot_id: botId,
|
|
1256
|
+
nudge_node_id: nudgeNodeIdRef.current ?? null,
|
|
1257
|
+
visitor_id: userKey || null,
|
|
1258
|
+
thread_key: threadKey ?? null,
|
|
1259
|
+
session_id: sessionId,
|
|
1260
|
+
defer: true
|
|
1261
|
+
})
|
|
1262
|
+
}).then((r) => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))).then((data) => {
|
|
1263
|
+
if (cancelled) return;
|
|
1264
|
+
if (conversationIdRef.current || messagesRef.current.length > 0) {
|
|
1265
|
+
setOnboarding(data);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
conversationIdRef.current = data.conversation_id;
|
|
1269
|
+
const restored = fromStoredMessages(data.messages ?? []);
|
|
1270
|
+
for (const m of restored) {
|
|
1271
|
+
for (const p of m.parts) if (p.kind === "tool") dispatchedClientActionsRef.current.add(p.toolCallId);
|
|
1272
|
+
}
|
|
1273
|
+
setMessages(restored);
|
|
1274
|
+
setResumed(restored.length > 0);
|
|
1275
|
+
setOnboarding(data);
|
|
1276
|
+
}).catch((e) => {
|
|
1277
|
+
if (!cancelled) setError(`\uBD07\uC744 \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`);
|
|
1278
|
+
});
|
|
1279
|
+
return () => {
|
|
1280
|
+
cancelled = true;
|
|
1281
|
+
};
|
|
1282
|
+
}, [apiBaseUrl, apiKey, botId, userKey, threadKey]);
|
|
1283
|
+
const openingMessage = options?.openingMessage;
|
|
1284
|
+
(0, import_react8.useEffect)(() => {
|
|
1285
|
+
if (!onboarding || !nudgeNodeId || !openingMessage) return;
|
|
1286
|
+
if (messagesRef.current.length === 0) return;
|
|
1287
|
+
setMessages((prev) => [
|
|
1288
|
+
...prev,
|
|
1289
|
+
{ role: "assistant", parts: [{ kind: "text", id: `nudge:${nudgeNodeId}:${prev.length}`, text: openingMessage }] }
|
|
1290
|
+
]);
|
|
1291
|
+
pendingOpeningRef.current = openingMessage;
|
|
1292
|
+
}, [onboarding, nudgeNodeId, openingMessage]);
|
|
1181
1293
|
const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
|
|
1182
1294
|
const next = [...prev];
|
|
1183
1295
|
next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
|
|
@@ -1190,7 +1302,7 @@ function useXbNovaChat(options) {
|
|
|
1190
1302
|
return next;
|
|
1191
1303
|
});
|
|
1192
1304
|
async function setFeedback(messageId, rating, reason) {
|
|
1193
|
-
const conversationId =
|
|
1305
|
+
const conversationId = conversationIdRef.current;
|
|
1194
1306
|
if (!conversationId || !messageId) return;
|
|
1195
1307
|
const prevRating = feedbackRef.current[messageId] ?? null;
|
|
1196
1308
|
const apply = (r) => setFeedbackState((prev) => {
|
|
@@ -1216,6 +1328,14 @@ function useXbNovaChat(options) {
|
|
|
1216
1328
|
apply(prevRating);
|
|
1217
1329
|
}
|
|
1218
1330
|
}
|
|
1331
|
+
function postClientActionEvent(kind, action, label) {
|
|
1332
|
+
const conversationId = conversationIdRef.current;
|
|
1333
|
+
if (!conversationId) return;
|
|
1334
|
+
sendEvent(kind, { conversation_id: conversationId, payload: label ? { action, label } : { action } });
|
|
1335
|
+
}
|
|
1336
|
+
function trackClientActionClick(action, label) {
|
|
1337
|
+
postClientActionEvent("cta_click", action, label);
|
|
1338
|
+
}
|
|
1219
1339
|
async function runClientAction(spec, part, existingParts) {
|
|
1220
1340
|
if (spec.dismissSheet) options?.onDismissSheet?.();
|
|
1221
1341
|
let result;
|
|
@@ -1239,7 +1359,7 @@ function useXbNovaChat(options) {
|
|
|
1239
1359
|
result = e instanceof Error ? `\uC624\uB958: ${e.message}` : "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
|
|
1240
1360
|
}
|
|
1241
1361
|
if (!spec.awaitResult) return;
|
|
1242
|
-
const conversationId =
|
|
1362
|
+
const conversationId = conversationIdRef.current;
|
|
1243
1363
|
if (!conversationId) return;
|
|
1244
1364
|
const structured = result && typeof result === "object" ? result : null;
|
|
1245
1365
|
if (structured?.view) {
|
|
@@ -1283,6 +1403,7 @@ function useXbNovaChat(options) {
|
|
|
1283
1403
|
if (!spec) continue;
|
|
1284
1404
|
if (dispatchedClientActionsRef.current.has(part.toolCallId)) continue;
|
|
1285
1405
|
dispatchedClientActionsRef.current.add(part.toolCallId);
|
|
1406
|
+
if (spec.render) postClientActionEvent("cta_impression", part.toolName, null);
|
|
1286
1407
|
void runClientAction(spec, part, last.parts);
|
|
1287
1408
|
}
|
|
1288
1409
|
}, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
|
|
@@ -1305,6 +1426,14 @@ function useXbNovaChat(options) {
|
|
|
1305
1426
|
const notes = collectContextEntries(contextRegistry);
|
|
1306
1427
|
const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
|
|
1307
1428
|
const context = focused || notes.length ? { ...screen, ...notes.length ? { notes } : {} } : void 0;
|
|
1429
|
+
let openingMessage2;
|
|
1430
|
+
if (messages.length === 0) {
|
|
1431
|
+
openingMessage2 = (options?.openingMessage ?? onboarding?.greeting) || null;
|
|
1432
|
+
setSentOpening(openingMessage2);
|
|
1433
|
+
} else {
|
|
1434
|
+
openingMessage2 = pendingOpeningRef.current;
|
|
1435
|
+
}
|
|
1436
|
+
pendingOpeningRef.current = null;
|
|
1308
1437
|
try {
|
|
1309
1438
|
await streamChat(
|
|
1310
1439
|
apiBaseUrl,
|
|
@@ -1312,12 +1441,22 @@ function useXbNovaChat(options) {
|
|
|
1312
1441
|
{
|
|
1313
1442
|
bot_id: botId,
|
|
1314
1443
|
message: trimmed,
|
|
1315
|
-
conversation_id:
|
|
1444
|
+
conversation_id: conversationIdRef.current,
|
|
1316
1445
|
context,
|
|
1317
1446
|
nudge_node_id: nudgeNodeId ?? null,
|
|
1318
|
-
opening_message:
|
|
1447
|
+
opening_message: openingMessage2,
|
|
1448
|
+
visitor_id: userKey || null,
|
|
1449
|
+
thread_key: threadKey ?? null,
|
|
1450
|
+
session_id: sessionId
|
|
1319
1451
|
},
|
|
1320
|
-
{
|
|
1452
|
+
{
|
|
1453
|
+
onPart: replaceLast,
|
|
1454
|
+
onMessageId: setLastAssistantId,
|
|
1455
|
+
onConversationId: (id) => {
|
|
1456
|
+
conversationIdRef.current = id;
|
|
1457
|
+
},
|
|
1458
|
+
onError: setError
|
|
1459
|
+
}
|
|
1321
1460
|
);
|
|
1322
1461
|
} catch (e) {
|
|
1323
1462
|
setError(e instanceof Error ? e.message : String(e));
|
|
@@ -1325,7 +1464,19 @@ function useXbNovaChat(options) {
|
|
|
1325
1464
|
setBusy(false);
|
|
1326
1465
|
}
|
|
1327
1466
|
}
|
|
1328
|
-
|
|
1467
|
+
const opening = resumed ? null : sentOpening ?? options?.openingMessage ?? onboarding?.greeting ?? null;
|
|
1468
|
+
return {
|
|
1469
|
+
onboarding,
|
|
1470
|
+
opening,
|
|
1471
|
+
messages,
|
|
1472
|
+
busy,
|
|
1473
|
+
error,
|
|
1474
|
+
send,
|
|
1475
|
+
clientResults,
|
|
1476
|
+
feedback,
|
|
1477
|
+
setFeedback,
|
|
1478
|
+
trackClientActionClick
|
|
1479
|
+
};
|
|
1329
1480
|
}
|
|
1330
1481
|
|
|
1331
1482
|
// src/views.tsx
|
|
@@ -1458,6 +1609,13 @@ function hasVisiblePart(parts, clientActionHandlers, clientResults) {
|
|
|
1458
1609
|
}
|
|
1459
1610
|
var DISCLOSURE_SEEN_KEY = "xbnova_disclosure_seen";
|
|
1460
1611
|
var SUMMARY_FACT_KEY = "xbnova_summary";
|
|
1612
|
+
function clickedControl(e) {
|
|
1613
|
+
const target = e.target instanceof Element ? e.target : null;
|
|
1614
|
+
const control = target?.closest("button, a, [role='button']");
|
|
1615
|
+
if (!control || !e.currentTarget.contains(control)) return null;
|
|
1616
|
+
const text = (control.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
1617
|
+
return { label: text || control.getAttribute("aria-label") || null };
|
|
1618
|
+
}
|
|
1461
1619
|
function readDisclosureSeen() {
|
|
1462
1620
|
if (typeof window === "undefined") return true;
|
|
1463
1621
|
try {
|
|
@@ -1587,9 +1745,10 @@ function XbNovaChat({
|
|
|
1587
1745
|
avatarSize = 28,
|
|
1588
1746
|
onVerdictAction,
|
|
1589
1747
|
feedback: feedbackEnabled = true,
|
|
1590
|
-
onFeedback
|
|
1748
|
+
onFeedback,
|
|
1749
|
+
onOpen
|
|
1591
1750
|
}) {
|
|
1592
|
-
const { auth, trackRegistry, factsRegistry,
|
|
1751
|
+
const { auth, trackRegistry, factsRegistry, clientActionHandlers, theme, sendEvent } = useXbNovaInternal();
|
|
1593
1752
|
const themeVars = {
|
|
1594
1753
|
"--xbnova-accent-1": theme.accentFrom,
|
|
1595
1754
|
"--xbnova-accent-2": theme.accentTo
|
|
@@ -1610,7 +1769,7 @@ function XbNovaChat({
|
|
|
1610
1769
|
const [input, setInput] = (0, import_react9.useState)("");
|
|
1611
1770
|
const bottomRef = (0, import_react9.useRef)(null);
|
|
1612
1771
|
const typedHintText = useTypewriter(hint?.text ?? "", hint?.triggerNodeId ?? "");
|
|
1613
|
-
const { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback } = useXbNovaChat({
|
|
1772
|
+
const { onboarding, opening, messages, busy, error, send, clientResults, feedback, setFeedback, trackClientActionClick } = useXbNovaChat({
|
|
1614
1773
|
onFeedback,
|
|
1615
1774
|
botId: activeBotId ?? botId,
|
|
1616
1775
|
nudgeNodeId: activeNudgeNodeId,
|
|
@@ -1632,12 +1791,19 @@ function XbNovaChat({
|
|
|
1632
1791
|
try {
|
|
1633
1792
|
window.localStorage.setItem(
|
|
1634
1793
|
`xbnova_branding:${effectiveBotId}`,
|
|
1635
|
-
JSON.stringify({
|
|
1794
|
+
JSON.stringify({
|
|
1795
|
+
logo_url: onboarding.branding.logo_url,
|
|
1796
|
+
avatar_text: onboarding.branding.avatar_text,
|
|
1797
|
+
badge_text: onboarding.branding.badge_text,
|
|
1798
|
+
disclaimer: onboarding.branding.disclaimer
|
|
1799
|
+
})
|
|
1636
1800
|
);
|
|
1637
1801
|
} catch {
|
|
1638
1802
|
}
|
|
1639
1803
|
}, [onboarding?.branding, effectiveBotId]);
|
|
1640
1804
|
const branding = onboarding?.branding ?? cachedBranding ?? void 0;
|
|
1805
|
+
const badgeText = branding?.badge_text?.trim() ?? "";
|
|
1806
|
+
const disclaimer = branding?.disclaimer?.trim() ?? "";
|
|
1641
1807
|
const brandingReady = Boolean(onboarding) || cachedBranding !== null || Boolean(error);
|
|
1642
1808
|
(0, import_react9.useEffect)(() => {
|
|
1643
1809
|
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
@@ -1667,18 +1833,7 @@ function XbNovaChat({
|
|
|
1667
1833
|
setOpeningLine(consumed.text);
|
|
1668
1834
|
setActiveNudgeNodeId(consumed.messageNodeId);
|
|
1669
1835
|
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
|
-
});
|
|
1836
|
+
sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
|
|
1682
1837
|
} else {
|
|
1683
1838
|
setNudgeQuickReplies([]);
|
|
1684
1839
|
setActiveNudgeNodeId(void 0);
|
|
@@ -1692,6 +1847,11 @@ function XbNovaChat({
|
|
|
1692
1847
|
markDisclosureSeen();
|
|
1693
1848
|
}
|
|
1694
1849
|
setOpen(true);
|
|
1850
|
+
try {
|
|
1851
|
+
onOpen?.({ fromNudge: Boolean(consumed), hintText: consumed?.text });
|
|
1852
|
+
} catch (err) {
|
|
1853
|
+
console.warn("[XbNova] onOpen \uCF5C\uBC31 \uC624\uB958", err);
|
|
1854
|
+
}
|
|
1695
1855
|
}
|
|
1696
1856
|
if (!open) {
|
|
1697
1857
|
if (barHidden) return null;
|
|
@@ -1787,7 +1947,6 @@ function XbNovaChat({
|
|
|
1787
1947
|
const summary = resolveFactValue(factsRegistry.get(SUMMARY_FACT_KEY));
|
|
1788
1948
|
const focusedLabel = getFocusedTrackEntry(trackRegistry)?.label;
|
|
1789
1949
|
const contextLine = summary ?? (focusedLabel ? `${focusedLabel} \uBCF4\uB294 \uC911` : void 0);
|
|
1790
|
-
const firstBubble = openingLine ?? onboarding?.greeting;
|
|
1791
1950
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
1792
1951
|
!isDesktop ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1793
1952
|
XbNovaShadowRoot,
|
|
@@ -1824,7 +1983,10 @@ function XbNovaChat({
|
|
|
1824
1983
|
}
|
|
1825
1984
|
),
|
|
1826
1985
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
1827
|
-
/* @__PURE__ */ (0, import_jsx_runtime7.
|
|
1986
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex min-w-0 items-center gap-1.5", children: [
|
|
1987
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "truncate text-sm font-semibold", children: onboarding?.branding?.title ?? "AI \uB3D9\uD589" }),
|
|
1988
|
+
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
|
|
1989
|
+
] }),
|
|
1828
1990
|
contextLine ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "truncate text-[11px] text-neutral-400", children: contextLine }) : null
|
|
1829
1991
|
] }),
|
|
1830
1992
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
@@ -1840,7 +2002,7 @@ function XbNovaChat({
|
|
|
1840
2002
|
] }),
|
|
1841
2003
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [
|
|
1842
2004
|
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
|
-
|
|
2005
|
+
opening ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssistantMarkdown, { text: opening }) }) : null,
|
|
1844
2006
|
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
2007
|
AnswerActionsFrame,
|
|
1846
2008
|
{
|
|
@@ -1868,7 +2030,17 @@ function XbNovaChat({
|
|
|
1868
2030
|
);
|
|
1869
2031
|
}
|
|
1870
2032
|
const spec = clientActionHandlers.get(part.toolName);
|
|
1871
|
-
return spec?.render ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
2033
|
+
return spec?.render ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
2034
|
+
"div",
|
|
2035
|
+
{
|
|
2036
|
+
onClickCapture: (e) => {
|
|
2037
|
+
const control = clickedControl(e);
|
|
2038
|
+
if (control) trackClientActionClick(part.toolName, control.label);
|
|
2039
|
+
},
|
|
2040
|
+
children: spec.render(part.input)
|
|
2041
|
+
},
|
|
2042
|
+
j
|
|
2043
|
+
) : null;
|
|
1872
2044
|
}
|
|
1873
2045
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ToolView, { part, viewRenderers, onVerdictAction }, j);
|
|
1874
2046
|
}),
|
|
@@ -1939,7 +2111,8 @@ function XbNovaChat({
|
|
|
1939
2111
|
)
|
|
1940
2112
|
]
|
|
1941
2113
|
}
|
|
1942
|
-
)
|
|
2114
|
+
),
|
|
2115
|
+
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
2116
|
] })
|
|
1944
2117
|
}
|
|
1945
2118
|
)
|