@customerhero/react 2.2.0 → 2.4.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.
@@ -0,0 +1,3182 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/preview.tsx
21
+ var preview_exports = {};
22
+ __export(preview_exports, {
23
+ PreviewWidget: () => PreviewWidget
24
+ });
25
+ module.exports = __toCommonJS(preview_exports);
26
+ var import_react12 = require("react");
27
+
28
+ // src/context.tsx
29
+ var import_react = require("react");
30
+ var import_js = require("@customerhero/js");
31
+ var import_jsx_runtime = require("react/jsx-runtime");
32
+ var CustomerHeroContext = (0, import_react.createContext)(null);
33
+ function CustomerHeroProvider({
34
+ children,
35
+ disableAutoFetch,
36
+ ...config
37
+ }) {
38
+ const clientRef = (0, import_react.useRef)(null);
39
+ if (!clientRef.current) {
40
+ clientRef.current = new import_js.CustomerHeroChat(config);
41
+ }
42
+ (0, import_react.useEffect)(() => {
43
+ if (disableAutoFetch) return;
44
+ clientRef.current?.fetchConfig();
45
+ }, [disableAutoFetch]);
46
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CustomerHeroContext.Provider, { value: clientRef.current, children });
47
+ }
48
+ function useCustomerHeroClient() {
49
+ const client = (0, import_react.useContext)(CustomerHeroContext);
50
+ if (!client) {
51
+ throw new Error(
52
+ "useCustomerHeroClient must be used within a <CustomerHeroProvider>"
53
+ );
54
+ }
55
+ return client;
56
+ }
57
+
58
+ // src/components/chat-bubble.tsx
59
+ var import_react5 = require("react");
60
+
61
+ // src/use-chat.ts
62
+ var import_react2 = require("react");
63
+ function useChat() {
64
+ const client = useCustomerHeroClient();
65
+ const state = (0, import_react2.useSyncExternalStore)(
66
+ (0, import_react2.useCallback)((cb) => client.subscribe(cb), [client]),
67
+ () => client.getState(),
68
+ () => client.getState()
69
+ );
70
+ return {
71
+ ...state,
72
+ t: client.t,
73
+ sendMessage: (0, import_react2.useCallback)(
74
+ (message, options) => client.sendMessage(message, options),
75
+ [client]
76
+ ),
77
+ uploadAttachment: (0, import_react2.useCallback)(
78
+ (blob, options) => client.uploadAttachment(blob, options),
79
+ [client]
80
+ ),
81
+ rateMessage: (0, import_react2.useCallback)(
82
+ (messageId, rating) => client.rateMessage(messageId, rating),
83
+ [client]
84
+ ),
85
+ approveAction: (0, import_react2.useCallback)(
86
+ (pendingId) => client.approveAction(pendingId),
87
+ [client]
88
+ ),
89
+ cancelAction: (0, import_react2.useCallback)(
90
+ (pendingId) => client.cancelAction(pendingId),
91
+ [client]
92
+ ),
93
+ setLocale: (0, import_react2.useCallback)((tag) => client.setLocale(tag), [client]),
94
+ toggle: (0, import_react2.useCallback)(() => client.toggle(), [client]),
95
+ open: (0, import_react2.useCallback)(() => client.open(), [client]),
96
+ close: (0, import_react2.useCallback)(() => client.close(), [client]),
97
+ reset: (0, import_react2.useCallback)(() => client.reset(), [client]),
98
+ identify: (0, import_react2.useCallback)(
99
+ (payload) => client.identify(payload),
100
+ [client]
101
+ ),
102
+ setConsent: (0, import_react2.useCallback)(
103
+ (consent) => client.setConsent(consent),
104
+ [client]
105
+ ),
106
+ setTraits: (0, import_react2.useCallback)(
107
+ (traits) => client.setTraits(traits),
108
+ [client]
109
+ ),
110
+ submitPreChatForm: (0, import_react2.useCallback)(
111
+ (submission) => client.submitPreChatForm(submission),
112
+ [client]
113
+ ),
114
+ cancelPreChatForm: (0, import_react2.useCallback)(() => client.cancelPreChatForm(), [client]),
115
+ fireTrigger: (0, import_react2.useCallback)(
116
+ (triggerId) => client.fireTrigger(triggerId),
117
+ [client]
118
+ ),
119
+ consumePendingPrefill: (0, import_react2.useCallback)(
120
+ () => client.consumePendingPrefill(),
121
+ [client]
122
+ ),
123
+ dismissIncidentBanner: (0, import_react2.useCallback)(
124
+ () => client.dismissIncidentBanner(),
125
+ [client]
126
+ )
127
+ };
128
+ }
129
+
130
+ // src/use-reduced-motion.ts
131
+ var import_react3 = require("react");
132
+ function useReducedMotion() {
133
+ const [reduced, setReduced] = (0, import_react3.useState)(() => {
134
+ if (typeof window === "undefined") return false;
135
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
136
+ });
137
+ (0, import_react3.useEffect)(() => {
138
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
139
+ const handler = (e) => setReduced(e.matches);
140
+ mq.addEventListener("change", handler);
141
+ return () => mq.removeEventListener("change", handler);
142
+ }, []);
143
+ return reduced;
144
+ }
145
+
146
+ // src/use-effective-theme.ts
147
+ var import_js2 = require("@customerhero/js");
148
+
149
+ // src/use-prefers-dark.ts
150
+ var import_react4 = require("react");
151
+ function usePrefersDark() {
152
+ const [prefersDark, setPrefersDark] = (0, import_react4.useState)(false);
153
+ (0, import_react4.useEffect)(() => {
154
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
155
+ return;
156
+ }
157
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
158
+ const update = () => setPrefersDark(mq.matches);
159
+ update();
160
+ if (typeof mq.addEventListener === "function") {
161
+ mq.addEventListener("change", update);
162
+ return () => mq.removeEventListener("change", update);
163
+ }
164
+ mq.addListener(update);
165
+ return () => mq.removeListener(update);
166
+ }, []);
167
+ return prefersDark;
168
+ }
169
+
170
+ // src/use-effective-theme.ts
171
+ function useEffectiveTheme() {
172
+ const { config } = useChat();
173
+ const prefersDark = usePrefersDark();
174
+ const scheme = (0, import_js2.resolveScheme)(config.colorScheme, prefersDark);
175
+ const colors = (0, import_js2.effectiveColors)(config, scheme);
176
+ const isDark = scheme === "dark";
177
+ return {
178
+ scheme,
179
+ primary: colors.primary,
180
+ background: colors.background,
181
+ text: colors.text,
182
+ bubbleBackground: isDark ? "rgba(255,255,255,0.08)" : "#f0f0f0",
183
+ divider: isDark ? "rgba(255,255,255,0.12)" : "#e0e0e0",
184
+ size: (0, import_js2.sizePreset)(config.size),
185
+ radius: (0, import_js2.panelRadius)(config.cornerStyle)
186
+ };
187
+ }
188
+
189
+ // src/components/chat-bubble.tsx
190
+ var import_jsx_runtime2 = require("react/jsx-runtime");
191
+ function ChatBubble({ embedded } = {}) {
192
+ const { toggle, config, t, isRtl } = useChat();
193
+ const reduced = useReducedMotion();
194
+ const theme = useEffectiveTheme();
195
+ const [mounted, setMounted] = (0, import_react5.useState)(false);
196
+ (0, import_react5.useEffect)(() => {
197
+ const id = requestAnimationFrame(() => setMounted(true));
198
+ return () => cancelAnimationFrame(id);
199
+ }, []);
200
+ const visible = mounted;
201
+ const effectivePosition = isRtl ? config.position === "bottom-right" ? "bottom-left" : "bottom-right" : config.position;
202
+ const colors = theme;
203
+ const preset = theme.size;
204
+ const hasLabel = !!config.launcher.label;
205
+ const width = hasLabel ? "auto" : preset.bubble;
206
+ const height = preset.bubble;
207
+ const borderRadius = hasLabel ? preset.bubble / 2 : "50%";
208
+ const style = {
209
+ position: embedded ? "absolute" : "fixed",
210
+ bottom: config.offset.bottom,
211
+ [effectivePosition === "bottom-left" ? "left" : "right"]: config.offset.side,
212
+ width,
213
+ height,
214
+ minWidth: preset.bubble,
215
+ paddingInline: hasLabel ? Math.round(preset.bubble * 0.35) : 0,
216
+ borderRadius,
217
+ background: colors.primary,
218
+ color: "#FFFFFF",
219
+ display: "flex",
220
+ alignItems: "center",
221
+ justifyContent: "center",
222
+ gap: hasLabel ? 8 : 0,
223
+ cursor: "pointer",
224
+ boxShadow: theme.scheme === "dark" ? "0 6px 20px rgba(0,0,0,0.55), 0 0 0 1px rgba(255,255,255,0.06)" : "0 4px 20px rgba(0,0,0,0.15)",
225
+ zIndex: config.zIndex,
226
+ border: "none",
227
+ fontSize: preset.fontSize,
228
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
229
+ fontWeight: 500,
230
+ opacity: visible ? 1 : 0,
231
+ transform: visible ? "scale(1)" : "scale(0.6)",
232
+ transition: reduced ? "none" : "opacity 0.25s ease, transform 0.25s ease",
233
+ pointerEvents: visible ? "auto" : "none"
234
+ };
235
+ const iconSize = Math.round(preset.bubble * 0.43);
236
+ const dotSize = Math.max(10, Math.round(preset.bubble * 0.22));
237
+ const dotOffset = Math.round(preset.bubble * 0.08);
238
+ const dotStyle = {
239
+ position: "absolute",
240
+ top: dotOffset,
241
+ [effectivePosition === "bottom-left" ? "left" : "right"]: dotOffset,
242
+ width: dotSize,
243
+ height: dotSize,
244
+ borderRadius: "50%",
245
+ background: "#22c55e",
246
+ border: "2px solid rgba(255,255,255,0.9)",
247
+ pointerEvents: "none"
248
+ };
249
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
250
+ "button",
251
+ {
252
+ onClick: toggle,
253
+ style,
254
+ dir: isRtl ? "rtl" : "ltr",
255
+ "aria-label": t("open_chat"),
256
+ onMouseEnter: (e) => {
257
+ if (!reduced) e.currentTarget.style.transform = "scale(1.06)";
258
+ },
259
+ onMouseLeave: (e) => {
260
+ if (!reduced) e.currentTarget.style.transform = "scale(1)";
261
+ },
262
+ children: [
263
+ config.launcher.iconUrl ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
264
+ "img",
265
+ {
266
+ src: config.launcher.iconUrl,
267
+ alt: "",
268
+ width: iconSize,
269
+ height: iconSize,
270
+ style: { display: "block" }
271
+ }
272
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
273
+ "svg",
274
+ {
275
+ width: iconSize,
276
+ height: iconSize,
277
+ viewBox: "0 0 24 24",
278
+ fill: "none",
279
+ stroke: "currentColor",
280
+ strokeWidth: "2",
281
+ strokeLinecap: "round",
282
+ strokeLinejoin: "round",
283
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })
284
+ }
285
+ ),
286
+ hasLabel && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: config.launcher.label }),
287
+ config.launcher.showOnlineDot && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: dotStyle, "aria-hidden": true })
288
+ ]
289
+ }
290
+ );
291
+ }
292
+
293
+ // src/components/chat-window.tsx
294
+ var import_react11 = require("react");
295
+
296
+ // src/components/chat-header.tsx
297
+ var import_react6 = require("react");
298
+ var import_jsx_runtime3 = require("react/jsx-runtime");
299
+ function ChatHeader() {
300
+ const { config, close, reset, t } = useChat();
301
+ const reduced = useReducedMotion();
302
+ const theme = useEffectiveTheme();
303
+ const [menuOpen, setMenuOpen] = (0, import_react6.useState)(false);
304
+ const menuRef = (0, import_react6.useRef)(null);
305
+ (0, import_react6.useEffect)(() => {
306
+ if (!menuOpen) return;
307
+ const handleClick = (e) => {
308
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
309
+ setMenuOpen(false);
310
+ }
311
+ };
312
+ document.addEventListener("mousedown", handleClick);
313
+ return () => document.removeEventListener("mousedown", handleClick);
314
+ }, [menuOpen]);
315
+ const headerStyle = {
316
+ background: theme.primary,
317
+ padding: 16,
318
+ display: "flex",
319
+ alignItems: "center",
320
+ gap: 12
321
+ };
322
+ const avatarStyle = {
323
+ width: 36,
324
+ height: 36,
325
+ borderRadius: "50%",
326
+ background: "rgba(255,255,255,0.2)",
327
+ display: "flex",
328
+ alignItems: "center",
329
+ justifyContent: "center",
330
+ flexShrink: 0
331
+ };
332
+ const titleStyle = {
333
+ fontSize: 14,
334
+ fontWeight: 600,
335
+ color: "white",
336
+ margin: 0
337
+ };
338
+ const subtitleStyle = {
339
+ fontSize: 11,
340
+ color: "rgba(255,255,255,0.7)",
341
+ margin: 0
342
+ };
343
+ const headerButtonStyle = {
344
+ background: "none",
345
+ border: "none",
346
+ color: "white",
347
+ cursor: "pointer",
348
+ opacity: 0.7,
349
+ padding: 4
350
+ };
351
+ const isDark = theme.scheme === "dark";
352
+ const menuStyle = {
353
+ position: "absolute",
354
+ top: "100%",
355
+ right: 0,
356
+ marginTop: 4,
357
+ background: theme.background,
358
+ border: `1px solid ${theme.divider}`,
359
+ borderRadius: 8,
360
+ boxShadow: isDark ? "0 4px 16px rgba(0,0,0,0.5)" : "0 4px 16px rgba(0,0,0,0.15)",
361
+ minWidth: 180,
362
+ overflow: "hidden",
363
+ zIndex: 10,
364
+ transformOrigin: "top right",
365
+ animation: reduced ? "none" : "ch-menu-in 0.15s ease"
366
+ };
367
+ const menuItemStyle = {
368
+ display: "flex",
369
+ alignItems: "center",
370
+ gap: 8,
371
+ width: "100%",
372
+ padding: "10px 14px",
373
+ border: "none",
374
+ background: "none",
375
+ cursor: "pointer",
376
+ fontSize: 13,
377
+ color: theme.text,
378
+ textAlign: "left",
379
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
380
+ };
381
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: headerStyle, children: [
382
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: avatarStyle, children: config.avatarUrl ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
383
+ "img",
384
+ {
385
+ src: config.avatarUrl,
386
+ alt: "",
387
+ style: { width: 36, height: 36, borderRadius: "50%" }
388
+ }
389
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
390
+ "svg",
391
+ {
392
+ width: "18",
393
+ height: "18",
394
+ viewBox: "0 0 24 24",
395
+ fill: "none",
396
+ stroke: "white",
397
+ strokeWidth: "2",
398
+ strokeLinecap: "round",
399
+ strokeLinejoin: "round",
400
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })
401
+ }
402
+ ) }),
403
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { flex: 1 }, children: [
404
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { style: titleStyle, children: config.title }),
405
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: subtitleStyle, children: t("online") })
406
+ ] }),
407
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { ref: menuRef, style: { position: "relative" }, children: [
408
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
409
+ "button",
410
+ {
411
+ onClick: () => setMenuOpen(!menuOpen),
412
+ style: headerButtonStyle,
413
+ "aria-label": t("menu"),
414
+ onMouseEnter: (e) => {
415
+ e.currentTarget.style.opacity = "1";
416
+ },
417
+ onMouseLeave: (e) => {
418
+ e.currentTarget.style.opacity = "0.7";
419
+ },
420
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
421
+ "svg",
422
+ {
423
+ width: "18",
424
+ height: "18",
425
+ viewBox: "0 0 24 24",
426
+ fill: "none",
427
+ stroke: "currentColor",
428
+ strokeWidth: "2",
429
+ strokeLinecap: "round",
430
+ strokeLinejoin: "round",
431
+ children: [
432
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "12", cy: "5", r: "1" }),
433
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "12", cy: "12", r: "1" }),
434
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "12", cy: "19", r: "1" })
435
+ ]
436
+ }
437
+ )
438
+ }
439
+ ),
440
+ menuOpen && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: menuStyle, children: [
441
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: `@keyframes ch-menu-in {
442
+ from { opacity: 0; transform: scale(0.9); }
443
+ to { opacity: 1; transform: scale(1); }
444
+ }` }),
445
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
446
+ "button",
447
+ {
448
+ style: menuItemStyle,
449
+ onClick: () => {
450
+ setMenuOpen(false);
451
+ reset();
452
+ },
453
+ onMouseEnter: (e) => {
454
+ e.currentTarget.style.background = theme.bubbleBackground;
455
+ },
456
+ onMouseLeave: (e) => {
457
+ e.currentTarget.style.background = "none";
458
+ },
459
+ children: [
460
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
461
+ "svg",
462
+ {
463
+ width: "14",
464
+ height: "14",
465
+ viewBox: "0 0 24 24",
466
+ fill: "none",
467
+ stroke: "currentColor",
468
+ strokeWidth: "2",
469
+ strokeLinecap: "round",
470
+ strokeLinejoin: "round",
471
+ children: [
472
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M12 20h9" }),
473
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" })
474
+ ]
475
+ }
476
+ ),
477
+ t("new_conversation")
478
+ ]
479
+ }
480
+ )
481
+ ] })
482
+ ] }),
483
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
484
+ "button",
485
+ {
486
+ onClick: close,
487
+ style: headerButtonStyle,
488
+ "aria-label": t("close_chat"),
489
+ onMouseEnter: (e) => {
490
+ e.currentTarget.style.opacity = "1";
491
+ },
492
+ onMouseLeave: (e) => {
493
+ e.currentTarget.style.opacity = "0.7";
494
+ },
495
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
496
+ "svg",
497
+ {
498
+ width: "20",
499
+ height: "20",
500
+ viewBox: "0 0 24 24",
501
+ fill: "none",
502
+ stroke: "currentColor",
503
+ strokeWidth: "2",
504
+ children: [
505
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
506
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
507
+ ]
508
+ }
509
+ )
510
+ }
511
+ )
512
+ ] });
513
+ }
514
+
515
+ // src/components/chat-messages.tsx
516
+ var import_react8 = require("react");
517
+
518
+ // src/markdown/render.tsx
519
+ var import_jsx_runtime4 = require("react/jsx-runtime");
520
+ function parseBlocks(source) {
521
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
522
+ const blocks = [];
523
+ let i = 0;
524
+ while (i < lines.length) {
525
+ const line = lines[i];
526
+ if (line.trim() === "") {
527
+ i++;
528
+ continue;
529
+ }
530
+ const fenceMatch = line.match(/^```(.*)$/);
531
+ if (fenceMatch) {
532
+ const lang = fenceMatch[1].trim() || void 0;
533
+ const body2 = [];
534
+ i++;
535
+ while (i < lines.length && !/^```\s*$/.test(lines[i])) {
536
+ body2.push(lines[i]);
537
+ i++;
538
+ }
539
+ if (i < lines.length) i++;
540
+ blocks.push({ kind: "code", lines: body2, lang });
541
+ continue;
542
+ }
543
+ const headingMatch = line.match(/^(#{1,6})\s+(.*?)\s*#*\s*$/);
544
+ if (headingMatch) {
545
+ blocks.push({
546
+ kind: "heading",
547
+ level: headingMatch[1].length,
548
+ lines: [headingMatch[2]]
549
+ });
550
+ i++;
551
+ continue;
552
+ }
553
+ if (/^(\s*)[-*+]\s+/.test(line)) {
554
+ const body2 = [];
555
+ while (i < lines.length && /^(\s*)[-*+]\s+/.test(lines[i])) {
556
+ body2.push(lines[i].replace(/^(\s*)[-*+]\s+/, ""));
557
+ i++;
558
+ }
559
+ blocks.push({ kind: "ul", lines: body2 });
560
+ continue;
561
+ }
562
+ if (/^(\s*)\d+\.\s+/.test(line)) {
563
+ const body2 = [];
564
+ while (i < lines.length && /^(\s*)\d+\.\s+/.test(lines[i])) {
565
+ body2.push(lines[i].replace(/^(\s*)\d+\.\s+/, ""));
566
+ i++;
567
+ }
568
+ blocks.push({ kind: "ol", lines: body2 });
569
+ continue;
570
+ }
571
+ if (/^>\s?/.test(line)) {
572
+ const body2 = [];
573
+ while (i < lines.length && /^>\s?/.test(lines[i])) {
574
+ body2.push(lines[i].replace(/^>\s?/, ""));
575
+ i++;
576
+ }
577
+ blocks.push({ kind: "blockquote", lines: body2 });
578
+ continue;
579
+ }
580
+ const body = [line];
581
+ i++;
582
+ while (i < lines.length) {
583
+ const next = lines[i];
584
+ if (next.trim() === "") break;
585
+ if (/^```/.test(next) || /^#{1,6}\s/.test(next) || /^(\s*)[-*+]\s+/.test(next) || /^(\s*)\d+\.\s+/.test(next) || /^>\s?/.test(next)) {
586
+ break;
587
+ }
588
+ body.push(next);
589
+ i++;
590
+ }
591
+ blocks.push({ kind: "paragraph", lines: body });
592
+ }
593
+ return blocks;
594
+ }
595
+ function findClosing(text, from, end) {
596
+ let i = from;
597
+ while (i <= text.length - end.length) {
598
+ const c = text[i];
599
+ if (c === "\\") {
600
+ i += 2;
601
+ continue;
602
+ }
603
+ if (text.startsWith(end, i)) return i;
604
+ i++;
605
+ }
606
+ return -1;
607
+ }
608
+ function renderInlineText(text, ctx) {
609
+ const out = [];
610
+ let buffer = "";
611
+ let i = 0;
612
+ const flushBuffer = () => {
613
+ if (buffer.length > 0) {
614
+ out.push(buffer);
615
+ buffer = "";
616
+ }
617
+ };
618
+ while (i < text.length) {
619
+ const c = text[i];
620
+ if (c === "\\" && i + 1 < text.length) {
621
+ buffer += text[i + 1];
622
+ i += 2;
623
+ continue;
624
+ }
625
+ if (c === " " && text[i + 1] === " " && text[i + 2] === "\n") {
626
+ flushBuffer();
627
+ out.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("br", {}, ctx.nextKey()));
628
+ i += 3;
629
+ continue;
630
+ }
631
+ if (c === "\n") {
632
+ buffer += " ";
633
+ i++;
634
+ continue;
635
+ }
636
+ if (c === "`") {
637
+ const close = findClosing(text, i + 1, "`");
638
+ if (close !== -1) {
639
+ flushBuffer();
640
+ out.push(
641
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
642
+ "code",
643
+ {
644
+ style: {
645
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
646
+ fontSize: "0.92em",
647
+ background: "rgba(0,0,0,0.06)",
648
+ padding: "1px 4px",
649
+ borderRadius: 3
650
+ },
651
+ children: text.slice(i + 1, close)
652
+ },
653
+ ctx.nextKey()
654
+ )
655
+ );
656
+ i = close + 1;
657
+ continue;
658
+ }
659
+ }
660
+ if ((text.startsWith("**", i) || text.startsWith("__", i)) && i + 2 < text.length) {
661
+ const marker = text.slice(i, i + 2);
662
+ const close = findClosing(text, i + 2, marker);
663
+ if (close !== -1 && close > i + 2) {
664
+ flushBuffer();
665
+ const inner = renderInlineText(text.slice(i + 2, close), ctx);
666
+ out.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { children: inner }, ctx.nextKey()));
667
+ i = close + 2;
668
+ continue;
669
+ }
670
+ }
671
+ if (c === "*" || c === "_") {
672
+ const prev = i === 0 ? " " : text[i - 1];
673
+ const isWordBoundaryBefore = c === "*" || !/\w/.test(prev);
674
+ if (isWordBoundaryBefore) {
675
+ const close = findClosing(text, i + 1, c);
676
+ const prevOfClose = close === -1 ? " " : close + 1 < text.length ? text[close + 1] : " ";
677
+ const isWordBoundaryAfter = c === "*" || !/\w/.test(prevOfClose);
678
+ if (close !== -1 && close > i + 1 && text[close + 1] !== c && isWordBoundaryAfter) {
679
+ flushBuffer();
680
+ const inner = renderInlineText(text.slice(i + 1, close), ctx);
681
+ out.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("em", { children: inner }, ctx.nextKey()));
682
+ i = close + 1;
683
+ continue;
684
+ }
685
+ }
686
+ }
687
+ if (c === "[") {
688
+ const closeLabel = findClosing(text, i + 1, "]");
689
+ if (closeLabel !== -1 && text[closeLabel + 1] === "(") {
690
+ const closeUrl = findClosing(text, closeLabel + 2, ")");
691
+ if (closeUrl !== -1) {
692
+ const label = text.slice(i + 1, closeLabel);
693
+ const url = text.slice(closeLabel + 2, closeUrl).trim();
694
+ if (isSafeUrl(url)) {
695
+ flushBuffer();
696
+ out.push(
697
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
698
+ "a",
699
+ {
700
+ href: url,
701
+ target: "_blank",
702
+ rel: "noopener noreferrer",
703
+ style: { color: ctx.linkColor, textDecoration: "underline" },
704
+ children: renderInlineText(label, ctx)
705
+ },
706
+ ctx.nextKey()
707
+ )
708
+ );
709
+ i = closeUrl + 1;
710
+ continue;
711
+ }
712
+ }
713
+ }
714
+ if (closeLabel !== -1 && ctx.sources && ctx.sources.length > 0) {
715
+ const body = text.slice(i + 1, closeLabel).trim();
716
+ const parts = body.split(/\s*,\s*/).filter((s) => s.length > 0);
717
+ const indices = parts.map((p) => Number(p));
718
+ const allValid = parts.length > 0 && indices.every(
719
+ (n) => Number.isInteger(n) && n >= 1 && n <= (ctx.sources?.length ?? 0)
720
+ );
721
+ if (allValid) {
722
+ flushBuffer();
723
+ out.push(
724
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("sup", { style: { whiteSpace: "nowrap" }, children: indices.map((n, idx) => {
725
+ const src = ctx.sources[n - 1];
726
+ const content = /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
727
+ "span",
728
+ {
729
+ style: {
730
+ fontSize: "0.75em",
731
+ color: ctx.linkColor,
732
+ cursor: src.url ? "pointer" : "default"
733
+ },
734
+ title: src.heading ? `${src.title} \u2014 ${src.heading}` : src.title,
735
+ children: [
736
+ "[",
737
+ n,
738
+ "]"
739
+ ]
740
+ }
741
+ );
742
+ const separator = idx > 0 ? " " : null;
743
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
744
+ separator,
745
+ src.url ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
746
+ "a",
747
+ {
748
+ href: src.url,
749
+ target: "_blank",
750
+ rel: "noopener noreferrer",
751
+ style: {
752
+ color: ctx.linkColor,
753
+ textDecoration: "none"
754
+ },
755
+ children: content
756
+ }
757
+ ) : content
758
+ ] }, `${ctx.keyRoot}-cit-${idx}`);
759
+ }) }, ctx.nextKey())
760
+ );
761
+ i = closeLabel + 1;
762
+ continue;
763
+ }
764
+ }
765
+ }
766
+ buffer += c;
767
+ i++;
768
+ }
769
+ flushBuffer();
770
+ return out;
771
+ }
772
+ function isSafeUrl(url) {
773
+ if (/^https?:\/\//i.test(url)) return true;
774
+ if (/^mailto:/i.test(url)) return true;
775
+ if (/^tel:/i.test(url)) return true;
776
+ if (/^\/[^/]/.test(url) || /^#/.test(url)) return true;
777
+ return false;
778
+ }
779
+ function renderMarkdown(source, opts = {}) {
780
+ const blocks = parseBlocks(source);
781
+ const linkColor = opts.linkColor ?? "#6C3CE1";
782
+ let keyCounter = 0;
783
+ const ctx = {
784
+ sources: opts.sources,
785
+ linkColor,
786
+ keyRoot: "md",
787
+ nextKey: () => `md-${keyCounter++}`
788
+ };
789
+ const renderInline = (line) => renderInlineText(line, ctx);
790
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: blocks.map((block, idx) => {
791
+ const key = `b-${idx}`;
792
+ switch (block.kind) {
793
+ case "heading": {
794
+ const level = block.level ?? 1;
795
+ const style = {
796
+ margin: "8px 0 4px",
797
+ fontWeight: 600,
798
+ fontSize: level <= 2 ? "1.15em" : level === 3 ? "1.05em" : "1em"
799
+ };
800
+ const children = renderInline(block.lines[0] ?? "");
801
+ if (level === 1)
802
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h1", { style, children }, key);
803
+ if (level === 2)
804
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h2", { style, children }, key);
805
+ if (level === 3)
806
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { style, children }, key);
807
+ if (level === 4)
808
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h4", { style, children }, key);
809
+ if (level === 5)
810
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h5", { style, children }, key);
811
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h6", { style, children }, key);
812
+ }
813
+ case "paragraph":
814
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: { margin: "0 0 8px", lineHeight: 1.5 }, children: renderInline(block.lines.join("\n")) }, key);
815
+ case "ul":
816
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
817
+ "ul",
818
+ {
819
+ style: {
820
+ margin: "0 0 8px",
821
+ paddingLeft: 20,
822
+ lineHeight: 1.5,
823
+ listStyleType: "disc",
824
+ listStylePosition: "outside"
825
+ },
826
+ children: block.lines.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { style: { display: "list-item" }, children: renderInline(l) }, i))
827
+ },
828
+ key
829
+ );
830
+ case "ol":
831
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
832
+ "ol",
833
+ {
834
+ style: {
835
+ margin: "0 0 8px",
836
+ paddingLeft: 22,
837
+ lineHeight: 1.5,
838
+ listStyleType: "decimal",
839
+ listStylePosition: "outside"
840
+ },
841
+ children: block.lines.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { style: { display: "list-item" }, children: renderInline(l) }, i))
842
+ },
843
+ key
844
+ );
845
+ case "code":
846
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
847
+ "pre",
848
+ {
849
+ style: {
850
+ margin: "0 0 8px",
851
+ padding: "10px 12px",
852
+ background: "rgba(0,0,0,0.06)",
853
+ borderRadius: 6,
854
+ overflowX: "auto",
855
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
856
+ fontSize: "0.88em",
857
+ lineHeight: 1.45
858
+ },
859
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("code", { children: block.lines.join("\n") })
860
+ },
861
+ key
862
+ );
863
+ case "blockquote":
864
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
865
+ "blockquote",
866
+ {
867
+ style: {
868
+ margin: "0 0 8px",
869
+ padding: "4px 0 4px 10px",
870
+ borderLeft: "3px solid rgba(0,0,0,0.15)",
871
+ color: "rgba(0,0,0,0.75)",
872
+ fontStyle: "italic"
873
+ },
874
+ children: renderInline(block.lines.join("\n"))
875
+ },
876
+ key
877
+ );
878
+ }
879
+ }) });
880
+ }
881
+
882
+ // src/components/action-confirmation-card.tsx
883
+ var import_react7 = require("react");
884
+ var import_jsx_runtime5 = require("react/jsx-runtime");
885
+ function ActionConfirmationCard({
886
+ block,
887
+ primaryColor,
888
+ t,
889
+ onApprove,
890
+ onCancel
891
+ }) {
892
+ const [chosen, setChosen] = (0, import_react7.useState)(null);
893
+ const [showSpinner, setShowSpinner] = (0, import_react7.useState)(false);
894
+ const [error, setError] = (0, import_react7.useState)(null);
895
+ const [showSummary, setShowSummary] = (0, import_react7.useState)(false);
896
+ const handle = async (choice) => {
897
+ if (chosen) return;
898
+ setChosen(choice);
899
+ setError(null);
900
+ const spinnerTimer = window.setTimeout(() => setShowSpinner(true), 800);
901
+ try {
902
+ const fn = choice === "approve" ? onApprove : onCancel;
903
+ await fn(block.pendingToolCallId);
904
+ } catch (e) {
905
+ const msg = e instanceof Error ? e.message : t("action_failed");
906
+ setError(msg);
907
+ setChosen(null);
908
+ } finally {
909
+ window.clearTimeout(spinnerTimer);
910
+ setShowSpinner(false);
911
+ }
912
+ };
913
+ const cardStyle = {
914
+ marginTop: 6,
915
+ padding: 12,
916
+ borderRadius: 12,
917
+ border: "1px solid #e0e0e0",
918
+ background: "#fff",
919
+ display: "flex",
920
+ flexDirection: "column",
921
+ gap: 8,
922
+ fontSize: 13,
923
+ lineHeight: 1.4
924
+ };
925
+ const titleStyle = {
926
+ fontWeight: 600,
927
+ color: "#222"
928
+ };
929
+ const disclosureBtn = {
930
+ background: "none",
931
+ border: "none",
932
+ padding: 0,
933
+ color: primaryColor,
934
+ fontSize: 12,
935
+ cursor: "pointer",
936
+ textAlign: "left",
937
+ fontFamily: "inherit"
938
+ };
939
+ const summaryStyle = {
940
+ color: "#555",
941
+ background: "#fafafa",
942
+ padding: 8,
943
+ borderRadius: 8,
944
+ whiteSpace: "pre-wrap"
945
+ };
946
+ const buttonRow = {
947
+ display: "flex",
948
+ gap: 8,
949
+ marginTop: 4
950
+ };
951
+ const baseBtn = (variant) => ({
952
+ flex: 1,
953
+ padding: "8px 12px",
954
+ borderRadius: 8,
955
+ fontSize: 13,
956
+ fontWeight: 500,
957
+ cursor: chosen ? "default" : "pointer",
958
+ border: variant === "primary" ? `1px solid ${primaryColor}` : "1px solid #ddd",
959
+ background: variant === "primary" ? primaryColor : "#fff",
960
+ color: variant === "primary" ? "#fff" : "#333",
961
+ opacity: chosen && chosen !== (variant === "primary" ? "approve" : "cancel") ? 0.5 : 1,
962
+ transition: "opacity 0.15s",
963
+ display: "flex",
964
+ alignItems: "center",
965
+ justifyContent: "center",
966
+ gap: 6,
967
+ fontFamily: "inherit"
968
+ });
969
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: cardStyle, role: "group", "aria-label": block.title, children: [
970
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: titleStyle, children: block.title }),
971
+ block.summary && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
972
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
973
+ "button",
974
+ {
975
+ type: "button",
976
+ style: disclosureBtn,
977
+ onClick: () => setShowSummary((s) => !s),
978
+ "aria-expanded": showSummary,
979
+ children: [
980
+ showSummary ? "\u25BE" : "\u25B8",
981
+ " ",
982
+ t("action_what_will_happen")
983
+ ]
984
+ }
985
+ ),
986
+ showSummary && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: summaryStyle, children: block.summary })
987
+ ] }),
988
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: buttonRow, children: [
989
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
990
+ "button",
991
+ {
992
+ type: "button",
993
+ style: baseBtn("ghost"),
994
+ disabled: chosen !== null,
995
+ onClick: () => handle("cancel"),
996
+ children: chosen === "cancel" && showSpinner ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Spinner, { color: "#333" }) : t("action_cancel")
997
+ }
998
+ ),
999
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1000
+ "button",
1001
+ {
1002
+ type: "button",
1003
+ style: baseBtn("primary"),
1004
+ disabled: chosen !== null,
1005
+ onClick: () => handle("approve"),
1006
+ children: chosen === "approve" && showSpinner ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Spinner, { color: "#fff" }) : t("action_approve")
1007
+ }
1008
+ )
1009
+ ] }),
1010
+ error && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { role: "alert", style: { color: "#b91c1c", fontSize: 12 }, children: error })
1011
+ ] });
1012
+ }
1013
+ function Spinner({ color }) {
1014
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1015
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("style", { children: `@keyframes ch-spin { to { transform: rotate(360deg); } }` }),
1016
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1017
+ "span",
1018
+ {
1019
+ "aria-hidden": "true",
1020
+ style: {
1021
+ width: 14,
1022
+ height: 14,
1023
+ borderRadius: "50%",
1024
+ border: `2px solid ${color}`,
1025
+ borderTopColor: "transparent",
1026
+ animation: "ch-spin 0.8s linear infinite",
1027
+ display: "inline-block"
1028
+ }
1029
+ }
1030
+ )
1031
+ ] });
1032
+ }
1033
+
1034
+ // src/components/chat-messages.tsx
1035
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1036
+ function MessageStatusPill({
1037
+ status,
1038
+ t
1039
+ }) {
1040
+ const failed = status === "failed";
1041
+ const containerStyle = {
1042
+ display: "flex",
1043
+ alignItems: "center",
1044
+ justifyContent: "flex-end",
1045
+ gap: 4,
1046
+ marginTop: 2,
1047
+ fontSize: 11,
1048
+ color: failed ? "#b91c1c" : "#888"
1049
+ };
1050
+ const labelKey = status === "sending" ? "status_sending" : status === "sent" ? "status_sent" : "status_failed";
1051
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: containerStyle, "aria-live": "polite", children: [
1052
+ status === "sending" && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ClockIcon, {}),
1053
+ status === "sent" && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CheckIcon, {}),
1054
+ status === "failed" && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ExclamationIcon, {}),
1055
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: t(labelKey) })
1056
+ ] });
1057
+ }
1058
+ function ClockIcon() {
1059
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1060
+ "svg",
1061
+ {
1062
+ width: "11",
1063
+ height: "11",
1064
+ viewBox: "0 0 24 24",
1065
+ fill: "none",
1066
+ stroke: "currentColor",
1067
+ strokeWidth: "2",
1068
+ strokeLinecap: "round",
1069
+ strokeLinejoin: "round",
1070
+ "aria-hidden": "true",
1071
+ children: [
1072
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
1073
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("polyline", { points: "12 6 12 12 16 14" })
1074
+ ]
1075
+ }
1076
+ );
1077
+ }
1078
+ function CheckIcon() {
1079
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1080
+ "svg",
1081
+ {
1082
+ width: "11",
1083
+ height: "11",
1084
+ viewBox: "0 0 24 24",
1085
+ fill: "none",
1086
+ stroke: "currentColor",
1087
+ strokeWidth: "2.5",
1088
+ strokeLinecap: "round",
1089
+ strokeLinejoin: "round",
1090
+ "aria-hidden": "true",
1091
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("polyline", { points: "20 6 9 17 4 12" })
1092
+ }
1093
+ );
1094
+ }
1095
+ function ExclamationIcon() {
1096
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1097
+ "svg",
1098
+ {
1099
+ width: "11",
1100
+ height: "11",
1101
+ viewBox: "0 0 24 24",
1102
+ fill: "none",
1103
+ stroke: "currentColor",
1104
+ strokeWidth: "2",
1105
+ strokeLinecap: "round",
1106
+ strokeLinejoin: "round",
1107
+ "aria-hidden": "true",
1108
+ children: [
1109
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
1110
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
1111
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
1112
+ ]
1113
+ }
1114
+ );
1115
+ }
1116
+ function MessageRatingButtons({
1117
+ messageId,
1118
+ onRate,
1119
+ primaryColor,
1120
+ t,
1121
+ reduced
1122
+ }) {
1123
+ const [rated, setRated] = (0, import_react8.useState)(null);
1124
+ const handleRate = (rating) => {
1125
+ setRated(rating);
1126
+ onRate(messageId, rating);
1127
+ };
1128
+ const buttonStyle = (isRated) => ({
1129
+ background: isRated ? `${primaryColor}11` : "none",
1130
+ border: `1px solid ${isRated ? primaryColor : "#ddd"}`,
1131
+ borderRadius: 4,
1132
+ padding: "4px 6px",
1133
+ cursor: rated ? "default" : "pointer",
1134
+ color: isRated ? primaryColor : "#888",
1135
+ display: "flex",
1136
+ alignItems: "center",
1137
+ justifyContent: "center",
1138
+ transition: reduced ? "color 0.15s" : "all 0.15s",
1139
+ transform: isRated && !reduced ? "scale(1.15)" : "scale(1)"
1140
+ });
1141
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", gap: 4, marginTop: 4 }, children: [
1142
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1143
+ "button",
1144
+ {
1145
+ onClick: () => handleRate("positive"),
1146
+ disabled: rated !== null,
1147
+ style: buttonStyle(rated === "positive"),
1148
+ title: t("helpful"),
1149
+ "aria-label": t("helpful"),
1150
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1151
+ "svg",
1152
+ {
1153
+ width: "14",
1154
+ height: "14",
1155
+ viewBox: "0 0 24 24",
1156
+ fill: "none",
1157
+ stroke: "currentColor",
1158
+ strokeWidth: "2",
1159
+ strokeLinecap: "round",
1160
+ strokeLinejoin: "round",
1161
+ children: [
1162
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M7 10v12" }),
1163
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2h0a3.13 3.13 0 0 1 3 3.88Z" })
1164
+ ]
1165
+ }
1166
+ )
1167
+ }
1168
+ ),
1169
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1170
+ "button",
1171
+ {
1172
+ onClick: () => handleRate("negative"),
1173
+ disabled: rated !== null,
1174
+ style: buttonStyle(rated === "negative"),
1175
+ title: t("not_helpful"),
1176
+ "aria-label": t("not_helpful"),
1177
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1178
+ "svg",
1179
+ {
1180
+ width: "14",
1181
+ height: "14",
1182
+ viewBox: "0 0 24 24",
1183
+ fill: "none",
1184
+ stroke: "currentColor",
1185
+ strokeWidth: "2",
1186
+ strokeLinecap: "round",
1187
+ strokeLinejoin: "round",
1188
+ children: [
1189
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M17 14V2" }),
1190
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22h0a3.13 3.13 0 0 1-3-3.88Z" })
1191
+ ]
1192
+ }
1193
+ )
1194
+ }
1195
+ )
1196
+ ] });
1197
+ }
1198
+ function AnimatedMessage({
1199
+ children,
1200
+ isUser,
1201
+ animate,
1202
+ reduced
1203
+ }) {
1204
+ const [visible, setVisible] = (0, import_react8.useState)(!animate);
1205
+ (0, import_react8.useEffect)(() => {
1206
+ if (!animate || reduced) {
1207
+ setVisible(true);
1208
+ return;
1209
+ }
1210
+ const id = requestAnimationFrame(() => setVisible(true));
1211
+ return () => cancelAnimationFrame(id);
1212
+ }, [animate, reduced]);
1213
+ const style = {
1214
+ alignSelf: isUser ? "flex-end" : "flex-start",
1215
+ maxWidth: "80%",
1216
+ opacity: visible ? 1 : 0,
1217
+ transform: visible ? "translateX(0)" : `translateX(${isUser ? "12px" : "-12px"})`,
1218
+ transition: animate && !reduced ? "opacity 0.2s ease, transform 0.2s ease" : "none"
1219
+ };
1220
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style, children });
1221
+ }
1222
+ function ChipRow({
1223
+ options,
1224
+ onSelect,
1225
+ primaryColor,
1226
+ reduced
1227
+ }) {
1228
+ const chip = {
1229
+ background: "none",
1230
+ border: "1px solid #e0e0e0",
1231
+ borderRadius: 20,
1232
+ padding: "6px 12px",
1233
+ fontSize: 13,
1234
+ color: "#333",
1235
+ cursor: "pointer",
1236
+ textAlign: "left",
1237
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
1238
+ transition: reduced ? "none" : "border-color 0.15s, background 0.15s"
1239
+ };
1240
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1241
+ "div",
1242
+ {
1243
+ style: {
1244
+ display: "flex",
1245
+ flexWrap: "wrap",
1246
+ gap: 6,
1247
+ marginTop: 6
1248
+ },
1249
+ children: options.map((text) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1250
+ "button",
1251
+ {
1252
+ style: chip,
1253
+ onClick: () => onSelect(text),
1254
+ onMouseEnter: (e) => {
1255
+ e.currentTarget.style.borderColor = primaryColor;
1256
+ e.currentTarget.style.background = `${primaryColor}08`;
1257
+ },
1258
+ onMouseLeave: (e) => {
1259
+ e.currentTarget.style.borderColor = "#e0e0e0";
1260
+ e.currentTarget.style.background = "none";
1261
+ },
1262
+ children: text
1263
+ },
1264
+ text
1265
+ ))
1266
+ }
1267
+ );
1268
+ }
1269
+ function BlockRenderer({
1270
+ block,
1271
+ onSend,
1272
+ onApproveAction,
1273
+ onCancelAction,
1274
+ primaryColor,
1275
+ reduced,
1276
+ t
1277
+ }) {
1278
+ switch (block.type) {
1279
+ case "quick_replies":
1280
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1281
+ ChipRow,
1282
+ {
1283
+ options: block.options,
1284
+ onSelect: onSend,
1285
+ primaryColor,
1286
+ reduced
1287
+ }
1288
+ );
1289
+ case "action_confirmation":
1290
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1291
+ ActionConfirmationCard,
1292
+ {
1293
+ block,
1294
+ primaryColor,
1295
+ t,
1296
+ onApprove: onApproveAction,
1297
+ onCancel: onCancelAction
1298
+ }
1299
+ );
1300
+ }
1301
+ }
1302
+ function StreamingCursor({ reduced }) {
1303
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1304
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("style", { children: `@keyframes ch-caret-blink {
1305
+ 0%, 50% { opacity: 1; }
1306
+ 50.01%, 100% { opacity: 0; }
1307
+ }` }),
1308
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1309
+ "span",
1310
+ {
1311
+ "aria-hidden": "true",
1312
+ style: {
1313
+ display: "inline-block",
1314
+ width: 2,
1315
+ height: "1em",
1316
+ marginLeft: 2,
1317
+ verticalAlign: "text-bottom",
1318
+ background: "#777",
1319
+ animation: reduced ? "none" : "ch-caret-blink 1s step-start infinite"
1320
+ }
1321
+ }
1322
+ )
1323
+ ] });
1324
+ }
1325
+ function formatBytes(n) {
1326
+ if (n == null || n <= 0) return null;
1327
+ if (n < 1024) return `${n} B`;
1328
+ if (n < 1024 * 1024) return `${Math.round(n / 1024)} KB`;
1329
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
1330
+ }
1331
+ function AttachmentList({
1332
+ attachments,
1333
+ isUser,
1334
+ primaryColor,
1335
+ t
1336
+ }) {
1337
+ const containerStyle = {
1338
+ display: "flex",
1339
+ flexDirection: "column",
1340
+ gap: 6,
1341
+ marginTop: 6,
1342
+ alignItems: isUser ? "flex-end" : "flex-start"
1343
+ };
1344
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: containerStyle, children: attachments.map((a) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1345
+ AttachmentTile,
1346
+ {
1347
+ attachment: a,
1348
+ isUser,
1349
+ primaryColor,
1350
+ t
1351
+ },
1352
+ a.id
1353
+ )) });
1354
+ }
1355
+ function AttachmentTile({
1356
+ attachment,
1357
+ isUser,
1358
+ primaryColor,
1359
+ t
1360
+ }) {
1361
+ const tileBg = isUser ? `${primaryColor}11` : "#f3f4f6";
1362
+ const tileBorder = isUser ? `${primaryColor}33` : "#e5e7eb";
1363
+ if (attachment.kind === "image" && attachment.url) {
1364
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1365
+ "a",
1366
+ {
1367
+ href: attachment.url,
1368
+ target: "_blank",
1369
+ rel: "noopener noreferrer",
1370
+ style: { display: "inline-block", maxWidth: 220, lineHeight: 0 },
1371
+ "aria-label": attachment.filename ?? t("attachment_image_alt"),
1372
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1373
+ "img",
1374
+ {
1375
+ src: attachment.url,
1376
+ alt: attachment.filename ?? t("attachment_image_alt"),
1377
+ style: {
1378
+ display: "block",
1379
+ maxWidth: 220,
1380
+ maxHeight: 220,
1381
+ width: "auto",
1382
+ height: "auto",
1383
+ borderRadius: 12,
1384
+ border: `1px solid ${tileBorder}`,
1385
+ objectFit: "cover"
1386
+ }
1387
+ }
1388
+ )
1389
+ }
1390
+ );
1391
+ }
1392
+ if (attachment.kind === "location" && attachment.location) {
1393
+ const { latitude, longitude, label } = attachment.location;
1394
+ const href = `https://www.openstreetmap.org/?mlat=${latitude}&mlon=${longitude}#map=15/${latitude}/${longitude}`;
1395
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1396
+ "a",
1397
+ {
1398
+ href,
1399
+ target: "_blank",
1400
+ rel: "noopener noreferrer",
1401
+ style: tileLinkStyle(tileBg, tileBorder),
1402
+ children: [
1403
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PinIcon, {}),
1404
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", flexDirection: "column", minWidth: 0 }, children: [
1405
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: tileTitleStyle(), children: label || t("attachment_location") }),
1406
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { style: tileSubStyle(), children: [
1407
+ latitude.toFixed(4),
1408
+ ", ",
1409
+ longitude.toFixed(4)
1410
+ ] })
1411
+ ] })
1412
+ ]
1413
+ }
1414
+ );
1415
+ }
1416
+ if (!attachment.url) return null;
1417
+ const sub = [attachment.mimeType, formatBytes(attachment.sizeBytes)].filter(Boolean).join(" \xB7 ");
1418
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1419
+ "a",
1420
+ {
1421
+ href: attachment.url,
1422
+ target: "_blank",
1423
+ rel: "noopener noreferrer",
1424
+ title: t("attachment_open"),
1425
+ style: tileLinkStyle(tileBg, tileBorder),
1426
+ children: [
1427
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FileIcon, { kind: attachment.kind }),
1428
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", flexDirection: "column", minWidth: 0 }, children: [
1429
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: tileTitleStyle(), children: attachment.filename ?? t("attachment_open") }),
1430
+ sub && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: tileSubStyle(), children: sub })
1431
+ ] })
1432
+ ]
1433
+ }
1434
+ );
1435
+ }
1436
+ function tileLinkStyle(bg, border) {
1437
+ return {
1438
+ display: "flex",
1439
+ alignItems: "center",
1440
+ gap: 10,
1441
+ padding: "8px 12px",
1442
+ background: bg,
1443
+ border: `1px solid ${border}`,
1444
+ borderRadius: 12,
1445
+ textDecoration: "none",
1446
+ color: "inherit",
1447
+ maxWidth: 260
1448
+ };
1449
+ }
1450
+ function tileTitleStyle() {
1451
+ return {
1452
+ fontSize: 13,
1453
+ fontWeight: 500,
1454
+ color: "#1f2937",
1455
+ overflow: "hidden",
1456
+ textOverflow: "ellipsis",
1457
+ whiteSpace: "nowrap",
1458
+ maxWidth: 200
1459
+ };
1460
+ }
1461
+ function tileSubStyle() {
1462
+ return {
1463
+ fontSize: 11,
1464
+ color: "#6b7280",
1465
+ overflow: "hidden",
1466
+ textOverflow: "ellipsis",
1467
+ whiteSpace: "nowrap",
1468
+ maxWidth: 200
1469
+ };
1470
+ }
1471
+ function FileIcon({ kind }) {
1472
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1473
+ "svg",
1474
+ {
1475
+ width: "20",
1476
+ height: "20",
1477
+ viewBox: "0 0 24 24",
1478
+ fill: "none",
1479
+ stroke: "#4b5563",
1480
+ strokeWidth: "2",
1481
+ strokeLinecap: "round",
1482
+ strokeLinejoin: "round",
1483
+ "aria-hidden": "true",
1484
+ style: { flexShrink: 0 },
1485
+ children: kind === "audio" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1486
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M9 18V5l12-2v13" }),
1487
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("circle", { cx: "6", cy: "18", r: "3" }),
1488
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("circle", { cx: "18", cy: "16", r: "3" })
1489
+ ] }) : kind === "video" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1490
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("rect", { x: "2", y: "6", width: "14", height: "12", rx: "2" }),
1491
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "m22 8-6 4 6 4z" })
1492
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1493
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
1494
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("polyline", { points: "14 2 14 8 20 8" })
1495
+ ] })
1496
+ }
1497
+ );
1498
+ }
1499
+ function PinIcon() {
1500
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1501
+ "svg",
1502
+ {
1503
+ width: "20",
1504
+ height: "20",
1505
+ viewBox: "0 0 24 24",
1506
+ fill: "none",
1507
+ stroke: "#4b5563",
1508
+ strokeWidth: "2",
1509
+ strokeLinecap: "round",
1510
+ strokeLinejoin: "round",
1511
+ "aria-hidden": "true",
1512
+ style: { flexShrink: 0 },
1513
+ children: [
1514
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M20 10c0 7-8 13-8 13s-8-6-8-13a8 8 0 0 1 16 0Z" }),
1515
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("circle", { cx: "12", cy: "10", r: "3" })
1516
+ ]
1517
+ }
1518
+ );
1519
+ }
1520
+ function Message({
1521
+ message,
1522
+ config,
1523
+ onRate,
1524
+ onSend,
1525
+ onApproveAction,
1526
+ onCancelAction,
1527
+ hasConversation,
1528
+ t,
1529
+ animate,
1530
+ reduced,
1531
+ showSuggestions
1532
+ }) {
1533
+ const isUser = message.role === "user";
1534
+ const bubbleStyle = {
1535
+ padding: "10px 14px",
1536
+ borderRadius: 16,
1537
+ fontSize: 14,
1538
+ lineHeight: 1.5,
1539
+ wordBreak: "break-word",
1540
+ ...isUser ? {
1541
+ background: config.primaryColor,
1542
+ color: "white",
1543
+ borderBottomRightRadius: 4
1544
+ } : {
1545
+ background: config.bubbleBackground,
1546
+ color: config.textColor,
1547
+ borderBottomLeftRadius: 4
1548
+ }
1549
+ };
1550
+ const linkColor = isUser ? "#ffffff" : config.primaryColor;
1551
+ const hasContent = message.content.trim().length > 0;
1552
+ const hasAttachments = !!message.attachments?.length;
1553
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(AnimatedMessage, { isUser, animate, reduced, children: [
1554
+ hasContent && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1555
+ "div",
1556
+ {
1557
+ style: bubbleStyle,
1558
+ "data-streaming-bubble": !isUser && message.streaming ? "true" : void 0,
1559
+ children: isUser ? message.content : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1560
+ renderMarkdown(message.content, {
1561
+ sources: message.sources,
1562
+ linkColor
1563
+ }),
1564
+ message.streaming && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StreamingCursor, { reduced })
1565
+ ] })
1566
+ }
1567
+ ),
1568
+ hasAttachments && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1569
+ AttachmentList,
1570
+ {
1571
+ attachments: message.attachments,
1572
+ isUser,
1573
+ primaryColor: config.primaryColor,
1574
+ t
1575
+ }
1576
+ ),
1577
+ isUser && message.status && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MessageStatusPill, { status: message.status, t }),
1578
+ !isUser && message.blocks?.map((block, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1579
+ BlockRenderer,
1580
+ {
1581
+ block,
1582
+ onSend,
1583
+ onApproveAction,
1584
+ onCancelAction,
1585
+ primaryColor: config.primaryColor,
1586
+ reduced,
1587
+ t
1588
+ },
1589
+ i
1590
+ )),
1591
+ !isUser && showSuggestions && message.suggestions?.length ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1592
+ ChipRow,
1593
+ {
1594
+ options: message.suggestions,
1595
+ onSelect: onSend,
1596
+ primaryColor: config.primaryColor,
1597
+ reduced
1598
+ }
1599
+ ) : null,
1600
+ message.role === "bot" && message.id && hasConversation && !message.streaming && !message.blocks?.some((b) => b.type === "action_confirmation") && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1601
+ MessageRatingButtons,
1602
+ {
1603
+ messageId: message.id,
1604
+ onRate,
1605
+ primaryColor: config.primaryColor,
1606
+ t,
1607
+ reduced
1608
+ }
1609
+ )
1610
+ ] });
1611
+ }
1612
+ function TypingDots({ reduced }) {
1613
+ const dotStyle = (delay) => ({
1614
+ width: 6,
1615
+ height: 6,
1616
+ borderRadius: "50%",
1617
+ background: "#999",
1618
+ animation: reduced ? "none" : `ch-dot-pulse 1.2s ease-in-out ${delay}s infinite`
1619
+ });
1620
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1621
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("style", { children: `@keyframes ch-dot-pulse {
1622
+ 0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
1623
+ 40% { opacity: 1; transform: scale(1); }
1624
+ }` }),
1625
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1626
+ "div",
1627
+ {
1628
+ style: {
1629
+ alignSelf: "flex-start",
1630
+ padding: "12px 16px",
1631
+ borderRadius: 16,
1632
+ borderBottomLeftRadius: 4,
1633
+ background: "#f0f0f0",
1634
+ display: "flex",
1635
+ gap: 4,
1636
+ alignItems: "center"
1637
+ },
1638
+ children: [
1639
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: dotStyle(0) }),
1640
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: dotStyle(0.2) }),
1641
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: dotStyle(0.4) })
1642
+ ]
1643
+ }
1644
+ )
1645
+ ] });
1646
+ }
1647
+ function ChatMessages() {
1648
+ const {
1649
+ messages,
1650
+ isLoading,
1651
+ error,
1652
+ conversationId,
1653
+ rateMessage,
1654
+ sendMessage,
1655
+ approveAction,
1656
+ cancelAction,
1657
+ t
1658
+ } = useChat();
1659
+ const theme = useEffectiveTheme();
1660
+ const themedConfig = {
1661
+ primaryColor: theme.primary,
1662
+ textColor: theme.text,
1663
+ bubbleBackground: theme.bubbleBackground
1664
+ };
1665
+ const reduced = useReducedMotion();
1666
+ const containerRef = (0, import_react8.useRef)(null);
1667
+ const messagesEndRef = (0, import_react8.useRef)(null);
1668
+ const isFirstRender = (0, import_react8.useRef)(true);
1669
+ const prevMessageCount = (0, import_react8.useRef)(0);
1670
+ const stickRef = (0, import_react8.useRef)(true);
1671
+ const suppressScrollRef = (0, import_react8.useRef)(false);
1672
+ const autoScrollTo = (top) => {
1673
+ const el = containerRef.current;
1674
+ if (!el) return;
1675
+ suppressScrollRef.current = true;
1676
+ el.scrollTop = top;
1677
+ requestAnimationFrame(() => {
1678
+ suppressScrollRef.current = false;
1679
+ });
1680
+ };
1681
+ const handleScroll = () => {
1682
+ if (suppressScrollRef.current) return;
1683
+ const el = containerRef.current;
1684
+ if (!el) return;
1685
+ const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
1686
+ stickRef.current = distFromBottom < 60;
1687
+ };
1688
+ (0, import_react8.useEffect)(() => {
1689
+ const el = containerRef.current;
1690
+ if (!el) return;
1691
+ const lastMsg2 = messages[messages.length - 1];
1692
+ if (messages.length > prevMessageCount.current && lastMsg2?.role === "user") {
1693
+ stickRef.current = true;
1694
+ }
1695
+ if (!stickRef.current && !isFirstRender.current) {
1696
+ return;
1697
+ }
1698
+ const streamingBubble = el.querySelector(
1699
+ "[data-streaming-bubble='true']"
1700
+ );
1701
+ let target = el.scrollHeight - el.clientHeight;
1702
+ if (streamingBubble && streamingBubble.offsetHeight > el.clientHeight - 24) {
1703
+ const containerTop = el.getBoundingClientRect().top;
1704
+ const bubbleTop = streamingBubble.getBoundingClientRect().top;
1705
+ const TOP_GAP = 16;
1706
+ target = el.scrollTop + (bubbleTop - containerTop) - TOP_GAP;
1707
+ stickRef.current = false;
1708
+ }
1709
+ autoScrollTo(target);
1710
+ isFirstRender.current = false;
1711
+ }, [messages, isLoading, reduced]);
1712
+ const newStartIndex = isFirstRender.current ? messages.length : prevMessageCount.current;
1713
+ (0, import_react8.useEffect)(() => {
1714
+ prevMessageCount.current = messages.length;
1715
+ }, [messages.length]);
1716
+ let lastBotIndex = -1;
1717
+ for (let i = messages.length - 1; i >= 0; i--) {
1718
+ if (messages[i].role === "bot") {
1719
+ lastBotIndex = i;
1720
+ break;
1721
+ }
1722
+ }
1723
+ const containerStyle = {
1724
+ flex: 1,
1725
+ overflowY: "auto",
1726
+ padding: 16,
1727
+ display: "flex",
1728
+ flexDirection: "column",
1729
+ gap: 12
1730
+ };
1731
+ const lastMsg = messages[messages.length - 1];
1732
+ const waitingForFirstToken = isLoading && (lastMsg?.role !== "bot" || lastMsg.streaming !== true);
1733
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { ref: containerRef, style: containerStyle, onScroll: handleScroll, children: [
1734
+ messages.map((msg, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1735
+ Message,
1736
+ {
1737
+ message: msg,
1738
+ config: themedConfig,
1739
+ onRate: rateMessage,
1740
+ onSend: sendMessage,
1741
+ onApproveAction: approveAction,
1742
+ onCancelAction: cancelAction,
1743
+ hasConversation: conversationId !== null,
1744
+ t,
1745
+ animate: i >= newStartIndex,
1746
+ reduced,
1747
+ showSuggestions: i === lastBotIndex && !isLoading && msg.streaming !== true
1748
+ },
1749
+ i
1750
+ )),
1751
+ waitingForFirstToken && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(TypingDots, { reduced }),
1752
+ error && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1753
+ "div",
1754
+ {
1755
+ role: "alert",
1756
+ style: {
1757
+ alignSelf: "flex-start",
1758
+ padding: "10px 14px",
1759
+ borderRadius: 16,
1760
+ borderBottomLeftRadius: 4,
1761
+ fontSize: 13,
1762
+ background: "#fee2e2",
1763
+ color: "#b91c1c"
1764
+ },
1765
+ children: error
1766
+ }
1767
+ ),
1768
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { ref: messagesEndRef })
1769
+ ] });
1770
+ }
1771
+
1772
+ // src/components/chat-suggestions.tsx
1773
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1774
+ function ChatSuggestions() {
1775
+ const { messages, isLoading, config, sendMessage } = useChat();
1776
+ const reduced = useReducedMotion();
1777
+ const theme = useEffectiveTheme();
1778
+ const hasUserMessage = messages.some((m) => m.role === "user");
1779
+ if (config.suggestedMessages.length === 0 || hasUserMessage || isLoading) {
1780
+ return null;
1781
+ }
1782
+ const isDark = theme.scheme === "dark";
1783
+ const idleBorder = isDark ? "rgba(255,255,255,0.18)" : "#e0e0e0";
1784
+ const containerStyle = {
1785
+ padding: "8px 16px",
1786
+ display: "flex",
1787
+ flexWrap: "wrap",
1788
+ gap: 6,
1789
+ justifyContent: "flex-end"
1790
+ };
1791
+ const chipStyle = {
1792
+ background: "none",
1793
+ border: `1px solid ${idleBorder}`,
1794
+ borderRadius: 20,
1795
+ padding: "7px 14px",
1796
+ fontSize: 13,
1797
+ color: theme.text,
1798
+ cursor: "pointer",
1799
+ textAlign: "left",
1800
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
1801
+ transition: reduced ? "none" : "border-color 0.15s, background 0.15s"
1802
+ };
1803
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: containerStyle, children: config.suggestedMessages.map((text) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1804
+ "button",
1805
+ {
1806
+ style: chipStyle,
1807
+ onClick: () => sendMessage(text),
1808
+ onMouseEnter: (e) => {
1809
+ e.currentTarget.style.borderColor = theme.primary;
1810
+ e.currentTarget.style.background = `${theme.primary}14`;
1811
+ },
1812
+ onMouseLeave: (e) => {
1813
+ e.currentTarget.style.borderColor = idleBorder;
1814
+ e.currentTarget.style.background = "none";
1815
+ },
1816
+ children: text
1817
+ },
1818
+ text
1819
+ )) });
1820
+ }
1821
+
1822
+ // src/components/chat-input.tsx
1823
+ var import_react9 = require("react");
1824
+ var import_js3 = require("@customerhero/js");
1825
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1826
+ var MAX_ATTACHMENTS = 3;
1827
+ var ALLOWED_MIME_TYPES = [
1828
+ "image/png",
1829
+ "image/jpeg",
1830
+ "image/webp",
1831
+ "application/pdf"
1832
+ ];
1833
+ var ACCEPT_ATTR = ALLOWED_MIME_TYPES.join(",");
1834
+ function isImageMime(mime) {
1835
+ return mime.startsWith("image/");
1836
+ }
1837
+ function ChatInput() {
1838
+ const {
1839
+ sendMessage,
1840
+ uploadAttachment,
1841
+ isLoading,
1842
+ readOnly,
1843
+ config,
1844
+ t,
1845
+ consumePendingPrefill,
1846
+ pendingPrefill
1847
+ } = useChat();
1848
+ const theme = useEffectiveTheme();
1849
+ const isDark = theme.scheme === "dark";
1850
+ const reduced = useReducedMotion();
1851
+ const [value, setValue] = (0, import_react9.useState)("");
1852
+ const [attachments, setAttachments] = (0, import_react9.useState)([]);
1853
+ const [captureSupported, setCaptureSupported] = (0, import_react9.useState)(false);
1854
+ const [menuOpen, setMenuOpen] = (0, import_react9.useState)(false);
1855
+ const [dragActive, setDragActive] = (0, import_react9.useState)(false);
1856
+ const [transientError, setTransientError] = (0, import_react9.useState)(null);
1857
+ const fileInputRef = (0, import_react9.useRef)(null);
1858
+ const textInputRef = (0, import_react9.useRef)(null);
1859
+ const menuRef = (0, import_react9.useRef)(null);
1860
+ const menuButtonRef = (0, import_react9.useRef)(null);
1861
+ const dragCounterRef = (0, import_react9.useRef)(0);
1862
+ (0, import_react9.useEffect)(() => {
1863
+ setCaptureSupported((0, import_js3.canCaptureScreenshot)());
1864
+ }, []);
1865
+ (0, import_react9.useEffect)(() => {
1866
+ const id = requestAnimationFrame(() => textInputRef.current?.focus());
1867
+ return () => cancelAnimationFrame(id);
1868
+ }, []);
1869
+ (0, import_react9.useLayoutEffect)(() => {
1870
+ const el = textInputRef.current;
1871
+ if (!el) return;
1872
+ el.style.height = "auto";
1873
+ el.style.height = `${el.scrollHeight}px`;
1874
+ }, [value]);
1875
+ (0, import_react9.useEffect)(() => {
1876
+ if (pendingPrefill === null) return;
1877
+ const text = consumePendingPrefill();
1878
+ if (text !== null) setValue(text);
1879
+ }, [pendingPrefill, consumePendingPrefill]);
1880
+ (0, import_react9.useEffect)(() => {
1881
+ return () => {
1882
+ for (const a of attachments) URL.revokeObjectURL(a.previewUrl);
1883
+ };
1884
+ }, []);
1885
+ (0, import_react9.useEffect)(() => {
1886
+ if (!menuOpen) return;
1887
+ const onClick = (e) => {
1888
+ const target = e.target;
1889
+ if (menuRef.current?.contains(target) || menuButtonRef.current?.contains(target)) {
1890
+ return;
1891
+ }
1892
+ setMenuOpen(false);
1893
+ };
1894
+ const onKey = (e) => {
1895
+ if (e.key === "Escape") setMenuOpen(false);
1896
+ };
1897
+ document.addEventListener("mousedown", onClick);
1898
+ document.addEventListener("keydown", onKey);
1899
+ return () => {
1900
+ document.removeEventListener("mousedown", onClick);
1901
+ document.removeEventListener("keydown", onKey);
1902
+ };
1903
+ }, [menuOpen]);
1904
+ (0, import_react9.useEffect)(() => {
1905
+ if (!transientError) return;
1906
+ const id = window.setTimeout(() => setTransientError(null), 4e3);
1907
+ return () => window.clearTimeout(id);
1908
+ }, [transientError]);
1909
+ const updateAttachment = (id, patch) => {
1910
+ setAttachments(
1911
+ (current) => current.map(
1912
+ (a) => a.id === id ? { ...a, ...patch } : a
1913
+ )
1914
+ );
1915
+ };
1916
+ const startUpload = async (blob, filename) => {
1917
+ if (attachments.length >= MAX_ATTACHMENTS) return;
1918
+ const id = `att_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1919
+ const previewUrl = URL.createObjectURL(blob);
1920
+ setAttachments((current) => [
1921
+ ...current,
1922
+ { id, status: "uploading", previewUrl, blob, filename }
1923
+ ]);
1924
+ try {
1925
+ const { attachmentToken } = await uploadAttachment(blob, { filename });
1926
+ updateAttachment(id, {
1927
+ status: "ready",
1928
+ token: attachmentToken
1929
+ });
1930
+ } catch {
1931
+ updateAttachment(id, { status: "error" });
1932
+ }
1933
+ };
1934
+ const ingestFiles = (files) => {
1935
+ const remainingSlots = Math.max(0, MAX_ATTACHMENTS - attachments.length);
1936
+ if (remainingSlots === 0) return 0;
1937
+ let queued = 0;
1938
+ let rejectedAny = false;
1939
+ for (const f of Array.from(files)) {
1940
+ if (queued >= remainingSlots) break;
1941
+ const type = f.type;
1942
+ if (!ALLOWED_MIME_TYPES.includes(
1943
+ type
1944
+ )) {
1945
+ rejectedAny = true;
1946
+ continue;
1947
+ }
1948
+ const filename = typeof f.name === "string" && f.name.length > 0 ? f.name : void 0;
1949
+ void startUpload(f, filename);
1950
+ queued += 1;
1951
+ }
1952
+ if (rejectedAny) {
1953
+ setTransientError(t("attachment_unsupported_type"));
1954
+ }
1955
+ return queued;
1956
+ };
1957
+ const handleCapture = async () => {
1958
+ setMenuOpen(false);
1959
+ try {
1960
+ const blob = await (0, import_js3.captureScreenshot)();
1961
+ await startUpload(blob);
1962
+ } catch (e) {
1963
+ if (e instanceof import_js3.ScreenshotCancelled) return;
1964
+ }
1965
+ };
1966
+ const handlePickFile = () => {
1967
+ setMenuOpen(false);
1968
+ fileInputRef.current?.click();
1969
+ };
1970
+ const handleFileInputChange = (e) => {
1971
+ const files = e.target.files;
1972
+ if (files && files.length > 0) {
1973
+ ingestFiles(files);
1974
+ }
1975
+ e.target.value = "";
1976
+ };
1977
+ const handleRemove = (id) => {
1978
+ setAttachments((current) => {
1979
+ const target = current.find((a) => a.id === id);
1980
+ if (target) URL.revokeObjectURL(target.previewUrl);
1981
+ return current.filter((a) => a.id !== id);
1982
+ });
1983
+ };
1984
+ const handlePaste = (e) => {
1985
+ const items = e.clipboardData?.items;
1986
+ if (!items || items.length === 0) return;
1987
+ const blobs = [];
1988
+ for (const item of Array.from(items)) {
1989
+ if (item.kind !== "file") continue;
1990
+ const blob = item.getAsFile();
1991
+ if (blob) blobs.push(blob);
1992
+ }
1993
+ if (blobs.length === 0) return;
1994
+ e.preventDefault();
1995
+ ingestFiles(blobs);
1996
+ };
1997
+ const handleDragEnter = (e) => {
1998
+ if (!hasFiles(e)) return;
1999
+ e.preventDefault();
2000
+ dragCounterRef.current += 1;
2001
+ setDragActive(true);
2002
+ };
2003
+ const handleDragOver = (e) => {
2004
+ if (!hasFiles(e)) return;
2005
+ e.preventDefault();
2006
+ };
2007
+ const handleDragLeave = (e) => {
2008
+ if (!hasFiles(e)) return;
2009
+ e.preventDefault();
2010
+ dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
2011
+ if (dragCounterRef.current === 0) setDragActive(false);
2012
+ };
2013
+ const handleDrop = (e) => {
2014
+ if (!hasFiles(e)) return;
2015
+ e.preventDefault();
2016
+ dragCounterRef.current = 0;
2017
+ setDragActive(false);
2018
+ const files = e.dataTransfer?.files;
2019
+ if (files && files.length > 0) {
2020
+ ingestFiles(files);
2021
+ }
2022
+ };
2023
+ const readyTokens = attachments.filter(
2024
+ (a) => a.status === "ready"
2025
+ ).map((a) => a.token);
2026
+ const handleSend = () => {
2027
+ if (!value.trim() || isLoading) return;
2028
+ sendMessage(
2029
+ value,
2030
+ readyTokens.length > 0 ? { attachmentTokens: readyTokens } : void 0
2031
+ );
2032
+ for (const a of attachments) URL.revokeObjectURL(a.previewUrl);
2033
+ setAttachments([]);
2034
+ setValue("");
2035
+ };
2036
+ const handleKeyDown = (e) => {
2037
+ if (e.key === "Enter" && !e.shiftKey) {
2038
+ e.preventDefault();
2039
+ handleSend();
2040
+ }
2041
+ };
2042
+ const containerStyle = {
2043
+ position: "relative",
2044
+ padding: "12px 16px",
2045
+ borderTop: `1px solid ${theme.divider}`,
2046
+ display: "flex",
2047
+ flexDirection: "column",
2048
+ gap: 8
2049
+ };
2050
+ const rowStyle = {
2051
+ display: "flex",
2052
+ // Anchor the icon buttons to the bottom of the row so a multi-line
2053
+ // textarea grows upward without dragging them along.
2054
+ alignItems: "flex-end",
2055
+ gap: 8
2056
+ };
2057
+ const TEXTAREA_MAX_HEIGHT = 140;
2058
+ const inputStyle = {
2059
+ flex: 1,
2060
+ border: `1px solid ${theme.divider}`,
2061
+ borderRadius: 18,
2062
+ padding: "10px 16px",
2063
+ fontSize: 14,
2064
+ lineHeight: 1.4,
2065
+ outline: "none",
2066
+ background: isDark ? "rgba(255,255,255,0.06)" : "#fafafa",
2067
+ color: theme.text,
2068
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
2069
+ resize: "none",
2070
+ overflowY: "auto",
2071
+ maxHeight: TEXTAREA_MAX_HEIGHT,
2072
+ boxSizing: "border-box"
2073
+ };
2074
+ const sendButtonStyle = {
2075
+ width: 36,
2076
+ height: 36,
2077
+ borderRadius: "50%",
2078
+ background: theme.primary,
2079
+ border: "none",
2080
+ color: "white",
2081
+ cursor: isLoading ? "not-allowed" : "pointer",
2082
+ display: "flex",
2083
+ alignItems: "center",
2084
+ justifyContent: "center",
2085
+ opacity: isLoading ? 0.5 : 1,
2086
+ transition: reduced ? "opacity 0.2s" : "opacity 0.2s, transform 0.15s",
2087
+ flexShrink: 0,
2088
+ transform: "scale(1)"
2089
+ };
2090
+ const iconButtonStyle = (disabled) => ({
2091
+ width: 36,
2092
+ height: 36,
2093
+ borderRadius: "50%",
2094
+ background: "transparent",
2095
+ border: "none",
2096
+ color: disabled ? "#ccc" : "#666",
2097
+ cursor: disabled ? "not-allowed" : "pointer",
2098
+ display: "flex",
2099
+ alignItems: "center",
2100
+ justifyContent: "center",
2101
+ flexShrink: 0,
2102
+ padding: 0
2103
+ });
2104
+ const menuStyle = {
2105
+ position: "absolute",
2106
+ bottom: "calc(100% + 4px)",
2107
+ left: 0,
2108
+ background: theme.background,
2109
+ border: `1px solid ${theme.divider}`,
2110
+ borderRadius: 8,
2111
+ boxShadow: isDark ? "0 4px 16px rgba(0,0,0,0.5)" : "0 4px 16px rgba(0,0,0,0.12)",
2112
+ padding: 4,
2113
+ minWidth: 180,
2114
+ zIndex: 10,
2115
+ display: "flex",
2116
+ flexDirection: "column"
2117
+ };
2118
+ const menuItemStyle = {
2119
+ display: "flex",
2120
+ alignItems: "center",
2121
+ gap: 10,
2122
+ padding: "8px 12px",
2123
+ background: "transparent",
2124
+ border: "none",
2125
+ borderRadius: 4,
2126
+ cursor: "pointer",
2127
+ fontSize: 14,
2128
+ color: theme.text,
2129
+ textAlign: "left",
2130
+ whiteSpace: "nowrap",
2131
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
2132
+ };
2133
+ const dropOverlayStyle = {
2134
+ position: "absolute",
2135
+ inset: 0,
2136
+ background: isDark ? "rgba(15,23,42,0.92)" : "rgba(255,255,255,0.92)",
2137
+ border: `2px dashed ${theme.primary}`,
2138
+ borderRadius: 4,
2139
+ display: "flex",
2140
+ alignItems: "center",
2141
+ justifyContent: "center",
2142
+ color: theme.primary,
2143
+ fontSize: 14,
2144
+ fontWeight: 500,
2145
+ pointerEvents: "none",
2146
+ zIndex: 11
2147
+ };
2148
+ const errorPillStyle = {
2149
+ alignSelf: "flex-start",
2150
+ background: "#fef2f2",
2151
+ color: "#b91c1c",
2152
+ border: "1px solid #fecaca",
2153
+ borderRadius: 12,
2154
+ padding: "4px 10px",
2155
+ fontSize: 12
2156
+ };
2157
+ const attachDisabled = attachments.length >= MAX_ATTACHMENTS || isLoading || readOnly;
2158
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2159
+ "div",
2160
+ {
2161
+ style: containerStyle,
2162
+ onDragEnter: handleDragEnter,
2163
+ onDragOver: handleDragOver,
2164
+ onDragLeave: handleDragLeave,
2165
+ onDrop: handleDrop,
2166
+ children: [
2167
+ dragActive && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: dropOverlayStyle, "aria-hidden": "true", children: t("drop_files_here") }),
2168
+ transientError && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { role: "alert", style: errorPillStyle, children: transientError }),
2169
+ attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2170
+ "div",
2171
+ {
2172
+ style: { display: "flex", gap: 8, flexWrap: "wrap" },
2173
+ "aria-label": "Attachments",
2174
+ children: attachments.map((a) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2175
+ Thumbnail,
2176
+ {
2177
+ attachment: a,
2178
+ onRemove: () => handleRemove(a.id),
2179
+ t
2180
+ },
2181
+ a.id
2182
+ ))
2183
+ }
2184
+ ),
2185
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: rowStyle, children: [
2186
+ config.allowAttachments !== false && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { position: "relative" }, children: [
2187
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2188
+ "button",
2189
+ {
2190
+ ref: menuButtonRef,
2191
+ type: "button",
2192
+ onClick: () => setMenuOpen((o) => !o),
2193
+ disabled: attachDisabled,
2194
+ style: iconButtonStyle(attachDisabled),
2195
+ "aria-label": t("attach_menu_open"),
2196
+ "aria-haspopup": "menu",
2197
+ "aria-expanded": menuOpen,
2198
+ title: t("attach_menu_open"),
2199
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PaperclipIcon, {})
2200
+ }
2201
+ ),
2202
+ menuOpen && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { ref: menuRef, role: "menu", style: menuStyle, children: [
2203
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2204
+ "button",
2205
+ {
2206
+ type: "button",
2207
+ role: "menuitem",
2208
+ onClick: handlePickFile,
2209
+ style: menuItemStyle,
2210
+ onMouseEnter: (e) => {
2211
+ e.currentTarget.style.background = theme.bubbleBackground;
2212
+ },
2213
+ onMouseLeave: (e) => {
2214
+ e.currentTarget.style.background = "transparent";
2215
+ },
2216
+ children: [
2217
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ImageIcon, {}),
2218
+ t("attach_photo")
2219
+ ]
2220
+ }
2221
+ ),
2222
+ captureSupported && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2223
+ "button",
2224
+ {
2225
+ type: "button",
2226
+ role: "menuitem",
2227
+ onClick: handleCapture,
2228
+ style: menuItemStyle,
2229
+ onMouseEnter: (e) => {
2230
+ e.currentTarget.style.background = theme.bubbleBackground;
2231
+ },
2232
+ onMouseLeave: (e) => {
2233
+ e.currentTarget.style.background = "transparent";
2234
+ },
2235
+ children: [
2236
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CameraIcon, {}),
2237
+ t("screenshot_capture")
2238
+ ]
2239
+ }
2240
+ )
2241
+ ] })
2242
+ ] }),
2243
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2244
+ "input",
2245
+ {
2246
+ ref: fileInputRef,
2247
+ type: "file",
2248
+ accept: ACCEPT_ATTR,
2249
+ multiple: true,
2250
+ onChange: handleFileInputChange,
2251
+ style: { display: "none" },
2252
+ "aria-hidden": "true",
2253
+ tabIndex: -1
2254
+ }
2255
+ ),
2256
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2257
+ "textarea",
2258
+ {
2259
+ ref: textInputRef,
2260
+ rows: 1,
2261
+ value,
2262
+ onChange: (e) => setValue(e.target.value),
2263
+ onKeyDown: handleKeyDown,
2264
+ onPaste: handlePaste,
2265
+ placeholder: config.placeholderText,
2266
+ style: inputStyle,
2267
+ disabled: isLoading || readOnly
2268
+ }
2269
+ ),
2270
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2271
+ "button",
2272
+ {
2273
+ onClick: handleSend,
2274
+ disabled: isLoading || readOnly || !value.trim(),
2275
+ style: sendButtonStyle,
2276
+ "aria-label": t("send_message"),
2277
+ onMouseEnter: (e) => {
2278
+ if (!reduced && !isLoading)
2279
+ e.currentTarget.style.transform = "scale(1.1)";
2280
+ },
2281
+ onMouseLeave: (e) => {
2282
+ if (!reduced) e.currentTarget.style.transform = "scale(1)";
2283
+ },
2284
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2285
+ "svg",
2286
+ {
2287
+ width: "16",
2288
+ height: "16",
2289
+ viewBox: "0 0 24 24",
2290
+ fill: "none",
2291
+ stroke: "currentColor",
2292
+ strokeWidth: "2",
2293
+ strokeLinecap: "round",
2294
+ strokeLinejoin: "round",
2295
+ children: [
2296
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
2297
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
2298
+ ]
2299
+ }
2300
+ )
2301
+ }
2302
+ )
2303
+ ] })
2304
+ ]
2305
+ }
2306
+ );
2307
+ }
2308
+ function hasFiles(e) {
2309
+ const types = e.dataTransfer?.types;
2310
+ if (!types) return false;
2311
+ for (let i = 0; i < types.length; i++) {
2312
+ if (types[i] === "Files") return true;
2313
+ }
2314
+ return false;
2315
+ }
2316
+ function Thumbnail({
2317
+ attachment,
2318
+ onRemove,
2319
+ t
2320
+ }) {
2321
+ const { status, previewUrl, blob, filename } = attachment;
2322
+ const isImage = isImageMime(blob.type);
2323
+ const wrap = {
2324
+ position: "relative",
2325
+ width: isImage ? 56 : 160,
2326
+ height: 56,
2327
+ borderRadius: 8,
2328
+ overflow: "hidden",
2329
+ border: status === "error" ? "2px solid #b91c1c" : "1px solid #e0e0e0",
2330
+ background: "#f5f5f5",
2331
+ display: "flex",
2332
+ alignItems: "center",
2333
+ justifyContent: isImage ? "stretch" : "flex-start",
2334
+ gap: isImage ? 0 : 8,
2335
+ padding: isImage ? 0 : "0 8px"
2336
+ };
2337
+ const img = {
2338
+ width: "100%",
2339
+ height: "100%",
2340
+ objectFit: "cover"
2341
+ };
2342
+ const removeBtn = {
2343
+ position: "absolute",
2344
+ top: 2,
2345
+ right: 2,
2346
+ width: 18,
2347
+ height: 18,
2348
+ borderRadius: "50%",
2349
+ border: "none",
2350
+ background: "rgba(0,0,0,0.6)",
2351
+ color: "white",
2352
+ fontSize: 11,
2353
+ lineHeight: "18px",
2354
+ padding: 0,
2355
+ cursor: "pointer",
2356
+ textAlign: "center"
2357
+ };
2358
+ const overlay = {
2359
+ position: "absolute",
2360
+ inset: 0,
2361
+ background: "rgba(255,255,255,0.7)",
2362
+ display: "flex",
2363
+ alignItems: "center",
2364
+ justifyContent: "center"
2365
+ };
2366
+ const docName = {
2367
+ fontSize: 12,
2368
+ fontWeight: 500,
2369
+ color: "#333",
2370
+ overflow: "hidden",
2371
+ textOverflow: "ellipsis",
2372
+ whiteSpace: "nowrap",
2373
+ maxWidth: 110,
2374
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
2375
+ };
2376
+ const docMeta = {
2377
+ fontSize: 11,
2378
+ color: "#888",
2379
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
2380
+ };
2381
+ const displayName = filename ?? (isImage ? "" : "Document");
2382
+ const sizeLabel = formatBytes2(blob.size);
2383
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: wrap, children: [
2384
+ isImage ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("img", { src: previewUrl, alt: "", style: img }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2385
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(DocIcon, {}),
2386
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2387
+ "div",
2388
+ {
2389
+ style: {
2390
+ display: "flex",
2391
+ flexDirection: "column",
2392
+ minWidth: 0,
2393
+ flex: 1
2394
+ },
2395
+ children: [
2396
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: docName, title: displayName, children: displayName }),
2397
+ sizeLabel && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: docMeta, children: sizeLabel })
2398
+ ]
2399
+ }
2400
+ )
2401
+ ] }),
2402
+ status === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: overlay, "aria-label": "Uploading", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Spinner2, {}) }),
2403
+ status === "error" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2404
+ "div",
2405
+ {
2406
+ style: { ...overlay, background: "rgba(255,255,255,0.85)" },
2407
+ "aria-label": t("status_failed"),
2408
+ title: t("status_failed"),
2409
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { color: "#b91c1c", fontSize: 11, fontWeight: 600 }, children: "!" })
2410
+ }
2411
+ ),
2412
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2413
+ "button",
2414
+ {
2415
+ type: "button",
2416
+ style: removeBtn,
2417
+ onClick: onRemove,
2418
+ "aria-label": t("attachment_remove"),
2419
+ children: "\xD7"
2420
+ }
2421
+ )
2422
+ ] });
2423
+ }
2424
+ function PaperclipIcon() {
2425
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2426
+ "svg",
2427
+ {
2428
+ width: "20",
2429
+ height: "20",
2430
+ viewBox: "0 0 24 24",
2431
+ fill: "none",
2432
+ stroke: "currentColor",
2433
+ strokeWidth: "2",
2434
+ strokeLinecap: "round",
2435
+ strokeLinejoin: "round",
2436
+ "aria-hidden": "true",
2437
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" })
2438
+ }
2439
+ );
2440
+ }
2441
+ function ImageIcon() {
2442
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2443
+ "svg",
2444
+ {
2445
+ width: "18",
2446
+ height: "18",
2447
+ viewBox: "0 0 24 24",
2448
+ fill: "none",
2449
+ stroke: "currentColor",
2450
+ strokeWidth: "2",
2451
+ strokeLinecap: "round",
2452
+ strokeLinejoin: "round",
2453
+ "aria-hidden": "true",
2454
+ children: [
2455
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
2456
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
2457
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polyline", { points: "21 15 16 10 5 21" })
2458
+ ]
2459
+ }
2460
+ );
2461
+ }
2462
+ function DocIcon() {
2463
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2464
+ "svg",
2465
+ {
2466
+ width: "20",
2467
+ height: "20",
2468
+ viewBox: "0 0 24 24",
2469
+ fill: "none",
2470
+ stroke: "currentColor",
2471
+ strokeWidth: "2",
2472
+ strokeLinecap: "round",
2473
+ strokeLinejoin: "round",
2474
+ "aria-hidden": "true",
2475
+ style: { color: "#666", flexShrink: 0 },
2476
+ children: [
2477
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
2478
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polyline", { points: "14 2 14 8 20 8" }),
2479
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
2480
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("line", { x1: "16", y1: "17", x2: "8", y2: "17" }),
2481
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polyline", { points: "10 9 9 9 8 9" })
2482
+ ]
2483
+ }
2484
+ );
2485
+ }
2486
+ function formatBytes2(bytes) {
2487
+ if (!Number.isFinite(bytes) || bytes <= 0) return "";
2488
+ if (bytes < 1024) return `${bytes} B`;
2489
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
2490
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2491
+ }
2492
+ function CameraIcon() {
2493
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2494
+ "svg",
2495
+ {
2496
+ width: "18",
2497
+ height: "18",
2498
+ viewBox: "0 0 24 24",
2499
+ fill: "none",
2500
+ stroke: "currentColor",
2501
+ strokeWidth: "2",
2502
+ strokeLinecap: "round",
2503
+ strokeLinejoin: "round",
2504
+ "aria-hidden": "true",
2505
+ children: [
2506
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" }),
2507
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "12", cy: "13", r: "4" })
2508
+ ]
2509
+ }
2510
+ );
2511
+ }
2512
+ function Spinner2() {
2513
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2514
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("style", { children: `@keyframes ch-att-spin { to { transform: rotate(360deg); } }` }),
2515
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2516
+ "span",
2517
+ {
2518
+ "aria-hidden": "true",
2519
+ style: {
2520
+ width: 16,
2521
+ height: 16,
2522
+ borderRadius: "50%",
2523
+ border: "2px solid #888",
2524
+ borderTopColor: "transparent",
2525
+ animation: "ch-att-spin 0.8s linear infinite",
2526
+ display: "inline-block"
2527
+ }
2528
+ }
2529
+ )
2530
+ ] });
2531
+ }
2532
+
2533
+ // src/components/incident-banner.tsx
2534
+ var import_react10 = require("react");
2535
+ var import_jsx_runtime9 = require("react/jsx-runtime");
2536
+ var PALETTE = {
2537
+ info: {
2538
+ bg: "#EFF6FF",
2539
+ fg: "#1E3A8A",
2540
+ fgMuted: "#3B5BA9",
2541
+ accent: "#2563EB",
2542
+ iconBg: "#DBEAFE",
2543
+ iconFg: "#2563EB"
2544
+ },
2545
+ warning: {
2546
+ bg: "#FFFBEB",
2547
+ fg: "#78350F",
2548
+ fgMuted: "#92541A",
2549
+ accent: "#B45309",
2550
+ iconBg: "#FEF3C7",
2551
+ iconFg: "#B45309"
2552
+ },
2553
+ outage: {
2554
+ bg: "#FEF2F2",
2555
+ fg: "#991B1B",
2556
+ fgMuted: "#B23A3A",
2557
+ accent: "#DC2626",
2558
+ iconBg: "#FEE2E2",
2559
+ iconFg: "#DC2626"
2560
+ }
2561
+ };
2562
+ function SeverityIcon({
2563
+ severity,
2564
+ color
2565
+ }) {
2566
+ const props = {
2567
+ width: 14,
2568
+ height: 14,
2569
+ viewBox: "0 0 24 24",
2570
+ fill: "none",
2571
+ stroke: color,
2572
+ strokeWidth: 2.25,
2573
+ strokeLinecap: "round",
2574
+ strokeLinejoin: "round",
2575
+ "aria-hidden": true
2576
+ };
2577
+ if (severity === "outage") {
2578
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("svg", { ...props, children: [
2579
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
2580
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
2581
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
2582
+ ] });
2583
+ }
2584
+ if (severity === "warning") {
2585
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("svg", { ...props, children: [
2586
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" }),
2587
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "12", y1: "9", x2: "12", y2: "13" }),
2588
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
2589
+ ] });
2590
+ }
2591
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("svg", { ...props, children: [
2592
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
2593
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "12", y1: "16", x2: "12", y2: "12" }),
2594
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "12", y1: "8", x2: "12.01", y2: "8" })
2595
+ ] });
2596
+ }
2597
+ function IncidentBanner() {
2598
+ const { incidentBanner, incidentBannerDismissed, dismissIncidentBanner, t } = useChat();
2599
+ const [linkHover, setLinkHover] = (0, import_react10.useState)(false);
2600
+ const [closeHover, setCloseHover] = (0, import_react10.useState)(false);
2601
+ if (!incidentBanner || incidentBannerDismissed) return null;
2602
+ const palette = PALETTE[incidentBanner.severity];
2603
+ const wrap = {
2604
+ background: palette.bg,
2605
+ color: palette.fg,
2606
+ padding: "12px 14px 12px 12px",
2607
+ display: "flex",
2608
+ gap: 10,
2609
+ alignItems: "flex-start",
2610
+ fontSize: 13,
2611
+ lineHeight: 1.45,
2612
+ boxShadow: "inset 0 -1px 0 rgba(0,0,0,0.04)"
2613
+ };
2614
+ const iconBadge = {
2615
+ width: 22,
2616
+ height: 22,
2617
+ borderRadius: "50%",
2618
+ background: palette.iconBg,
2619
+ display: "flex",
2620
+ alignItems: "center",
2621
+ justifyContent: "center",
2622
+ flexShrink: 0,
2623
+ marginTop: 1
2624
+ };
2625
+ const titleStyle = {
2626
+ margin: 0,
2627
+ fontSize: 13,
2628
+ fontWeight: 600,
2629
+ letterSpacing: "-0.005em"
2630
+ };
2631
+ const bodyStyle = {
2632
+ margin: "3px 0 0",
2633
+ fontSize: 12.5,
2634
+ color: palette.fgMuted
2635
+ };
2636
+ const footerRowStyle = {
2637
+ display: "flex",
2638
+ flexWrap: "wrap",
2639
+ alignItems: "center",
2640
+ gap: 10,
2641
+ marginTop: 8
2642
+ };
2643
+ const etaStyle = {
2644
+ display: "inline-block",
2645
+ padding: "2px 8px",
2646
+ borderRadius: 4,
2647
+ fontSize: 11,
2648
+ fontWeight: 500,
2649
+ color: palette.accent,
2650
+ background: palette.iconBg,
2651
+ lineHeight: 1.4
2652
+ };
2653
+ const linkStyle = {
2654
+ display: "inline-flex",
2655
+ alignItems: "center",
2656
+ gap: 4,
2657
+ color: palette.accent,
2658
+ textDecoration: linkHover ? "underline" : "none",
2659
+ textUnderlineOffset: 3,
2660
+ fontSize: 12.5,
2661
+ fontWeight: 600
2662
+ };
2663
+ const closeButtonStyle = {
2664
+ background: closeHover ? "rgba(0,0,0,0.06)" : "transparent",
2665
+ border: "none",
2666
+ color: palette.fgMuted,
2667
+ cursor: "pointer",
2668
+ padding: 4,
2669
+ borderRadius: 6,
2670
+ flexShrink: 0,
2671
+ lineHeight: 0,
2672
+ marginTop: -2,
2673
+ marginRight: -2,
2674
+ transition: "background-color 0.12s ease"
2675
+ };
2676
+ const role = incidentBanner.severity === "outage" ? "alert" : "status";
2677
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { role, style: wrap, children: [
2678
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: iconBadge, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2679
+ SeverityIcon,
2680
+ {
2681
+ severity: incidentBanner.severity,
2682
+ color: palette.iconFg
2683
+ }
2684
+ ) }),
2685
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [
2686
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: titleStyle, children: incidentBanner.title }),
2687
+ incidentBanner.body ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: bodyStyle, children: incidentBanner.body }) : null,
2688
+ incidentBanner.eta || incidentBanner.link ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: footerRowStyle, children: [
2689
+ incidentBanner.eta ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: etaStyle, children: incidentBanner.eta }) : null,
2690
+ incidentBanner.link ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2691
+ "a",
2692
+ {
2693
+ href: incidentBanner.link.url,
2694
+ target: "_blank",
2695
+ rel: "noopener noreferrer",
2696
+ style: linkStyle,
2697
+ onMouseEnter: () => setLinkHover(true),
2698
+ onMouseLeave: () => setLinkHover(false),
2699
+ children: [
2700
+ incidentBanner.link.label ?? t("incident_default_link_label"),
2701
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2702
+ "svg",
2703
+ {
2704
+ width: "12",
2705
+ height: "12",
2706
+ viewBox: "0 0 24 24",
2707
+ fill: "none",
2708
+ stroke: "currentColor",
2709
+ strokeWidth: "2.5",
2710
+ strokeLinecap: "round",
2711
+ strokeLinejoin: "round",
2712
+ "aria-hidden": "true",
2713
+ children: [
2714
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "5", y1: "12", x2: "19", y2: "12" }),
2715
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("polyline", { points: "12 5 19 12 12 19" })
2716
+ ]
2717
+ }
2718
+ )
2719
+ ]
2720
+ }
2721
+ ) : null
2722
+ ] }) : null
2723
+ ] }),
2724
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2725
+ "button",
2726
+ {
2727
+ type: "button",
2728
+ onClick: dismissIncidentBanner,
2729
+ "aria-label": t("incident_dismiss"),
2730
+ style: closeButtonStyle,
2731
+ onMouseEnter: () => setCloseHover(true),
2732
+ onMouseLeave: () => setCloseHover(false),
2733
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2734
+ "svg",
2735
+ {
2736
+ width: "14",
2737
+ height: "14",
2738
+ viewBox: "0 0 24 24",
2739
+ fill: "none",
2740
+ stroke: "currentColor",
2741
+ strokeWidth: "2",
2742
+ strokeLinecap: "round",
2743
+ strokeLinejoin: "round",
2744
+ "aria-hidden": "true",
2745
+ children: [
2746
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
2747
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
2748
+ ]
2749
+ }
2750
+ )
2751
+ }
2752
+ )
2753
+ ] });
2754
+ }
2755
+
2756
+ // src/components/chat-window.tsx
2757
+ var import_jsx_runtime10 = require("react/jsx-runtime");
2758
+ function ConfigError({ message, title }) {
2759
+ const errorStyle = {
2760
+ flex: 1,
2761
+ display: "flex",
2762
+ flexDirection: "column",
2763
+ alignItems: "center",
2764
+ justifyContent: "center",
2765
+ padding: 24,
2766
+ textAlign: "center",
2767
+ gap: 8
2768
+ };
2769
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: errorStyle, children: [
2770
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
2771
+ "svg",
2772
+ {
2773
+ width: "32",
2774
+ height: "32",
2775
+ viewBox: "0 0 24 24",
2776
+ fill: "none",
2777
+ stroke: "#999",
2778
+ strokeWidth: "2",
2779
+ strokeLinecap: "round",
2780
+ strokeLinejoin: "round",
2781
+ children: [
2782
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
2783
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
2784
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
2785
+ ]
2786
+ }
2787
+ ),
2788
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: { fontSize: 14, fontWeight: 500, color: "#333", margin: 0 }, children: title }),
2789
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: { fontSize: 12, color: "#999", margin: 0 }, children: message })
2790
+ ] });
2791
+ }
2792
+ function fieldKey(field) {
2793
+ if (field.kind === "name" || field.kind === "email" || field.kind === "phone") {
2794
+ return field.kind;
2795
+ }
2796
+ return field.key;
2797
+ }
2798
+ function fieldLabel(field) {
2799
+ if (field.kind === "name") return field.label ?? "Name";
2800
+ if (field.kind === "email") return field.label ?? "Email";
2801
+ if (field.kind === "phone") return field.label ?? "Phone";
2802
+ return field.label;
2803
+ }
2804
+ function validateField(field, value) {
2805
+ const required = "required" in field ? !!field.required : false;
2806
+ if (required && (value === void 0 || value === "" || value === false)) {
2807
+ return "Required";
2808
+ }
2809
+ if (field.kind === "email" && typeof value === "string" && value !== "") {
2810
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) return "Invalid email";
2811
+ }
2812
+ return null;
2813
+ }
2814
+ function PreChatFormView() {
2815
+ const { preChatForm, submitPreChatForm, cancelPreChatForm, config, t } = useChat();
2816
+ const [values, setValues] = (0, import_react11.useState)({});
2817
+ const [errors, setErrors] = (0, import_react11.useState)({});
2818
+ const [submitting, setSubmitting] = (0, import_react11.useState)(false);
2819
+ if (!preChatForm) return null;
2820
+ function setValue(key, value) {
2821
+ setValues((prev) => ({ ...prev, [key]: value }));
2822
+ setErrors((prev) => ({ ...prev, [key]: null }));
2823
+ }
2824
+ async function handleSubmit(e) {
2825
+ e.preventDefault();
2826
+ if (!preChatForm) return;
2827
+ const nextErrors = {};
2828
+ let hasError = false;
2829
+ for (const f of preChatForm.fields) {
2830
+ const k = fieldKey(f);
2831
+ const err = validateField(f, values[k]);
2832
+ nextErrors[k] = err;
2833
+ if (err) hasError = true;
2834
+ }
2835
+ setErrors(nextErrors);
2836
+ if (hasError) return;
2837
+ const submission = {};
2838
+ const properties = {};
2839
+ for (const f of preChatForm.fields) {
2840
+ const k = fieldKey(f);
2841
+ const v = values[k];
2842
+ if (v === void 0 || v === "") continue;
2843
+ if (f.kind === "name" && typeof v === "string") submission.name = v;
2844
+ else if (f.kind === "email" && typeof v === "string")
2845
+ submission.email = v;
2846
+ else if (f.kind === "phone" && typeof v === "string")
2847
+ submission.phone = v;
2848
+ else if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
2849
+ properties[k] = v;
2850
+ }
2851
+ }
2852
+ if (Object.keys(properties).length > 0) submission.properties = properties;
2853
+ setSubmitting(true);
2854
+ try {
2855
+ await submitPreChatForm(submission);
2856
+ } finally {
2857
+ setSubmitting(false);
2858
+ }
2859
+ }
2860
+ const containerStyle = {
2861
+ flex: 1,
2862
+ overflowY: "auto",
2863
+ padding: 20,
2864
+ display: "flex",
2865
+ flexDirection: "column",
2866
+ gap: 12,
2867
+ background: config.backgroundColor,
2868
+ color: config.textColor
2869
+ };
2870
+ const labelStyle = {
2871
+ fontSize: 13,
2872
+ fontWeight: 500,
2873
+ display: "flex",
2874
+ flexDirection: "column",
2875
+ gap: 4
2876
+ };
2877
+ const inputStyle = {
2878
+ width: "100%",
2879
+ border: "1px solid #d4d4d8",
2880
+ borderRadius: 8,
2881
+ padding: "8px 10px",
2882
+ fontSize: 14,
2883
+ background: "white",
2884
+ color: "#111",
2885
+ boxSizing: "border-box"
2886
+ };
2887
+ const errorStyle = { color: "#dc2626", fontSize: 12 };
2888
+ const buttonRowStyle = {
2889
+ display: "flex",
2890
+ gap: 8,
2891
+ marginTop: 8
2892
+ };
2893
+ const submitStyle = {
2894
+ flex: 1,
2895
+ background: config.primaryColor,
2896
+ color: "white",
2897
+ border: "none",
2898
+ borderRadius: 8,
2899
+ padding: "10px 14px",
2900
+ fontSize: 14,
2901
+ fontWeight: 600,
2902
+ cursor: submitting ? "not-allowed" : "pointer",
2903
+ opacity: submitting ? 0.7 : 1
2904
+ };
2905
+ const cancelStyle = {
2906
+ background: "transparent",
2907
+ color: config.textColor,
2908
+ border: "1px solid #d4d4d8",
2909
+ borderRadius: 8,
2910
+ padding: "10px 14px",
2911
+ fontSize: 14,
2912
+ cursor: "pointer"
2913
+ };
2914
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
2915
+ "form",
2916
+ {
2917
+ style: containerStyle,
2918
+ onSubmit: handleSubmit,
2919
+ "data-customerhero-prechat-form": true,
2920
+ children: [
2921
+ preChatForm.title && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h3", { style: { fontSize: 16, fontWeight: 600, margin: 0 }, children: preChatForm.title }),
2922
+ preChatForm.description && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: { fontSize: 13, margin: 0, opacity: 0.8 }, children: preChatForm.description }),
2923
+ preChatForm.fields.map((field) => {
2924
+ const k = fieldKey(field);
2925
+ const v = values[k];
2926
+ const err = errors[k];
2927
+ const required = "required" in field ? !!field.required : false;
2928
+ const label = fieldLabel(field);
2929
+ if (field.kind === "textarea") {
2930
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("label", { style: labelStyle, children: [
2931
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { children: [
2932
+ label,
2933
+ required && " *"
2934
+ ] }),
2935
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2936
+ "textarea",
2937
+ {
2938
+ style: { ...inputStyle, minHeight: 80, resize: "vertical" },
2939
+ value: v ?? "",
2940
+ maxLength: field.maxLength,
2941
+ onChange: (e) => setValue(k, e.target.value)
2942
+ }
2943
+ ),
2944
+ err && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: errorStyle, children: err })
2945
+ ] }, k);
2946
+ }
2947
+ if (field.kind === "select") {
2948
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("label", { style: labelStyle, children: [
2949
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { children: [
2950
+ label,
2951
+ required && " *"
2952
+ ] }),
2953
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
2954
+ "select",
2955
+ {
2956
+ style: inputStyle,
2957
+ value: v ?? "",
2958
+ onChange: (e) => setValue(k, e.target.value),
2959
+ children: [
2960
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("option", { value: "", children: "\u2014" }),
2961
+ field.options.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("option", { value: opt.value, children: opt.label }, opt.value))
2962
+ ]
2963
+ }
2964
+ ),
2965
+ err && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: errorStyle, children: err })
2966
+ ] }, k);
2967
+ }
2968
+ if (field.kind === "consent") {
2969
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
2970
+ "label",
2971
+ {
2972
+ style: {
2973
+ ...labelStyle,
2974
+ flexDirection: "row",
2975
+ alignItems: "flex-start",
2976
+ gap: 8
2977
+ },
2978
+ children: [
2979
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2980
+ "input",
2981
+ {
2982
+ type: "checkbox",
2983
+ checked: v === true,
2984
+ onChange: (e) => setValue(k, e.target.checked),
2985
+ style: { marginTop: 3 }
2986
+ }
2987
+ ),
2988
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { style: { fontSize: 13, fontWeight: 400 }, children: [
2989
+ field.label,
2990
+ field.url && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
2991
+ " ",
2992
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2993
+ "a",
2994
+ {
2995
+ href: field.url,
2996
+ target: "_blank",
2997
+ rel: "noopener noreferrer",
2998
+ style: { color: config.primaryColor },
2999
+ children: "\u2197"
3000
+ }
3001
+ )
3002
+ ] })
3003
+ ] }),
3004
+ err && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: errorStyle, children: err })
3005
+ ]
3006
+ },
3007
+ k
3008
+ );
3009
+ }
3010
+ const inputType = field.kind === "email" ? "email" : field.kind === "phone" ? "tel" : "text";
3011
+ const maxLength = field.kind === "text" ? field.maxLength : void 0;
3012
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("label", { style: labelStyle, children: [
3013
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { children: [
3014
+ label,
3015
+ required && " *"
3016
+ ] }),
3017
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
3018
+ "input",
3019
+ {
3020
+ type: inputType,
3021
+ style: inputStyle,
3022
+ value: v ?? "",
3023
+ maxLength,
3024
+ onChange: (e) => setValue(k, e.target.value)
3025
+ }
3026
+ ),
3027
+ err && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: errorStyle, children: err })
3028
+ ] }, k);
3029
+ }),
3030
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: buttonRowStyle, children: [
3031
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
3032
+ "button",
3033
+ {
3034
+ type: "button",
3035
+ onClick: cancelPreChatForm,
3036
+ style: cancelStyle,
3037
+ disabled: submitting,
3038
+ children: t("action_cancel")
3039
+ }
3040
+ ),
3041
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "submit", style: submitStyle, disabled: submitting, children: preChatForm.submitLabel })
3042
+ ] })
3043
+ ]
3044
+ }
3045
+ );
3046
+ }
3047
+ function ChatWindow({ embedded } = {}) {
3048
+ const { isOpen, config, configError, t, isRtl, preChatFormVisible } = useChat();
3049
+ const reduced = useReducedMotion();
3050
+ const theme = useEffectiveTheme();
3051
+ const [visible, setVisible] = (0, import_react11.useState)(false);
3052
+ const [shouldRender, setShouldRender] = (0, import_react11.useState)(false);
3053
+ (0, import_react11.useEffect)(() => {
3054
+ if (isOpen) {
3055
+ setShouldRender(true);
3056
+ requestAnimationFrame(() => {
3057
+ requestAnimationFrame(() => setVisible(true));
3058
+ });
3059
+ } else {
3060
+ setVisible(false);
3061
+ if (reduced) {
3062
+ setShouldRender(false);
3063
+ } else {
3064
+ const timer = setTimeout(() => setShouldRender(false), 250);
3065
+ return () => clearTimeout(timer);
3066
+ }
3067
+ }
3068
+ }, [isOpen, reduced]);
3069
+ if (!shouldRender) return null;
3070
+ const effectivePosition = isRtl ? config.position === "bottom-right" ? "bottom-left" : "bottom-right" : config.position;
3071
+ const colors = theme;
3072
+ const preset = theme.size;
3073
+ const radius = theme.radius;
3074
+ const panelBottom = config.offset.bottom + preset.bubble + 14;
3075
+ const style = {
3076
+ position: embedded ? "absolute" : "fixed",
3077
+ bottom: panelBottom,
3078
+ [effectivePosition === "bottom-left" ? "left" : "right"]: config.offset.side,
3079
+ width: preset.width,
3080
+ maxWidth: embedded ? "calc(100% - 16px)" : "calc(100vw - 40px)",
3081
+ height: preset.height,
3082
+ maxHeight: embedded ? `calc(100% - ${panelBottom + 16}px)` : `calc(100vh - ${panelBottom + 30}px)`,
3083
+ borderRadius: radius,
3084
+ overflow: "hidden",
3085
+ display: "flex",
3086
+ flexDirection: "column",
3087
+ // In dark mode the default 0.15 opacity black shadow is invisible
3088
+ // against a dark page; switch to a stronger shadow plus a subtle 1px
3089
+ // light outline so the panel still reads as a separated surface.
3090
+ boxShadow: theme.scheme === "dark" ? "0 12px 40px rgba(0,0,0,0.55), 0 0 0 1px rgba(255,255,255,0.06)" : "0 8px 40px rgba(0,0,0,0.15)",
3091
+ zIndex: config.zIndex,
3092
+ background: colors.background,
3093
+ color: colors.text,
3094
+ fontSize: preset.fontSize,
3095
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
3096
+ opacity: visible ? 1 : 0,
3097
+ transform: visible ? "translateY(0) scale(1)" : "translateY(16px) scale(0.97)",
3098
+ transition: reduced ? "none" : "opacity 0.25s ease, transform 0.25s ease"
3099
+ };
3100
+ const isDark = theme.scheme === "dark";
3101
+ const poweredStyle = {
3102
+ textAlign: "center",
3103
+ padding: 6,
3104
+ fontSize: 10,
3105
+ color: isDark ? "rgba(255,255,255,0.45)" : "#aaa"
3106
+ };
3107
+ const linkStyle = {
3108
+ color: isDark ? "rgba(255,255,255,0.6)" : "#888",
3109
+ textDecoration: "underline",
3110
+ textUnderlineOffset: 2
3111
+ };
3112
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style, dir: isRtl ? "rtl" : "ltr", children: [
3113
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ChatHeader, {}),
3114
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(IncidentBanner, {}),
3115
+ configError ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ConfigError, { title: t("unable_to_load"), message: configError }) : preChatFormVisible ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PreChatFormView, {}) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
3116
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ChatMessages, {}),
3117
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ChatSuggestions, {}),
3118
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ChatInput, {})
3119
+ ] }),
3120
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: poweredStyle, children: [
3121
+ t("powered_by"),
3122
+ " ",
3123
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
3124
+ "a",
3125
+ {
3126
+ href: "https://customerhero.app",
3127
+ target: "_blank",
3128
+ rel: "noopener noreferrer",
3129
+ style: linkStyle,
3130
+ children: "CustomerHero"
3131
+ }
3132
+ )
3133
+ ] })
3134
+ ] });
3135
+ }
3136
+
3137
+ // src/preview.tsx
3138
+ var import_jsx_runtime11 = require("react/jsx-runtime");
3139
+ function PreviewBootstrap({
3140
+ config,
3141
+ banner
3142
+ }) {
3143
+ const client = useCustomerHeroClient();
3144
+ (0, import_react12.useEffect)(() => {
3145
+ client.__seedForPreview(config, { banner: banner ?? null });
3146
+ }, [client]);
3147
+ const configKey = JSON.stringify(config);
3148
+ const bannerKey = JSON.stringify(banner ?? null);
3149
+ (0, import_react12.useEffect)(() => {
3150
+ client.__seedForPreview(config, { banner: banner ?? null });
3151
+ }, [client, configKey, bannerKey]);
3152
+ return null;
3153
+ }
3154
+ var wrapperStyleBase = {
3155
+ position: "relative",
3156
+ overflow: "hidden"
3157
+ };
3158
+ function PreviewWidget({
3159
+ style,
3160
+ className,
3161
+ banner,
3162
+ ...config
3163
+ }) {
3164
+ const initialConfig = (0, import_react12.useMemo)(() => config, []);
3165
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3166
+ "div",
3167
+ {
3168
+ className,
3169
+ style: { ...wrapperStyleBase, ...style },
3170
+ "data-customerhero-preview": "true",
3171
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(CustomerHeroProvider, { disableAutoFetch: true, ...initialConfig, children: [
3172
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PreviewBootstrap, { config, banner }),
3173
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ChatBubble, { embedded: true }),
3174
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ChatWindow, { embedded: true })
3175
+ ] })
3176
+ }
3177
+ );
3178
+ }
3179
+ // Annotate the CommonJS export names for ESM import in node:
3180
+ 0 && (module.exports = {
3181
+ PreviewWidget
3182
+ });