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