@candor-ui/react 0.0.0-stage → 1.0.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 +24 -0
- package/README.md +187 -2
- package/dist/adapters/ag-ui.d.ts +75 -0
- package/dist/adapters/ag-ui.js +441 -0
- package/dist/adapters/ai-sdk.d.ts +22 -0
- package/dist/adapters/ai-sdk.js +128 -0
- package/dist/adapters/claude-agent-sdk.d.ts +80 -0
- package/dist/adapters/claude-agent-sdk.js +265 -0
- package/dist/adapters/claude-agent-server.d.ts +53 -0
- package/dist/adapters/claude-agent-server.js +104 -0
- package/dist/adapters/use-ag-ui.d.ts +55 -0
- package/dist/adapters/use-ag-ui.js +168 -0
- package/dist/adapters/use-candor-chat.d.ts +40 -0
- package/dist/adapters/use-candor-chat.js +56 -0
- package/dist/adapters/use-claude-agent.d.ts +48 -0
- package/dist/adapters/use-claude-agent.js +143 -0
- package/dist/ag-ui.d.ts +2 -0
- package/dist/ag-ui.js +2 -0
- package/dist/ai-sdk.d.ts +2 -0
- package/dist/ai-sdk.js +2 -0
- package/dist/blocks/copy-field.d.ts +13 -0
- package/dist/blocks/copy-field.js +15 -0
- package/dist/blocks/degraded-panel.d.ts +19 -0
- package/dist/blocks/degraded-panel.js +14 -0
- package/dist/blocks/locked.d.ts +26 -0
- package/dist/blocks/locked.js +36 -0
- package/dist/blocks/relative-time.d.ts +13 -0
- package/dist/blocks/relative-time.js +62 -0
- package/dist/blocks/status-dot.d.ts +16 -0
- package/dist/blocks/status-dot.js +25 -0
- package/dist/blocks/step-list.d.ts +23 -0
- package/dist/blocks/step-list.js +37 -0
- package/dist/blocks/streaming-text.d.ts +17 -0
- package/dist/blocks/streaming-text.js +14 -0
- package/dist/blocks/surface.d.ts +9 -0
- package/dist/blocks/surface.js +35 -0
- package/dist/blocks/terms.d.ts +24 -0
- package/dist/blocks/terms.js +17 -0
- package/dist/chat/agent-avatar.d.ts +34 -0
- package/dist/chat/agent-avatar.js +42 -0
- package/dist/chat/card-registry.d.ts +39 -0
- package/dist/chat/card-registry.js +49 -0
- package/dist/chat/citation.d.ts +29 -0
- package/dist/chat/citation.js +44 -0
- package/dist/chat/composer.d.ts +54 -0
- package/dist/chat/composer.js +168 -0
- package/dist/chat/context-meter.d.ts +14 -0
- package/dist/chat/context-meter.js +30 -0
- package/dist/chat/entity-chip.d.ts +37 -0
- package/dist/chat/entity-chip.js +52 -0
- package/dist/chat/message-actions.d.ts +21 -0
- package/dist/chat/message-actions.js +20 -0
- package/dist/chat/message-content.d.ts +25 -0
- package/dist/chat/message-content.js +109 -0
- package/dist/chat/message.d.ts +39 -0
- package/dist/chat/message.js +44 -0
- package/dist/chat/payload.d.ts +16 -0
- package/dist/chat/payload.js +18 -0
- package/dist/chat/prompt-suggestions.d.ts +20 -0
- package/dist/chat/prompt-suggestions.js +18 -0
- package/dist/chat/reasoning.d.ts +18 -0
- package/dist/chat/reasoning.js +31 -0
- package/dist/chat/thread.d.ts +18 -0
- package/dist/chat/thread.js +60 -0
- package/dist/chat/tool-call.d.ts +19 -0
- package/dist/chat/tool-call.js +33 -0
- package/dist/claude-server.d.ts +1 -0
- package/dist/claude-server.js +2 -0
- package/dist/claude.d.ts +2 -0
- package/dist/claude.js +2 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +38 -0
- package/dist/lib/chat.d.ts +221 -0
- package/dist/lib/chat.js +1 -0
- package/dist/lib/know.d.ts +123 -0
- package/dist/lib/know.js +1 -0
- package/dist/lib/render-prop.d.ts +16 -0
- package/dist/lib/render-prop.js +7 -0
- package/dist/lib/safe-markdown.d.ts +7 -0
- package/dist/lib/safe-markdown.js +89 -0
- package/dist/lib/status.d.ts +21 -0
- package/dist/lib/status.js +26 -0
- package/dist/lib/use-copy.d.ts +10 -0
- package/dist/lib/use-copy.js +24 -0
- package/dist/lib/use-shortcut.d.ts +15 -0
- package/dist/lib/use-shortcut.js +62 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/styles.css +460 -0
- package/dist/ui/button.d.ts +12 -0
- package/dist/ui/button.js +41 -0
- package/dist/ui/collapsible.d.ts +6 -0
- package/dist/ui/collapsible.js +15 -0
- package/dist/ui/popover.d.ts +11 -0
- package/dist/ui/popover.js +29 -0
- package/dist/ui/skeleton.d.ts +2 -0
- package/dist/ui/skeleton.js +6 -0
- package/dist/ui/tooltip.d.ts +9 -0
- package/dist/ui/tooltip.js +24 -0
- package/package.json +91 -4
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type LucideIcon } from "lucide-react";
|
|
3
|
+
import type { EntityRef } from "../lib/chat.js";
|
|
4
|
+
export type EntityKindInfo = {
|
|
5
|
+
Icon: LucideIcon;
|
|
6
|
+
noun: string;
|
|
7
|
+
};
|
|
8
|
+
/** Kinds most apps have. Add your own (or a preset) with EntityKindsProvider. */
|
|
9
|
+
export declare const baseKinds: Record<string, EntityKindInfo>;
|
|
10
|
+
/**
|
|
11
|
+
* The kinds of thing your app's entities can be: an icon and a noun each.
|
|
12
|
+
* Nested providers add to (and can override) the kinds above them.
|
|
13
|
+
*/
|
|
14
|
+
declare function EntityKindsProvider({ kinds, children, }: {
|
|
15
|
+
/** Kind ids mapped to an icon and a noun. */
|
|
16
|
+
kinds: Record<string, EntityKindInfo>;
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
}): React.JSX.Element;
|
|
19
|
+
/** Look up a kind's icon and noun; unknown kinds read as "Item". */
|
|
20
|
+
declare function useEntityKinds(): (kind: string) => EntityKindInfo;
|
|
21
|
+
/**
|
|
22
|
+
* A thing in the user's system (a record, a table, a field, a workflow…) named
|
|
23
|
+
* in a message or the composer. The kind's icon and noun are always present,
|
|
24
|
+
* so "Account" the table and "Account" the report never read alike. The
|
|
25
|
+
* identifier is the label unless a friendlier one is given; then the
|
|
26
|
+
* identifier is on hover and for screen readers.
|
|
27
|
+
*/
|
|
28
|
+
declare function EntityChip({ entity, onOpen, onRemove, className, }: {
|
|
29
|
+
/** The thing to show: its kind, identifier and optional label. */
|
|
30
|
+
entity: EntityRef;
|
|
31
|
+
/** Makes the chip a button that opens the thing. */
|
|
32
|
+
onOpen?: (entity: EntityRef) => void;
|
|
33
|
+
/** Adds a remove button, e.g. in the composer. */
|
|
34
|
+
onRemove?: (entity: EntityRef) => void;
|
|
35
|
+
className?: string;
|
|
36
|
+
}): React.JSX.Element;
|
|
37
|
+
export { EntityChip, EntityKindsProvider, useEntityKinds };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Box, Braces, ChartColumn, File, FileText, Gauge, Link, Table2, TextCursorInput, UserRound, Waypoints, } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
/** Kinds most apps have. Add your own (or a preset) with EntityKindsProvider. */
|
|
7
|
+
export const baseKinds = {
|
|
8
|
+
record: { Icon: FileText, noun: "Record" },
|
|
9
|
+
document: { Icon: FileText, noun: "Document" },
|
|
10
|
+
file: { Icon: File, noun: "File" },
|
|
11
|
+
table: { Icon: Table2, noun: "Table" },
|
|
12
|
+
field: { Icon: TextCursorInput, noun: "Field" },
|
|
13
|
+
person: { Icon: UserRound, noun: "Person" },
|
|
14
|
+
link: { Icon: Link, noun: "Link" },
|
|
15
|
+
code: { Icon: Braces, noun: "Code" },
|
|
16
|
+
workflow: { Icon: Waypoints, noun: "Workflow" },
|
|
17
|
+
report: { Icon: ChartColumn, noun: "Report" },
|
|
18
|
+
dashboard: { Icon: Gauge, noun: "Dashboard" },
|
|
19
|
+
};
|
|
20
|
+
/** What an unregistered kind shows: still labelled, never a crash or a blank. */
|
|
21
|
+
const unknownKind = { Icon: Box, noun: "Item" };
|
|
22
|
+
const KindsContext = React.createContext(baseKinds);
|
|
23
|
+
/**
|
|
24
|
+
* The kinds of thing your app's entities can be: an icon and a noun each.
|
|
25
|
+
* Nested providers add to (and can override) the kinds above them.
|
|
26
|
+
*/
|
|
27
|
+
function EntityKindsProvider({ kinds, children, }) {
|
|
28
|
+
const parent = React.useContext(KindsContext);
|
|
29
|
+
const value = React.useMemo(() => ({ ...parent, ...kinds }), [parent, kinds]);
|
|
30
|
+
return _jsx(KindsContext.Provider, { value: value, children: children });
|
|
31
|
+
}
|
|
32
|
+
/** Look up a kind's icon and noun; unknown kinds read as "Item". */
|
|
33
|
+
function useEntityKinds() {
|
|
34
|
+
const kinds = React.useContext(KindsContext);
|
|
35
|
+
return React.useCallback((kind) => kinds[kind] ?? unknownKind, [kinds]);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A thing in the user's system (a record, a table, a field, a workflow…) named
|
|
39
|
+
* in a message or the composer. The kind's icon and noun are always present,
|
|
40
|
+
* so "Account" the table and "Account" the report never read alike. The
|
|
41
|
+
* identifier is the label unless a friendlier one is given; then the
|
|
42
|
+
* identifier is on hover and for screen readers.
|
|
43
|
+
*/
|
|
44
|
+
function EntityChip({ entity, onOpen, onRemove, className, }) {
|
|
45
|
+
const { Icon, noun } = useEntityKinds()(entity.kind);
|
|
46
|
+
const shown = entity.label ?? entity.name;
|
|
47
|
+
const title = `${noun}: ${entity.name}${entity.location ? ` in ${entity.location}` : ""}`;
|
|
48
|
+
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
|
+
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": `Remove ${shown}`, onClick: () => onRemove(entity), className: "-ml-px rounded-r-sm border border-l-0 bg-card px-1 py-px text-faint-foreground hover:text-foreground", children: "\u00D7" }))] }));
|
|
51
|
+
}
|
|
52
|
+
export { EntityChip, EntityKindsProvider, useEntityKinds };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type Rating = "up" | "down" | null;
|
|
3
|
+
/**
|
|
4
|
+
* Copy, retry and rate under an agent's reply. Each action appears only when
|
|
5
|
+
* its handler is given. Copy reports what the clipboard did. A rating is the
|
|
6
|
+
* caller's state, so it shows only what the service recorded.
|
|
7
|
+
*/
|
|
8
|
+
declare function MessageActions({ text, onRetry, rating, onRate, meta, className, }: {
|
|
9
|
+
/** What Copy copies; without it there's no Copy. */
|
|
10
|
+
text?: string;
|
|
11
|
+
/** Shows Retry. */
|
|
12
|
+
onRetry?: () => void;
|
|
13
|
+
/** The current rating: up, down or none. */
|
|
14
|
+
rating?: Rating;
|
|
15
|
+
/** Shows thumbs up and down. */
|
|
16
|
+
onRate?: (rating: Rating) => void;
|
|
17
|
+
/** Facts after the actions, e.g. the model and cost. */
|
|
18
|
+
meta?: React.ReactNode;
|
|
19
|
+
className?: string;
|
|
20
|
+
}): React.JSX.Element;
|
|
21
|
+
export { MessageActions };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Check, Copy, RotateCw, ThumbsDown, ThumbsUp, X } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
import { useCopy } from "../lib/use-copy.js";
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
|
|
8
|
+
function ActionButton({ label, onClick, pressed, children, }) {
|
|
9
|
+
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
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Copy, retry and rate under an agent's reply. Each action appears only when
|
|
13
|
+
* its handler is given. Copy reports what the clipboard did. A rating is the
|
|
14
|
+
* caller's state, so it shows only what the service recorded.
|
|
15
|
+
*/
|
|
16
|
+
function MessageActions({ text, onRetry, rating, onRate, meta, className, }) {
|
|
17
|
+
const { state, copy } = useCopy();
|
|
18
|
+
return (_jsxs("div", { "data-slot": "message-actions", className: cn("flex items-center gap-0.5", className), children: [text !== undefined && (_jsx(ActionButton, { label: state === "copied" ? "Copied" : state === "failed" ? "Couldn’t copy" : "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: "Retry", onClick: onRetry, children: _jsx(RotateCw, {}) })), onRate && (_jsxs(_Fragment, { children: [_jsx(ActionButton, { label: "Good answer", pressed: rating === "up", onClick: () => onRate(rating === "up" ? null : "up"), children: _jsx(ThumbsUp, { className: cn(rating === "up" && "fill-current") }) }), _jsx(ActionButton, { label: "Bad answer", pressed: rating === "down", onClick: () => onRate(rating === "down" ? null : "down"), children: _jsx(ThumbsDown, { className: cn(rating === "down" && "fill-current") }) })] })), meta && _jsx("span", { className: "ml-1.5 truncate text-xs text-faint-foreground", children: meta })] }));
|
|
19
|
+
}
|
|
20
|
+
export { MessageActions };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { EntityRef, InlineNode, MessageBlock, MessageSource } from "../lib/chat.js";
|
|
3
|
+
/**
|
|
4
|
+
* Inline nodes. Every string here may come from a model or the user's system, so it is
|
|
5
|
+
* only ever rendered as a React text child: no markdown parsing, no HTML.
|
|
6
|
+
*/
|
|
7
|
+
declare function Inline({ nodes, }: {
|
|
8
|
+
/** Inline nodes: text, strong, code, entities, citations. */
|
|
9
|
+
nodes: InlineNode[];
|
|
10
|
+
}): React.JSX.Element;
|
|
11
|
+
/** 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. */
|
|
14
|
+
blocks: MessageBlock[];
|
|
15
|
+
/** Sources the blocks cite by marker. */
|
|
16
|
+
sources?: MessageSource[];
|
|
17
|
+
/** Makes entity chips open their thing. */
|
|
18
|
+
onOpenEntity?: (entity: EntityRef) => void;
|
|
19
|
+
/** Follows in-app links; links from a model are never rendered. */
|
|
20
|
+
onNavigate?: (to: string) => void;
|
|
21
|
+
className?: string;
|
|
22
|
+
}): React.JSX.Element;
|
|
23
|
+
/** Plain text of a message, for copy. Citations become [n]; entities their API name. */
|
|
24
|
+
export declare function blocksToText(blocks: MessageBlock[], sources?: MessageSource[]): string;
|
|
25
|
+
export { MessageContent, Inline };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { useTerms } from "../blocks/terms.js";
|
|
6
|
+
import { DegradedPanel } from "../blocks/degraded-panel.js";
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
|
|
8
|
+
import { Citation } from "./citation.js";
|
|
9
|
+
import { EntityChip } from "./entity-chip.js";
|
|
10
|
+
import { Card } from "./card-registry.js";
|
|
11
|
+
import { Payload } from "./payload.js";
|
|
12
|
+
import { ToolCallGroup } from "./tool-call.js";
|
|
13
|
+
const ContentContext = React.createContext({ sources: new Map() });
|
|
14
|
+
/**
|
|
15
|
+
* Inline nodes. Every string here may come from a model or the user's system, so it is
|
|
16
|
+
* only ever rendered as a React text child: no markdown parsing, no HTML.
|
|
17
|
+
*/
|
|
18
|
+
function Inline({ nodes, }) {
|
|
19
|
+
const terms = useTerms();
|
|
20
|
+
const ctx = React.useContext(ContentContext);
|
|
21
|
+
return (_jsx(_Fragment, { children: nodes.map((n, i) => {
|
|
22
|
+
switch (n.kind) {
|
|
23
|
+
case "text":
|
|
24
|
+
return _jsx(React.Fragment, { children: n.text }, i);
|
|
25
|
+
case "strong":
|
|
26
|
+
return _jsx("strong", { className: "font-semibold", children: n.text }, i);
|
|
27
|
+
case "em":
|
|
28
|
+
return _jsx("em", { children: n.text }, i);
|
|
29
|
+
case "code":
|
|
30
|
+
return (_jsx("code", { className: "rounded-xs bg-muted px-1 py-px font-mono text-[0.88em]", children: n.text }, i));
|
|
31
|
+
case "orgData":
|
|
32
|
+
return (_jsx("span", { title: n.source ? `From the ${terms.environment}: ${n.source}` : `From the ${terms.environment}`, className: "font-medium tabular-nums text-foreground", children: n.text }, i));
|
|
33
|
+
case "provisional":
|
|
34
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("button", { type: "button", "data-provisional": true, className: "inline rounded-xs text-left 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: " (not verified)" })] }) }), _jsx(TooltipContent, { children: n.reason ?? `The agent hasn’t verified this against the ${terms.environment}.` })] }, i));
|
|
35
|
+
case "citation":
|
|
36
|
+
return _jsx(Citation, { source: ctx.sources.get(n.sourceId) }, i);
|
|
37
|
+
case "entity":
|
|
38
|
+
return _jsx(EntityChip, { entity: n.entity, onOpen: ctx.onOpenEntity }, i);
|
|
39
|
+
case "link":
|
|
40
|
+
return (_jsx("a", { href: n.to, onClick: (e) => {
|
|
41
|
+
if (ctx.onNavigate) {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
ctx.onNavigate(n.to);
|
|
44
|
+
}
|
|
45
|
+
}, className: "font-medium text-accent-text underline-offset-2 hover:underline", children: n.text }, i));
|
|
46
|
+
}
|
|
47
|
+
}) }));
|
|
48
|
+
}
|
|
49
|
+
function Block({ block, }) {
|
|
50
|
+
switch (block.kind) {
|
|
51
|
+
case "paragraph":
|
|
52
|
+
return (_jsx("p", { children: _jsx(Inline, { nodes: block.nodes }) }));
|
|
53
|
+
case "list": {
|
|
54
|
+
const List = block.ordered ? "ol" : "ul";
|
|
55
|
+
return (_jsx(List, { className: cn("flex flex-col gap-1 pl-5", block.ordered ? "list-decimal" : "list-disc marker:text-faint-foreground"), children: block.items.map((item, i) => (_jsx("li", { className: "pl-0.5", children: _jsx(Inline, { nodes: item }) }, i))) }));
|
|
56
|
+
}
|
|
57
|
+
case "payload":
|
|
58
|
+
return _jsx(Payload, { language: block.language, text: block.text, title: block.title });
|
|
59
|
+
case "tools":
|
|
60
|
+
return _jsx(ToolCallGroup, { calls: block.calls });
|
|
61
|
+
case "degraded":
|
|
62
|
+
return _jsx(DegradedPanel, { reason: block.reason, missing: block.missing });
|
|
63
|
+
case "card":
|
|
64
|
+
return _jsx(Card, { name: block.card, props: block.props, summary: block.summary });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** 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-base leading-relaxed", className), children: blocks.map((b, i) => (_jsx(Block, { block: b }, i))) }) }));
|
|
71
|
+
}
|
|
72
|
+
/** Plain text of a message, for copy. Citations become [n]; entities their API name. */
|
|
73
|
+
export function blocksToText(blocks, sources = []) {
|
|
74
|
+
const marker = new Map(sources.map((s) => [s.id, s.marker]));
|
|
75
|
+
const inline = (nodes) => nodes
|
|
76
|
+
.map((n) => {
|
|
77
|
+
switch (n.kind) {
|
|
78
|
+
case "provisional":
|
|
79
|
+
return inline(n.nodes);
|
|
80
|
+
case "citation":
|
|
81
|
+
return `[${marker.get(n.sourceId) ?? "?"}]`;
|
|
82
|
+
case "entity":
|
|
83
|
+
return n.entity.name;
|
|
84
|
+
default:
|
|
85
|
+
return n.text;
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
.join("");
|
|
89
|
+
return blocks
|
|
90
|
+
.map((b) => {
|
|
91
|
+
switch (b.kind) {
|
|
92
|
+
case "paragraph":
|
|
93
|
+
return inline(b.nodes);
|
|
94
|
+
case "list":
|
|
95
|
+
return b.items.map((it, i) => `${b.ordered ? `${i + 1}.` : "-"} ${inline(it)}`).join("\n");
|
|
96
|
+
case "payload":
|
|
97
|
+
return b.text;
|
|
98
|
+
case "degraded":
|
|
99
|
+
return b.reason;
|
|
100
|
+
case "card":
|
|
101
|
+
return b.summary;
|
|
102
|
+
case "tools":
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.join("\n\n");
|
|
108
|
+
}
|
|
109
|
+
export { MessageContent, Inline };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { EntityRef, Message as MessageData } from "../lib/chat.js";
|
|
2
|
+
import { type Rating } from "./message-actions.js";
|
|
3
|
+
/**
|
|
4
|
+
* One message in a thread.
|
|
5
|
+
*
|
|
6
|
+
* - A person's message sits right, on the user surface.
|
|
7
|
+
* - An agent's sits left, under its avatar, name and role, with reasoning above
|
|
8
|
+
* the answer, sources and actions below. While streaming, a caret follows the
|
|
9
|
+
* last block; an interrupted reply keeps what arrived and says it stopped.
|
|
10
|
+
* - A person on the team (support, say) sits left like an agent, under a
|
|
11
|
+
* neutral avatar with their name and team, so they never read as the agent.
|
|
12
|
+
* - A system line is centred and quiet.
|
|
13
|
+
* - A reply a person checked (or edited) before it went out can say so in a
|
|
14
|
+
* caption under it, so a reader knows a person stood behind it.
|
|
15
|
+
*/
|
|
16
|
+
declare function Message({ message, onOpenEntity, onNavigate, onRetry, onContinue, rating, onRate, reviewedBy, className, }: {
|
|
17
|
+
/** The message: author, status, blocks, and optional reasoning, sources and attribution. */
|
|
18
|
+
message: MessageData;
|
|
19
|
+
/** Makes entity chips open their thing. */
|
|
20
|
+
onOpenEntity?: (entity: EntityRef) => void;
|
|
21
|
+
/** Follows in-app links. */
|
|
22
|
+
onNavigate?: (to: string) => void;
|
|
23
|
+
/** Shows Retry on a failed or finished reply. */
|
|
24
|
+
onRetry?: () => void;
|
|
25
|
+
/** Shows Continue on an interrupted reply. */
|
|
26
|
+
onContinue?: () => void;
|
|
27
|
+
/** The current rating of an agent reply. */
|
|
28
|
+
rating?: Rating;
|
|
29
|
+
/** Shows thumbs up and down under agent replies. */
|
|
30
|
+
onRate?: (rating: Rating) => void;
|
|
31
|
+
/** A person who checked this agent reply before it was sent: "Checked by Sam", or "Edited by Sam" when they changed it. */
|
|
32
|
+
reviewedBy?: {
|
|
33
|
+
name: string;
|
|
34
|
+
at?: string;
|
|
35
|
+
edited?: boolean;
|
|
36
|
+
};
|
|
37
|
+
className?: string;
|
|
38
|
+
}): import("react").JSX.Element;
|
|
39
|
+
export { Message };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { CircleStop, Info, UserCheck, UserRound } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { DegradedPanel } from "../blocks/degraded-panel.js";
|
|
6
|
+
import { RelativeTime } from "../blocks/relative-time.js";
|
|
7
|
+
import { AgentAvatar, useAgentRoles } from "./agent-avatar.js";
|
|
8
|
+
import { SourceList } from "./citation.js";
|
|
9
|
+
import { blocksToText, MessageContent } from "./message-content.js";
|
|
10
|
+
import { MessageActions } from "./message-actions.js";
|
|
11
|
+
import { Reasoning } from "./reasoning.js";
|
|
12
|
+
/**
|
|
13
|
+
* One message in a thread.
|
|
14
|
+
*
|
|
15
|
+
* - A person's message sits right, on the user surface.
|
|
16
|
+
* - An agent's sits left, under its avatar, name and role, with reasoning above
|
|
17
|
+
* the answer, sources and actions below. While streaming, a caret follows the
|
|
18
|
+
* last block; an interrupted reply keeps what arrived and says it stopped.
|
|
19
|
+
* - A person on the team (support, say) sits left like an agent, under a
|
|
20
|
+
* neutral avatar with their name and team, so they never read as the agent.
|
|
21
|
+
* - A system line is centred and quiet.
|
|
22
|
+
* - A reply a person checked (or edited) before it went out can say so in a
|
|
23
|
+
* caption under it, so a reader knows a person stood behind it.
|
|
24
|
+
*/
|
|
25
|
+
function Message({ message, onOpenEntity, onNavigate, onRetry, onContinue, rating, onRate, reviewedBy, className, }) {
|
|
26
|
+
const { author } = message;
|
|
27
|
+
const roleOf = useAgentRoles();
|
|
28
|
+
if (author.role === "system") {
|
|
29
|
+
return (_jsxs("div", { "data-slot": "message", "data-role": "system", className: cn("flex items-center justify-center gap-1.5 py-1 text-xs text-faint-foreground", className), children: [_jsx(Info, { "aria-hidden": true, className: "size-3" }), _jsx(MessageContent, { blocks: message.blocks, className: "text-xs leading-normal" })] }));
|
|
30
|
+
}
|
|
31
|
+
if (author.role === "user") {
|
|
32
|
+
return (_jsx("div", { "data-slot": "message", "data-role": "user", className: cn("flex justify-end pl-10 sm:pl-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-br-sm bg-surface-user px-3.5 py-2.5", children: _jsx(MessageContent, { blocks: message.blocks, onOpenEntity: onOpenEntity, onNavigate: onNavigate, className: "gap-2" }) }), _jsxs("span", { className: "px-1 text-2xs text-faint-foreground", children: [author.name ?? "You", message.createdAt && (_jsxs(_Fragment, { children: [" · ", _jsx(RelativeTime, { date: message.createdAt })] }))] })] }) }));
|
|
33
|
+
}
|
|
34
|
+
if (author.role === "person") {
|
|
35
|
+
return (_jsxs("article", { "data-slot": "message", "data-role": "person", "aria-label": author.team ? `${author.name}, ${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: [_jsxs("header", { className: "flex h-7 items-center gap-2", children: [_jsx("span", { className: "text-sm font-semibold", children: author.name }), author.team && _jsx("span", { className: "truncate text-xs text-muted-foreground", children: author.team }), message.createdAt && (_jsx("span", { className: "text-xs text-faint-foreground", children: _jsx(RelativeTime, { date: message.createdAt }) }))] }), _jsx(MessageContent, { blocks: message.blocks, sources: message.sources, onOpenEntity: onOpenEntity, onNavigate: onNavigate, className: "mt-1" })] })] }));
|
|
36
|
+
}
|
|
37
|
+
const { agent } = author;
|
|
38
|
+
const streaming = message.status === "streaming";
|
|
39
|
+
const thinking = streaming && message.blocks.length === 0;
|
|
40
|
+
const text = blocksToText(message.blocks, message.sources);
|
|
41
|
+
const meta = [message.attribution?.model, message.attribution?.cost].filter(Boolean).join(" · ");
|
|
42
|
+
return (_jsxs("article", { "data-slot": "message", "data-role": "agent", "data-status": message.status, "aria-busy": streaming, "aria-label": `${agent.name}, ${roleOf(agent.role).noun} agent`, 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: [_jsxs("header", { className: "flex h-7 items-center gap-2", children: [_jsx("span", { className: "text-sm font-semibold", children: agent.name }), agent.specialty && _jsx("span", { className: "truncate text-xs text-muted-foreground", children: agent.specialty }), message.createdAt && (_jsx("span", { className: "text-xs text-faint-foreground", children: _jsx(RelativeTime, { date: message.createdAt }) }))] }), _jsxs("div", { className: "mt-1 flex flex-col gap-3", 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 }), streaming && (_jsx("span", { "aria-hidden": true, className: "ml-0.5 inline-block h-[1.05em] w-[2px] translate-y-[0.18em] rounded-full bg-primary animate-caret" }))] })), 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" }), "Stopped before it finished. What arrived is kept.", onContinue && (_jsx("button", { type: "button", onClick: onContinue, className: "font-medium text-accent-text hover:underline", children: "Continue" }))] })), message.status === "failed" && (_jsx(DegradedPanel, { reason: "The agent couldn\u2019t finish this reply.", missing: "The rest of the answer", 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" }), reviewedBy.edited ? "Edited" : "Checked", " by ", reviewedBy.name, " before sending", reviewedBy.at && (_jsxs(_Fragment, { children: [" · ", _jsx(RelativeTime, { date: reviewedBy.at })] }))] })), !streaming && message.status !== "failed" && (_jsx(MessageActions, { text: text, onRetry: onRetry, rating: rating, onRate: onRate, meta: meta || undefined, className: "-ml-1.5 opacity-100 transition-opacity duration-150 sm:opacity-0 sm:group-hover/message:opacity-100 sm:group-focus-within/message:opacity-100" }))] })] })] }));
|
|
43
|
+
}
|
|
44
|
+
export { Message };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PayloadLanguage } from "../lib/chat.js";
|
|
2
|
+
/**
|
|
3
|
+
* A block of code or query text from the agent: a query it ran, code it
|
|
4
|
+
* 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.
|
|
6
|
+
*/
|
|
7
|
+
declare function Payload({ language, text, title, className, }: {
|
|
8
|
+
/** Shown as the label, e.g. json, sql, python. Never executed or highlighted as HTML. */
|
|
9
|
+
language: PayloadLanguage;
|
|
10
|
+
/** The code or query, shown as text. */
|
|
11
|
+
text: string;
|
|
12
|
+
/** What it is, e.g. "The query I ran". */
|
|
13
|
+
title?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
16
|
+
export { Payload };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Check, Copy, X } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { useCopy } from "../lib/use-copy.js";
|
|
6
|
+
const knownLabels = { json: "JSON", xml: "XML", sql: "SQL", yaml: "YAML", html: "HTML", css: "CSS", js: "JavaScript", ts: "TypeScript", tsx: "TSX", python: "Python", bash: "Shell", sh: "Shell", text: "Text" };
|
|
7
|
+
/** "json" → "JSON", "python" → "Python", short unknown ones upper-cased ("hcl" → "HCL"). */
|
|
8
|
+
const languageLabel = (lang) => knownLabels[lang] ?? (lang.length <= 4 ? lang.toUpperCase() : lang.charAt(0).toUpperCase() + lang.slice(1));
|
|
9
|
+
/**
|
|
10
|
+
* A block of code or query text from the agent: a query it ran, code it
|
|
11
|
+
* 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.
|
|
13
|
+
*/
|
|
14
|
+
function Payload({ language, text, title, className, }) {
|
|
15
|
+
const { state, copy } = useCopy();
|
|
16
|
+
return (_jsxs("figure", { "data-slot": "payload", 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) }), 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" }), " Copied"] })) : state === "failed" ? (_jsxs(_Fragment, { children: [_jsx(X, { className: "size-3 text-destructive-text" }), " Couldn\u2019t copy"] })) : (_jsxs(_Fragment, { children: [_jsx(Copy, { className: "size-3" }), " Copy"] })) })] }), _jsx("pre", { className: "max-h-80 overflow-auto px-3 py-2.5 font-mono text-xs leading-relaxed text-foreground", children: _jsx("code", { children: text }) })] }));
|
|
17
|
+
}
|
|
18
|
+
export { Payload };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PromptSuggestion } from "../lib/chat.js";
|
|
2
|
+
/**
|
|
3
|
+
* Prompts to start from. Choosing one puts it in the composer to edit, rather
|
|
4
|
+
* than sending it, because suggestions are model output too. Loading shows
|
|
5
|
+
* placeholders of the right shape; an empty list renders nothing.
|
|
6
|
+
*/
|
|
7
|
+
declare function PromptSuggestions({ suggestions, onChoose, label, loading, variant, className, }: {
|
|
8
|
+
/** What to offer. */
|
|
9
|
+
suggestions: PromptSuggestion[];
|
|
10
|
+
/** Called with the chosen one; put it in the composer to edit rather than sending it. */
|
|
11
|
+
onChoose: (suggestion: PromptSuggestion) => void;
|
|
12
|
+
/** The heading above the suggestions. */
|
|
13
|
+
label?: string;
|
|
14
|
+
/** Shows placeholders while suggestions load. */
|
|
15
|
+
loading?: boolean;
|
|
16
|
+
/** Compact chips, or larger cards for an empty thread. */
|
|
17
|
+
variant?: "chips" | "cards";
|
|
18
|
+
className?: string;
|
|
19
|
+
}): import("react").JSX.Element | null;
|
|
20
|
+
export { PromptSuggestions };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ArrowUpRight, Sparkles } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { Skeleton } from "../ui/skeleton.js";
|
|
6
|
+
/**
|
|
7
|
+
* Prompts to start from. Choosing one puts it in the composer to edit, rather
|
|
8
|
+
* than sending it, because suggestions are model output too. Loading shows
|
|
9
|
+
* placeholders of the right shape; an empty list renders nothing.
|
|
10
|
+
*/
|
|
11
|
+
function PromptSuggestions({ suggestions, onChoose, label = "Try asking", loading = false, variant = "chips", className, }) {
|
|
12
|
+
if (!loading && suggestions.length === 0)
|
|
13
|
+
return null;
|
|
14
|
+
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
|
+
? 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-left 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
|
+
}
|
|
18
|
+
export { PromptSuggestions };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { ReasoningEntry } from "../lib/chat.js";
|
|
3
|
+
/**
|
|
4
|
+
* The agent's stated reasoning above its answer. Each step says what it rests
|
|
5
|
+
* on: verified against evidence, believed, or assumed (with the assumption
|
|
6
|
+
* spelled out). Open while the agent is thinking; folds to a one-line summary
|
|
7
|
+
* when it answers.
|
|
8
|
+
*/
|
|
9
|
+
declare function Reasoning({ entries, thinking, thoughtFor, className, }: {
|
|
10
|
+
/** The steps, each verified (with evidence), believed or assumed. */
|
|
11
|
+
entries: ReasoningEntry[];
|
|
12
|
+
/** The agent is still thinking; opens the panel and shows progress. */
|
|
13
|
+
thinking?: boolean;
|
|
14
|
+
/** How long it thought, once known, e.g. "6s". */
|
|
15
|
+
thoughtFor?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
}): React.JSX.Element;
|
|
18
|
+
export { Reasoning };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Brain, ChevronRight } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
import { ShimmerLabel } from "../blocks/streaming-text.js";
|
|
7
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
|
|
8
|
+
const basisLabel = {
|
|
9
|
+
verified: "Verified",
|
|
10
|
+
believed: "Believed",
|
|
11
|
+
assumed: "Assumed",
|
|
12
|
+
};
|
|
13
|
+
const basisTone = {
|
|
14
|
+
verified: "bg-success-weak text-success-text",
|
|
15
|
+
believed: "bg-muted text-muted-foreground",
|
|
16
|
+
assumed: "bg-warning-weak text-warning-text",
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The agent's stated reasoning above its answer. Each step says what it rests
|
|
20
|
+
* on: verified against evidence, believed, or assumed (with the assumption
|
|
21
|
+
* spelled out). Open while the agent is thinking; folds to a one-line summary
|
|
22
|
+
* when it answers.
|
|
23
|
+
*/
|
|
24
|
+
function Reasoning({ entries, thinking = false, thoughtFor, className, }) {
|
|
25
|
+
const [open, setOpen] = React.useState(thinking);
|
|
26
|
+
React.useEffect(() => setOpen(thinking), [thinking]);
|
|
27
|
+
const assumed = entries.filter((e) => e.basis === "assumed").length;
|
|
28
|
+
const summary = thinking ? null : (_jsxs(_Fragment, { children: ["Reasoning", thoughtFor && _jsxs(_Fragment, { children: [" \u00B7 ", thoughtFor] }), " \u00B7 ", entries.length, " ", entries.length === 1 ? "step" : "steps", assumed > 0 && _jsxs("span", { className: "text-warning-text", children: [" \u00B7 ", assumed, " assumed"] })] }));
|
|
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 pr-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: "Thinking" }) : summary, _jsx(ChevronRight, { "aria-hidden": true, className: "size-3 transition-transform duration-200 ease-standard group-data-[state=open]/reasoning:rotate-90" })] }), _jsx(CollapsibleContent, { children: _jsx("ol", { className: "mt-2 ml-1.5 flex flex-col gap-3 border-l pl-4", children: entries.map((e) => (_jsxs("li", { className: "relative animate-rise-in", children: [_jsx("span", { "aria-hidden": true, className: "absolute top-1.5 -left-[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: basisLabel[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: ["Evidence: ", e.evidence] }) : _jsxs(_Fragment, { children: ["Assumption: ", e.assumption] }) })] }, e.id))) }) })] }));
|
|
30
|
+
}
|
|
31
|
+
export { Reasoning };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The scrolling message column. It follows new content while you're at the
|
|
4
|
+
* bottom and stops following the moment you scroll up to read, offering a
|
|
5
|
+
* "Jump to latest" button instead. Pass `empty` for the first-run state and
|
|
6
|
+
* the composer as `footer`.
|
|
7
|
+
*/
|
|
8
|
+
declare function Thread({ children, empty, footer, className, contentClassName, }: {
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
/** Shown before the first message. */
|
|
11
|
+
empty?: React.ReactNode;
|
|
12
|
+
/** Below the messages, usually the composer. */
|
|
13
|
+
footer?: React.ReactNode;
|
|
14
|
+
className?: string;
|
|
15
|
+
/** Classes for the message column. */
|
|
16
|
+
contentClassName?: string;
|
|
17
|
+
}): React.JSX.Element;
|
|
18
|
+
export { Thread };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ArrowDown } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
/**
|
|
7
|
+
* The scrolling message column. It follows new content while you're at the
|
|
8
|
+
* bottom and stops following the moment you scroll up to read, offering a
|
|
9
|
+
* "Jump to latest" button instead. Pass `empty` for the first-run state and
|
|
10
|
+
* the composer as `footer`.
|
|
11
|
+
*/
|
|
12
|
+
function Thread({ children, empty, footer, className, contentClassName, }) {
|
|
13
|
+
const scroller = React.useRef(null);
|
|
14
|
+
const content = React.useRef(null);
|
|
15
|
+
const pinnedRef = React.useRef(true);
|
|
16
|
+
const [pinned, setPinned] = React.useState(true);
|
|
17
|
+
const toBottom = React.useCallback((behavior = "auto") => {
|
|
18
|
+
const el = scroller.current;
|
|
19
|
+
if (el)
|
|
20
|
+
el.scrollTo({ top: el.scrollHeight, behavior });
|
|
21
|
+
}, []);
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
const el = content.current;
|
|
24
|
+
if (!el)
|
|
25
|
+
return;
|
|
26
|
+
const observer = new ResizeObserver(() => {
|
|
27
|
+
if (pinnedRef.current)
|
|
28
|
+
toBottom();
|
|
29
|
+
});
|
|
30
|
+
observer.observe(el);
|
|
31
|
+
return () => observer.disconnect();
|
|
32
|
+
}, [toBottom]);
|
|
33
|
+
// Scroll events arrive a frame late, after streaming content may already have
|
|
34
|
+
// grown the column, so distance from the bottom alone would read growth as
|
|
35
|
+
// the reader scrolling away. Only an actual move up unpins.
|
|
36
|
+
const lastTop = React.useRef(0);
|
|
37
|
+
const onScroll = () => {
|
|
38
|
+
const el = scroller.current;
|
|
39
|
+
if (!el)
|
|
40
|
+
return;
|
|
41
|
+
const top = el.scrollTop;
|
|
42
|
+
const atBottom = el.scrollHeight - top - el.clientHeight < 48;
|
|
43
|
+
if (atBottom) {
|
|
44
|
+
pinnedRef.current = true;
|
|
45
|
+
setPinned(true);
|
|
46
|
+
}
|
|
47
|
+
else if (top < lastTop.current - 2) {
|
|
48
|
+
pinnedRef.current = false;
|
|
49
|
+
setPinned(false);
|
|
50
|
+
}
|
|
51
|
+
lastTop.current = top;
|
|
52
|
+
};
|
|
53
|
+
const hasMessages = React.Children.count(children) > 0;
|
|
54
|
+
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, className: "h-full overflow-y-auto overscroll-contain", children: _jsx("div", { ref: content, role: "log", "aria-label": "Messages", className: cn("mx-auto flex w-full max-w-3xl flex-col gap-7 px-4 py-6", contentClassName), children: hasMessages ? children : empty }) }), !pinned && hasMessages && (_jsxs("button", { type: "button", onClick: () => {
|
|
55
|
+
pinnedRef.current = true;
|
|
56
|
+
setPinned(true);
|
|
57
|
+
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" }), " Jump to latest"] })), _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-3xl px-4 pb-4", children: footer })] }));
|
|
59
|
+
}
|
|
60
|
+
export { Thread };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { ToolCall as ToolCallData } from "../lib/chat.js";
|
|
3
|
+
/**
|
|
4
|
+
* One tool call as a compact row: what it did in plain words, its status, and
|
|
5
|
+
* how long it took. Expands to the input and output. A failed call opens by
|
|
6
|
+
* default and leads with its error.
|
|
7
|
+
*/
|
|
8
|
+
declare function ToolCall({ call, className, }: {
|
|
9
|
+
/** The call: tool name, plain-words title, status, input, output, duration or error. */
|
|
10
|
+
call: ToolCallData;
|
|
11
|
+
className?: string;
|
|
12
|
+
}): React.JSX.Element;
|
|
13
|
+
/** A run of tool calls, with a count when it's long enough to fold. */
|
|
14
|
+
declare function ToolCallGroup({ calls, className, }: {
|
|
15
|
+
/** Several calls, summarised as a group. */
|
|
16
|
+
calls: ToolCallData[];
|
|
17
|
+
className?: string;
|
|
18
|
+
}): React.JSX.Element;
|
|
19
|
+
export { ToolCall, ToolCallGroup };
|