@candor-ui/react 1.0.1 → 1.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/LICENSE +3 -2
- package/dist/blocks/copy-field.d.ts +6 -0
- package/dist/blocks/copy-field.js +9 -1
- package/dist/blocks/degraded-panel.d.ts +6 -0
- package/dist/blocks/degraded-panel.js +9 -1
- package/dist/blocks/locale.d.ts +86 -0
- package/dist/blocks/locale.js +146 -0
- package/dist/blocks/relative-time.d.ts +21 -3
- package/dist/blocks/relative-time.js +30 -11
- package/dist/blocks/status-dot.js +4 -2
- package/dist/blocks/step-list.js +4 -2
- package/dist/blocks/streaming-text.js +1 -1
- package/dist/blocks/surface.d.ts +2 -2
- package/dist/chat/agent-avatar.d.ts +14 -0
- package/dist/chat/agent-avatar.js +26 -2
- package/dist/chat/attachment-preview.d.ts +53 -0
- package/dist/chat/attachment-preview.js +82 -0
- package/dist/chat/card-registry.d.ts +7 -0
- package/dist/chat/card-registry.js +14 -4
- package/dist/chat/citation.d.ts +15 -0
- package/dist/chat/citation.js +34 -7
- package/dist/chat/composer.d.ts +30 -4
- package/dist/chat/composer.js +69 -20
- package/dist/chat/context-meter.d.ts +9 -0
- package/dist/chat/context-meter.js +21 -4
- package/dist/chat/entity-chip.d.ts +19 -0
- package/dist/chat/entity-chip.js +37 -3
- package/dist/chat/folded-turns.d.ts +33 -0
- package/dist/chat/folded-turns.js +29 -0
- package/dist/chat/message-actions.d.ts +9 -0
- package/dist/chat/message-actions.js +12 -1
- package/dist/chat/message-content.d.ts +12 -3
- package/dist/chat/message-content.js +24 -6
- package/dist/chat/message.d.ts +81 -4
- package/dist/chat/message.js +50 -9
- package/dist/chat/payload.d.ts +21 -4
- package/dist/chat/payload.js +38 -5
- package/dist/chat/prompt-suggestions.d.ts +6 -2
- package/dist/chat/prompt-suggestions.js +9 -2
- package/dist/chat/reasoning.d.ts +15 -0
- package/dist/chat/reasoning.js +12 -3
- package/dist/chat/thread-divider.d.ts +18 -0
- package/dist/chat/thread-divider.js +39 -0
- package/dist/chat/thread-event.d.ts +19 -0
- package/dist/chat/thread-event.js +15 -0
- package/dist/chat/thread.d.ts +12 -1
- package/dist/chat/thread.js +32 -4
- package/dist/chat/tool-call.d.ts +12 -0
- package/dist/chat/tool-call.js +16 -3
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/lib/chat.d.ts +11 -1
- package/dist/lib/highlight.d.ts +15 -0
- package/dist/lib/highlight.js +224 -0
- package/dist/lib/status.d.ts +15 -0
- package/dist/lib/status.js +10 -1
- package/dist/lib/use-virtual-list.d.ts +39 -0
- package/dist/lib/use-virtual-list.js +124 -0
- package/dist/styles.css +40 -1
- package/dist/ui/button.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { File, FileArchive, FileCode, FileImage, FileSpreadsheet, FileText, Music, Paperclip, Video, X } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
import { useFormat, useStrings } from "../blocks/locale.js";
|
|
7
|
+
/** AttachmentPreview's fixed words; override them under `attachmentPreview` in LocaleProvider's `strings`. */
|
|
8
|
+
export const attachmentPreviewStrings = {
|
|
9
|
+
remove: "Remove {name}",
|
|
10
|
+
open: "Open {name}",
|
|
11
|
+
attachments: { one: "{count} attachment", other: "{count} attachments" },
|
|
12
|
+
/** File sizes; `{size}` is already formatted. */
|
|
13
|
+
bytes: "{size} B",
|
|
14
|
+
kilobytes: "{size} KB",
|
|
15
|
+
megabytes: "{size} MB",
|
|
16
|
+
};
|
|
17
|
+
/** "820 B", "14 KB", "2.4 MB", in the locale. */
|
|
18
|
+
export function formatFileSize(bytes, number, t) {
|
|
19
|
+
if (bytes === undefined)
|
|
20
|
+
return null;
|
|
21
|
+
if (bytes < 1024)
|
|
22
|
+
return t("bytes", { size: number(bytes, { useGrouping: false }) });
|
|
23
|
+
if (bytes < 1024 * 1024)
|
|
24
|
+
return t("kilobytes", { size: number(Math.round(bytes / 1024), { useGrouping: false }) });
|
|
25
|
+
return t("megabytes", { size: number(bytes / 1024 / 1024, { minimumFractionDigits: 1, maximumFractionDigits: 1, useGrouping: false }) });
|
|
26
|
+
}
|
|
27
|
+
/** An icon for a file, from its media type or, failing that, its extension. */
|
|
28
|
+
export function fileIcon(a) {
|
|
29
|
+
const type = a.mediaType ?? "";
|
|
30
|
+
const ext = a.name.split(".").pop()?.toLowerCase() ?? "";
|
|
31
|
+
if (type.startsWith("image/") || ["png", "jpg", "jpeg", "gif", "webp", "svg", "avif"].includes(ext))
|
|
32
|
+
return FileImage;
|
|
33
|
+
if (type.startsWith("audio/"))
|
|
34
|
+
return Music;
|
|
35
|
+
if (type.startsWith("video/"))
|
|
36
|
+
return Video;
|
|
37
|
+
if (/csv|spreadsheet|excel/.test(type) || ["csv", "xls", "xlsx", "tsv"].includes(ext))
|
|
38
|
+
return FileSpreadsheet;
|
|
39
|
+
if (/zip|tar|gzip|compressed/.test(type) || ["zip", "tar", "gz", "tgz", "7z"].includes(ext))
|
|
40
|
+
return FileArchive;
|
|
41
|
+
if (/json|javascript|typescript|xml|x-sh|x-python/.test(type) || ["json", "js", "ts", "tsx", "py", "sql", "sh", "yaml", "yml", "xml"].includes(ext))
|
|
42
|
+
return FileCode;
|
|
43
|
+
if (type.startsWith("text/") || type === "application/pdf" || ["txt", "md", "pdf", "doc", "docx", "rtf"].includes(ext))
|
|
44
|
+
return FileText;
|
|
45
|
+
return File;
|
|
46
|
+
}
|
|
47
|
+
/** The icon for a file's type. */
|
|
48
|
+
function FileGlyph({ attachment }) {
|
|
49
|
+
return React.createElement(fileIcon(attachment), { className: "size-4" });
|
|
50
|
+
}
|
|
51
|
+
/** A thumbnail only for images whose preview the app supplied. */
|
|
52
|
+
const thumbnail = (a) => (a.previewUrl && (a.mediaType ?? "").startsWith("image/") ? a.previewUrl : undefined);
|
|
53
|
+
/**
|
|
54
|
+
* A file waiting to be sent, as a chip in the composer: its name, then upload progress, an error, or its size, and a
|
|
55
|
+
* remove button. Images show their preview when the app gives one.
|
|
56
|
+
*/
|
|
57
|
+
function AttachmentChip({ attachment: a, onRemove, className, }) {
|
|
58
|
+
const t = useStrings("attachmentPreview", attachmentPreviewStrings);
|
|
59
|
+
const fmt = useFormat();
|
|
60
|
+
const thumb = thumbnail(a);
|
|
61
|
+
return (_jsxs("span", { "data-slot": "attachment-chip", className: cn("relative inline-flex max-w-56 items-center gap-1.5 overflow-hidden rounded-md border bg-card py-1 pe-1 ps-2 text-xs", a.error && "border-destructive/50", className), children: [thumb ? _jsx("img", { src: thumb, alt: "", className: "size-4 shrink-0 rounded-xs object-cover" }) : _jsx(Paperclip, { "aria-hidden": true, className: "size-3 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: a.name }), _jsx("span", { className: cn("shrink-0 tabular-nums", a.error ? "text-destructive-text" : "text-faint-foreground"), children: a.error ? a.error : a.progress !== undefined ? fmt.number(Math.round(a.progress * 100) / 100, { style: "percent" }) : formatFileSize(a.size, fmt.number, t) }), onRemove && (_jsx("button", { type: "button", "aria-label": t("remove", { name: a.name }), onClick: () => onRemove(a.id), className: "rounded-sm p-0.5 text-faint-foreground hover:bg-accent hover:text-foreground", children: _jsx(X, { className: "size-3" }) })), a.progress !== undefined && !a.error && _jsx("span", { "aria-hidden": true, className: "absolute bottom-0 start-0 h-0.5 bg-primary transition-[width] duration-300", style: { width: `${a.progress * 100}%` } })] }));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A sent file, as a tile in a message: an image preview when the app supplied one, else an icon for its type, with
|
|
65
|
+
* its name and size. The name is never a link: with `onOpen` the tile is a button and your app decides what opening
|
|
66
|
+
* means (a viewer, a signed download).
|
|
67
|
+
*/
|
|
68
|
+
function AttachmentPreview({ attachment: a, onOpen, className, }) {
|
|
69
|
+
const t = useStrings("attachmentPreview", attachmentPreviewStrings);
|
|
70
|
+
const fmt = useFormat();
|
|
71
|
+
const thumb = thumbnail(a);
|
|
72
|
+
const size = formatFileSize(a.size, fmt.number, t);
|
|
73
|
+
const body = (_jsxs(_Fragment, { children: [thumb ? (_jsx("img", { src: thumb, alt: "", className: "h-24 w-full rounded-md bg-muted object-cover" })) : (_jsx("span", { "aria-hidden": true, className: "flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: _jsx(FileGlyph, { attachment: a }) })), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-sm", children: a.name }), (size || a.error) && _jsx("span", { className: cn("block text-xs tabular-nums", a.error ? "text-destructive-text" : "text-faint-foreground"), children: a.error ?? size })] })] }));
|
|
74
|
+
const frame = cn("flex w-56 max-w-full gap-2.5 rounded-lg border bg-card p-2 text-start", thumb ? "flex-col" : "items-center", className);
|
|
75
|
+
return onOpen ? (_jsx("button", { type: "button", "data-slot": "attachment-preview", "aria-label": t("open", { name: a.name }), onClick: () => onOpen(a), className: cn(frame, "outline-none transition-colors duration-150 hover:border-border-strong focus-visible:ring-[3px] focus-visible:ring-ring/40"), children: body })) : (_jsx("div", { "data-slot": "attachment-preview", className: frame, children: body }));
|
|
76
|
+
}
|
|
77
|
+
/** Several sent files, as a wrapping row of tiles, announced as a list. */
|
|
78
|
+
function AttachmentList({ attachments, onOpen, className, }) {
|
|
79
|
+
const t = useStrings("attachmentPreview", attachmentPreviewStrings);
|
|
80
|
+
return (_jsx("ul", { "data-slot": "attachment-list", "aria-label": t("attachments", { count: attachments.length }), className: cn("flex flex-wrap gap-2", className), children: attachments.map((a) => (_jsx("li", { className: "max-w-full", children: _jsx(AttachmentPreview, { attachment: a, onOpen: onOpen }) }, a.id))) }));
|
|
81
|
+
}
|
|
82
|
+
export { AttachmentChip, AttachmentPreview, AttachmentList };
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
/** CardRegistry's fixed words; override them under `cardRegistry` in LocaleProvider's `strings`. */
|
|
3
|
+
export declare const cardRegistryStrings: {
|
|
4
|
+
showAsText: string;
|
|
5
|
+
unknown: string;
|
|
6
|
+
invalid: string;
|
|
7
|
+
failed: string;
|
|
8
|
+
};
|
|
2
9
|
/**
|
|
3
10
|
* A card an agent's reply can carry. `check` decides whether the props are
|
|
4
11
|
* the shape `render` needs: it returns null when they are, or a sentence
|
|
@@ -4,6 +4,14 @@ import * as React from "react";
|
|
|
4
4
|
import { ChevronRight, LayoutTemplate } from "lucide-react";
|
|
5
5
|
import { DegradedPanel } from "../blocks/degraded-panel.js";
|
|
6
6
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
|
|
7
|
+
import { useStrings } from "../blocks/locale.js";
|
|
8
|
+
/** CardRegistry's fixed words; override them under `cardRegistry` in LocaleProvider's `strings`. */
|
|
9
|
+
export const cardRegistryStrings = {
|
|
10
|
+
showAsText: "Show it as text",
|
|
11
|
+
unknown: "This reply includes a “{name}” card this app can’t show.",
|
|
12
|
+
invalid: "The {label} couldn’t be shown: {problem}",
|
|
13
|
+
failed: "The {label} failed while drawing.",
|
|
14
|
+
};
|
|
7
15
|
/** Define a card with typed props; the check is what makes the cast safe. */
|
|
8
16
|
export function defineCard(card) {
|
|
9
17
|
return card;
|
|
@@ -23,7 +31,8 @@ class CardBoundary extends React.Component {
|
|
|
23
31
|
}
|
|
24
32
|
}
|
|
25
33
|
function Summary({ text, }) {
|
|
26
|
-
|
|
34
|
+
const t = useStrings("cardRegistry", cardRegistryStrings);
|
|
35
|
+
return (_jsxs(Collapsible, { className: "group/summary", children: [_jsxs(CollapsibleTrigger, { className: "inline-flex items-center gap-1 text-xs text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/40 rounded-sm", children: [_jsx(ChevronRight, { "aria-hidden": true, className: "rtl:-scale-x-100 size-3 transition-transform group-data-[state=open]/summary:[transform:rotate(90deg)]" }), " ", t("showAsText")] }), _jsx(CollapsibleContent, { children: _jsx("p", { className: "mt-1.5 rounded-md bg-sunken px-3 py-2 text-sm whitespace-pre-wrap", children: text }) })] }));
|
|
27
36
|
}
|
|
28
37
|
/**
|
|
29
38
|
* Renders one card from the registry. Three honest fallbacks, each with the
|
|
@@ -31,16 +40,17 @@ function Summary({ text, }) {
|
|
|
31
40
|
* fit (and how); or it failed while drawing.
|
|
32
41
|
*/
|
|
33
42
|
function Card({ name, props, summary, }) {
|
|
43
|
+
const t = useStrings("cardRegistry", cardRegistryStrings);
|
|
34
44
|
const cards = React.useContext(Registry);
|
|
35
45
|
const def = cards[name];
|
|
36
46
|
if (!def) {
|
|
37
|
-
return (_jsxs("div", { "data-slot": "card", "data-card": name, "data-state": "unknown", className: "flex flex-col gap-1.5 rounded-lg border border-dashed px-3 py-2.5", children: [_jsxs("p", { className: "flex items-center gap-1.5 text-sm text-muted-foreground", children: [_jsx(LayoutTemplate, { "aria-hidden": true, className: "size-3.5" }), "
|
|
47
|
+
return (_jsxs("div", { "data-slot": "card", "data-card": name, "data-state": "unknown", className: "flex flex-col gap-1.5 rounded-lg border border-dashed px-3 py-2.5", children: [_jsxs("p", { className: "flex items-center gap-1.5 text-sm text-muted-foreground", children: [_jsx(LayoutTemplate, { "aria-hidden": true, className: "size-3.5" }), " ", t("unknown", { name })] }), _jsx(Summary, { text: summary })] }));
|
|
38
48
|
}
|
|
39
49
|
const problem = def.check(props);
|
|
40
50
|
if (problem) {
|
|
41
|
-
return (_jsxs("div", { "data-slot": "card", "data-card": name, "data-state": "invalid", className: "flex flex-col gap-1.5", children: [_jsx(DegradedPanel, { reason:
|
|
51
|
+
return (_jsxs("div", { "data-slot": "card", "data-card": name, "data-state": "invalid", className: "flex flex-col gap-1.5", children: [_jsx(DegradedPanel, { reason: t("invalid", { label: def.label.toLowerCase(), problem }) }), _jsx(Summary, { text: summary })] }));
|
|
42
52
|
}
|
|
43
|
-
return (_jsx("div", { "data-slot": "card", "data-card": name, "data-state": "ready", children: _jsx(CardBoundary, { fallback: _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(DegradedPanel, { reason:
|
|
53
|
+
return (_jsx("div", { "data-slot": "card", "data-card": name, "data-state": "ready", children: _jsx(CardBoundary, { fallback: _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(DegradedPanel, { reason: t("failed", { label: def.label.toLowerCase() }) }), _jsx(Summary, { text: summary })] }), children: def.render(props) }) }));
|
|
44
54
|
}
|
|
45
55
|
/* ---------------------------------------------------------------- small checking helpers */
|
|
46
56
|
export const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
package/dist/chat/citation.d.ts
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { type LucideIcon } from "lucide-react";
|
|
2
2
|
import type { MessageSource } from "../lib/chat.js";
|
|
3
|
+
/** Citation's fixed words; override them under `citation` in LocaleProvider's `strings`. */
|
|
4
|
+
export declare const citationStrings: {
|
|
5
|
+
document: string;
|
|
6
|
+
web: string;
|
|
7
|
+
query: string;
|
|
8
|
+
data: string;
|
|
9
|
+
code: string;
|
|
10
|
+
workflow: string;
|
|
11
|
+
knowledge: string;
|
|
12
|
+
/** A kind not listed above. */
|
|
13
|
+
source: string;
|
|
14
|
+
notVerified: string;
|
|
15
|
+
sourceLabel: string;
|
|
16
|
+
sources: string;
|
|
17
|
+
};
|
|
3
18
|
/** Kinds of evidence most apps cite. Others read as "Source". */
|
|
4
19
|
export declare const sourceKinds: Record<string, {
|
|
5
20
|
Icon: LucideIcon;
|
package/dist/chat/citation.js
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
3
|
import { BookOpen, Braces, Database, FileText, Globe, Table2, Waypoints } from "lucide-react";
|
|
3
4
|
import { cn } from "../lib/utils.js";
|
|
4
5
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
|
|
6
|
+
import { useStrings } from "../blocks/locale.js";
|
|
7
|
+
/** Citation's fixed words; override them under `citation` in LocaleProvider's `strings`. */
|
|
8
|
+
export const citationStrings = {
|
|
9
|
+
document: "Document",
|
|
10
|
+
web: "Web page",
|
|
11
|
+
query: "Query",
|
|
12
|
+
data: "Data",
|
|
13
|
+
code: "Code",
|
|
14
|
+
workflow: "Workflow",
|
|
15
|
+
knowledge: "Knowledge",
|
|
16
|
+
/** A kind not listed above. */
|
|
17
|
+
source: "Source",
|
|
18
|
+
notVerified: "The agent cites this but hasn’t verified it.",
|
|
19
|
+
sourceLabel: "Source {marker}: {label}",
|
|
20
|
+
sources: "Sources",
|
|
21
|
+
};
|
|
5
22
|
/** Kinds of evidence most apps cite. Others read as "Source". */
|
|
6
23
|
export const sourceKinds = {
|
|
7
24
|
document: { Icon: FileText, noun: "Document" },
|
|
@@ -12,10 +29,18 @@ export const sourceKinds = {
|
|
|
12
29
|
workflow: { Icon: Waypoints, noun: "Workflow" },
|
|
13
30
|
knowledge: { Icon: BookOpen, noun: "Knowledge" },
|
|
14
31
|
};
|
|
15
|
-
const
|
|
32
|
+
const builtInKinds = ["document", "web", "query", "data", "code", "workflow", "knowledge"];
|
|
33
|
+
const isBuiltIn = (kind) => builtInKinds.includes(kind);
|
|
34
|
+
const kindOf = (kind, t) => {
|
|
35
|
+
const info = sourceKinds[kind];
|
|
36
|
+
if (!info)
|
|
37
|
+
return { Icon: FileText, noun: t("source") };
|
|
38
|
+
return isBuiltIn(kind) ? { ...info, noun: t(kind) } : info;
|
|
39
|
+
};
|
|
16
40
|
function SourceDetail({ source, }) {
|
|
17
|
-
const
|
|
18
|
-
|
|
41
|
+
const t = useStrings("citation", citationStrings);
|
|
42
|
+
const { Icon, noun } = kindOf(source.kind, t);
|
|
43
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-sm bg-muted text-muted-foreground", children: _jsx(Icon, { className: "size-3" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-sm font-medium leading-snug", children: source.label }), _jsx("p", { className: "text-xs text-muted-foreground", children: source.origin ?? noun })] })] }), source.snippet && (_jsx("blockquote", { className: "max-h-40 overflow-auto rounded-md bg-sunken px-2.5 py-2 text-xs leading-relaxed whitespace-pre-wrap text-muted-foreground", children: source.snippet })), source.provisional && (_jsx("p", { className: "text-xs text-provisional", children: t("notVerified") }))] }));
|
|
19
44
|
}
|
|
20
45
|
/**
|
|
21
46
|
* An inline [n]. Opens the source in a popover on click or Enter, so evidence is
|
|
@@ -23,22 +48,24 @@ function SourceDetail({ source, }) {
|
|
|
23
48
|
* renders as a plain bracket rather than a guess.
|
|
24
49
|
*/
|
|
25
50
|
function Citation({ source, marker, }) {
|
|
51
|
+
const t = useStrings("citation", citationStrings);
|
|
26
52
|
if (!source) {
|
|
27
53
|
return _jsxs("sup", { className: "text-faint-foreground", children: ["[", marker ?? "?", "]"] });
|
|
28
54
|
}
|
|
29
|
-
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label":
|
|
55
|
+
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": t("sourceLabel", { marker: source.marker, label: source.label }), className: cn("mx-0.5 inline-flex h-4 min-w-4 -translate-y-px items-center justify-center rounded-sm px-1 align-middle text-2xs font-semibold tabular-nums transition-colors duration-150 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40", source.provisional
|
|
30
56
|
? "border border-dashed border-provisional/60 text-provisional hover:bg-muted"
|
|
31
57
|
: "bg-accent-weak text-accent-text hover:bg-primary hover:text-primary-foreground"), children: source.marker }) }), _jsx(PopoverContent, { align: "start", className: "w-80 p-3", children: _jsx(SourceDetail, { source: source }) })] }));
|
|
32
58
|
}
|
|
33
59
|
/** The sources under a reply, as chips that open the same detail. */
|
|
34
60
|
function SourceList({ sources, className, }) {
|
|
61
|
+
const t = useStrings("citation", citationStrings);
|
|
35
62
|
if (sources.length === 0)
|
|
36
63
|
return null;
|
|
37
|
-
return (_jsxs("div", { "data-slot": "source-list", className: cn("flex flex-wrap items-center gap-1.5", className), children: [_jsx("span", { className: "
|
|
64
|
+
return (_jsxs("div", { "data-slot": "source-list", className: cn("flex flex-wrap items-center gap-1.5", className), children: [_jsx("span", { className: "me-0.5 text-xs text-faint-foreground", children: t("sources") }), [...sources]
|
|
38
65
|
.sort((a, b) => a.marker - b.marker)
|
|
39
66
|
.map((s) => {
|
|
40
|
-
const { Icon } = kindOf(s.kind);
|
|
41
|
-
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: cn("inline-flex max-w-56 items-center gap-1.5 rounded-md border bg-card py-0.5
|
|
67
|
+
const { Icon } = kindOf(s.kind, t);
|
|
68
|
+
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: cn("inline-flex max-w-56 items-center gap-1.5 rounded-md border bg-card py-0.5 pe-2 ps-1 text-xs text-muted-foreground transition-colors duration-150 outline-none hover:border-border-strong hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/40", s.provisional && "border-dashed"), children: [_jsx("span", { className: "flex h-4 min-w-4 items-center justify-center rounded-sm bg-muted px-1 text-2xs font-semibold tabular-nums", children: s.marker }), _jsx(Icon, { "aria-hidden": true, className: "size-3 shrink-0" }), _jsx("span", { className: "truncate", children: s.label })] }) }), _jsx(PopoverContent, { align: "start", className: "w-80 p-3", children: _jsx(SourceDetail, { source: s }) })] }, s.id));
|
|
42
69
|
})] }));
|
|
43
70
|
}
|
|
44
71
|
export { Citation, SourceList, SourceDetail };
|
package/dist/chat/composer.d.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { Attachment, ComposerScope, EntityRef, MentionOption, SlashCommand } from "../lib/chat.js";
|
|
3
|
+
/** Composer's fixed words; override them under `composer` in LocaleProvider's `strings`. */
|
|
4
|
+
export declare const composerStrings: {
|
|
5
|
+
placeholder: string;
|
|
6
|
+
label: string;
|
|
7
|
+
commands: string;
|
|
8
|
+
mentions: string;
|
|
9
|
+
attachFiles: string;
|
|
10
|
+
production: string;
|
|
11
|
+
nonProduction: string;
|
|
12
|
+
offline: string;
|
|
13
|
+
hintSend: string;
|
|
14
|
+
hintNewLine: string;
|
|
15
|
+
hintMention: string;
|
|
16
|
+
stop: string;
|
|
17
|
+
send: string;
|
|
18
|
+
dropFiles: string;
|
|
19
|
+
};
|
|
3
20
|
export type ComposerState = "ready" | "sending" | "streaming" | {
|
|
4
21
|
kind: "locked";
|
|
5
22
|
reason: string;
|
|
@@ -14,9 +31,11 @@ export type ComposerState = "ready" | "sending" | "streaming" | {
|
|
|
14
31
|
* against, and turns to a warning when that is production.
|
|
15
32
|
*
|
|
16
33
|
* Enter sends, Shift+Enter breaks a line. While an agent replies the send button
|
|
17
|
-
* becomes Stop. A locked composer says why; an offline one says so.
|
|
34
|
+
* becomes Stop. A locked composer says why; an offline one says so. With
|
|
35
|
+
* `onAttach`, files can also be pasted or dropped onto it. `tools` adds your
|
|
36
|
+
* own controls to the toolbar, such as a dictation button.
|
|
18
37
|
*/
|
|
19
|
-
declare function Composer({ value, onChange, onSubmit, onStop, state, scope, onScopeClick, mentions, commands, references, onReferencesChange, attachments, onAttach, onRemoveAttachment, placeholder, label, className, }: {
|
|
38
|
+
declare function Composer({ value, onChange, onSubmit, onStop, state, scope, onScopeClick, mentions, commands, references, onReferencesChange, attachments, onAttach, accept, onRemoveAttachment, placeholder: placeholderProp, label: labelProp, tools, className, }: {
|
|
20
39
|
/** The text in the box. */
|
|
21
40
|
value: string;
|
|
22
41
|
/** Called with the new text as the person types. */
|
|
@@ -43,12 +62,19 @@ declare function Composer({ value, onChange, onSubmit, onStop, state, scope, onS
|
|
|
43
62
|
attachments?: Attachment[];
|
|
44
63
|
/** Shows an attach button; called with the chosen files. */
|
|
45
64
|
onAttach?: (files: File[]) => void;
|
|
65
|
+
/**
|
|
66
|
+
* Which files the attach button's picker offers, as for `<input accept>` (e.g. "image/*,.pdf"). Files pasted or
|
|
67
|
+
* dropped arrive as they are, so check types where you upload.
|
|
68
|
+
*/
|
|
69
|
+
accept?: string;
|
|
46
70
|
/** Called when a file is removed. */
|
|
47
71
|
onRemoveAttachment?: (id: string) => void;
|
|
48
|
-
/** Shown in the empty box. */
|
|
72
|
+
/** Shown in the empty box; "Ask a question, or describe a change" by default. */
|
|
49
73
|
placeholder?: string;
|
|
50
|
-
/** The accessible name of the text box. */
|
|
74
|
+
/** The accessible name of the text box; "Message" by default. */
|
|
51
75
|
label?: string;
|
|
76
|
+
/** Your own controls in the toolbar, after the attach button: a dictation button, a model picker. */
|
|
77
|
+
tools?: React.ReactNode;
|
|
52
78
|
className?: string;
|
|
53
79
|
}): React.JSX.Element;
|
|
54
80
|
export { Composer };
|
package/dist/chat/composer.js
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { ArrowUp, LoaderCircle, Paperclip, Square, TriangleAlert
|
|
4
|
+
import { ArrowUp, LoaderCircle, Paperclip, Square, TriangleAlert } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
6
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
|
|
7
7
|
import { EntityChip, useEntityKinds } from "./entity-chip.js";
|
|
8
|
+
import { AttachmentChip } from "./attachment-preview.js";
|
|
9
|
+
import { useStrings } from "../blocks/locale.js";
|
|
10
|
+
/** Composer's fixed words; override them under `composer` in LocaleProvider's `strings`. */
|
|
11
|
+
export const composerStrings = {
|
|
12
|
+
placeholder: "Ask a question, or describe a change",
|
|
13
|
+
label: "Message",
|
|
14
|
+
commands: "Commands",
|
|
15
|
+
mentions: "Mentions",
|
|
16
|
+
attachFiles: "Attach files",
|
|
17
|
+
production: "Production",
|
|
18
|
+
nonProduction: "Non-production",
|
|
19
|
+
offline: "You’re offline. Your message will wait here.",
|
|
20
|
+
hintSend: "send",
|
|
21
|
+
hintNewLine: "new line",
|
|
22
|
+
hintMention: "mention",
|
|
23
|
+
stop: "Stop",
|
|
24
|
+
send: "Send",
|
|
25
|
+
dropFiles: "Drop files to attach",
|
|
26
|
+
};
|
|
8
27
|
/** The @ or / token the caret sits in, if any. A / only counts at the start of the message. */
|
|
9
28
|
function findTrigger(value, caret) {
|
|
10
29
|
const before = value.slice(0, caret);
|
|
@@ -17,16 +36,9 @@ function findTrigger(value, caret) {
|
|
|
17
36
|
return null;
|
|
18
37
|
return { char, start, query: m[3] };
|
|
19
38
|
}
|
|
20
|
-
const groupOf = (item) => (item.type === "command" ?
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return null;
|
|
24
|
-
if (bytes < 1024)
|
|
25
|
-
return `${bytes} B`;
|
|
26
|
-
if (bytes < 1024 * 1024)
|
|
27
|
-
return `${Math.round(bytes / 1024)} KB`;
|
|
28
|
-
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
29
|
-
}
|
|
39
|
+
const groupOf = (item, commandsLabel = "Commands") => (item.type === "command" ? commandsLabel : item.option.group);
|
|
40
|
+
/** Whether a drag carries files (not text or a link being dragged around the page). */
|
|
41
|
+
const hasFiles = (e) => Array.from(e.dataTransfer?.types ?? []).includes("Files");
|
|
30
42
|
/**
|
|
31
43
|
* Where a person asks. Mentions (@) attach things in the user's system as chips the agent
|
|
32
44
|
* receives as references, not as text it has to parse; slash commands (/) start
|
|
@@ -34,16 +46,24 @@ function formatSize(bytes) {
|
|
|
34
46
|
* against, and turns to a warning when that is production.
|
|
35
47
|
*
|
|
36
48
|
* Enter sends, Shift+Enter breaks a line. While an agent replies the send button
|
|
37
|
-
* becomes Stop. A locked composer says why; an offline one says so.
|
|
49
|
+
* becomes Stop. A locked composer says why; an offline one says so. With
|
|
50
|
+
* `onAttach`, files can also be pasted or dropped onto it. `tools` adds your
|
|
51
|
+
* own controls to the toolbar, such as a dictation button.
|
|
38
52
|
*/
|
|
39
|
-
function Composer({ value, onChange, onSubmit, onStop, state = "ready", scope, onScopeClick, mentions = [], commands = [], references = [], onReferencesChange, attachments = [], onAttach, onRemoveAttachment, placeholder
|
|
53
|
+
function Composer({ value, onChange, onSubmit, onStop, state = "ready", scope, onScopeClick, mentions = [], commands = [], references = [], onReferencesChange, attachments = [], onAttach, accept, onRemoveAttachment, placeholder: placeholderProp, label: labelProp, tools, className, }) {
|
|
40
54
|
const kindOf = useEntityKinds();
|
|
55
|
+
const t = useStrings("composer", composerStrings);
|
|
56
|
+
const placeholder = placeholderProp ?? t("placeholder");
|
|
57
|
+
const label = labelProp ?? t("label");
|
|
41
58
|
const id = React.useId();
|
|
42
59
|
const textarea = React.useRef(null);
|
|
60
|
+
// Files dragged over the composer: a count of enter/leave pairs, since child elements fire their own.
|
|
61
|
+
const [dragDepth, setDragDepth] = React.useState(0);
|
|
43
62
|
const fileInput = React.useRef(null);
|
|
44
63
|
const [trigger, setTrigger] = React.useState(null);
|
|
45
64
|
const [active, setActive] = React.useState(0);
|
|
46
65
|
const locked = typeof state === "object" ? state : null;
|
|
66
|
+
const canAttach = Boolean(onAttach) && !locked;
|
|
47
67
|
const busy = state === "sending" || state === "streaming";
|
|
48
68
|
const canSend = !locked && !busy && (value.trim().length > 0 || references.length > 0);
|
|
49
69
|
const items = React.useMemo(() => {
|
|
@@ -143,26 +163,55 @@ function Composer({ value, onChange, onSubmit, onStop, state = "ready", scope, o
|
|
|
143
163
|
const production = !!scope?.production;
|
|
144
164
|
const listId = `${id}-menu`;
|
|
145
165
|
const optionId = (i) => `${id}-opt-${i}`;
|
|
146
|
-
return (_jsxs("div", { "data-slot": "composer",
|
|
147
|
-
|
|
166
|
+
return (_jsxs("div", { "data-slot": "composer", "data-dragging": dragDepth > 0 || undefined, className: cn("@container relative", className), onDragEnter: (e) => {
|
|
167
|
+
if (!canAttach || !hasFiles(e))
|
|
168
|
+
return;
|
|
169
|
+
e.preventDefault();
|
|
170
|
+
setDragDepth((d) => d + 1);
|
|
171
|
+
}, onDragOver: (e) => {
|
|
172
|
+
if (!hasFiles(e))
|
|
173
|
+
return;
|
|
174
|
+
// Always claim a file drag, so a drop that can't attach doesn't open the file in place of the app.
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
e.dataTransfer.dropEffect = canAttach ? "copy" : "none";
|
|
177
|
+
}, onDragLeave: () => setDragDepth((d) => Math.max(0, d - 1)), onDrop: (e) => {
|
|
178
|
+
if (!hasFiles(e))
|
|
179
|
+
return;
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
setDragDepth(0);
|
|
182
|
+
if (!canAttach)
|
|
183
|
+
return;
|
|
184
|
+
const files = Array.from(e.dataTransfer.files);
|
|
185
|
+
if (files.length)
|
|
186
|
+
onAttach(files);
|
|
187
|
+
}, children: [dragDepth > 0 && (_jsx("div", { "aria-hidden": true, className: "pointer-events-none absolute inset-0 z-30 flex items-center justify-center rounded-2xl border-2 border-dashed border-primary bg-card/90 text-sm font-medium text-accent-text", children: t("dropFiles") })), menuOpen && (_jsx("div", { id: listId, role: "listbox", "aria-label": trigger?.char === "/" ? t("commands") : t("mentions"), className: "absolute inset-x-0 bottom-full z-20 mb-2 max-h-72 overflow-y-auto rounded-xl border bg-popover p-1 shadow-float animate-in fade-in-0 slide-in-from-bottom-1", children: items.map((item, i) => {
|
|
188
|
+
const group = groupOf(item, t("commands"));
|
|
148
189
|
const prev = items[i - 1];
|
|
149
|
-
const header = !prev || groupOf(prev) !== group ? group : null;
|
|
190
|
+
const header = !prev || groupOf(prev, t("commands")) !== group ? group : null;
|
|
150
191
|
const Icon = item.type === "mention" && item.option.entity ? kindOf(item.option.entity.kind).Icon : null;
|
|
151
192
|
return (_jsxs(React.Fragment, { children: [header && _jsx("p", { className: "px-2 pt-2 pb-1 text-2xs font-medium text-faint-foreground", children: header }), _jsx("div", { id: optionId(i), role: "option", tabIndex: -1, "aria-selected": i === active, onMouseDown: (e) => {
|
|
152
193
|
e.preventDefault();
|
|
153
194
|
choose(item);
|
|
154
|
-
}, onMouseMove: () => setActive(i), className: "flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-1.5 text-sm aria-selected:bg-accent", children: item.type === "command" ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "font-mono text-xs text-accent-text", children: ["/", item.command.name] }), item.command.args && _jsx("span", { className: "font-mono text-xs text-faint-foreground", children: item.command.args }), _jsx("span", { className: "
|
|
155
|
-
}) })), _jsxs("div", { className: cn("rounded-2xl border bg-card shadow-raised transition-[border-color,box-shadow] duration-150 focus-within:border-border-strong focus-within:shadow-float", locked && "bg-muted shadow-none"), children: [(references.length > 0 || attachments.length > 0) && (_jsxs("div", { className: "flex flex-wrap gap-1.5 px-3 pt-3", children: [references.map((r) => (_jsx(EntityChip, { entity: r, onRemove: (e) => onReferencesChange?.(references.filter((x) => x !== e)) }, `${r.kind}:${r.name}`))), attachments.map((a) => (
|
|
195
|
+
}, onMouseMove: () => setActive(i), className: "flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-1.5 text-sm aria-selected:bg-accent", children: item.type === "command" ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "font-mono text-xs text-accent-text", children: ["/", item.command.name] }), item.command.args && _jsx("span", { className: "font-mono text-xs text-faint-foreground", children: item.command.args }), _jsx("span", { className: "ms-auto truncate text-xs text-muted-foreground", children: item.command.description })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex size-6 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: Icon ? _jsx(Icon, { className: "size-3.5" }) : _jsx("span", { className: "text-xs", children: "@" }) }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate", children: item.option.label }), (item.option.description ?? item.option.entity?.name) !== item.option.label && (item.option.description ?? item.option.entity?.name) && (_jsx("span", { className: "block truncate font-mono text-2xs text-faint-foreground", children: item.option.description ?? item.option.entity?.name }))] })] })) })] }, item.type === "command" ? item.command.id : item.option.id));
|
|
196
|
+
}) })), _jsxs("div", { className: cn("rounded-2xl border bg-card shadow-raised transition-[border-color,box-shadow] duration-150 focus-within:border-border-strong focus-within:shadow-float", locked && "bg-muted shadow-none"), children: [(references.length > 0 || attachments.length > 0) && (_jsxs("div", { className: "flex flex-wrap gap-1.5 px-3 pt-3", children: [references.map((r) => (_jsx(EntityChip, { entity: r, onRemove: (e) => onReferencesChange?.(references.filter((x) => x !== e)) }, `${r.kind}:${r.name}`))), attachments.map((a) => (_jsx(AttachmentChip, { attachment: a, onRemove: onRemoveAttachment }, a.id)))] })), _jsx("label", { htmlFor: id, className: "sr-only", children: label }), _jsx("textarea", { ref: textarea, id: id, rows: 1, value: value, disabled: Boolean(locked), placeholder: locked ? undefined : placeholder, onChange: (e) => {
|
|
156
197
|
onChange(e.target.value);
|
|
157
198
|
requestAnimationFrame(syncTrigger);
|
|
158
199
|
}, onKeyDown: onKeyDown, onKeyUp: (e) => {
|
|
159
200
|
if (e.key === "ArrowLeft" || e.key === "ArrowRight")
|
|
160
201
|
syncTrigger();
|
|
161
|
-
}, onClick: syncTrigger, onBlur: () => setTrigger(null),
|
|
202
|
+
}, onClick: syncTrigger, onBlur: () => setTrigger(null), onPaste: (e) => {
|
|
203
|
+
// Pasted files (a screenshot, files copied in Finder) attach; pasted text is typed as usual.
|
|
204
|
+
const files = Array.from(e.clipboardData?.files ?? []);
|
|
205
|
+
if (!files.length || !canAttach)
|
|
206
|
+
return;
|
|
207
|
+
onAttach(files);
|
|
208
|
+
if (!e.clipboardData.getData("text/plain"))
|
|
209
|
+
e.preventDefault();
|
|
210
|
+
}, "aria-controls": menuOpen ? listId : undefined, "aria-activedescendant": menuOpen ? optionId(active) : undefined, "aria-autocomplete": "list", "aria-describedby": locked ? `${id}-locked` : undefined, className: cn("block max-h-60 min-h-12 w-full resize-none bg-transparent px-4 pt-3 pb-1 text-prose leading-relaxed outline-none [field-sizing:content] placeholder:text-faint-foreground disabled:cursor-not-allowed", locked && !value && "min-h-0 h-3 pt-3 pb-0") }), locked && (_jsx("p", { id: `${id}-locked`, className: "flex items-center gap-1.5 px-4 pb-1 text-sm text-muted-foreground", children: locked.kind === "offline" ? locked.reason ?? t("offline") : locked.reason })), _jsxs("div", { className: "flex items-center gap-1.5 px-2 pt-1 pb-2", children: [onAttach && (_jsxs(_Fragment, { children: [_jsx("input", { ref: fileInput, type: "file", multiple: true, accept: accept, tabIndex: -1, "aria-label": t("attachFiles"), className: "hidden", onChange: (e) => {
|
|
162
211
|
const files = Array.from(e.target.files ?? []);
|
|
163
212
|
if (files.length)
|
|
164
213
|
onAttach(files);
|
|
165
214
|
e.target.value = "";
|
|
166
|
-
} }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": "
|
|
215
|
+
} }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": t("attachFiles"), disabled: Boolean(locked), onClick: () => fileInput.current?.click(), className: "flex size-8 items-center justify-center rounded-lg text-muted-foreground transition-colors duration-150 hover:bg-accent hover:text-foreground disabled:opacity-40", children: _jsx(Paperclip, { className: "size-4" }) }) }), _jsx(TooltipContent, { children: t("attachFiles") })] })] })), tools, scope && (_jsxs("button", { type: "button", onClick: onScopeClick, disabled: !onScopeClick, className: cn("inline-flex min-w-0 items-center gap-1.5 rounded-lg px-2 py-1 text-xs transition-colors duration-150 enabled:hover:bg-accent", production ? "bg-warning-weak text-warning-text enabled:hover:bg-warning-weak" : "text-muted-foreground"), children: [production ? _jsx(TriangleAlert, { "aria-hidden": true, className: "size-3 shrink-0" }) : _jsx("span", { "aria-hidden": true, className: "size-1.5 shrink-0 rounded-full bg-success" }), _jsxs("span", { className: "truncate", children: [scope.environment, " \u00B7 ", production ? t("production") : (scope.detail ?? t("nonProduction"))] })] })), _jsxs("span", { className: "ms-auto hidden text-2xs text-faint-foreground @lg:inline", children: [_jsx("kbd", { className: "font-sans", children: "\u21B5" }), " ", t("hintSend"), " \u00B7 ", _jsx("kbd", { className: "font-sans", children: "\u21E7\u21B5" }), " ", t("hintNewLine"), " \u00B7 ", _jsx("kbd", { className: "font-sans", children: "@" }), " ", t("hintMention")] }), state === "streaming" && onStop ? (_jsx("button", { type: "button", "aria-label": t("stop"), onClick: onStop, className: "ms-auto flex size-8 items-center justify-center rounded-lg bg-foreground text-background transition-transform duration-150 active:scale-95 @lg:ms-1", children: _jsx(Square, { className: "size-3 fill-current" }) })) : (_jsx("button", { type: "button", "aria-label": t("send"), onClick: submit, disabled: !canSend, "aria-busy": state === "sending", className: "ms-auto flex size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground transition-[background-color,opacity,transform] duration-150 enabled:hover:bg-primary-hover active:scale-95 disabled:opacity-30 @lg:ms-1", children: state === "sending" ? _jsx(LoaderCircle, { className: "size-4 animate-spin" }) : _jsx(ArrowUp, { className: "size-4", strokeWidth: 2.5 }) }))] })] })] }));
|
|
167
216
|
}
|
|
168
217
|
export { Composer };
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { ContextSegment } from "../lib/know.js";
|
|
2
|
+
/** ContextMeter's fixed words; override them under `contextMeter` in LocaleProvider's `strings`. */
|
|
3
|
+
export declare const contextMeterStrings: {
|
|
4
|
+
/** A token count in thousands; `{n}` is already formatted. */
|
|
5
|
+
thousands: string;
|
|
6
|
+
label: string;
|
|
7
|
+
context: string;
|
|
8
|
+
usage: string;
|
|
9
|
+
nearlyFull: string;
|
|
10
|
+
};
|
|
2
11
|
/**
|
|
3
12
|
* What fills the model's context budget, as a ring in the composer that
|
|
4
13
|
* opens to a breakdown: system, instructions, Know, the thread, tools, files.
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
3
|
import { cn } from "../lib/utils.js";
|
|
3
4
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
|
|
5
|
+
import { useFormat, useStrings } from "../blocks/locale.js";
|
|
6
|
+
/** ContextMeter's fixed words; override them under `contextMeter` in LocaleProvider's `strings`. */
|
|
7
|
+
export const contextMeterStrings = {
|
|
8
|
+
/** A token count in thousands; `{n}` is already formatted. */
|
|
9
|
+
thousands: "{n}k",
|
|
10
|
+
label: "Context: {used} of {limit} tokens used",
|
|
11
|
+
context: "Context",
|
|
12
|
+
usage: "{used} / {limit} tokens",
|
|
13
|
+
nearlyFull: "Nearly full. Older thread turns will be summarised to make room.",
|
|
14
|
+
};
|
|
4
15
|
const kindColor = {
|
|
5
16
|
system: "var(--chart-5)",
|
|
6
17
|
instructions: "var(--chart-1)",
|
|
@@ -9,8 +20,11 @@ const kindColor = {
|
|
|
9
20
|
tools: "var(--chart-3)",
|
|
10
21
|
files: "var(--faint-foreground)",
|
|
11
22
|
};
|
|
12
|
-
function k(n) {
|
|
13
|
-
|
|
23
|
+
function k(n, format, thousands) {
|
|
24
|
+
if (n < 1000)
|
|
25
|
+
return format(n, { useGrouping: false });
|
|
26
|
+
const digits = n >= 10_000 ? 0 : 1;
|
|
27
|
+
return thousands(format(n / 1000, { minimumFractionDigits: digits, maximumFractionDigits: digits, useGrouping: false }));
|
|
14
28
|
}
|
|
15
29
|
/**
|
|
16
30
|
* What fills the model's context budget, as a ring in the composer that
|
|
@@ -18,13 +32,16 @@ function k(n) {
|
|
|
18
32
|
* Counts are the service's own token counts; the ring warns past 80%.
|
|
19
33
|
*/
|
|
20
34
|
function ContextMeter({ segments, limit, className, }) {
|
|
35
|
+
const t = useStrings("contextMeter", contextMeterStrings);
|
|
36
|
+
const fmt = useFormat();
|
|
37
|
+
const tokens = (n) => k(n, fmt.number, (v) => t("thousands", { n: v }));
|
|
21
38
|
const used = segments.reduce((n, s) => n + s.tokens, 0);
|
|
22
39
|
const pct = Math.min(1, used / limit);
|
|
23
40
|
const warn = pct > 0.8;
|
|
24
41
|
const r = 7;
|
|
25
42
|
const c = 2 * Math.PI * r;
|
|
26
|
-
return (_jsxs(Popover, { children: [_jsxs(PopoverTrigger, { "aria-label":
|
|
43
|
+
return (_jsxs(Popover, { children: [_jsxs(PopoverTrigger, { "aria-label": t("label", { used: tokens(used), limit: tokens(limit) }), className: cn("inline-flex items-center gap-1.5 rounded-md px-1.5 py-1 text-2xs tabular-nums outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/40", warn ? "text-warning-text" : "text-muted-foreground", className), children: [_jsxs("svg", { viewBox: "0 0 18 18", className: "size-4 -rotate-90", "aria-hidden": true, children: [_jsx("circle", { cx: "9", cy: "9", r: r, fill: "none", stroke: "var(--muted)", strokeWidth: "2.5" }), _jsx("circle", { cx: "9", cy: "9", r: r, fill: "none", stroke: warn ? "var(--warning)" : "var(--primary)", strokeWidth: "2.5", strokeDasharray: `${pct * c} ${c}`, strokeLinecap: "round" })] }), fmt.number(Math.round(pct * 100) / 100, { style: "percent" })] }), _jsxs(PopoverContent, { align: "start", className: "w-72 p-3", children: [_jsxs("p", { className: "flex items-baseline justify-between text-sm font-medium", children: [t("context"), " ", _jsx("span", { className: "text-xs font-normal text-muted-foreground tabular-nums", children: t("usage", { used: tokens(used), limit: tokens(limit) }) })] }), _jsx("div", { "aria-hidden": true, className: "mt-2 flex h-2 overflow-hidden rounded-full bg-muted", children: segments.map((s) => (_jsx("span", { style: { width: `${(s.tokens / limit) * 100}%`, background: kindColor[s.kind] } }, s.label))) }), _jsx("ul", { className: "mt-3 flex flex-col gap-1.5", children: [...segments]
|
|
27
44
|
.sort((a, b) => b.tokens - a.tokens)
|
|
28
|
-
.map((s) => (_jsxs("li", { className: "flex items-center gap-2 text-xs", children: [_jsx("span", { "aria-hidden": true, className: "size-2 rounded-xs", style: { background: kindColor[s.kind] } }), _jsx("span", { className: "flex-1", children: s.label }), _jsx("span", { className: "text-muted-foreground tabular-nums", children:
|
|
45
|
+
.map((s) => (_jsxs("li", { className: "flex items-center gap-2 text-xs", children: [_jsx("span", { "aria-hidden": true, className: "size-2 rounded-xs", style: { background: kindColor[s.kind] } }), _jsx("span", { className: "flex-1", children: s.label }), _jsx("span", { className: "text-muted-foreground tabular-nums", children: tokens(s.tokens) })] }, s.label))) }), warn && _jsx("p", { className: "mt-3 text-xs text-warning-text", children: t("nearlyFull") })] })] }));
|
|
29
46
|
}
|
|
30
47
|
export { ContextMeter };
|
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { type LucideIcon } from "lucide-react";
|
|
3
3
|
import type { EntityRef } from "../lib/chat.js";
|
|
4
|
+
/** EntityChip's fixed words; override them under `entityChip` in LocaleProvider's `strings`. */
|
|
5
|
+
export declare const entityChipStrings: {
|
|
6
|
+
record: string;
|
|
7
|
+
document: string;
|
|
8
|
+
file: string;
|
|
9
|
+
table: string;
|
|
10
|
+
field: string;
|
|
11
|
+
person: string;
|
|
12
|
+
link: string;
|
|
13
|
+
code: string;
|
|
14
|
+
workflow: string;
|
|
15
|
+
report: string;
|
|
16
|
+
dashboard: string;
|
|
17
|
+
/** An unregistered kind. */
|
|
18
|
+
item: string;
|
|
19
|
+
title: string;
|
|
20
|
+
titleIn: string;
|
|
21
|
+
remove: string;
|
|
22
|
+
};
|
|
4
23
|
export type EntityKindInfo = {
|
|
5
24
|
Icon: LucideIcon;
|
|
6
25
|
noun: string;
|