@xbrain-swe/nova-chat-widget 0.1.10 → 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 +260 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -5
- package/dist/index.d.ts +57 -5
- package/dist/index.js +307 -104
- package/dist/index.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
XbNovaTrack: () => XbNovaTrack,
|
|
39
39
|
trackXbNovaEvent: () => trackXbNovaEvent,
|
|
40
40
|
useXbNovaChat: () => useXbNovaChat,
|
|
41
|
+
useXbNovaContext: () => useXbNovaContext,
|
|
41
42
|
useXbNovaFact: () => useXbNovaFact,
|
|
42
43
|
useXbNovaFacts: () => useXbNovaFacts,
|
|
43
44
|
useXbNovaSettings: () => useXbNovaSettings,
|
|
@@ -71,6 +72,7 @@ function useXbNovaInternal() {
|
|
|
71
72
|
return ctx;
|
|
72
73
|
}
|
|
73
74
|
var VISITOR_STORAGE_KEY = "xbnova_visitor_id";
|
|
75
|
+
var WIDGET_VIEW_WAIT_MS = 1e4;
|
|
74
76
|
function resolveVisitorId() {
|
|
75
77
|
if (typeof window === "undefined") return "";
|
|
76
78
|
try {
|
|
@@ -86,6 +88,17 @@ function resolveVisitorId() {
|
|
|
86
88
|
function resolveFactValue(value) {
|
|
87
89
|
return typeof value === "function" ? value() : value;
|
|
88
90
|
}
|
|
91
|
+
function collectContextEntries(registry) {
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const source of registry.values()) {
|
|
94
|
+
const entries = typeof source === "function" ? source() : source;
|
|
95
|
+
if (!Array.isArray(entries)) continue;
|
|
96
|
+
for (const e of entries) {
|
|
97
|
+
if (e && typeof e.title === "string" && typeof e.description === "string") out.push(e);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
89
102
|
function InternalProvider({
|
|
90
103
|
auth,
|
|
91
104
|
clientActionHandlers,
|
|
@@ -94,21 +107,54 @@ function InternalProvider({
|
|
|
94
107
|
}) {
|
|
95
108
|
const trackRegistry = (0, import_react.useRef)(/* @__PURE__ */ new Map()).current;
|
|
96
109
|
const factsRegistry = (0, import_react.useRef)(/* @__PURE__ */ new Map()).current;
|
|
110
|
+
const contextRegistry = (0, import_react.useRef)(/* @__PURE__ */ new Map()).current;
|
|
97
111
|
const emptyHandlers = (0, import_react.useRef)(/* @__PURE__ */ new Map()).current;
|
|
98
112
|
const visitorIdRef = (0, import_react.useRef)(null);
|
|
99
113
|
if (visitorIdRef.current === null) {
|
|
100
114
|
visitorIdRef.current = resolveVisitorId();
|
|
101
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]);
|
|
102
145
|
const value = (0, import_react.useMemo)(
|
|
103
146
|
() => ({
|
|
104
147
|
auth,
|
|
105
148
|
trackRegistry,
|
|
106
149
|
factsRegistry,
|
|
150
|
+
contextRegistry,
|
|
107
151
|
clientActionHandlers: clientActionHandlers ?? emptyHandlers,
|
|
108
|
-
visitorId
|
|
152
|
+
visitorId,
|
|
153
|
+
sessionId,
|
|
154
|
+
sendEvent,
|
|
109
155
|
theme
|
|
110
156
|
}),
|
|
111
|
-
[auth, trackRegistry, factsRegistry, clientActionHandlers, emptyHandlers, theme]
|
|
157
|
+
[auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, visitorId, sessionId, sendEvent, theme]
|
|
112
158
|
);
|
|
113
159
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InternalContext.Provider, { value, children });
|
|
114
160
|
}
|
|
@@ -411,7 +457,7 @@ function getFocusedTrackEntry(trackRegistry) {
|
|
|
411
457
|
}
|
|
412
458
|
|
|
413
459
|
// src/nudge-loop.tsx
|
|
414
|
-
var EVAL_INTERVAL_MS =
|
|
460
|
+
var EVAL_INTERVAL_MS = 250;
|
|
415
461
|
var XBNOVA_EVENT_NAME = "xbnova:event";
|
|
416
462
|
var SESSION_COUNT_KEY_PREFIX = "xbnova_hint_session_count:";
|
|
417
463
|
var SCROLL_SETTLE_MS = 500;
|
|
@@ -604,7 +650,7 @@ function clearDebugOverlay(refs) {
|
|
|
604
650
|
refs.bandLinesRef.current.clear();
|
|
605
651
|
}
|
|
606
652
|
function XbNovaNudgeLoop({ appId }) {
|
|
607
|
-
const { auth, trackRegistry, factsRegistry,
|
|
653
|
+
const { auth, trackRegistry, factsRegistry, sendEvent } = useXbNovaInternal();
|
|
608
654
|
const { apiKey, apiBaseUrl, screen } = auth;
|
|
609
655
|
const { hint, offerHint, autoHideHint, hintsEnabled } = useNudgeState();
|
|
610
656
|
const graphRef = (0, import_react5.useRef)(null);
|
|
@@ -637,8 +683,10 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
637
683
|
const debugEnabled = isDebugEnabled();
|
|
638
684
|
const debugRefs = { panelRef: debugPanelRef, boxesRef: debugBoxesRef, bandLinesRef: debugBandLinesRef };
|
|
639
685
|
if (!debugEnabled && debugPanelRef.current) clearDebugOverlay(debugRefs);
|
|
686
|
+
const tickDebug = debugEnabled ? (msg) => console.debug(msg) : () => {
|
|
687
|
+
};
|
|
640
688
|
if (!graph) {
|
|
641
|
-
|
|
689
|
+
tickDebug("[XbNova] nudge \uD3C9\uAC00 \uAC74\uB108\uB700: \uADF8\uB798\uD504\uB97C \uC544\uC9C1 \uBABB \uBD88\uB7EC\uC634");
|
|
642
690
|
return;
|
|
643
691
|
}
|
|
644
692
|
const canOffer = !hintRef.current && hintsEnabledRef.current;
|
|
@@ -689,18 +737,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
689
737
|
autoHideMs: messageNode.auto_hide_ms ?? 2e4,
|
|
690
738
|
quickReplies: messageNode.quick_replies ?? []
|
|
691
739
|
});
|
|
692
|
-
|
|
693
|
-
method: "POST",
|
|
694
|
-
headers: { "Content-Type": "application/json" },
|
|
695
|
-
body: JSON.stringify({
|
|
696
|
-
app_id: appId,
|
|
697
|
-
nudge_node_id: messageNode.id,
|
|
698
|
-
kind: "nudge_impression",
|
|
699
|
-
bot_id: botId,
|
|
700
|
-
visitor_id: visitorId
|
|
701
|
-
})
|
|
702
|
-
}).catch(() => {
|
|
703
|
-
});
|
|
740
|
+
sendEvent("nudge_impression", { nudge_node_id: messageNode.id, bot_id: botId });
|
|
704
741
|
};
|
|
705
742
|
const tryFireFrom = (node) => {
|
|
706
743
|
const nextEdge = (edgesFrom.get(node.id) ?? [])[0];
|
|
@@ -728,7 +765,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
728
765
|
const maxPerSession = node.max_per_session === 0 ? Infinity : node.max_per_session ?? 3;
|
|
729
766
|
const sessionCount = peekSessionCount(node.id);
|
|
730
767
|
if (sessionCount >= maxPerSession) {
|
|
731
|
-
|
|
768
|
+
tickDebug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id}(${node.trigger_type}) \uAC74\uB108\uB700: \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC`);
|
|
732
769
|
if (debugEnabled && node.trigger_type === "dwell") {
|
|
733
770
|
debugLines.push(`- [${node.id}] \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC \u2014 \uB354 \uC548 \uC7BC`);
|
|
734
771
|
}
|
|
@@ -763,7 +800,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
763
800
|
overlapPx = status.overlapPx;
|
|
764
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}`;
|
|
765
802
|
}
|
|
766
|
-
|
|
803
|
+
tickDebug(
|
|
767
804
|
`[XbNova] band dwell \uD2B8\uB9AC\uAC70=${node.id} region="${region}" band%=${fromPercent}~${toPercent} | ${statusDetail}`
|
|
768
805
|
);
|
|
769
806
|
dwellRecords.push({ node, state, requiredMs, region, found, screenMatches, inBand, overlapPx });
|
|
@@ -772,7 +809,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
772
809
|
const fires = triggerFires(node, { screen, firedEventName });
|
|
773
810
|
if (!fires) continue;
|
|
774
811
|
if (!canOffer) {
|
|
775
|
-
|
|
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`);
|
|
776
813
|
continue;
|
|
777
814
|
}
|
|
778
815
|
if (tryFireFrom(node)) {
|
|
@@ -872,7 +909,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
872
909
|
ensureDebugPanel(debugPanelRef, debugCollapsedRef).body.textContent = debugLines.join("\n");
|
|
873
910
|
}
|
|
874
911
|
},
|
|
875
|
-
[apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry,
|
|
912
|
+
[apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, sendEvent]
|
|
876
913
|
);
|
|
877
914
|
(0, import_react5.useEffect)(() => {
|
|
878
915
|
const interval = window.setInterval(() => evaluate(null), EVAL_INTERVAL_MS);
|
|
@@ -903,6 +940,9 @@ function XbNovaProvider({
|
|
|
903
940
|
apiBaseUrl = DEFAULT_API_BASE_URL,
|
|
904
941
|
clientActions,
|
|
905
942
|
theme,
|
|
943
|
+
userKey,
|
|
944
|
+
threadKey,
|
|
945
|
+
sessionKey,
|
|
906
946
|
children
|
|
907
947
|
}) {
|
|
908
948
|
(0, import_react6.useEffect)(() => {
|
|
@@ -919,8 +959,17 @@ function XbNovaProvider({
|
|
|
919
959
|
if (screen) bumpVisitCount(screen);
|
|
920
960
|
}, [screen]);
|
|
921
961
|
const auth = (0, import_react6.useMemo)(
|
|
922
|
-
() => ({
|
|
923
|
-
|
|
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]
|
|
924
973
|
);
|
|
925
974
|
const resolvedTheme = (0, import_react6.useMemo)(
|
|
926
975
|
() => ({ ...DEFAULT_THEME, ...theme }),
|
|
@@ -1026,12 +1075,41 @@ function XbNovaShadowRoot({
|
|
|
1026
1075
|
var import_react8 = require("react");
|
|
1027
1076
|
|
|
1028
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
|
+
}
|
|
1029
1106
|
async function consumeSse(response, handlers) {
|
|
1030
1107
|
if (!response.ok || !response.body) {
|
|
1031
1108
|
handlers.onError?.(`\uC694\uCCAD \uC2E4\uD328 (${response.status})`);
|
|
1032
1109
|
return null;
|
|
1033
1110
|
}
|
|
1034
1111
|
const conversationId = response.headers.get("x-conversation-id");
|
|
1112
|
+
if (conversationId) handlers.onConversationId?.(conversationId);
|
|
1035
1113
|
const reader = response.body.getReader();
|
|
1036
1114
|
const decoder = new TextDecoder();
|
|
1037
1115
|
let buffer = "";
|
|
@@ -1136,12 +1214,19 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
|
|
|
1136
1214
|
// src/use-chat.ts
|
|
1137
1215
|
var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
|
|
1138
1216
|
function useXbNovaChat(options) {
|
|
1139
|
-
const { auth, trackRegistry, clientActionHandlers, visitorId } = useXbNovaInternal();
|
|
1217
|
+
const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId, sessionId, sendEvent } = useXbNovaInternal();
|
|
1140
1218
|
const botId = options?.botId ?? auth.botId;
|
|
1141
1219
|
const nudgeNodeId = options?.nudgeNodeId;
|
|
1142
|
-
const { apiKey, apiBaseUrl } = auth;
|
|
1220
|
+
const { apiKey, apiBaseUrl, threadKey } = auth;
|
|
1221
|
+
const userKey = visitorId;
|
|
1143
1222
|
const [onboarding, setOnboarding] = (0, import_react8.useState)(null);
|
|
1144
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;
|
|
1145
1230
|
const [busy, setBusy] = (0, import_react8.useState)(false);
|
|
1146
1231
|
const [error, setError] = (0, import_react8.useState)(null);
|
|
1147
1232
|
const [clientResults, setClientResults] = (0, import_react8.useState)({});
|
|
@@ -1152,18 +1237,59 @@ function useXbNovaChat(options) {
|
|
|
1152
1237
|
const nudgeNodeIdRef = (0, import_react8.useRef)(nudgeNodeId);
|
|
1153
1238
|
nudgeNodeIdRef.current = nudgeNodeId;
|
|
1154
1239
|
(0, import_react8.useEffect)(() => {
|
|
1240
|
+
let cancelled = false;
|
|
1155
1241
|
setOnboarding(null);
|
|
1156
1242
|
setMessages([]);
|
|
1243
|
+
setResumed(false);
|
|
1244
|
+
setSentOpening(null);
|
|
1157
1245
|
setError(null);
|
|
1158
1246
|
setClientResults({});
|
|
1159
1247
|
setFeedbackState({});
|
|
1160
1248
|
dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
|
|
1249
|
+
conversationIdRef.current = null;
|
|
1250
|
+
pendingOpeningRef.current = null;
|
|
1161
1251
|
fetch(`${apiBaseUrl}/v1/conversations`, {
|
|
1162
1252
|
method: "POST",
|
|
1163
1253
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
1164
|
-
body: JSON.stringify({
|
|
1165
|
-
|
|
1166
|
-
|
|
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]);
|
|
1167
1293
|
const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
|
|
1168
1294
|
const next = [...prev];
|
|
1169
1295
|
next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
|
|
@@ -1176,7 +1302,7 @@ function useXbNovaChat(options) {
|
|
|
1176
1302
|
return next;
|
|
1177
1303
|
});
|
|
1178
1304
|
async function setFeedback(messageId, rating, reason) {
|
|
1179
|
-
const conversationId =
|
|
1305
|
+
const conversationId = conversationIdRef.current;
|
|
1180
1306
|
if (!conversationId || !messageId) return;
|
|
1181
1307
|
const prevRating = feedbackRef.current[messageId] ?? null;
|
|
1182
1308
|
const apply = (r) => setFeedbackState((prev) => {
|
|
@@ -1202,6 +1328,14 @@ function useXbNovaChat(options) {
|
|
|
1202
1328
|
apply(prevRating);
|
|
1203
1329
|
}
|
|
1204
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
|
+
}
|
|
1205
1339
|
async function runClientAction(spec, part, existingParts) {
|
|
1206
1340
|
if (spec.dismissSheet) options?.onDismissSheet?.();
|
|
1207
1341
|
let result;
|
|
@@ -1225,7 +1359,7 @@ function useXbNovaChat(options) {
|
|
|
1225
1359
|
result = e instanceof Error ? `\uC624\uB958: ${e.message}` : "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
|
|
1226
1360
|
}
|
|
1227
1361
|
if (!spec.awaitResult) return;
|
|
1228
|
-
const conversationId =
|
|
1362
|
+
const conversationId = conversationIdRef.current;
|
|
1229
1363
|
if (!conversationId) return;
|
|
1230
1364
|
const structured = result && typeof result === "object" ? result : null;
|
|
1231
1365
|
if (structured?.view) {
|
|
@@ -1269,6 +1403,7 @@ function useXbNovaChat(options) {
|
|
|
1269
1403
|
if (!spec) continue;
|
|
1270
1404
|
if (dispatchedClientActionsRef.current.has(part.toolCallId)) continue;
|
|
1271
1405
|
dispatchedClientActionsRef.current.add(part.toolCallId);
|
|
1406
|
+
if (spec.render) postClientActionEvent("cta_impression", part.toolName, null);
|
|
1272
1407
|
void runClientAction(spec, part, last.parts);
|
|
1273
1408
|
}
|
|
1274
1409
|
}, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
|
|
@@ -1288,7 +1423,17 @@ function useXbNovaChat(options) {
|
|
|
1288
1423
|
return next;
|
|
1289
1424
|
});
|
|
1290
1425
|
const focused = getFocusedTrackEntry(trackRegistry);
|
|
1291
|
-
const
|
|
1426
|
+
const notes = collectContextEntries(contextRegistry);
|
|
1427
|
+
const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
|
|
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;
|
|
1292
1437
|
try {
|
|
1293
1438
|
await streamChat(
|
|
1294
1439
|
apiBaseUrl,
|
|
@@ -1296,11 +1441,22 @@ function useXbNovaChat(options) {
|
|
|
1296
1441
|
{
|
|
1297
1442
|
bot_id: botId,
|
|
1298
1443
|
message: trimmed,
|
|
1299
|
-
conversation_id:
|
|
1444
|
+
conversation_id: conversationIdRef.current,
|
|
1300
1445
|
context,
|
|
1301
|
-
nudge_node_id: nudgeNodeId ?? null
|
|
1446
|
+
nudge_node_id: nudgeNodeId ?? null,
|
|
1447
|
+
opening_message: openingMessage2,
|
|
1448
|
+
visitor_id: userKey || null,
|
|
1449
|
+
thread_key: threadKey ?? null,
|
|
1450
|
+
session_id: sessionId
|
|
1302
1451
|
},
|
|
1303
|
-
{
|
|
1452
|
+
{
|
|
1453
|
+
onPart: replaceLast,
|
|
1454
|
+
onMessageId: setLastAssistantId,
|
|
1455
|
+
onConversationId: (id) => {
|
|
1456
|
+
conversationIdRef.current = id;
|
|
1457
|
+
},
|
|
1458
|
+
onError: setError
|
|
1459
|
+
}
|
|
1304
1460
|
);
|
|
1305
1461
|
} catch (e) {
|
|
1306
1462
|
setError(e instanceof Error ? e.message : String(e));
|
|
@@ -1308,7 +1464,19 @@ function useXbNovaChat(options) {
|
|
|
1308
1464
|
setBusy(false);
|
|
1309
1465
|
}
|
|
1310
1466
|
}
|
|
1311
|
-
|
|
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
|
+
};
|
|
1312
1480
|
}
|
|
1313
1481
|
|
|
1314
1482
|
// src/views.tsx
|
|
@@ -1441,6 +1609,13 @@ function hasVisiblePart(parts, clientActionHandlers, clientResults) {
|
|
|
1441
1609
|
}
|
|
1442
1610
|
var DISCLOSURE_SEEN_KEY = "xbnova_disclosure_seen";
|
|
1443
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
|
+
}
|
|
1444
1619
|
function readDisclosureSeen() {
|
|
1445
1620
|
if (typeof window === "undefined") return true;
|
|
1446
1621
|
try {
|
|
@@ -1570,9 +1745,10 @@ function XbNovaChat({
|
|
|
1570
1745
|
avatarSize = 28,
|
|
1571
1746
|
onVerdictAction,
|
|
1572
1747
|
feedback: feedbackEnabled = true,
|
|
1573
|
-
onFeedback
|
|
1748
|
+
onFeedback,
|
|
1749
|
+
onOpen
|
|
1574
1750
|
}) {
|
|
1575
|
-
const { auth, trackRegistry, factsRegistry,
|
|
1751
|
+
const { auth, trackRegistry, factsRegistry, clientActionHandlers, theme, sendEvent } = useXbNovaInternal();
|
|
1576
1752
|
const themeVars = {
|
|
1577
1753
|
"--xbnova-accent-1": theme.accentFrom,
|
|
1578
1754
|
"--xbnova-accent-2": theme.accentTo
|
|
@@ -1593,10 +1769,11 @@ function XbNovaChat({
|
|
|
1593
1769
|
const [input, setInput] = (0, import_react9.useState)("");
|
|
1594
1770
|
const bottomRef = (0, import_react9.useRef)(null);
|
|
1595
1771
|
const typedHintText = useTypewriter(hint?.text ?? "", hint?.triggerNodeId ?? "");
|
|
1596
|
-
const { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback } = useXbNovaChat({
|
|
1772
|
+
const { onboarding, opening, messages, busy, error, send, clientResults, feedback, setFeedback, trackClientActionClick } = useXbNovaChat({
|
|
1597
1773
|
onFeedback,
|
|
1598
1774
|
botId: activeBotId ?? botId,
|
|
1599
1775
|
nudgeNodeId: activeNudgeNodeId,
|
|
1776
|
+
openingMessage: openingLine ?? void 0,
|
|
1600
1777
|
onDismissSheet: () => setOpen(false)
|
|
1601
1778
|
});
|
|
1602
1779
|
const effectiveBotId = activeBotId ?? botId;
|
|
@@ -1614,12 +1791,19 @@ function XbNovaChat({
|
|
|
1614
1791
|
try {
|
|
1615
1792
|
window.localStorage.setItem(
|
|
1616
1793
|
`xbnova_branding:${effectiveBotId}`,
|
|
1617
|
-
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
|
+
})
|
|
1618
1800
|
);
|
|
1619
1801
|
} catch {
|
|
1620
1802
|
}
|
|
1621
1803
|
}, [onboarding?.branding, effectiveBotId]);
|
|
1622
1804
|
const branding = onboarding?.branding ?? cachedBranding ?? void 0;
|
|
1805
|
+
const badgeText = branding?.badge_text?.trim() ?? "";
|
|
1806
|
+
const disclaimer = branding?.disclaimer?.trim() ?? "";
|
|
1623
1807
|
const brandingReady = Boolean(onboarding) || cachedBranding !== null || Boolean(error);
|
|
1624
1808
|
(0, import_react9.useEffect)(() => {
|
|
1625
1809
|
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
@@ -1649,18 +1833,7 @@ function XbNovaChat({
|
|
|
1649
1833
|
setOpeningLine(consumed.text);
|
|
1650
1834
|
setActiveNudgeNodeId(consumed.messageNodeId);
|
|
1651
1835
|
setNudgeQuickReplies(consumed.quickReplies);
|
|
1652
|
-
|
|
1653
|
-
method: "POST",
|
|
1654
|
-
headers: { "Content-Type": "application/json" },
|
|
1655
|
-
body: JSON.stringify({
|
|
1656
|
-
app_id: auth.appId,
|
|
1657
|
-
nudge_node_id: consumed.messageNodeId,
|
|
1658
|
-
kind: "nudge_open",
|
|
1659
|
-
bot_id: consumed.botId,
|
|
1660
|
-
visitor_id: visitorId
|
|
1661
|
-
})
|
|
1662
|
-
}).catch(() => {
|
|
1663
|
-
});
|
|
1836
|
+
sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
|
|
1664
1837
|
} else {
|
|
1665
1838
|
setNudgeQuickReplies([]);
|
|
1666
1839
|
setActiveNudgeNodeId(void 0);
|
|
@@ -1674,6 +1847,11 @@ function XbNovaChat({
|
|
|
1674
1847
|
markDisclosureSeen();
|
|
1675
1848
|
}
|
|
1676
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
|
+
}
|
|
1677
1855
|
}
|
|
1678
1856
|
if (!open) {
|
|
1679
1857
|
if (barHidden) return null;
|
|
@@ -1769,7 +1947,6 @@ function XbNovaChat({
|
|
|
1769
1947
|
const summary = resolveFactValue(factsRegistry.get(SUMMARY_FACT_KEY));
|
|
1770
1948
|
const focusedLabel = getFocusedTrackEntry(trackRegistry)?.label;
|
|
1771
1949
|
const contextLine = summary ?? (focusedLabel ? `${focusedLabel} \uBCF4\uB294 \uC911` : void 0);
|
|
1772
|
-
const firstBubble = openingLine ?? onboarding?.greeting;
|
|
1773
1950
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
1774
1951
|
!isDesktop ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1775
1952
|
XbNovaShadowRoot,
|
|
@@ -1806,7 +1983,10 @@ function XbNovaChat({
|
|
|
1806
1983
|
}
|
|
1807
1984
|
),
|
|
1808
1985
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
1809
|
-
/* @__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
|
+
] }),
|
|
1810
1990
|
contextLine ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "truncate text-[11px] text-neutral-400", children: contextLine }) : null
|
|
1811
1991
|
] }),
|
|
1812
1992
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
@@ -1822,7 +2002,7 @@ function XbNovaChat({
|
|
|
1822
2002
|
] }),
|
|
1823
2003
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [
|
|
1824
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,
|
|
1825
|
-
|
|
2005
|
+
opening ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssistantMarkdown, { text: opening }) }) : null,
|
|
1826
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)(
|
|
1827
2007
|
AnswerActionsFrame,
|
|
1828
2008
|
{
|
|
@@ -1850,7 +2030,17 @@ function XbNovaChat({
|
|
|
1850
2030
|
);
|
|
1851
2031
|
}
|
|
1852
2032
|
const spec = clientActionHandlers.get(part.toolName);
|
|
1853
|
-
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;
|
|
1854
2044
|
}
|
|
1855
2045
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ToolView, { part, viewRenderers, onVerdictAction }, j);
|
|
1856
2046
|
}),
|
|
@@ -1921,7 +2111,8 @@ function XbNovaChat({
|
|
|
1921
2111
|
)
|
|
1922
2112
|
]
|
|
1923
2113
|
}
|
|
1924
|
-
)
|
|
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
|
|
1925
2116
|
] })
|
|
1926
2117
|
}
|
|
1927
2118
|
)
|
|
@@ -2200,6 +2391,18 @@ function useXbNovaFacts(values) {
|
|
|
2200
2391
|
};
|
|
2201
2392
|
}, [factsRegistry]);
|
|
2202
2393
|
}
|
|
2394
|
+
function useXbNovaContext(entries) {
|
|
2395
|
+
const { contextRegistry } = useXbNovaInternal();
|
|
2396
|
+
const id = (0, import_react10.useId)();
|
|
2397
|
+
(0, import_react10.useEffect)(() => {
|
|
2398
|
+
contextRegistry.set(id, entries);
|
|
2399
|
+
});
|
|
2400
|
+
(0, import_react10.useEffect)(() => {
|
|
2401
|
+
return () => {
|
|
2402
|
+
contextRegistry.delete(id);
|
|
2403
|
+
};
|
|
2404
|
+
}, [id, contextRegistry]);
|
|
2405
|
+
}
|
|
2203
2406
|
function useXbNovaVisitor() {
|
|
2204
2407
|
const { visitorId } = useXbNovaInternal();
|
|
2205
2408
|
return visitorId;
|
|
@@ -2221,6 +2424,7 @@ function XbNovaFacts({ values }) {
|
|
|
2221
2424
|
XbNovaTrack,
|
|
2222
2425
|
trackXbNovaEvent,
|
|
2223
2426
|
useXbNovaChat,
|
|
2427
|
+
useXbNovaContext,
|
|
2224
2428
|
useXbNovaFact,
|
|
2225
2429
|
useXbNovaFacts,
|
|
2226
2430
|
useXbNovaSettings,
|