@customerhero/react 2.0.0 → 2.1.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.
Files changed (3) hide show
  1. package/dist/index.cjs +531 -107
  2. package/dist/index.js +535 -109
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components/chat-widget.tsx
2
- import { useEffect as useEffect8, useRef as useRef4 } from "react";
2
+ import { useEffect as useEffect8, useRef as useRef5 } from "react";
3
3
 
4
4
  // src/context.tsx
5
5
  import {
@@ -36,9 +36,6 @@ function useCustomerHeroClient() {
36
36
  return client;
37
37
  }
38
38
 
39
- // src/components/chat-bubble.tsx
40
- import { useEffect as useEffect3, useState as useState2 } from "react";
41
-
42
39
  // src/use-chat.ts
43
40
  import { useCallback, useSyncExternalStore } from "react";
44
41
  function useChat() {
@@ -104,6 +101,9 @@ function useChat() {
104
101
  };
105
102
  }
106
103
 
104
+ // src/components/chat-bubble.tsx
105
+ import { useEffect as useEffect3, useState as useState2 } from "react";
106
+
107
107
  // src/use-reduced-motion.ts
108
108
  import { useEffect as useEffect2, useState } from "react";
109
109
  function useReducedMotion() {
@@ -711,9 +711,11 @@ function renderMarkdown(source, opts = {}) {
711
711
  style: {
712
712
  margin: "0 0 8px",
713
713
  paddingLeft: 20,
714
- lineHeight: 1.5
714
+ lineHeight: 1.5,
715
+ listStyleType: "disc",
716
+ listStylePosition: "outside"
715
717
  },
716
- children: block.lines.map((l, i) => /* @__PURE__ */ jsx4("li", { children: renderInline(l) }, i))
718
+ children: block.lines.map((l, i) => /* @__PURE__ */ jsx4("li", { style: { display: "list-item" }, children: renderInline(l) }, i))
717
719
  },
718
720
  key
719
721
  );
@@ -724,9 +726,11 @@ function renderMarkdown(source, opts = {}) {
724
726
  style: {
725
727
  margin: "0 0 8px",
726
728
  paddingLeft: 22,
727
- lineHeight: 1.5
729
+ lineHeight: 1.5,
730
+ listStyleType: "decimal",
731
+ listStylePosition: "outside"
728
732
  },
729
- children: block.lines.map((l, i) => /* @__PURE__ */ jsx4("li", { children: renderInline(l) }, i))
733
+ children: block.lines.map((l, i) => /* @__PURE__ */ jsx4("li", { style: { display: "list-item" }, children: renderInline(l) }, i))
730
734
  },
731
735
  key
732
736
  );
