@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.cjs CHANGED
@@ -57,9 +57,6 @@ function useCustomerHeroClient() {
57
57
  return client;
58
58
  }
59
59
 
60
- // src/components/chat-bubble.tsx
61
- var import_react4 = require("react");
62
-
63
60
  // src/use-chat.ts
64
61
  var import_react2 = require("react");
65
62
  function useChat() {
@@ -125,6 +122,9 @@ function useChat() {
125
122
  };
126
123
  }
127
124
 
125
+ // src/components/chat-bubble.tsx
126
+ var import_react4 = require("react");
127
+
128
128
  // src/use-reduced-motion.ts
129
129
  var import_react3 = require("react");
130
130
  function useReducedMotion() {
@@ -732,9 +732,11 @@ function renderMarkdown(source, opts = {}) {
732
732
  style: {
733
733
  margin: "0 0 8px",
734
734
  paddingLeft: 20,
735
- lineHeight: 1.5
735
+ lineHeight: 1.5,
736
+ listStyleType: "disc",
737
+ listStylePosition: "outside"
736
738
  },
737
- children: block.lines.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { children: renderInline(l) }, i))
739
+ children: block.lines.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { style: { display: "list-item" }, children: renderInline(l) }, i))
738
740
  },
739
741
  key
740
742
  );
@@ -745,9 +747,11 @@ function renderMarkdown(source, opts = {}) {
745
747
  style: {
746
748
  margin: "0 0 8px",
747
749
  paddingLeft: 22,
748
- lineHeight: 1.5
750
+ lineHeight: 1.5,
751
+ listStyleType: "decimal",
752
+ listStylePosition: "outside"
749
753
  },
750
- children: block.lines.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { children: renderInline(l) }, i))
754
+ children: block.lines.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { style: { display: "list-item" }, children: renderInline(l) }, i))
751
755
  },
752
756
  key
753
757
  );
@@ -950,11 +954,11 @@ function MessageStatusPill({
950
954
  const containerStyle = {
951
955
  display: "flex",
952
956
  alignItems: "center",
957
+ justifyContent: "flex-end",
953
958
  gap: 4,
954
959
  marginTop: 2,
955
960
  fontSize: 11,
956
- color: failed ? "#b91c1c" : "#888",
957
- alignSelf: "flex-end"
961
+ color: failed ? "#b91c1c" : "#888"
958
962
  };
959
963
  const labelKey = status === "sending" ? "status_sending" : status === "sent" ? "status_sent" : "status_failed";
960
964
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: containerStyle, "aria-live": "polite", children: [
@@ -1263,13 +1267,20 @@ function Message({
1263
1267
  };
1264
1268
  const linkColor = isUser ? "#ffffff" : config.primaryColor;
1265
1269
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(AnimatedMessage, { isUser, animate, reduced, children: [
1266
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: bubbleStyle, children: isUser ? message.content : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1267
- renderMarkdown(message.content, {
1268
- sources: message.sources,
1269
- linkColor
1270
- }),
1271
- message.streaming && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StreamingCursor, { reduced })
1272
- ] }) }),
1270
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1271
+ "div",
1272
+ {
1273
+ style: bubbleStyle,
1274
+ "data-streaming-bubble": !isUser && message.streaming ? "true" : void 0,
1275
+ children: isUser ? message.content : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1276
+ renderMarkdown(message.content, {
1277
+ sources: message.sources,
1278
+ linkColor
1279
+ }),
1280
+ message.streaming && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StreamingCursor, { reduced })
1281
+ ] })
1282
+ }
1283
+ ),
1273
1284
  isUser && message.status && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MessageStatusPill, { status: message.status, t }),
