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