@candor-ui/react 1.0.1 → 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/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 +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 +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 +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/styles.css +40 -1
- package/dist/ui/button.d.ts +1 -1
- package/package.json +1 -1
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) {
|
|
@@ -30,9 +40,9 @@ function Inline({ nodes, }) {
|
|
|
30
40
|
return (_jsx("code", { className: "rounded-xs bg-muted px-1 py-px font-mono text-[0.88em]", children: n.text }, i));
|
|
31
41
|
case "measured":
|
|
32
42
|
case "orgData":
|
|
33
|
-
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));
|
|
34
44
|
case "provisional":
|
|
35
|
-
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));
|
|
36
46
|
case "citation":
|
|
37
47
|
return _jsx(Citation, { source: ctx.sources.get(n.sourceId) }, i);
|
|
38
48
|
case "entity":
|
|
@@ -47,6 +57,10 @@ function Inline({ nodes, }) {
|
|
|
47
57
|
}
|
|
48
58
|
}) }));
|
|
49
59
|
}
|
|
60
|
+
function AttachmentBlock({ files }) {
|
|
61
|
+
const ctx = React.useContext(ContentContext);
|
|
62
|
+
return _jsx(AttachmentList, { attachments: files, onOpen: ctx.onOpenAttachment });
|
|
63
|
+
}
|
|
50
64
|
function Block({ block, }) {
|
|
51
65
|
switch (block.kind) {
|
|
52
66
|
case "paragraph":
|
|
@@ -62,7 +76,7 @@ function Block({ block, }) {
|
|
|
62
76
|
}
|
|
63
77
|
case "list": {
|
|
64
78
|
const List = block.ordered ? "ol" : "ul";
|
|
65
|
-
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))) }));
|
|
66
80
|
}
|
|
67
81
|
case "payload":
|
|
68
82
|
return _jsx(Payload, { language: block.language, text: block.text, title: block.title });
|
|
@@ -72,12 +86,14 @@ function Block({ block, }) {
|
|
|
72
86
|
return _jsx(DegradedPanel, { reason: block.reason, missing: block.missing });
|
|
73
87
|
case "card":
|
|
74
88
|
return _jsx(Card, { name: block.card, props: block.props, summary: block.summary });
|
|
89
|
+
case "attachments":
|
|
90
|
+
return _jsx(AttachmentBlock, { files: block.files });
|
|
75
91
|
}
|
|
76
92
|
}
|
|
77
93
|
/** A message's blocks, in order. */
|
|
78
|
-
function MessageContent({ blocks, sources = [], onOpenEntity, onNavigate, className, }) {
|
|
79
|
-
const ctx = React.useMemo(() => ({ sources: new Map(sources.map((s) => [s.id, s])), onOpenEntity, onNavigate }), [sources, onOpenEntity, onNavigate]);
|
|
80
|
-
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))) }) }));
|
|
81
97
|
}
|
|
82
98
|
/** Plain text of a message, for copy. Citations become [n]; entities their API name. */
|
|
83
99
|
export function blocksToText(blocks, sources = []) {
|
|
@@ -112,6 +128,8 @@ export function blocksToText(blocks, sources = []) {
|
|
|
112
128
|
return b.reason;
|
|
113
129
|
case "card":
|
|
114
130
|
return b.summary;
|
|
131
|
+
case "attachments":
|
|
132
|
+
return b.files.map((f) => f.name).join("\n");
|
|
115
133
|
case "tools":
|
|
116
134
|
return "";
|
|
117
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 };
|
package/dist/chat/payload.d.ts
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
|
+
import * as React from "react";
|
|
1
2
|
import type { PayloadLanguage } from "../lib/chat.js";
|
|
3
|
+
/** Payload's fixed words; override them under `payload` in LocaleProvider's `strings`. */
|
|
4
|
+
export declare const payloadStrings: {
|
|
5
|
+
copy: string;
|
|
6
|
+
copied: string;
|
|
7
|
+
copyFailed: string;
|
|
8
|
+
shell: string;
|
|
9
|
+
text: string;
|
|
10
|
+
};
|
|
2
11
|
/**
|
|
3
12
|
* A block of code or query text from the agent: a query it ran, code it
|
|
4
13
|
* wrote, a config file. Rendered as text in a sunken well, with its language and a copy
|
|
5
|
-
* control that only says "Copied" once the clipboard took it.
|
|
14
|
+
* control that only says "Copied" once the clipboard took it. `bare` drops the frame and
|
|
15
|
+
* the caption, for a card or panel that already has its own.
|
|
16
|
+
*
|
|
17
|
+
* Code is split into tokens (keywords, strings, comments…) rendered as spans of text, never as HTML. They're coloured
|
|
18
|
+
* when `highlight` is set, or anywhere under `data-syntax="highlight"` (set it on `<html>` for the whole app).
|
|
6
19
|
*/
|
|
7
|
-
declare function Payload({ language, text, title, className, }: {
|
|
20
|
+
declare function Payload({ language, text, title, bare, highlight, className, }: {
|
|
8
21
|
/** Shown as the label, e.g. json, sql, python. Never executed or highlighted as HTML. */
|
|
9
22
|
language: PayloadLanguage;
|
|
10
23
|
/** The code or query, shown as text. */
|
|
11
24
|
text: string;
|
|
12
|
-
/** What it is, e.g. "The query I ran". */
|
|
25
|
+
/** What it is, e.g. "The query I ran". Not shown when `bare`. */
|
|
13
26
|
title?: string;
|
|
27
|
+
/** Just the code: no border, language label or copy button, for inside something that has them. */
|
|
28
|
+
bare?: boolean;
|
|
29
|
+
/** Colour the code by its syntax. Left out, it follows the nearest `data-syntax` attribute (off by default). */
|
|
30
|
+
highlight?: boolean;
|
|
14
31
|
className?: string;
|
|
15
|
-
}):
|
|
32
|
+
}): React.JSX.Element;
|
|
16
33
|
export { Payload };
|
package/dist/chat/payload.js
CHANGED
|
@@ -1,18 +1,51 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
3
4
|
import { Check, Copy, X } from "lucide-react";
|
|
4
5
|
import { cn } from "../lib/utils.js";
|
|
5
6
|
import { useCopy } from "../lib/use-copy.js";
|
|
6
|
-
|
|
7
|
+
import { tokenize } from "../lib/highlight.js";
|
|
8
|
+
import { useStrings } from "../blocks/locale.js";
|
|
9
|
+
/** Payload's fixed words; override them under `payload` in LocaleProvider's `strings`. */
|
|
10
|
+
export const payloadStrings = {
|
|
11
|
+
copy: "Copy",
|
|
12
|
+
copied: "Copied",
|
|
13
|
+
copyFailed: "Couldn’t copy",
|
|
14
|
+
shell: "Shell",
|
|
15
|
+
text: "Text",
|
|
16
|
+
};
|
|
17
|
+
// Language names are the same in every language; only the generic ones ("Shell", "Text") come from the strings table.
|
|
18
|
+
const knownLabels = { json: "JSON", xml: "XML", sql: "SQL", yaml: "YAML", html: "HTML", css: "CSS", js: "JavaScript", ts: "TypeScript", tsx: "TSX", python: "Python" };
|
|
19
|
+
const translatedLabels = { bash: "shell", sh: "shell", text: "text" };
|
|
7
20
|
/** "json" → "JSON", "python" → "Python", short unknown ones upper-cased ("hcl" → "HCL"). */
|
|
8
|
-
const languageLabel = (lang) =>
|
|
21
|
+
const languageLabel = (lang, t) => {
|
|
22
|
+
const key = translatedLabels[lang];
|
|
23
|
+
if (key)
|
|
24
|
+
return t(key);
|
|
25
|
+
return knownLabels[lang] ?? (lang.length <= 4 ? lang.toUpperCase() : lang.charAt(0).toUpperCase() + lang.slice(1));
|
|
26
|
+
};
|
|
9
27
|
/**
|
|
10
28
|
* A block of code or query text from the agent: a query it ran, code it
|
|
11
29
|
* wrote, a config file. Rendered as text in a sunken well, with its language and a copy
|
|
12
|
-
* control that only says "Copied" once the clipboard took it.
|
|
30
|
+
* control that only says "Copied" once the clipboard took it. `bare` drops the frame and
|
|
31
|
+
* the caption, for a card or panel that already has its own.
|
|
32
|
+
*
|
|
33
|
+
* Code is split into tokens (keywords, strings, comments…) rendered as spans of text, never as HTML. They're coloured
|
|
34
|
+
* when `highlight` is set, or anywhere under `data-syntax="highlight"` (set it on `<html>` for the whole app).
|
|
13
35
|
*/
|
|
14
|
-
function Payload({ language, text, title, className, }) {
|
|
36
|
+
function Payload({ language, text, title, bare = false, highlight, className, }) {
|
|
15
37
|
const { state, copy } = useCopy();
|
|
16
|
-
|
|
38
|
+
const t = useStrings("payload", payloadStrings);
|
|
39
|
+
const syntax = highlight === undefined ? undefined : highlight ? "highlight" : "off";
|
|
40
|
+
const code = _jsx(Code, { text: text, language: language });
|
|
41
|
+
if (bare) {
|
|
42
|
+
return (_jsx("pre", { "data-slot": "payload", "data-bare": "", "data-language": language, "data-syntax": syntax, className: cn("max-h-80 overflow-auto rounded-md bg-sunken px-3 py-2.5 font-mono text-xs leading-relaxed text-foreground", className), children: code }));
|
|
43
|
+
}
|
|
44
|
+
return (_jsxs("figure", { "data-slot": "payload", "data-language": language, "data-syntax": syntax, className: cn("overflow-hidden rounded-lg border bg-sunken", className), children: [_jsxs("figcaption", { className: "flex items-center justify-between gap-2 border-b px-3 py-1.5", children: [_jsxs("span", { className: "truncate text-xs text-muted-foreground", children: [_jsx("span", { className: "font-medium text-foreground", children: languageLabel(language, t) }), title && _jsxs("span", { children: [" \u00B7 ", title] })] }), _jsx("button", { type: "button", onClick: () => copy(text), "aria-live": "polite", className: "inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs text-muted-foreground transition-colors duration-150 outline-none hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/40", children: state === "copied" ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "size-3 text-success-text" }), " ", t("copied")] })) : state === "failed" ? (_jsxs(_Fragment, { children: [_jsx(X, { className: "size-3 text-destructive-text" }), " ", t("copyFailed")] })) : (_jsxs(_Fragment, { children: [_jsx(Copy, { className: "size-3" }), " ", t("copy")] })) })] }), _jsx("pre", { className: "max-h-80 overflow-auto px-3 py-2.5 font-mono text-xs leading-relaxed text-foreground", children: code })] }));
|
|
45
|
+
}
|
|
46
|
+
/** The code as token spans. Each token is a text child, so nothing in it is parsed as markup. */
|
|
47
|
+
function Code({ text, language }) {
|
|
48
|
+
const tokens = React.useMemo(() => tokenize(text, language), [text, language]);
|
|
49
|
+
return (_jsx("code", { children: tokens.map((tok, i) => tok.type === "plain" ? (_jsx(React.Fragment, { children: tok.text }, i)) : (_jsx("span", { "data-token": tok.type, children: tok.text }, i))) }));
|
|
17
50
|
}
|
|
18
51
|
export { Payload };
|