@customerhero/react 0.0.2 → 1.0.1

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,5 +1,5 @@
1
1
  // src/components/chat-widget.tsx
2
- import { useEffect as useEffect7, useRef as useRef4 } from "react";
2
+ import { useEffect as useEffect8, useRef as useRef4 } from "react";
3
3
 
4
4
  // src/context.tsx
5
5
  import {
@@ -52,13 +52,26 @@ function useChat() {
52
52
  ...state,
53
53
  t: client.t,
54
54
  sendMessage: useCallback(
55
- (message) => client.sendMessage(message),
55
+ (message, options) => client.sendMessage(message, options),
56
+ [client]
57
+ ),
58
+ uploadAttachment: useCallback(
59
+ (blob, options) => client.uploadAttachment(blob, options),
56
60
  [client]
57
61
  ),
58
62
  rateMessage: useCallback(
59
63
  (messageId, rating) => client.rateMessage(messageId, rating),
60
64
  [client]
61
65
  ),
66
+ approveAction: useCallback(
67
+ (pendingId) => client.approveAction(pendingId),
68
+ [client]
69
+ ),
70
+ cancelAction: useCallback(
71
+ (pendingId) => client.cancelAction(pendingId),
72
+ [client]
73
+ ),
74
+ setLocale: useCallback((tag) => client.setLocale(tag), [client]),
62
75
  toggle: useCallback(() => client.toggle(), [client]),
63
76
  open: useCallback(() => client.open(), [client]),
64
77
  close: useCallback(() => client.close(), [client]),
@@ -89,7 +102,7 @@ function useReducedMotion() {
89
102
  // src/components/chat-bubble.tsx
90
103
  import { jsx as jsx2 } from "react/jsx-runtime";
91
104
  function ChatBubble() {
92
- const { toggle, config, t } = useChat();
105
+ const { toggle, config, t, isRtl } = useChat();
93
106
  const reduced = useReducedMotion();
94
107
  const [mounted, setMounted] = useState2(false);
95
108
  useEffect3(() => {
@@ -97,10 +110,11 @@ function ChatBubble() {
97
110
  return () => cancelAnimationFrame(id);
98
111
  }, []);
99
112
  const visible = mounted;
113
+ const effectivePosition = isRtl ? config.position === "bottom-right" ? "bottom-left" : "bottom-right" : config.position;
100
114
  const style = {
101
115
  position: "fixed",
102
116
  bottom: 20,
103
- [config.position === "bottom-left" ? "left" : "right"]: 20,
117
+ [effectivePosition === "bottom-left" ? "left" : "right"]: 20,
104
118
  width: 56,
105
119
  height: 56,
106
120
  borderRadius: "50%",
@@ -124,6 +138,7 @@ function ChatBubble() {
124
138
  {
125
139
  onClick: toggle,
126
140
  style,
141
+ dir: isRtl ? "rtl" : "ltr",
127
142
  "aria-label": t("open_chat"),
128
143
  onMouseEnter: (e) => {
129
144
  if (!reduced) e.currentTarget.style.transform = "scale(1.1)";
@@ -150,7 +165,7 @@ function ChatBubble() {
150
165
  }
151
166
 
152
167
  // src/components/chat-window.tsx
153
- import { useEffect as useEffect6, useState as useState6 } from "react";
168
+ import { useEffect as useEffect7, useState as useState7 } from "react";
154
169
 
155
170
  // src/components/chat-header.tsx
156
171
  import { useState as useState3, useEffect as useEffect4, useRef as useRef2 } from "react";
@@ -369,8 +384,602 @@ function ChatHeader() {
369
384
  }
370
385
 
371
386
  // src/components/chat-messages.tsx
372
- import { useEffect as useEffect5, useRef as useRef3, useState as useState4 } from "react";
387
+ import { useEffect as useEffect5, useRef as useRef3, useState as useState5 } from "react";
388
+
389
+ // src/markdown/render.tsx
373
390
  import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
391
+ function parseBlocks(source) {
392
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
393
+ const blocks = [];
394
+ let i = 0;
395
+ while (i < lines.length) {
396
+ const line = lines[i];
397
+ if (line.trim() === "") {
398
+ i++;
399
+ continue;
400
+ }
401
+ const fenceMatch = line.match(/^```(.*)$/);
402
+ if (fenceMatch) {
403
+ const lang = fenceMatch[1].trim() || void 0;
404
+ const body2 = [];
405
+ i++;
406
+ while (i < lines.length && !/^```\s*$/.test(lines[i])) {
407
+ body2.push(lines[i]);
408
+ i++;
409
+ }
410
+ if (i < lines.length) i++;
411
+ blocks.push({ kind: "code", lines: body2, lang });
412
+ continue;
413
+ }
414
+ const headingMatch = line.match(/^(#{1,6})\s+(.*?)\s*#*\s*$/);
415
+ if (headingMatch) {
416
+ blocks.push({
417
+ kind: "heading",
418
+ level: headingMatch[1].length,
419
+ lines: [headingMatch[2]]
420
+ });
421
+ i++;
422
+ continue;
423
+ }
424
+ if (/^(\s*)[-*+]\s+/.test(line)) {
425
+ const body2 = [];
426
+ while (i < lines.length && /^(\s*)[-*+]\s+/.test(lines[i])) {
427
+ body2.push(lines[i].replace(/^(\s*)[-*+]\s+/, ""));
428
+ i++;
429
+ }
430
+ blocks.push({ kind: "ul", lines: body2 });
431
+ continue;
432
+ }
433
+ if (/^(\s*)\d+\.\s+/.test(line)) {
434
+ const body2 = [];
435
+ while (i < lines.length && /^(\s*)\d+\.\s+/.test(lines[i])) {
436
+ body2.push(lines[i].replace(/^(\s*)\d+\.\s+/, ""));
437
+ i++;
438
+ }
439
+ blocks.push({ kind: "ol", lines: body2 });
440
+ continue;
441
+ }
442
+ if (/^>\s?/.test(line)) {
443
+ const body2 = [];
444
+ while (i < lines.length && /^>\s?/.test(lines[i])) {
445
+ body2.push(lines[i].replace(/^>\s?/, ""));
446
+ i++;
447
+ }
448
+ blocks.push({ kind: "blockquote", lines: body2 });
449
+ continue;
450
+ }
451
+ const body = [line];
452
+ i++;
453
+ while (i < lines.length) {
454
+ const next = lines[i];
455
+ if (next.trim() === "") break;
456
+ if (/^```/.test(next) || /^#{1,6}\s/.test(next) || /^(\s*)[-*+]\s+/.test(next) || /^(\s*)\d+\.\s+/.test(next) || /^>\s?/.test(next)) {
457
+ break;
458
+ }
459
+ body.push(next);
460
+ i++;
461
+ }
462
+ blocks.push({ kind: "paragraph", lines: body });
463
+ }
464
+ return blocks;
465
+ }
466
+ function findClosing(text, from, end) {
467
+ let i = from;
468
+ while (i <= text.length - end.length) {
469
+ const c = text[i];
470
+ if (c === "\\") {
471
+ i += 2;
472
+ continue;
473
+ }
474
+ if (text.startsWith(end, i)) return i;
475
+ i++;
476
+ }
477
+ return -1;
478
+ }
479
+ function renderInlineText(text, ctx) {
480
+ const out = [];
481
+ let buffer = "";
482
+ let i = 0;
483
+ const flushBuffer = () => {
484
+ if (buffer.length > 0) {
485
+ out.push(buffer);
486
+ buffer = "";
487
+ }
488
+ };
489
+ while (i < text.length) {
490
+ const c = text[i];
491
+ if (c === "\\" && i + 1 < text.length) {
492
+ buffer += text[i + 1];
493
+ i += 2;
494
+ continue;
495
+ }
496
+ if (c === " " && text[i + 1] === " " && text[i + 2] === "\n") {
497
+ flushBuffer();
498
+ out.push(/* @__PURE__ */ jsx4("br", {}, ctx.nextKey()));
499
+ i += 3;
500
+ continue;
501
+ }
502
+ if (c === "\n") {
503
+ buffer += " ";
504
+ i++;
505
+ continue;
506
+ }
507
+ if (c === "`") {
508
+ const close = findClosing(text, i + 1, "`");
509
+ if (close !== -1) {
510
+ flushBuffer();
511
+ out.push(
512
+ /* @__PURE__ */ jsx4(
513
+ "code",
514
+ {
515
+ style: {
516
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
517
+ fontSize: "0.92em",
518
+ background: "rgba(0,0,0,0.06)",
519
+ padding: "1px 4px",
520
+ borderRadius: 3
521
+ },
522
+ children: text.slice(i + 1, close)
523
+ },
524
+ ctx.nextKey()
525
+ )
526
+ );
527
+ i = close + 1;
528
+ continue;
529
+ }
530
+ }
531
+ if ((text.startsWith("**", i) || text.startsWith("__", i)) && i + 2 < text.length) {
532
+ const marker = text.slice(i, i + 2);
533
+ const close = findClosing(text, i + 2, marker);
534
+ if (close !== -1 && close > i + 2) {
535
+ flushBuffer();
536
+ const inner = renderInlineText(text.slice(i + 2, close), ctx);
537
+ out.push(/* @__PURE__ */ jsx4("strong", { children: inner }, ctx.nextKey()));
538
+ i = close + 2;
539
+ continue;
540
+ }
541
+ }
542
+ if (c === "*" || c === "_") {
543
+ const prev = i === 0 ? " " : text[i - 1];
544
+ const isWordBoundaryBefore = c === "*" || !/\w/.test(prev);
545
+ if (isWordBoundaryBefore) {
546
+ const close = findClosing(text, i + 1, c);
547
+ const prevOfClose = close === -1 ? " " : close + 1 < text.length ? text[close + 1] : " ";
548
+ const isWordBoundaryAfter = c === "*" || !/\w/.test(prevOfClose);
549
+ if (close !== -1 && close > i + 1 && text[close + 1] !== c && isWordBoundaryAfter) {
550
+ flushBuffer();
551
+ const inner = renderInlineText(text.slice(i + 1, close), ctx);
552
+ out.push(/* @__PURE__ */ jsx4("em", { children: inner }, ctx.nextKey()));
553
+ i = close + 1;
554
+ continue;
555
+ }
556
+ }
557
+ }
558
+ if (c === "[") {
559
+ const closeLabel = findClosing(text, i + 1, "]");
560
+ if (closeLabel !== -1 && text[closeLabel + 1] === "(") {
561
+ const closeUrl = findClosing(text, closeLabel + 2, ")");
562
+ if (closeUrl !== -1) {
563
+ const label = text.slice(i + 1, closeLabel);
564
+ const url = text.slice(closeLabel + 2, closeUrl).trim();
565
+ if (isSafeUrl(url)) {
566
+ flushBuffer();
567
+ out.push(
568
+ /* @__PURE__ */ jsx4(
569
+ "a",
570
+ {
571
+ href: url,
572
+ target: "_blank",
573
+ rel: "noopener noreferrer",
574
+ style: { color: ctx.linkColor, textDecoration: "underline" },
575
+ children: renderInlineText(label, ctx)
576
+ },
577
+ ctx.nextKey()
578
+ )
579
+ );
580
+ i = closeUrl + 1;
581
+ continue;
582
+ }
583
+ }
584
+ }
585
+ if (closeLabel !== -1 && ctx.sources && ctx.sources.length > 0) {
586
+ const body = text.slice(i + 1, closeLabel).trim();
587
+ const parts = body.split(/\s*,\s*/).filter((s) => s.length > 0);
588
+ const indices = parts.map((p) => Number(p));
589
+ const allValid = parts.length > 0 && indices.every(
590
+ (n) => Number.isInteger(n) && n >= 1 && n <= (ctx.sources?.length ?? 0)
591
+ );
592
+ if (allValid) {
593
+ flushBuffer();
594
+ out.push(
595
+ /* @__PURE__ */ jsx4("sup", { style: { whiteSpace: "nowrap" }, children: indices.map((n, idx) => {
596
+ const src = ctx.sources[n - 1];
597
+ const content = /* @__PURE__ */ jsxs2(
598
+ "span",
599
+ {
600
+ style: {
601
+ fontSize: "0.75em",
602
+ color: ctx.linkColor,
603
+ cursor: src.url ? "pointer" : "default"
604
+ },
605
+ title: src.heading ? `${src.title} \u2014 ${src.heading}` : src.title,
606
+ children: [
607
+ "[",
608
+ n,
609
+ "]"
610
+ ]
611
+ }
612
+ );
613
+ const separator = idx > 0 ? " " : null;
614
+ return /* @__PURE__ */ jsxs2("span", { children: [
615
+ separator,
616
+ src.url ? /* @__PURE__ */ jsx4(
617
+ "a",
618
+ {
619
+ href: src.url,
620
+ target: "_blank",
621
+ rel: "noopener noreferrer",
622
+ style: {
623
+ color: ctx.linkColor,
624
+ textDecoration: "none"
625
+ },
626
+ children: content
627
+ }
628
+ ) : content
629
+ ] }, `${ctx.keyRoot}-cit-${idx}`);
630
+ }) }, ctx.nextKey())
631
+ );
632
+ i = closeLabel + 1;
633
+ continue;
634
+ }
635
+ }
636
+ }
637
+ buffer += c;
638
+ i++;
639
+ }
640
+ flushBuffer();
641
+ return out;
642
+ }
643
+ function isSafeUrl(url) {
644
+ if (/^https?:\/\//i.test(url)) return true;
645
+ if (/^mailto:/i.test(url)) return true;
646
+ if (/^tel:/i.test(url)) return true;
647
+ if (/^\/[^/]/.test(url) || /^#/.test(url)) return true;
648
+ return false;
649
+ }
650
+ function renderMarkdown(source, opts = {}) {
651
+ const blocks = parseBlocks(source);
652
+ const linkColor = opts.linkColor ?? "#6C3CE1";
653
+ let keyCounter = 0;
654
+ const ctx = {
655
+ sources: opts.sources,
656
+ linkColor,
657
+ keyRoot: "md",
658
+ nextKey: () => `md-${keyCounter++}`
659
+ };
660
+ const renderInline = (line) => renderInlineText(line, ctx);
661
+ return /* @__PURE__ */ jsx4(Fragment, { children: blocks.map((block, idx) => {
662
+ const key = `b-${idx}`;
663
+ switch (block.kind) {
664
+ case "heading": {
665
+ const level = block.level ?? 1;
666
+ const style = {
667
+ margin: "8px 0 4px",
668
+ fontWeight: 600,
669
+ fontSize: level <= 2 ? "1.15em" : level === 3 ? "1.05em" : "1em"
670
+ };
671
+ const children = renderInline(block.lines[0] ?? "");
672
+ if (level === 1)
673
+ return /* @__PURE__ */ jsx4("h1", { style, children }, key);
674
+ if (level === 2)
675
+ return /* @__PURE__ */ jsx4("h2", { style, children }, key);
676
+ if (level === 3)
677
+ return /* @__PURE__ */ jsx4("h3", { style, children }, key);
678
+ if (level === 4)
679
+ return /* @__PURE__ */ jsx4("h4", { style, children }, key);
680
+ if (level === 5)
681
+ return /* @__PURE__ */ jsx4("h5", { style, children }, key);
682
+ return /* @__PURE__ */ jsx4("h6", { style, children }, key);
683
+ }
684
+ case "paragraph":
685
+ return /* @__PURE__ */ jsx4("p", { style: { margin: "0 0 8px", lineHeight: 1.5 }, children: renderInline(block.lines.join("\n")) }, key);
686
+ case "ul":
687
+ return /* @__PURE__ */ jsx4(
688
+ "ul",
689
+ {
690
+ style: {
691
+ margin: "0 0 8px",
692
+ paddingLeft: 20,
693
+ lineHeight: 1.5
694
+ },
695
+ children: block.lines.map((l, i) => /* @__PURE__ */ jsx4("li", { children: renderInline(l) }, i))
696
+ },
697
+ key
698
+ );
699
+ case "ol":
700
+ return /* @__PURE__ */ jsx4(
701
+ "ol",
702
+ {
703
+ style: {
704
+ margin: "0 0 8px",
705
+ paddingLeft: 22,
706
+ lineHeight: 1.5
707
+ },
708
+ children: block.lines.map((l, i) => /* @__PURE__ */ jsx4("li", { children: renderInline(l) }, i))
709
+ },
710
+ key
711
+ );
712
+ case "code":
713
+ return /* @__PURE__ */ jsx4(
714
+ "pre",
715
+ {
716
+ style: {
717
+ margin: "0 0 8px",
718
+ padding: "10px 12px",
719
+ background: "rgba(0,0,0,0.06)",
720
+ borderRadius: 6,
721
+ overflowX: "auto",
722
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
723
+ fontSize: "0.88em",
724
+ lineHeight: 1.45
725
+ },
726
+ children: /* @__PURE__ */ jsx4("code", { children: block.lines.join("\n") })
727
+ },
728
+ key
729
+ );
730
+ case "blockquote":
731
+ return /* @__PURE__ */ jsx4(
732
+ "blockquote",
733
+ {
734
+ style: {
735
+ margin: "0 0 8px",
736
+ padding: "4px 0 4px 10px",
737
+ borderLeft: "3px solid rgba(0,0,0,0.15)",
738
+ color: "rgba(0,0,0,0.75)",
739
+ fontStyle: "italic"
740
+ },
741
+ children: renderInline(block.lines.join("\n"))
742
+ },
743
+ key
744
+ );
745
+ }
746
+ }) });
747
+ }
748
+
749
+ // src/components/action-confirmation-card.tsx
750
+ import { useState as useState4 } from "react";
751
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
752
+ function ActionConfirmationCard({
753
+ block,
754
+ primaryColor,
755
+ t,
756
+ onApprove,
757
+ onCancel
758
+ }) {
759
+ const [chosen, setChosen] = useState4(null);
760
+ const [showSpinner, setShowSpinner] = useState4(false);
761
+ const [error, setError] = useState4(null);
762
+ const [showSummary, setShowSummary] = useState4(false);
763
+ const handle = async (choice) => {
764
+ if (chosen) return;
765
+ setChosen(choice);
766
+ setError(null);
767
+ const spinnerTimer = window.setTimeout(() => setShowSpinner(true), 800);
768
+ try {
769
+ const fn = choice === "approve" ? onApprove : onCancel;
770
+ await fn(block.pendingToolCallId);
771
+ } catch (e) {
772
+ const msg = e instanceof Error ? e.message : t("action_failed");
773
+ setError(msg);
774
+ setChosen(null);
775
+ } finally {
776
+ window.clearTimeout(spinnerTimer);
777
+ setShowSpinner(false);
778
+ }
779
+ };
780
+ const cardStyle = {
781
+ marginTop: 6,
782
+ padding: 12,
783
+ borderRadius: 12,
784
+ border: "1px solid #e0e0e0",
785
+ background: "#fff",
786
+ display: "flex",
787
+ flexDirection: "column",
788
+ gap: 8,
789
+ fontSize: 13,
790
+ lineHeight: 1.4
791
+ };
792
+ const titleStyle = {
793
+ fontWeight: 600,
794
+ color: "#222"
795
+ };
796
+ const disclosureBtn = {
797
+ background: "none",
798
+ border: "none",
799
+ padding: 0,
800
+ color: primaryColor,
801
+ fontSize: 12,
802
+ cursor: "pointer",
803
+ textAlign: "left",
804
+ fontFamily: "inherit"
805
+ };
806
+ const summaryStyle = {
807
+ color: "#555",
808
+ background: "#fafafa",
809
+ padding: 8,
810
+ borderRadius: 8,
811
+ whiteSpace: "pre-wrap"
812
+ };
813
+ const buttonRow = {
814
+ display: "flex",
815
+ gap: 8,
816
+ marginTop: 4
817
+ };
818
+ const baseBtn = (variant) => ({
819
+ flex: 1,
820
+ padding: "8px 12px",
821
+ borderRadius: 8,
822
+ fontSize: 13,
823
+ fontWeight: 500,
824
+ cursor: chosen ? "default" : "pointer",
825
+ border: variant === "primary" ? `1px solid ${primaryColor}` : "1px solid #ddd",
826
+ background: variant === "primary" ? primaryColor : "#fff",
827
+ color: variant === "primary" ? "#fff" : "#333",
828
+ opacity: chosen && chosen !== (variant === "primary" ? "approve" : "cancel") ? 0.5 : 1,
829
+ transition: "opacity 0.15s",
830
+ display: "flex",
831
+ alignItems: "center",
832
+ justifyContent: "center",
833
+ gap: 6,
834
+ fontFamily: "inherit"
835
+ });
836
+ return /* @__PURE__ */ jsxs3("div", { style: cardStyle, role: "group", "aria-label": block.title, children: [
837
+ /* @__PURE__ */ jsx5("div", { style: titleStyle, children: block.title }),
838
+ block.summary && /* @__PURE__ */ jsxs3(Fragment2, { children: [
839
+ /* @__PURE__ */ jsxs3(
840
+ "button",
841
+ {
842
+ type: "button",
843
+ style: disclosureBtn,
844
+ onClick: () => setShowSummary((s) => !s),
845
+ "aria-expanded": showSummary,
846
+ children: [
847
+ showSummary ? "\u25BE" : "\u25B8",
848
+ " ",
849
+ t("action_what_will_happen")
850
+ ]
851
+ }
852
+ ),
853
+ showSummary && /* @__PURE__ */ jsx5("div", { style: summaryStyle, children: block.summary })
854
+ ] }),
855
+ /* @__PURE__ */ jsxs3("div", { style: buttonRow, children: [
856
+ /* @__PURE__ */ jsx5(
857
+ "button",
858
+ {
859
+ type: "button",
860
+ style: baseBtn("ghost"),
861
+ disabled: chosen !== null,
862
+ onClick: () => handle("cancel"),
863
+ children: chosen === "cancel" && showSpinner ? /* @__PURE__ */ jsx5(Spinner, { color: "#333" }) : t("action_cancel")
864
+ }
865
+ ),
866
+ /* @__PURE__ */ jsx5(
867
+ "button",
868
+ {
869
+ type: "button",
870
+ style: baseBtn("primary"),
871
+ disabled: chosen !== null,
872
+ onClick: () => handle("approve"),
873
+ children: chosen === "approve" && showSpinner ? /* @__PURE__ */ jsx5(Spinner, { color: "#fff" }) : t("action_approve")
874
+ }
875
+ )
876
+ ] }),
877
+ error && /* @__PURE__ */ jsx5("div", { role: "alert", style: { color: "#b91c1c", fontSize: 12 }, children: error })
878
+ ] });
879
+ }
880
+ function Spinner({ color }) {
881
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
882
+ /* @__PURE__ */ jsx5("style", { children: `@keyframes ch-spin { to { transform: rotate(360deg); } }` }),
883
+ /* @__PURE__ */ jsx5(
884
+ "span",
885
+ {
886
+ "aria-hidden": "true",
887
+ style: {
888
+ width: 14,
889
+ height: 14,
890
+ borderRadius: "50%",
891
+ border: `2px solid ${color}`,
892
+ borderTopColor: "transparent",
893
+ animation: "ch-spin 0.8s linear infinite",
894
+ display: "inline-block"
895
+ }
896
+ }
897
+ )
898
+ ] });
899
+ }
900
+
901
+ // src/components/chat-messages.tsx
902
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
903
+ function MessageStatusPill({
904
+ status,
905
+ t
906
+ }) {
907
+ const failed = status === "failed";
908
+ const containerStyle = {
909
+ display: "flex",
910
+ alignItems: "center",
911
+ gap: 4,
912
+ marginTop: 2,
913
+ fontSize: 11,
914
+ color: failed ? "#b91c1c" : "#888",
915
+ alignSelf: "flex-end"
916
+ };
917
+ const labelKey = status === "sending" ? "status_sending" : status === "sent" ? "status_sent" : "status_failed";
918
+ return /* @__PURE__ */ jsxs4("div", { style: containerStyle, "aria-live": "polite", children: [
919
+ status === "sending" && /* @__PURE__ */ jsx6(ClockIcon, {}),
920
+ status === "sent" && /* @__PURE__ */ jsx6(CheckIcon, {}),
921
+ status === "failed" && /* @__PURE__ */ jsx6(ExclamationIcon, {}),
922
+ /* @__PURE__ */ jsx6("span", { children: t(labelKey) })
923
+ ] });
924
+ }
925
+ function ClockIcon() {
926
+ return /* @__PURE__ */ jsxs4(
927
+ "svg",
928
+ {
929
+ width: "11",
930
+ height: "11",
931
+ viewBox: "0 0 24 24",
932
+ fill: "none",
933
+ stroke: "currentColor",
934
+ strokeWidth: "2",
935
+ strokeLinecap: "round",
936
+ strokeLinejoin: "round",
937
+ "aria-hidden": "true",
938
+ children: [
939
+ /* @__PURE__ */ jsx6("circle", { cx: "12", cy: "12", r: "10" }),
940
+ /* @__PURE__ */ jsx6("polyline", { points: "12 6 12 12 16 14" })
941
+ ]
942
+ }
943
+ );
944
+ }
945
+ function CheckIcon() {
946
+ return /* @__PURE__ */ jsx6(
947
+ "svg",
948
+ {
949
+ width: "11",
950
+ height: "11",
951
+ viewBox: "0 0 24 24",
952
+ fill: "none",
953
+ stroke: "currentColor",
954
+ strokeWidth: "2.5",
955
+ strokeLinecap: "round",
956
+ strokeLinejoin: "round",
957
+ "aria-hidden": "true",
958
+ children: /* @__PURE__ */ jsx6("polyline", { points: "20 6 9 17 4 12" })
959
+ }
960
+ );
961
+ }
962
+ function ExclamationIcon() {
963
+ return /* @__PURE__ */ jsxs4(
964
+ "svg",
965
+ {
966
+ width: "11",
967
+ height: "11",
968
+ viewBox: "0 0 24 24",
969
+ fill: "none",
970
+ stroke: "currentColor",
971
+ strokeWidth: "2",
972
+ strokeLinecap: "round",
973
+ strokeLinejoin: "round",
974
+ "aria-hidden": "true",
975
+ children: [
976
+ /* @__PURE__ */ jsx6("circle", { cx: "12", cy: "12", r: "10" }),
977
+ /* @__PURE__ */ jsx6("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
978
+ /* @__PURE__ */ jsx6("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
979
+ ]
980
+ }
981
+ );
982
+ }
374
983
  function MessageRatingButtons({
375
984
  messageId,
376
985
  onRate,
@@ -378,7 +987,7 @@ function MessageRatingButtons({
378
987
  t,
379
988
  reduced
380
989
  }) {
381
- const [rated, setRated] = useState4(null);
990
+ const [rated, setRated] = useState5(null);
382
991
  const handleRate = (rating) => {
383
992
  setRated(rating);
384
993
  onRate(messageId, rating);
@@ -396,15 +1005,16 @@ function MessageRatingButtons({
396
1005
  transition: reduced ? "color 0.15s" : "all 0.15s",
397
1006
  transform: isRated && !reduced ? "scale(1.15)" : "scale(1)"
398
1007
  });
399
- return /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 4, marginTop: 4 }, children: [
400
- /* @__PURE__ */ jsx4(
1008
+ return /* @__PURE__ */ jsxs4("div", { style: { display: "flex", gap: 4, marginTop: 4 }, children: [
1009
+ /* @__PURE__ */ jsx6(
401
1010
  "button",
402
1011
  {
403
1012
  onClick: () => handleRate("positive"),
404
1013
  disabled: rated !== null,
405
1014
  style: buttonStyle(rated === "positive"),
406
1015
  title: t("helpful"),
407
- children: /* @__PURE__ */ jsxs2(
1016
+ "aria-label": t("helpful"),
1017
+ children: /* @__PURE__ */ jsxs4(
408
1018
  "svg",
409
1019
  {
410
1020
  width: "14",
@@ -416,21 +1026,22 @@ function MessageRatingButtons({
416
1026
  strokeLinecap: "round",
417
1027
  strokeLinejoin: "round",
418
1028
  children: [
419
- /* @__PURE__ */ jsx4("path", { d: "M7 10v12" }),
420
- /* @__PURE__ */ jsx4("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" })
1029
+ /* @__PURE__ */ jsx6("path", { d: "M7 10v12" }),
1030
+ /* @__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" })
421
1031
  ]
422
1032
  }
423
1033
  )
424
1034
  }
425
1035
  ),
426
- /* @__PURE__ */ jsx4(
1036
+ /* @__PURE__ */ jsx6(
427
1037
  "button",
428
1038
  {
429
1039
  onClick: () => handleRate("negative"),
430
1040
  disabled: rated !== null,
431
1041
  style: buttonStyle(rated === "negative"),
432
1042
  title: t("not_helpful"),
433
- children: /* @__PURE__ */ jsxs2(
1043
+ "aria-label": t("not_helpful"),
1044
+ children: /* @__PURE__ */ jsxs4(
434
1045
  "svg",
435
1046
  {
436
1047
  width: "14",
@@ -442,8 +1053,8 @@ function MessageRatingButtons({
442
1053
  strokeLinecap: "round",
443
1054
  strokeLinejoin: "round",
444
1055
  children: [
445
- /* @__PURE__ */ jsx4("path", { d: "M17 14V2" }),
446
- /* @__PURE__ */ jsx4("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" })
1056
+ /* @__PURE__ */ jsx6("path", { d: "M17 14V2" }),
1057
+ /* @__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" })
447
1058
  ]
448
1059
  }
449
1060
  )
@@ -457,12 +1068,14 @@ function AnimatedMessage({
457
1068
  animate,
458
1069
  reduced
459
1070
  }) {
460
- const [visible, setVisible] = useState4(!animate);
1071
+ const [visible, setVisible] = useState5(!animate);
461
1072
  useEffect5(() => {
462
- if (animate && !reduced) {
463
- const id = requestAnimationFrame(() => setVisible(true));
464
- return () => cancelAnimationFrame(id);
1073
+ if (!animate || reduced) {
1074
+ setVisible(true);
1075
+ return;
465
1076
  }
1077
+ const id = requestAnimationFrame(() => setVisible(true));
1078
+ return () => cancelAnimationFrame(id);
466
1079
  }, [animate, reduced]);
467
1080
  const style = {
468
1081
  alignSelf: isUser ? "flex-end" : "flex-start",
@@ -471,16 +1084,123 @@ function AnimatedMessage({
471
1084
  transform: visible ? "translateX(0)" : `translateX(${isUser ? "12px" : "-12px"})`,
472
1085
  transition: animate && !reduced ? "opacity 0.2s ease, transform 0.2s ease" : "none"
473
1086
  };
474
- return /* @__PURE__ */ jsx4("div", { style, children });
1087
+ return /* @__PURE__ */ jsx6("div", { style, children });
1088
+ }
1089
+ function ChipRow({
1090
+ options,
1091
+ onSelect,
1092
+ primaryColor,
1093
+ reduced
1094
+ }) {
1095
+ const chip = {
1096
+ background: "none",
1097
+ border: "1px solid #e0e0e0",
1098
+ borderRadius: 20,
1099
+ padding: "6px 12px",
1100
+ fontSize: 13,
1101
+ color: "#333",
1102
+ cursor: "pointer",
1103
+ textAlign: "left",
1104
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
1105
+ transition: reduced ? "none" : "border-color 0.15s, background 0.15s"
1106
+ };
1107
+ return /* @__PURE__ */ jsx6(
1108
+ "div",
1109
+ {
1110
+ style: {
1111
+ display: "flex",
1112
+ flexWrap: "wrap",
1113
+ gap: 6,
1114
+ marginTop: 6
1115
+ },
1116
+ children: options.map((text) => /* @__PURE__ */ jsx6(
1117
+ "button",
1118
+ {
1119
+ style: chip,
1120
+ onClick: () => onSelect(text),
1121
+ onMouseEnter: (e) => {
1122
+ e.currentTarget.style.borderColor = primaryColor;
1123
+ e.currentTarget.style.background = `${primaryColor}08`;
1124
+ },
1125
+ onMouseLeave: (e) => {
1126
+ e.currentTarget.style.borderColor = "#e0e0e0";
1127
+ e.currentTarget.style.background = "none";
1128
+ },
1129
+ children: text
1130
+ },
1131
+ text
1132
+ ))
1133
+ }
1134
+ );
1135
+ }
1136
+ function BlockRenderer({
1137
+ block,
1138
+ onSend,
1139
+ onApproveAction,
1140
+ onCancelAction,
1141
+ primaryColor,
1142
+ reduced,
1143
+ t
1144
+ }) {
1145
+ switch (block.type) {
1146
+ case "quick_replies":
1147
+ return /* @__PURE__ */ jsx6(
1148
+ ChipRow,
1149
+ {
1150
+ options: block.options,
1151
+ onSelect: onSend,
1152
+ primaryColor,
1153
+ reduced
1154
+ }
1155
+ );
1156
+ case "action_confirmation":
1157
+ return /* @__PURE__ */ jsx6(
1158
+ ActionConfirmationCard,
1159
+ {
1160
+ block,
1161
+ primaryColor,
1162
+ t,
1163
+ onApprove: onApproveAction,
1164
+ onCancel: onCancelAction
1165
+ }
1166
+ );
1167
+ }
1168
+ }
1169
+ function StreamingCursor({ reduced }) {
1170
+ return /* @__PURE__ */ jsxs4(Fragment3, { children: [
1171
+ /* @__PURE__ */ jsx6("style", { children: `@keyframes ch-caret-blink {
1172
+ 0%, 50% { opacity: 1; }
1173
+ 50.01%, 100% { opacity: 0; }
1174
+ }` }),
1175
+ /* @__PURE__ */ jsx6(
1176
+ "span",
1177
+ {
1178
+ "aria-hidden": "true",
1179
+ style: {
1180
+ display: "inline-block",
1181
+ width: 2,
1182
+ height: "1em",
1183
+ marginLeft: 2,
1184
+ verticalAlign: "text-bottom",
1185
+ background: "#777",
1186
+ animation: reduced ? "none" : "ch-caret-blink 1s step-start infinite"
1187
+ }
1188
+ }
1189
+ )
1190
+ ] });
475
1191
  }
476
1192
  function Message({
477
1193
  message,
478
1194
  config,
479
1195
  onRate,
1196
+ onSend,
1197
+ onApproveAction,
1198
+ onCancelAction,
480
1199
  hasConversation,
481
1200
  t,
482
1201
  animate,
483
- reduced
1202
+ reduced,
1203
+ showSuggestions
484
1204
  }) {
485
1205
  const isUser = message.role === "user";
486
1206
  const bubbleStyle = {
@@ -499,9 +1219,39 @@ function Message({
499
1219
  borderBottomLeftRadius: 4
500
1220
  }
501
1221
  };
502
- return /* @__PURE__ */ jsxs2(AnimatedMessage, { isUser, animate, reduced, children: [
503
- /* @__PURE__ */ jsx4("div", { style: bubbleStyle, children: message.content }),
504
- message.role === "bot" && message.id && hasConversation && /* @__PURE__ */ jsx4(
1222
+ const linkColor = isUser ? "#ffffff" : config.primaryColor;
1223
+ return /* @__PURE__ */ jsxs4(AnimatedMessage, { isUser, animate, reduced, children: [
1224
+ /* @__PURE__ */ jsx6("div", { style: bubbleStyle, children: isUser ? message.content : /* @__PURE__ */ jsxs4(Fragment3, { children: [
1225
+ renderMarkdown(message.content, {
1226
+ sources: message.sources,
1227
+ linkColor
1228
+ }),
1229
+ message.streaming && /* @__PURE__ */ jsx6(StreamingCursor, { reduced })
1230
+ ] }) }),
1231
+ isUser && message.status && /* @__PURE__ */ jsx6(MessageStatusPill, { status: message.status, t }),
1232
+ !isUser && message.blocks?.map((block, i) => /* @__PURE__ */ jsx6(
1233
+ BlockRenderer,
1234
+ {
1235
+ block,
1236
+ onSend,
1237
+ onApproveAction,
1238
+ onCancelAction,
1239
+ primaryColor: config.primaryColor,
1240
+ reduced,
1241
+ t
1242
+ },
1243
+ i
1244
+ )),
1245
+ !isUser && showSuggestions && message.suggestions?.length ? /* @__PURE__ */ jsx6(
1246
+ ChipRow,
1247
+ {
1248
+ options: message.suggestions,
1249
+ onSelect: onSend,
1250
+ primaryColor: config.primaryColor,
1251
+ reduced
1252
+ }
1253
+ ) : null,
1254
+ message.role === "bot" && message.id && hasConversation && !message.streaming && !message.blocks?.some((b) => b.type === "action_confirmation") && /* @__PURE__ */ jsx6(
505
1255
  MessageRatingButtons,
506
1256
  {
507
1257
  messageId: message.id,
@@ -521,12 +1271,12 @@ function TypingDots({ reduced }) {
521
1271
  background: "#999",
522
1272
  animation: reduced ? "none" : `ch-dot-pulse 1.2s ease-in-out ${delay}s infinite`
523
1273
  });
524
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
525
- /* @__PURE__ */ jsx4("style", { children: `@keyframes ch-dot-pulse {
1274
+ return /* @__PURE__ */ jsxs4(Fragment3, { children: [
1275
+ /* @__PURE__ */ jsx6("style", { children: `@keyframes ch-dot-pulse {
526
1276
  0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
527
1277
  40% { opacity: 1; transform: scale(1); }
528
1278
  }` }),
529
- /* @__PURE__ */ jsxs2(
1279
+ /* @__PURE__ */ jsxs4(
530
1280
  "div",
531
1281
  {
532
1282
  style: {
@@ -540,16 +1290,27 @@ function TypingDots({ reduced }) {
540
1290
  alignItems: "center"
541
1291
  },
542
1292
  children: [
543
- /* @__PURE__ */ jsx4("div", { style: dotStyle(0) }),
544
- /* @__PURE__ */ jsx4("div", { style: dotStyle(0.2) }),
545
- /* @__PURE__ */ jsx4("div", { style: dotStyle(0.4) })
1293
+ /* @__PURE__ */ jsx6("div", { style: dotStyle(0) }),
1294
+ /* @__PURE__ */ jsx6("div", { style: dotStyle(0.2) }),
1295
+ /* @__PURE__ */ jsx6("div", { style: dotStyle(0.4) })
546
1296
  ]
547
1297
  }
548
1298
  )
549
1299
  ] });
550
1300
  }
551
1301
  function ChatMessages() {
552
- const { messages, isLoading, error, config, conversationId, rateMessage, t } = useChat();
1302
+ const {
1303
+ messages,
1304
+ isLoading,
1305
+ error,
1306
+ config,
1307
+ conversationId,
1308
+ rateMessage,
1309
+ sendMessage,
1310
+ approveAction,
1311
+ cancelAction,
1312
+ t
1313
+ } = useChat();
553
1314
  const reduced = useReducedMotion();
554
1315
  const messagesEndRef = useRef3(null);
555
1316
  const isFirstRender = useRef3(true);
@@ -566,6 +1327,13 @@ function ChatMessages() {
566
1327
  useEffect5(() => {
567
1328
  prevMessageCount.current = messages.length;
568
1329
  }, [messages.length]);
1330
+ let lastBotIndex = -1;
1331
+ for (let i = messages.length - 1; i >= 0; i--) {
1332
+ if (messages[i].role === "bot") {
1333
+ lastBotIndex = i;
1334
+ break;
1335
+ }
1336
+ }
569
1337
  const containerStyle = {
570
1338
  flex: 1,
571
1339
  overflowY: "auto",
@@ -574,24 +1342,31 @@ function ChatMessages() {
574
1342
  flexDirection: "column",
575
1343
  gap: 12
576
1344
  };
577
- return /* @__PURE__ */ jsxs2("div", { style: containerStyle, children: [
578
- messages.map((msg, i) => /* @__PURE__ */ jsx4(
1345
+ const lastMsg = messages[messages.length - 1];
1346
+ const waitingForFirstToken = isLoading && (lastMsg?.role !== "bot" || lastMsg.streaming !== true);
1347
+ return /* @__PURE__ */ jsxs4("div", { style: containerStyle, children: [
1348
+ messages.map((msg, i) => /* @__PURE__ */ jsx6(
579
1349
  Message,
580
1350
  {
581
1351
  message: msg,
582
1352
  config,
583
1353
  onRate: rateMessage,
1354
+ onSend: sendMessage,
1355
+ onApproveAction: approveAction,
1356
+ onCancelAction: cancelAction,
584
1357
  hasConversation: conversationId !== null,
585
1358
  t,
586
1359
  animate: i >= newStartIndex,
587
- reduced
1360
+ reduced,
1361
+ showSuggestions: i === lastBotIndex && !isLoading && msg.streaming !== true
588
1362
  },
589
1363
  i
590
1364
  )),
591
- isLoading && /* @__PURE__ */ jsx4(TypingDots, { reduced }),
592
- error && /* @__PURE__ */ jsx4(
1365
+ waitingForFirstToken && /* @__PURE__ */ jsx6(TypingDots, { reduced }),
1366
+ error && /* @__PURE__ */ jsx6(
593
1367
  "div",
594
1368
  {
1369
+ role: "alert",
595
1370
  style: {
596
1371
  alignSelf: "flex-start",
597
1372
  padding: "10px 14px",
@@ -604,12 +1379,12 @@ function ChatMessages() {
604
1379
  children: error
605
1380
  }
606
1381
  ),
607
- /* @__PURE__ */ jsx4("div", { ref: messagesEndRef })
1382
+ /* @__PURE__ */ jsx6("div", { ref: messagesEndRef })
608
1383
  ] });
609
1384
  }
610
1385
 
611
1386
  // src/components/chat-suggestions.tsx
612
- import { jsx as jsx5 } from "react/jsx-runtime";
1387
+ import { jsx as jsx7 } from "react/jsx-runtime";
613
1388
  function ChatSuggestions() {
614
1389
  const { messages, isLoading, config, sendMessage } = useChat();
615
1390
  const reduced = useReducedMotion();
@@ -636,7 +1411,7 @@ function ChatSuggestions() {
636
1411
  fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
637
1412
  transition: reduced ? "none" : "border-color 0.15s, background 0.15s"
638
1413
  };
639
- return /* @__PURE__ */ jsx5("div", { style: containerStyle, children: config.suggestedMessages.map((text) => /* @__PURE__ */ jsx5(
1414
+ return /* @__PURE__ */ jsx7("div", { style: containerStyle, children: config.suggestedMessages.map((text) => /* @__PURE__ */ jsx7(
640
1415
  "button",
641
1416
  {
642
1417
  style: chipStyle,
@@ -656,15 +1431,82 @@ function ChatSuggestions() {
656
1431
  }
657
1432
 
658
1433
  // src/components/chat-input.tsx
659
- import { useState as useState5 } from "react";
660
- import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1434
+ import {
1435
+ useEffect as useEffect6,
1436
+ useState as useState6
1437
+ } from "react";
1438
+ import {
1439
+ ScreenshotCancelled,
1440
+ canCaptureScreenshot,
1441
+ captureScreenshot
1442
+ } from "@customerhero/js";
1443
+ import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1444
+ var MAX_ATTACHMENTS = 3;
661
1445
  function ChatInput() {
662
- const { sendMessage, isLoading, config, t } = useChat();
1446
+ const { sendMessage, uploadAttachment, isLoading, config, t } = useChat();
663
1447
  const reduced = useReducedMotion();
664
- const [value, setValue] = useState5("");
1448
+ const [value, setValue] = useState6("");
1449
+ const [attachments, setAttachments] = useState6([]);
1450
+ const [captureSupported, setCaptureSupported] = useState6(false);
1451
+ useEffect6(() => {
1452
+ setCaptureSupported(canCaptureScreenshot());
1453
+ }, []);
1454
+ useEffect6(() => {
1455
+ return () => {
1456
+ for (const a of attachments) URL.revokeObjectURL(a.previewUrl);
1457
+ };
1458
+ }, []);
1459
+ const updateAttachment = (id, patch) => {
1460
+ setAttachments(
1461
+ (current) => current.map(
1462
+ (a) => a.id === id ? { ...a, ...patch } : a
1463
+ )
1464
+ );
1465
+ };
1466
+ const startUpload = async (blob) => {
1467
+ if (attachments.length >= MAX_ATTACHMENTS) return;
1468
+ const id = `att_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1469
+ const previewUrl = URL.createObjectURL(blob);
1470
+ setAttachments((current) => [
1471
+ ...current,
1472
+ { id, status: "uploading", previewUrl, blob }
1473
+ ]);
1474
+ try {
1475
+ const { attachmentToken } = await uploadAttachment(blob);
1476
+ updateAttachment(id, {
1477
+ status: "ready",
1478
+ token: attachmentToken
1479
+ });
1480
+ } catch {
1481
+ updateAttachment(id, { status: "error" });
1482
+ }
1483
+ };
1484
+ const handleCapture = async () => {
1485
+ try {
1486
+ const blob = await captureScreenshot();
1487
+ await startUpload(blob);
1488
+ } catch (e) {
1489
+ if (e instanceof ScreenshotCancelled) return;
1490
+ }
1491
+ };
1492
+ const handleRemove = (id) => {
1493
+ setAttachments((current) => {
1494
+ const target = current.find((a) => a.id === id);
1495
+ if (target) URL.revokeObjectURL(target.previewUrl);
1496
+ return current.filter((a) => a.id !== id);
1497
+ });
1498
+ };
1499
+ const readyTokens = attachments.filter(
1500
+ (a) => a.status === "ready"
1501
+ ).map((a) => a.token);
665
1502
  const handleSend = () => {
666
1503
  if (!value.trim() || isLoading) return;
667
- sendMessage(value);
1504
+ sendMessage(
1505
+ value,
1506
+ readyTokens.length > 0 ? { attachmentTokens: readyTokens } : void 0
1507
+ );
1508
+ for (const a of attachments) URL.revokeObjectURL(a.previewUrl);
1509
+ setAttachments([]);
668
1510
  setValue("");
669
1511
  };
670
1512
  const handleKeyDown = (e) => {
@@ -676,6 +1518,11 @@ function ChatInput() {
676
1518
  const containerStyle = {
677
1519
  padding: "12px 16px",
678
1520
  borderTop: "1px solid #eee",
1521
+ display: "flex",
1522
+ flexDirection: "column",
1523
+ gap: 8
1524
+ };
1525
+ const rowStyle = {
679
1526
  display: "flex",
680
1527
  alignItems: "center",
681
1528
  gap: 8
@@ -691,7 +1538,7 @@ function ChatInput() {
691
1538
  color: config.textColor,
692
1539
  fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
693
1540
  };
694
- const buttonStyle = {
1541
+ const sendButtonStyle = {
695
1542
  width: 36,
696
1543
  height: 36,
697
1544
  borderRadius: "50%",
@@ -707,57 +1554,210 @@ function ChatInput() {
707
1554
  flexShrink: 0,
708
1555
  transform: "scale(1)"
709
1556
  };
710
- return /* @__PURE__ */ jsxs3("div", { style: containerStyle, children: [
711
- /* @__PURE__ */ jsx6(
712
- "input",
1557
+ const iconButtonStyle = (disabled) => ({
1558
+ width: 36,
1559
+ height: 36,
1560
+ borderRadius: "50%",
1561
+ background: "transparent",
1562
+ border: "none",
1563
+ color: disabled ? "#ccc" : "#666",
1564
+ cursor: disabled ? "not-allowed" : "pointer",
1565
+ display: "flex",
1566
+ alignItems: "center",
1567
+ justifyContent: "center",
1568
+ flexShrink: 0,
1569
+ padding: 0
1570
+ });
1571
+ const captureDisabled = attachments.length >= MAX_ATTACHMENTS || isLoading;
1572
+ return /* @__PURE__ */ jsxs5("div", { style: containerStyle, children: [
1573
+ attachments.length > 0 && /* @__PURE__ */ jsx8(
1574
+ "div",
713
1575
  {
714
- type: "text",
715
- value,
716
- onChange: (e) => setValue(e.target.value),
717
- onKeyDown: handleKeyDown,
718
- placeholder: config.placeholderText,
719
- style: inputStyle,
720
- disabled: isLoading
1576
+ style: { display: "flex", gap: 8, flexWrap: "wrap" },
1577
+ "aria-label": "Attachments",
1578
+ children: attachments.map((a) => /* @__PURE__ */ jsx8(
1579
+ Thumbnail,
1580
+ {
1581
+ attachment: a,
1582
+ onRemove: () => handleRemove(a.id),
1583
+ t
1584
+ },
1585
+ a.id
1586
+ ))
721
1587
  }
722
1588
  ),
723
- /* @__PURE__ */ jsx6(
1589
+ /* @__PURE__ */ jsxs5("div", { style: rowStyle, children: [
1590
+ captureSupported && /* @__PURE__ */ jsx8(
1591
+ "button",
1592
+ {
1593
+ type: "button",
1594
+ onClick: handleCapture,
1595
+ disabled: captureDisabled,
1596
+ style: iconButtonStyle(captureDisabled),
1597
+ "aria-label": t("screenshot_capture"),
1598
+ title: t("screenshot_capture"),
1599
+ children: /* @__PURE__ */ jsx8(CameraIcon, {})
1600
+ }
1601
+ ),
1602
+ /* @__PURE__ */ jsx8(
1603
+ "input",
1604
+ {
1605
+ type: "text",
1606
+ value,
1607
+ onChange: (e) => setValue(e.target.value),
1608
+ onKeyDown: handleKeyDown,
1609
+ placeholder: config.placeholderText,
1610
+ style: inputStyle,
1611
+ disabled: isLoading
1612
+ }
1613
+ ),
1614
+ /* @__PURE__ */ jsx8(
1615
+ "button",
1616
+ {
1617
+ onClick: handleSend,
1618
+ disabled: isLoading || !value.trim(),
1619
+ style: sendButtonStyle,
1620
+ "aria-label": t("send_message"),
1621
+ onMouseEnter: (e) => {
1622
+ if (!reduced && !isLoading)
1623
+ e.currentTarget.style.transform = "scale(1.1)";
1624
+ },
1625
+ onMouseLeave: (e) => {
1626
+ if (!reduced) e.currentTarget.style.transform = "scale(1)";
1627
+ },
1628
+ children: /* @__PURE__ */ jsxs5(
1629
+ "svg",
1630
+ {
1631
+ width: "16",
1632
+ height: "16",
1633
+ viewBox: "0 0 24 24",
1634
+ fill: "none",
1635
+ stroke: "currentColor",
1636
+ strokeWidth: "2",
1637
+ strokeLinecap: "round",
1638
+ strokeLinejoin: "round",
1639
+ children: [
1640
+ /* @__PURE__ */ jsx8("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
1641
+ /* @__PURE__ */ jsx8("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
1642
+ ]
1643
+ }
1644
+ )
1645
+ }
1646
+ )
1647
+ ] })
1648
+ ] });
1649
+ }
1650
+ function Thumbnail({
1651
+ attachment,
1652
+ onRemove,
1653
+ t
1654
+ }) {
1655
+ const { status, previewUrl } = attachment;
1656
+ const wrap = {
1657
+ position: "relative",
1658
+ width: 56,
1659
+ height: 56,
1660
+ borderRadius: 8,
1661
+ overflow: "hidden",
1662
+ border: status === "error" ? "2px solid #b91c1c" : "1px solid #e0e0e0",
1663
+ background: "#f5f5f5"
1664
+ };
1665
+ const img = {
1666
+ width: "100%",
1667
+ height: "100%",
1668
+ objectFit: "cover"
1669
+ };
1670
+ const removeBtn = {
1671
+ position: "absolute",
1672
+ top: 2,
1673
+ right: 2,
1674
+ width: 18,
1675
+ height: 18,
1676
+ borderRadius: "50%",
1677
+ border: "none",
1678
+ background: "rgba(0,0,0,0.6)",
1679
+ color: "white",
1680
+ fontSize: 11,
1681
+ lineHeight: "18px",
1682
+ padding: 0,
1683
+ cursor: "pointer",
1684
+ textAlign: "center"
1685
+ };
1686
+ const overlay = {
1687
+ position: "absolute",
1688
+ inset: 0,
1689
+ background: "rgba(255,255,255,0.7)",
1690
+ display: "flex",
1691
+ alignItems: "center",
1692
+ justifyContent: "center"
1693
+ };
1694
+ return /* @__PURE__ */ jsxs5("div", { style: wrap, children: [
1695
+ /* @__PURE__ */ jsx8("img", { src: previewUrl, alt: "", style: img }),
1696
+ status === "uploading" && /* @__PURE__ */ jsx8("div", { style: overlay, "aria-label": "Uploading", children: /* @__PURE__ */ jsx8(Spinner2, {}) }),
1697
+ status === "error" && /* @__PURE__ */ jsx8(
1698
+ "div",
1699
+ {
1700
+ style: { ...overlay, background: "rgba(255,255,255,0.85)" },
1701
+ "aria-label": t("status_failed"),
1702
+ title: t("status_failed"),
1703
+ children: /* @__PURE__ */ jsx8("span", { style: { color: "#b91c1c", fontSize: 11, fontWeight: 600 }, children: "!" })
1704
+ }
1705
+ ),
1706
+ /* @__PURE__ */ jsx8(
724
1707
  "button",
725
1708
  {
726
- onClick: handleSend,
727
- disabled: isLoading || !value.trim(),
728
- style: buttonStyle,
729
- "aria-label": t("send_message"),
730
- onMouseEnter: (e) => {
731
- if (!reduced && !isLoading)
732
- e.currentTarget.style.transform = "scale(1.1)";
733
- },
734
- onMouseLeave: (e) => {
735
- if (!reduced) e.currentTarget.style.transform = "scale(1)";
736
- },
737
- children: /* @__PURE__ */ jsxs3(
738
- "svg",
739
- {
740
- width: "16",
741
- height: "16",
742
- viewBox: "0 0 24 24",
743
- fill: "none",
744
- stroke: "currentColor",
745
- strokeWidth: "2",
746
- strokeLinecap: "round",
747
- strokeLinejoin: "round",
748
- children: [
749
- /* @__PURE__ */ jsx6("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
750
- /* @__PURE__ */ jsx6("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
751
- ]
752
- }
753
- )
1709
+ type: "button",
1710
+ style: removeBtn,
1711
+ onClick: onRemove,
1712
+ "aria-label": t("attachment_remove"),
1713
+ children: "\xD7"
1714
+ }
1715
+ )
1716
+ ] });
1717
+ }
1718
+ function CameraIcon() {
1719
+ return /* @__PURE__ */ jsxs5(
1720
+ "svg",
1721
+ {
1722
+ width: "20",
1723
+ height: "20",
1724
+ viewBox: "0 0 24 24",
1725
+ fill: "none",
1726
+ stroke: "currentColor",
1727
+ strokeWidth: "2",
1728
+ strokeLinecap: "round",
1729
+ strokeLinejoin: "round",
1730
+ "aria-hidden": "true",
1731
+ children: [
1732
+ /* @__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" }),
1733
+ /* @__PURE__ */ jsx8("circle", { cx: "12", cy: "13", r: "4" })
1734
+ ]
1735
+ }
1736
+ );
1737
+ }
1738
+ function Spinner2() {
1739
+ return /* @__PURE__ */ jsxs5(Fragment4, { children: [
1740
+ /* @__PURE__ */ jsx8("style", { children: `@keyframes ch-att-spin { to { transform: rotate(360deg); } }` }),
1741
+ /* @__PURE__ */ jsx8(
1742
+ "span",
1743
+ {
1744
+ "aria-hidden": "true",
1745
+ style: {
1746
+ width: 16,
1747
+ height: 16,
1748
+ borderRadius: "50%",
1749
+ border: "2px solid #888",
1750
+ borderTopColor: "transparent",
1751
+ animation: "ch-att-spin 0.8s linear infinite",
1752
+ display: "inline-block"
1753
+ }
754
1754
  }
755
1755
  )
756
1756
  ] });
757
1757
  }
758
1758
 
759
1759
  // src/components/chat-window.tsx
760
- import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
1760
+ import { Fragment as Fragment5, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
761
1761
  function ConfigError({ message, title }) {
762
1762
  const errorStyle = {
763
1763
  flex: 1,
@@ -769,8 +1769,8 @@ function ConfigError({ message, title }) {
769
1769
  textAlign: "center",
770
1770
  gap: 8
771
1771
  };
772
- return /* @__PURE__ */ jsxs4("div", { style: errorStyle, children: [
773
- /* @__PURE__ */ jsxs4(
1772
+ return /* @__PURE__ */ jsxs6("div", { style: errorStyle, children: [
1773
+ /* @__PURE__ */ jsxs6(
774
1774
  "svg",
775
1775
  {
776
1776
  width: "32",
@@ -782,22 +1782,22 @@ function ConfigError({ message, title }) {
782
1782
  strokeLinecap: "round",
783
1783
  strokeLinejoin: "round",
784
1784
  children: [
785
- /* @__PURE__ */ jsx7("circle", { cx: "12", cy: "12", r: "10" }),
786
- /* @__PURE__ */ jsx7("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
787
- /* @__PURE__ */ jsx7("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
1785
+ /* @__PURE__ */ jsx9("circle", { cx: "12", cy: "12", r: "10" }),
1786
+ /* @__PURE__ */ jsx9("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
1787
+ /* @__PURE__ */ jsx9("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
788
1788
  ]
789
1789
  }
790
1790
  ),
791
- /* @__PURE__ */ jsx7("p", { style: { fontSize: 14, fontWeight: 500, color: "#333", margin: 0 }, children: title }),
792
- /* @__PURE__ */ jsx7("p", { style: { fontSize: 12, color: "#999", margin: 0 }, children: message })
1791
+ /* @__PURE__ */ jsx9("p", { style: { fontSize: 14, fontWeight: 500, color: "#333", margin: 0 }, children: title }),
1792
+ /* @__PURE__ */ jsx9("p", { style: { fontSize: 12, color: "#999", margin: 0 }, children: message })
793
1793
  ] });
794
1794
  }
795
1795
  function ChatWindow() {
796
- const { isOpen, config, configError, t } = useChat();
1796
+ const { isOpen, config, configError, t, isRtl } = useChat();
797
1797
  const reduced = useReducedMotion();
798
- const [visible, setVisible] = useState6(false);
799
- const [shouldRender, setShouldRender] = useState6(false);
800
- useEffect6(() => {
1798
+ const [visible, setVisible] = useState7(false);
1799
+ const [shouldRender, setShouldRender] = useState7(false);
1800
+ useEffect7(() => {
801
1801
  if (isOpen) {
802
1802
  setShouldRender(true);
803
1803
  requestAnimationFrame(() => {
@@ -814,10 +1814,11 @@ function ChatWindow() {
814
1814
  }
815
1815
  }, [isOpen, reduced]);
816
1816
  if (!shouldRender) return null;
1817
+ const effectivePosition = isRtl ? config.position === "bottom-right" ? "bottom-left" : "bottom-right" : config.position;
817
1818
  const style = {
818
1819
  position: "fixed",
819
1820
  bottom: 90,
820
- [config.position === "bottom-left" ? "left" : "right"]: 20,
1821
+ [effectivePosition === "bottom-left" ? "left" : "right"]: 20,
821
1822
  width: 380,
822
1823
  maxWidth: "calc(100vw - 40px)",
823
1824
  height: 520,
@@ -845,17 +1846,17 @@ function ChatWindow() {
845
1846
  textDecoration: "underline",
846
1847
  textUnderlineOffset: 2
847
1848
  };
848
- return /* @__PURE__ */ jsxs4("div", { style, children: [
849
- /* @__PURE__ */ jsx7(ChatHeader, {}),
850
- configError ? /* @__PURE__ */ jsx7(ConfigError, { title: t("unable_to_load"), message: configError }) : /* @__PURE__ */ jsxs4(Fragment2, { children: [
851
- /* @__PURE__ */ jsx7(ChatMessages, {}),
852
- /* @__PURE__ */ jsx7(ChatSuggestions, {}),
853
- /* @__PURE__ */ jsx7(ChatInput, {})
1849
+ return /* @__PURE__ */ jsxs6("div", { style, dir: isRtl ? "rtl" : "ltr", children: [
1850
+ /* @__PURE__ */ jsx9(ChatHeader, {}),
1851
+ configError ? /* @__PURE__ */ jsx9(ConfigError, { title: t("unable_to_load"), message: configError }) : /* @__PURE__ */ jsxs6(Fragment5, { children: [
1852
+ /* @__PURE__ */ jsx9(ChatMessages, {}),
1853
+ /* @__PURE__ */ jsx9(ChatSuggestions, {}),
1854
+ /* @__PURE__ */ jsx9(ChatInput, {})
854
1855
  ] }),
855
- /* @__PURE__ */ jsxs4("div", { style: poweredStyle, children: [
1856
+ /* @__PURE__ */ jsxs6("div", { style: poweredStyle, children: [
856
1857
  t("powered_by"),
857
1858
  " ",
858
- /* @__PURE__ */ jsx7(
1859
+ /* @__PURE__ */ jsx9(
859
1860
  "a",
860
1861
  {
861
1862
  href: "https://customerhero.app",
@@ -870,11 +1871,11 @@ function ChatWindow() {
870
1871
  }
871
1872
 
872
1873
  // src/components/chat-widget.tsx
873
- import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1874
+ import { Fragment as Fragment6, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
874
1875
  function ChatWidgetInner({ identity }) {
875
1876
  const client = useCustomerHeroClient();
876
1877
  const prevIdentityRef = useRef4(void 0);
877
- useEffect7(() => {
1878
+ useEffect8(() => {
878
1879
  const key = identity ? JSON.stringify(identity) : void 0;
879
1880
  if (key !== prevIdentityRef.current) {
880
1881
  prevIdentityRef.current = key;
@@ -883,14 +1884,16 @@ function ChatWidgetInner({ identity }) {
883
1884
  }
884
1885
  }
885
1886
  }, [identity, client]);
886
- return /* @__PURE__ */ jsxs5(Fragment3, { children: [
887
- /* @__PURE__ */ jsx8(ChatBubble, {}),
888
- /* @__PURE__ */ jsx8(ChatWindow, {})
1887
+ return /* @__PURE__ */ jsxs7(Fragment6, { children: [
1888
+ /* @__PURE__ */ jsx10(ChatBubble, {}),
1889
+ /* @__PURE__ */ jsx10(ChatWindow, {})
889
1890
  ] });
890
1891
  }
891
1892
  function ChatWidget({ identity, ...config }) {
892
- return /* @__PURE__ */ jsx8(CustomerHeroProvider, { ...config, children: /* @__PURE__ */ jsx8(ChatWidgetInner, { identity }) });
1893
+ return /* @__PURE__ */ jsx10(CustomerHeroProvider, { ...config, children: /* @__PURE__ */ jsx10(ChatWidgetInner, { identity }) });
893
1894
  }
894
1895
  export {
895
- ChatWidget
1896
+ ActionConfirmationCard,
1897
+ ChatWidget,
1898
+ useChat
896
1899
  };