1274
1285
  !isUser && message.blocks?.map((block, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1275
1286
  BlockRenderer,
@@ -1354,16 +1365,51 @@ function ChatMessages() {
1354
1365
  t
1355
1366
  } = useChat();
1356
1367
  const reduced = useReducedMotion();
1368
+ const containerRef = (0, import_react7.useRef)(null);
1357
1369
  const messagesEndRef = (0, import_react7.useRef)(null);
1358
1370
  const isFirstRender = (0, import_react7.useRef)(true);
1359
1371
  const prevMessageCount = (0, import_react7.useRef)(0);
1372
+ const stickRef = (0, import_react7.useRef)(true);
1373
+ const suppressScrollRef = (0, import_react7.useRef)(false);
1374
+ const autoScrollTo = (top) => {
1375
+ const el = containerRef.current;
1376
+ if (!el) return;
1377
+ suppressScrollRef.current = true;
1378
+ el.scrollTop = top;
1379
+ requestAnimationFrame(() => {
1380
+ suppressScrollRef.current = false;
1381
+ });
1382
+ };
1383
+ const handleScroll = () => {
1384
+ if (suppressScrollRef.current) return;
1385
+ const el = containerRef.current;
1386
+ if (!el) return;
1387
+ const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
1388
+ stickRef.current = distFromBottom < 60;
1389
+ };
1360
1390
  (0, import_react7.useEffect)(() => {
1361
- if (messagesEndRef.current) {
1362
- messagesEndRef.current.scrollIntoView({
1363
- behavior: isFirstRender.current || reduced ? "auto" : "smooth"
1364
- });
1365
- isFirstRender.current = false;
1391
+ const el = containerRef.current;
1392
+ if (!el) return;
1393
+ const lastMsg2 = messages[messages.length - 1];
1394
+ if (messages.length > prevMessageCount.current && lastMsg2?.role === "user") {
1395
+ stickRef.current = true;
1366
1396
  }
1397
+ if (!stickRef.current && !isFirstRender.current) {
1398
+ return;
1399
+ }
1400
+ const streamingBubble = el.querySelector(
1401
+ "[data-streaming-bubble='true']"
1402
+ );
1403
+ let target = el.scrollHeight - el.clientHeight;
1404
+ if (streamingBubble && streamingBubble.offsetHeight > el.clientHeight - 24) {
1405
+ const containerTop = el.getBoundingClientRect().top;
1406
+ const bubbleTop = streamingBubble.getBoundingClientRect().top;
1407
+ const TOP_GAP = 16;
1408
+ target = el.scrollTop + (bubbleTop - containerTop) - TOP_GAP;
1409
+ stickRef.current = false;
1410
+ }
1411
+ autoScrollTo(target);
1412
+ isFirstRender.current = false;
1367
1413
  }, [messages, isLoading, reduced]);
1368
1414
  const newStartIndex = isFirstRender.current ? messages.length : prevMessageCount.current;
1369
1415
  (0, import_react7.useEffect)(() => {
@@ -1386,7 +1432,7 @@ function ChatMessages() {
1386
1432
  };
1387
1433
  const lastMsg = messages[messages.length - 1];
1388
1434
  const waitingForFirstToken = isLoading && (lastMsg?.role !== "bot" || lastMsg.streaming !== true);
1389
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: containerStyle, children: [
1435
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { ref: containerRef, style: containerStyle, onScroll: handleScroll, children: [
1390
1436
  messages.map((msg, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1391
1437
  Message,
1392
1438
  {
@@ -1477,6 +1523,16 @@ var import_react8 = require("react");
1477
1523
  var import_js2 = require("@customerhero/js");
1478
1524
  var import_jsx_runtime8 = require("react/jsx-runtime");
1479
1525
  var MAX_ATTACHMENTS = 3;
1526
+ var ALLOWED_MIME_TYPES = [
1527
+ "image/png",
1528
+ "image/jpeg",
1529
+ "image/webp",
1530
+ "application/pdf"
1531
+ ];
1532
+ var ACCEPT_ATTR = ALLOWED_MIME_TYPES.join(",");
1533
+ function isImageMime(mime) {
1534
+ return mime.startsWith("image/");
1535
+ }
1480
1536
  function ChatInput() {
1481
1537
  const {
1482
1538
  sendMessage,
@@ -1491,9 +1547,27 @@ function ChatInput() {
1491
1547
  const [value, setValue] = (0, import_react8.useState)("");
1492
1548
  const [attachments, setAttachments] = (0, import_react8.useState)([]);
1493
1549
  const [captureSupported, setCaptureSupported] = (0, import_react8.useState)(false);
1550
+ const [menuOpen, setMenuOpen] = (0, import_react8.useState)(false);
1551
+ const [dragActive, setDragActive] = (0, import_react8.useState)(false);
1552
+ const [transientError, setTransientError] = (0, import_react8.useState)(null);
1553
+ const fileInputRef = (0, import_react8.useRef)(null);
1554
+ const textInputRef = (0, import_react8.useRef)(null);
1555
+ const menuRef = (0, import_react8.useRef)(null);
1556
+ const menuButtonRef = (0, import_react8.useRef)(null);
1557
+ const dragCounterRef = (0, import_react8.useRef)(0);
1494
1558
  (0, import_react8.useEffect)(() => {
1495
1559
  setCaptureSupported((0, import_js2.canCaptureScreenshot)());
1496
1560
  }, []);
1561
+ (0, import_react8.useEffect)(() => {
1562
+ const id = requestAnimationFrame(() => textInputRef.current?.focus());
1563
+ return () => cancelAnimationFrame(id);
1564
+ }, []);
1565
+ (0, import_react8.useLayoutEffect)(() => {
1566
+ const el = textInputRef.current;
1567
+ if (!el) return;
1568
+ el.style.height = "auto";
1569
+ el.style.height = `${el.scrollHeight}px`;
1570
+ }, [value]);
1497
1571
  (0, import_react8.useEffect)(() => {
1498
1572
  if (pendingPrefill === null) return;
1499
1573
  const text = consumePendingPrefill();
@@ -1504,6 +1578,30 @@ function ChatInput() {
1504
1578
  for (const a of attachments) URL.revokeObjectURL(a.previewUrl);
1505
1579
  };
1506
1580
  }, []);
1581
+ (0, import_react8.useEffect)(() => {
1582
+ if (!menuOpen) return;
1583
+ const onClick = (e) => {
1584
+ const target = e.target;
1585
+ if (menuRef.current?.contains(target) || menuButtonRef.current?.contains(target)) {
1586
+ return;
1587
+ }
1588
+ setMenuOpen(false);
1589
+ };
1590
+ const onKey = (e) => {
1591
+ if (e.key === "Escape") setMenuOpen(false);
1592
+ };
1593
+ document.addEventListener("mousedown", onClick);
1594
+ document.addEventListener("keydown", onKey);
1595
+ return () => {
1596
+ document.removeEventListener("mousedown", onClick);
1597
+ document.removeEventListener("keydown", onKey);
1598
+ };
1599
+ }, [menuOpen]);
1600
+ (0, import_react8.useEffect)(() => {
1601
+ if (!transientError) return;
1602
+ const id = window.setTimeout(() => setTransientError(null), 4e3);
1603
+ return () => window.clearTimeout(id);
1604
+ }, [transientError]);
1507
1605
  const updateAttachment = (id, patch) => {
1508
1606
  setAttachments(
1509
1607
  (current) => current.map(
@@ -1511,16 +1609,16 @@ function ChatInput() {
1511
1609
  )
1512
1610
  );
1513
1611
  };
1514
- const startUpload = async (blob) => {
1612
+ const startUpload = async (blob, filename) => {
1515
1613
  if (attachments.length >= MAX_ATTACHMENTS) return;
1516
1614
  const id = `att_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1517
1615
  const previewUrl = URL.createObjectURL(blob);
1518
1616
  setAttachments((current) => [
1519
1617
  ...current,
1520
- { id, status: "uploading", previewUrl, blob }
1618
+ { id, status: "uploading", previewUrl, blob, filename }
1521
1619
  ]);
1522
1620
  try {
1523
- const { attachmentToken } = await uploadAttachment(blob);
1621
+ const { attachmentToken } = await uploadAttachment(blob, { filename });
1524
1622
  updateAttachment(id, {
1525
1623
  status: "ready",
1526
1624
  token: attachmentToken
@@ -1529,7 +1627,31 @@ function ChatInput() {
1529
1627
  updateAttachment(id, { status: "error" });
1530
1628
  }
1531
1629
  };
1630
+ const ingestFiles = (files) => {
1631
+ const remainingSlots = Math.max(0, MAX_ATTACHMENTS - attachments.length);
1632
+ if (remainingSlots === 0) return 0;
1633
+ let queued = 0;
1634
+ let rejectedAny = false;
1635
+ for (const f of Array.from(files)) {
1636
+ if (queued >= remainingSlots) break;
1637
+ const type = f.type;
1638
+ if (!ALLOWED_MIME_TYPES.includes(
1639
+ type
1640
+ )) {
1641
+ rejectedAny = true;
1642
+ continue;
1643
+ }
1644
+ const filename = typeof f.name === "string" && f.name.length > 0 ? f.name : void 0;
1645
+ void startUpload(f, filename);
1646
+ queued += 1;
1647
+ }
1648
+ if (rejectedAny) {
1649
+ setTransientError(t("attachment_unsupported_type"));
1650
+ }
1651
+ return queued;
1652
+ };
1532
1653
  const handleCapture = async () => {
1654
+ setMenuOpen(false);
1533
1655
  try {
1534
1656
  const blob = await (0, import_js2.captureScreenshot)();
1535
1657
  await startUpload(blob);
@@ -1537,6 +1659,17 @@ function ChatInput() {
1537
1659
  if (e instanceof import_js2.ScreenshotCancelled) return;
1538
1660
  }
1539
1661
  };
1662
+ const handlePickFile = () => {
1663
+ setMenuOpen(false);
1664
+ fileInputRef.current?.click();
1665
+ };
1666
+ const handleFileInputChange = (e) => {
1667
+ const files = e.target.files;
1668
+ if (files && files.length > 0) {
1669
+ ingestFiles(files);
1670
+ }
1671
+ e.target.value = "";
1672
+ };
1540
1673
  const handleRemove = (id) => {
1541
1674
  setAttachments((current) => {
1542
1675
  const target = current.find((a) => a.id === id);
@@ -1544,6 +1677,45 @@ function ChatInput() {
1544
1677
  return current.filter((a) => a.id !== id);
1545
1678
  });
1546
1679
  };
1680
+ const handlePaste = (e) => {
1681
+ const items = e.clipboardData?.items;
1682
+ if (!items || items.length === 0) return;
1683
+ const blobs = [];
1684
+ for (const item of Array.from(items)) {
1685
+ if (item.kind !== "file") continue;
1686
+ const blob = item.getAsFile();
1687
+ if (blob) blobs.push(blob);
1688
+ }
1689
+ if (blobs.length === 0) return;
1690
+ e.preventDefault();
1691
+ ingestFiles(blobs);
1692
+ };
1693
+ const handleDragEnter = (e) => {
1694
+ if (!hasFiles(e)) return;
1695
+ e.preventDefault();
1696
+ dragCounterRef.current += 1;
1697
+ setDragActive(true);
1698
+ };
1699
+ const handleDragOver = (e) => {
1700
+ if (!hasFiles(e)) return;
1701
+ e.preventDefault();
1702
+ };
1703
+ const handleDragLeave = (e) => {
1704
+ if (!hasFiles(e)) return;
1705
+ e.preventDefault();
1706
+ dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
1707
+ if (dragCounterRef.current === 0) setDragActive(false);
1708
+ };
1709
+ const handleDrop = (e) => {
1710
+ if (!hasFiles(e)) return;
1711
+ e.preventDefault();
1712
+ dragCounterRef.current = 0;
1713
+ setDragActive(false);
1714
+ const files = e.dataTransfer?.files;
1715
+ if (files && files.length > 0) {
1716
+ ingestFiles(files);
1717
+ }
1718
+ };
1547
1719
  const readyTokens = attachments.filter(
1548
1720
  (a) => a.status === "ready"
1549
1721
  ).map((a) => a.token);
@@ -1564,6 +1736,7 @@ function ChatInput() {
1564
1736
  }
1565
1737
  };
1566
1738
  const containerStyle = {
1739
+ position: "relative",
1567
1740
  padding: "12px 16px",
1568
1741
  borderTop: "1px solid #eee",
1569
1742
  display: "flex",
@@ -1572,19 +1745,27 @@ function ChatInput() {
1572
1745
  };
1573
1746
  const rowStyle = {
1574
1747
  display: "flex",
1575
- alignItems: "center",
1748
+ // Anchor the icon buttons to the bottom of the row so a multi-line
1749
+ // textarea grows upward without dragging them along.
1750
+ alignItems: "flex-end",
1576
1751
  gap: 8
1577
1752
  };
1753
+ const TEXTAREA_MAX_HEIGHT = 140;
1578
1754
  const inputStyle = {
1579
1755
  flex: 1,
1580
1756
  border: "1px solid #e0e0e0",
1581
- borderRadius: 24,
1757
+ borderRadius: 18,
1582
1758
  padding: "10px 16px",
1583
1759
  fontSize: 14,
1760
+ lineHeight: 1.4,
1584
1761
  outline: "none",
1585
1762
  background: "#fafafa",
1586
1763
  color: config.textColor,
1587
- fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
1764
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
1765
+ resize: "none",
1766
+ overflowY: "auto",
1767
+ maxHeight: TEXTAREA_MAX_HEIGHT,
1768
+ boxSizing: "border-box"
1588
1769
  };
1589
1770
  const sendButtonStyle = {
1590
1771
  width: 36,
@@ -1616,99 +1797,238 @@ function ChatInput() {
1616
1797
  flexShrink: 0,
1617
1798
  padding: 0
1618
1799
  });
1619
- const captureDisabled = attachments.length >= MAX_ATTACHMENTS || isLoading;
1620
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: containerStyle, children: [
1621
- attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1622
- "div",
1623
- {
1624
- style: { display: "flex", gap: 8, flexWrap: "wrap" },
1625
- "aria-label": "Attachments",
1626
- children: attachments.map((a) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1627
- Thumbnail,
1800
+ const menuStyle = {
1801
+ position: "absolute",
1802
+ bottom: "calc(100% + 4px)",
1803
+ left: 0,
1804
+ background: "white",
1805
+ border: "1px solid #e0e0e0",
1806
+ borderRadius: 8,
1807
+ boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
1808
+ padding: 4,
1809
+ minWidth: 180,
1810
+ zIndex: 10,
1811
+ display: "flex",
1812
+ flexDirection: "column"
1813
+ };
1814
+ const menuItemStyle = {
1815
+ display: "flex",
1816
+ alignItems: "center",
1817
+ gap: 10,
1818
+ padding: "8px 12px",
1819
+ background: "transparent",
1820
+ border: "none",
1821
+ borderRadius: 4,
1822
+ cursor: "pointer",
1823
+ fontSize: 14,
1824
+ color: "#333",
1825
+ textAlign: "left",
1826
+ whiteSpace: "nowrap",
1827
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
1828
+ };
1829
+ const dropOverlayStyle = {
1830
+ position: "absolute",
1831
+ inset: 0,
1832
+ background: "rgba(255,255,255,0.92)",
1833
+ border: `2px dashed ${config.primaryColor}`,
1834
+ borderRadius: 4,
1835
+ display: "flex",
1836
+ alignItems: "center",
1837
+ justifyContent: "center",
1838
+ color: config.primaryColor,
1839
+ fontSize: 14,
1840
+ fontWeight: 500,
1841
+ pointerEvents: "none",
1842
+ zIndex: 11
1843
+ };
1844
+ const errorPillStyle = {
1845
+ alignSelf: "flex-start",
1846
+ background: "#fef2f2",
1847
+ color: "#b91c1c",
1848
+ border: "1px solid #fecaca",
1849
+ borderRadius: 12,
1850
+ padding: "4px 10px",
1851
+ fontSize: 12
1852
+ };
1853
+ const attachDisabled = attachments.length >= MAX_ATTACHMENTS || isLoading;
1854
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1855
+ "div",
1856
+ {
1857
+ style: containerStyle,
1858
+ onDragEnter: handleDragEnter,
1859
+ onDragOver: handleDragOver,
1860
+ onDragLeave: handleDragLeave,
1861
+ onDrop: handleDrop,
1862
+ children: [
1863
+ dragActive && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: dropOverlayStyle, "aria-hidden": "true", children: t("drop_files_here") }),
1864
+ transientError && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { role: "alert", style: errorPillStyle, children: transientError }),
1865
+ attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1866
+ "div",
1628
1867
  {
1629
- attachment: a,
1630
- onRemove: () => handleRemove(a.id),
1631
- t
1632
- },
1633
- a.id
1634
- ))
1635
- }
1636
- ),
1637
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: rowStyle, children: [
1638
- captureSupported && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1639
- "button",
1640
- {
1641
- type: "button",
1642
- onClick: handleCapture,
1643
- disabled: captureDisabled,
1644
- style: iconButtonStyle(captureDisabled),
1645
- "aria-label": t("screenshot_capture"),
1646
- title: t("screenshot_capture"),
1647
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CameraIcon, {})
1648
- }
1649
- ),
1650
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1651
- "input",
1652
- {
1653
- type: "text",
1654
- value,
1655
- onChange: (e) => setValue(e.target.value),
1656
- onKeyDown: handleKeyDown,
1657
- placeholder: config.placeholderText,
1658
- style: inputStyle,
1659
- disabled: isLoading
1660
- }
1661
- ),
1662
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1663
- "button",
1664
- {
1665
- onClick: handleSend,
1666
- disabled: isLoading || !value.trim(),
1667
- style: sendButtonStyle,
1668
- "aria-label": t("send_message"),
1669
- onMouseEnter: (e) => {
1670
- if (!reduced && !isLoading)
1671
- e.currentTarget.style.transform = "scale(1.1)";
1672
- },
1673
- onMouseLeave: (e) => {
1674
- if (!reduced) e.currentTarget.style.transform = "scale(1)";
1675
- },
1676
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1677
- "svg",
1868
+ style: { display: "flex", gap: 8, flexWrap: "wrap" },
1869
+ "aria-label": "Attachments",
1870
+ children: attachments.map((a) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1871
+ Thumbnail,
1872
+ {
1873
+ attachment: a,
1874
+ onRemove: () => handleRemove(a.id),
1875
+ t
1876
+ },
1877
+ a.id
1878
+ ))
1879
+ }
1880
+ ),
1881
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: rowStyle, children: [
1882
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { position: "relative" }, children: [
1883
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1884
+ "button",
1885
+ {
1886
+ ref: menuButtonRef,
1887
+ type: "button",
1888
+ onClick: () => setMenuOpen((o) => !o),
1889
+ disabled: attachDisabled,
1890
+ style: iconButtonStyle(attachDisabled),
1891
+ "aria-label": t("attach_menu_open"),
1892
+ "aria-haspopup": "menu",
1893
+ "aria-expanded": menuOpen,
1894
+ title: t("attach_menu_open"),
1895
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PaperclipIcon, {})
1896
+ }
1897
+ ),
1898
+ menuOpen && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { ref: menuRef, role: "menu", style: menuStyle, children: [
1899
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1900
+ "button",
1901
+ {
1902
+ type: "button",
1903
+ role: "menuitem",
1904
+ onClick: handlePickFile,
1905
+ style: menuItemStyle,
1906
+ onMouseEnter: (e) => {
1907
+ e.currentTarget.style.background = "#f5f5f5";
1908
+ },
1909
+ onMouseLeave: (e) => {
1910
+ e.currentTarget.style.background = "transparent";
1911
+ },
1912
+ children: [
1913
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ImageIcon, {}),
1914
+ t("attach_photo")
1915
+ ]
1916
+ }
1917
+ ),
1918
+ captureSupported && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1919
+ "button",
1920
+ {
1921
+ type: "button",
1922
+ role: "menuitem",
1923
+ onClick: handleCapture,
1924
+ style: menuItemStyle,
1925
+ onMouseEnter: (e) => {
1926
+ e.currentTarget.style.background = "#f5f5f5";
1927
+ },
1928
+ onMouseLeave: (e) => {
1929
+ e.currentTarget.style.background = "transparent";
1930
+ },
1931
+ children: [
1932
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CameraIcon, {}),
1933
+ t("screenshot_capture")
1934
+ ]
1935
+ }
1936
+ )
1937
+ ] })
1938
+ ] }),
1939
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1940
+ "input",
1678
1941
  {
1679
- width: "16",
1680
- height: "16",
1681
- viewBox: "0 0 24 24",
1682
- fill: "none",
1683
- stroke: "currentColor",
1684
- strokeWidth: "2",
1685
- strokeLinecap: "round",
1686
- strokeLinejoin: "round",
1687
- children: [
1688
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
1689
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
1690
- ]
1942
+ ref: fileInputRef,
1943
+ type: "file",
1944
+ accept: ACCEPT_ATTR,
1945
+ multiple: true,
1946
+ onChange: handleFileInputChange,
1947
+ style: { display: "none" },
1948
+ "aria-hidden": "true",
1949
+ tabIndex: -1
1950
+ }
1951
+ ),
1952
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1953
+ "textarea",
1954
+ {
1955
+ ref: textInputRef,
1956
+ rows: 1,
1957
+ value,
1958
+ onChange: (e) => setValue(e.target.value),
1959
+ onKeyDown: handleKeyDown,
1960
+ onPaste: handlePaste,
1961
+ placeholder: config.placeholderText,
1962
+ style: inputStyle,
1963
+ disabled: isLoading
1964
+ }
1965
+ ),
1966
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1967
+ "button",
1968
+ {
1969
+ onClick: handleSend,
1970
+ disabled: isLoading || !value.trim(),
1971
+ style: sendButtonStyle,
1972
+ "aria-label": t("send_message"),
1973
+ onMouseEnter: (e) => {
1974
+ if (!reduced && !isLoading)
1975
+ e.currentTarget.style.transform = "scale(1.1)";
1976
+ },
1977
+ onMouseLeave: (e) => {
1978
+ if (!reduced) e.currentTarget.style.transform = "scale(1)";
1979
+ },
1980
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1981
+ "svg",
1982
+ {
1983
+ width: "16",
1984
+ height: "16",
1985
+ viewBox: "0 0 24 24",
1986
+ fill: "none",
1987
+ stroke: "currentColor",
1988
+ strokeWidth: "2",
1989
+ strokeLinecap: "round",
1990
+ strokeLinejoin: "round",
1991
+ children: [
1992
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
1993
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
1994
+ ]
1995
+ }
1996
+ )
1691
1997
  }
1692
1998
  )
1693
- }
1694
- )
1695
- ] })
1696
- ] });
1999
+ ] })
2000
+ ]
2001
+ }
2002
+ );
2003
+ }
2004
+ function hasFiles(e) {
2005
+ const types = e.dataTransfer?.types;
2006
+ if (!types) return false;
2007
+ for (let i = 0; i < types.length; i++) {
2008
+ if (types[i] === "Files") return true;
2009
+ }
2010
+ return false;
1697
2011
  }
1698
2012
  function Thumbnail({
1699
2013
  attachment,
1700
2014
  onRemove,
1701
2015
  t
1702
2016
  }) {
1703
- const { status, previewUrl } = attachment;
2017
+ const { status, previewUrl, blob, filename } = attachment;
2018
+ const isImage = isImageMime(blob.type);
1704
2019
  const wrap = {
1705
2020
  position: "relative",
1706
- width: 56,
2021
+ width: isImage ? 56 : 160,
1707
2022
  height: 56,
1708
2023
  borderRadius: 8,
1709
2024
  overflow: "hidden",
1710
2025
  border: status === "error" ? "2px solid #b91c1c" : "1px solid #e0e0e0",
1711
- background: "#f5f5f5"
2026
+ background: "#f5f5f5",
2027
+ display: "flex",
2028
+ alignItems: "center",
2029
+ justifyContent: isImage ? "stretch" : "flex-start",
2030
+ gap: isImage ? 0 : 8,
2031
+ padding: isImage ? 0 : "0 8px"
1712
2032
  };
1713
2033
  const img = {
1714
2034
  width: "100%",
@@ -1739,8 +2059,42 @@ function Thumbnail({
1739
2059
  alignItems: "center",
1740
2060
  justifyContent: "center"
1741
2061
  };
2062
+ const docName = {
2063
+ fontSize: 12,
2064
+ fontWeight: 500,
2065
+ color: "#333",
2066
+ overflow: "hidden",
2067
+ textOverflow: "ellipsis",
2068
+ whiteSpace: "nowrap",
2069
+ maxWidth: 110,
2070
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
2071
+ };
2072
+ const docMeta = {
2073
+ fontSize: 11,
2074
+ color: "#888",
2075
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
2076
+ };
2077
+ const displayName = filename ?? (isImage ? "" : "Document");
2078
+ const sizeLabel = formatBytes(blob.size);
1742
2079
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: wrap, children: [
1743
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("img", { src: previewUrl, alt: "", style: img }),
2080
+ isImage ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("img", { src: previewUrl, alt: "", style: img }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2081
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(DocIcon, {}),
2082
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2083
+ "div",
2084
+ {
2085
+ style: {
2086
+ display: "flex",
2087
+ flexDirection: "column",
2088
+ minWidth: 0,
2089
+ flex: 1
2090
+ },
2091
+ children: [
2092
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: docName, title: displayName, children: displayName }),
2093
+ sizeLabel && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: docMeta, children: sizeLabel })
2094
+ ]
2095
+ }
2096
+ )
2097
+ ] }),
1744
2098
  status === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: overlay, "aria-label": "Uploading", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Spinner2, {}) }),
1745
2099
  status === "error" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1746
2100
  "div",
@@ -1763,7 +2117,45 @@ function Thumbnail({
1763
2117
  )
1764
2118
  ] });
1765
2119
  }
1766
- function CameraIcon() {
2120
+ function PaperclipIcon() {
2121
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2122
+ "svg",
2123
+ {
2124
+ width: "20",
2125
+ height: "20",
2126
+ viewBox: "0 0 24 24",
2127
+ fill: "none",
2128
+ stroke: "currentColor",
2129
+ strokeWidth: "2",
2130
+ strokeLinecap: "round",
2131
+ strokeLinejoin: "round",
2132
+ "aria-hidden": "true",
2133
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("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" })
2134
+ }
2135
+ );
2136
+ }
2137
+ function ImageIcon() {
2138
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2139
+ "svg",
2140
+ {
2141
+ width: "18",
2142
+ height: "18",
2143
+ viewBox: "0 0 24 24",
2144
+ fill: "none",
2145
+ stroke: "currentColor",
2146
+ strokeWidth: "2",
2147
+ strokeLinecap: "round",
2148
+ strokeLinejoin: "round",
2149
+ "aria-hidden": "true",
2150
+ children: [
2151
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
2152
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
2153
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polyline", { points: "21 15 16 10 5 21" })
2154
+ ]
2155
+ }
2156
+ );
2157
+ }
2158
+ function DocIcon() {
1767
2159
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1768
2160
  "svg",
1769
2161
  {
@@ -1776,6 +2168,36 @@ function CameraIcon() {
1776
2168
  strokeLinecap: "round",
1777
2169
  strokeLinejoin: "round",
1778
2170
  "aria-hidden": "true",
2171
+ style: { color: "#666", flexShrink: 0 },
2172
+ children: [
2173
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
2174
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polyline", { points: "14 2 14 8 20 8" }),
2175
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
2176
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("line", { x1: "16", y1: "17", x2: "8", y2: "17" }),
2177
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polyline", { points: "10 9 9 9 8 9" })
2178
+ ]
2179
+ }
2180
+ );
2181
+ }
2182
+ function formatBytes(bytes) {
2183
+ if (!Number.isFinite(bytes) || bytes <= 0) return "";
2184
+ if (bytes < 1024) return `${bytes} B`;
2185
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
2186
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2187
+ }
2188
+ function CameraIcon() {
2189
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2190
+ "svg",
2191
+ {
2192
+ width: "18",
2193
+ height: "18",
2194
+ viewBox: "0 0 24 24",
2195
+ fill: "none",
2196
+ stroke: "currentColor",
2197
+ strokeWidth: "2",
2198
+ strokeLinecap: "round",
2199
+ strokeLinejoin: "round",
2200
+ "aria-hidden": "true",
1779
2201
  children: [
1780
2202
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("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" }),
1781
2203
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "12", cy: "13", r: "4" })
@@ -2177,6 +2599,7 @@ function ChatWindow() {
2177
2599
  var import_jsx_runtime10 = require("react/jsx-runtime");
2178
2600
  function ChatWidgetInner({ identity }) {
2179
2601
  const client = useCustomerHeroClient();
2602
+ const { configLoaded, configError } = useChat();
2180
2603
  const prevIdentityRef = (0, import_react10.useRef)(void 0);
2181
2604
  (0, import_react10.useEffect)(() => {
2182
2605
  const key = identity ? JSON.stringify(identity) : void 0;
@@ -2187,6 +2610,7 @@ function ChatWidgetInner({ identity }) {
2187
2610
  }
2188
2611
  }
2189
2612
  }, [identity, client]);
2613
+ if (!configLoaded || configError) return null;
2190
2614
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
2191
2615
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ChatBubble, {}),
2192
2616
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ChatWindow, {})