@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.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/provider.tsx
|
|
4
|
-
import { useEffect as
|
|
4
|
+
import { useEffect as useEffect6, useMemo as useMemo2 } from "react";
|
|
5
5
|
|
|
6
6
|
// src/client-actions.tsx
|
|
7
|
-
import { useEffect, useRef as useRef2 } from "react";
|
|
7
|
+
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
8
8
|
|
|
9
9
|
// src/internal-context.tsx
|
|
10
|
-
import { createContext, useContext, useMemo, useRef } from "react";
|
|
10
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef } from "react";
|
|
11
11
|
import { jsx } from "react/jsx-runtime";
|
|
12
12
|
var DEFAULT_THEME = {
|
|
13
13
|
accentFrom: "#4f46e5",
|
|
@@ -25,6 +25,7 @@ function useXbNovaInternal() {
|
|
|
25
25
|
return ctx;
|
|
26
26
|
}
|
|
27
27
|
var VISITOR_STORAGE_KEY = "xbnova_visitor_id";
|
|
28
|
+
var WIDGET_VIEW_WAIT_MS = 1e4;
|
|
28
29
|
function resolveVisitorId() {
|
|
29
30
|
if (typeof window === "undefined") return "";
|
|
30
31
|
try {
|
|
@@ -65,6 +66,44 @@ function InternalProvider({
|
|
|
65
66
|
if (visitorIdRef.current === null) {
|
|
66
67
|
visitorIdRef.current = resolveVisitorId();
|
|
67
68
|
}
|
|
69
|
+
const visitorId = auth.userKey || visitorIdRef.current;
|
|
70
|
+
const sessionId = auth.sessionKey || null;
|
|
71
|
+
const latestRef = useRef({ auth, visitorId, sessionId });
|
|
72
|
+
latestRef.current = { auth, visitorId, sessionId };
|
|
73
|
+
const sendEvent = useCallback((event, properties = {}) => {
|
|
74
|
+
const { auth: a, visitorId: v, sessionId: sid } = latestRef.current;
|
|
75
|
+
if (!v) return;
|
|
76
|
+
const { conversation_id: conversationId, ...rest } = properties;
|
|
77
|
+
fetch(`${a.apiBaseUrl}/v1/events`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
keepalive: true,
|
|
80
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${a.apiKey}` },
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
app_id: a.appId,
|
|
83
|
+
event,
|
|
84
|
+
visitor_id: v,
|
|
85
|
+
session_id: sid,
|
|
86
|
+
conversation_id: conversationId ?? null,
|
|
87
|
+
properties: { screen: a.screen, ...rest }
|
|
88
|
+
})
|
|
89
|
+
}).catch(() => {
|
|
90
|
+
});
|
|
91
|
+
}, []);
|
|
92
|
+
const widgetViewSentRef = useRef(false);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (widgetViewSentRef.current) return;
|
|
95
|
+
const send = () => {
|
|
96
|
+
if (widgetViewSentRef.current) return;
|
|
97
|
+
widgetViewSentRef.current = true;
|
|
98
|
+
sendEvent("widget_view");
|
|
99
|
+
};
|
|
100
|
+
if (sessionId) {
|
|
101
|
+
send();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const timer = setTimeout(send, WIDGET_VIEW_WAIT_MS);
|
|
105
|
+
return () => clearTimeout(timer);
|
|
106
|
+
}, [sessionId, sendEvent]);
|
|
68
107
|
const value = useMemo(
|
|
69
108
|
() => ({
|
|
70
109
|
auth,
|
|
@@ -72,10 +111,12 @@ function InternalProvider({
|
|
|
72
111
|
factsRegistry,
|
|
73
112
|
contextRegistry,
|
|
74
113
|
clientActionHandlers: clientActionHandlers ?? emptyHandlers,
|
|
75
|
-
visitorId
|
|
114
|
+
visitorId,
|
|
115
|
+
sessionId,
|
|
116
|
+
sendEvent,
|
|
76
117
|
theme
|
|
77
118
|
}),
|
|
78
|
-
[auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, theme]
|
|
119
|
+
[auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, visitorId, sessionId, sendEvent, theme]
|
|
79
120
|
);
|
|
80
121
|
return /* @__PURE__ */ jsx(InternalContext.Provider, { value, children });
|
|
81
122
|
}
|
|
@@ -96,7 +137,7 @@ function XbNovaClientActionSync({ actions }) {
|
|
|
96
137
|
const { auth } = useXbNovaInternal();
|
|
97
138
|
const { apiKey, apiBaseUrl, appId } = auth;
|
|
98
139
|
const lastSyncedRef = useRef2(null);
|
|
99
|
-
|
|
140
|
+
useEffect2(() => {
|
|
100
141
|
if (!actions || Object.keys(actions).length === 0) return;
|
|
101
142
|
const serialized = JSON.stringify(toSyncPayload(actions));
|
|
102
143
|
if (serialized === lastSyncedRef.current) return;
|
|
@@ -150,7 +191,7 @@ function peekVisitCount(screen) {
|
|
|
150
191
|
}
|
|
151
192
|
|
|
152
193
|
// src/nudge-context.tsx
|
|
153
|
-
import { createContext as createContext2, useCallback, useContext as useContext2, useEffect as
|
|
194
|
+
import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useEffect as useEffect3, useRef as useRef3, useState } from "react";
|
|
154
195
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
155
196
|
var NudgeContext = createContext2(null);
|
|
156
197
|
var HINTS_DISABLED_KEY = "xbnova_hints_disabled";
|
|
@@ -194,43 +235,43 @@ function NudgeStateProvider({ children }) {
|
|
|
194
235
|
const [hintsEnabled, setHintsEnabled] = useState(() => !readFlag(HINTS_DISABLED_KEY));
|
|
195
236
|
const [barHidden, setBarHidden] = useState(() => readFlag(BAR_HIDDEN_KEY));
|
|
196
237
|
const streakRef = useRef3(readStreak());
|
|
197
|
-
const disableHints =
|
|
238
|
+
const disableHints = useCallback2(() => {
|
|
198
239
|
setHintsEnabled(false);
|
|
199
240
|
writeFlag(HINTS_DISABLED_KEY, true);
|
|
200
241
|
}, []);
|
|
201
|
-
const enableHints =
|
|
242
|
+
const enableHints = useCallback2(() => {
|
|
202
243
|
setHintsEnabled(true);
|
|
203
244
|
writeFlag(HINTS_DISABLED_KEY, false);
|
|
204
245
|
streakRef.current = 0;
|
|
205
246
|
writeStreak(0);
|
|
206
247
|
}, []);
|
|
207
|
-
const hideBar =
|
|
248
|
+
const hideBar = useCallback2(() => {
|
|
208
249
|
setBarHidden(true);
|
|
209
250
|
writeFlag(BAR_HIDDEN_KEY, true);
|
|
210
251
|
}, []);
|
|
211
|
-
const showBar =
|
|
252
|
+
const showBar = useCallback2(() => {
|
|
212
253
|
setBarHidden(false);
|
|
213
254
|
writeFlag(BAR_HIDDEN_KEY, false);
|
|
214
255
|
}, []);
|
|
215
|
-
const offerHint =
|
|
256
|
+
const offerHint = useCallback2((next) => {
|
|
216
257
|
setHint((current) => current ? current : next);
|
|
217
258
|
}, []);
|
|
218
|
-
|
|
259
|
+
useEffect3(() => {
|
|
219
260
|
if (hint) setLastHint(hint);
|
|
220
261
|
}, [hint]);
|
|
221
|
-
const dismissHint =
|
|
262
|
+
const dismissHint = useCallback2(() => {
|
|
222
263
|
const limit = hint?.autoDisableAfterDismissals ?? DEFAULT_DISMISS_STREAK_LIMIT;
|
|
223
264
|
setHint(null);
|
|
224
265
|
streakRef.current += 1;
|
|
225
266
|
writeStreak(streakRef.current);
|
|
226
267
|
if (streakRef.current >= limit) disableHints();
|
|
227
268
|
}, [hint, disableHints]);
|
|
228
|
-
const openHint =
|
|
269
|
+
const openHint = useCallback2(() => {
|
|
229
270
|
setHint(null);
|
|
230
271
|
streakRef.current = 0;
|
|
231
272
|
writeStreak(0);
|
|
232
273
|
}, []);
|
|
233
|
-
const autoHideHint =
|
|
274
|
+
const autoHideHint = useCallback2(() => {
|
|
234
275
|
setHint(null);
|
|
235
276
|
}, []);
|
|
236
277
|
return /* @__PURE__ */ jsx2(
|
|
@@ -267,10 +308,10 @@ function useXbNovaSettings() {
|
|
|
267
308
|
}
|
|
268
309
|
|
|
269
310
|
// src/nudge-loop.tsx
|
|
270
|
-
import { useCallback as
|
|
311
|
+
import { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef5 } from "react";
|
|
271
312
|
|
|
272
313
|
// src/track.tsx
|
|
273
|
-
import { useCallback as
|
|
314
|
+
import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef4 } from "react";
|
|
274
315
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
275
316
|
var VISIBILITY_THRESHOLD = 0;
|
|
276
317
|
function finalizeIfVisible(registry, region) {
|
|
@@ -287,7 +328,7 @@ function useXbNovaTrack(region, data, label) {
|
|
|
287
328
|
dataRef.current = data;
|
|
288
329
|
const labelRef = useRef4(label);
|
|
289
330
|
labelRef.current = label;
|
|
290
|
-
|
|
331
|
+
useEffect4(() => {
|
|
291
332
|
const entry = trackRegistry.get(region);
|
|
292
333
|
if (entry) {
|
|
293
334
|
entry.data = data;
|
|
@@ -296,7 +337,7 @@ function useXbNovaTrack(region, data, label) {
|
|
|
296
337
|
trackRegistry.set(region, { region, data, label, enteredAt: null, totalMs: 0, node: null });
|
|
297
338
|
}
|
|
298
339
|
}, [region, data, label, trackRegistry]);
|
|
299
|
-
|
|
340
|
+
useEffect4(() => {
|
|
300
341
|
return () => {
|
|
301
342
|
observerRef.current?.disconnect();
|
|
302
343
|
finalizeIfVisible(trackRegistry, region);
|
|
@@ -304,7 +345,7 @@ function useXbNovaTrack(region, data, label) {
|
|
|
304
345
|
if (entry) entry.node = null;
|
|
305
346
|
};
|
|
306
347
|
}, [region, trackRegistry]);
|
|
307
|
-
return
|
|
348
|
+
return useCallback3(
|
|
308
349
|
(node) => {
|
|
309
350
|
observerRef.current?.disconnect();
|
|
310
351
|
observerRef.current = null;
|
|
@@ -378,7 +419,7 @@ function getFocusedTrackEntry(trackRegistry) {
|
|
|
378
419
|
}
|
|
379
420
|
|
|
380
421
|
// src/nudge-loop.tsx
|
|
381
|
-
var EVAL_INTERVAL_MS =
|
|
422
|
+
var EVAL_INTERVAL_MS = 250;
|
|
382
423
|
var XBNOVA_EVENT_NAME = "xbnova:event";
|
|
383
424
|
var SESSION_COUNT_KEY_PREFIX = "xbnova_hint_session_count:";
|
|
384
425
|
var SCROLL_SETTLE_MS = 500;
|
|
@@ -571,7 +612,7 @@ function clearDebugOverlay(refs) {
|
|
|
571
612
|
refs.bandLinesRef.current.clear();
|
|
572
613
|
}
|
|
573
614
|
function XbNovaNudgeLoop({ appId }) {
|
|
574
|
-
const { auth, trackRegistry, factsRegistry,
|
|
615
|
+
const { auth, trackRegistry, factsRegistry, sendEvent } = useXbNovaInternal();
|
|
575
616
|
const { apiKey, apiBaseUrl, screen } = auth;
|
|
576
617
|
const { hint, offerHint, autoHideHint, hintsEnabled } = useNudgeState();
|
|
577
618
|
const graphRef = useRef5(null);
|
|
@@ -586,7 +627,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
586
627
|
hintRef.current = hint;
|
|
587
628
|
const hintsEnabledRef = useRef5(hintsEnabled);
|
|
588
629
|
hintsEnabledRef.current = hintsEnabled;
|
|
589
|
-
|
|
630
|
+
useEffect5(() => {
|
|
590
631
|
let cancelled = false;
|
|
591
632
|
fetch(`${apiBaseUrl}/v1/apps/${appId}/nudge-graph`, {
|
|
592
633
|
headers: { Authorization: `Bearer ${apiKey}` }
|
|
@@ -598,14 +639,16 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
598
639
|
cancelled = true;
|
|
599
640
|
};
|
|
600
641
|
}, [apiBaseUrl, apiKey, appId]);
|
|
601
|
-
const evaluate =
|
|
642
|
+
const evaluate = useCallback4(
|
|
602
643
|
(firedEventName) => {
|
|
603
644
|
const graph = graphRef.current;
|
|
604
645
|
const debugEnabled = isDebugEnabled();
|
|
605
646
|
const debugRefs = { panelRef: debugPanelRef, boxesRef: debugBoxesRef, bandLinesRef: debugBandLinesRef };
|
|
606
647
|
if (!debugEnabled && debugPanelRef.current) clearDebugOverlay(debugRefs);
|
|
648
|
+
const tickDebug = debugEnabled ? (msg) => console.debug(msg) : () => {
|
|
649
|
+
};
|
|
607
650
|
if (!graph) {
|
|
608
|
-
|
|
651
|
+
tickDebug("[XbNova] nudge \uD3C9\uAC00 \uAC74\uB108\uB700: \uADF8\uB798\uD504\uB97C \uC544\uC9C1 \uBABB \uBD88\uB7EC\uC634");
|
|
609
652
|
return;
|
|
610
653
|
}
|
|
611
654
|
const canOffer = !hintRef.current && hintsEnabledRef.current;
|
|
@@ -656,18 +699,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
656
699
|
autoHideMs: messageNode.auto_hide_ms ?? 2e4,
|
|
657
700
|
quickReplies: messageNode.quick_replies ?? []
|
|
658
701
|
});
|
|
659
|
-
|
|
660
|
-
method: "POST",
|
|
661
|
-
headers: { "Content-Type": "application/json" },
|
|
662
|
-
body: JSON.stringify({
|
|
663
|
-
app_id: appId,
|
|
664
|
-
nudge_node_id: messageNode.id,
|
|
665
|
-
kind: "nudge_impression",
|
|
666
|
-
bot_id: botId,
|
|
667
|
-
visitor_id: visitorId
|
|
668
|
-
})
|
|
669
|
-
}).catch(() => {
|
|
670
|
-
});
|
|
702
|
+
sendEvent("nudge_impression", { nudge_node_id: messageNode.id, bot_id: botId });
|
|
671
703
|
};
|
|
672
704
|
const tryFireFrom = (node) => {
|
|
673
705
|
const nextEdge = (edgesFrom.get(node.id) ?? [])[0];
|
|
@@ -695,7 +727,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
695
727
|
const maxPerSession = node.max_per_session === 0 ? Infinity : node.max_per_session ?? 3;
|
|
696
728
|
const sessionCount = peekSessionCount(node.id);
|
|
697
729
|
if (sessionCount >= maxPerSession) {
|
|
698
|
-
|
|
730
|
+
tickDebug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id}(${node.trigger_type}) \uAC74\uB108\uB700: \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC`);
|
|
699
731
|
if (debugEnabled && node.trigger_type === "dwell") {
|
|
700
732
|
debugLines.push(`- [${node.id}] \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC \u2014 \uB354 \uC548 \uC7BC`);
|
|
701
733
|
}
|
|
@@ -730,7 +762,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
730
762
|
overlapPx = status.overlapPx;
|
|
731
763
|
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}`;
|
|
732
764
|
}
|
|
733
|
-
|
|
765
|
+
tickDebug(
|
|
734
766
|
`[XbNova] band dwell \uD2B8\uB9AC\uAC70=${node.id} region="${region}" band%=${fromPercent}~${toPercent} | ${statusDetail}`
|
|
735
767
|
);
|
|
736
768
|
dwellRecords.push({ node, state, requiredMs, region, found, screenMatches, inBand, overlapPx });
|
|
@@ -739,7 +771,7 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
739
771
|
const fires = triggerFires(node, { screen, firedEventName });
|
|
740
772
|
if (!fires) continue;
|
|
741
773
|
if (!canOffer) {
|
|
742
|
-
|
|
774
|
+
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`);
|
|
743
775
|
continue;
|
|
744
776
|
}
|
|
745
777
|
if (tryFireFrom(node)) {
|
|
@@ -839,9 +871,9 @@ function XbNovaNudgeLoop({ appId }) {
|
|
|
839
871
|
ensureDebugPanel(debugPanelRef, debugCollapsedRef).body.textContent = debugLines.join("\n");
|
|
840
872
|
}
|
|
841
873
|
},
|
|
842
|
-
[apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry,
|
|
874
|
+
[apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, sendEvent]
|
|
843
875
|
);
|
|
844
|
-
|
|
876
|
+
useEffect5(() => {
|
|
845
877
|
const interval = window.setInterval(() => evaluate(null), EVAL_INTERVAL_MS);
|
|
846
878
|
const onEvent = (e) => evaluate(e.detail.eventName);
|
|
847
879
|
window.addEventListener(XBNOVA_EVENT_NAME, onEvent);
|
|
@@ -870,24 +902,36 @@ function XbNovaProvider({
|
|
|
870
902
|
apiBaseUrl = DEFAULT_API_BASE_URL,
|
|
871
903
|
clientActions,
|
|
872
904
|
theme,
|
|
905
|
+
userKey,
|
|
906
|
+
threadKey,
|
|
907
|
+
sessionKey,
|
|
873
908
|
children
|
|
874
909
|
}) {
|
|
875
|
-
|
|
910
|
+
useEffect6(() => {
|
|
876
911
|
if (apiKey.startsWith("sk_")) {
|
|
877
912
|
console.error(
|
|
878
913
|
"[XbNova] sk_ \uD0A4\uB97C \uBE0C\uB77C\uC6B0\uC800\uC5D0 \uB178\uCD9C\uD588\uC2B5\uB2C8\uB2E4 \u2014 pk_(publishable) \uD0A4\uB97C \uC4F0\uC138\uC694. sk_\uB294 \uC11C\uBC84 \uC804\uC6A9\uC785\uB2C8\uB2E4."
|
|
879
914
|
);
|
|
880
915
|
}
|
|
881
916
|
}, [apiKey]);
|
|
882
|
-
|
|
917
|
+
useEffect6(() => {
|
|
883
918
|
markSessionStart();
|
|
884
919
|
}, []);
|
|
885
|
-
|
|
920
|
+
useEffect6(() => {
|
|
886
921
|
if (screen) bumpVisitCount(screen);
|
|
887
922
|
}, [screen]);
|
|
888
923
|
const auth = useMemo2(
|
|
889
|
-
() => ({
|
|
890
|
-
|
|
924
|
+
() => ({
|
|
925
|
+
apiKey,
|
|
926
|
+
botId,
|
|
927
|
+
appId,
|
|
928
|
+
apiBaseUrl,
|
|
929
|
+
screen,
|
|
930
|
+
userKey: userKey || void 0,
|
|
931
|
+
threadKey: threadKey || void 0,
|
|
932
|
+
sessionKey: sessionKey || void 0
|
|
933
|
+
}),
|
|
934
|
+
[apiKey, botId, appId, apiBaseUrl, screen, userKey, threadKey, sessionKey]
|
|
891
935
|
);
|
|
892
936
|
const resolvedTheme = useMemo2(
|
|
893
937
|
() => ({ ...DEFAULT_THEME, ...theme }),
|
|
@@ -916,13 +960,13 @@ function XbNovaProvider({
|
|
|
916
960
|
}
|
|
917
961
|
|
|
918
962
|
// src/chat.tsx
|
|
919
|
-
import { useEffect as
|
|
963
|
+
import { useEffect as useEffect9, useRef as useRef8, useState as useState4 } from "react";
|
|
920
964
|
import ReactMarkdown from "react-markdown";
|
|
921
965
|
import remarkBreaks from "remark-breaks";
|
|
922
966
|
import remarkGfm from "remark-gfm";
|
|
923
967
|
|
|
924
968
|
// src/shadow-root.tsx
|
|
925
|
-
import { useEffect as
|
|
969
|
+
import { useEffect as useEffect7, useRef as useRef6, useState as useState2 } from "react";
|
|
926
970
|
import { createPortal } from "react-dom";
|
|
927
971
|
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
928
972
|
var cssPromise = null;
|
|
@@ -938,7 +982,7 @@ function loadCss() {
|
|
|
938
982
|
}
|
|
939
983
|
function useResolvedScheme(scheme) {
|
|
940
984
|
const [systemDark, setSystemDark] = useState2(false);
|
|
941
|
-
|
|
985
|
+
useEffect7(() => {
|
|
942
986
|
if (scheme !== "system") return;
|
|
943
987
|
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
944
988
|
setSystemDark(mq.matches);
|
|
@@ -958,10 +1002,10 @@ function XbNovaShadowRoot({
|
|
|
958
1002
|
const hostRef = useRef6(null);
|
|
959
1003
|
const [mountNode, setMountNode] = useState2(null);
|
|
960
1004
|
const [portalTarget, setPortalTarget] = useState2(null);
|
|
961
|
-
|
|
1005
|
+
useEffect7(() => {
|
|
962
1006
|
setPortalTarget(document.body);
|
|
963
1007
|
}, []);
|
|
964
|
-
|
|
1008
|
+
useEffect7(() => {
|
|
965
1009
|
const host = hostRef.current;
|
|
966
1010
|
if (!host || host.shadowRoot) return;
|
|
967
1011
|
const shadow = host.attachShadow({ mode: "open" });
|
|
@@ -976,7 +1020,7 @@ function XbNovaShadowRoot({
|
|
|
976
1020
|
shadow.appendChild(mount);
|
|
977
1021
|
setMountNode(mount);
|
|
978
1022
|
}, [portalTarget]);
|
|
979
|
-
|
|
1023
|
+
useEffect7(() => {
|
|
980
1024
|
if (!mountNode) return;
|
|
981
1025
|
mountNode.dataset.xbnovaScheme = scheme;
|
|
982
1026
|
mountNode.style.colorScheme = scheme;
|
|
@@ -989,15 +1033,44 @@ function XbNovaShadowRoot({
|
|
|
989
1033
|
}
|
|
990
1034
|
|
|
991
1035
|
// src/use-chat.ts
|
|
992
|
-
import { useEffect as
|
|
1036
|
+
import { useEffect as useEffect8, useRef as useRef7, useState as useState3 } from "react";
|
|
993
1037
|
|
|
994
1038
|
// src/stream.ts
|
|
1039
|
+
function fromStoredMessages(stored) {
|
|
1040
|
+
return stored.map((m) => ({
|
|
1041
|
+
id: m.role === "assistant" ? m.id : void 0,
|
|
1042
|
+
role: m.role === "assistant" ? "assistant" : "user",
|
|
1043
|
+
parts: m.parts.flatMap((p, i) => {
|
|
1044
|
+
const type = String(p.type ?? "");
|
|
1045
|
+
if (type === "text") {
|
|
1046
|
+
const text = typeof p.text === "string" ? p.text : "";
|
|
1047
|
+
return text ? [{ kind: "text", id: `${m.id}:${i}`, text }] : [];
|
|
1048
|
+
}
|
|
1049
|
+
if (type.startsWith("tool-")) {
|
|
1050
|
+
const meta = p.providerMetadata;
|
|
1051
|
+
return [
|
|
1052
|
+
{
|
|
1053
|
+
kind: "tool",
|
|
1054
|
+
toolCallId: String(p.toolCallId ?? `${m.id}:${i}`),
|
|
1055
|
+
toolName: type.slice("tool-".length),
|
|
1056
|
+
state: p.state === "output-error" ? "error" : "output",
|
|
1057
|
+
input: p.input,
|
|
1058
|
+
output: p.output,
|
|
1059
|
+
view: meta?.["kr.dsauto.chat"]?.view ?? null
|
|
1060
|
+
}
|
|
1061
|
+
];
|
|
1062
|
+
}
|
|
1063
|
+
return [];
|
|
1064
|
+
})
|
|
1065
|
+
}));
|
|
1066
|
+
}
|
|
995
1067
|
async function consumeSse(response, handlers) {
|
|
996
1068
|
if (!response.ok || !response.body) {
|
|
997
1069
|
handlers.onError?.(`\uC694\uCCAD \uC2E4\uD328 (${response.status})`);
|
|
998
1070
|
return null;
|
|
999
1071
|
}
|
|
1000
1072
|
const conversationId = response.headers.get("x-conversation-id");
|
|
1073
|
+
if (conversationId) handlers.onConversationId?.(conversationId);
|
|
1001
1074
|
const reader = response.body.getReader();
|
|
1002
1075
|
const decoder = new TextDecoder();
|
|
1003
1076
|
let buffer = "";
|
|
@@ -1102,12 +1175,19 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
|
|
|
1102
1175
|
// src/use-chat.ts
|
|
1103
1176
|
var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
|
|
1104
1177
|
function useXbNovaChat(options) {
|
|
1105
|
-
const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId } = useXbNovaInternal();
|
|
1178
|
+
const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId, sessionId, sendEvent } = useXbNovaInternal();
|
|
1106
1179
|
const botId = options?.botId ?? auth.botId;
|
|
1107
1180
|
const nudgeNodeId = options?.nudgeNodeId;
|
|
1108
|
-
const { apiKey, apiBaseUrl } = auth;
|
|
1181
|
+
const { apiKey, apiBaseUrl, threadKey } = auth;
|
|
1182
|
+
const userKey = visitorId;
|
|
1109
1183
|
const [onboarding, setOnboarding] = useState3(null);
|
|
1110
1184
|
const [messages, setMessages] = useState3([]);
|
|
1185
|
+
const [resumed, setResumed] = useState3(false);
|
|
1186
|
+
const [sentOpening, setSentOpening] = useState3(null);
|
|
1187
|
+
const conversationIdRef = useRef7(null);
|
|
1188
|
+
const pendingOpeningRef = useRef7(null);
|
|
1189
|
+
const messagesRef = useRef7(messages);
|
|
1190
|
+
messagesRef.current = messages;
|
|
1111
1191
|
const [busy, setBusy] = useState3(false);
|
|
1112
1192
|
const [error, setError] = useState3(null);
|
|
1113
1193
|
const [clientResults, setClientResults] = useState3({});
|
|
@@ -1117,19 +1197,60 @@ function useXbNovaChat(options) {
|
|
|
1117
1197
|
const dispatchedClientActionsRef = useRef7(/* @__PURE__ */ new Set());
|
|
1118
1198
|
const nudgeNodeIdRef = useRef7(nudgeNodeId);
|
|
1119
1199
|
nudgeNodeIdRef.current = nudgeNodeId;
|
|
1120
|
-
|
|
1200
|
+
useEffect8(() => {
|
|
1201
|
+
let cancelled = false;
|
|
1121
1202
|
setOnboarding(null);
|
|
1122
1203
|
setMessages([]);
|
|
1204
|
+
setResumed(false);
|
|
1205
|
+
setSentOpening(null);
|
|
1123
1206
|
setError(null);
|
|
1124
1207
|
setClientResults({});
|
|
1125
1208
|
setFeedbackState({});
|
|
1126
1209
|
dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
|
|
1210
|
+
conversationIdRef.current = null;
|
|
1211
|
+
pendingOpeningRef.current = null;
|
|
1127
1212
|
fetch(`${apiBaseUrl}/v1/conversations`, {
|
|
1128
1213
|
method: "POST",
|
|
1129
1214
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
1130
|
-
body: JSON.stringify({
|
|
1131
|
-
|
|
1132
|
-
|
|
1215
|
+
body: JSON.stringify({
|
|
1216
|
+
bot_id: botId,
|
|
1217
|
+
nudge_node_id: nudgeNodeIdRef.current ?? null,
|
|
1218
|
+
visitor_id: userKey || null,
|
|
1219
|
+
thread_key: threadKey ?? null,
|
|
1220
|
+
session_id: sessionId,
|
|
1221
|
+
defer: true
|
|
1222
|
+
})
|
|
1223
|
+
}).then((r) => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))).then((data) => {
|
|
1224
|
+
if (cancelled) return;
|
|
1225
|
+
if (conversationIdRef.current || messagesRef.current.length > 0) {
|
|
1226
|
+
setOnboarding(data);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
conversationIdRef.current = data.conversation_id;
|
|
1230
|
+
const restored = fromStoredMessages(data.messages ?? []);
|
|
1231
|
+
for (const m of restored) {
|
|
1232
|
+
for (const p of m.parts) if (p.kind === "tool") dispatchedClientActionsRef.current.add(p.toolCallId);
|
|
1233
|
+
}
|
|
1234
|
+
setMessages(restored);
|
|
1235
|
+
setResumed(restored.length > 0);
|
|
1236
|
+
setOnboarding(data);
|
|
1237
|
+
}).catch((e) => {
|
|
1238
|
+
if (!cancelled) setError(`\uBD07\uC744 \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`);
|
|
1239
|
+
});
|
|
1240
|
+
return () => {
|
|
1241
|
+
cancelled = true;
|
|
1242
|
+
};
|
|
1243
|
+
}, [apiBaseUrl, apiKey, botId, userKey, threadKey]);
|
|
1244
|
+
const openingMessage = options?.openingMessage;
|
|
1245
|
+
useEffect8(() => {
|
|
1246
|
+
if (!onboarding || !nudgeNodeId || !openingMessage) return;
|
|
1247
|
+
if (messagesRef.current.length === 0) return;
|
|
1248
|
+
setMessages((prev) => [
|
|
1249
|
+
...prev,
|
|
1250
|
+
{ role: "assistant", parts: [{ kind: "text", id: `nudge:${nudgeNodeId}:${prev.length}`, text: openingMessage }] }
|
|
1251
|
+
]);
|
|
1252
|
+
pendingOpeningRef.current = openingMessage;
|
|
1253
|
+
}, [onboarding, nudgeNodeId, openingMessage]);
|
|
1133
1254
|
const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
|
|
1134
1255
|
const next = [...prev];
|
|
1135
1256
|
next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
|
|
@@ -1142,7 +1263,7 @@ function useXbNovaChat(options) {
|
|
|
1142
1263
|
return next;
|
|
1143
1264
|
});
|
|
1144
1265
|
async function setFeedback(messageId, rating, reason) {
|
|
1145
|
-
const conversationId =
|
|
1266
|
+
const conversationId = conversationIdRef.current;
|
|
1146
1267
|
if (!conversationId || !messageId) return;
|
|
1147
1268
|
const prevRating = feedbackRef.current[messageId] ?? null;
|
|
1148
1269
|
const apply = (r) => setFeedbackState((prev) => {
|
|
@@ -1162,12 +1283,21 @@ function useXbNovaChat(options) {
|
|
|
1162
1283
|
}
|
|
1163
1284
|
);
|
|
1164
1285
|
if (!res.ok) throw new Error(`${res.status}`);
|
|
1286
|
+
sendEvent("feedback_submit", { conversation_id: conversationId, message_id: messageId, rating, reason });
|
|
1165
1287
|
options?.onFeedback?.({ conversationId, messageId, rating, reason });
|
|
1166
1288
|
} catch (e) {
|
|
1167
1289
|
console.warn("[xbnova] \uD53C\uB4DC\uBC31 \uC800\uC7A5 \uC2E4\uD328", e);
|
|
1168
1290
|
apply(prevRating);
|
|
1169
1291
|
}
|
|
1170
1292
|
}
|
|
1293
|
+
function postClientActionEvent(kind, action, label) {
|
|
1294
|
+
const conversationId = conversationIdRef.current;
|
|
1295
|
+
if (!conversationId) return;
|
|
1296
|
+
sendEvent(kind, { conversation_id: conversationId, action, label });
|
|
1297
|
+
}
|
|
1298
|
+
function trackClientActionClick(action, label) {
|
|
1299
|
+
postClientActionEvent("cta_click", action, label);
|
|
1300
|
+
}
|
|
1171
1301
|
async function runClientAction(spec, part, existingParts) {
|
|
1172
1302
|
if (spec.dismissSheet) options?.onDismissSheet?.();
|
|
1173
1303
|
let result;
|
|
@@ -1191,7 +1321,7 @@ function useXbNovaChat(options) {
|
|
|
1191
1321
|
result = e instanceof Error ? `\uC624\uB958: ${e.message}` : "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
|
|
1192
1322
|
}
|
|
1193
1323
|
if (!spec.awaitResult) return;
|
|
1194
|
-
const conversationId =
|
|
1324
|
+
const conversationId = conversationIdRef.current;
|
|
1195
1325
|
if (!conversationId) return;
|
|
1196
1326
|
const structured = result && typeof result === "object" ? result : null;
|
|
1197
1327
|
if (structured?.view) {
|
|
@@ -1225,7 +1355,7 @@ function useXbNovaChat(options) {
|
|
|
1225
1355
|
setBusy(false);
|
|
1226
1356
|
}
|
|
1227
1357
|
}
|
|
1228
|
-
|
|
1358
|
+
useEffect8(() => {
|
|
1229
1359
|
if (clientActionHandlers.size === 0) return;
|
|
1230
1360
|
const last = messages[messages.length - 1];
|
|
1231
1361
|
if (!last || last.role !== "assistant") return;
|
|
@@ -1235,12 +1365,18 @@ function useXbNovaChat(options) {
|
|
|
1235
1365
|
if (!spec) continue;
|
|
1236
1366
|
if (dispatchedClientActionsRef.current.has(part.toolCallId)) continue;
|
|
1237
1367
|
dispatchedClientActionsRef.current.add(part.toolCallId);
|
|
1368
|
+
if (spec.render) postClientActionEvent("cta_impression", part.toolName, null);
|
|
1238
1369
|
void runClientAction(spec, part, last.parts);
|
|
1239
1370
|
}
|
|
1240
1371
|
}, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
|
|
1241
1372
|
async function send(text) {
|
|
1242
1373
|
const trimmed = text.trim();
|
|
1243
1374
|
if (!trimmed || busy) return;
|
|
1375
|
+
sendEvent("message_send", {
|
|
1376
|
+
conversation_id: conversationIdRef.current,
|
|
1377
|
+
is_first: messages.length === 0,
|
|
1378
|
+
length: trimmed.length
|
|
1379
|
+
});
|
|
1244
1380
|
setError(null);
|
|
1245
1381
|
setBusy(true);
|
|
1246
1382
|
setMessages((prev) => [
|
|
@@ -1257,6 +1393,14 @@ function useXbNovaChat(options) {
|
|
|
1257
1393
|
const notes = collectContextEntries(contextRegistry);
|
|
1258
1394
|
const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
|
|
1259
1395
|
const context = focused || notes.length ? { ...screen, ...notes.length ? { notes } : {} } : void 0;
|
|
1396
|
+
let openingMessage2;
|
|
1397
|
+
if (messages.length === 0) {
|
|
1398
|
+
openingMessage2 = (options?.openingMessage ?? onboarding?.greeting) || null;
|
|
1399
|
+
setSentOpening(openingMessage2);
|
|
1400
|
+
} else {
|
|
1401
|
+
openingMessage2 = pendingOpeningRef.current;
|
|
1402
|
+
}
|
|
1403
|
+
pendingOpeningRef.current = null;
|
|
1260
1404
|
try {
|
|
1261
1405
|
await streamChat(
|
|
1262
1406
|
apiBaseUrl,
|
|
@@ -1264,12 +1408,22 @@ function useXbNovaChat(options) {
|
|
|
1264
1408
|
{
|
|
1265
1409
|
bot_id: botId,
|
|
1266
1410
|
message: trimmed,
|
|
1267
|
-
conversation_id:
|
|
1411
|
+
conversation_id: conversationIdRef.current,
|
|
1268
1412
|
context,
|
|
1269
1413
|
nudge_node_id: nudgeNodeId ?? null,
|
|
1270
|
-
opening_message:
|
|
1414
|
+
opening_message: openingMessage2,
|
|
1415
|
+
visitor_id: userKey || null,
|
|
1416
|
+
thread_key: threadKey ?? null,
|
|
1417
|
+
session_id: sessionId
|
|
1271
1418
|
},
|
|
1272
|
-
{
|
|
1419
|
+
{
|
|
1420
|
+
onPart: replaceLast,
|
|
1421
|
+
onMessageId: setLastAssistantId,
|
|
1422
|
+
onConversationId: (id) => {
|
|
1423
|
+
conversationIdRef.current = id;
|
|
1424
|
+
},
|
|
1425
|
+
onError: setError
|
|
1426
|
+
}
|
|
1273
1427
|
);
|
|
1274
1428
|
} catch (e) {
|
|
1275
1429
|
setError(e instanceof Error ? e.message : String(e));
|
|
@@ -1277,7 +1431,19 @@ function useXbNovaChat(options) {
|
|
|
1277
1431
|
setBusy(false);
|
|
1278
1432
|
}
|
|
1279
1433
|
}
|
|
1280
|
-
|
|
1434
|
+
const opening = resumed ? null : sentOpening ?? options?.openingMessage ?? onboarding?.greeting ?? null;
|
|
1435
|
+
return {
|
|
1436
|
+
onboarding,
|
|
1437
|
+
opening,
|
|
1438
|
+
messages,
|
|
1439
|
+
busy,
|
|
1440
|
+
error,
|
|
1441
|
+
send,
|
|
1442
|
+
clientResults,
|
|
1443
|
+
feedback,
|
|
1444
|
+
setFeedback,
|
|
1445
|
+
trackClientActionClick
|
|
1446
|
+
};
|
|
1281
1447
|
}
|
|
1282
1448
|
|
|
1283
1449
|
// src/views.tsx
|
|
@@ -1410,6 +1576,13 @@ function hasVisiblePart(parts, clientActionHandlers, clientResults) {
|
|
|
1410
1576
|
}
|
|
1411
1577
|
var DISCLOSURE_SEEN_KEY = "xbnova_disclosure_seen";
|
|
1412
1578
|
var SUMMARY_FACT_KEY = "xbnova_summary";
|
|
1579
|
+
function clickedControl(e) {
|
|
1580
|
+
const target = e.target instanceof Element ? e.target : null;
|
|
1581
|
+
const control = target?.closest("button, a, [role='button']");
|
|
1582
|
+
if (!control || !e.currentTarget.contains(control)) return null;
|
|
1583
|
+
const text = (control.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
1584
|
+
return { label: text || control.getAttribute("aria-label") || null };
|
|
1585
|
+
}
|
|
1413
1586
|
function readDisclosureSeen() {
|
|
1414
1587
|
if (typeof window === "undefined") return true;
|
|
1415
1588
|
try {
|
|
@@ -1428,7 +1601,7 @@ function markDisclosureSeen() {
|
|
|
1428
1601
|
var DESKTOP_MEDIA_QUERY = "(min-width: 640px)";
|
|
1429
1602
|
function useIsDesktopViewport() {
|
|
1430
1603
|
const [isDesktop, setIsDesktop] = useState4(false);
|
|
1431
|
-
|
|
1604
|
+
useEffect9(() => {
|
|
1432
1605
|
if (typeof window === "undefined") return;
|
|
1433
1606
|
const mql = window.matchMedia(DESKTOP_MEDIA_QUERY);
|
|
1434
1607
|
setIsDesktop(mql.matches);
|
|
@@ -1452,7 +1625,7 @@ function backdropBaseStyle(isDesktop) {
|
|
|
1452
1625
|
}
|
|
1453
1626
|
function useTypewriter(text, key, speedMs = 28) {
|
|
1454
1627
|
const [shown, setShown] = useState4("");
|
|
1455
|
-
|
|
1628
|
+
useEffect9(() => {
|
|
1456
1629
|
setShown("");
|
|
1457
1630
|
if (!text) return;
|
|
1458
1631
|
let i = 0;
|
|
@@ -1539,9 +1712,10 @@ function XbNovaChat({
|
|
|
1539
1712
|
avatarSize = 28,
|
|
1540
1713
|
onVerdictAction,
|
|
1541
1714
|
feedback: feedbackEnabled = true,
|
|
1542
|
-
onFeedback
|
|
1715
|
+
onFeedback,
|
|
1716
|
+
onOpen
|
|
1543
1717
|
}) {
|
|
1544
|
-
const { auth, trackRegistry, factsRegistry,
|
|
1718
|
+
const { auth, trackRegistry, factsRegistry, clientActionHandlers, theme, sendEvent } = useXbNovaInternal();
|
|
1545
1719
|
const themeVars = {
|
|
1546
1720
|
"--xbnova-accent-1": theme.accentFrom,
|
|
1547
1721
|
"--xbnova-accent-2": theme.accentTo
|
|
@@ -1562,7 +1736,7 @@ function XbNovaChat({
|
|
|
1562
1736
|
const [input, setInput] = useState4("");
|
|
1563
1737
|
const bottomRef = useRef8(null);
|
|
1564
1738
|
const typedHintText = useTypewriter(hint?.text ?? "", hint?.triggerNodeId ?? "");
|
|
1565
|
-
const { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback } = useXbNovaChat({
|
|
1739
|
+
const { onboarding, opening, messages, busy, error, send, clientResults, feedback, setFeedback, trackClientActionClick } = useXbNovaChat({
|
|
1566
1740
|
onFeedback,
|
|
1567
1741
|
botId: activeBotId ?? botId,
|
|
1568
1742
|
nudgeNodeId: activeNudgeNodeId,
|
|
@@ -1571,7 +1745,7 @@ function XbNovaChat({
|
|
|
1571
1745
|
});
|
|
1572
1746
|
const effectiveBotId = activeBotId ?? botId;
|
|
1573
1747
|
const [cachedBranding, setCachedBranding] = useState4(null);
|
|
1574
|
-
|
|
1748
|
+
useEffect9(() => {
|
|
1575
1749
|
try {
|
|
1576
1750
|
const raw = window.localStorage.getItem(`xbnova_branding:${effectiveBotId}`);
|
|
1577
1751
|
setCachedBranding(raw ? JSON.parse(raw) : null);
|
|
@@ -1579,22 +1753,29 @@ function XbNovaChat({
|
|
|
1579
1753
|
setCachedBranding(null);
|
|
1580
1754
|
}
|
|
1581
1755
|
}, [effectiveBotId]);
|
|
1582
|
-
|
|
1756
|
+
useEffect9(() => {
|
|
1583
1757
|
if (!onboarding?.branding) return;
|
|
1584
1758
|
try {
|
|
1585
1759
|
window.localStorage.setItem(
|
|
1586
1760
|
`xbnova_branding:${effectiveBotId}`,
|
|
1587
|
-
JSON.stringify({
|
|
1761
|
+
JSON.stringify({
|
|
1762
|
+
logo_url: onboarding.branding.logo_url,
|
|
1763
|
+
avatar_text: onboarding.branding.avatar_text,
|
|
1764
|
+
badge_text: onboarding.branding.badge_text,
|
|
1765
|
+
disclaimer: onboarding.branding.disclaimer
|
|
1766
|
+
})
|
|
1588
1767
|
);
|
|
1589
1768
|
} catch {
|
|
1590
1769
|
}
|
|
1591
1770
|
}, [onboarding?.branding, effectiveBotId]);
|
|
1592
1771
|
const branding = onboarding?.branding ?? cachedBranding ?? void 0;
|
|
1772
|
+
const badgeText = branding?.badge_text?.trim() ?? "";
|
|
1773
|
+
const disclaimer = branding?.disclaimer?.trim() ?? "";
|
|
1593
1774
|
const brandingReady = Boolean(onboarding) || cachedBranding !== null || Boolean(error);
|
|
1594
|
-
|
|
1775
|
+
useEffect9(() => {
|
|
1595
1776
|
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
1596
1777
|
}, [messages]);
|
|
1597
|
-
|
|
1778
|
+
useEffect9(() => {
|
|
1598
1779
|
if (!open) {
|
|
1599
1780
|
setEntered(false);
|
|
1600
1781
|
return;
|
|
@@ -1602,7 +1783,7 @@ function XbNovaChat({
|
|
|
1602
1783
|
const raf = requestAnimationFrame(() => setEntered(true));
|
|
1603
1784
|
return () => cancelAnimationFrame(raf);
|
|
1604
1785
|
}, [open]);
|
|
1605
|
-
|
|
1786
|
+
useEffect9(() => {
|
|
1606
1787
|
if (!hint || hint.autoHideMs <= 0) return;
|
|
1607
1788
|
console.debug(`[XbNova] \uD78C\uD2B8 "${hint.text}" \uD45C\uC2DC \u2014 ${hint.autoHideMs}ms \uD6C4 \uC790\uB3D9\uC73C\uB85C \uC0AC\uB77C\uC9D0`);
|
|
1608
1789
|
const timer = window.setTimeout(() => {
|
|
@@ -1619,18 +1800,7 @@ function XbNovaChat({
|
|
|
1619
1800
|
setOpeningLine(consumed.text);
|
|
1620
1801
|
setActiveNudgeNodeId(consumed.messageNodeId);
|
|
1621
1802
|
setNudgeQuickReplies(consumed.quickReplies);
|
|
1622
|
-
|
|
1623
|
-
method: "POST",
|
|
1624
|
-
headers: { "Content-Type": "application/json" },
|
|
1625
|
-
body: JSON.stringify({
|
|
1626
|
-
app_id: auth.appId,
|
|
1627
|
-
nudge_node_id: consumed.messageNodeId,
|
|
1628
|
-
kind: "nudge_open",
|
|
1629
|
-
bot_id: consumed.botId,
|
|
1630
|
-
visitor_id: visitorId
|
|
1631
|
-
})
|
|
1632
|
-
}).catch(() => {
|
|
1633
|
-
});
|
|
1803
|
+
sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
|
|
1634
1804
|
} else {
|
|
1635
1805
|
setNudgeQuickReplies([]);
|
|
1636
1806
|
setActiveNudgeNodeId(void 0);
|
|
@@ -1644,6 +1814,16 @@ function XbNovaChat({
|
|
|
1644
1814
|
markDisclosureSeen();
|
|
1645
1815
|
}
|
|
1646
1816
|
setOpen(true);
|
|
1817
|
+
sendEvent("chat_open", { from_nudge: Boolean(consumed) });
|
|
1818
|
+
try {
|
|
1819
|
+
onOpen?.({ fromNudge: Boolean(consumed), hintText: consumed?.text });
|
|
1820
|
+
} catch (err) {
|
|
1821
|
+
console.warn("[XbNova] onOpen \uCF5C\uBC31 \uC624\uB958", err);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
function closeSheet() {
|
|
1825
|
+
setOpen(false);
|
|
1826
|
+
sendEvent("chat_close");
|
|
1647
1827
|
}
|
|
1648
1828
|
if (!open) {
|
|
1649
1829
|
if (barHidden) return null;
|
|
@@ -1739,7 +1919,6 @@ function XbNovaChat({
|
|
|
1739
1919
|
const summary = resolveFactValue(factsRegistry.get(SUMMARY_FACT_KEY));
|
|
1740
1920
|
const focusedLabel = getFocusedTrackEntry(trackRegistry)?.label;
|
|
1741
1921
|
const contextLine = summary ?? (focusedLabel ? `${focusedLabel} \uBCF4\uB294 \uC911` : void 0);
|
|
1742
|
-
const firstBubble = openingLine ?? onboarding?.greeting;
|
|
1743
1922
|
return /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
1744
1923
|
!isDesktop ? /* @__PURE__ */ jsx7(
|
|
1745
1924
|
XbNovaShadowRoot,
|
|
@@ -1750,7 +1929,7 @@ function XbNovaChat({
|
|
|
1750
1929
|
transition: "opacity 260ms ease-out",
|
|
1751
1930
|
...backdropStyle
|
|
1752
1931
|
},
|
|
1753
|
-
children: /* @__PURE__ */ jsx7("div", { className: "h-full w-full bg-black/45", onClick:
|
|
1932
|
+
children: /* @__PURE__ */ jsx7("div", { className: "h-full w-full bg-black/45", onClick: closeSheet })
|
|
1754
1933
|
}
|
|
1755
1934
|
) : null,
|
|
1756
1935
|
/* @__PURE__ */ jsx7(
|
|
@@ -1776,14 +1955,17 @@ function XbNovaChat({
|
|
|
1776
1955
|
}
|
|
1777
1956
|
),
|
|
1778
1957
|
/* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
|
|
1779
|
-
/* @__PURE__ */
|
|
1958
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex min-w-0 items-center gap-1.5", children: [
|
|
1959
|
+
/* @__PURE__ */ jsx7("span", { className: "truncate text-sm font-semibold", children: onboarding?.branding?.title ?? "AI \uB3D9\uD589" }),
|
|
1960
|
+
badgeText ? /* @__PURE__ */ jsx7("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
|
|
1961
|
+
] }),
|
|
1780
1962
|
contextLine ? /* @__PURE__ */ jsx7("div", { className: "truncate text-[11px] text-neutral-400", children: contextLine }) : null
|
|
1781
1963
|
] }),
|
|
1782
1964
|
/* @__PURE__ */ jsx7(
|
|
1783
1965
|
"button",
|
|
1784
1966
|
{
|
|
1785
1967
|
type: "button",
|
|
1786
|
-
onClick:
|
|
1968
|
+
onClick: closeSheet,
|
|
1787
1969
|
"aria-label": "\uCC44\uD305 \uB2EB\uAE30",
|
|
1788
1970
|
className: "shrink-0 text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200",
|
|
1789
1971
|
children: "\xD7"
|
|
@@ -1792,7 +1974,7 @@ function XbNovaChat({
|
|
|
1792
1974
|
] }),
|
|
1793
1975
|
/* @__PURE__ */ jsxs3("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [
|
|
1794
1976
|
showDisclosure ? /* @__PURE__ */ jsx7("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,
|
|
1795
|
-
|
|
1977
|
+
opening ? /* @__PURE__ */ jsx7(Bubble, { role: "assistant", children: /* @__PURE__ */ jsx7(AssistantMarkdown, { text: opening }) }) : null,
|
|
1796
1978
|
messages.map((message, i) => /* @__PURE__ */ jsx7("div", { children: message.role === "user" ? /* @__PURE__ */ jsx7(Bubble, { role: "user", children: message.parts.map((p) => p.kind === "text" ? p.text : "").join("") }) : /* @__PURE__ */ jsxs3(
|
|
1797
1979
|
AnswerActionsFrame,
|
|
1798
1980
|
{
|
|
@@ -1820,7 +2002,17 @@ function XbNovaChat({
|
|
|
1820
2002
|
);
|
|
1821
2003
|
}
|
|
1822
2004
|
const spec = clientActionHandlers.get(part.toolName);
|
|
1823
|
-
return spec?.render ? /* @__PURE__ */ jsx7(
|
|
2005
|
+
return spec?.render ? /* @__PURE__ */ jsx7(
|
|
2006
|
+
"div",
|
|
2007
|
+
{
|
|
2008
|
+
onClickCapture: (e) => {
|
|
2009
|
+
const control = clickedControl(e);
|
|
2010
|
+
if (control) trackClientActionClick(part.toolName, control.label);
|
|
2011
|
+
},
|
|
2012
|
+
children: spec.render(part.input)
|
|
2013
|
+
},
|
|
2014
|
+
j
|
|
2015
|
+
) : null;
|
|
1824
2016
|
}
|
|
1825
2017
|
return /* @__PURE__ */ jsx7(ToolView, { part, viewRenderers, onVerdictAction }, j);
|
|
1826
2018
|
}),
|
|
@@ -1891,7 +2083,8 @@ function XbNovaChat({
|
|
|
1891
2083
|
)
|
|
1892
2084
|
]
|
|
1893
2085
|
}
|
|
1894
|
-
)
|
|
2086
|
+
),
|
|
2087
|
+
disclaimer ? /* @__PURE__ */ jsx7("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
|
|
1895
2088
|
] })
|
|
1896
2089
|
}
|
|
1897
2090
|
)
|
|
@@ -1993,8 +2186,8 @@ function AnswerActions({
|
|
|
1993
2186
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
1994
2187
|
timerRef.current = null;
|
|
1995
2188
|
};
|
|
1996
|
-
|
|
1997
|
-
|
|
2189
|
+
useEffect9(() => clearTimer, []);
|
|
2190
|
+
useEffect9(() => {
|
|
1998
2191
|
if (!menuOpen) {
|
|
1999
2192
|
setEntered(false);
|
|
2000
2193
|
return;
|
|
@@ -2141,13 +2334,13 @@ function AnswerActionsFrame({
|
|
|
2141
2334
|
}
|
|
2142
2335
|
|
|
2143
2336
|
// src/facts.ts
|
|
2144
|
-
import { useEffect as
|
|
2337
|
+
import { useEffect as useEffect10, useId, useRef as useRef9 } from "react";
|
|
2145
2338
|
function useXbNovaFact(key, value) {
|
|
2146
2339
|
const { factsRegistry } = useXbNovaInternal();
|
|
2147
|
-
|
|
2340
|
+
useEffect10(() => {
|
|
2148
2341
|
factsRegistry.set(key, value);
|
|
2149
2342
|
});
|
|
2150
|
-
|
|
2343
|
+
useEffect10(() => {
|
|
2151
2344
|
return () => {
|
|
2152
2345
|
factsRegistry.delete(key);
|
|
2153
2346
|
};
|
|
@@ -2156,13 +2349,13 @@ function useXbNovaFact(key, value) {
|
|
|
2156
2349
|
function useXbNovaFacts(values) {
|
|
2157
2350
|
const { factsRegistry } = useXbNovaInternal();
|
|
2158
2351
|
const keysRef = useRef9([]);
|
|
2159
|
-
|
|
2352
|
+
useEffect10(() => {
|
|
2160
2353
|
for (const [key, value] of Object.entries(values)) {
|
|
2161
2354
|
factsRegistry.set(key, value);
|
|
2162
2355
|
}
|
|
2163
2356
|
keysRef.current = Object.keys(values);
|
|
2164
2357
|
});
|
|
2165
|
-
|
|
2358
|
+
useEffect10(() => {
|
|
2166
2359
|
return () => {
|
|
2167
2360
|
for (const key of keysRef.current) {
|
|
2168
2361
|
factsRegistry.delete(key);
|
|
@@ -2173,10 +2366,10 @@ function useXbNovaFacts(values) {
|
|
|
2173
2366
|
function useXbNovaContext(entries) {
|
|
2174
2367
|
const { contextRegistry } = useXbNovaInternal();
|
|
2175
2368
|
const id = useId();
|
|
2176
|
-
|
|
2369
|
+
useEffect10(() => {
|
|
2177
2370
|
contextRegistry.set(id, entries);
|
|
2178
2371
|
});
|
|
2179
|
-
|
|
2372
|
+
useEffect10(() => {
|
|
2180
2373
|
return () => {
|
|
2181
2374
|
contextRegistry.delete(id);
|
|
2182
2375
|
};
|