@candor-ui/react 1.0.0 → 1.1.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/adapters/ag-ui.js +3 -3
- package/dist/adapters/ai-sdk.d.ts +2 -2
- package/dist/adapters/ai-sdk.js +7 -6
- package/dist/adapters/claude-agent-sdk.js +2 -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 +27 -4
- package/dist/chat/composer.js +68 -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 +37 -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 +5 -0
- package/dist/chat/thread.js +9 -2
- package/dist/chat/tool-call.d.ts +12 -0
- package/dist/chat/tool-call.js +16 -3
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lib/chat.d.ts +31 -1
- package/dist/lib/highlight.d.ts +15 -0
- package/dist/lib/highlight.js +224 -0
- package/dist/lib/safe-markdown.d.ts +7 -2
- package/dist/lib/safe-markdown.js +13 -4
- package/dist/lib/status.d.ts +15 -0
- package/dist/lib/status.js +10 -1
- package/dist/styles.css +40 -1
- package/dist/ui/button.d.ts +1 -1
- package/package.json +2 -3
|
@@ -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;
|
package/dist/chat/entity-chip.js
CHANGED
|
@@ -3,6 +3,30 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Box, Braces, ChartColumn, File, FileText, Gauge, Link, Table2, TextCursorInput, UserRound, Waypoints, } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
|
+
import { useStrings } from "../blocks/locale.js";
|
|
7
|
+
/** EntityChip's fixed words; override them under `entityChip` in LocaleProvider's `strings`. */
|
|
8
|
+
export const entityChipStrings = {
|
|
9
|
+
record: "Record",
|
|
10
|
+
document: "Document",
|
|
11
|
+
file: "File",
|
|
12
|
+
table: "Table",
|
|
13
|
+
field: "Field",
|
|
14
|
+
person: "Person",
|
|
15
|
+
link: "Link",
|
|
16
|
+
code: "Code",
|
|
17
|
+
workflow: "Workflow",
|
|
18
|
+
report: "Report",
|
|
19
|
+
dashboard: "Dashboard",
|
|
20
|
+
/** An unregistered kind. */
|
|
21
|
+
item: "Item",
|
|
22
|
+
title: "{noun}: {name}",
|
|
23
|
+
titleIn: "{noun}: {name} in {location}",
|
|
24
|
+
remove: "Remove {name}",
|
|
25
|
+
};
|
|
26
|
+
const baseKindNames = {
|
|
27
|
+
record: 1, document: 1, file: 1, table: 1, field: 1, person: 1, link: 1, code: 1, workflow: 1, report: 1, dashboard: 1,
|
|
28
|
+
};
|
|
29
|
+
const isBaseKind = (kind) => Object.hasOwn(baseKindNames, kind);
|
|
6
30
|
/** Kinds most apps have. Add your own (or a preset) with EntityKindsProvider. */
|
|
7
31
|
export const baseKinds = {
|
|
8
32
|
record: { Icon: FileText, noun: "Record" },
|
|
@@ -32,7 +56,14 @@ function EntityKindsProvider({ kinds, children, }) {
|
|
|
32
56
|
/** Look up a kind's icon and noun; unknown kinds read as "Item". */
|
|
33
57
|
function useEntityKinds() {
|
|
34
58
|
const kinds = React.useContext(KindsContext);
|
|
35
|
-
|
|
59
|
+
const t = useStrings("entityChip", entityChipStrings);
|
|
60
|
+
return React.useCallback((kind) => {
|
|
61
|
+
const info = kinds[kind];
|
|
62
|
+
if (!info)
|
|
63
|
+
return { ...unknownKind, noun: t("item") };
|
|
64
|
+
// Built-in kinds take their noun from the strings table; an app's own kinds keep theirs.
|
|
65
|
+
return isBaseKind(kind) && info === baseKinds[kind] ? { ...info, noun: t(kind) } : info;
|
|
66
|
+
}, [kinds, t]);
|
|
36
67
|
}
|
|
37
68
|
/**
|
|
38
69
|
* A thing in the user's system (a record, a table, a field, a workflow…) named
|
|
@@ -43,10 +74,13 @@ function useEntityKinds() {
|
|
|
43
74
|
*/
|
|
44
75
|
function EntityChip({ entity, onOpen, onRemove, className, }) {
|
|
45
76
|
const { Icon, noun } = useEntityKinds()(entity.kind);
|
|
77
|
+
const t = useStrings("entityChip", entityChipStrings);
|
|
46
78
|
const shown = entity.label ?? entity.name;
|
|
47
|
-
const title =
|
|
79
|
+
const title = entity.location
|
|
80
|
+
? t("titleIn", { noun, name: entity.name, location: entity.location })
|
|
81
|
+
: t("title", { noun, name: entity.name });
|
|
48
82
|
const body = (_jsxs(_Fragment, { children: [_jsx(Icon, { "aria-hidden": true, className: "size-3 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: shown }), _jsxs("span", { className: "sr-only", children: [" (", title, ")"] })] }));
|
|
49
83
|
const base = "inline-flex max-w-full items-center gap-1 rounded-sm border bg-card px-1.5 py-px align-baseline font-mono text-[0.92em] leading-[1.35] text-foreground";
|
|
50
|
-
return (_jsxs("span", { "data-slot": "entity-chip", "data-kind": entity.kind, title: title, className: cn("inline-flex max-w-full min-w-0 items-center sm:max-w-64", className), children: [onOpen ? (_jsx("button", { type: "button", onClick: () => onOpen(entity), className: cn(base, "cursor-pointer transition-colors duration-150 hover:border-border-strong hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/40 outline-none"), children: body })) : (_jsx("span", { className: base, children: body })), onRemove && (_jsx("button", { type: "button", "aria-label":
|
|
84
|
+
return (_jsxs("span", { "data-slot": "entity-chip", "data-kind": entity.kind, title: title, className: cn("inline-flex max-w-full min-w-0 items-center sm:max-w-64", className), children: [onOpen ? (_jsx("button", { type: "button", onClick: () => onOpen(entity), className: cn(base, "cursor-pointer transition-colors duration-150 hover:border-border-strong hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/40 outline-none"), children: body })) : (_jsx("span", { className: base, children: body })), onRemove && (_jsx("button", { type: "button", "aria-label": t("remove", { name: shown }), onClick: () => onRemove(entity), className: "-ms-px rounded-e-sm border border-s-0 bg-card px-1 py-px text-faint-foreground hover:text-foreground", children: "\u00D7" }))] }));
|
|
51
85
|
}
|
|
52
86
|
export { EntityChip, EntityKindsProvider, useEntityKinds };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** FoldedTurns' fixed words; override them under `foldedTurns` in LocaleProvider's `strings`. */
|
|
3
|
+
export declare const foldedTurnsStrings: {
|
|
4
|
+
show: {
|
|
5
|
+
one: string;
|
|
6
|
+
other: string;
|
|
7
|
+
};
|
|
8
|
+
hide: {
|
|
9
|
+
one: string;
|
|
10
|
+
other: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* A run of turns folded into one line, for long agent work a reader doesn't need to see step by step: "Drafted three
|
|
15
|
+
* versions and compared them". The line says how many turns are inside and opens them in place; `summary` is yours
|
|
16
|
+
* (a sentence, a count of tool calls, a cost). Folding never hides that the turns happened.
|
|
17
|
+
*/
|
|
18
|
+
declare function FoldedTurns({ count, summary, defaultOpen, open, onOpenChange, children, className, }: {
|
|
19
|
+
/** How many turns are inside, for the button ("Show 12 turns"). */
|
|
20
|
+
count: number;
|
|
21
|
+
/** What happened in them, shown on the folded line: text or your own small summary. */
|
|
22
|
+
summary?: React.ReactNode;
|
|
23
|
+
/** Start open. */
|
|
24
|
+
defaultOpen?: boolean;
|
|
25
|
+
/** Controlled open state. */
|
|
26
|
+
open?: boolean;
|
|
27
|
+
/** Called when a person opens or closes it. */
|
|
28
|
+
onOpenChange?: (open: boolean) => void;
|
|
29
|
+
/** The turns, usually Messages. */
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
className?: string;
|
|
32
|
+
}): React.JSX.Element;
|
|
33
|
+
export { FoldedTurns };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ChevronRight } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
import { useStrings } from "../blocks/locale.js";
|
|
7
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
|
|
8
|
+
/** FoldedTurns' fixed words; override them under `foldedTurns` in LocaleProvider's `strings`. */
|
|
9
|
+
export const foldedTurnsStrings = {
|
|
10
|
+
show: { one: "Show {count} turn", other: "Show {count} turns" },
|
|
11
|
+
hide: { one: "Hide {count} turn", other: "Hide {count} turns" },
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* A run of turns folded into one line, for long agent work a reader doesn't need to see step by step: "Drafted three
|
|
15
|
+
* versions and compared them". The line says how many turns are inside and opens them in place; `summary` is yours
|
|
16
|
+
* (a sentence, a count of tool calls, a cost). Folding never hides that the turns happened.
|
|
17
|
+
*/
|
|
18
|
+
function FoldedTurns({ count, summary, defaultOpen = false, open, onOpenChange, children, className, }) {
|
|
19
|
+
const t = useStrings("foldedTurns", foldedTurnsStrings);
|
|
20
|
+
const [own, setOwn] = React.useState(defaultOpen);
|
|
21
|
+
const isOpen = open ?? own;
|
|
22
|
+
const set = (next) => {
|
|
23
|
+
if (open === undefined)
|
|
24
|
+
setOwn(next);
|
|
25
|
+
onOpenChange?.(next);
|
|
26
|
+
};
|
|
27
|
+
return (_jsxs(Collapsible, { open: isOpen, onOpenChange: set, "data-slot": "folded-turns", className: cn("group/folded flex flex-col gap-7", className), children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 rounded-lg border border-dashed px-3 py-2 text-sm", children: [summary && _jsx("div", { className: "min-w-0 flex-1 text-muted-foreground", children: summary }), _jsxs(CollapsibleTrigger, { className: "ms-auto inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-xs font-medium text-accent-text outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/40", children: [_jsx(ChevronRight, { "aria-hidden": true, className: "rtl:-scale-x-100 size-3.5 transition-transform duration-200 group-data-[state=open]/folded:[transform:rotate(90deg)]" }), t(isOpen ? "hide" : "show", { count })] })] }), _jsx(CollapsibleContent, { className: "flex flex-col gap-7", children: children })] }));
|
|
28
|
+
}
|
|
29
|
+
export { FoldedTurns };
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
/** MessageActions' fixed words; override them under `messageActions` in LocaleProvider's `strings`. */
|
|
3
|
+
export declare const messageActionsStrings: {
|
|
4
|
+
copy: string;
|
|
5
|
+
copied: string;
|
|
6
|
+
copyFailed: string;
|
|
7
|
+
retry: string;
|
|
8
|
+
good: string;
|
|
9
|
+
bad: string;
|
|
10
|
+
};
|
|
2
11
|
export type Rating = "up" | "down" | null;
|
|
3
12
|
/**
|
|
4
13
|
* Copy, retry and rate under an agent's reply. Each action appears only when
|
|
@@ -5,6 +5,16 @@ import { Check, Copy, RotateCw, ThumbsDown, ThumbsUp, X } from "lucide-react";
|
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
6
|
import { useCopy } from "../lib/use-copy.js";
|
|
7
7
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
|
|
8
|
+
import { useStrings } from "../blocks/locale.js";
|
|
9
|
+
/** MessageActions' fixed words; override them under `messageActions` in LocaleProvider's `strings`. */
|
|
10
|
+
export const messageActionsStrings = {
|
|
11
|
+
copy: "Copy",
|
|
12
|
+
copied: "Copied",
|
|
13
|
+
copyFailed: "Couldn’t copy",
|
|
14
|
+
retry: "Retry",
|
|
15
|
+
good: "Good answer",
|
|
16
|
+
bad: "Bad answer",
|
|
17
|
+
};
|
|
8
18
|
function ActionButton({ label, onClick, pressed, children, }) {
|
|
9
19
|
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": label, "aria-pressed": pressed, onClick: onClick, className: "flex size-7 items-center justify-center rounded-md text-faint-foreground transition-colors duration-150 outline-none hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/40 aria-pressed:text-accent-text [&_svg]:size-3.5", children: children }) }), _jsx(TooltipContent, { children: label })] }));
|
|
10
20
|
}
|
|
@@ -15,6 +25,7 @@ function ActionButton({ label, onClick, pressed, children, }) {
|
|
|
15
25
|
*/
|
|
16
26
|
function MessageActions({ text, onRetry, rating, onRate, meta, className, }) {
|
|
17
27
|
const { state, copy } = useCopy();
|
|
18
|
-
|
|
28
|
+
const t = useStrings("messageActions", messageActionsStrings);
|
|
29
|
+
return (_jsxs("div", { "data-slot": "message-actions", className: cn("flex items-center gap-0.5", className), children: [text !== undefined && (_jsx(ActionButton, { label: state === "copied" ? t("copied") : state === "failed" ? t("copyFailed") : t("copy"), onClick: () => copy(text), children: state === "copied" ? _jsx(Check, { className: "text-success-text" }) : state === "failed" ? _jsx(X, { className: "text-destructive-text" }) : _jsx(Copy, {}) })), onRetry && (_jsx(ActionButton, { label: t("retry"), onClick: onRetry, children: _jsx(RotateCw, {}) })), onRate && (_jsxs(_Fragment, { children: [_jsx(ActionButton, { label: t("good"), pressed: rating === "up", onClick: () => onRate(rating === "up" ? null : "up"), children: _jsx(ThumbsUp, { className: cn(rating === "up" && "fill-current") }) }), _jsx(ActionButton, { label: t("bad"), pressed: rating === "down", onClick: () => onRate(rating === "down" ? null : "down"), children: _jsx(ThumbsDown, { className: cn(rating === "down" && "fill-current") }) })] })), meta && _jsx("span", { className: "ms-1.5 truncate text-xs text-faint-foreground", children: meta })] }));
|
|
19
30
|
}
|
|
20
31
|
export { MessageActions };
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import type { EntityRef, InlineNode, MessageBlock, MessageSource } from "../lib/chat.js";
|
|
2
|
+
import type { Attachment, EntityRef, InlineNode, MessageBlock, MessageSource } from "../lib/chat.js";
|
|
3
|
+
/** MessageContent's fixed words; override them under `messageContent` in LocaleProvider's `strings`. */
|
|
4
|
+
export declare const messageContentStrings: {
|
|
5
|
+
measured: string;
|
|
6
|
+
measuredFrom: string;
|
|
7
|
+
notVerified: string;
|
|
8
|
+
provisional: string;
|
|
9
|
+
};
|
|
3
10
|
/**
|
|
4
11
|
* Inline nodes. Every string here may come from a model or the user's system, so it is
|
|
5
12
|
* only ever rendered as a React text child: no markdown parsing, no HTML.
|
|
@@ -9,8 +16,8 @@ declare function Inline({ nodes, }: {
|
|
|
9
16
|
nodes: InlineNode[];
|
|
10
17
|
}): React.JSX.Element;
|
|
11
18
|
/** A message's blocks, in order. */
|
|
12
|
-
declare function MessageContent({ blocks, sources, onOpenEntity, onNavigate, className, }: {
|
|
13
|
-
/** The reply as typed blocks: paragraphs, lists, payloads, tool calls, cards. */
|
|
19
|
+
declare function MessageContent({ blocks, sources, onOpenEntity, onNavigate, onOpenAttachment, className, }: {
|
|
20
|
+
/** The reply as typed blocks: paragraphs, lists, payloads, tool calls, cards, attachments. */
|
|
14
21
|
blocks: MessageBlock[];
|
|
15
22
|
/** Sources the blocks cite by marker. */
|
|
16
23
|
sources?: MessageSource[];
|
|
@@ -18,6 +25,8 @@ declare function MessageContent({ blocks, sources, onOpenEntity, onNavigate, cla
|
|
|
18
25
|
onOpenEntity?: (entity: EntityRef) => void;
|
|
19
26
|
/** Follows in-app links; links from a model are never rendered. */
|
|
20
27
|
onNavigate?: (to: string) => void;
|
|
28
|
+
/** Makes attachment tiles buttons that open their file. */
|
|
29
|
+
onOpenAttachment?: (attachment: Attachment) => void;
|
|
21
30
|
className?: string;
|
|
22
31
|
}): React.JSX.Element;
|
|
23
32
|
/** Plain text of a message, for copy. Citations become [n]; entities their API name. */
|
|
@@ -9,7 +9,16 @@ import { Citation } from "./citation.js";
|
|
|
9
9
|
import { EntityChip } from "./entity-chip.js";
|
|
10
10
|
import { Card } from "./card-registry.js";
|
|
11
11
|
import { Payload } from "./payload.js";
|
|
12
|
+
import { AttachmentList } from "./attachment-preview.js";
|
|
12
13
|
import { ToolCallGroup } from "./tool-call.js";
|
|
14
|
+
import { useStrings } from "../blocks/locale.js";
|
|
15
|
+
/** MessageContent's fixed words; override them under `messageContent` in LocaleProvider's `strings`. */
|
|
16
|
+
export const messageContentStrings = {
|
|
17
|
+
measured: "From the {environment}",
|
|
18
|
+
measuredFrom: "From the {environment}: {source}",
|
|
19
|
+
notVerified: " (not verified)",
|
|
20
|
+
provisional: "The agent hasn’t verified this against the {environment}.",
|
|
21
|
+
};
|
|
13
22
|
const ContentContext = React.createContext({ sources: new Map() });
|
|
14
23
|
/**
|
|
15
24
|
* Inline nodes. Every string here may come from a model or the user's system, so it is
|
|
@@ -17,6 +26,7 @@ const ContentContext = React.createContext({ sources: new Map() });
|
|
|
17
26
|
*/
|
|
18
27
|
function Inline({ nodes, }) {
|
|
19
28
|
const terms = useTerms();
|
|
29
|
+
const t = useStrings("messageContent", messageContentStrings);
|
|
20
30
|
const ctx = React.useContext(ContentContext);
|
|
21
31
|
return (_jsx(_Fragment, { children: nodes.map((n, i) => {
|
|
22
32
|
switch (n.kind) {
|
|
@@ -28,10 +38,11 @@ function Inline({ nodes, }) {
|
|
|
28
38
|
return _jsx("em", { children: n.text }, i);
|
|
29
39
|
case "code":
|
|
30
40
|
return (_jsx("code", { className: "rounded-xs bg-muted px-1 py-px font-mono text-[0.88em]", children: n.text }, i));
|
|
41
|
+
case "measured":
|
|
31
42
|
case "orgData":
|
|
32
|
-
return (_jsx("span", { title: n.source ?
|
|
43
|
+
return (_jsx("span", { title: n.source ? t("measuredFrom", { environment: terms.environment, source: n.source }) : t("measured", { environment: terms.environment }), className: "font-medium tabular-nums text-foreground", children: n.text }, i));
|
|
33
44
|
case "provisional":
|
|
34
|
-
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("button", { type: "button", "data-provisional": true, className: "inline rounded-xs text-
|
|
45
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("button", { type: "button", "data-provisional": true, className: "inline rounded-xs text-start text-provisional underline decoration-provisional/50 decoration-dotted underline-offset-[3px] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40", children: [_jsx(Inline, { nodes: n.nodes }), _jsx("span", { className: "sr-only", children: t("notVerified") })] }) }), _jsx(TooltipContent, { children: n.reason ?? t("provisional", { environment: terms.environment }) })] }, i));
|
|
35
46
|
case "citation":
|
|
36
47
|
return _jsx(Citation, { source: ctx.sources.get(n.sourceId) }, i);
|
|
37
48
|
case "entity":
|
|
@@ -46,13 +57,26 @@ function Inline({ nodes, }) {
|
|
|
46
57
|
}
|
|
47
58
|
}) }));
|
|
48
59
|
}
|
|
60
|
+
function AttachmentBlock({ files }) {
|
|
61
|
+
const ctx = React.useContext(ContentContext);
|
|
62
|
+
return _jsx(AttachmentList, { attachments: files, onOpen: ctx.onOpenAttachment });
|
|
63
|
+
}
|
|
49
64
|
function Block({ block, }) {
|
|
50
65
|
switch (block.kind) {
|
|
51
66
|
case "paragraph":
|
|
52
67
|
return (_jsx("p", { children: _jsx(Inline, { nodes: block.nodes }) }));
|
|
68
|
+
case "plain":
|
|
69
|
+
return _jsx("p", { className: "whitespace-pre-wrap break-words", children: block.text });
|
|
70
|
+
case "heading": {
|
|
71
|
+
// Levels are relative to the message: the thread page owns h1 and h2.
|
|
72
|
+
// Blocks can arrive as JSON, so an out-of-range level is clamped rather than trusted.
|
|
73
|
+
const level = Math.min(Math.max(Math.round(block.level) || 1, 1), 3);
|
|
74
|
+
const H = ["h3", "h4", "h5"][level - 1];
|
|
75
|
+
return (_jsx(H, { className: cn("font-semibold tracking-tight text-foreground", level === 1 ? "mt-1 text-lg" : level === 2 ? "mt-1 text-base" : "text-sm"), children: _jsx(Inline, { nodes: block.nodes }) }));
|
|
76
|
+
}
|
|
53
77
|
case "list": {
|
|
54
78
|
const List = block.ordered ? "ol" : "ul";
|
|
55
|
-
return (_jsx(List, { className: cn("flex flex-col gap-1
|
|
79
|
+
return (_jsx(List, { className: cn("flex flex-col gap-1 ps-5", block.ordered ? "list-decimal" : "list-disc marker:text-faint-foreground"), children: block.items.map((item, i) => (_jsx("li", { className: "ps-0.5", children: _jsx(Inline, { nodes: item }) }, i))) }));
|
|
56
80
|
}
|
|
57
81
|
case "payload":
|
|
58
82
|
return _jsx(Payload, { language: block.language, text: block.text, title: block.title });
|
|
@@ -62,12 +86,14 @@ function Block({ block, }) {
|
|
|
62
86
|
return _jsx(DegradedPanel, { reason: block.reason, missing: block.missing });
|
|
63
87
|
case "card":
|
|
64
88
|
return _jsx(Card, { name: block.card, props: block.props, summary: block.summary });
|
|
89
|
+
case "attachments":
|
|
90
|
+
return _jsx(AttachmentBlock, { files: block.files });
|
|
65
91
|
}
|
|
66
92
|
}
|
|
67
93
|
/** A message's blocks, in order. */
|
|
68
|
-
function MessageContent({ blocks, sources = [], onOpenEntity, onNavigate, className, }) {
|
|
69
|
-
const ctx = React.useMemo(() => ({ sources: new Map(sources.map((s) => [s.id, s])), onOpenEntity, onNavigate }), [sources, onOpenEntity, onNavigate]);
|
|
70
|
-
return (_jsx(ContentContext.Provider, { value: ctx, children: _jsx("div", { "data-slot": "message-content", className: cn("flex flex-col gap-3 text-
|
|
94
|
+
function MessageContent({ blocks, sources = [], onOpenEntity, onNavigate, onOpenAttachment, className, }) {
|
|
95
|
+
const ctx = React.useMemo(() => ({ sources: new Map(sources.map((s) => [s.id, s])), onOpenEntity, onNavigate, onOpenAttachment }), [sources, onOpenEntity, onNavigate, onOpenAttachment]);
|
|
96
|
+
return (_jsx(ContentContext.Provider, { value: ctx, children: _jsx("div", { "data-slot": "message-content", className: cn("flex flex-col gap-3 text-prose leading-relaxed", className), children: blocks.map((b, i) => (_jsx(Block, { block: b }, i))) }) }));
|
|
71
97
|
}
|
|
72
98
|
/** Plain text of a message, for copy. Citations become [n]; entities their API name. */
|
|
73
99
|
export function blocksToText(blocks, sources = []) {
|
|
@@ -90,7 +116,10 @@ export function blocksToText(blocks, sources = []) {
|
|
|
90
116
|
.map((b) => {
|
|
91
117
|
switch (b.kind) {
|
|
92
118
|
case "paragraph":
|
|
119
|
+
case "heading":
|
|
93
120
|
return inline(b.nodes);
|
|
121
|
+
case "plain":
|
|
122
|
+
return b.text;
|
|
94
123
|
case "list":
|
|
95
124
|
return b.items.map((it, i) => `${b.ordered ? `${i + 1}.` : "-"} ${inline(it)}`).join("\n");
|
|
96
125
|
case "payload":
|
|
@@ -99,6 +128,8 @@ export function blocksToText(blocks, sources = []) {
|
|
|
99
128
|
return b.reason;
|
|
100
129
|
case "card":
|
|
101
130
|
return b.summary;
|
|
131
|
+
case "attachments":
|
|
132
|
+
return b.files.map((f) => f.name).join("\n");
|
|
102
133
|
case "tools":
|
|
103
134
|
return "";
|
|
104
135
|
}
|
package/dist/chat/message.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import type { Attachment, EntityRef, Message as MessageData } from "../lib/chat.js";
|
|
2
3
|
import { type Rating } from "./message-actions.js";
|
|
4
|
+
/** Message's fixed words; override them under `message` in LocaleProvider's `strings`. */
|
|
5
|
+
export declare const messageStrings: {
|
|
6
|
+
you: string;
|
|
7
|
+
personLabel: string;
|
|
8
|
+
agentLabel: string;
|
|
9
|
+
interrupted: string;
|
|
10
|
+
continue: string;
|
|
11
|
+
failed: string;
|
|
12
|
+
failedMissing: string;
|
|
13
|
+
checkedBy: string;
|
|
14
|
+
editedBy: string;
|
|
15
|
+
};
|
|
3
16
|
/**
|
|
4
17
|
* One message in a thread.
|
|
5
18
|
*
|
|
@@ -13,13 +26,15 @@ import { type Rating } from "./message-actions.js";
|
|
|
13
26
|
* - A reply a person checked (or edited) before it went out can say so in a
|
|
14
27
|
* caption under it, so a reader knows a person stood behind it.
|
|
15
28
|
*/
|
|
16
|
-
declare function Message({ message, onOpenEntity, onNavigate, onRetry, onContinue, rating, onRate, reviewedBy, className, }: {
|
|
29
|
+
declare function Message({ message, onOpenEntity, onNavigate, onOpenAttachment, onRetry, onContinue, rating, onRate, reviewedBy, actions, className, }: {
|
|
17
30
|
/** The message: author, status, blocks, and optional reasoning, sources and attribution. */
|
|
18
31
|
message: MessageData;
|
|
19
32
|
/** Makes entity chips open their thing. */
|
|
20
33
|
onOpenEntity?: (entity: EntityRef) => void;
|
|
21
34
|
/** Follows in-app links. */
|
|
22
35
|
onNavigate?: (to: string) => void;
|
|
36
|
+
/** Makes attachment tiles buttons that open their file. */
|
|
37
|
+
onOpenAttachment?: (attachment: Attachment) => void;
|
|
23
38
|
/** Shows Retry on a failed or finished reply. */
|
|
24
39
|
onRetry?: () => void;
|
|
25
40
|
/** Shows Continue on an interrupted reply. */
|
|
@@ -34,6 +49,68 @@ declare function Message({ message, onOpenEntity, onNavigate, onRetry, onContinu
|
|
|
34
49
|
at?: string;
|
|
35
50
|
edited?: boolean;
|
|
36
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* The row under an agent reply (copy, retry, rating). `false` hides it; a node replaces it, e.g. your own buttons.
|
|
54
|
+
* Model and cost still show, on their own line. To rebuild a message, use MessageHeader, MessageBody and
|
|
55
|
+
* MessageFooter.
|
|
56
|
+
*/
|
|
57
|
+
actions?: false | React.ReactNode;
|
|
37
58
|
className?: string;
|
|
38
|
-
}):
|
|
39
|
-
|
|
59
|
+
}): React.JSX.Element;
|
|
60
|
+
/** The row above a reply: who sent it, a detail (their specialty or team), and when. */
|
|
61
|
+
declare function MessageHeader({ name, detail, createdAt, children, className, }: {
|
|
62
|
+
/** The sender's displayed name. */
|
|
63
|
+
name: string;
|
|
64
|
+
/** A quieter detail after the name: an agent's specialty, a person's team. */
|
|
65
|
+
detail?: string;
|
|
66
|
+
/** When it was sent; shown as a relative time. */
|
|
67
|
+
createdAt?: string;
|
|
68
|
+
/** Anything else for the row, after the time (a badge, a menu). */
|
|
69
|
+
children?: React.ReactNode;
|
|
70
|
+
className?: string;
|
|
71
|
+
}): React.JSX.Element;
|
|
72
|
+
/** A reply's content: its reasoning, then its blocks, with a caret after the last one while it streams. */
|
|
73
|
+
declare function MessageBody({ message, onOpenEntity, onNavigate, onOpenAttachment, className, }: {
|
|
74
|
+
/** The message whose reasoning and blocks to show. */
|
|
75
|
+
message: MessageData;
|
|
76
|
+
/** Makes entity chips open their thing. */
|
|
77
|
+
onOpenEntity?: (entity: EntityRef) => void;
|
|
78
|
+
/** Follows in-app links. */
|
|
79
|
+
onNavigate?: (to: string) => void;
|
|
80
|
+
/** Makes attachment tiles buttons that open their file. */
|
|
81
|
+
onOpenAttachment?: (attachment: Attachment) => void;
|
|
82
|
+
className?: string;
|
|
83
|
+
}): React.JSX.Element;
|
|
84
|
+
/** Model and cost, on a line of their own: what the actions row shows, for when it's hidden or replaced. */
|
|
85
|
+
declare function MessageAttribution({ attribution, className, }: {
|
|
86
|
+
/** The service's measured values; nothing shows when both are absent. */
|
|
87
|
+
attribution?: MessageData["attribution"];
|
|
88
|
+
className?: string;
|
|
89
|
+
}): React.JSX.Element | null;
|
|
90
|
+
/**
|
|
91
|
+
* What sits under a reply: that it stopped or failed, its sources, who checked it, then the actions row with model
|
|
92
|
+
* and cost. `actions={false}` hides the row and a node replaces it; either way the model and cost stay, on their own
|
|
93
|
+
* line, because a reader should always see what produced the answer.
|
|
94
|
+
*/
|
|
95
|
+
declare function MessageFooter({ message, onRetry, onContinue, rating, onRate, reviewedBy, actions, className, }: {
|
|
96
|
+
/** The message the footer belongs to. */
|
|
97
|
+
message: MessageData;
|
|
98
|
+
/** Shows Retry on a failed or finished reply. */
|
|
99
|
+
onRetry?: () => void;
|
|
100
|
+
/** Shows Continue on an interrupted reply. */
|
|
101
|
+
onContinue?: () => void;
|
|
102
|
+
/** The current rating. */
|
|
103
|
+
rating?: Rating;
|
|
104
|
+
/** Shows thumbs up and down. */
|
|
105
|
+
onRate?: (rating: Rating) => void;
|
|
106
|
+
/** A person who checked the reply before it was sent. */
|
|
107
|
+
reviewedBy?: {
|
|
108
|
+
name: string;
|
|
109
|
+
at?: string;
|
|
110
|
+
edited?: boolean;
|
|
111
|
+
};
|
|
112
|
+
/** `false` hides the actions row, a node replaces it; model and cost still show. */
|
|
113
|
+
actions?: false | React.ReactNode;
|
|
114
|
+
className?: string;
|
|
115
|
+
}): React.JSX.Element;
|
|
116
|
+
export { Message, MessageHeader, MessageBody, MessageFooter, MessageAttribution };
|
package/dist/chat/message.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx,
|
|
3
|
-
import { CircleStop,
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { CircleStop, UserCheck, UserRound } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
5
|
import { DegradedPanel } from "../blocks/degraded-panel.js";
|
|
6
6
|
import { RelativeTime } from "../blocks/relative-time.js";
|
|
@@ -9,6 +9,20 @@ import { SourceList } from "./citation.js";
|
|
|
9
9
|
import { blocksToText, MessageContent } from "./message-content.js";
|
|
10
10
|
import { MessageActions } from "./message-actions.js";
|
|
11
11
|
import { Reasoning } from "./reasoning.js";
|
|
12
|
+
import { ThreadEvent } from "./thread-event.js";
|
|
13
|
+
import { useStrings } from "../blocks/locale.js";
|
|
14
|
+
/** Message's fixed words; override them under `message` in LocaleProvider's `strings`. */
|
|
15
|
+
export const messageStrings = {
|
|
16
|
+
you: "You",
|
|
17
|
+
personLabel: "{name}, {team}",
|
|
18
|
+
agentLabel: "{name}, {noun} agent",
|
|
19
|
+
interrupted: "Stopped before it finished. What arrived is kept.",
|
|
20
|
+
continue: "Continue",
|
|
21
|
+
failed: "The agent couldn’t finish this reply.",
|
|
22
|
+
failedMissing: "The rest of the answer",
|
|
23
|
+
checkedBy: "Checked by {name} before sending",
|
|
24
|
+
editedBy: "Edited by {name} before sending",
|
|
25
|
+
};
|
|
12
26
|
/**
|
|
13
27
|
* One message in a thread.
|
|
14
28
|
*
|
|
@@ -22,23 +36,50 @@ import { Reasoning } from "./reasoning.js";
|
|
|
22
36
|
* - A reply a person checked (or edited) before it went out can say so in a
|
|
23
37
|
* caption under it, so a reader knows a person stood behind it.
|
|
24
38
|
*/
|
|
25
|
-
function Message({ message, onOpenEntity, onNavigate, onRetry, onContinue, rating, onRate, reviewedBy, className, }) {
|
|
39
|
+
function Message({ message, onOpenEntity, onNavigate, onOpenAttachment, onRetry, onContinue, rating, onRate, reviewedBy, actions, className, }) {
|
|
26
40
|
const { author } = message;
|
|
27
41
|
const roleOf = useAgentRoles();
|
|
42
|
+
const t = useStrings("message", messageStrings);
|
|
28
43
|
if (author.role === "system") {
|
|
29
|
-
return (
|
|
44
|
+
return (_jsx(ThreadEvent, { "data-slot": "message", "data-role": "system", className: className, children: _jsx(MessageContent, { blocks: message.blocks, className: "text-xs leading-normal" }) }));
|
|
30
45
|
}
|
|
31
46
|
if (author.role === "user") {
|
|
32
|
-
return (_jsx("div", { "data-slot": "message", "data-role": "user", className: cn("flex justify-end
|
|
47
|
+
return (_jsx("div", { "data-slot": "message", "data-role": "user", className: cn("flex justify-end ps-10 sm:ps-24 animate-rise-in", className), children: _jsxs("div", { className: "flex max-w-full flex-col items-end gap-1", children: [_jsx("div", { className: "rounded-xl rounded-ee-sm bg-surface-user px-3.5 py-2.5", children: _jsx(MessageContent, { blocks: message.blocks, onOpenEntity: onOpenEntity, onNavigate: onNavigate, onOpenAttachment: onOpenAttachment, className: "gap-2" }) }), _jsxs("span", { className: "px-1 text-2xs text-faint-foreground", children: [author.name ?? t("you"), message.createdAt && (_jsxs(_Fragment, { children: [" · ", _jsx(RelativeTime, { date: message.createdAt })] }))] })] }) }));
|
|
33
48
|
}
|
|
34
49
|
if (author.role === "person") {
|
|
35
|
-
return (_jsxs("article", { "data-slot": "message", "data-role": "person", "aria-label": author.team ?
|
|
50
|
+
return (_jsxs("article", { "data-slot": "message", "data-role": "person", "aria-label": author.team ? t("personLabel", { name: author.name, team: author.team }) : author.name, className: cn("flex gap-3 animate-rise-in", className), children: [_jsx("span", { "aria-hidden": true, className: "mt-px flex size-7 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground", children: _jsx(UserRound, { className: "size-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(MessageHeader, { name: author.name, detail: author.team, createdAt: message.createdAt }), _jsx(MessageContent, { blocks: message.blocks, sources: message.sources, onOpenEntity: onOpenEntity, onNavigate: onNavigate, onOpenAttachment: onOpenAttachment, className: "mt-1" })] })] }));
|
|
36
51
|
}
|
|
37
52
|
const { agent } = author;
|
|
53
|
+
const streaming = message.status === "streaming";
|
|
54
|
+
return (_jsxs("article", { "data-slot": "message", "data-role": "agent", "data-status": message.status, "aria-busy": streaming, "aria-label": t("agentLabel", { name: agent.name, noun: roleOf(agent.role).noun }), className: cn("group/message flex gap-3 animate-rise-in", className), children: [_jsx(AgentAvatar, { agent: agent, className: "mt-px" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(MessageHeader, { name: agent.name, detail: agent.specialty, createdAt: message.createdAt }), _jsxs("div", { className: "mt-1 flex flex-col gap-3", children: [_jsx(MessageBody, { message: message, onOpenEntity: onOpenEntity, onNavigate: onNavigate, onOpenAttachment: onOpenAttachment }), _jsx(MessageFooter, { message: message, onRetry: onRetry, onContinue: onContinue, rating: rating, onRate: onRate, reviewedBy: reviewedBy, actions: actions })] })] })] }));
|
|
55
|
+
}
|
|
56
|
+
/** The row above a reply: who sent it, a detail (their specialty or team), and when. */
|
|
57
|
+
function MessageHeader({ name, detail, createdAt, children, className, }) {
|
|
58
|
+
return (_jsxs("header", { "data-slot": "message-header", className: cn("flex h-7 items-center gap-2", className), children: [_jsx("span", { className: "text-sm font-semibold", children: name }), detail && _jsx("span", { className: "truncate text-xs text-muted-foreground", children: detail }), createdAt && (_jsx("span", { className: "text-xs text-faint-foreground", children: _jsx(RelativeTime, { date: createdAt }) })), children] }));
|
|
59
|
+
}
|
|
60
|
+
/** A reply's content: its reasoning, then its blocks, with a caret after the last one while it streams. */
|
|
61
|
+
function MessageBody({ message, onOpenEntity, onNavigate, onOpenAttachment, className, }) {
|
|
38
62
|
const streaming = message.status === "streaming";
|
|
39
63
|
const thinking = streaming && message.blocks.length === 0;
|
|
40
|
-
|
|
64
|
+
return (_jsxs("div", { "data-slot": "message-body", className: cn("flex flex-col gap-3", className), children: [message.reasoning && message.reasoning.length > 0 && _jsx(Reasoning, { entries: message.reasoning, thinking: thinking, thoughtFor: message.thoughtFor }), message.blocks.length > 0 && (_jsxs("div", { className: "relative", children: [_jsx(MessageContent, { blocks: message.blocks, sources: message.sources, onOpenEntity: onOpenEntity, onNavigate: onNavigate, onOpenAttachment: onOpenAttachment }), streaming && _jsx("span", { "aria-hidden": true, className: "ms-0.5 inline-block h-[1.05em] w-[2px] translate-y-[0.18em] rounded-full bg-primary animate-caret" })] }))] }));
|
|
65
|
+
}
|
|
66
|
+
/** Model and cost, on a line of their own: what the actions row shows, for when it's hidden or replaced. */
|
|
67
|
+
function MessageAttribution({ attribution, className, }) {
|
|
68
|
+
const meta = [attribution?.model, attribution?.cost].filter(Boolean).join(" · ");
|
|
69
|
+
if (!meta)
|
|
70
|
+
return null;
|
|
71
|
+
return (_jsx("p", { "data-slot": "message-attribution", className: cn("text-xs text-faint-foreground", className), children: meta }));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* What sits under a reply: that it stopped or failed, its sources, who checked it, then the actions row with model
|
|
75
|
+
* and cost. `actions={false}` hides the row and a node replaces it; either way the model and cost stay, on their own
|
|
76
|
+
* line, because a reader should always see what produced the answer.
|
|
77
|
+
*/
|
|
78
|
+
function MessageFooter({ message, onRetry, onContinue, rating, onRate, reviewedBy, actions, className, }) {
|
|
79
|
+
const t = useStrings("message", messageStrings);
|
|
80
|
+
const streaming = message.status === "streaming";
|
|
81
|
+
const settled = !streaming && message.status !== "failed";
|
|
41
82
|
const meta = [message.attribution?.model, message.attribution?.cost].filter(Boolean).join(" · ");
|
|
42
|
-
return (_jsxs("
|
|
83
|
+
return (_jsxs("div", { "data-slot": "message-footer", className: cn("flex flex-col gap-3 empty:hidden", className), children: [message.status === "interrupted" && (_jsxs("div", { className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground", children: [_jsx(CircleStop, { "aria-hidden": true, className: "size-3.5" }), t("interrupted"), onContinue && (_jsx("button", { type: "button", onClick: onContinue, className: "font-medium text-accent-text hover:underline", children: t("continue") }))] })), message.status === "failed" && _jsx(DegradedPanel, { reason: t("failed"), missing: t("failedMissing"), onRetry: onRetry }), !streaming && message.sources && message.sources.length > 0 && _jsx(SourceList, { sources: message.sources }), reviewedBy && (_jsxs("p", { "data-slot": "message-reviewed", className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [_jsx(UserCheck, { "aria-hidden": true, className: "size-3.5" }), t(reviewedBy.edited ? "editedBy" : "checkedBy", { name: reviewedBy.name }), reviewedBy.at && (_jsxs(_Fragment, { children: [" · ", _jsx(RelativeTime, { date: reviewedBy.at })] }))] })), settled && actions === undefined && (_jsx(MessageActions, { text: blocksToText(message.blocks, message.sources), onRetry: onRetry, rating: rating, onRate: onRate, meta: meta || undefined, className: "-ms-1.5 opacity-100 transition-opacity duration-150 sm:opacity-0 sm:group-hover/message:opacity-100 sm:group-focus-within/message:opacity-100" })), settled && actions !== undefined && actions !== false && actions, settled && actions !== undefined && _jsx(MessageAttribution, { attribution: message.attribution })] }));
|
|
43
84
|
}
|
|
44
|
-
export { Message };
|
|
85
|
+
export { Message, MessageHeader, MessageBody, MessageFooter, MessageAttribution };
|