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