@coffer-org/plugin-webchat 7.1.0 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ChatWidget-BhGpxS9f.js → ChatWidget-DocoFt8q.js} +430 -200
- package/dist/runtime/format.js +9 -1
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/refresh-tool.d.ts +2 -0
- package/dist/runtime/refresh-tool.js +49 -0
- package/dist/runtime/send.js +2 -0
- package/dist/web.js +1 -1
- package/locales/en.json +4 -3
- package/locales/ru.json +5 -3
- package/locales/uk.json +5 -3
- package/package.json +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as __fedReact from "react";
|
|
2
|
-
import { Dialog, DialogContent, Icon, MarkdownView,
|
|
3
|
-
import { createContext, createElement, forwardRef, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Button, Dialog, DialogContent, Icon, MarkdownView, fileSizeLabel, recordGone, refreshData, 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
|
|
500
|
-
|
|
501
|
-
|
|
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",
|
|
@@ -897,6 +1051,12 @@ async function openChannel(convId, handlers, signal) {
|
|
|
897
1051
|
parentMsgId: String(data["parentMsgId"] ?? ""),
|
|
898
1052
|
suggestions: Array.isArray(data["suggestions"]) ? data["suggestions"] : null
|
|
899
1053
|
});
|
|
1054
|
+
else if (frame.event === "refresh") handlers.onRefresh({
|
|
1055
|
+
...typeof data["library"] === "string" ? { library: data["library"] } : {},
|
|
1056
|
+
...typeof data["shelf"] === "string" ? { shelf: data["shelf"] } : {},
|
|
1057
|
+
...typeof data["id"] === "string" ? { id: data["id"] } : {},
|
|
1058
|
+
...data["gone"] === true ? { gone: true } : {}
|
|
1059
|
+
});
|
|
900
1060
|
else if (frame.event === "error") handlers.onError({
|
|
901
1061
|
message: data["message"] ?? null,
|
|
902
1062
|
parentMsgId: typeof data["parentMsgId"] === "string" ? data["parentMsgId"] : null
|
|
@@ -1091,6 +1251,11 @@ function useChat() {
|
|
|
1091
1251
|
kind: "done",
|
|
1092
1252
|
data
|
|
1093
1253
|
}),
|
|
1254
|
+
onRefresh: ({ gone, ...scope }) => {
|
|
1255
|
+
if (cancelled) return;
|
|
1256
|
+
if (gone) recordGone(scope);
|
|
1257
|
+
else refreshData(scope);
|
|
1258
|
+
},
|
|
1094
1259
|
onError: (data) => dispatch({
|
|
1095
1260
|
kind: "error",
|
|
1096
1261
|
data
|
|
@@ -1376,6 +1541,149 @@ function ChatMarkdown({ text, className }) {
|
|
|
1376
1541
|
});
|
|
1377
1542
|
}
|
|
1378
1543
|
//#endregion
|
|
1544
|
+
//#region src/ui/ChatAttachments.tsx
|
|
1545
|
+
/**
|
|
1546
|
+
* The ONE presentation of a chat attachment — the files staged in the composer and the files a
|
|
1547
|
+
* sent turn carries are drawn by this component, so the two cannot drift apart.
|
|
1548
|
+
*
|
|
1549
|
+
* Three decisions are worth stating, because each replaces something that was there before:
|
|
1550
|
+
*
|
|
1551
|
+
* - **A file is a ROW, not a word.** The bubble used to print `a.label ?? a.name` as bare text
|
|
1552
|
+
* in a wrapping flex, which for eight uploads meant eight lines of 40-character storage names
|
|
1553
|
+
* running to the bubble's edge and clipping mid-name. A row states the three facts a file has
|
|
1554
|
+
* — what kind it is, what it is called, how big it is — and states them in the same places
|
|
1555
|
+
* every time, which is what makes a stack of them scannable.
|
|
1556
|
+
* - **The name breaks in the MIDDLE, not at the end.** The extension is the most informative
|
|
1557
|
+
* part of a long filename and the part a tail-truncation always eats. The stem takes the
|
|
1558
|
+
* truncation (`truncate`), the extension is pinned beside it (`shrink-0`), so `…_2026.pdf`
|
|
1559
|
+
* keeps saying `.pdf` at any width. CSS does this, not a character budget — the same row
|
|
1560
|
+
* works in a 380px panel and in the 1040px modal without measuring anything.
|
|
1561
|
+
* - **Past two files the group COLLAPSES.** A turn carrying eight PDFs is a turn whose text a
|
|
1562
|
+
* reader still has to reach; the attachments are its luggage, not its content. Two rows say
|
|
1563
|
+
* what was sent, `+N` says how much more there is, and one click shows the rest.
|
|
1564
|
+
*
|
|
1565
|
+
* The rows are inert on purpose: an attachment names itself and does nothing else. Opening one
|
|
1566
|
+
* would be a second file-viewing surface beside the record page's, and the chat is not where a
|
|
1567
|
+
* file is read (decided 2026-09-15).
|
|
1568
|
+
*/
|
|
1569
|
+
/** How many rows stand before the rest fold away. Two is what a bubble can carry without the
|
|
1570
|
+
* luggage outweighing the message — see the component's note. */
|
|
1571
|
+
var COLLAPSE_AFTER = 2;
|
|
1572
|
+
/** Extension families, in the order a name is tested against them. */
|
|
1573
|
+
var BY_EXT = [
|
|
1574
|
+
[/\.(png|jpe?g|gif|webp|avif|svg|bmp|heic|heif)$/i, FileImage],
|
|
1575
|
+
[/\.(mp3|wav|ogg|oga|flac|m4a|aac|opus)$/i, FileHeadphone],
|
|
1576
|
+
[/\.(mp4|mov|mkv|webm|avi|m4v)$/i, FilePlay],
|
|
1577
|
+
[/\.(zip|rar|7z|tar|gz|tgz|bz2|xz)$/i, FileArchive],
|
|
1578
|
+
[/\.(csv|tsv|xlsx?|ods|numbers)$/i, FileSpreadsheet],
|
|
1579
|
+
[/\.(json|xml|ya?ml|toml|ini|js|jsx|ts|tsx|css|html?|sh|py|sql)$/i, FileCode]
|
|
1580
|
+
];
|
|
1581
|
+
/**
|
|
1582
|
+
* What KIND of file this is, at a glance. The mime type is the server's answer and is trusted
|
|
1583
|
+
* first; an upload that arrives with none (or with the `application/octet-stream` a browser
|
|
1584
|
+
* falls back to) is read from its extension instead, which is the only fact left.
|
|
1585
|
+
*/
|
|
1586
|
+
function attachmentIcon(a) {
|
|
1587
|
+
const mime = a.mime?.toLowerCase().split(";", 1)[0] ?? "";
|
|
1588
|
+
if (mime.startsWith("image/")) return FileImage;
|
|
1589
|
+
if (mime.startsWith("audio/")) return FileHeadphone;
|
|
1590
|
+
if (mime.startsWith("video/")) return FilePlay;
|
|
1591
|
+
if (mime === "text/csv" || mime.includes("spreadsheet") || mime.includes("excel")) return FileSpreadsheet;
|
|
1592
|
+
if (mime === "application/json" || mime === "application/xml" || mime.startsWith("text/x")) return FileCode;
|
|
1593
|
+
if (mime === "application/zip" || mime.includes("compressed") || mime.includes("tar")) return FileArchive;
|
|
1594
|
+
const name = a.label ?? a.name;
|
|
1595
|
+
for (const [re, icon] of BY_EXT) if (re.test(name)) return icon;
|
|
1596
|
+
return FileText;
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Split a filename into the part that may be truncated and the part that may not. A leading dot
|
|
1600
|
+
* (`.gitignore`) is not an extension, and neither is a "dot" eleven characters from the end of a
|
|
1601
|
+
* sentence-like name — an extension is short, so anything longer is left in the stem where the
|
|
1602
|
+
* truncation can reach it.
|
|
1603
|
+
*/
|
|
1604
|
+
function splitFileName(name) {
|
|
1605
|
+
const dot = name.lastIndexOf(".");
|
|
1606
|
+
if (dot <= 0 || name.length - dot > 11) return {
|
|
1607
|
+
stem: name,
|
|
1608
|
+
ext: ""
|
|
1609
|
+
};
|
|
1610
|
+
return {
|
|
1611
|
+
stem: name.slice(0, dot),
|
|
1612
|
+
ext: name.slice(dot)
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
function AttachmentRow({ a, onRemove }) {
|
|
1616
|
+
const { t } = useTranslation();
|
|
1617
|
+
const name = a.label ?? a.name;
|
|
1618
|
+
const { stem, ext } = splitFileName(name);
|
|
1619
|
+
const size = fileSizeLabel(a.size);
|
|
1620
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
1621
|
+
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",
|
|
1622
|
+
children: [
|
|
1623
|
+
/* @__PURE__ */ jsx(attachmentIcon(a), {
|
|
1624
|
+
size: 14,
|
|
1625
|
+
"aria-hidden": true,
|
|
1626
|
+
className: "shrink-0 text-muted-foreground"
|
|
1627
|
+
}),
|
|
1628
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1629
|
+
className: "flex min-w-0 flex-1 text-xs",
|
|
1630
|
+
title: name,
|
|
1631
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1632
|
+
className: "truncate",
|
|
1633
|
+
children: stem
|
|
1634
|
+
}), ext && /* @__PURE__ */ jsx("span", {
|
|
1635
|
+
className: "shrink-0 text-muted-foreground",
|
|
1636
|
+
children: ext
|
|
1637
|
+
})]
|
|
1638
|
+
}),
|
|
1639
|
+
size && /* @__PURE__ */ jsx("span", {
|
|
1640
|
+
className: "shrink-0 text-[0.6875rem] tabular-nums text-muted-foreground",
|
|
1641
|
+
children: size
|
|
1642
|
+
}),
|
|
1643
|
+
onRemove && /* @__PURE__ */ jsx("button", {
|
|
1644
|
+
type: "button",
|
|
1645
|
+
"aria-label": t("webchat.removeAttachment", { name }),
|
|
1646
|
+
title: t("webchat.removeAttachment", { name }),
|
|
1647
|
+
className: "shrink-0 rounded p-0.5 text-muted-foreground hover:text-destructive focus-visible:outline-2 focus-visible:outline-ring",
|
|
1648
|
+
onClick: onRemove,
|
|
1649
|
+
children: /* @__PURE__ */ jsx(X, { size: 13 })
|
|
1650
|
+
})
|
|
1651
|
+
]
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
function ChatAttachments({ items, onRemove, className = "" }) {
|
|
1655
|
+
const { t } = useTranslation();
|
|
1656
|
+
const [expanded, setExpanded] = useState(false);
|
|
1657
|
+
const listId = useId();
|
|
1658
|
+
if (!items.length) return null;
|
|
1659
|
+
const hidden = expanded ? 0 : Math.max(0, items.length - COLLAPSE_AFTER);
|
|
1660
|
+
const shown = hidden ? items.slice(0, COLLAPSE_AFTER) : items;
|
|
1661
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1662
|
+
className: `flex flex-col gap-1 ${className}`,
|
|
1663
|
+
"data-chat-attachments": true,
|
|
1664
|
+
children: [/* @__PURE__ */ jsx("ul", {
|
|
1665
|
+
id: listId,
|
|
1666
|
+
"aria-label": t("webchat.attachments"),
|
|
1667
|
+
className: "flex flex-col gap-1",
|
|
1668
|
+
children: shown.map((a, i) => /* @__PURE__ */ jsx(AttachmentRow, {
|
|
1669
|
+
a,
|
|
1670
|
+
...onRemove ? { onRemove: () => onRemove(i) } : {}
|
|
1671
|
+
}, `${a.name}-${i}`))
|
|
1672
|
+
}), items.length > COLLAPSE_AFTER && /* @__PURE__ */ jsxs("button", {
|
|
1673
|
+
type: "button",
|
|
1674
|
+
"aria-expanded": expanded,
|
|
1675
|
+
"aria-controls": listId,
|
|
1676
|
+
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",
|
|
1677
|
+
onClick: () => setExpanded((v) => !v),
|
|
1678
|
+
children: [/* @__PURE__ */ jsx(ChevronDown, {
|
|
1679
|
+
size: 12,
|
|
1680
|
+
"aria-hidden": true,
|
|
1681
|
+
className: expanded ? "rotate-180 transition-transform" : "transition-transform"
|
|
1682
|
+
}), expanded ? t("webchat.fewerFiles") : t("webchat.moreFiles", { count: hidden })]
|
|
1683
|
+
})]
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
//#endregion
|
|
1379
1687
|
//#region src/ui/ChatMessages.tsx
|
|
1380
1688
|
function ChatMessages({ messages, pending, pendingReasoning, trailing }) {
|
|
1381
1689
|
const { t } = useTranslation();
|
|
@@ -1389,8 +1697,15 @@ function ChatMessages({ messages, pending, pendingReasoning, trailing }) {
|
|
|
1389
1697
|
"data-chat-list": true,
|
|
1390
1698
|
className: "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-2",
|
|
1391
1699
|
children: [!messages.length && pending === null && pendingReasoning === null ? /* @__PURE__ */ jsx("div", {
|
|
1392
|
-
className: "flex-1
|
|
1393
|
-
children:
|
|
1700
|
+
className: "grid flex-1 place-items-center px-6 text-center text-sm text-muted-foreground",
|
|
1701
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
1702
|
+
className: "flex flex-col items-center gap-2",
|
|
1703
|
+
children: [/* @__PURE__ */ jsx(MessagesSquare, {
|
|
1704
|
+
size: 28,
|
|
1705
|
+
"aria-hidden": true,
|
|
1706
|
+
className: "text-faint"
|
|
1707
|
+
}), t("webchat.empty")]
|
|
1708
|
+
})
|
|
1394
1709
|
}) : /* @__PURE__ */ jsxs(Fragment, { children: [messages.map((m) => /* @__PURE__ */ jsx(Bubble, {
|
|
1395
1710
|
role: m.role,
|
|
1396
1711
|
text: m.text,
|
|
@@ -1412,23 +1727,23 @@ function ChatMessages({ messages, pending, pendingReasoning, trailing }) {
|
|
|
1412
1727
|
var Bubble = memo(function Bubble({ role, text, reasoning, attachments }) {
|
|
1413
1728
|
const { t } = useTranslation();
|
|
1414
1729
|
if (role === "user") return /* @__PURE__ */ jsxs("div", {
|
|
1415
|
-
className: "
|
|
1416
|
-
children: [attachments?.length ? /* @__PURE__ */ jsx(
|
|
1417
|
-
|
|
1418
|
-
|
|
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))
|
|
1730
|
+
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",
|
|
1731
|
+
children: [attachments?.length ? /* @__PURE__ */ jsx(ChatAttachments, {
|
|
1732
|
+
items: attachments,
|
|
1733
|
+
className: `-mx-1 ${text ? "mb-2" : ""}`
|
|
1423
1734
|
}) : null, text]
|
|
1424
1735
|
});
|
|
1425
1736
|
return /* @__PURE__ */ jsxs("div", {
|
|
1426
|
-
className: "
|
|
1737
|
+
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
1738
|
children: [reasoning ? /* @__PURE__ */ jsxs("details", {
|
|
1428
|
-
className: "mb-2 rounded-
|
|
1429
|
-
children: [/* @__PURE__ */
|
|
1430
|
-
className: "cursor-pointer select-none text-xs text-muted-foreground",
|
|
1431
|
-
children:
|
|
1739
|
+
className: "group/reason -mx-1 mb-2 rounded-[calc(var(--radius-md)+1px)] border border-border bg-background px-2 py-1",
|
|
1740
|
+
children: [/* @__PURE__ */ jsxs("summary", {
|
|
1741
|
+
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",
|
|
1742
|
+
children: [/* @__PURE__ */ jsx(ChevronRight, {
|
|
1743
|
+
size: 12,
|
|
1744
|
+
"aria-hidden": true,
|
|
1745
|
+
className: "shrink-0 transition-transform group-open/reason:rotate-90"
|
|
1746
|
+
}), t("webchat.reasoning")]
|
|
1432
1747
|
}), /* @__PURE__ */ jsx(ChatMarkdown, {
|
|
1433
1748
|
text: reasoning,
|
|
1434
1749
|
className: "md-static md-chat md-chat-sub mt-1 text-muted-foreground"
|
|
@@ -1441,81 +1756,23 @@ var Bubble = memo(function Bubble({ role, text, reasoning, attachments }) {
|
|
|
1441
1756
|
});
|
|
1442
1757
|
//#endregion
|
|
1443
1758
|
//#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
1759
|
/**
|
|
1496
|
-
*
|
|
1497
|
-
*
|
|
1498
|
-
*
|
|
1499
|
-
*
|
|
1500
|
-
*
|
|
1760
|
+
* Flatten the catalog to one list: with a single registered agent the agent level is
|
|
1761
|
+
* noise, and with several the agent name simply prefixes its presets.
|
|
1762
|
+
*
|
|
1763
|
+
* ONE mode, a list of names. Until 2026-09-16 there was a second — a row of stars in the
|
|
1764
|
+
* composer, where a preset's `quality` rating addressed it by rung and the model name was
|
|
1765
|
+
* never printed. It is gone, with the ratings it read. A star count can only address a
|
|
1766
|
+
* preset when the presets form a single totally ordered line, and `claude-agent`'s do
|
|
1767
|
+
* not: nothing published ranks `sonnet-high` against `opus-low`, and the cheaper of the
|
|
1768
|
+
* two is not the weaker one. The list that used to be the FALLBACK — for several agents,
|
|
1769
|
+
* a gap in the ladder, a preset with no rating — was always the honest control, and is
|
|
1770
|
+
* now the only one. See
|
|
1771
|
+
* `docs/superpowers/specs/2026-09-16-agent-preset-is-a-model-choice-design.md`.
|
|
1501
1772
|
*/
|
|
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
1773
|
function AgentPicker({ agents, defaultAgentId, selection, onChange }) {
|
|
1516
1774
|
const { t } = useTranslation();
|
|
1517
1775
|
const [open, setOpen] = useState(false);
|
|
1518
|
-
const [preview, setPreview] = useState(null);
|
|
1519
1776
|
const rootRef = useRef(null);
|
|
1520
1777
|
const triggerRef = useRef(null);
|
|
1521
1778
|
useEffect(() => {
|
|
@@ -1542,48 +1799,10 @@ function AgentPicker({ agents, defaultAgentId, selection, onChange }) {
|
|
|
1542
1799
|
presetId: p.id,
|
|
1543
1800
|
label: multi ? `${a.title} · ${p.title}` : p.title,
|
|
1544
1801
|
hint: p.hint,
|
|
1545
|
-
quality: p.quality,
|
|
1546
|
-
time: p.time,
|
|
1547
1802
|
isDefault: p.default === true
|
|
1548
1803
|
})));
|
|
1549
1804
|
if (rows.length === 0) return null;
|
|
1550
1805
|
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
1806
|
return /* @__PURE__ */ jsxs("div", {
|
|
1588
1807
|
className: "relative",
|
|
1589
1808
|
ref: rootRef,
|
|
@@ -1596,7 +1815,7 @@ function AgentPicker({ agents, defaultAgentId, selection, onChange }) {
|
|
|
1596
1815
|
onClick: () => setOpen((v) => !v),
|
|
1597
1816
|
children: [/* @__PURE__ */ jsx("span", {
|
|
1598
1817
|
className: "max-w-40 truncate",
|
|
1599
|
-
children: current.label
|
|
1818
|
+
children: current.hint ? `${current.label} · ${current.hint}` : current.label
|
|
1600
1819
|
}), /* @__PURE__ */ jsx(ChevronDown, {
|
|
1601
1820
|
size: 12,
|
|
1602
1821
|
className: "shrink-0"
|
|
@@ -1626,11 +1845,10 @@ function AgentPicker({ agents, defaultAgentId, selection, onChange }) {
|
|
|
1626
1845
|
className: "min-w-0 flex-1 truncate",
|
|
1627
1846
|
children: r.label
|
|
1628
1847
|
}),
|
|
1629
|
-
/* @__PURE__ */ jsx(
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
})
|
|
1848
|
+
r.hint ? /* @__PURE__ */ jsx("span", {
|
|
1849
|
+
className: "shrink-0 text-muted-foreground",
|
|
1850
|
+
children: r.hint
|
|
1851
|
+
}) : null
|
|
1634
1852
|
]
|
|
1635
1853
|
}) }, `${r.agentId}/${r.presetId}`);
|
|
1636
1854
|
})
|
|
@@ -1649,29 +1867,12 @@ function ChatComposer({ streaming, onSend, onStop, attachments, uploading, uploa
|
|
|
1649
1867
|
return /* @__PURE__ */ jsxs("div", {
|
|
1650
1868
|
className: "flex shrink-0 flex-col gap-2 border-t border-border p-2",
|
|
1651
1869
|
children: [(attachments.length > 0 || uploading || uploadError) && /* @__PURE__ */ jsxs("div", {
|
|
1652
|
-
className: "flex flex-
|
|
1653
|
-
"data-chat-attachments": true,
|
|
1870
|
+
className: "flex flex-col gap-1.5 px-1",
|
|
1654
1871
|
children: [
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
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}`)),
|
|
1872
|
+
/* @__PURE__ */ jsx(ChatAttachments, {
|
|
1873
|
+
items: attachments,
|
|
1874
|
+
onRemove: onRemoveAttachment
|
|
1875
|
+
}),
|
|
1675
1876
|
uploading && /* @__PURE__ */ jsxs("div", {
|
|
1676
1877
|
className: "flex min-w-32 flex-1 items-center gap-2",
|
|
1677
1878
|
"aria-label": t("webchat.uploading"),
|
|
@@ -1705,7 +1906,7 @@ function ChatComposer({ streaming, onSend, onStop, attachments, uploading, uploa
|
|
|
1705
1906
|
children: t(uploadError)
|
|
1706
1907
|
}), canRetryUpload && /* @__PURE__ */ jsxs("button", {
|
|
1707
1908
|
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",
|
|
1909
|
+
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
1910
|
onClick: onRetryUpload,
|
|
1710
1911
|
children: [
|
|
1711
1912
|
/* @__PURE__ */ jsx(RotateCcw, { size: 12 }),
|
|
@@ -1718,7 +1919,7 @@ function ChatComposer({ streaming, onSend, onStop, attachments, uploading, uploa
|
|
|
1718
1919
|
}), /* @__PURE__ */ jsxs("div", {
|
|
1719
1920
|
className: "flex flex-col gap-1",
|
|
1720
1921
|
children: [/* @__PURE__ */ jsx("textarea", {
|
|
1721
|
-
className: "w-full resize-none rounded-lg border border-border bg-background px-3 py-2 text-sm
|
|
1922
|
+
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
1923
|
rows: 1,
|
|
1723
1924
|
value: text,
|
|
1724
1925
|
placeholder: t("webchat.placeholder"),
|
|
@@ -1733,12 +1934,16 @@ function ChatComposer({ streaming, onSend, onStop, attachments, uploading, uploa
|
|
|
1733
1934
|
className: "flex items-center gap-1",
|
|
1734
1935
|
children: [
|
|
1735
1936
|
attachmentsAllowed && /* @__PURE__ */ jsxs("label", {
|
|
1736
|
-
|
|
1737
|
-
className: `shrink-0 cursor-pointer
|
|
1738
|
-
children: [/* @__PURE__ */ jsx(Paperclip, {
|
|
1937
|
+
title: t("webchat.attach"),
|
|
1938
|
+
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" : ""}`,
|
|
1939
|
+
children: [/* @__PURE__ */ jsx(Paperclip, {
|
|
1940
|
+
size: 18,
|
|
1941
|
+
"aria-hidden": true
|
|
1942
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
1739
1943
|
type: "file",
|
|
1740
1944
|
multiple: true,
|
|
1741
|
-
|
|
1945
|
+
"aria-label": t("webchat.attach"),
|
|
1946
|
+
className: "sr-only",
|
|
1742
1947
|
disabled: uploading,
|
|
1743
1948
|
onChange: (e) => {
|
|
1744
1949
|
if (e.target.files?.length) onPickFiles(e.target.files);
|
|
@@ -1753,14 +1958,23 @@ function ChatComposer({ streaming, onSend, onStop, attachments, uploading, uploa
|
|
|
1753
1958
|
onChange: onChooseAgent
|
|
1754
1959
|
}),
|
|
1755
1960
|
/* @__PURE__ */ jsx("div", { className: "flex-1" }),
|
|
1756
|
-
streaming ? /* @__PURE__ */ jsx(
|
|
1961
|
+
streaming ? /* @__PURE__ */ jsx(Button, {
|
|
1962
|
+
type: "button",
|
|
1963
|
+
variant: "ghost",
|
|
1964
|
+
size: "iconSm",
|
|
1757
1965
|
"aria-label": t("webchat.stop"),
|
|
1758
|
-
|
|
1966
|
+
title: t("webchat.stop"),
|
|
1967
|
+
className: "shrink-0",
|
|
1759
1968
|
onClick: onStop,
|
|
1760
1969
|
children: /* @__PURE__ */ jsx(Square, { size: 18 })
|
|
1761
|
-
}) : /* @__PURE__ */ jsx(
|
|
1970
|
+
}) : /* @__PURE__ */ jsx(Button, {
|
|
1971
|
+
type: "button",
|
|
1972
|
+
variant: "ghost",
|
|
1973
|
+
size: "iconSm",
|
|
1762
1974
|
"aria-label": t("webchat.send"),
|
|
1763
|
-
|
|
1975
|
+
title: t("webchat.send"),
|
|
1976
|
+
disabled: !text.trim(),
|
|
1977
|
+
className: "shrink-0 hover:text-primary",
|
|
1764
1978
|
onClick: submit,
|
|
1765
1979
|
children: /* @__PURE__ */ jsx(Send, { size: 18 })
|
|
1766
1980
|
})
|
|
@@ -1775,12 +1989,15 @@ function ChatThreadList({ threads, activeId, onOpen, onNew, onTogglePrivate, cla
|
|
|
1775
1989
|
const { t } = useTranslation();
|
|
1776
1990
|
return /* @__PURE__ */ jsxs("aside", {
|
|
1777
1991
|
"aria-label": t("webchat.threads"),
|
|
1778
|
-
className: `${className} border-r border-border
|
|
1992
|
+
className: `${className} flex flex-col border-r border-border`,
|
|
1779
1993
|
children: [/* @__PURE__ */ jsxs("button", {
|
|
1780
|
-
className: "flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted",
|
|
1994
|
+
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
1995
|
onClick: onNew,
|
|
1782
1996
|
children: [
|
|
1783
|
-
/* @__PURE__ */ jsx(Plus, {
|
|
1997
|
+
/* @__PURE__ */ jsx(Plus, {
|
|
1998
|
+
size: 16,
|
|
1999
|
+
"aria-hidden": true
|
|
2000
|
+
}),
|
|
1784
2001
|
" ",
|
|
1785
2002
|
t("webchat.newConversation")
|
|
1786
2003
|
]
|
|
@@ -1789,10 +2006,10 @@ function ChatThreadList({ threads, activeId, onOpen, onNew, onTogglePrivate, cla
|
|
|
1789
2006
|
children: threads.map((th) => {
|
|
1790
2007
|
const active = th.convId === activeId;
|
|
1791
2008
|
return /* @__PURE__ */ jsxs("div", {
|
|
1792
|
-
className: `flex items-center gap-1
|
|
2009
|
+
className: `flex items-center gap-1 border-l-2 pr-2 pl-1 ${active ? "border-ring bg-accent" : "border-transparent hover:bg-muted"}`,
|
|
1793
2010
|
children: [/* @__PURE__ */ jsxs("button", {
|
|
1794
2011
|
onClick: () => onOpen(th.convId),
|
|
1795
|
-
className: `min-w-0 flex-1 py-2 pl-2 text-left text-sm ${active ? "font-medium" : ""}`,
|
|
2012
|
+
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
2013
|
children: [/* @__PURE__ */ jsx("span", {
|
|
1797
2014
|
className: "block truncate",
|
|
1798
2015
|
children: th.title || t("webchat.newConversation")
|
|
@@ -2047,36 +2264,49 @@ function ChatWidget() {
|
|
|
2047
2264
|
const hasConversation = chat.messages.length > 0 || chat.pending !== null;
|
|
2048
2265
|
const activeTitle = chat.threads.find((th) => th.convId === chat.convId)?.title;
|
|
2049
2266
|
const header = /* @__PURE__ */ jsxs("div", {
|
|
2050
|
-
className: "flex shrink-0 items-center justify-between gap-2 border-b border-border px-3 py-
|
|
2267
|
+
className: "flex shrink-0 items-center justify-between gap-2 border-b border-border px-3 py-1.5",
|
|
2051
2268
|
children: [/* @__PURE__ */ jsx("span", {
|
|
2052
2269
|
className: "min-w-0 truncate text-sm font-medium",
|
|
2053
2270
|
children: activeTitle || t("webchat.title")
|
|
2054
2271
|
}), /* @__PURE__ */ jsxs("div", {
|
|
2055
|
-
className: "flex shrink-0 items-center gap-
|
|
2272
|
+
className: "-mr-1 flex shrink-0 items-center gap-0.5",
|
|
2056
2273
|
children: [
|
|
2057
|
-
|
|
2274
|
+
hasConversation && /* @__PURE__ */ jsx(Button, {
|
|
2275
|
+
type: "button",
|
|
2276
|
+
variant: "ghost",
|
|
2277
|
+
size: "iconSm",
|
|
2278
|
+
"aria-label": t("webchat.newConversation"),
|
|
2279
|
+
title: t("webchat.newConversation"),
|
|
2280
|
+
onClick: startNew,
|
|
2281
|
+
children: /* @__PURE__ */ jsx(Plus, { size: 16 })
|
|
2282
|
+
}),
|
|
2283
|
+
!hasSidebar && /* @__PURE__ */ jsx(Button, {
|
|
2284
|
+
type: "button",
|
|
2285
|
+
variant: "ghost",
|
|
2286
|
+
size: "iconSm",
|
|
2058
2287
|
"aria-label": t("webchat.threads"),
|
|
2288
|
+
title: t("webchat.threads"),
|
|
2059
2289
|
"aria-expanded": threadsOpen,
|
|
2060
|
-
className:
|
|
2290
|
+
className: threadsOpen ? "bg-muted text-foreground" : "",
|
|
2061
2291
|
onClick: () => setThreadsOpen((v) => !v),
|
|
2062
2292
|
children: /* @__PURE__ */ jsx(List, { size: 16 })
|
|
2063
2293
|
}),
|
|
2064
|
-
|
|
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", {
|
|
2294
|
+
chatModes(wide).includes("panel") && /* @__PURE__ */ jsx(Button, {
|
|
2071
2295
|
ref: expandBtnRef,
|
|
2296
|
+
type: "button",
|
|
2297
|
+
variant: "ghost",
|
|
2298
|
+
size: "iconSm",
|
|
2072
2299
|
"aria-label": mode === "modal" ? t("webchat.collapse") : t("webchat.expand"),
|
|
2073
|
-
|
|
2300
|
+
title: mode === "modal" ? t("webchat.collapse") : t("webchat.expand"),
|
|
2074
2301
|
onClick: () => setMode(stepMode(mode, wide, mode === "modal" ? -1 : 1)),
|
|
2075
2302
|
children: mode === "modal" ? /* @__PURE__ */ jsx(Minimize2, { size: 16 }) : /* @__PURE__ */ jsx(Maximize2, { size: 16 })
|
|
2076
2303
|
}),
|
|
2077
|
-
/* @__PURE__ */ jsx(
|
|
2304
|
+
/* @__PURE__ */ jsx(Button, {
|
|
2305
|
+
type: "button",
|
|
2306
|
+
variant: "ghost",
|
|
2307
|
+
size: "iconSm",
|
|
2078
2308
|
"aria-label": t("webchat.close"),
|
|
2079
|
-
|
|
2309
|
+
title: t("webchat.close"),
|
|
2080
2310
|
onClick: () => setMode("bubble"),
|
|
2081
2311
|
children: /* @__PURE__ */ jsx(X, { size: 16 })
|
|
2082
2312
|
})
|
package/dist/runtime/format.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { frontendInstructions } from '@coffer-org/server/frontend-agent';
|
|
2
2
|
import { NEUTRAL_FORMAT } from '@coffer-org/markdown';
|
|
3
|
+
const LIVE_PAGE = [
|
|
4
|
+
'You are answering inside the app, in a panel over a page the person is reading.',
|
|
5
|
+
'That page does not reload itself. After you create, update, delete or restore a record,',
|
|
6
|
+
'call `refresh_open_page` — naming the record when you know which one — so what they see matches',
|
|
7
|
+
'what you just told them. If you DELETED the record, pass `deleted: true` with it, so the page',
|
|
8
|
+
'showing it returns to the list instead of sitting on something that is no longer there.',
|
|
9
|
+
'Skip the call when you only read.',
|
|
10
|
+
].join(' ');
|
|
3
11
|
export async function webChannelSystem() {
|
|
4
12
|
const site = await frontendInstructions('');
|
|
5
|
-
return [NEUTRAL_FORMAT, site].filter(Boolean).join('\n\n');
|
|
13
|
+
return [NEUTRAL_FORMAT, site, LIVE_PAGE].filter(Boolean).join('\n\n');
|
|
6
14
|
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PluginHooks } from '@coffer-org/server/plugin-hooks';
|
|
2
2
|
export type { ThreadSummary, HistoryMsg } from './actions.ts';
|
|
3
3
|
export { makeStreamingConnector } from './connector.ts';
|
|
4
|
+
export { makeRefreshTool } from './refresh-tool.ts';
|
|
4
5
|
export { getConversation as getSelection, setConversation as setSelection, } from '@coffer-org/server/conversation-store';
|
|
5
6
|
export { sendAction, pageContext } from './send.ts';
|
|
6
7
|
export declare const serverHooks: PluginHooks;
|
package/dist/runtime/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { initFanout, teardownFanout } from "./channel-registry.js";
|
|
|
6
6
|
const THREAD_TTL_MS = Number(process.env['WEBCHAT_THREAD_TTL_MS'] ?? 30 * 86_400_000);
|
|
7
7
|
let unregisterConnector;
|
|
8
8
|
export { makeStreamingConnector } from "./connector.js";
|
|
9
|
+
export { makeRefreshTool } from "./refresh-tool.js";
|
|
9
10
|
export { getConversation as getSelection, setConversation as setSelection, } from '@coffer-org/server/conversation-store';
|
|
10
11
|
export { sendAction, pageContext } from "./send.js";
|
|
11
12
|
export const serverHooks = {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { getLogger } from '@coffer-org/sdk/logger';
|
|
2
|
+
import { broadcast, subscriberCount } from "./channel-registry.js";
|
|
3
|
+
const log = getLogger('webchat');
|
|
4
|
+
function str(value) {
|
|
5
|
+
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
6
|
+
}
|
|
7
|
+
export function makeRefreshTool(conversationId) {
|
|
8
|
+
return {
|
|
9
|
+
tools: [
|
|
10
|
+
{
|
|
11
|
+
name: 'mcp__coffer__refresh_open_page',
|
|
12
|
+
description: 'Reload the data on the page the user currently has open in the browser. ' +
|
|
13
|
+
'Call it right after you create, update, delete or restore a record, so what they see ' +
|
|
14
|
+
'matches what you just changed — otherwise the page keeps showing the values it loaded ' +
|
|
15
|
+
'before your edit. Name the record you touched when you know it. ' +
|
|
16
|
+
'When you DELETED or trashed the record, pass deleted: true with its library, shelf and ' +
|
|
17
|
+
'id: the page showing it then returns to the list instead of sitting on a record that is ' +
|
|
18
|
+
'no longer there. ' +
|
|
19
|
+
'It is a display-only call: it changes no data, costs the user nothing, and is safe to ' +
|
|
20
|
+
'repeat. Do not call it when you only READ records.',
|
|
21
|
+
inputSchema: {
|
|
22
|
+
library: { type: 'string', description: 'Library of the record you changed, e.g. health' },
|
|
23
|
+
shelf: { type: 'string', description: 'Shelf of the record you changed, e.g. drug' },
|
|
24
|
+
id: { type: 'string', description: 'Id of the record you changed' },
|
|
25
|
+
deleted: {
|
|
26
|
+
type: 'boolean',
|
|
27
|
+
description: 'True when you deleted or trashed that record, so the page leaves it',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
handler: async (args) => {
|
|
31
|
+
const library = str(args['library']);
|
|
32
|
+
const shelf = str(args['shelf']);
|
|
33
|
+
const id = str(args['id']);
|
|
34
|
+
const gone = args['deleted'] === true && Boolean(id);
|
|
35
|
+
const scope = {
|
|
36
|
+
...(library ? { library } : {}),
|
|
37
|
+
...(shelf ? { shelf } : {}),
|
|
38
|
+
...(id ? { id } : {}),
|
|
39
|
+
...(gone ? { gone: true } : {}),
|
|
40
|
+
};
|
|
41
|
+
broadcast(conversationId, 'refresh', scope);
|
|
42
|
+
const viewers = subscriberCount(conversationId);
|
|
43
|
+
log.info(`refresh_open_page ${JSON.stringify(scope)} → ${viewers} viewer(s) on ${conversationId}`);
|
|
44
|
+
return { requested: true, gone, viewers };
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
package/dist/runtime/send.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getConversation, setConversation, readAndTouchConversation, } from '@co
|
|
|
7
7
|
import { mayWrite, mayRead } from '@coffer-org/server/orchestrator';
|
|
8
8
|
import { policy } from "./config.js";
|
|
9
9
|
import { makeStreamingConnector } from "./connector.js";
|
|
10
|
+
import { makeRefreshTool } from "./refresh-tool.js";
|
|
10
11
|
import { broadcast, initFanout } from "./channel-registry.js";
|
|
11
12
|
import { webChannelSystem } from "./format.js";
|
|
12
13
|
import { loadReasoningDisplay } from "./settings.js";
|
|
@@ -87,6 +88,7 @@ export async function sendAction(body, caller, deps = {}) {
|
|
|
87
88
|
replyToMsgId: replyTo,
|
|
88
89
|
},
|
|
89
90
|
capabilities: CAPABILITIES,
|
|
91
|
+
toolProvider: makeRefreshTool(conversationId),
|
|
90
92
|
systemPrompt: { channel: [await webChannelSystem()] },
|
|
91
93
|
...(turnContext.length ? { turnContext } : {}),
|
|
92
94
|
...(agentId ? { agentId } : {}),
|
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-
|
|
14
|
+
load: () => import("./ChatWidget-DocoFt8q.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.
|
|
3
|
+
"version": "7.3.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.
|
|
32
|
-
"@coffer-org/server": "^7.
|
|
31
|
+
"@coffer-org/sdk": "^7.4.1",
|
|
32
|
+
"@coffer-org/server": "^7.6.0",
|
|
33
33
|
"lucide-react": "^1.17.0"
|
|
34
34
|
},
|
|
35
35
|
"coffer": {
|