@xbrain-swe/nova-chat-widget 0.1.0

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 ADDED
@@ -0,0 +1,1857 @@
1
+ "use client";
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.ts
32
+ var index_exports = {};
33
+ __export(index_exports, {
34
+ XbNovaChat: () => XbNovaChat,
35
+ XbNovaFact: () => XbNovaFact,
36
+ XbNovaFacts: () => XbNovaFacts,
37
+ XbNovaProvider: () => XbNovaProvider,
38
+ XbNovaTrack: () => XbNovaTrack,
39
+ trackXbNovaEvent: () => trackXbNovaEvent,
40
+ useXbNovaChat: () => useXbNovaChat,
41
+ useXbNovaFact: () => useXbNovaFact,
42
+ useXbNovaFacts: () => useXbNovaFacts,
43
+ useXbNovaSettings: () => useXbNovaSettings,
44
+ useXbNovaTrack: () => useXbNovaTrack,
45
+ useXbNovaVisitor: () => useXbNovaVisitor
46
+ });
47
+ module.exports = __toCommonJS(index_exports);
48
+
49
+ // src/provider.tsx
50
+ var import_react6 = require("react");
51
+
52
+ // src/client-actions.tsx
53
+ var import_react2 = require("react");
54
+
55
+ // src/internal-context.tsx
56
+ var import_react = require("react");
57
+ var import_jsx_runtime = require("react/jsx-runtime");
58
+ var DEFAULT_THEME = {
59
+ accentFrom: "#4f46e5",
60
+ accentTo: "#7c3aed"
61
+ };
62
+ var InternalContext = (0, import_react.createContext)(null);
63
+ function useXbNovaInternal() {
64
+ const ctx = (0, import_react.useContext)(InternalContext);
65
+ if (!ctx) {
66
+ throw new Error(
67
+ "XbNova \uD6C5/\uCEF4\uD3EC\uB10C\uD2B8\uB294 <XbNovaProvider> \uC548\uC5D0\uC11C\uB9CC \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uC571 \uB8E8\uD2B8\uB97C <XbNovaProvider>\uB85C \uAC10\uC2F8\uC138\uC694."
68
+ );
69
+ }
70
+ return ctx;
71
+ }
72
+ var VISITOR_STORAGE_KEY = "xbnova_visitor_id";
73
+ function resolveVisitorId() {
74
+ if (typeof window === "undefined") return "";
75
+ try {
76
+ const existing = window.localStorage.getItem(VISITOR_STORAGE_KEY);
77
+ if (existing) return existing;
78
+ const id = `v_${crypto.randomUUID()}`;
79
+ window.localStorage.setItem(VISITOR_STORAGE_KEY, id);
80
+ return id;
81
+ } catch {
82
+ return `v_${Math.random().toString(36).slice(2)}`;
83
+ }
84
+ }
85
+ function resolveFactValue(value) {
86
+ return typeof value === "function" ? value() : value;
87
+ }
88
+ function InternalProvider({
89
+ auth,
90
+ clientActionHandlers,
91
+ theme,
92
+ children
93
+ }) {
94
+ const trackRegistry = (0, import_react.useRef)(/* @__PURE__ */ new Map()).current;
95
+ const factsRegistry = (0, import_react.useRef)(/* @__PURE__ */ new Map()).current;
96
+ const emptyHandlers = (0, import_react.useRef)(/* @__PURE__ */ new Map()).current;
97
+ const visitorIdRef = (0, import_react.useRef)(null);
98
+ if (visitorIdRef.current === null) {
99
+ visitorIdRef.current = resolveVisitorId();
100
+ }
101
+ const value = (0, import_react.useMemo)(
102
+ () => ({
103
+ auth,
104
+ trackRegistry,
105
+ factsRegistry,
106
+ clientActionHandlers: clientActionHandlers ?? emptyHandlers,
107
+ visitorId: visitorIdRef.current,
108
+ theme
109
+ }),
110
+ [auth, trackRegistry, factsRegistry, clientActionHandlers, emptyHandlers, theme]
111
+ );
112
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InternalContext.Provider, { value, children });
113
+ }
114
+
115
+ // src/client-actions.tsx
116
+ function toSyncPayload(actions) {
117
+ return {
118
+ actions: Object.entries(actions).map(([name, spec]) => ({
119
+ name,
120
+ description: spec.description,
121
+ input_schema: spec.inputSchema ?? { type: "object", properties: {} },
122
+ result_text: spec.resultText ?? "\uC644\uB8CC\uD588\uC2B5\uB2C8\uB2E4.",
123
+ await_result: spec.awaitResult ?? false
124
+ }))
125
+ };
126
+ }
127
+ function XbNovaClientActionSync({ actions }) {
128
+ const { auth } = useXbNovaInternal();
129
+ const { apiKey, apiBaseUrl, appId } = auth;
130
+ const lastSyncedRef = (0, import_react2.useRef)(null);
131
+ (0, import_react2.useEffect)(() => {
132
+ if (!actions || Object.keys(actions).length === 0) return;
133
+ const serialized = JSON.stringify(toSyncPayload(actions));
134
+ if (serialized === lastSyncedRef.current) return;
135
+ lastSyncedRef.current = serialized;
136
+ fetch(`${apiBaseUrl}/v1/apps/${appId}/client-actions`, {
137
+ method: "PUT",
138
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
139
+ body: serialized
140
+ }).catch(() => {
141
+ });
142
+ }, [actions, apiKey, apiBaseUrl, appId]);
143
+ return null;
144
+ }
145
+
146
+ // src/built-in-facts.ts
147
+ var sessionStart = null;
148
+ function markSessionStart() {
149
+ if (sessionStart === null) sessionStart = Date.now();
150
+ }
151
+ function getSessionDurationMs() {
152
+ if (sessionStart === null) return 0;
153
+ return Date.now() - sessionStart;
154
+ }
155
+ function getScrollDepth() {
156
+ if (typeof document === "undefined" || typeof window === "undefined") return 0;
157
+ const scrollable = document.documentElement.scrollHeight - window.innerHeight;
158
+ if (scrollable <= 0) return 100;
159
+ return Math.min(100, Math.max(0, Math.round(window.scrollY / scrollable * 100)));
160
+ }
161
+ function visitCountKey(screen) {
162
+ return `xbnova_visit_count:${screen}`;
163
+ }
164
+ function bumpVisitCount(screen) {
165
+ if (typeof window === "undefined") return 1;
166
+ try {
167
+ const key = visitCountKey(screen);
168
+ const next = (Number(window.localStorage.getItem(key)) || 0) + 1;
169
+ window.localStorage.setItem(key, String(next));
170
+ return next;
171
+ } catch {
172
+ return 1;
173
+ }
174
+ }
175
+ function peekVisitCount(screen) {
176
+ if (typeof window === "undefined") return 0;
177
+ try {
178
+ return Number(window.localStorage.getItem(visitCountKey(screen))) || 0;
179
+ } catch {
180
+ return 0;
181
+ }
182
+ }
183
+
184
+ // src/nudge-context.tsx
185
+ var import_react3 = require("react");
186
+ var import_jsx_runtime2 = require("react/jsx-runtime");
187
+ var NudgeContext = (0, import_react3.createContext)(null);
188
+ var HINTS_DISABLED_KEY = "xbnova_hints_disabled";
189
+ var BAR_HIDDEN_KEY = "xbnova_bar_hidden";
190
+ var DISMISS_STREAK_KEY = "xbnova_hint_dismiss_streak";
191
+ var DEFAULT_DISMISS_STREAK_LIMIT = 3;
192
+ function readFlag(key) {
193
+ if (typeof window === "undefined") return false;
194
+ try {
195
+ return window.localStorage.getItem(key) === "1";
196
+ } catch {
197
+ return false;
198
+ }
199
+ }
200
+ function writeFlag(key, value) {
201
+ if (typeof window === "undefined") return;
202
+ try {
203
+ if (value) window.localStorage.setItem(key, "1");
204
+ else window.localStorage.removeItem(key);
205
+ } catch {
206
+ }
207
+ }
208
+ function readStreak() {
209
+ if (typeof window === "undefined") return 0;
210
+ try {
211
+ return Number(window.localStorage.getItem(DISMISS_STREAK_KEY)) || 0;
212
+ } catch {
213
+ return 0;
214
+ }
215
+ }
216
+ function writeStreak(value) {
217
+ if (typeof window === "undefined") return;
218
+ try {
219
+ window.localStorage.setItem(DISMISS_STREAK_KEY, String(value));
220
+ } catch {
221
+ }
222
+ }
223
+ function NudgeStateProvider({ children }) {
224
+ const [hint, setHint] = (0, import_react3.useState)(null);
225
+ const [lastHint, setLastHint] = (0, import_react3.useState)(null);
226
+ const [hintsEnabled, setHintsEnabled] = (0, import_react3.useState)(() => !readFlag(HINTS_DISABLED_KEY));
227
+ const [barHidden, setBarHidden] = (0, import_react3.useState)(() => readFlag(BAR_HIDDEN_KEY));
228
+ const streakRef = (0, import_react3.useRef)(readStreak());
229
+ const disableHints = (0, import_react3.useCallback)(() => {
230
+ setHintsEnabled(false);
231
+ writeFlag(HINTS_DISABLED_KEY, true);
232
+ }, []);
233
+ const enableHints = (0, import_react3.useCallback)(() => {
234
+ setHintsEnabled(true);
235
+ writeFlag(HINTS_DISABLED_KEY, false);
236
+ streakRef.current = 0;
237
+ writeStreak(0);
238
+ }, []);
239
+ const hideBar = (0, import_react3.useCallback)(() => {
240
+ setBarHidden(true);
241
+ writeFlag(BAR_HIDDEN_KEY, true);
242
+ }, []);
243
+ const showBar = (0, import_react3.useCallback)(() => {
244
+ setBarHidden(false);
245
+ writeFlag(BAR_HIDDEN_KEY, false);
246
+ }, []);
247
+ const offerHint = (0, import_react3.useCallback)((next) => {
248
+ setHint((current) => current ? current : next);
249
+ }, []);
250
+ (0, import_react3.useEffect)(() => {
251
+ if (hint) setLastHint(hint);
252
+ }, [hint]);
253
+ const dismissHint = (0, import_react3.useCallback)(() => {
254
+ const limit = hint?.autoDisableAfterDismissals ?? DEFAULT_DISMISS_STREAK_LIMIT;
255
+ setHint(null);
256
+ streakRef.current += 1;
257
+ writeStreak(streakRef.current);
258
+ if (streakRef.current >= limit) disableHints();
259
+ }, [hint, disableHints]);
260
+ const openHint = (0, import_react3.useCallback)(() => {
261
+ setHint(null);
262
+ streakRef.current = 0;
263
+ writeStreak(0);
264
+ }, []);
265
+ const autoHideHint = (0, import_react3.useCallback)(() => {
266
+ setHint(null);
267
+ }, []);
268
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
269
+ NudgeContext.Provider,
270
+ {
271
+ value: {
272
+ hint,
273
+ lastHint,
274
+ offerHint,
275
+ dismissHint,
276
+ openHint,
277
+ autoHideHint,
278
+ hintsEnabled,
279
+ disableHints,
280
+ enableHints,
281
+ barHidden,
282
+ hideBar,
283
+ showBar
284
+ },
285
+ children
286
+ }
287
+ );
288
+ }
289
+ function useNudgeState() {
290
+ const ctx = (0, import_react3.useContext)(NudgeContext);
291
+ if (!ctx) {
292
+ throw new Error("useNudgeState\uB294 <XbNovaProvider> \uC548\uC5D0\uC11C\uB9CC \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
293
+ }
294
+ return ctx;
295
+ }
296
+ function useXbNovaSettings() {
297
+ const { hintsEnabled, disableHints, enableHints, barHidden, hideBar, showBar } = useNudgeState();
298
+ return { hintsEnabled, disableHints, enableHints, barHidden, hideBar, showBar };
299
+ }
300
+
301
+ // src/nudge-loop.tsx
302
+ var import_react5 = require("react");
303
+
304
+ // src/track.tsx
305
+ var import_react4 = require("react");
306
+ var import_jsx_runtime3 = (
307
+ // `data-xbnova-region`은 호스트 앱이 React context 없이도 이 요소를 찾을 수 있게
308
+ // 한다 — client action 핸들러(`<XbNovaProvider clientActions={{...}}>`)는
309
+ // `<XbNovaProvider>`에 넘기는 prop이라 그 안의 훅(`useXbNovaInternal`)을 못 쓴다.
310
+ // `document.querySelector('[data-xbnova-region="..."]')`로 스크롤·하이라이트하면 된다.
311
+ require("react/jsx-runtime")
312
+ );
313
+ var VISIBILITY_THRESHOLD = 0;
314
+ function finalizeIfVisible(registry, region) {
315
+ const entry = registry.get(region);
316
+ if (entry && entry.enteredAt !== null) {
317
+ entry.totalMs += Date.now() - entry.enteredAt;
318
+ entry.enteredAt = null;
319
+ }
320
+ }
321
+ function useXbNovaTrack(region, data, label) {
322
+ const { trackRegistry } = useXbNovaInternal();
323
+ const observerRef = (0, import_react4.useRef)(null);
324
+ const dataRef = (0, import_react4.useRef)(data);
325
+ dataRef.current = data;
326
+ const labelRef = (0, import_react4.useRef)(label);
327
+ labelRef.current = label;
328
+ (0, import_react4.useEffect)(() => {
329
+ const entry = trackRegistry.get(region);
330
+ if (entry) {
331
+ entry.data = data;
332
+ entry.label = label;
333
+ } else {
334
+ trackRegistry.set(region, { region, data, label, enteredAt: null, totalMs: 0, node: null });
335
+ }
336
+ }, [region, data, label, trackRegistry]);
337
+ (0, import_react4.useEffect)(() => {
338
+ return () => {
339
+ observerRef.current?.disconnect();
340
+ finalizeIfVisible(trackRegistry, region);
341
+ const entry = trackRegistry.get(region);
342
+ if (entry) entry.node = null;
343
+ };
344
+ }, [region, trackRegistry]);
345
+ return (0, import_react4.useCallback)(
346
+ (node) => {
347
+ observerRef.current?.disconnect();
348
+ observerRef.current = null;
349
+ finalizeIfVisible(trackRegistry, region);
350
+ let entry = trackRegistry.get(region);
351
+ if (!entry) {
352
+ entry = { region, data: dataRef.current, label: labelRef.current, enteredAt: null, totalMs: 0, node: null };
353
+ trackRegistry.set(region, entry);
354
+ }
355
+ entry.node = node;
356
+ if (!node || typeof IntersectionObserver === "undefined") return;
357
+ const observer = new IntersectionObserver(
358
+ ([intersection]) => {
359
+ const entry2 = trackRegistry.get(region);
360
+ if (!entry2) return;
361
+ const now = Date.now();
362
+ if (intersection.isIntersecting) {
363
+ if (entry2.enteredAt === null) entry2.enteredAt = now;
364
+ } else if (entry2.enteredAt !== null) {
365
+ entry2.totalMs += now - entry2.enteredAt;
366
+ entry2.enteredAt = null;
367
+ }
368
+ console.debug(
369
+ `[XbNova] track "${region}" isIntersecting=${intersection.isIntersecting} ratio=${intersection.intersectionRatio.toFixed(2)} totalMs=${entry2.totalMs}${entry2.enteredAt !== null ? " (\uC9C4\uC785 \uC911)" : ""}`
370
+ );
371
+ },
372
+ { threshold: VISIBILITY_THRESHOLD }
373
+ );
374
+ observer.observe(node);
375
+ observerRef.current = observer;
376
+ },
377
+ [region, trackRegistry]
378
+ );
379
+ }
380
+ function XbNovaTrack({
381
+ region,
382
+ data,
383
+ label,
384
+ className,
385
+ children
386
+ }) {
387
+ const ref = useXbNovaTrack(region, data, label);
388
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref, className, "data-xbnova-region": region, children });
389
+ }
390
+ function dwellMsOf(entry) {
391
+ const live = entry.enteredAt !== null ? Date.now() - entry.enteredAt : 0;
392
+ return entry.totalMs + live;
393
+ }
394
+ function getRegionDwellMs(trackRegistry, region) {
395
+ const entry = trackRegistry.get(region);
396
+ return entry ? dwellMsOf(entry) : 0;
397
+ }
398
+ function getFocusedTrackEntry(trackRegistry) {
399
+ let visible = null;
400
+ let longest = null;
401
+ for (const entry of trackRegistry.values()) {
402
+ if (entry.enteredAt !== null && (visible === null || entry.enteredAt < (visible.enteredAt ?? Infinity))) {
403
+ visible = entry;
404
+ }
405
+ if (longest === null || dwellMsOf(entry) > dwellMsOf(longest)) {
406
+ longest = entry;
407
+ }
408
+ }
409
+ return visible ?? longest;
410
+ }
411
+
412
+ // src/nudge-loop.tsx
413
+ var EVAL_INTERVAL_MS = 1e3;
414
+ var XBNOVA_EVENT_NAME = "xbnova:event";
415
+ var SESSION_COUNT_KEY_PREFIX = "xbnova_hint_session_count:";
416
+ var SCROLL_SETTLE_MS = 500;
417
+ function trackXbNovaEvent(eventName) {
418
+ if (typeof window === "undefined") return;
419
+ window.dispatchEvent(new CustomEvent(XBNOVA_EVENT_NAME, { detail: { eventName } }));
420
+ }
421
+ function peekSessionCount(triggerId) {
422
+ if (typeof window === "undefined") return 0;
423
+ try {
424
+ return Number(window.sessionStorage.getItem(SESSION_COUNT_KEY_PREFIX + triggerId)) || 0;
425
+ } catch {
426
+ return 0;
427
+ }
428
+ }
429
+ function bumpSessionCount(triggerId) {
430
+ if (typeof window === "undefined") return;
431
+ try {
432
+ window.sessionStorage.setItem(SESSION_COUNT_KEY_PREFIX + triggerId, String(peekSessionCount(triggerId) + 1));
433
+ } catch {
434
+ }
435
+ }
436
+ function resolveConditionValue(field, ctx) {
437
+ if (field === "scroll_depth") return getScrollDepth();
438
+ if (field === "session_duration_ms") return getSessionDurationMs();
439
+ if (field === "visit_count") return ctx.screen ? peekVisitCount(ctx.screen) : 0;
440
+ if (field.startsWith("region_dwell_ms.")) {
441
+ return getRegionDwellMs(ctx.trackRegistry, field.slice("region_dwell_ms.".length));
442
+ }
443
+ return resolveFactValue(ctx.factsRegistry.get(field));
444
+ }
445
+ function compare(actual, operator, expected) {
446
+ switch (operator) {
447
+ case "eq":
448
+ return String(actual) === String(expected);
449
+ case "ne":
450
+ return String(actual) !== String(expected);
451
+ case "gt":
452
+ return Number(actual) > Number(expected);
453
+ case "gte":
454
+ return Number(actual) >= Number(expected);
455
+ case "lt":
456
+ return Number(actual) < Number(expected);
457
+ case "lte":
458
+ return Number(actual) <= Number(expected);
459
+ default:
460
+ return false;
461
+ }
462
+ }
463
+ function triggerFires(node, ctx) {
464
+ const params = node.trigger_params ?? {};
465
+ switch (node.trigger_type) {
466
+ case "visit_count": {
467
+ const threshold = Number(params.threshold ?? 0);
468
+ return Boolean(ctx.screen) && peekVisitCount(ctx.screen) >= threshold;
469
+ }
470
+ case "event":
471
+ return ctx.firedEventName !== null && ctx.firedEventName === params.event_name;
472
+ default:
473
+ return false;
474
+ }
475
+ }
476
+ function computeBandStatus(el, fromPercent, toPercent) {
477
+ const rect = el.getBoundingClientRect();
478
+ const vh = window.innerHeight;
479
+ const bandTop = vh * fromPercent / 100;
480
+ const bandBottom = vh * toPercent / 100;
481
+ const overlapPx = Math.max(0, Math.min(rect.bottom, bandBottom) - Math.max(rect.top, bandTop));
482
+ const inBand = overlapPx > 0;
483
+ return { inBand, overlapPx, rectTop: rect.top, rectBottom: rect.bottom, bandTop, bandBottom, vh };
484
+ }
485
+ function isDebugEnabled() {
486
+ if (typeof window === "undefined") return false;
487
+ try {
488
+ if (new URLSearchParams(window.location.search).get("xbnova_debug") === "1") return true;
489
+ return window.localStorage.getItem("xbnova_debug") === "1";
490
+ } catch {
491
+ return false;
492
+ }
493
+ }
494
+ var DEBUG_COLLAPSED_KEY = "xbnova_debug_collapsed";
495
+ function readDebugCollapsed() {
496
+ if (typeof window === "undefined") return true;
497
+ try {
498
+ return window.localStorage.getItem(DEBUG_COLLAPSED_KEY) !== "0";
499
+ } catch {
500
+ return true;
501
+ }
502
+ }
503
+ function ensureDebugPanel(ref, collapsedRef) {
504
+ if (ref.current) {
505
+ return { body: ref.current.querySelector("[data-xbnova-debug-body]") };
506
+ }
507
+ const container = document.createElement("div");
508
+ container.style.position = "fixed";
509
+ container.style.top = "8px";
510
+ container.style.left = "8px";
511
+ container.style.zIndex = "2147483647";
512
+ container.style.maxWidth = "min(420px, 90vw)";
513
+ container.style.borderRadius = "8px";
514
+ container.style.background = "rgba(17, 17, 17, 0.9)";
515
+ container.style.color = "#fff";
516
+ container.style.font = "11px/1.5 ui-monospace, monospace";
517
+ container.style.boxShadow = "0 2px 12px rgba(0,0,0,0.4)";
518
+ const header = document.createElement("div");
519
+ header.style.display = "flex";
520
+ header.style.alignItems = "center";
521
+ header.style.justifyContent = "space-between";
522
+ header.style.gap = "8px";
523
+ header.style.padding = "6px 10px";
524
+ header.style.cursor = "pointer";
525
+ header.style.userSelect = "none";
526
+ header.style.fontWeight = "600";
527
+ const title = document.createElement("span");
528
+ title.textContent = "XbNova dwell debug";
529
+ const toggle = document.createElement("span");
530
+ header.append(title, toggle);
531
+ const body = document.createElement("div");
532
+ body.setAttribute("data-xbnova-debug-body", "1");
533
+ body.style.padding = "0 10px 8px";
534
+ body.style.maxHeight = "70vh";
535
+ body.style.overflowY = "auto";
536
+ body.style.whiteSpace = "pre-wrap";
537
+ body.style.wordBreak = "break-all";
538
+ const applyCollapsed = () => {
539
+ body.style.display = collapsedRef.current ? "none" : "block";
540
+ toggle.textContent = collapsedRef.current ? "\u25B8 \uD3BC\uCE58\uAE30" : "\u25BE \uC811\uAE30";
541
+ };
542
+ applyCollapsed();
543
+ header.addEventListener("click", () => {
544
+ collapsedRef.current = !collapsedRef.current;
545
+ applyCollapsed();
546
+ try {
547
+ window.localStorage.setItem(DEBUG_COLLAPSED_KEY, collapsedRef.current ? "1" : "0");
548
+ } catch {
549
+ }
550
+ });
551
+ container.append(header, body);
552
+ document.body.appendChild(container);
553
+ ref.current = container;
554
+ return { body };
555
+ }
556
+ function ensureDebugBox(map, key) {
557
+ let box = map.get(key);
558
+ if (!box) {
559
+ box = document.createElement("div");
560
+ box.style.position = "fixed";
561
+ box.style.zIndex = "2147483646";
562
+ box.style.boxSizing = "border-box";
563
+ box.style.pointerEvents = "none";
564
+ box.style.display = "flex";
565
+ box.style.alignItems = "flex-start";
566
+ box.style.font = "11px/1.2 ui-monospace, monospace";
567
+ box.style.color = "#fff";
568
+ box.style.padding = "2px 4px";
569
+ document.body.appendChild(box);
570
+ map.set(key, box);
571
+ }
572
+ return box;
573
+ }
574
+ function ensureBandLines(map, key) {
575
+ let pair = map.get(key);
576
+ if (!pair) {
577
+ const makeLine = () => {
578
+ const line = document.createElement("div");
579
+ line.style.position = "fixed";
580
+ line.style.left = "0";
581
+ line.style.right = "0";
582
+ line.style.height = "0";
583
+ line.style.borderTop = "2px dashed #3b82f6";
584
+ line.style.zIndex = "2147483645";
585
+ line.style.pointerEvents = "none";
586
+ document.body.appendChild(line);
587
+ return line;
588
+ };
589
+ pair = { top: makeLine(), bottom: makeLine() };
590
+ map.set(key, pair);
591
+ }
592
+ return pair;
593
+ }
594
+ function clearDebugOverlay(refs) {
595
+ refs.panelRef.current?.remove();
596
+ refs.panelRef.current = null;
597
+ for (const box of refs.boxesRef.current.values()) box.remove();
598
+ refs.boxesRef.current.clear();
599
+ for (const pair of refs.bandLinesRef.current.values()) {
600
+ pair.top.remove();
601
+ pair.bottom.remove();
602
+ }
603
+ refs.bandLinesRef.current.clear();
604
+ }
605
+ function XbNovaNudgeLoop({ appId }) {
606
+ const { auth, trackRegistry, factsRegistry, visitorId } = useXbNovaInternal();
607
+ const { apiKey, apiBaseUrl, screen } = auth;
608
+ const { hint, offerHint, autoHideHint, hintsEnabled } = useNudgeState();
609
+ const graphRef = (0, import_react5.useRef)(null);
610
+ const bandDwellRef = (0, import_react5.useRef)(/* @__PURE__ */ new Map());
611
+ const lastScrollAtRef = (0, import_react5.useRef)(0);
612
+ const lastTickLogRef = (0, import_react5.useRef)("");
613
+ const debugPanelRef = (0, import_react5.useRef)(null);
614
+ const debugCollapsedRef = (0, import_react5.useRef)(readDebugCollapsed());
615
+ const debugBoxesRef = (0, import_react5.useRef)(/* @__PURE__ */ new Map());
616
+ const debugBandLinesRef = (0, import_react5.useRef)(/* @__PURE__ */ new Map());
617
+ const hintRef = (0, import_react5.useRef)(hint);
618
+ hintRef.current = hint;
619
+ const hintsEnabledRef = (0, import_react5.useRef)(hintsEnabled);
620
+ hintsEnabledRef.current = hintsEnabled;
621
+ (0, import_react5.useEffect)(() => {
622
+ let cancelled = false;
623
+ fetch(`${apiBaseUrl}/v1/apps/${appId}/nudge-graph`, {
624
+ headers: { Authorization: `Bearer ${apiKey}` }
625
+ }).then((r) => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))).then((graph) => {
626
+ if (!cancelled) graphRef.current = graph;
627
+ }).catch(() => {
628
+ });
629
+ return () => {
630
+ cancelled = true;
631
+ };
632
+ }, [apiBaseUrl, apiKey, appId]);
633
+ const evaluate = (0, import_react5.useCallback)(
634
+ (firedEventName) => {
635
+ const graph = graphRef.current;
636
+ const debugEnabled = isDebugEnabled();
637
+ const debugRefs = { panelRef: debugPanelRef, boxesRef: debugBoxesRef, bandLinesRef: debugBandLinesRef };
638
+ if (!debugEnabled && debugPanelRef.current) clearDebugOverlay(debugRefs);
639
+ if (!graph) {
640
+ console.debug("[XbNova] nudge \uD3C9\uAC00 \uAC74\uB108\uB700: \uADF8\uB798\uD504\uB97C \uC544\uC9C1 \uBABB \uBD88\uB7EC\uC634");
641
+ return;
642
+ }
643
+ const canOffer = !hintRef.current && hintsEnabledRef.current;
644
+ const dwellTriggerCount = graph.nodes.filter((n) => n.kind === "trigger" && n.trigger_type === "dwell").length;
645
+ const scrollIdleMs = Date.now() - lastScrollAtRef.current;
646
+ const debugLines = debugEnabled ? [
647
+ `[XbNova] dwell \uD2B8\uB9AC\uAC70 ${dwellTriggerCount}\uAC1C (\uD654\uBA74="${screen}")${canOffer ? "" : " \u2014 \uC0C8 \uD78C\uD2B8 \uD45C\uC2DC\uB294 \uC7A0\uAE50 \uB9C9\uD798(\uB204\uC801\uC740 \uACC4\uC18D\uB428)"}`,
648
+ scrollIdleMs < SCROLL_SETTLE_MS ? `\uC2A4\uD06C\uB864 \uC911(\uC720\uD734 ${scrollIdleMs}ms < ${SCROLL_SETTLE_MS}ms) \u2014 \uC0C8 \uD78C\uD2B8 \uBC1C\uB3D9 \uBCF4\uB958` : `\uC2A4\uD06C\uB864 \uC720\uD734 ${scrollIdleMs >= 1e4 ? "10s+" : scrollIdleMs + "ms"}`
649
+ ] : [];
650
+ const activeDebugTriggerIds = /* @__PURE__ */ new Set();
651
+ const nodesById = new Map(graph.nodes.map((n) => [n.id, n]));
652
+ const edgesFrom = /* @__PURE__ */ new Map();
653
+ for (const edge of graph.edges) {
654
+ const list = edgesFrom.get(edge.from_id) ?? [];
655
+ list.push(edge);
656
+ edgesFrom.set(edge.from_id, list);
657
+ }
658
+ const walkToMessage = (nodeId) => {
659
+ const node = nodesById.get(nodeId);
660
+ if (!node) return null;
661
+ if (node.kind === "message") return node;
662
+ if (node.kind === "condition") {
663
+ const matches = compare(
664
+ resolveConditionValue(node.condition_field ?? "", { screen, trackRegistry, factsRegistry }),
665
+ node.operator,
666
+ node.value
667
+ );
668
+ const branch = matches ? "true" : "false";
669
+ const nextEdge = (edgesFrom.get(nodeId) ?? []).find((e) => e.branch_label === branch);
670
+ return nextEdge ? walkToMessage(nextEdge.to_id) : null;
671
+ }
672
+ return null;
673
+ };
674
+ const fire = (node, text, messageNode) => {
675
+ console.debug(`[XbNova] \uD78C\uD2B8 \uBC1C\uB3D9: \uD2B8\uB9AC\uAC70 ${node.id} \u2192 \uBA54\uC2DC\uC9C0 "${text}"`);
676
+ const handoffEdge = (edgesFrom.get(messageNode.id) ?? [])[0];
677
+ const handoffNode = handoffEdge ? nodesById.get(handoffEdge.to_id) : void 0;
678
+ const botId = handoffNode?.kind === "ai_handoff" ? handoffNode.bot_id ?? null : null;
679
+ bumpSessionCount(node.id);
680
+ offerHint({
681
+ messageNodeId: messageNode.id,
682
+ triggerNodeId: node.id,
683
+ text,
684
+ botId,
685
+ autoDisableAfterDismissals: node.auto_disable_after_dismissals ?? 3,
686
+ // 여러 트리거가 같은 message 노드를 공유할 수 있어서(DAG), "몇 ms 후 사라질지"는
687
+ // 트리거(node)가 아니라 메시지(messageNode) 자체의 속성이다.
688
+ autoHideMs: messageNode.auto_hide_ms ?? 2e4,
689
+ quickReplies: messageNode.quick_replies ?? []
690
+ });
691
+ fetch(`${apiBaseUrl}/v1/nudge-events`, {
692
+ method: "POST",
693
+ headers: { "Content-Type": "application/json" },
694
+ body: JSON.stringify({
695
+ app_id: appId,
696
+ nudge_node_id: messageNode.id,
697
+ kind: "nudge_impression",
698
+ bot_id: botId,
699
+ visitor_id: visitorId
700
+ })
701
+ }).catch(() => {
702
+ });
703
+ };
704
+ const tryFireFrom = (node) => {
705
+ const nextEdge = (edgesFrom.get(node.id) ?? [])[0];
706
+ if (!nextEdge) {
707
+ console.debug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id} \uBC1C\uB3D9\uD588\uC9C0\uB9CC \uB098\uAC00\uB294 \uC5E3\uC9C0\uAC00 \uC5C6\uC74C`);
708
+ return false;
709
+ }
710
+ const messageNode = walkToMessage(nextEdge.to_id);
711
+ if (!messageNode) {
712
+ console.debug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id} \uBC1C\uB3D9\uD588\uC9C0\uB9CC message \uB178\uB4DC\uC5D0 \uBABB \uB2FF\uC74C(\uC870\uAC74 \uBD84\uAE30 \uD655\uC778)`);
713
+ return false;
714
+ }
715
+ const text = messageNode.messages?.[0];
716
+ if (!text) {
717
+ console.debug(`[XbNova] message \uB178\uB4DC ${messageNode.id}\uC5D0 \uBB38\uAD6C\uAC00 \uC5C6\uC74C`);
718
+ return false;
719
+ }
720
+ fire(node, text, messageNode);
721
+ return true;
722
+ };
723
+ const dwellRecords = [];
724
+ let firedThisTick = false;
725
+ for (const node of graph.nodes) {
726
+ if (node.kind !== "trigger") continue;
727
+ const maxPerSession = node.max_per_session === 0 ? Infinity : node.max_per_session ?? 3;
728
+ const sessionCount = peekSessionCount(node.id);
729
+ if (sessionCount >= maxPerSession) {
730
+ console.debug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id}(${node.trigger_type}) \uAC74\uB108\uB700: \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC`);
731
+ if (debugEnabled && node.trigger_type === "dwell") {
732
+ debugLines.push(`- [${node.id}] \uC138\uC158\uB2F9 \uC0C1\uD55C(${maxPerSession}) \uB3C4\uB2EC \u2014 \uB354 \uC548 \uC7BC`);
733
+ }
734
+ continue;
735
+ }
736
+ if (node.trigger_type === "dwell") {
737
+ const params = node.trigger_params ?? {};
738
+ const region = String(params.region ?? "");
739
+ const triggerScreen = String(params.screen ?? "");
740
+ const requiredMs = Number(params.duration_ms ?? 0);
741
+ const fromPercent = Number(params.region_visible_from_percent ?? 35);
742
+ const toPercent = Number(params.region_visible_to_percent ?? 65);
743
+ const state = bandDwellRef.current.get(node.id) ?? {
744
+ accumulatedMs: 0,
745
+ lastCheckedAt: null,
746
+ firedAndAwaitingExit: false
747
+ };
748
+ const trackEntry = trackRegistry.get(region);
749
+ const el = trackEntry?.node ?? null;
750
+ const screenMatches = screen === triggerScreen;
751
+ const found = screenMatches && el !== null;
752
+ let inBand = false;
753
+ let overlapPx = 0;
754
+ let statusDetail;
755
+ if (!screenMatches) {
756
+ statusDetail = `screen \uBD88\uC77C\uCE58 (\uD604\uC7AC="${screen}" \uD544\uC694="${triggerScreen}")`;
757
+ } else if (!el) {
758
+ statusDetail = `\uD2B8\uB799 \uC694\uC18C\uB97C \uBABB \uCC3E\uC74C \u2014 region="${region}"\uC73C\uB85C \uB4F1\uB85D\uB41C <XbNovaTrack>\uC774 \uC774 \uD398\uC774\uC9C0\uC5D0 \uC5C6\uAC70\uB098 \uD2B8\uB799 \uD0A4\uAC00 \uB2E4\uB984 (\uB4F1\uB85D\uB41C region \uBAA9\uB85D: ${Array.from(trackRegistry.keys()).join(", ") || "(\uC5C6\uC74C)"})`;
759
+ } else {
760
+ const status = computeBandStatus(el, fromPercent, toPercent);
761
+ inBand = status.inBand;
762
+ overlapPx = status.overlapPx;
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}`;
764
+ }
765
+ console.debug(
766
+ `[XbNova] band dwell \uD2B8\uB9AC\uAC70=${node.id} region="${region}" band%=${fromPercent}~${toPercent} | ${statusDetail}`
767
+ );
768
+ dwellRecords.push({ node, state, requiredMs, region, found, screenMatches, inBand, overlapPx });
769
+ continue;
770
+ }
771
+ const fires = triggerFires(node, { screen, firedEventName });
772
+ if (!fires) continue;
773
+ if (!canOffer) {
774
+ console.debug(`[XbNova] \uD2B8\uB9AC\uAC70 ${node.id} \uC870\uAC74\uC740 \uCDA9\uC871\uD588\uC9C0\uB9CC \uC0C8 \uD78C\uD2B8 \uD45C\uC2DC\uB294 \uC7A0\uAE50 \uB9C9\uD798`);
775
+ continue;
776
+ }
777
+ if (tryFireFrom(node)) {
778
+ firedThisTick = true;
779
+ break;
780
+ }
781
+ }
782
+ let primaryId = null;
783
+ let primaryOverlap = -1;
784
+ for (const r of dwellRecords) {
785
+ if (r.inBand && r.overlapPx > primaryOverlap) {
786
+ primaryId = r.node.id;
787
+ primaryOverlap = r.overlapPx;
788
+ }
789
+ }
790
+ for (const r of dwellRecords) {
791
+ const isPrimary = r.node.id === primaryId;
792
+ const now = Date.now();
793
+ if (isPrimary && r.inBand) {
794
+ if (!r.state.firedAndAwaitingExit && r.state.lastCheckedAt !== null) {
795
+ r.state.accumulatedMs += now - r.state.lastCheckedAt;
796
+ }
797
+ r.state.lastCheckedAt = now;
798
+ } else {
799
+ r.state.accumulatedMs = 0;
800
+ r.state.lastCheckedAt = null;
801
+ r.state.firedAndAwaitingExit = false;
802
+ }
803
+ bandDwellRef.current.set(r.node.id, r.state);
804
+ if (debugEnabled) {
805
+ activeDebugTriggerIds.add(r.node.id);
806
+ const statusLabel = r.found ? r.inBand ? isPrimary ? "IN-BAND(\uB300\uD45C)" : "IN-BAND(\uACB9\uCE68, \uB300\uD45C \uC544\uB2D8)" : "band \uBC16(\uD654\uBA74\uC5D4 \uC788\uC74C)" : r.screenMatches ? "\uC694\uC18C \uBABB \uCC3E\uC74C" : "\uB2E4\uB978 \uD654\uBA74";
807
+ const lockLabel = r.state.firedAndAwaitingExit ? " \u{1F512}\uC7AC\uBC1C\uB3D9\uC7A0\uAE08" : "";
808
+ debugLines.push(`- ${r.region || "(region \uC5C6\uC74C)"} [${r.node.id}] ${statusLabel} ${r.state.accumulatedMs}/${r.requiredMs}ms${lockLabel}`);
809
+ const el = trackRegistry.get(r.region)?.node ?? null;
810
+ if (r.found && el) {
811
+ const rect = el.getBoundingClientRect();
812
+ const box = ensureDebugBox(debugBoxesRef.current, r.node.id);
813
+ box.style.left = `${rect.left}px`;
814
+ box.style.top = `${rect.top}px`;
815
+ box.style.width = `${Math.max(rect.width, 2)}px`;
816
+ box.style.height = `${Math.max(rect.height, 2)}px`;
817
+ if (r.inBand && isPrimary) {
818
+ box.style.border = "3px solid #22c55e";
819
+ box.style.backgroundColor = "rgba(34,197,94,0.28)";
820
+ } else if (r.inBand) {
821
+ box.style.border = "2px solid #86efac";
822
+ box.style.backgroundColor = "rgba(134,239,172,0.12)";
823
+ } else {
824
+ box.style.border = "2px dashed #f97316";
825
+ box.style.backgroundColor = "rgba(249,115,22,0.10)";
826
+ }
827
+ box.textContent = `${r.region}${isPrimary ? " \u2605" : ""} ${r.state.accumulatedMs}/${r.requiredMs}ms${lockLabel}`;
828
+ const params = r.node.trigger_params ?? {};
829
+ const fromPercent = Number(params.region_visible_from_percent ?? 35);
830
+ const toPercent = Number(params.region_visible_to_percent ?? 65);
831
+ const bandKey = `${fromPercent}_${toPercent}`;
832
+ const bandLines = ensureBandLines(debugBandLinesRef.current, bandKey);
833
+ const vh = window.innerHeight;
834
+ bandLines.top.style.top = `${vh * fromPercent / 100}px`;
835
+ bandLines.bottom.style.top = `${vh * toPercent / 100}px`;
836
+ } else {
837
+ debugBoxesRef.current.get(r.node.id)?.remove();
838
+ debugBoxesRef.current.delete(r.node.id);
839
+ }
840
+ }
841
+ if (firedThisTick || !isPrimary || !hintsEnabledRef.current) continue;
842
+ if (r.state.accumulatedMs < r.requiredMs) continue;
843
+ if (Date.now() - lastScrollAtRef.current < SCROLL_SETTLE_MS) continue;
844
+ const shownHint = hintRef.current;
845
+ if (shownHint && shownHint.triggerNodeId === r.node.id) continue;
846
+ if (shownHint) autoHideHint();
847
+ if (tryFireFrom(r.node)) {
848
+ r.state.accumulatedMs = 0;
849
+ r.state.lastCheckedAt = null;
850
+ r.state.firedAndAwaitingExit = true;
851
+ firedThisTick = true;
852
+ }
853
+ }
854
+ if (debugEnabled) {
855
+ const snapshot = dwellRecords.map(
856
+ (r) => `${r.region}:${r.inBand ? "IN" : "OUT"}(${r.overlapPx.toFixed(0)}px) ${r.state.accumulatedMs}/${r.requiredMs}ms${r.state.firedAndAwaitingExit ? "\u{1F512}" : ""}`
857
+ ).join(" | ");
858
+ const tickLog = `primary=${primaryId} :: ${snapshot}`;
859
+ if (tickLog !== lastTickLogRef.current) {
860
+ lastTickLogRef.current = tickLog;
861
+ console.debug(`[XbNova TICK ${(/* @__PURE__ */ new Date()).toISOString().slice(11, 23)}] ${tickLog}`);
862
+ }
863
+ }
864
+ if (debugEnabled) {
865
+ for (const [id, box] of debugBoxesRef.current) {
866
+ if (!activeDebugTriggerIds.has(id)) {
867
+ box.remove();
868
+ debugBoxesRef.current.delete(id);
869
+ }
870
+ }
871
+ ensureDebugPanel(debugPanelRef, debugCollapsedRef).body.textContent = debugLines.join("\n");
872
+ }
873
+ },
874
+ [apiBaseUrl, appId, autoHideHint, factsRegistry, offerHint, screen, trackRegistry, visitorId]
875
+ );
876
+ (0, import_react5.useEffect)(() => {
877
+ const interval = window.setInterval(() => evaluate(null), EVAL_INTERVAL_MS);
878
+ const onEvent = (e) => evaluate(e.detail.eventName);
879
+ window.addEventListener(XBNOVA_EVENT_NAME, onEvent);
880
+ const onScroll = () => {
881
+ lastScrollAtRef.current = Date.now();
882
+ };
883
+ window.addEventListener("scroll", onScroll, { passive: true, capture: true });
884
+ return () => {
885
+ window.clearInterval(interval);
886
+ window.removeEventListener(XBNOVA_EVENT_NAME, onEvent);
887
+ window.removeEventListener("scroll", onScroll, { capture: true });
888
+ clearDebugOverlay({ panelRef: debugPanelRef, boxesRef: debugBoxesRef, bandLinesRef: debugBandLinesRef });
889
+ };
890
+ }, [evaluate]);
891
+ return null;
892
+ }
893
+
894
+ // src/provider.tsx
895
+ var import_jsx_runtime4 = require("react/jsx-runtime");
896
+ var DEFAULT_API_BASE_URL = "http://localhost:10100";
897
+ function XbNovaProvider({
898
+ apiKey,
899
+ botId,
900
+ appId,
901
+ screen,
902
+ apiBaseUrl = DEFAULT_API_BASE_URL,
903
+ clientActions,
904
+ theme,
905
+ children
906
+ }) {
907
+ (0, import_react6.useEffect)(() => {
908
+ if (apiKey.startsWith("sk_")) {
909
+ console.error(
910
+ "[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."
911
+ );
912
+ }
913
+ }, [apiKey]);
914
+ (0, import_react6.useEffect)(() => {
915
+ markSessionStart();
916
+ }, []);
917
+ (0, import_react6.useEffect)(() => {
918
+ if (screen) bumpVisitCount(screen);
919
+ }, [screen]);
920
+ const auth = (0, import_react6.useMemo)(
921
+ () => ({ apiKey, botId, appId, apiBaseUrl, screen }),
922
+ [apiKey, botId, appId, apiBaseUrl, screen]
923
+ );
924
+ const resolvedTheme = (0, import_react6.useMemo)(
925
+ () => ({ ...DEFAULT_THEME, ...theme }),
926
+ [theme]
927
+ );
928
+ const clientActionHandlers = (0, import_react6.useMemo)(() => {
929
+ if (!clientActions) return void 0;
930
+ return new Map(
931
+ Object.entries(clientActions).map(([name, spec]) => [
932
+ name,
933
+ {
934
+ handler: spec.handler,
935
+ awaitResult: spec.awaitResult ?? false,
936
+ dismissSheet: spec.dismissSheet ?? false,
937
+ render: spec.render
938
+ }
939
+ ])
940
+ );
941
+ }, [clientActions]);
942
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(InternalProvider, { auth, clientActionHandlers, theme: resolvedTheme, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(NudgeStateProvider, { children: [
943
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(XbNovaNudgeLoop, { appId }),
944
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(XbNovaClientActionSync, { actions: clientActions }),
945
+ children
946
+ ] }) });
947
+ }
948
+
949
+ // src/chat.tsx
950
+ var import_react9 = require("react");
951
+ var import_react_markdown = __toESM(require("react-markdown"), 1);
952
+ var import_remark_breaks = __toESM(require("remark-breaks"), 1);
953
+ var import_remark_gfm = __toESM(require("remark-gfm"), 1);
954
+
955
+ // src/shadow-root.tsx
956
+ var import_react7 = require("react");
957
+ var import_react_dom = require("react-dom");
958
+ var import_jsx_runtime5 = require("react/jsx-runtime");
959
+ var import_meta = {};
960
+ var cssPromise = null;
961
+ function loadCss() {
962
+ if (!cssPromise) {
963
+ try {
964
+ cssPromise = fetch(new URL("./style.css", import_meta.url)).then((r) => r.ok ? r.text() : "").catch(() => "");
965
+ } catch {
966
+ cssPromise = Promise.resolve("");
967
+ }
968
+ }
969
+ return cssPromise;
970
+ }
971
+ function XbNovaShadowRoot({
972
+ children,
973
+ style
974
+ }) {
975
+ const hostRef = (0, import_react7.useRef)(null);
976
+ const [mountNode, setMountNode] = (0, import_react7.useState)(null);
977
+ const [portalTarget, setPortalTarget] = (0, import_react7.useState)(null);
978
+ (0, import_react7.useEffect)(() => {
979
+ setPortalTarget(document.body);
980
+ }, []);
981
+ (0, import_react7.useEffect)(() => {
982
+ const host = hostRef.current;
983
+ if (!host || host.shadowRoot) return;
984
+ const shadow = host.attachShadow({ mode: "open" });
985
+ const style2 = document.createElement("style");
986
+ shadow.appendChild(style2);
987
+ loadCss().then((css) => {
988
+ style2.textContent = css;
989
+ });
990
+ const mount = document.createElement("div");
991
+ mount.style.height = "100%";
992
+ mount.style.width = "100%";
993
+ shadow.appendChild(mount);
994
+ setMountNode(mount);
995
+ }, [portalTarget]);
996
+ if (!portalTarget) return null;
997
+ return (0, import_react_dom.createPortal)(
998
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { ref: hostRef, style, children: mountNode ? (0, import_react_dom.createPortal)(children, mountNode) : null }),
999
+ portalTarget
1000
+ );
1001
+ }
1002
+
1003
+ // src/use-chat.ts
1004
+ var import_react8 = require("react");
1005
+
1006
+ // src/stream.ts
1007
+ async function consumeSse(response, handlers) {
1008
+ if (!response.ok || !response.body) {
1009
+ handlers.onError?.(`\uC694\uCCAD \uC2E4\uD328 (${response.status})`);
1010
+ return null;
1011
+ }
1012
+ const conversationId = response.headers.get("x-conversation-id");
1013
+ const reader = response.body.getReader();
1014
+ const decoder = new TextDecoder();
1015
+ let buffer = "";
1016
+ const parts = [];
1017
+ const textIndex = /* @__PURE__ */ new Map();
1018
+ const toolIndex = /* @__PURE__ */ new Map();
1019
+ const emit = () => handlers.onPart([...parts]);
1020
+ while (true) {
1021
+ const { done, value } = await reader.read();
1022
+ if (done) break;
1023
+ buffer += decoder.decode(value, { stream: true });
1024
+ const lines = buffer.split("\n\n");
1025
+ buffer = lines.pop() ?? "";
1026
+ for (const chunk of lines) {
1027
+ const line = chunk.trim();
1028
+ if (!line.startsWith("data:")) continue;
1029
+ const payload = line.slice(5).trim();
1030
+ if (payload === "[DONE]") continue;
1031
+ let event;
1032
+ try {
1033
+ event = JSON.parse(payload);
1034
+ } catch {
1035
+ continue;
1036
+ }
1037
+ switch (event.type) {
1038
+ case "text-start": {
1039
+ const id = String(event.id);
1040
+ textIndex.set(id, parts.length);
1041
+ parts.push({ kind: "text", id, text: "" });
1042
+ emit();
1043
+ break;
1044
+ }
1045
+ case "text-delta": {
1046
+ const id = String(event.id);
1047
+ let at = textIndex.get(id);
1048
+ if (at === void 0) {
1049
+ at = parts.length;
1050
+ textIndex.set(id, at);
1051
+ parts.push({ kind: "text", id, text: "" });
1052
+ }
1053
+ parts[at].text += String(event.delta ?? "");
1054
+ emit();
1055
+ break;
1056
+ }
1057
+ case "tool-input-available": {
1058
+ const id = String(event.toolCallId);
1059
+ toolIndex.set(id, parts.length);
1060
+ parts.push({
1061
+ kind: "tool",
1062
+ toolCallId: id,
1063
+ toolName: String(event.toolName),
1064
+ state: "input",
1065
+ input: event.input
1066
+ });
1067
+ emit();
1068
+ break;
1069
+ }
1070
+ case "tool-output-available": {
1071
+ const id = String(event.toolCallId);
1072
+ const at = toolIndex.get(id);
1073
+ if (at !== void 0) {
1074
+ const meta = event.providerMetadata;
1075
+ const part = parts[at];
1076
+ part.state = event.isError ? "error" : "output";
1077
+ part.output = event.output;
1078
+ part.view = meta?.["kr.dsauto.chat"]?.view ?? null;
1079
+ emit();
1080
+ }
1081
+ break;
1082
+ }
1083
+ case "error":
1084
+ handlers.onError?.(String(event.errorText ?? "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958"));
1085
+ break;
1086
+ case "finish":
1087
+ handlers.onFinish?.();
1088
+ break;
1089
+ }
1090
+ }
1091
+ }
1092
+ return conversationId;
1093
+ }
1094
+ async function streamChat(apiBaseUrl, apiKey, body, handlers) {
1095
+ const response = await fetch(`${apiBaseUrl}/v1/chat`, {
1096
+ method: "POST",
1097
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
1098
+ body: JSON.stringify(body)
1099
+ });
1100
+ return consumeSse(response, handlers);
1101
+ }
1102
+ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
1103
+ const response = await fetch(`${apiBaseUrl}/v1/conversations/${body.conversation_id}/tool-results`, {
1104
+ method: "POST",
1105
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
1106
+ body: JSON.stringify(body)
1107
+ });
1108
+ return consumeSse(response, handlers);
1109
+ }
1110
+
1111
+ // src/use-chat.ts
1112
+ function useXbNovaChat(options) {
1113
+ const { auth, trackRegistry, clientActionHandlers } = useXbNovaInternal();
1114
+ const botId = options?.botId ?? auth.botId;
1115
+ const nudgeNodeId = options?.nudgeNodeId;
1116
+ const { apiKey, apiBaseUrl } = auth;
1117
+ const [onboarding, setOnboarding] = (0, import_react8.useState)(null);
1118
+ const [messages, setMessages] = (0, import_react8.useState)([]);
1119
+ const [busy, setBusy] = (0, import_react8.useState)(false);
1120
+ const [error, setError] = (0, import_react8.useState)(null);
1121
+ const dispatchedClientActionsRef = (0, import_react8.useRef)(/* @__PURE__ */ new Set());
1122
+ const nudgeNodeIdRef = (0, import_react8.useRef)(nudgeNodeId);
1123
+ nudgeNodeIdRef.current = nudgeNodeId;
1124
+ (0, import_react8.useEffect)(() => {
1125
+ setOnboarding(null);
1126
+ setMessages([]);
1127
+ setError(null);
1128
+ dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
1129
+ fetch(`${apiBaseUrl}/v1/conversations`, {
1130
+ method: "POST",
1131
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
1132
+ body: JSON.stringify({ bot_id: botId, nudge_node_id: nudgeNodeIdRef.current ?? null })
1133
+ }).then((r) => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))).then(setOnboarding).catch((e) => setError(`\uBD07\uC744 \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`));
1134
+ }, [apiBaseUrl, apiKey, botId]);
1135
+ const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
1136
+ const next = [...prev];
1137
+ next[next.length - 1] = { role: "assistant", parts: [...existingParts, ...newParts] };
1138
+ return next;
1139
+ });
1140
+ async function runClientAction(spec, part, existingParts) {
1141
+ if (spec.dismissSheet) options?.onDismissSheet?.();
1142
+ let result;
1143
+ try {
1144
+ result = await spec.handler(part.input);
1145
+ } catch (e) {
1146
+ if (!spec.awaitResult) return;
1147
+ result = e instanceof Error ? `\uC624\uB958: ${e.message}` : "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
1148
+ }
1149
+ if (!spec.awaitResult) return;
1150
+ const conversationId = onboarding?.conversation_id;
1151
+ if (!conversationId) return;
1152
+ const resultText = typeof result === "string" && result.trim() ? result : "\uD578\uB4E4\uB7EC\uAC00 \uACB0\uACFC\uB97C \uBC18\uD658\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.";
1153
+ setBusy(true);
1154
+ try {
1155
+ await resumeToolResult(
1156
+ apiBaseUrl,
1157
+ apiKey,
1158
+ {
1159
+ bot_id: botId,
1160
+ conversation_id: conversationId,
1161
+ tool_call_id: part.toolCallId,
1162
+ result_text: resultText
1163
+ },
1164
+ { onPart: (newParts) => appendToLastAssistant(existingParts, newParts), onError: setError }
1165
+ );
1166
+ } catch (e) {
1167
+ setError(e instanceof Error ? e.message : String(e));
1168
+ } finally {
1169
+ setBusy(false);
1170
+ }
1171
+ }
1172
+ (0, import_react8.useEffect)(() => {
1173
+ if (clientActionHandlers.size === 0) return;
1174
+ const last = messages[messages.length - 1];
1175
+ if (!last || last.role !== "assistant") return;
1176
+ for (const part of last.parts) {
1177
+ if (part.kind !== "tool" || part.state !== "input") continue;
1178
+ const spec = clientActionHandlers.get(part.toolName);
1179
+ if (!spec) continue;
1180
+ if (dispatchedClientActionsRef.current.has(part.toolCallId)) continue;
1181
+ dispatchedClientActionsRef.current.add(part.toolCallId);
1182
+ void runClientAction(spec, part, last.parts);
1183
+ }
1184
+ }, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
1185
+ async function send(text) {
1186
+ const trimmed = text.trim();
1187
+ if (!trimmed || busy) return;
1188
+ setError(null);
1189
+ setBusy(true);
1190
+ setMessages((prev) => [
1191
+ ...prev,
1192
+ { role: "user", parts: [{ kind: "text", id: "u", text: trimmed }] },
1193
+ { role: "assistant", parts: [] }
1194
+ ]);
1195
+ const replaceLast = (parts) => setMessages((prev) => {
1196
+ const next = [...prev];
1197
+ next[next.length - 1] = { role: "assistant", parts };
1198
+ return next;
1199
+ });
1200
+ const focused = getFocusedTrackEntry(trackRegistry);
1201
+ const context = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : void 0;
1202
+ try {
1203
+ await streamChat(
1204
+ apiBaseUrl,
1205
+ apiKey,
1206
+ {
1207
+ bot_id: botId,
1208
+ message: trimmed,
1209
+ conversation_id: onboarding?.conversation_id ?? null,
1210
+ context,
1211
+ nudge_node_id: nudgeNodeId ?? null
1212
+ },
1213
+ { onPart: replaceLast, onError: setError }
1214
+ );
1215
+ } catch (e) {
1216
+ setError(e instanceof Error ? e.message : String(e));
1217
+ } finally {
1218
+ setBusy(false);
1219
+ }
1220
+ }
1221
+ return { onboarding, messages, busy, error, send };
1222
+ }
1223
+
1224
+ // src/views.tsx
1225
+ var import_jsx_runtime6 = (
1226
+ // eslint-disable-next-line @next/next/no-img-element
1227
+ require("react/jsx-runtime")
1228
+ );
1229
+ function formatPrice(price) {
1230
+ if (!price?.amount) return "\uAC00\uACA9\uBB38\uC758";
1231
+ return `${(price.amount / 1e4).toLocaleString()}\uB9CC\uC6D0`;
1232
+ }
1233
+ function ProductCard({ item, detailed = false }) {
1234
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1235
+ "a",
1236
+ {
1237
+ href: item.href ?? void 0,
1238
+ target: "_blank",
1239
+ rel: "noreferrer",
1240
+ className: "flex w-64 shrink-0 flex-col overflow-hidden rounded-xl border border-neutral-200 bg-white transition hover:shadow-md dark:border-neutral-700 dark:bg-neutral-900",
1241
+ children: [
1242
+ item.imageUrl ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("img", { src: item.imageUrl, alt: item.title, className: "h-36 w-full object-cover" }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex h-36 w-full items-center justify-center bg-neutral-100 text-xs text-neutral-400 dark:bg-neutral-800", children: "\uC774\uBBF8\uC9C0 \uC5C6\uC74C" }),
1243
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-1 flex-col gap-1 p-3", children: [
1244
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-sm font-semibold", children: item.title }),
1245
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-xs text-neutral-500", children: item.subtitle }),
1246
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "xbnova-accent-price mt-1 text-base font-bold", children: formatPrice(item.price) }),
1247
+ item.badges?.length ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mt-1 flex flex-wrap gap-1", children: item.badges.map((b) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1248
+ "span",
1249
+ {
1250
+ className: "rounded bg-neutral-100 px-1.5 py-0.5 text-[11px] text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300",
1251
+ children: b
1252
+ },
1253
+ b
1254
+ )) }) : null,
1255
+ detailed && item.attributes?.length ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("dl", { className: "mt-2 grid grid-cols-2 gap-x-2 gap-y-1 text-[11px]", children: item.attributes.map((a) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex gap-1", children: [
1256
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("dt", { className: "text-neutral-400", children: a.label }),
1257
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("dd", { className: "text-neutral-700 dark:text-neutral-300", children: a.value })
1258
+ ] }, a.label)) }) : null
1259
+ ] })
1260
+ ]
1261
+ }
1262
+ );
1263
+ }
1264
+ function ProductList({ output }) {
1265
+ const data = output;
1266
+ if (!data?.items?.length) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Fallback, { label: "\uAC80\uC0C9 \uACB0\uACFC \uC5C6\uC74C" });
1267
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "my-2", children: [
1268
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mb-1 text-xs text-neutral-500", children: [
1269
+ "\uCD1D ",
1270
+ data.total?.toLocaleString(),
1271
+ "\uAC74"
1272
+ ] }),
1273
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex gap-2 overflow-x-auto pb-2", children: data.items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ProductCard, { item }, item.id)) })
1274
+ ] });
1275
+ }
1276
+ function ProductDetail({ output }) {
1277
+ const data = output;
1278
+ if (!data?.item) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Fallback, { label: "\uB9E4\uBB3C \uC5C6\uC74C" });
1279
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "my-2", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ProductCard, { item: data.item, detailed: true }) });
1280
+ }
1281
+ function Verdict({
1282
+ output,
1283
+ onVerdictAction
1284
+ }) {
1285
+ const data = output;
1286
+ if (!data?.conclusion) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Fallback, { label: "\uD310\uB2E8 \uACB0\uACFC \uC5C6\uC74C" });
1287
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "my-2 max-w-[92%] overflow-hidden rounded-xl border border-amber-200 bg-white text-xs dark:border-amber-900 dark:bg-neutral-900", children: [
1288
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "border-b border-amber-200 bg-amber-50 px-3 py-1.5 font-bold text-amber-800 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-300", children: [
1289
+ "\u25B3 ",
1290
+ data.headline ?? "\uC870\uAC74\uBD80 \uAC80\uD1A0"
1291
+ ] }),
1292
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex gap-2 border-b border-neutral-100 px-3 py-1.5 dark:border-neutral-800", children: [
1293
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "w-10 shrink-0 pt-px font-semibold text-neutral-400", children: "\uACB0\uB860" }),
1294
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-neutral-800 dark:text-neutral-100", children: data.conclusion })
1295
+ ] }),
1296
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex gap-2 border-b border-neutral-100 px-3 py-1.5 dark:border-neutral-800", children: [
1297
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "w-10 shrink-0 pt-px font-semibold text-neutral-400", children: "\uADFC\uAC70" }),
1298
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-neutral-600 dark:text-neutral-300", children: data.evidence })
1299
+ ] }),
1300
+ data.unknowns?.length ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex gap-2 border-b border-neutral-100 px-3 py-1.5 dark:border-neutral-800", children: [
1301
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "w-10 shrink-0 pt-px font-semibold text-neutral-400", children: "\uD655\uC778 \uBD88\uAC00" }),
1302
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-neutral-500", children: data.unknowns.join(" \xB7 ") })
1303
+ ] }) : null,
1304
+ data.actions?.length ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex flex-wrap gap-1.5 p-2", children: data.actions.map((action) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1305
+ "button",
1306
+ {
1307
+ type: "button",
1308
+ onClick: () => onVerdictAction?.(action.action),
1309
+ className: action.kind === "ghost" ? "xbnova-accent-btn-ghost rounded-md border px-2.5 py-1 text-[11px] font-semibold" : "xbnova-accent-btn rounded-md px-2.5 py-1 text-[11px] font-semibold text-white",
1310
+ children: action.label
1311
+ },
1312
+ action.label
1313
+ )) }) : null
1314
+ ] });
1315
+ }
1316
+ function Fallback({ label }) {
1317
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "my-2 rounded-lg border border-dashed border-neutral-300 px-3 py-2 text-xs text-neutral-500 dark:border-neutral-700", children: label });
1318
+ }
1319
+ var DEFAULT_RENDERERS = {
1320
+ product_list: ProductList,
1321
+ product_detail: ProductDetail,
1322
+ verdict: Verdict
1323
+ };
1324
+ function ToolView({
1325
+ part,
1326
+ viewRenderers,
1327
+ onVerdictAction
1328
+ }) {
1329
+ if (part.state === "input") {
1330
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Fallback, { label: `${part.toolName} \uC870\uD68C \uC911\u2026` });
1331
+ }
1332
+ if (part.state === "error") {
1333
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Fallback, { label: `${part.toolName} \uC2E4\uD328` });
1334
+ }
1335
+ const Renderer = part.view ? viewRenderers?.[part.view] ?? DEFAULT_RENDERERS[part.view] : void 0;
1336
+ if (!Renderer) {
1337
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Fallback, { label: `${part.toolName} \uACB0\uACFC (view: ${part.view ?? "\uC5C6\uC74C"})` });
1338
+ }
1339
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Renderer, { output: part.output, onVerdictAction });
1340
+ }
1341
+
1342
+ // src/chat.tsx
1343
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1344
+ function hasVisiblePart(parts, clientActionHandlers) {
1345
+ return parts.some((part) => {
1346
+ if (part.kind === "text") return true;
1347
+ const spec = clientActionHandlers.get(part.toolName);
1348
+ return spec ? Boolean(spec.render) : true;
1349
+ });
1350
+ }
1351
+ var DISCLOSURE_SEEN_KEY = "xbnova_disclosure_seen";
1352
+ var SUMMARY_FACT_KEY = "xbnova_summary";
1353
+ function readDisclosureSeen() {
1354
+ if (typeof window === "undefined") return true;
1355
+ try {
1356
+ return window.localStorage.getItem(DISCLOSURE_SEEN_KEY) === "1";
1357
+ } catch {
1358
+ return true;
1359
+ }
1360
+ }
1361
+ function markDisclosureSeen() {
1362
+ if (typeof window === "undefined") return;
1363
+ try {
1364
+ window.localStorage.setItem(DISCLOSURE_SEEN_KEY, "1");
1365
+ } catch {
1366
+ }
1367
+ }
1368
+ var DESKTOP_MEDIA_QUERY = "(min-width: 640px)";
1369
+ function useIsDesktopViewport() {
1370
+ const [isDesktop, setIsDesktop] = (0, import_react9.useState)(false);
1371
+ (0, import_react9.useEffect)(() => {
1372
+ if (typeof window === "undefined") return;
1373
+ const mql = window.matchMedia(DESKTOP_MEDIA_QUERY);
1374
+ setIsDesktop(mql.matches);
1375
+ const onChange = () => setIsDesktop(mql.matches);
1376
+ mql.addEventListener("change", onChange);
1377
+ return () => mql.removeEventListener("change", onChange);
1378
+ }, []);
1379
+ return isDesktop;
1380
+ }
1381
+ var Z_BAR = 2147483e3;
1382
+ var Z_PANEL = 2147483e3;
1383
+ var Z_BACKDROP = 2147482999;
1384
+ function barBaseStyle(isDesktop) {
1385
+ return isDesktop ? { position: "fixed", right: "1rem", bottom: "1rem", width: "20rem", zIndex: Z_BAR } : { position: "fixed", left: "0.625rem", right: "0.625rem", bottom: "1rem", zIndex: Z_BAR };
1386
+ }
1387
+ function panelBaseStyle(isDesktop) {
1388
+ return isDesktop ? { position: "fixed", right: "1rem", bottom: "1rem", width: "20rem", height: "32rem", zIndex: Z_PANEL } : { position: "fixed", left: 0, right: 0, top: "7rem", bottom: 0, zIndex: Z_PANEL };
1389
+ }
1390
+ function backdropBaseStyle(isDesktop) {
1391
+ return isDesktop ? { display: "none" } : { position: "fixed", inset: 0, zIndex: Z_BACKDROP };
1392
+ }
1393
+ function useTypewriter(text, key, speedMs = 28) {
1394
+ const [shown, setShown] = (0, import_react9.useState)("");
1395
+ (0, import_react9.useEffect)(() => {
1396
+ setShown("");
1397
+ if (!text) return;
1398
+ let i = 0;
1399
+ const id = window.setInterval(() => {
1400
+ i += 1;
1401
+ setShown(text.slice(0, i));
1402
+ if (i >= text.length) window.clearInterval(id);
1403
+ }, speedMs);
1404
+ return () => window.clearInterval(id);
1405
+ }, [key, text, speedMs]);
1406
+ return shown;
1407
+ }
1408
+ function GlowBlobs() {
1409
+ const blobBase = {
1410
+ position: "absolute",
1411
+ width: "6.5rem",
1412
+ height: "6.5rem",
1413
+ borderRadius: "9999px",
1414
+ pointerEvents: "none"
1415
+ };
1416
+ const accent1 = "var(--xbnova-accent-1, #4f46e5)";
1417
+ const accent2 = "var(--xbnova-accent-2, #7c3aed)";
1418
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1419
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1420
+ "span",
1421
+ {
1422
+ className: "xbnova-blob-1",
1423
+ style: {
1424
+ ...blobBase,
1425
+ width: "7.5rem",
1426
+ height: "7.5rem",
1427
+ left: "0%",
1428
+ top: "5%",
1429
+ background: accent1,
1430
+ opacity: 0.62
1431
+ }
1432
+ }
1433
+ ),
1434
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1435
+ "span",
1436
+ {
1437
+ className: "xbnova-blob-2",
1438
+ style: { ...blobBase, left: "70%", top: "45%", background: accent2, opacity: 0.42 }
1439
+ }
1440
+ ),
1441
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1442
+ "span",
1443
+ {
1444
+ className: "xbnova-blob-3",
1445
+ style: {
1446
+ ...blobBase,
1447
+ left: "35%",
1448
+ top: "70%",
1449
+ background: `color-mix(in srgb, ${accent1} 65%, ${accent2} 35%)`,
1450
+ opacity: 0.48
1451
+ }
1452
+ }
1453
+ ),
1454
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1455
+ "span",
1456
+ {
1457
+ className: "xbnova-blob-4",
1458
+ style: {
1459
+ ...blobBase,
1460
+ width: "7.5rem",
1461
+ height: "7.5rem",
1462
+ left: "50%",
1463
+ top: "10%",
1464
+ background: accent1,
1465
+ opacity: 0.5
1466
+ }
1467
+ }
1468
+ )
1469
+ ] });
1470
+ }
1471
+ function XbNovaChat({
1472
+ viewRenderers,
1473
+ botId,
1474
+ barStyle,
1475
+ panelStyle,
1476
+ backdropStyle,
1477
+ defaultOpen = false,
1478
+ idleText = "AI\uC5D0\uAC8C \uC774 \uCC28 \uBB3C\uC5B4\uBCF4\uAE30",
1479
+ onVerdictAction
1480
+ }) {
1481
+ const { auth, trackRegistry, factsRegistry, visitorId, clientActionHandlers, theme } = useXbNovaInternal();
1482
+ const themeVars = {
1483
+ "--xbnova-accent-1": theme.accentFrom,
1484
+ "--xbnova-accent-2": theme.accentTo
1485
+ };
1486
+ const avatarGradientStyle = {
1487
+ background: "linear-gradient(135deg, var(--xbnova-accent-1), var(--xbnova-accent-2))"
1488
+ };
1489
+ const { hint, lastHint, dismissHint, openHint, autoHideHint, hintsEnabled, barHidden } = useNudgeState();
1490
+ const isDesktop = useIsDesktopViewport();
1491
+ const [open, setOpen] = (0, import_react9.useState)(defaultOpen);
1492
+ const [entered, setEntered] = (0, import_react9.useState)(false);
1493
+ const [activeBotId, setActiveBotId] = (0, import_react9.useState)(void 0);
1494
+ const [activeNudgeNodeId, setActiveNudgeNodeId] = (0, import_react9.useState)(void 0);
1495
+ const [nudgeQuickReplies, setNudgeQuickReplies] = (0, import_react9.useState)([]);
1496
+ const [openingLine, setOpeningLine] = (0, import_react9.useState)(null);
1497
+ const [showDisclosure, setShowDisclosure] = (0, import_react9.useState)(false);
1498
+ const [input, setInput] = (0, import_react9.useState)("");
1499
+ const bottomRef = (0, import_react9.useRef)(null);
1500
+ const typedHintText = useTypewriter(hint?.text ?? "", hint?.triggerNodeId ?? "");
1501
+ const { onboarding, messages, busy, error, send } = useXbNovaChat({
1502
+ botId: activeBotId ?? botId,
1503
+ nudgeNodeId: activeNudgeNodeId,
1504
+ onDismissSheet: () => setOpen(false)
1505
+ });
1506
+ (0, import_react9.useEffect)(() => {
1507
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
1508
+ }, [messages]);
1509
+ (0, import_react9.useEffect)(() => {
1510
+ if (!open) {
1511
+ setEntered(false);
1512
+ return;
1513
+ }
1514
+ const raf = requestAnimationFrame(() => setEntered(true));
1515
+ return () => cancelAnimationFrame(raf);
1516
+ }, [open]);
1517
+ (0, import_react9.useEffect)(() => {
1518
+ if (!hint || hint.autoHideMs <= 0) return;
1519
+ console.debug(`[XbNova] \uD78C\uD2B8 "${hint.text}" \uD45C\uC2DC \u2014 ${hint.autoHideMs}ms \uD6C4 \uC790\uB3D9\uC73C\uB85C \uC0AC\uB77C\uC9D0`);
1520
+ const timer = window.setTimeout(() => {
1521
+ console.debug(`[XbNova] \uD78C\uD2B8 \uC790\uB3D9 \uC18C\uBA78(\uD0C0\uC784\uC544\uC6C3) \u2014 "${hint.text}"`);
1522
+ autoHideHint();
1523
+ }, hint.autoHideMs);
1524
+ return () => window.clearTimeout(timer);
1525
+ }, [hint, autoHideHint]);
1526
+ function openSheet() {
1527
+ const consumed = hint ?? lastHint;
1528
+ openHint();
1529
+ if (consumed?.botId) setActiveBotId(consumed.botId);
1530
+ if (consumed) {
1531
+ setOpeningLine(consumed.text);
1532
+ setActiveNudgeNodeId(consumed.messageNodeId);
1533
+ setNudgeQuickReplies(consumed.quickReplies);
1534
+ fetch(`${auth.apiBaseUrl}/v1/nudge-events`, {
1535
+ method: "POST",
1536
+ headers: { "Content-Type": "application/json" },
1537
+ body: JSON.stringify({
1538
+ app_id: auth.appId,
1539
+ nudge_node_id: consumed.messageNodeId,
1540
+ kind: "nudge_open",
1541
+ bot_id: consumed.botId,
1542
+ visitor_id: visitorId
1543
+ })
1544
+ }).catch(() => {
1545
+ });
1546
+ } else {
1547
+ setNudgeQuickReplies([]);
1548
+ setActiveNudgeNodeId(void 0);
1549
+ const focused = getFocusedTrackEntry(trackRegistry);
1550
+ setOpeningLine(
1551
+ focused ? `${focused.label ?? focused.region}\uC744(\uB97C) \uBCF4\uACE0 \uACC4\uC2DC\uB124\uC694. \uAD81\uAE08\uD558\uC2E0 \uC810\uC744 \uD3B8\uD558\uAC8C \uBB3C\uC5B4\uBCF4\uC138\uC694.` : null
1552
+ );
1553
+ }
1554
+ if (!readDisclosureSeen()) {
1555
+ setShowDisclosure(true);
1556
+ markDisclosureSeen();
1557
+ }
1558
+ setOpen(true);
1559
+ }
1560
+ if (!open) {
1561
+ if (barHidden) return null;
1562
+ const showHint = Boolean(hint && hintsEnabled);
1563
+ const collapsedSize = "3.25rem";
1564
+ const idleLabel = lastHint?.text ?? idleText;
1565
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(XbNovaShadowRoot, { style: { ...barBaseStyle(isDesktop), ...themeVars, ...barStyle }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1566
+ "div",
1567
+ {
1568
+ style: {
1569
+ minHeight: collapsedSize,
1570
+ height: showHint ? "auto" : collapsedSize,
1571
+ width: showHint ? "100%" : collapsedSize,
1572
+ marginLeft: "auto",
1573
+ borderRadius: showHint ? "1.5rem" : "9999px",
1574
+ transition: "width 320ms cubic-bezier(0.22, 1, 0.36, 1), height 200ms ease, border-radius 200ms ease"
1575
+ },
1576
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1577
+ "div",
1578
+ {
1579
+ className: `flex h-full w-full items-center overflow-hidden ${showHint ? "xbnova-glow-bg" : ""}`,
1580
+ style: {
1581
+ borderRadius: "inherit",
1582
+ // GlowBlobs(SVG)를 이 안에 절대위치로 깔려면 이 요소가 위치 기준점이어야
1583
+ // 한다 — 인라인으로 줘서 shadow DOM 비동기 CSS 로딩과 경합하지 않는다.
1584
+ position: "relative",
1585
+ // 접혀있을 땐 이 컨테이너 자체가 완전히 안 보인다(배경도 그림자도 없음) —
1586
+ // 스크롤 중에도 마찬가지다. 예전엔 스크롤 중 반투명 회색 배경을 깔았는데,
1587
+ // 그 배경 자체가 페이지 위에 원형 얼룩처럼 보인다는 피드백을 받았다
1588
+ // ("스크롤 좀만 움직여도 저 투명한게 회색으로 바뀜") — 그냥 안 보이는 게
1589
+ // 맞다. 펼쳐지면(showHint) xbnova-glow-bg 클래스가 배경/블러를 전부 맡는다.
1590
+ background: showHint ? void 0 : "transparent",
1591
+ // 접힌 상태의 "떠 있는" 느낌은 이 컨테이너가 아니라 안쪽 아바타 배지
1592
+ // (아래 `shadow-md`, 지름 28px)만 낸다 — 52px 컨테이너 전체에 그림자를
1593
+ // 주면 그 자체가 회색 원 얼룩처럼 보인다.
1594
+ // 참고 이미지(카드 위에 살짝 겹쳐 얹힌 "프로"/"미완" 뱃지)처럼 진짜 스티커가
1595
+ // 붕 떠 있는 느낌은 은은하게 퍼지는 그림자가 아니라 — 붙어있는 면에 짧고
1596
+ // 진한 그림자(contact shadow) + 그보다 넓게 퍼지지만 방향성 있는 그림자를
1597
+ // 겹쳐야 난다. 예전 값(퍼지기만 하는 단일 그림자)은 배경에 스며드는
1598
+ // "glow"처럼 보여서 뜬 느낌이 안 났다.
1599
+ boxShadow: showHint ? "0 2px 3px rgba(16,24,40,.35), 0 14px 24px -6px rgba(16,24,40,.45)" : "none",
1600
+ transition: "background 200ms ease, box-shadow 200ms ease"
1601
+ },
1602
+ children: [
1603
+ showHint ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(GlowBlobs, {}) : null,
1604
+ showHint ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1605
+ "button",
1606
+ {
1607
+ type: "button",
1608
+ onClick: dismissHint,
1609
+ "aria-label": "\uD78C\uD2B8 \uB2EB\uAE30",
1610
+ className: "shrink-0 py-3.5 pl-3.5 pr-1 text-slate-400 hover:text-slate-200",
1611
+ style: { position: "relative" },
1612
+ children: "\u2715"
1613
+ }
1614
+ ) : null,
1615
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1616
+ "button",
1617
+ {
1618
+ type: "button",
1619
+ onClick: openSheet,
1620
+ "aria-label": showHint ? hint.text : idleLabel,
1621
+ className: "flex min-w-0 flex-1 items-center text-left",
1622
+ style: { position: "relative" },
1623
+ children: [
1624
+ showHint ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "min-w-0 flex-1 whitespace-normal break-words py-3.5 pl-2 pr-1 text-[13px] font-semibold leading-snug text-white", children: typedHintText }) : null,
1625
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1626
+ "span",
1627
+ {
1628
+ className: "flex shrink-0 items-center justify-center",
1629
+ style: { width: collapsedSize, height: collapsedSize },
1630
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1631
+ "span",
1632
+ {
1633
+ className: "flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-bold text-white shadow-md",
1634
+ style: avatarGradientStyle,
1635
+ children: "AI"
1636
+ }
1637
+ )
1638
+ }
1639
+ )
1640
+ ]
1641
+ }
1642
+ )
1643
+ ]
1644
+ }
1645
+ )
1646
+ }
1647
+ ) });
1648
+ }
1649
+ const summary = resolveFactValue(factsRegistry.get(SUMMARY_FACT_KEY));
1650
+ const focusedLabel = getFocusedTrackEntry(trackRegistry)?.label;
1651
+ const contextLine = summary ?? (focusedLabel ? `${focusedLabel} \uBCF4\uB294 \uC911` : void 0);
1652
+ const firstBubble = openingLine ?? onboarding?.greeting;
1653
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1654
+ !isDesktop ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1655
+ XbNovaShadowRoot,
1656
+ {
1657
+ style: {
1658
+ ...backdropBaseStyle(isDesktop),
1659
+ opacity: entered ? 1 : 0,
1660
+ transition: "opacity 260ms ease-out",
1661
+ ...backdropStyle
1662
+ },
1663
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "h-full w-full bg-black/45", onClick: () => setOpen(false) })
1664
+ }
1665
+ ) : null,
1666
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1667
+ XbNovaShadowRoot,
1668
+ {
1669
+ style: {
1670
+ ...panelBaseStyle(isDesktop),
1671
+ ...themeVars,
1672
+ transform: entered ? "translateY(0)" : "translateY(100%)",
1673
+ transition: "transform 280ms cubic-bezier(0.22, 1, 0.36, 1)",
1674
+ ...panelStyle
1675
+ },
1676
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex h-full w-full flex-col rounded-t-[20px] bg-white shadow-2xl sm:rounded-2xl sm:border sm:border-neutral-200 dark:bg-neutral-900 sm:dark:border-neutral-700", children: [
1677
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("header", { className: "flex items-center gap-2 border-b border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700", children: [
1678
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1679
+ "span",
1680
+ {
1681
+ className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[10px] font-bold text-white",
1682
+ style: avatarGradientStyle,
1683
+ children: "AI"
1684
+ }
1685
+ ),
1686
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "min-w-0 flex-1", children: [
1687
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "text-sm font-semibold", children: onboarding?.branding?.title ?? "AI \uB3D9\uD589" }),
1688
+ contextLine ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "truncate text-[11px] text-neutral-400", children: contextLine }) : null
1689
+ ] }),
1690
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1691
+ "button",
1692
+ {
1693
+ type: "button",
1694
+ onClick: () => setOpen(false),
1695
+ "aria-label": "\uCC44\uD305 \uB2EB\uAE30",
1696
+ className: "shrink-0 text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200",
1697
+ children: "\xD7"
1698
+ }
1699
+ )
1700
+ ] }),
1701
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [
1702
+ 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,
1703
+ firstBubble ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssistantMarkdown, { text: firstBubble }) }) : null,
1704
+ 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)("div", { className: "space-y-1", children: [
1705
+ message.parts.map((part, j) => {
1706
+ if (part.kind === "text") {
1707
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssistantMarkdown, { text: part.text }) }, j);
1708
+ }
1709
+ if (clientActionHandlers.has(part.toolName)) {
1710
+ const spec = clientActionHandlers.get(part.toolName);
1711
+ return spec?.render ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: spec.render(part.input) }, j) : null;
1712
+ }
1713
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ToolView, { part, viewRenderers, onVerdictAction }, j);
1714
+ }),
1715
+ !hasVisiblePart(message.parts, clientActionHandlers) && busy ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "text-neutral-400", children: "\uC0DD\uAC01\uD558\uB294 \uC911\u2026" }) }) : null
1716
+ ] }) }, i)),
1717
+ error ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rounded-lg bg-red-50 px-3 py-2 text-xs text-red-700 dark:bg-red-950 dark:text-red-300", children: error }) : null,
1718
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: bottomRef })
1719
+ ] }),
1720
+ (() => {
1721
+ if (nudgeQuickReplies.length > 0) {
1722
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "flex flex-wrap gap-1.5 border-t border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700", children: nudgeQuickReplies.map((q) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1723
+ "button",
1724
+ {
1725
+ onClick: () => {
1726
+ setNudgeQuickReplies([]);
1727
+ send(q);
1728
+ },
1729
+ className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
1730
+ children: q
1731
+ },
1732
+ q
1733
+ )) });
1734
+ }
1735
+ const quickReplies = onboarding?.quick_replies ?? [];
1736
+ if (messages.length !== 0 || quickReplies.length === 0) return null;
1737
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "flex flex-wrap gap-1.5 border-t border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700", children: quickReplies.map((q) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1738
+ "button",
1739
+ {
1740
+ onClick: () => send(q),
1741
+ className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
1742
+ children: q
1743
+ },
1744
+ q
1745
+ )) });
1746
+ })(),
1747
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1748
+ "form",
1749
+ {
1750
+ onSubmit: (e) => {
1751
+ e.preventDefault();
1752
+ setNudgeQuickReplies([]);
1753
+ send(input);
1754
+ setInput("");
1755
+ },
1756
+ className: "flex items-center gap-2 border-t border-neutral-200 px-3 py-2.5 dark:border-neutral-700",
1757
+ children: [
1758
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1759
+ "input",
1760
+ {
1761
+ value: input,
1762
+ onChange: (e) => setInput(e.target.value),
1763
+ placeholder: "\uC774 \uCC28\uC5D0 \uB300\uD574 \uBB34\uC5C7\uC774\uB4E0 \uBB3C\uC5B4\uBCF4\uC138\uC694",
1764
+ disabled: busy,
1765
+ className: "xbnova-input-accent flex-1 rounded-full border border-neutral-300 px-3.5 py-2 text-sm outline-none disabled:opacity-50 dark:border-neutral-600 dark:bg-neutral-800"
1766
+ }
1767
+ ),
1768
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1769
+ "button",
1770
+ {
1771
+ type: "submit",
1772
+ disabled: busy || !input.trim(),
1773
+ "aria-label": "\uC804\uC1A1",
1774
+ className: "xbnova-accent-btn flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-white transition disabled:opacity-40",
1775
+ children: "\u2191"
1776
+ }
1777
+ )
1778
+ ]
1779
+ }
1780
+ )
1781
+ ] })
1782
+ }
1783
+ )
1784
+ ] });
1785
+ }
1786
+ function Bubble({ role, children }) {
1787
+ const mine = role === "user";
1788
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: mine ? "flex justify-end" : "flex justify-start", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1789
+ "div",
1790
+ {
1791
+ className: `max-w-[85%] rounded-2xl px-3.5 py-2 text-sm ${mine ? "whitespace-pre-wrap" : ""} ${mine ? "xbnova-accent-btn text-white" : "bg-neutral-100 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-100"}`,
1792
+ children
1793
+ }
1794
+ ) });
1795
+ }
1796
+ function AssistantMarkdown({ text }) {
1797
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "space-y-1.5 text-sm leading-relaxed [&_ol]:list-decimal [&_ul]:list-disc [&_ol]:pl-5 [&_ul]:pl-5 [&_li]:pl-0.5 [&_strong]:font-semibold [&_a]:underline [&_code]:rounded [&_code]:bg-black/10 [&_code]:px-1 [&_code]:py-0.5 [&_code]:text-[0.85em] [&_p]:mb-1.5 last:[&_p]:mb-0", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_markdown.default, { remarkPlugins: [import_remark_gfm.default, import_remark_breaks.default], children: text }) });
1798
+ }
1799
+
1800
+ // src/facts.ts
1801
+ var import_react10 = require("react");
1802
+ function useXbNovaFact(key, value) {
1803
+ const { factsRegistry } = useXbNovaInternal();
1804
+ (0, import_react10.useEffect)(() => {
1805
+ factsRegistry.set(key, value);
1806
+ });
1807
+ (0, import_react10.useEffect)(() => {
1808
+ return () => {
1809
+ factsRegistry.delete(key);
1810
+ };
1811
+ }, [key, factsRegistry]);
1812
+ }
1813
+ function useXbNovaFacts(values) {
1814
+ const { factsRegistry } = useXbNovaInternal();
1815
+ const keysRef = (0, import_react10.useRef)([]);
1816
+ (0, import_react10.useEffect)(() => {
1817
+ for (const [key, value] of Object.entries(values)) {
1818
+ factsRegistry.set(key, value);
1819
+ }
1820
+ keysRef.current = Object.keys(values);
1821
+ });
1822
+ (0, import_react10.useEffect)(() => {
1823
+ return () => {
1824
+ for (const key of keysRef.current) {
1825
+ factsRegistry.delete(key);
1826
+ }
1827
+ };
1828
+ }, [factsRegistry]);
1829
+ }
1830
+ function useXbNovaVisitor() {
1831
+ const { visitorId } = useXbNovaInternal();
1832
+ return visitorId;
1833
+ }
1834
+ function XbNovaFact({ id, value }) {
1835
+ useXbNovaFact(id, value);
1836
+ return null;
1837
+ }
1838
+ function XbNovaFacts({ values }) {
1839
+ useXbNovaFacts(values);
1840
+ return null;
1841
+ }
1842
+ // Annotate the CommonJS export names for ESM import in node:
1843
+ 0 && (module.exports = {
1844
+ XbNovaChat,
1845
+ XbNovaFact,
1846
+ XbNovaFacts,
1847
+ XbNovaProvider,
1848
+ XbNovaTrack,
1849
+ trackXbNovaEvent,
1850
+ useXbNovaChat,
1851
+ useXbNovaFact,
1852
+ useXbNovaFacts,
1853
+ useXbNovaSettings,
1854
+ useXbNovaTrack,
1855
+ useXbNovaVisitor
1856
+ });
1857
+ //# sourceMappingURL=index.cjs.map