@coffer-org/plugin-webchat 7.1.0 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import * as __fedReact from "react";
2
- import { Dialog, DialogContent, Icon, MarkdownView, toneVar, uploadFile, useMinWidth } from "@coffer-org/web-ui";
3
- import { createContext, createElement, forwardRef, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
2
+ import { Button, Dialog, DialogContent, Icon, MarkdownView, fileSizeLabel, uploadFile, useMinWidth } from "@coffer-org/web-ui";
3
+ import { createContext, createElement, forwardRef, memo, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
4
4
  import { useLocation } from "react-router-dom";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  //#region \0rolldown/runtime.js
@@ -496,14 +496,9 @@ var ChevronDown = createLucideIcon("chevron-down", [["path", {
496
496
  * This source code is licensed under the ISC license.
497
497
  * See the LICENSE file in the root directory of this source tree.
498
498
  */
499
- var Clock = createLucideIcon("clock", [["circle", {
500
- cx: "12",
501
- cy: "12",
502
- r: "10",
503
- key: "1mglay"
504
- }], ["path", {
505
- d: "M12 6v6l4 2",
506
- key: "mmk7yg"
499
+ var ChevronRight = createLucideIcon("chevron-right", [["path", {
500
+ d: "m9 18 6-6-6-6",
501
+ key: "mthhwq"
507
502
  }]]);
508
503
  /**
509
504
  * @license lucide-react v1.24.0 - ISC
@@ -511,6 +506,162 @@ var Clock = createLucideIcon("clock", [["circle", {
511
506
  * This source code is licensed under the ISC license.
512
507
  * See the LICENSE file in the root directory of this source tree.
513
508
  */
509
+ var FileArchive = createLucideIcon("file-archive", [
510
+ ["path", {
511
+ d: "M13.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v11.5",
512
+ key: "4pqfef"
513
+ }],
514
+ ["path", {
515
+ d: "M14 2v5a1 1 0 0 0 1 1h5",
516
+ key: "wfsgrz"
517
+ }],
518
+ ["path", {
519
+ d: "M8 12v-1",
520
+ key: "1ej8lb"
521
+ }],
522
+ ["path", {
523
+ d: "M8 18v-2",
524
+ key: "qcmpov"
525
+ }],
526
+ ["path", {
527
+ d: "M8 7V6",
528
+ key: "1nbb54"
529
+ }],
530
+ ["circle", {
531
+ cx: "8",
532
+ cy: "20",
533
+ r: "2",
534
+ key: "ckkr5m"
535
+ }]
536
+ ]);
537
+ /**
538
+ * @license lucide-react v1.24.0 - ISC
539
+ *
540
+ * This source code is licensed under the ISC license.
541
+ * See the LICENSE file in the root directory of this source tree.
542
+ */
543
+ var FileCode = createLucideIcon("file-code", [
544
+ ["path", {
545
+ d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",
546
+ key: "1oefj6"
547
+ }],
548
+ ["path", {
549
+ d: "M14 2v5a1 1 0 0 0 1 1h5",
550
+ key: "wfsgrz"
551
+ }],
552
+ ["path", {
553
+ d: "M10 12.5 8 15l2 2.5",
554
+ key: "1tg20x"
555
+ }],
556
+ ["path", {
557
+ d: "m14 12.5 2 2.5-2 2.5",
558
+ key: "yinavb"
559
+ }]
560
+ ]);
561
+ /**
562
+ * @license lucide-react v1.24.0 - ISC
563
+ *
564
+ * This source code is licensed under the ISC license.
565
+ * See the LICENSE file in the root directory of this source tree.
566
+ */
567
+ var FileHeadphone = createLucideIcon("file-headphone", [
568
+ ["path", {
569
+ d: "M4 6.835V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2h-.343",
570
+ key: "1vfytu"
571
+ }],
572
+ ["path", {
573
+ d: "M14 2v5a1 1 0 0 0 1 1h5",
574
+ key: "wfsgrz"
575
+ }],
576
+ ["path", {
577
+ d: "M2 19a2 2 0 0 1 4 0v1a2 2 0 0 1-4 0v-4a6 6 0 0 1 12 0v4a2 2 0 0 1-4 0v-1a2 2 0 0 1 4 0",
578
+ key: "1etmh7"
579
+ }]
580
+ ]);
581
+ /**
582
+ * @license lucide-react v1.24.0 - ISC
583
+ *
584
+ * This source code is licensed under the ISC license.
585
+ * See the LICENSE file in the root directory of this source tree.
586
+ */
587
+ var FileImage = createLucideIcon("file-image", [
588
+ ["path", {
589
+ d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",
590
+ key: "1oefj6"
591
+ }],
592
+ ["path", {
593
+ d: "M14 2v5a1 1 0 0 0 1 1h5",
594
+ key: "wfsgrz"
595
+ }],
596
+ ["circle", {
597
+ cx: "10",
598
+ cy: "12",
599
+ r: "2",
600
+ key: "737tya"
601
+ }],
602
+ ["path", {
603
+ d: "m20 17-1.296-1.296a2.41 2.41 0 0 0-3.408 0L9 22",
604
+ key: "wt3hpn"
605
+ }]
606
+ ]);
607
+ /**
608
+ * @license lucide-react v1.24.0 - ISC
609
+ *
610
+ * This source code is licensed under the ISC license.
611
+ * See the LICENSE file in the root directory of this source tree.
612
+ */
613
+ var FilePlay = createLucideIcon("file-play", [
614
+ ["path", {
615
+ d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",
616
+ key: "1oefj6"
617
+ }],
618
+ ["path", {
619
+ d: "M14 2v5a1 1 0 0 0 1 1h5",
620
+ key: "wfsgrz"
621
+ }],
622
+ ["path", {
623
+ d: "M15.033 13.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56v-4.704a.645.645 0 0 1 .967-.56z",
624
+ key: "1tzo1f"
625
+ }]
626
+ ]);
627
+ /**
628
+ * @license lucide-react v1.24.0 - ISC
629
+ *
630
+ * This source code is licensed under the ISC license.
631
+ * See the LICENSE file in the root directory of this source tree.
632
+ */
633
+ var FileSpreadsheet = createLucideIcon("file-spreadsheet", [
634
+ ["path", {
635
+ d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",
636
+ key: "1oefj6"
637
+ }],
638
+ ["path", {
639
+ d: "M14 2v5a1 1 0 0 0 1 1h5",
640
+ key: "wfsgrz"
641
+ }],
642
+ ["path", {
643
+ d: "M8 13h2",
644
+ key: "yr2amv"
645
+ }],
646
+ ["path", {
647
+ d: "M14 13h2",
648
+ key: "un5t4a"
649
+ }],
650
+ ["path", {
651
+ d: "M8 17h2",
652
+ key: "2yhykz"
653
+ }],
654
+ ["path", {
655
+ d: "M14 17h2",
656
+ key: "10kma7"
657
+ }]
658
+ ]);
659
+ /**
660
+ * @license lucide-react v1.24.0 - ISC
661
+ *
662
+ * This source code is licensed under the ISC license.
663
+ * See the LICENSE file in the root directory of this source tree.
664
+ */
514
665
  var FileText = createLucideIcon("file-text", [
515
666
  ["path", {
516
667
  d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",
@@ -615,6 +766,19 @@ var MessageCircle = createLucideIcon("message-circle", [["path", {
615
766
  * This source code is licensed under the ISC license.
616
767
  * See the LICENSE file in the root directory of this source tree.
617
768
  */
769
+ var MessagesSquare = createLucideIcon("messages-square", [["path", {
770
+ d: "M16 10a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 14.286V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z",
771
+ key: "1n2ejm"
772
+ }], ["path", {
773
+ d: "M20 9a2 2 0 0 1 2 2v10.286a.71.71 0 0 1-1.212.502l-2.202-2.202A2 2 0 0 0 17.172 19H10a2 2 0 0 1-2-2v-1",
774
+ key: "1qfcsi"
775
+ }]]);
776
+ /**
777
+ * @license lucide-react v1.24.0 - ISC
778
+ *
779
+ * This source code is licensed under the ISC license.
780
+ * See the LICENSE file in the root directory of this source tree.
781
+ */
618
782
  var Minimize2 = createLucideIcon("minimize-2", [
619
783
  ["path", {
620
784
  d: "m14 10 7-7",
@@ -702,16 +866,6 @@ var Square = createLucideIcon("square", [["rect", {
702
866
  * This source code is licensed under the ISC license.
703
867
  * See the LICENSE file in the root directory of this source tree.
704
868
  */
705
- var Star = createLucideIcon("star", [["path", {
706
- d: "M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z",
707
- key: "r04s7s"
708
- }]]);
709
- /**
710
- * @license lucide-react v1.24.0 - ISC
711
- *
712
- * This source code is licensed under the ISC license.
713
- * See the LICENSE file in the root directory of this source tree.
714
- */
715
869
  var Upload = createLucideIcon("upload", [
716
870
  ["path", {
717
871
  d: "M12 3v12",
@@ -1376,6 +1530,149 @@ function ChatMarkdown({ text, className }) {
1376
1530
  });
1377
1531
  }
1378
1532
  //#endregion
1533
+ //#region src/ui/ChatAttachments.tsx
1534
+ /**
1535
+ * The ONE presentation of a chat attachment — the files staged in the composer and the files a
1536
+ * sent turn carries are drawn by this component, so the two cannot drift apart.
1537
+ *
1538
+ * Three decisions are worth stating, because each replaces something that was there before:
1539
+ *
1540
+ * - **A file is a ROW, not a word.** The bubble used to print `a.label ?? a.name` as bare text
1541
+ * in a wrapping flex, which for eight uploads meant eight lines of 40-character storage names
1542
+ * running to the bubble's edge and clipping mid-name. A row states the three facts a file has
1543
+ * — what kind it is, what it is called, how big it is — and states them in the same places
1544
+ * every time, which is what makes a stack of them scannable.
1545
+ * - **The name breaks in the MIDDLE, not at the end.** The extension is the most informative
1546
+ * part of a long filename and the part a tail-truncation always eats. The stem takes the
1547
+ * truncation (`truncate`), the extension is pinned beside it (`shrink-0`), so `…_2026.pdf`
1548
+ * keeps saying `.pdf` at any width. CSS does this, not a character budget — the same row
1549
+ * works in a 380px panel and in the 1040px modal without measuring anything.
1550
+ * - **Past two files the group COLLAPSES.** A turn carrying eight PDFs is a turn whose text a
1551
+ * reader still has to reach; the attachments are its luggage, not its content. Two rows say
1552
+ * what was sent, `+N` says how much more there is, and one click shows the rest.
1553
+ *
1554
+ * The rows are inert on purpose: an attachment names itself and does nothing else. Opening one
1555
+ * would be a second file-viewing surface beside the record page's, and the chat is not where a
1556
+ * file is read (decided 2026-09-15).
1557
+ */
1558
+ /** How many rows stand before the rest fold away. Two is what a bubble can carry without the
1559
+ * luggage outweighing the message — see the component's note. */
1560
+ var COLLAPSE_AFTER = 2;
1561
+ /** Extension families, in the order a name is tested against them. */
1562
+ var BY_EXT = [
1563
+ [/\.(png|jpe?g|gif|webp|avif|svg|bmp|heic|heif)$/i, FileImage],
1564
+ [/\.(mp3|wav|ogg|oga|flac|m4a|aac|opus)$/i, FileHeadphone],
1565
+ [/\.(mp4|mov|mkv|webm|avi|m4v)$/i, FilePlay],
1566
+ [/\.(zip|rar|7z|tar|gz|tgz|bz2|xz)$/i, FileArchive],
1567
+ [/\.(csv|tsv|xlsx?|ods|numbers)$/i, FileSpreadsheet],
1568
+ [/\.(json|xml|ya?ml|toml|ini|js|jsx|ts|tsx|css|html?|sh|py|sql)$/i, FileCode]
1569
+ ];
1570
+ /**
1571
+ * What KIND of file this is, at a glance. The mime type is the server's answer and is trusted
1572
+ * first; an upload that arrives with none (or with the `application/octet-stream` a browser
1573
+ * falls back to) is read from its extension instead, which is the only fact left.
1574
+ */
1575
+ function attachmentIcon(a) {
1576
+ const mime = a.mime?.toLowerCase().split(";", 1)[0] ?? "";
1577
+ if (mime.startsWith("image/")) return FileImage;
1578
+ if (mime.startsWith("audio/")) return FileHeadphone;
1579
+ if (mime.startsWith("video/")) return FilePlay;
1580
+ if (mime === "text/csv" || mime.includes("spreadsheet") || mime.includes("excel")) return FileSpreadsheet;
1581
+ if (mime === "application/json" || mime === "application/xml" || mime.startsWith("text/x")) return FileCode;
1582
+ if (mime === "application/zip" || mime.includes("compressed") || mime.includes("tar")) return FileArchive;
1583
+ const name = a.label ?? a.name;
1584
+ for (const [re, icon] of BY_EXT) if (re.test(name)) return icon;
1585
+ return FileText;
1586
+ }
1587
+ /**
1588
+ * Split a filename into the part that may be truncated and the part that may not. A leading dot
1589
+ * (`.gitignore`) is not an extension, and neither is a "dot" eleven characters from the end of a
1590
+ * sentence-like name — an extension is short, so anything longer is left in the stem where the
1591
+ * truncation can reach it.
1592
+ */
1593
+ function splitFileName(name) {
1594
+ const dot = name.lastIndexOf(".");
1595
+ if (dot <= 0 || name.length - dot > 11) return {
1596
+ stem: name,
1597
+ ext: ""
1598
+ };
1599
+ return {
1600
+ stem: name.slice(0, dot),
1601
+ ext: name.slice(dot)
1602
+ };
1603
+ }
1604
+ function AttachmentRow({ a, onRemove }) {
1605
+ const { t } = useTranslation();
1606
+ const name = a.label ?? a.name;
1607
+ const { stem, ext } = splitFileName(name);
1608
+ const size = fileSizeLabel(a.size);
1609
+ return /* @__PURE__ */ jsxs("li", {
1610
+ className: "flex min-w-0 items-center gap-2 rounded-[calc(var(--radius-md)+1px)] border border-border bg-background py-1 pr-1.5 pl-2",
1611
+ children: [
1612
+ /* @__PURE__ */ jsx(attachmentIcon(a), {
1613
+ size: 14,
1614
+ "aria-hidden": true,
1615
+ className: "shrink-0 text-muted-foreground"
1616
+ }),
1617
+ /* @__PURE__ */ jsxs("span", {
1618
+ className: "flex min-w-0 flex-1 text-xs",
1619
+ title: name,
1620
+ children: [/* @__PURE__ */ jsx("span", {
1621
+ className: "truncate",
1622
+ children: stem
1623
+ }), ext && /* @__PURE__ */ jsx("span", {
1624
+ className: "shrink-0 text-muted-foreground",
1625
+ children: ext
1626
+ })]
1627
+ }),
1628
+ size && /* @__PURE__ */ jsx("span", {
1629
+ className: "shrink-0 text-[0.6875rem] tabular-nums text-muted-foreground",
1630
+ children: size
1631
+ }),
1632
+ onRemove && /* @__PURE__ */ jsx("button", {
1633
+ type: "button",
1634
+ "aria-label": t("webchat.removeAttachment", { name }),
1635
+ title: t("webchat.removeAttachment", { name }),
1636
+ className: "shrink-0 rounded p-0.5 text-muted-foreground hover:text-destructive focus-visible:outline-2 focus-visible:outline-ring",
1637
+ onClick: onRemove,
1638
+ children: /* @__PURE__ */ jsx(X, { size: 13 })
1639
+ })
1640
+ ]
1641
+ });
1642
+ }
1643
+ function ChatAttachments({ items, onRemove, className = "" }) {
1644
+ const { t } = useTranslation();
1645
+ const [expanded, setExpanded] = useState(false);
1646
+ const listId = useId();
1647
+ if (!items.length) return null;
1648
+ const hidden = expanded ? 0 : Math.max(0, items.length - COLLAPSE_AFTER);
1649
+ const shown = hidden ? items.slice(0, COLLAPSE_AFTER) : items;
1650
+ return /* @__PURE__ */ jsxs("div", {
1651
+ className: `flex flex-col gap-1 ${className}`,
1652
+ "data-chat-attachments": true,
1653
+ children: [/* @__PURE__ */ jsx("ul", {
1654
+ id: listId,
1655
+ "aria-label": t("webchat.attachments"),
1656
+ className: "flex flex-col gap-1",
1657
+ children: shown.map((a, i) => /* @__PURE__ */ jsx(AttachmentRow, {
1658
+ a,
1659
+ ...onRemove ? { onRemove: () => onRemove(i) } : {}
1660
+ }, `${a.name}-${i}`))
1661
+ }), items.length > COLLAPSE_AFTER && /* @__PURE__ */ jsxs("button", {
1662
+ type: "button",
1663
+ "aria-expanded": expanded,
1664
+ "aria-controls": listId,
1665
+ className: "inline-flex items-center gap-1 self-start rounded px-1 py-0.5 text-xs text-muted-foreground hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring",
1666
+ onClick: () => setExpanded((v) => !v),
1667
+ children: [/* @__PURE__ */ jsx(ChevronDown, {
1668
+ size: 12,
1669
+ "aria-hidden": true,
1670
+ className: expanded ? "rotate-180 transition-transform" : "transition-transform"
1671
+ }), expanded ? t("webchat.fewerFiles") : t("webchat.moreFiles", { count: hidden })]
1672
+ })]
1673
+ });
1674
+ }
1675
+ //#endregion
1379
1676
  //#region src/ui/ChatMessages.tsx
1380
1677
  function ChatMessages({ messages, pending, pendingReasoning, trailing }) {
1381
1678
  const { t } = useTranslation();
@@ -1389,8 +1686,15 @@ function ChatMessages({ messages, pending, pendingReasoning, trailing }) {
1389
1686
  "data-chat-list": true,
1390
1687
  className: "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-2",
1391
1688
  children: [!messages.length && pending === null && pendingReasoning === null ? /* @__PURE__ */ jsx("div", {
1392
- className: "flex-1 grid place-items-center text-muted-foreground text-sm",
1393
- children: t("webchat.empty")
1689
+ className: "grid flex-1 place-items-center px-6 text-center text-sm text-muted-foreground",
1690
+ children: /* @__PURE__ */ jsxs("span", {
1691
+ className: "flex flex-col items-center gap-2",
1692
+ children: [/* @__PURE__ */ jsx(MessagesSquare, {
1693
+ size: 28,
1694
+ "aria-hidden": true,
1695
+ className: "text-faint"
1696
+ }), t("webchat.empty")]
1697
+ })
1394
1698
  }) : /* @__PURE__ */ jsxs(Fragment, { children: [messages.map((m) => /* @__PURE__ */ jsx(Bubble, {
1395
1699
  role: m.role,
1396
1700
  text: m.text,
@@ -1412,23 +1716,23 @@ function ChatMessages({ messages, pending, pendingReasoning, trailing }) {
1412
1716
  var Bubble = memo(function Bubble({ role, text, reasoning, attachments }) {
1413
1717
  const { t } = useTranslation();
1414
1718
  if (role === "user") return /* @__PURE__ */ jsxs("div", {
1415
- className: "self-end max-w-[85%] rounded-2xl border border-ring bg-muted px-3 py-2 text-[0.8125rem] whitespace-pre-wrap break-words",
1416
- children: [attachments?.length ? /* @__PURE__ */ jsx("div", {
1417
- className: "mb-1.5 flex flex-wrap gap-1",
1418
- children: attachments.map((a) => /* @__PURE__ */ jsx("span", {
1419
- className: "inline-flex max-w-full items-center rounded-md bg-background px-1.5 py-0.5 text-xs",
1420
- title: a.name,
1421
- children: a.label ?? a.name
1422
- }, a.name))
1719
+ className: "max-w-[85%] min-w-0 self-end rounded-2xl rounded-br-sm border border-border bg-accent px-3 py-2 text-[0.8125rem] break-words whitespace-pre-wrap",
1720
+ children: [attachments?.length ? /* @__PURE__ */ jsx(ChatAttachments, {
1721
+ items: attachments,
1722
+ className: `-mx-1 ${text ? "mb-2" : ""}`
1423
1723
  }) : null, text]
1424
1724
  });
1425
1725
  return /* @__PURE__ */ jsxs("div", {
1426
- className: "self-start max-w-[95%] min-w-0 rounded-2xl bg-muted px-3 py-2 text-sm break-words",
1726
+ className: "max-w-[95%] min-w-0 self-start rounded-2xl rounded-bl-sm bg-muted px-3 py-2 text-sm break-words",
1427
1727
  children: [reasoning ? /* @__PURE__ */ jsxs("details", {
1428
- className: "mb-2 rounded-lg border border-border px-2 py-1",
1429
- children: [/* @__PURE__ */ jsx("summary", {
1430
- className: "cursor-pointer select-none text-xs text-muted-foreground",
1431
- children: t("webchat.reasoning")
1728
+ className: "group/reason -mx-1 mb-2 rounded-[calc(var(--radius-md)+1px)] border border-border bg-background px-2 py-1",
1729
+ children: [/* @__PURE__ */ jsxs("summary", {
1730
+ className: "flex cursor-pointer list-none items-center gap-1 select-none text-xs text-muted-foreground hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring [&::-webkit-details-marker]:hidden",
1731
+ children: [/* @__PURE__ */ jsx(ChevronRight, {
1732
+ size: 12,
1733
+ "aria-hidden": true,
1734
+ className: "shrink-0 transition-transform group-open/reason:rotate-90"
1735
+ }), t("webchat.reasoning")]
1432
1736
  }), /* @__PURE__ */ jsx(ChatMarkdown, {
1433
1737
  text: reasoning,
1434
1738
  className: "md-static md-chat md-chat-sub mt-1 text-muted-foreground"
@@ -1441,81 +1745,23 @@ var Bubble = memo(function Bubble({ role, text, reasoning, attachments }) {
1441
1745
  });
1442
1746
  //#endregion
1443
1747
  //#region src/ui/AgentPicker.tsx
1444
- var MAX_STARS = 5;
1445
- /** Rating pair: "3 star, 3 clock". Two digits read at a glance and stay the same
1446
- * width on every row, which a free-text hint ("fastest and cheapest" next to
1447
- * "quick") does not. Both are read the same way — the star is what you get, the
1448
- * clock is what it costs you in waiting — so a heavier model raises both numbers.
1449
- * Falls back to the hint when the agent publishes no ratings. */
1450
- function PresetRating({ quality, time, hint }) {
1451
- const { t } = useTranslation();
1452
- if (quality === void 0 || time === void 0) return hint ? /* @__PURE__ */ jsx("span", {
1453
- className: "shrink-0 text-muted-foreground",
1454
- children: hint
1455
- }) : null;
1456
- const both = quality !== time;
1457
- return /* @__PURE__ */ jsxs("span", {
1458
- className: "flex shrink-0 items-center gap-1.5 text-muted-foreground tabular-nums",
1459
- children: [/* @__PURE__ */ jsxs("span", {
1460
- className: "flex items-center gap-0.5",
1461
- title: t("webchat.quality", { value: quality }),
1462
- children: [
1463
- quality,
1464
- /* @__PURE__ */ jsx(Star, {
1465
- size: 11,
1466
- "aria-hidden": true,
1467
- className: "shrink-0"
1468
- }),
1469
- /* @__PURE__ */ jsx("span", {
1470
- className: "sr-only",
1471
- children: t("webchat.quality", { value: quality })
1472
- })
1473
- ]
1474
- }), both ? /* @__PURE__ */ jsxs("span", {
1475
- className: "flex items-center gap-0.5",
1476
- title: t("webchat.time", { value: time }),
1477
- children: [
1478
- time,
1479
- /* @__PURE__ */ jsx(Clock, {
1480
- size: 11,
1481
- "aria-hidden": true,
1482
- className: "shrink-0"
1483
- }),
1484
- /* @__PURE__ */ jsx("span", {
1485
- className: "sr-only",
1486
- children: t("webchat.time", { value: time })
1487
- })
1488
- ]
1489
- }) : /* @__PURE__ */ jsx("span", {
1490
- className: "sr-only",
1491
- children: t("webchat.time", { value: time })
1492
- })]
1493
- });
1494
- }
1495
1748
  /**
1496
- * The presets of ONE agent, ordered by their level, when they form a complete ladder:
1497
- * a unique `quality` per preset, contiguous from 1, at most five rungs. Only then does a
1498
- * star count address a preset without ambiguity, and only then may the picker stop
1499
- * printing model names. Anything else two agents, a gap in the ladder, a preset with
1500
- * no rating returns null and the picker keeps its list of names.
1749
+ * Flatten the catalog to one list: with a single registered agent the agent level is
1750
+ * noise, and with several the agent name simply prefixes its presets.
1751
+ *
1752
+ * ONE mode, a list of names. Until 2026-09-16 there was a second a row of stars in the
1753
+ * composer, where a preset's `quality` rating addressed it by rung and the model name was
1754
+ * never printed. It is gone, with the ratings it read. A star count can only address a
1755
+ * preset when the presets form a single totally ordered line, and `claude-agent`'s do
1756
+ * not: nothing published ranks `sonnet-high` against `opus-low`, and the cheaper of the
1757
+ * two is not the weaker one. The list that used to be the FALLBACK — for several agents,
1758
+ * a gap in the ladder, a preset with no rating — was always the honest control, and is
1759
+ * now the only one. See
1760
+ * `docs/superpowers/specs/2026-09-16-agent-preset-is-a-model-choice-design.md`.
1501
1761
  */
1502
- function starLadder(rows, multi) {
1503
- if (multi || rows.length < 2 || rows.length > MAX_STARS) return null;
1504
- const byLevel = Array.from({ length: rows.length });
1505
- for (const r of rows) {
1506
- const q = r.quality;
1507
- if (q === void 0 || !Number.isInteger(q) || q < 1 || q > rows.length) return null;
1508
- if (byLevel[q - 1]) return null;
1509
- byLevel[q - 1] = r;
1510
- }
1511
- return byLevel.every((r) => r !== void 0) ? byLevel : null;
1512
- }
1513
- /** Flatten the catalog to one list: with a single registered agent the agent
1514
- * level is noise, and with several the agent name simply prefixes its presets. */
1515
1762
  function AgentPicker({ agents, defaultAgentId, selection, onChange }) {
1516
1763
  const { t } = useTranslation();
1517
1764
  const [open, setOpen] = useState(false);
1518
- const [preview, setPreview] = useState(null);
1519
1765
  const rootRef = useRef(null);
1520
1766
  const triggerRef = useRef(null);
1521
1767
  useEffect(() => {
@@ -1542,48 +1788,10 @@ function AgentPicker({ agents, defaultAgentId, selection, onChange }) {
1542
1788
  presetId: p.id,
1543
1789
  label: multi ? `${a.title} · ${p.title}` : p.title,
1544
1790
  hint: p.hint,
1545
- quality: p.quality,
1546
- time: p.time,
1547
1791
  isDefault: p.default === true
1548
1792
  })));
1549
1793
  if (rows.length === 0) return null;
1550
1794
  const current = rows.find((r) => r.agentId === selection.agentId && r.presetId === selection.presetId) ?? rows.find((r) => r.agentId === defaultAgentId && r.isDefault) ?? rows.find((r) => r.isDefault) ?? rows[0];
1551
- const ladder = starLadder(rows, multi);
1552
- if (ladder) {
1553
- const level = ladder.indexOf(current) + 1;
1554
- const shown = preview ?? level;
1555
- const levelName = (row, n) => `${row.label} · ${t("webchat.level", {
1556
- value: n,
1557
- max: ladder.length
1558
- })}`;
1559
- return /* @__PURE__ */ jsx("ul", {
1560
- "aria-label": t("webchat.agentPicker"),
1561
- className: "flex shrink-0 items-center",
1562
- onPointerLeave: () => setPreview(null),
1563
- children: ladder.map((r, i) => {
1564
- const n = i + 1;
1565
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
1566
- type: "button",
1567
- ...r === current ? { "aria-current": true } : {},
1568
- "aria-label": levelName(r, n),
1569
- className: "flex rounded p-1 focus-visible:outline-2 focus-visible:outline-ring",
1570
- onPointerEnter: () => setPreview(n),
1571
- onFocus: () => setPreview(n),
1572
- onBlur: () => setPreview(null),
1573
- onClick: () => onChange({
1574
- agentId: r.agentId,
1575
- presetId: r.presetId
1576
- }),
1577
- children: /* @__PURE__ */ jsx(Star, {
1578
- size: 16,
1579
- "aria-hidden": true,
1580
- className: n <= shown ? "fill-current" : "text-border",
1581
- style: n <= shown ? { color: toneVar("yellow") } : void 0
1582
- })
1583
- }) }, `${r.agentId}/${r.presetId}`);
1584
- })
1585
- });
1586
- }
1587
1795
  return /* @__PURE__ */ jsxs("div", {
1588
1796
  className: "relative",
1589
1797
  ref: rootRef,
@@ -1596,7 +1804,7 @@ function AgentPicker({ agents, defaultAgentId, selection, onChange }) {
1596
1804
  onClick: () => setOpen((v) => !v),
1597
1805
  children: [/* @__PURE__ */ jsx("span", {
1598
1806
  className: "max-w-40 truncate",
1599
- children: current.label
1807
+ children: current.hint ? `${current.label} · ${current.hint}` : current.label
1600
1808
  }), /* @__PURE__ */ jsx(ChevronDown, {
1601
1809
  size: 12,
1602
1810
  className: "shrink-0"
@@ -1626,11 +1834,10 @@ function AgentPicker({ agents, defaultAgentId, selection, onChange }) {
1626
1834
  className: "min-w-0 flex-1 truncate",
1627
1835
  children: r.label
1628
1836
  }),
1629
- /* @__PURE__ */ jsx(PresetRating, {
1630
- quality: r.quality,
1631
- time: r.time,
1632
- hint: r.hint
1633
- })
1837
+ r.hint ? /* @__PURE__ */ jsx("span", {
1838
+ className: "shrink-0 text-muted-foreground",
1839
+ children: r.hint
1840
+ }) : null
1634
1841
  ]
1635
1842
  }) }, `${r.agentId}/${r.presetId}`);
1636
1843
  })
@@ -1649,29 +1856,12 @@ function ChatComposer({ streaming, onSend, onStop, attachments, uploading, uploa
1649
1856
  return /* @__PURE__ */ jsxs("div", {
1650
1857
  className: "flex shrink-0 flex-col gap-2 border-t border-border p-2",
1651
1858
  children: [(attachments.length > 0 || uploading || uploadError) && /* @__PURE__ */ jsxs("div", {
1652
- className: "flex flex-wrap items-center gap-1.5 px-1",
1653
- "data-chat-attachments": true,
1859
+ className: "flex flex-col gap-1.5 px-1",
1654
1860
  children: [
1655
- attachments.map((a, i) => /* @__PURE__ */ jsxs("span", {
1656
- className: "inline-flex min-w-0 max-w-full items-center gap-1 rounded-md bg-muted px-2 py-1 text-xs",
1657
- children: [
1658
- /* @__PURE__ */ jsx(FileText, {
1659
- size: 13,
1660
- className: "shrink-0 text-muted-foreground"
1661
- }),
1662
- /* @__PURE__ */ jsx("span", {
1663
- className: "max-w-48 truncate",
1664
- children: a.label ?? a.name
1665
- }),
1666
- /* @__PURE__ */ jsx("button", {
1667
- type: "button",
1668
- "aria-label": t("webchat.removeAttachment", { name: a.label ?? a.name }),
1669
- className: "shrink-0 rounded p-0.5 text-muted-foreground hover:text-destructive",
1670
- onClick: () => onRemoveAttachment(i),
1671
- children: /* @__PURE__ */ jsx(X, { size: 13 })
1672
- })
1673
- ]
1674
- }, `${a.name}-${i}`)),
1861
+ /* @__PURE__ */ jsx(ChatAttachments, {
1862
+ items: attachments,
1863
+ onRemove: onRemoveAttachment
1864
+ }),
1675
1865
  uploading && /* @__PURE__ */ jsxs("div", {
1676
1866
  className: "flex min-w-32 flex-1 items-center gap-2",
1677
1867
  "aria-label": t("webchat.uploading"),
@@ -1705,7 +1895,7 @@ function ChatComposer({ streaming, onSend, onStop, attachments, uploading, uploa
1705
1895
  children: t(uploadError)
1706
1896
  }), canRetryUpload && /* @__PURE__ */ jsxs("button", {
1707
1897
  type: "button",
1708
- className: "inline-flex shrink-0 items-center gap-1 rounded px-1.5 py-0.5 font-medium text-primary hover:bg-muted",
1898
+ className: "inline-flex shrink-0 items-center gap-1 rounded px-1.5 py-0.5 font-medium text-primary hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring",
1709
1899
  onClick: onRetryUpload,
1710
1900
  children: [
1711
1901
  /* @__PURE__ */ jsx(RotateCcw, { size: 12 }),
@@ -1718,7 +1908,7 @@ function ChatComposer({ streaming, onSend, onStop, attachments, uploading, uploa
1718
1908
  }), /* @__PURE__ */ jsxs("div", {
1719
1909
  className: "flex flex-col gap-1",
1720
1910
  children: [/* @__PURE__ */ jsx("textarea", {
1721
- className: "w-full resize-none rounded-lg border border-border bg-background px-3 py-2 text-sm max-h-32",
1911
+ className: "max-h-32 w-full resize-none rounded-lg border border-border bg-background px-3 py-2 text-sm transition-colors outline-none placeholder:text-faint focus:border-ring focus:ring-2 focus:ring-ring",
1722
1912
  rows: 1,
1723
1913
  value: text,
1724
1914
  placeholder: t("webchat.placeholder"),
@@ -1733,12 +1923,16 @@ function ChatComposer({ streaming, onSend, onStop, attachments, uploading, uploa
1733
1923
  className: "flex items-center gap-1",
1734
1924
  children: [
1735
1925
  attachmentsAllowed && /* @__PURE__ */ jsxs("label", {
1736
- "aria-label": t("webchat.attach"),
1737
- className: `shrink-0 cursor-pointer rounded-lg p-1.5 text-muted-foreground hover:bg-muted hover:text-primary ${uploading ? "pointer-events-none opacity-60" : ""}`,
1738
- children: [/* @__PURE__ */ jsx(Paperclip, { size: 18 }), /* @__PURE__ */ jsx("input", {
1926
+ title: t("webchat.attach"),
1927
+ className: `flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-within:ring-2 focus-within:ring-ring ${uploading ? "pointer-events-none opacity-60" : ""}`,
1928
+ children: [/* @__PURE__ */ jsx(Paperclip, {
1929
+ size: 18,
1930
+ "aria-hidden": true
1931
+ }), /* @__PURE__ */ jsx("input", {
1739
1932
  type: "file",
1740
1933
  multiple: true,
1741
- className: "hidden",
1934
+ "aria-label": t("webchat.attach"),
1935
+ className: "sr-only",
1742
1936
  disabled: uploading,
1743
1937
  onChange: (e) => {
1744
1938
  if (e.target.files?.length) onPickFiles(e.target.files);
@@ -1753,14 +1947,23 @@ function ChatComposer({ streaming, onSend, onStop, attachments, uploading, uploa
1753
1947
  onChange: onChooseAgent
1754
1948
  }),
1755
1949
  /* @__PURE__ */ jsx("div", { className: "flex-1" }),
1756
- streaming ? /* @__PURE__ */ jsx("button", {
1950
+ streaming ? /* @__PURE__ */ jsx(Button, {
1951
+ type: "button",
1952
+ variant: "ghost",
1953
+ size: "iconSm",
1757
1954
  "aria-label": t("webchat.stop"),
1758
- className: "shrink-0 rounded-lg p-1.5 hover:bg-muted",
1955
+ title: t("webchat.stop"),
1956
+ className: "shrink-0",
1759
1957
  onClick: onStop,
1760
1958
  children: /* @__PURE__ */ jsx(Square, { size: 18 })
1761
- }) : /* @__PURE__ */ jsx("button", {
1959
+ }) : /* @__PURE__ */ jsx(Button, {
1960
+ type: "button",
1961
+ variant: "ghost",
1962
+ size: "iconSm",
1762
1963
  "aria-label": t("webchat.send"),
1763
- className: "shrink-0 rounded-lg p-1.5 hover:bg-muted",
1964
+ title: t("webchat.send"),
1965
+ disabled: !text.trim(),
1966
+ className: "shrink-0 hover:text-primary",
1764
1967
  onClick: submit,
1765
1968
  children: /* @__PURE__ */ jsx(Send, { size: 18 })
1766
1969
  })
@@ -1775,12 +1978,15 @@ function ChatThreadList({ threads, activeId, onOpen, onNew, onTogglePrivate, cla
1775
1978
  const { t } = useTranslation();
1776
1979
  return /* @__PURE__ */ jsxs("aside", {
1777
1980
  "aria-label": t("webchat.threads"),
1778
- className: `${className} border-r border-border flex flex-col`,
1981
+ className: `${className} flex flex-col border-r border-border`,
1779
1982
  children: [/* @__PURE__ */ jsxs("button", {
1780
- className: "flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted",
1983
+ className: "flex items-center gap-2 border-b border-border px-3 py-2 text-sm font-medium text-primary hover:bg-muted focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring",
1781
1984
  onClick: onNew,
1782
1985
  children: [
1783
- /* @__PURE__ */ jsx(Plus, { size: 16 }),
1986
+ /* @__PURE__ */ jsx(Plus, {
1987
+ size: 16,
1988
+ "aria-hidden": true
1989
+ }),
1784
1990
  " ",
1785
1991
  t("webchat.newConversation")
1786
1992
  ]
@@ -1789,10 +1995,10 @@ function ChatThreadList({ threads, activeId, onOpen, onNew, onTogglePrivate, cla
1789
1995
  children: threads.map((th) => {
1790
1996
  const active = th.convId === activeId;
1791
1997
  return /* @__PURE__ */ jsxs("div", {
1792
- className: `flex items-center gap-1 pl-1 pr-2 ${active ? "bg-muted" : "hover:bg-muted"}`,
1998
+ className: `flex items-center gap-1 border-l-2 pr-2 pl-1 ${active ? "border-ring bg-accent" : "border-transparent hover:bg-muted"}`,
1793
1999
  children: [/* @__PURE__ */ jsxs("button", {
1794
2000
  onClick: () => onOpen(th.convId),
1795
- className: `min-w-0 flex-1 py-2 pl-2 text-left text-sm ${active ? "font-medium" : ""}`,
2001
+ className: `min-w-0 flex-1 rounded-sm py-2 pl-2 text-left text-sm focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring ${active ? "font-medium" : ""}`,
1796
2002
  children: [/* @__PURE__ */ jsx("span", {
1797
2003
  className: "block truncate",
1798
2004
  children: th.title || t("webchat.newConversation")
@@ -2047,36 +2253,49 @@ function ChatWidget() {
2047
2253
  const hasConversation = chat.messages.length > 0 || chat.pending !== null;
2048
2254
  const activeTitle = chat.threads.find((th) => th.convId === chat.convId)?.title;
2049
2255
  const header = /* @__PURE__ */ jsxs("div", {
2050
- className: "flex shrink-0 items-center justify-between gap-2 border-b border-border px-3 py-2",
2256
+ className: "flex shrink-0 items-center justify-between gap-2 border-b border-border px-3 py-1.5",
2051
2257
  children: [/* @__PURE__ */ jsx("span", {
2052
2258
  className: "min-w-0 truncate text-sm font-medium",
2053
2259
  children: activeTitle || t("webchat.title")
2054
2260
  }), /* @__PURE__ */ jsxs("div", {
2055
- className: "flex shrink-0 items-center gap-1",
2261
+ className: "-mr-1 flex shrink-0 items-center gap-0.5",
2056
2262
  children: [
2057
- !hasSidebar && /* @__PURE__ */ jsx("button", {
2263
+ hasConversation && /* @__PURE__ */ jsx(Button, {
2264
+ type: "button",
2265
+ variant: "ghost",
2266
+ size: "iconSm",
2267
+ "aria-label": t("webchat.newConversation"),
2268
+ title: t("webchat.newConversation"),
2269
+ onClick: startNew,
2270
+ children: /* @__PURE__ */ jsx(Plus, { size: 16 })
2271
+ }),
2272
+ !hasSidebar && /* @__PURE__ */ jsx(Button, {
2273
+ type: "button",
2274
+ variant: "ghost",
2275
+ size: "iconSm",
2058
2276
  "aria-label": t("webchat.threads"),
2277
+ title: t("webchat.threads"),
2059
2278
  "aria-expanded": threadsOpen,
2060
- className: "rounded p-1 hover:bg-muted",
2279
+ className: threadsOpen ? "bg-muted text-foreground" : "",
2061
2280
  onClick: () => setThreadsOpen((v) => !v),
2062
2281
  children: /* @__PURE__ */ jsx(List, { size: 16 })
2063
2282
  }),
2064
- hasConversation && /* @__PURE__ */ jsx("button", {
2065
- "aria-label": t("webchat.newConversation"),
2066
- className: "rounded p-1 hover:bg-muted",
2067
- onClick: startNew,
2068
- children: /* @__PURE__ */ jsx(Plus, { size: 16 })
2069
- }),
2070
- chatModes(wide).includes("panel") && /* @__PURE__ */ jsx("button", {
2283
+ chatModes(wide).includes("panel") && /* @__PURE__ */ jsx(Button, {
2071
2284
  ref: expandBtnRef,
2285
+ type: "button",
2286
+ variant: "ghost",
2287
+ size: "iconSm",
2072
2288
  "aria-label": mode === "modal" ? t("webchat.collapse") : t("webchat.expand"),
2073
- className: "rounded p-1 hover:bg-muted",
2289
+ title: mode === "modal" ? t("webchat.collapse") : t("webchat.expand"),
2074
2290
  onClick: () => setMode(stepMode(mode, wide, mode === "modal" ? -1 : 1)),
2075
2291
  children: mode === "modal" ? /* @__PURE__ */ jsx(Minimize2, { size: 16 }) : /* @__PURE__ */ jsx(Maximize2, { size: 16 })
2076
2292
  }),
2077
- /* @__PURE__ */ jsx("button", {
2293
+ /* @__PURE__ */ jsx(Button, {
2294
+ type: "button",
2295
+ variant: "ghost",
2296
+ size: "iconSm",
2078
2297
  "aria-label": t("webchat.close"),
2079
- className: "rounded p-1 hover:bg-muted",
2298
+ title: t("webchat.close"),
2080
2299
  onClick: () => setMode("bubble"),
2081
2300
  children: /* @__PURE__ */ jsx(X, { size: 16 })
2082
2301
  })
package/dist/web.js CHANGED
@@ -11,7 +11,7 @@ import { definePluginUI } from "@coffer-org/web-ui";
11
11
  var ui_default = definePluginUI({ slots: [{
12
12
  slot: "overlay",
13
13
  id: "webchat",
14
- load: () => import("./ChatWidget-BhGpxS9f.js")
14
+ load: () => import("./ChatWidget-BIPLpTMp.js")
15
15
  }] });
16
16
  //#endregion
17
17
  export { ui_default as default };
package/locales/en.json CHANGED
@@ -21,6 +21,10 @@
21
21
  "stop": "Stop",
22
22
  "attach": "Attach file",
23
23
  "removeAttachment": "Remove {{name}}",
24
+ "attachments": "Attached files",
25
+ "moreFiles_one": "{{count}} more file",
26
+ "moreFiles_other": "{{count}} more files",
27
+ "fewerFiles": "Show fewer",
24
28
  "uploading": "Uploading…",
25
29
  "uploadProgress": "Upload progress",
26
30
  "uploadTooLarge": "File is too large (maximum 25 MB).",
@@ -32,9 +36,6 @@
32
36
  "error": "Error: {{message}}",
33
37
  "reasoning": "Reasoning",
34
38
  "agentPicker": "Agent and preset",
35
- "level": "Level {{value}} of {{max}}",
36
- "quality": "Quality {{value}} of 5",
37
- "time": "Time {{value}} of 5",
38
39
  "settings": {
39
40
  "label": "Web chat",
40
41
  "reasoning_display": "Reasoning display",
package/locales/ru.json CHANGED
@@ -21,6 +21,11 @@
21
21
  "stop": "Стоп",
22
22
  "attach": "Прикрепить файл",
23
23
  "removeAttachment": "Удалить {{name}}",
24
+ "attachments": "Прикреплённые файлы",
25
+ "moreFiles_one": "ещё {{count}} файл",
26
+ "moreFiles_few": "ещё {{count}} файла",
27
+ "moreFiles_many": "ещё {{count}} файлов",
28
+ "fewerFiles": "Свернуть",
24
29
  "uploading": "Загрузка…",
25
30
  "uploadProgress": "Прогресс загрузки",
26
31
  "uploadTooLarge": "Файл слишком большой (максимум 25 МБ).",
@@ -32,9 +37,6 @@
32
37
  "error": "Ошибка: {{message}}",
33
38
  "reasoning": "Рассуждения",
34
39
  "agentPicker": "Агент и пресет",
35
- "level": "Уровень {{value}} из {{max}}",
36
- "quality": "Качество {{value}} из 5",
37
- "time": "Время {{value}} из 5",
38
40
  "settings": {
39
41
  "label": "Чат на сайте",
40
42
  "reasoning_display": "Показ рассуждений",
package/locales/uk.json CHANGED
@@ -21,6 +21,11 @@
21
21
  "stop": "Стоп",
22
22
  "attach": "Додати файл",
23
23
  "removeAttachment": "Видалити {{name}}",
24
+ "attachments": "Прикріплені файли",
25
+ "moreFiles_one": "ще {{count}} файл",
26
+ "moreFiles_few": "ще {{count}} файли",
27
+ "moreFiles_many": "ще {{count}} файлів",
28
+ "fewerFiles": "Згорнути",
24
29
  "uploading": "Завантаження…",
25
30
  "uploadProgress": "Прогрес завантаження",
26
31
  "uploadTooLarge": "Файл завеликий (максимум 25 МБ).",
@@ -32,9 +37,6 @@
32
37
  "error": "Помилка: {{message}}",
33
38
  "reasoning": "Міркування",
34
39
  "agentPicker": "Агент і пресет",
35
- "level": "Рівень {{value}} з {{max}}",
36
- "quality": "Якість {{value}} з 5",
37
- "time": "Час {{value}} з 5",
38
40
  "settings": {
39
41
  "label": "Чат на сайті",
40
42
  "reasoning_display": "Показ міркувань",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffer-org/plugin-webchat",
3
- "version": "7.1.0",
3
+ "version": "7.2.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -28,8 +28,8 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@coffer-org/markdown": "^7.4.0",
31
- "@coffer-org/sdk": "^7.4.0",
32
- "@coffer-org/server": "^7.4.0",
31
+ "@coffer-org/sdk": "^7.4.1",
32
+ "@coffer-org/server": "^7.5.0",
33
33
  "lucide-react": "^1.17.0"
34
34
  },
35
35
  "coffer": {