@candor-ui/react 1.0.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +3 -2
- package/dist/blocks/copy-field.d.ts +6 -0
- package/dist/blocks/copy-field.js +9 -1
- package/dist/blocks/degraded-panel.d.ts +6 -0
- package/dist/blocks/degraded-panel.js +9 -1
- package/dist/blocks/locale.d.ts +86 -0
- package/dist/blocks/locale.js +146 -0
- package/dist/blocks/relative-time.d.ts +21 -3
- package/dist/blocks/relative-time.js +30 -11
- package/dist/blocks/status-dot.js +4 -2
- package/dist/blocks/step-list.js +4 -2
- package/dist/blocks/streaming-text.js +1 -1
- package/dist/blocks/surface.d.ts +2 -2
- package/dist/chat/agent-avatar.d.ts +14 -0
- package/dist/chat/agent-avatar.js +26 -2
- package/dist/chat/attachment-preview.d.ts +53 -0
- package/dist/chat/attachment-preview.js +82 -0
- package/dist/chat/card-registry.d.ts +7 -0
- package/dist/chat/card-registry.js +14 -4
- package/dist/chat/citation.d.ts +15 -0
- package/dist/chat/citation.js +34 -7
- package/dist/chat/composer.d.ts +30 -4
- package/dist/chat/composer.js +69 -20
- package/dist/chat/context-meter.d.ts +9 -0
- package/dist/chat/context-meter.js +21 -4
- package/dist/chat/entity-chip.d.ts +19 -0
- package/dist/chat/entity-chip.js +37 -3
- package/dist/chat/folded-turns.d.ts +33 -0
- package/dist/chat/folded-turns.js +29 -0
- package/dist/chat/message-actions.d.ts +9 -0
- package/dist/chat/message-actions.js +12 -1
- package/dist/chat/message-content.d.ts +12 -3
- package/dist/chat/message-content.js +24 -6
- package/dist/chat/message.d.ts +81 -4
- package/dist/chat/message.js +50 -9
- package/dist/chat/payload.d.ts +21 -4
- package/dist/chat/payload.js +38 -5
- package/dist/chat/prompt-suggestions.d.ts +6 -2
- package/dist/chat/prompt-suggestions.js +9 -2
- package/dist/chat/reasoning.d.ts +15 -0
- package/dist/chat/reasoning.js +12 -3
- package/dist/chat/thread-divider.d.ts +18 -0
- package/dist/chat/thread-divider.js +39 -0
- package/dist/chat/thread-event.d.ts +19 -0
- package/dist/chat/thread-event.js +15 -0
- package/dist/chat/thread.d.ts +12 -1
- package/dist/chat/thread.js +32 -4
- package/dist/chat/tool-call.d.ts +12 -0
- package/dist/chat/tool-call.js +16 -3
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/lib/chat.d.ts +11 -1
- package/dist/lib/highlight.d.ts +15 -0
- package/dist/lib/highlight.js +224 -0
- package/dist/lib/status.d.ts +15 -0
- package/dist/lib/status.js +10 -1
- package/dist/lib/use-virtual-list.d.ts +39 -0
- package/dist/lib/use-virtual-list.js +124 -0
- package/dist/styles.css +40 -1
- package/dist/ui/button.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import type { PromptSuggestion } from "../lib/chat.js";
|
|
2
|
+
/** PromptSuggestions' fixed words; override them under `promptSuggestions` in LocaleProvider's `strings`. */
|
|
3
|
+
export declare const promptSuggestionsStrings: {
|
|
4
|
+
label: string;
|
|
5
|
+
};
|
|
2
6
|
/**
|
|
3
7
|
* Prompts to start from. Choosing one puts it in the composer to edit, rather
|
|
4
8
|
* than sending it, because suggestions are model output too. Loading shows
|
|
5
9
|
* placeholders of the right shape; an empty list renders nothing.
|
|
6
10
|
*/
|
|
7
|
-
declare function PromptSuggestions({ suggestions, onChoose, label, loading, variant, className, }: {
|
|
11
|
+
declare function PromptSuggestions({ suggestions, onChoose, label: labelProp, loading, variant, className, }: {
|
|
8
12
|
/** What to offer. */
|
|
9
13
|
suggestions: PromptSuggestion[];
|
|
10
14
|
/** Called with the chosen one; put it in the composer to edit rather than sending it. */
|
|
11
15
|
onChoose: (suggestion: PromptSuggestion) => void;
|
|
12
|
-
/** The heading above the suggestions. */
|
|
16
|
+
/** The heading above the suggestions; "Try asking" by default. */
|
|
13
17
|
label?: string;
|
|
14
18
|
/** Shows placeholders while suggestions load. */
|
|
15
19
|
loading?: boolean;
|
|
@@ -3,16 +3,23 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { ArrowUpRight, Sparkles } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
5
|
import { Skeleton } from "../ui/skeleton.js";
|
|
6
|
+
import { useStrings } from "../blocks/locale.js";
|
|
7
|
+
/** PromptSuggestions' fixed words; override them under `promptSuggestions` in LocaleProvider's `strings`. */
|
|
8
|
+
export const promptSuggestionsStrings = {
|
|
9
|
+
label: "Try asking",
|
|
10
|
+
};
|
|
6
11
|
/**
|
|
7
12
|
* Prompts to start from. Choosing one puts it in the composer to edit, rather
|
|
8
13
|
* than sending it, because suggestions are model output too. Loading shows
|
|
9
14
|
* placeholders of the right shape; an empty list renders nothing.
|
|
10
15
|
*/
|
|
11
|
-
function PromptSuggestions({ suggestions, onChoose, label
|
|
16
|
+
function PromptSuggestions({ suggestions, onChoose, label: labelProp, loading = false, variant = "chips", className, }) {
|
|
17
|
+
const t = useStrings("promptSuggestions", promptSuggestionsStrings);
|
|
18
|
+
const label = labelProp ?? t("label");
|
|
12
19
|
if (!loading && suggestions.length === 0)
|
|
13
20
|
return null;
|
|
14
21
|
return (_jsxs("section", { "data-slot": "prompt-suggestions", "aria-label": label, className: cn("flex flex-col gap-2", className), children: [_jsxs("p", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [_jsx(Sparkles, { "aria-hidden": true, className: "size-3" }), " ", label] }), _jsx("div", { className: cn(variant === "cards" ? "grid gap-2 sm:grid-cols-2" : "flex flex-wrap gap-1.5"), children: loading
|
|
15
22
|
? Array.from({ length: variant === "cards" ? 4 : 3 }, (_, i) => (_jsx(Skeleton, { className: variant === "cards" ? "h-16 rounded-xl" : "h-8 w-44 rounded-lg" }, i)))
|
|
16
|
-
: suggestions.map((s) => variant === "cards" ? (_jsxs("button", { type: "button", onClick: () => onChoose(s), className: "group/suggestion flex items-start justify-between gap-3 rounded-xl border bg-card p-3 text-
|
|
23
|
+
: suggestions.map((s) => variant === "cards" ? (_jsxs("button", { type: "button", onClick: () => onChoose(s), className: "group/suggestion flex items-start justify-between gap-3 rounded-xl border bg-card p-3 text-start text-sm transition-[border-color,box-shadow,transform] duration-150 ease-standard outline-none hover:border-border-strong hover:shadow-raised active:scale-[0.99] focus-visible:ring-[3px] focus-visible:ring-ring/40", children: [_jsx("span", { children: s.text }), _jsx(ArrowUpRight, { "aria-hidden": true, className: "size-3.5 shrink-0 text-faint-foreground transition-colors group-hover/suggestion:text-accent-text" })] }, s.id)) : (_jsx("button", { type: "button", onClick: () => onChoose(s), className: "rounded-lg border bg-card px-3 py-1.5 text-sm text-muted-foreground transition-colors duration-150 outline-none hover:border-border-strong hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/40", children: s.text }, s.id))) })] }));
|
|
17
24
|
}
|
|
18
25
|
export { PromptSuggestions };
|
package/dist/chat/reasoning.d.ts
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { ReasoningEntry } from "../lib/chat.js";
|
|
3
|
+
/** Reasoning's fixed words; override them under `reasoning` in LocaleProvider's `strings`. */
|
|
4
|
+
export declare const reasoningStrings: {
|
|
5
|
+
verified: string;
|
|
6
|
+
believed: string;
|
|
7
|
+
assumed: string;
|
|
8
|
+
reasoning: string;
|
|
9
|
+
steps: {
|
|
10
|
+
one: string;
|
|
11
|
+
other: string;
|
|
12
|
+
};
|
|
13
|
+
assumedCount: string;
|
|
14
|
+
thinking: string;
|
|
15
|
+
evidence: string;
|
|
16
|
+
assumption: string;
|
|
17
|
+
};
|
|
3
18
|
/**
|
|
4
19
|
* The agent's stated reasoning above its answer. Each step says what it rests
|
|
5
20
|
* on: verified against evidence, believed, or assumed (with the assumption
|
package/dist/chat/reasoning.js
CHANGED
|
@@ -5,10 +5,18 @@ import { Brain, ChevronRight } from "lucide-react";
|
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
6
|
import { ShimmerLabel } from "../blocks/streaming-text.js";
|
|
7
7
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
|
|
8
|
-
|
|
8
|
+
import { useStrings } from "../blocks/locale.js";
|
|
9
|
+
/** Reasoning's fixed words; override them under `reasoning` in LocaleProvider's `strings`. */
|
|
10
|
+
export const reasoningStrings = {
|
|
9
11
|
verified: "Verified",
|
|
10
12
|
believed: "Believed",
|
|
11
13
|
assumed: "Assumed",
|
|
14
|
+
reasoning: "Reasoning",
|
|
15
|
+
steps: { one: "{count} step", other: "{count} steps" },
|
|
16
|
+
assumedCount: "{count} assumed",
|
|
17
|
+
thinking: "Thinking",
|
|
18
|
+
evidence: "Evidence:",
|
|
19
|
+
assumption: "Assumption:",
|
|
12
20
|
};
|
|
13
21
|
const basisTone = {
|
|
14
22
|
verified: "bg-success-weak text-success-text",
|
|
@@ -22,10 +30,11 @@ const basisTone = {
|
|
|
22
30
|
* when it answers.
|
|
23
31
|
*/
|
|
24
32
|
function Reasoning({ entries, thinking = false, thoughtFor, className, }) {
|
|
33
|
+
const t = useStrings("reasoning", reasoningStrings);
|
|
25
34
|
const [open, setOpen] = React.useState(thinking);
|
|
26
35
|
React.useEffect(() => setOpen(thinking), [thinking]);
|
|
27
36
|
const assumed = entries.filter((e) => e.basis === "assumed").length;
|
|
28
|
-
const summary = thinking ? null : (_jsxs(_Fragment, { children: ["
|
|
29
|
-
return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "reasoning", className: cn("group/reasoning", className), children: [_jsxs(CollapsibleTrigger, { className: "inline-flex items-center gap-1.5 rounded-md py-0.5
|
|
37
|
+
const summary = thinking ? null : (_jsxs(_Fragment, { children: [t("reasoning"), thoughtFor && _jsxs(_Fragment, { children: [" \u00B7 ", thoughtFor] }), " \u00B7 ", t("steps", { count: entries.length }), assumed > 0 && _jsxs("span", { className: "text-warning-text", children: [" \u00B7 ", t("assumedCount", { count: assumed })] })] }));
|
|
38
|
+
return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "reasoning", className: cn("group/reasoning", className), children: [_jsxs(CollapsibleTrigger, { className: "inline-flex items-center gap-1.5 rounded-md py-0.5 pe-1.5 text-xs text-muted-foreground outline-none transition-colors duration-150 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/40", children: [_jsx(Brain, { "aria-hidden": true, className: cn("size-3.5", thinking && "text-accent-text") }), thinking ? _jsx(ShimmerLabel, { className: "text-xs", children: t("thinking") }) : summary, _jsx(ChevronRight, { "aria-hidden": true, className: "rtl:-scale-x-100 size-3 transition-transform duration-200 ease-standard group-data-[state=open]/reasoning:[transform:rotate(90deg)]" })] }), _jsx(CollapsibleContent, { children: _jsx("ol", { className: "mt-2 ms-1.5 flex flex-col gap-3 border-s ps-4", children: entries.map((e) => (_jsxs("li", { className: "relative animate-rise-in", children: [_jsx("span", { "aria-hidden": true, className: "absolute top-1.5 -start-[1.2rem] size-1.5 rounded-full bg-border-strong" }), _jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 gap-y-1", children: [_jsx("p", { className: "text-sm font-medium", children: e.title }), _jsx("span", { className: cn("rounded-xs px-1 text-2xs font-medium", basisTone[e.basis]), children: t(e.basis) })] }), e.body && _jsx("p", { className: "mt-0.5 text-sm text-muted-foreground", children: e.body }), _jsx("p", { className: "mt-1 text-xs text-faint-foreground", children: e.basis === "verified" ? _jsxs(_Fragment, { children: [t("evidence"), " ", e.evidence] }) : _jsxs(_Fragment, { children: [t("assumption"), " ", e.assumption] }) })] }, e.id))) }) })] }));
|
|
30
39
|
}
|
|
31
40
|
export { Reasoning };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** ThreadDivider's fixed words; override them under `threadDivider` in LocaleProvider's `strings`. */
|
|
2
|
+
export declare const threadDividerStrings: {
|
|
3
|
+
today: string;
|
|
4
|
+
yesterday: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* A rule across the thread with a label in the middle: a day ("Today", "Yesterday", "5 Nov 2026") or a chapter of
|
|
8
|
+
* the conversation ("Drafting", "Review"). Read by screen readers as a separator with that label. Dates wait for the
|
|
9
|
+
* browser's clock and time zone, like every time in Candor, so server and browser render the same thing.
|
|
10
|
+
*/
|
|
11
|
+
declare function ThreadDivider({ date, label, className, }: {
|
|
12
|
+
/** A day to mark, shown as Today, Yesterday or the date in the locale. With neither this nor `label`, a plain rule. */
|
|
13
|
+
date?: Date | string;
|
|
14
|
+
/** A chapter or any other label; wins over `date`. */
|
|
15
|
+
label?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
18
|
+
export { ThreadDivider };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from "../lib/utils.js";
|
|
4
|
+
import { useFormat, useLocale, useStrings } from "../blocks/locale.js";
|
|
5
|
+
import { useNow } from "../blocks/relative-time.js";
|
|
6
|
+
/** ThreadDivider's fixed words; override them under `threadDivider` in LocaleProvider's `strings`. */
|
|
7
|
+
export const threadDividerStrings = {
|
|
8
|
+
today: "Today",
|
|
9
|
+
yesterday: "Yesterday",
|
|
10
|
+
};
|
|
11
|
+
/** The calendar day of a time, as y-m-d in a time zone (the browser's when left out). */
|
|
12
|
+
function dayOf(d, timeZone) {
|
|
13
|
+
return new Intl.DateTimeFormat("en-CA", { year: "numeric", month: "2-digit", day: "2-digit", ...(timeZone && { timeZone }) }).format(d);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A rule across the thread with a label in the middle: a day ("Today", "Yesterday", "5 Nov 2026") or a chapter of
|
|
17
|
+
* the conversation ("Drafting", "Review"). Read by screen readers as a separator with that label. Dates wait for the
|
|
18
|
+
* browser's clock and time zone, like every time in Candor, so server and browser render the same thing.
|
|
19
|
+
*/
|
|
20
|
+
function ThreadDivider({ date, label, className, }) {
|
|
21
|
+
const t = useStrings("threadDivider", threadDividerStrings);
|
|
22
|
+
const fmt = useFormat();
|
|
23
|
+
const now = useNow();
|
|
24
|
+
const { timeZone } = useLocale();
|
|
25
|
+
let text = label ?? "";
|
|
26
|
+
if (!label && date !== undefined) {
|
|
27
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
28
|
+
if (now !== null) {
|
|
29
|
+
const day = dayOf(d, timeZone);
|
|
30
|
+
const today = dayOf(new Date(now), timeZone);
|
|
31
|
+
const yesterday = dayOf(new Date(now - 86_400_000), timeZone);
|
|
32
|
+
text = day === today ? t("today") : day === yesterday ? t("yesterday") : fmt.date(d, "date");
|
|
33
|
+
}
|
|
34
|
+
else
|
|
35
|
+
text = fmt.date(d, "date");
|
|
36
|
+
}
|
|
37
|
+
return (_jsxs("div", { "data-slot": "thread-divider", role: "separator", "aria-label": text || undefined, className: cn("flex items-center gap-3 py-1 text-xs font-medium text-muted-foreground", className), children: [_jsx("span", { "aria-hidden": true, className: "h-px flex-1 bg-border" }), _jsx("span", { "aria-hidden": true, children: text }), _jsx("span", { "aria-hidden": true, className: "h-px flex-1 bg-border" })] }));
|
|
38
|
+
}
|
|
39
|
+
export { ThreadDivider };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type LucideIcon } from "lucide-react";
|
|
3
|
+
/**
|
|
4
|
+
* A quiet, centred line in a thread for something that happened rather than something said: "Sam joined", "Switched
|
|
5
|
+
* to the staging environment", "Run stopped by a guardrail". System messages are drawn with it too, so app events and
|
|
6
|
+
* system lines look the same. Text here is the app's own, never model output.
|
|
7
|
+
*/
|
|
8
|
+
declare function ThreadEvent({ icon: Icon, tone, at, children, className, ...props }: {
|
|
9
|
+
/** The glyph before the text; `null` for none. Default: an info mark. */
|
|
10
|
+
icon?: LucideIcon | null;
|
|
11
|
+
/** "quiet" (the default) or "attention", for an event a reader shouldn't miss. */
|
|
12
|
+
tone?: "quiet" | "attention";
|
|
13
|
+
/** When it happened, shown after the text as a relative time. */
|
|
14
|
+
at?: string;
|
|
15
|
+
/** What happened, in a short line. */
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
className?: string;
|
|
18
|
+
} & Omit<React.ComponentProps<"div">, "children">): React.JSX.Element;
|
|
19
|
+
export { ThreadEvent };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Info } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
import { RelativeTime } from "../blocks/relative-time.js";
|
|
7
|
+
/**
|
|
8
|
+
* A quiet, centred line in a thread for something that happened rather than something said: "Sam joined", "Switched
|
|
9
|
+
* to the staging environment", "Run stopped by a guardrail". System messages are drawn with it too, so app events and
|
|
10
|
+
* system lines look the same. Text here is the app's own, never model output.
|
|
11
|
+
*/
|
|
12
|
+
function ThreadEvent({ icon: Icon = Info, tone = "quiet", at, children, className, ...props }) {
|
|
13
|
+
return (_jsxs("div", { "data-slot": "thread-event", "data-tone": tone, className: cn("flex items-center justify-center gap-1.5 py-1 text-center text-xs", tone === "attention" ? "text-attention-text" : "text-faint-foreground", className), ...props, children: [Icon && _jsx(Icon, { "aria-hidden": true, className: "size-3 shrink-0" }), children, at && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx(RelativeTime, { date: at })] }))] }));
|
|
14
|
+
}
|
|
15
|
+
export { ThreadEvent };
|
package/dist/chat/thread.d.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
/** Thread's fixed words; override them under `thread` in LocaleProvider's `strings`. */
|
|
3
|
+
export declare const threadStrings: {
|
|
4
|
+
messages: string;
|
|
5
|
+
jumpToLatest: string;
|
|
6
|
+
};
|
|
2
7
|
/**
|
|
3
8
|
* The scrolling message column. It follows new content while you're at the
|
|
4
9
|
* bottom and stops following the moment you scroll up to read, offering a
|
|
5
10
|
* "Jump to latest" button instead. Pass `empty` for the first-run state and
|
|
6
11
|
* the composer as `footer`.
|
|
12
|
+
*
|
|
13
|
+
* For long histories, `virtualize` renders only the messages in or near view
|
|
14
|
+
* (each child is one item, keyed by its `key`). The column stays a `log`, the
|
|
15
|
+
* message holding focus stays rendered, and Jump to latest works the same.
|
|
7
16
|
*/
|
|
8
|
-
declare function Thread({ children, empty, footer, className, contentClassName, }: {
|
|
17
|
+
declare function Thread({ children, empty, footer, className, contentClassName, virtualize, }: {
|
|
9
18
|
children?: React.ReactNode;
|
|
10
19
|
/** Shown before the first message. */
|
|
11
20
|
empty?: React.ReactNode;
|
|
@@ -14,5 +23,7 @@ declare function Thread({ children, empty, footer, className, contentClassName,
|
|
|
14
23
|
className?: string;
|
|
15
24
|
/** Classes for the message column. */
|
|
16
25
|
contentClassName?: string;
|
|
26
|
+
/** Render only the messages in or near view, for histories of hundreds of messages. Give each child a `key`. Defaults to false. */
|
|
27
|
+
virtualize?: boolean;
|
|
17
28
|
}): React.JSX.Element;
|
|
18
29
|
export { Thread };
|
package/dist/chat/thread.js
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { ArrowDown } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
|
+
import { useStrings } from "../blocks/locale.js";
|
|
7
|
+
import { useVirtualList } from "../lib/use-virtual-list.js";
|
|
8
|
+
/** Thread's fixed words; override them under `thread` in LocaleProvider's `strings`. */
|
|
9
|
+
export const threadStrings = {
|
|
10
|
+
messages: "Messages",
|
|
11
|
+
jumpToLatest: "Jump to latest",
|
|
12
|
+
};
|
|
6
13
|
/**
|
|
7
14
|
* The scrolling message column. It follows new content while you're at the
|
|
8
15
|
* bottom and stops following the moment you scroll up to read, offering a
|
|
9
16
|
* "Jump to latest" button instead. Pass `empty` for the first-run state and
|
|
10
17
|
* the composer as `footer`.
|
|
18
|
+
*
|
|
19
|
+
* For long histories, `virtualize` renders only the messages in or near view
|
|
20
|
+
* (each child is one item, keyed by its `key`). The column stays a `log`, the
|
|
21
|
+
* message holding focus stays rendered, and Jump to latest works the same.
|
|
11
22
|
*/
|
|
12
|
-
function Thread({ children, empty, footer, className, contentClassName, }) {
|
|
23
|
+
function Thread({ children, empty, footer, className, contentClassName, virtualize = false, }) {
|
|
24
|
+
const t = useStrings("thread", threadStrings);
|
|
13
25
|
const scroller = React.useRef(null);
|
|
14
26
|
const content = React.useRef(null);
|
|
15
27
|
const pinnedRef = React.useRef(true);
|
|
@@ -51,10 +63,26 @@ function Thread({ children, empty, footer, className, contentClassName, }) {
|
|
|
51
63
|
lastTop.current = top;
|
|
52
64
|
};
|
|
53
65
|
const hasMessages = React.Children.count(children) > 0;
|
|
54
|
-
|
|
66
|
+
const items = virtualize ? React.Children.toArray(children) : [];
|
|
67
|
+
const keyOf = (i) => (React.isValidElement(items[i]) ? (items[i].key ?? i) : i);
|
|
68
|
+
// The message holding focus stays rendered, so focus isn't lost when it scrolls out of view.
|
|
69
|
+
const [focused, setFocused] = React.useState(null);
|
|
70
|
+
const list = useVirtualList({
|
|
71
|
+
enabled: virtualize,
|
|
72
|
+
count: items.length,
|
|
73
|
+
keyOf,
|
|
74
|
+
scroller,
|
|
75
|
+
pin: focused,
|
|
76
|
+
gap: () => (content.current ? Number.parseFloat(getComputedStyle(content.current).rowGap) || 0 : 0),
|
|
77
|
+
});
|
|
78
|
+
return (_jsxs("div", { "data-slot": "thread", className: cn("flex min-h-0 flex-col", className), children: [_jsxs("div", { className: "relative min-h-0 flex-1", children: [_jsx("div", { ref: scroller, onScroll: onScroll, onFocus: virtualize
|
|
79
|
+
? (e) => {
|
|
80
|
+
setFocused(e.target.closest("[data-virtual-key]")?.getAttribute("data-virtual-key") ?? null);
|
|
81
|
+
}
|
|
82
|
+
: undefined, onBlur: virtualize ? (e) => !e.currentTarget.contains(e.relatedTarget) && setFocused(null) : undefined, className: "h-full overflow-y-auto overscroll-contain", children: _jsx("div", { ref: content, role: "log", "aria-label": t("messages"), className: cn("mx-auto flex w-full max-w-(--cd-thread-width) flex-col gap-7 px-4 py-6", contentClassName), children: !hasMessages ? (empty) : virtualize ? (_jsx(_Fragment, { children: list.rows.map((row) => "spacer" in row ? (_jsx("div", { "aria-hidden": true, style: { height: row.spacer } }, row.key)) : (_jsx("div", { ref: list.measure, "data-virtual-key": String(keyOf(row.index)), className: "flex flex-col", children: items[row.index] }, keyOf(row.index)))) })) : (children) }) }), !pinned && hasMessages && (_jsxs("button", { type: "button", onClick: () => {
|
|
55
83
|
pinnedRef.current = true;
|
|
56
84
|
setPinned(true);
|
|
57
85
|
toBottom("smooth");
|
|
58
|
-
}, className: "absolute bottom-3 left-1/2 z-10 inline-flex -translate-x-1/2 items-center gap-1.5 rounded-full border bg-popover px-3 py-1.5 text-xs font-medium shadow-float animate-in fade-in-0 slide-in-from-bottom-1", children: [_jsx(ArrowDown, { className: "size-3" }), "
|
|
86
|
+
}, className: "absolute bottom-3 left-1/2 z-10 inline-flex -translate-x-1/2 items-center gap-1.5 rounded-full border bg-popover px-3 py-1.5 text-xs font-medium shadow-float animate-in fade-in-0 slide-in-from-bottom-1", children: [_jsx(ArrowDown, { "aria-hidden": true, className: "size-3" }), " ", t("jumpToLatest")] })), _jsx("div", { "aria-hidden": true, className: "pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-background to-transparent" })] }), footer && _jsx("div", { className: "mx-auto w-full max-w-(--cd-thread-width) px-4 pb-4", children: footer })] }));
|
|
59
87
|
}
|
|
60
88
|
export { Thread };
|
package/dist/chat/tool-call.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { ToolCall as ToolCallData } from "../lib/chat.js";
|
|
3
|
+
/** ToolCall's fixed words; override them under `toolCall` in LocaleProvider's `strings`. */
|
|
4
|
+
export declare const toolCallStrings: {
|
|
5
|
+
input: string;
|
|
6
|
+
output: string;
|
|
7
|
+
calls: {
|
|
8
|
+
one: string;
|
|
9
|
+
other: string;
|
|
10
|
+
};
|
|
11
|
+
done: string;
|
|
12
|
+
failed: string;
|
|
13
|
+
working: string;
|
|
14
|
+
};
|
|
3
15
|
/**
|
|
4
16
|
* One tool call as a compact row: what it did in plain words, its status, and
|
|
5
17
|
* how long it took. Expands to the input and output. A failed call opens by
|
package/dist/chat/tool-call.js
CHANGED
|
@@ -5,22 +5,35 @@ import { ChevronRight, Wrench } from "lucide-react";
|
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
6
|
import { StepMark } from "../blocks/step-list.js";
|
|
7
7
|
import { ShimmerLabel } from "../blocks/streaming-text.js";
|
|
8
|
-
import {
|
|
8
|
+
import { useStatusLabel } from "../lib/status.js";
|
|
9
9
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
|
|
10
10
|
import { Payload } from "./payload.js";
|
|
11
|
+
import { useStrings } from "../blocks/locale.js";
|
|
12
|
+
/** ToolCall's fixed words; override them under `toolCall` in LocaleProvider's `strings`. */
|
|
13
|
+
export const toolCallStrings = {
|
|
14
|
+
input: "Input",
|
|
15
|
+
output: "Output",
|
|
16
|
+
calls: { one: "{count} tool call", other: "{count} tool calls" },
|
|
17
|
+
done: "{count} done",
|
|
18
|
+
failed: "{count} failed",
|
|
19
|
+
working: "working",
|
|
20
|
+
};
|
|
11
21
|
/**
|
|
12
22
|
* One tool call as a compact row: what it did in plain words, its status, and
|
|
13
23
|
* how long it took. Expands to the input and output. A failed call opens by
|
|
14
24
|
* default and leads with its error.
|
|
15
25
|
*/
|
|
16
26
|
function ToolCall({ call, className, }) {
|
|
27
|
+
const t = useStrings("toolCall", toolCallStrings);
|
|
28
|
+
const statusLabel = useStatusLabel();
|
|
17
29
|
const failed = call.status === "failed";
|
|
18
30
|
const [open, setOpen] = React.useState(failed);
|
|
19
31
|
const hasDetail = Boolean(call.input || call.output || call.error);
|
|
20
|
-
return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "tool-call", "data-status": call.status, className: cn("group/tool", className), children: [_jsxs(CollapsibleTrigger, { disabled: !hasDetail, className: "flex w-full items-center gap-2.5 rounded-md px-1.5 py-1 text-
|
|
32
|
+
return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "tool-call", "data-status": call.status, className: cn("group/tool", className), children: [_jsxs(CollapsibleTrigger, { disabled: !hasDetail, className: "flex w-full items-center gap-2.5 rounded-md px-1.5 py-1 text-start outline-none transition-colors duration-150 enabled:hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/40", children: [_jsx(StepMark, { status: call.status }), _jsxs("span", { className: "min-w-0 flex-1 truncate text-sm", children: [call.status === "running" ? _jsx(ShimmerLabel, { className: "font-normal", children: call.title }) : call.title, _jsxs("span", { className: "sr-only", children: [" \u2014 ", statusLabel(call.status)] })] }), _jsx("code", { className: "hidden shrink-0 font-mono text-2xs text-faint-foreground sm:inline", children: call.tool }), call.duration && _jsx("span", { className: "shrink-0 text-xs tabular-nums text-faint-foreground", children: call.duration }), hasDetail && (_jsx(ChevronRight, { "aria-hidden": true, className: "rtl:-scale-x-100 size-3.5 shrink-0 text-faint-foreground transition-transform duration-200 ease-standard group-data-[state=open]/tool:[transform:rotate(90deg)]" }))] }), _jsx(CollapsibleContent, { className: "data-[state=open]:animate-in data-[state=open]:fade-in-0", children: _jsxs("div", { className: "mt-1 mb-2 ms-[1.375rem] flex flex-col gap-2 border-s ps-3.5", children: [call.error && (_jsx("p", { className: "rounded-md bg-destructive-weak px-2.5 py-1.5 text-xs text-destructive-text", children: call.error })), call.input && _jsx(Payload, { language: "json", title: t("input"), text: call.input }), call.output && _jsx(Payload, { language: call.outputLanguage ?? "text", title: t("output"), text: call.output })] }) })] }));
|
|
21
33
|
}
|
|
22
34
|
/** A run of tool calls, with a count when it's long enough to fold. */
|
|
23
35
|
function ToolCallGroup({ calls, className, }) {
|
|
36
|
+
const t = useStrings("toolCall", toolCallStrings);
|
|
24
37
|
const done = calls.filter((c) => c.status === "succeeded").length;
|
|
25
38
|
const failed = calls.filter((c) => c.status === "failed").length;
|
|
26
39
|
const running = calls.some((c) => c.status === "running");
|
|
@@ -28,6 +41,6 @@ function ToolCallGroup({ calls, className, }) {
|
|
|
28
41
|
if (calls.length <= 2) {
|
|
29
42
|
return (_jsx("div", { "data-slot": "tool-call-group", className: cn("flex flex-col", className), children: calls.map((c) => (_jsx(ToolCall, { call: c }, c.id))) }));
|
|
30
43
|
}
|
|
31
|
-
return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "tool-call-group", className: cn("group/tools rounded-lg border bg-card", className), children: [_jsxs(CollapsibleTrigger, { className: "flex w-full items-center gap-2 px-3 py-2 text-
|
|
44
|
+
return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "tool-call-group", className: cn("group/tools rounded-lg border bg-card", className), children: [_jsxs(CollapsibleTrigger, { className: "flex w-full items-center gap-2 px-3 py-2 text-start text-xs text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/40 rounded-lg", children: [_jsx(Wrench, { "aria-hidden": true, className: "size-3.5" }), _jsxs("span", { className: "flex-1", children: [t("calls", { count: calls.length }), done > 0 && ` · ${t("done", { count: done })}`, failed > 0 && _jsxs("span", { className: "text-destructive-text", children: [" \u00B7 ", t("failed", { count: failed })] }), running && ` · ${t("working")}`] }), _jsx(ChevronRight, { "aria-hidden": true, className: "rtl:-scale-x-100 size-3.5 transition-transform duration-200 ease-standard group-data-[state=open]/tools:[transform:rotate(90deg)]" })] }), _jsx(CollapsibleContent, { children: _jsx("div", { className: "flex flex-col border-t px-1.5 py-1.5", children: calls.map((c) => (_jsx(ToolCall, { call: c }, c.id))) }) })] }));
|
|
32
45
|
}
|
|
33
46
|
export { ToolCall, ToolCallGroup };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,9 @@ export * from "./lib/chat.js";
|
|
|
2
2
|
export * from "./lib/status.js";
|
|
3
3
|
export * from "./lib/use-copy.js";
|
|
4
4
|
export * from "./lib/use-shortcut.js";
|
|
5
|
+
export * from "./lib/use-virtual-list.js";
|
|
5
6
|
export * from "./blocks/terms.js";
|
|
7
|
+
export * from "./blocks/locale.js";
|
|
6
8
|
export * from "./blocks/locked.js";
|
|
7
9
|
export * from "./blocks/status-dot.js";
|
|
8
10
|
export * from "./blocks/step-list.js";
|
|
@@ -14,6 +16,7 @@ export * from "./blocks/surface.js";
|
|
|
14
16
|
export * from "./chat/agent-avatar.js";
|
|
15
17
|
export * from "./chat/entity-chip.js";
|
|
16
18
|
export * from "./chat/citation.js";
|
|
19
|
+
export * from "./lib/highlight.js";
|
|
17
20
|
export * from "./chat/payload.js";
|
|
18
21
|
export * from "./chat/message-content.js";
|
|
19
22
|
export * from "./chat/message.js";
|
|
@@ -21,7 +24,11 @@ export * from "./chat/message-actions.js";
|
|
|
21
24
|
export * from "./chat/reasoning.js";
|
|
22
25
|
export * from "./chat/tool-call.js";
|
|
23
26
|
export * from "./chat/thread.js";
|
|
27
|
+
export * from "./chat/thread-divider.js";
|
|
28
|
+
export * from "./chat/thread-event.js";
|
|
29
|
+
export * from "./chat/folded-turns.js";
|
|
24
30
|
export * from "./chat/composer.js";
|
|
31
|
+
export * from "./chat/attachment-preview.js";
|
|
25
32
|
export * from "./chat/prompt-suggestions.js";
|
|
26
33
|
export * from "./chat/context-meter.js";
|
|
27
34
|
export * from "./chat/card-registry.js";
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,9 @@ export * from "./lib/chat.js";
|
|
|
2
2
|
export * from "./lib/status.js";
|
|
3
3
|
export * from "./lib/use-copy.js";
|
|
4
4
|
export * from "./lib/use-shortcut.js";
|
|
5
|
+
export * from "./lib/use-virtual-list.js";
|
|
5
6
|
export * from "./blocks/terms.js";
|
|
7
|
+
export * from "./blocks/locale.js";
|
|
6
8
|
export * from "./blocks/locked.js";
|
|
7
9
|
export * from "./blocks/status-dot.js";
|
|
8
10
|
export * from "./blocks/step-list.js";
|
|
@@ -14,6 +16,7 @@ export * from "./blocks/surface.js";
|
|
|
14
16
|
export * from "./chat/agent-avatar.js";
|
|
15
17
|
export * from "./chat/entity-chip.js";
|
|
16
18
|
export * from "./chat/citation.js";
|
|
19
|
+
export * from "./lib/highlight.js";
|
|
17
20
|
export * from "./chat/payload.js";
|
|
18
21
|
export * from "./chat/message-content.js";
|
|
19
22
|
export * from "./chat/message.js";
|
|
@@ -21,7 +24,11 @@ export * from "./chat/message-actions.js";
|
|
|
21
24
|
export * from "./chat/reasoning.js";
|
|
22
25
|
export * from "./chat/tool-call.js";
|
|
23
26
|
export * from "./chat/thread.js";
|
|
27
|
+
export * from "./chat/thread-divider.js";
|
|
28
|
+
export * from "./chat/thread-event.js";
|
|
29
|
+
export * from "./chat/folded-turns.js";
|
|
24
30
|
export * from "./chat/composer.js";
|
|
31
|
+
export * from "./chat/attachment-preview.js";
|
|
25
32
|
export * from "./chat/prompt-suggestions.js";
|
|
26
33
|
export * from "./chat/context-meter.js";
|
|
27
34
|
export * from "./chat/card-registry.js";
|
package/dist/lib/chat.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export type MessageAuthor = {
|
|
|
44
44
|
export type EntityKind = string;
|
|
45
45
|
export type EntityRef = {
|
|
46
46
|
kind: EntityKind;
|
|
47
|
-
/** The identifier, e.g. "
|
|
47
|
+
/** The identifier, e.g. "orders.status" or "billing.invoice.due_date". */
|
|
48
48
|
name: string;
|
|
49
49
|
/** A friendlier label when there is one, e.g. "Region". */
|
|
50
50
|
label?: string;
|
|
@@ -185,6 +185,11 @@ export type MessageBlock = {
|
|
|
185
185
|
card: string;
|
|
186
186
|
props: unknown;
|
|
187
187
|
summary: string;
|
|
188
|
+
}
|
|
189
|
+
/** Files sent with the message, shown as tiles. New in 1.1: a `switch` over kinds needs a `default` branch. */
|
|
190
|
+
| {
|
|
191
|
+
kind: "attachments";
|
|
192
|
+
files: Attachment[];
|
|
188
193
|
};
|
|
189
194
|
export type MessageStatus = "streaming" | "complete" | "interrupted" | "failed";
|
|
190
195
|
export type Message = {
|
|
@@ -229,7 +234,12 @@ export type SlashCommand = {
|
|
|
229
234
|
export type Attachment = {
|
|
230
235
|
id: string;
|
|
231
236
|
name: string;
|
|
237
|
+
/** In bytes. */
|
|
232
238
|
size?: number;
|
|
239
|
+
/** e.g. "image/png", "application/pdf": picks the icon, and allows an image preview. */
|
|
240
|
+
mediaType?: string;
|
|
241
|
+
/** A thumbnail your app serves (or a blob: URL), shown for images. Never a URL from model output. */
|
|
242
|
+
previewUrl?: string;
|
|
233
243
|
/** 0–1 while uploading; absent when done. */
|
|
234
244
|
progress?: number;
|
|
235
245
|
error?: string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small syntax highlighter for code an agent shows: it splits text into typed tokens (keyword, string, comment…),
|
|
3
|
+
* and Payload renders each as a span. The output is text, never HTML, so nothing in the code can run or inject markup.
|
|
4
|
+
* It covers the languages agents show most (JS/TS, Python, SQL, JSON, shell, YAML, CSS, HTML/XML, Go, Rust, Java, C)
|
|
5
|
+
* in a few kilobytes; anything else comes back as one plain token.
|
|
6
|
+
*/
|
|
7
|
+
export type TokenType = "keyword" | "string" | "number" | "comment" | "function" | "property" | "type" | "tag" | "variable" | "operator" | "plain";
|
|
8
|
+
export type Token = {
|
|
9
|
+
type: TokenType;
|
|
10
|
+
text: string;
|
|
11
|
+
};
|
|
12
|
+
/** Whether `language` has a grammar here. */
|
|
13
|
+
export declare function canHighlight(language: string): boolean;
|
|
14
|
+
/** The text split into typed tokens; their texts joined give back the input exactly. */
|
|
15
|
+
export declare function tokenize(text: string, language: string): Token[];
|