@@ -929,11 +933,11 @@ function MessageStatusPill({
929
933
  const containerStyle = {
930
934
  display: "flex",
931
935
  alignItems: "center",
936
+ justifyContent: "flex-end",
932
937
  gap: 4,
933
938
  marginTop: 2,
934
939
  fontSize: 11,
935
- color: failed ? "#b91c1c" : "#888",
936
- alignSelf: "flex-end"
940
+ color: failed ? "#b91c1c" : "#888"
937
941
  };
938
942
  const labelKey = status === "sending" ? "status_sending" : status === "sent" ? "status_sent" : "status_failed";
939
943
  return /* @__PURE__ */ jsxs4("div", { style: containerStyle, "aria-live": "polite", children: [
@@ -1242,13 +1246,20 @@ function Message({
1242
1246
  };
1243
1247
  const linkColor = isUser ? "#ffffff" : config.primaryColor;
1244
1248
  return /* @__PURE__ */ jsxs4(AnimatedMessage, { isUser, animate, reduced, children: [
1245
- /* @__PURE__ */ jsx6("div", { style: bubbleStyle, children: isUser ? message.content : /* @__PURE__ */ jsxs4(Fragment3, { children: [
1246
- renderMarkdown(message.content, {
1247
- sources: message.sources,
1248
- linkColor
1249
- }),
1250
- message.streaming && /* @__PURE__ */ jsx6(StreamingCursor, { reduced })
1251
- ] }) }),
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
+ ),
1252
1263
  isUser && message.status && /* @__PURE__ */ jsx6(MessageStatusPill, { status: message.status, t }),
1253
1264
  !isUser && message.blocks?.map((block, i) => /* @__PURE__ */ jsx6(
1254
1265
  BlockRenderer,
@@ -1333,16 +1344,51 @@ function ChatMessages() {
1333
1344
  t
1334
1345
  } = useChat();
1335
1346
  const reduced = useReducedMotion();
1347
+ const containerRef = useRef3(null);
1336
1348
  const messagesEndRef = useRef3(null);
1337
1349
  const isFirstRender = useRef3(true);
1338
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
+ };
1339
1369
  useEffect5(() => {
1340
- if (messagesEndRef.current) {
1341
- messagesEndRef.current.scrollIntoView({
1342
- behavior: isFirstRender.current || reduced ? "auto" : "smooth"
1343
- });
1344
- isFirstRender.current = false;
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;
1345
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;
1346
1392
  }, [messages, isLoading, reduced]);
1347
1393
  const newStartIndex = isFirstRender.current ? messages.length : prevMessageCount.current;
1348
1394
  useEffect5(() => {
@@ -1365,7 +1411,7 @@ function ChatMessages() {
1365
1411
  };
1366
1412
  const lastMsg = messages[messages.length - 1];
1367
1413
  const waitingForFirstToken = isLoading && (lastMsg?.role !== "bot" || lastMsg.streaming !== true);
1368
- return /* @__PURE__ */ jsxs4("div", { style: containerStyle, children: [
1414
+ return /* @__PURE__ */ jsxs4("div", { ref: containerRef, style: containerStyle, onScroll: handleScroll, children: [
1369
1415
  messages.map((msg, i) => /* @__PURE__ */ jsx6(
1370
1416
  Message,
1371
1417
  {
@@ -1454,6 +1500,8 @@ function ChatSuggestions() {
1454
1500
  // src/components/chat-input.tsx
1455
1501
  import {
1456
1502
  useEffect as useEffect6,
1503
+ useLayoutEffect,
1504
+ useRef as useRef4,
1457
1505
  useState as useState6
1458
1506
  } from "react";
1459
1507
  import {
@@ -1463,6 +1511,16 @@ import {
1463
1511
  } from "@customerhero/js";
1464
1512
  import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1465
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
+ }
1466
1524
  function ChatInput() {
1467
1525
  const {
1468
1526
  sendMessage,
@@ -1477,9 +1535,27 @@ function ChatInput() {
1477
1535
  const [value, setValue] = useState6("");
1478
1536
  const [attachments, setAttachments] = useState6([]);
1479
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);
1480
1546
  useEffect6(() => {
1481
1547
  setCaptureSupported(canCaptureScreenshot());
1482
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]);
1483
1559
  useEffect6(() => {
1484
1560
  if (pendingPrefill === null) return;
1485
1561
  const text = consumePendingPrefill();
@@ -1490,6 +1566,30 @@ function ChatInput() {
1490
1566
  for (const a of attachments) URL.revokeObjectURL(a.previewUrl);
1491
1567
  };
1492
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]);
1493
1593
  const updateAttachment = (id, patch) => {
1494
1594
  setAttachments(
1495
1595
  (current) => current.map(
@@ -1497,16 +1597,16 @@ function ChatInput() {
1497
1597
  )
1498
1598
  );
1499
1599
  };
1500
- const startUpload = async (blob) => {
1600
+ const startUpload = async (blob, filename) => {
1501
1601
  if (attachments.length >= MAX_ATTACHMENTS) return;
1502
1602
  const id = `att_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1503
1603
  const previewUrl = URL.createObjectURL(blob);
1504
1604
  setAttachments((current) => [
1505
1605
  ...current,
1506
- { id, status: "uploading", previewUrl, blob }
1606
+ { id, status: "uploading", previewUrl, blob, filename }
1507
1607
  ]);
1508
1608
  try {
1509
- const { attachmentToken } = await uploadAttachment(blob);
1609
+ const { attachmentToken } = await uploadAttachment(blob, { filename });
1510
1610
  updateAttachment(id, {
1511
1611
  status: "ready",
1512
1612
  token: attachmentToken
@@ -1515,7 +1615,31 @@ function ChatInput() {
1515
1615
  updateAttachment(id, { status: "error" });
1516
1616
  }
1517
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
+ };
1518
1641
  const handleCapture = async () => {
1642
+ setMenuOpen(false);
1519
1643
  try {
1520
1644
  const blob = await captureScreenshot();
1521
1645
  await startUpload(blob);
@@ -1523,6 +1647,17 @@ function ChatInput() {
1523
1647
  if (e instanceof ScreenshotCancelled) return;
1524
1648
  }
1525
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
+ };
1526
1661
  const handleRemove = (id) => {
1527
1662
  setAttachments((current) => {
1528
1663
  const target = current.find((a) => a.id === id);
@@ -1530,6 +1665,45 @@ function ChatInput() {
1530
1665
  return current.filter((a) => a.id !== id);
1531
1666
  });
1532
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
+ };
1533
1707
  const readyTokens = attachments.filter(
1534
1708
  (a) => a.status === "ready"
1535
1709
  ).map((a) => a.token);
@@ -1550,6 +1724,7 @@ function ChatInput() {
1550
1724
  }
1551
1725
  };
1552
1726
  const containerStyle = {
1727
+ position: "relative",
1553
1728
  padding: "12px 16px",
1554
1729
  borderTop: "1px solid #eee",
1555
1730
  display: "flex",
@@ -1558,19 +1733,27 @@ function ChatInput() {
1558
1733
  };
1559
1734
  const rowStyle = {
1560
1735
  display: "flex",
1561
- alignItems: "center",
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",
1562
1739
  gap: 8
1563
1740
  };
1741
+ const TEXTAREA_MAX_HEIGHT = 140;
1564
1742
  const inputStyle = {
1565
1743
  flex: 1,
1566
1744
  border: "1px solid #e0e0e0",
1567
- borderRadius: 24,
1745
+ borderRadius: 18,
1568
1746
  padding: "10px 16px",
1569
1747
  fontSize: 14,
1748
+ lineHeight: 1.4,
1570
1749
  outline: "none",
1571
1750
  background: "#fafafa",
1572
1751
  color: config.textColor,
1573
- fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
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"
1574
1757
  };
1575
1758
  const sendButtonStyle = {
1576
1759
  width: 36,
@@ -1602,99 +1785,238 @@ function ChatInput() {
1602
1785
  flexShrink: 0,
1603
1786
  padding: 0
1604
1787
  });
1605
- const captureDisabled = attachments.length >= MAX_ATTACHMENTS || isLoading;
1606
- return /* @__PURE__ */ jsxs5("div", { style: containerStyle, children: [
1607
- attachments.length > 0 && /* @__PURE__ */ jsx8(
1608
- "div",
1609
- {
1610
- style: { display: "flex", gap: 8, flexWrap: "wrap" },
1611
- "aria-label": "Attachments",
1612
- children: attachments.map((a) => /* @__PURE__ */ jsx8(
1613
- Thumbnail,
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",
1614
1855
  {
1615
- attachment: a,
1616
- onRemove: () => handleRemove(a.id),
1617
- t
1618
- },
1619
- a.id
1620
- ))
1621
- }
1622
- ),
1623
- /* @__PURE__ */ jsxs5("div", { style: rowStyle, children: [
1624
- captureSupported && /* @__PURE__ */ jsx8(
1625
- "button",
1626
- {
1627
- type: "button",
1628
- onClick: handleCapture,
1629
- disabled: captureDisabled,
1630
- style: iconButtonStyle(captureDisabled),
1631
- "aria-label": t("screenshot_capture"),
1632
- title: t("screenshot_capture"),
1633
- children: /* @__PURE__ */ jsx8(CameraIcon, {})
1634
- }
1635
- ),
1636
- /* @__PURE__ */ jsx8(
1637
- "input",
1638
- {
1639
- type: "text",
1640
- value,
1641
- onChange: (e) => setValue(e.target.value),
1642
- onKeyDown: handleKeyDown,
1643
- placeholder: config.placeholderText,
1644
- style: inputStyle,
1645
- disabled: isLoading
1646
- }
1647
- ),
1648
- /* @__PURE__ */ jsx8(
1649
- "button",
1650
- {
1651
- onClick: handleSend,
1652
- disabled: isLoading || !value.trim(),
1653
- style: sendButtonStyle,
1654
- "aria-label": t("send_message"),
1655
- onMouseEnter: (e) => {
1656
- if (!reduced && !isLoading)
1657
- e.currentTarget.style.transform = "scale(1.1)";
1658
- },
1659
- onMouseLeave: (e) => {
1660
- if (!reduced) e.currentTarget.style.transform = "scale(1)";
1661
- },
1662
- children: /* @__PURE__ */ jsxs5(
1663
- "svg",
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",
1664
1929
  {
1665
- width: "16",
1666
- height: "16",
1667
- viewBox: "0 0 24 24",
1668
- fill: "none",
1669
- stroke: "currentColor",
1670
- strokeWidth: "2",
1671
- strokeLinecap: "round",
1672
- strokeLinejoin: "round",
1673
- children: [
1674
- /* @__PURE__ */ jsx8("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
1675
- /* @__PURE__ */ jsx8("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
1676
- ]
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
+ )
1677
1985
  }
1678
1986
  )
1679
- }
1680
- )
1681
- ] })
1682
- ] });
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;
1683
1999
  }
1684
2000
  function Thumbnail({
1685
2001
  attachment,
1686
2002
  onRemove,
1687
2003
  t
1688
2004
  }) {
1689
- const { status, previewUrl } = attachment;
2005
+ const { status, previewUrl, blob, filename } = attachment;
2006
+ const isImage = isImageMime(blob.type);
1690
2007
  const wrap = {
1691
2008
  position: "relative",
1692
- width: 56,
2009
+ width: isImage ? 56 : 160,
1693
2010
  height: 56,
1694
2011
  borderRadius: 8,
1695
2012
  overflow: "hidden",
1696
2013
  border: status === "error" ? "2px solid #b91c1c" : "1px solid #e0e0e0",
1697
- background: "#f5f5f5"
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"
1698
2020
  };
1699
2021
  const img = {
1700
2022
  width: "100%",
@@ -1725,8 +2047,42 @@ function Thumbnail({
1725
2047
  alignItems: "center",
1726
2048
  justifyContent: "center"
1727
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);
1728
2067
  return /* @__PURE__ */ jsxs5("div", { style: wrap, children: [
1729
- /* @__PURE__ */ jsx8("img", { src: previewUrl, alt: "", style: img }),
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
+ ] }),
1730
2086
  status === "uploading" && /* @__PURE__ */ jsx8("div", { style: overlay, "aria-label": "Uploading", children: /* @__PURE__ */ jsx8(Spinner2, {}) }),
1731
2087
  status === "error" && /* @__PURE__ */ jsx8(
1732
2088
  "div",
@@ -1749,7 +2105,45 @@ function Thumbnail({
1749
2105
  )
1750
2106
  ] });
1751
2107
  }
1752
- function CameraIcon() {
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() {
1753
2147
  return /* @__PURE__ */ jsxs5(
1754
2148
  "svg",
1755
2149
  {
@@ -1762,6 +2156,36 @@ function CameraIcon() {
1762
2156
  strokeLinecap: "round",
1763
2157
  strokeLinejoin: "round",
1764
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",
1765
2189
  children: [
1766
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" }),
1767
2191
  /* @__PURE__ */ jsx8("circle", { cx: "12", cy: "13", r: "4" })
@@ -2163,7 +2587,8 @@ function ChatWindow() {
2163
2587
  import { Fragment as Fragment6, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
2164
2588
  function ChatWidgetInner({ identity }) {
2165
2589
  const client = useCustomerHeroClient();
2166
- const prevIdentityRef = useRef4(void 0);
2590
+ const { configLoaded, configError } = useChat();
2591
+ const prevIdentityRef = useRef5(void 0);
2167
2592
  useEffect8(() => {
2168
2593
  const key = identity ? JSON.stringify(identity) : void 0;
2169
2594
  if (key !== prevIdentityRef.current) {
@@ -2173,6 +2598,7 @@ function ChatWidgetInner({ identity }) {
2173
2598
  }
2174
2599
  }
2175
2600
  }, [identity, client]);
2601
+ if (!configLoaded || configError) return null;
2176
2602
  return /* @__PURE__ */ jsxs7(Fragment6, { children: [
2177
2603
  /* @__PURE__ */ jsx10(ChatBubble, {}),
2178
2604
  /* @__PURE__ */ jsx10(ChatWindow, {})