@candor-ui/react 2.2.0 → 2.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/dist/adapters/use-ag-ui.d.ts +1 -1
- package/dist/adapters/use-candor-chat.d.ts +1 -1
- package/dist/adapters/use-claude-agent.d.ts +1 -1
- package/dist/blocks/copy-field.js +4 -1
- package/dist/blocks/degraded-panel.js +3 -1
- package/dist/blocks/locked.js +5 -1
- package/dist/blocks/status-dot.d.ts +14 -3
- package/dist/blocks/status-dot.js +17 -22
- package/dist/blocks/step-list.d.ts +2 -1
- package/dist/blocks/step-list.js +19 -27
- package/dist/blocks/streaming-text.js +3 -1
- package/dist/blocks/surface.d.ts +1 -1
- package/dist/chat/attachment-preview.js +5 -2
- package/dist/chat/card-registry.js +3 -1
- package/dist/chat/citation.js +2 -2
- package/dist/chat/composer.js +19 -53
- package/dist/chat/context-meter.js +4 -1
- package/dist/chat/entity-chip.js +1 -1
- package/dist/chat/folded-turns.js +3 -1
- package/dist/chat/message-actions.js +5 -2
- package/dist/chat/message.js +9 -5
- package/dist/chat/payload.js +1 -1
- package/dist/chat/permission-prompt.js +6 -1
- package/dist/chat/prompt-suggestions.js +4 -1
- package/dist/chat/reasoning.js +6 -1
- package/dist/chat/thread.js +19 -8
- package/dist/chat/tool-call.js +6 -2
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/lib/motion.d.ts +451 -0
- package/dist/lib/motion.js +350 -0
- package/dist/styles.css +543 -44
- package/dist/ui/accordion.d.ts +23 -0
- package/dist/ui/accordion.js +45 -0
- package/dist/ui/alert.d.ts +20 -0
- package/dist/ui/alert.js +51 -0
- package/dist/ui/button.d.ts +1 -1
- package/dist/ui/button.js +5 -3
- package/dist/ui/checkbox.d.ts +14 -0
- package/dist/ui/checkbox.js +32 -0
- package/dist/ui/collapsible.d.ts +3 -2
- package/dist/ui/collapsible.js +6 -3
- package/dist/ui/label.d.ts +10 -0
- package/dist/ui/label.js +16 -0
- package/dist/ui/popover.d.ts +1 -1
- package/dist/ui/popover.js +4 -2
- package/dist/ui/progress.d.ts +29 -0
- package/dist/ui/progress.js +27 -0
- package/dist/ui/skeleton.d.ts +1 -1
- package/dist/ui/skeleton.js +5 -2
- package/dist/ui/slider.d.ts +28 -0
- package/dist/ui/slider.js +41 -0
- package/dist/ui/switch.d.ts +14 -0
- package/dist/ui/switch.js +23 -0
- package/dist/ui/tooltip.d.ts +1 -1
- package/dist/ui/tooltip.js +4 -2
- package/package.json +1 -1
|
@@ -52,7 +52,7 @@ export declare function useAgUi(options: UseAgUiOptions): {
|
|
|
52
52
|
composer: {
|
|
53
53
|
value: string;
|
|
54
54
|
onChange: React.Dispatch<React.SetStateAction<string>>;
|
|
55
|
-
state: "streaming" | "
|
|
55
|
+
state: "streaming" | "sending" | "ready";
|
|
56
56
|
references: EntityRef[];
|
|
57
57
|
onReferencesChange: React.Dispatch<React.SetStateAction<EntityRef[]>>;
|
|
58
58
|
onStop: () => void | undefined;
|
|
@@ -31,7 +31,7 @@ export declare function useCandorChat(chat: UseChatHelpers<UIMessage>, options:
|
|
|
31
31
|
composer: {
|
|
32
32
|
value: string;
|
|
33
33
|
onChange: React.Dispatch<React.SetStateAction<string>>;
|
|
34
|
-
state: "streaming" | "
|
|
34
|
+
state: "streaming" | "sending" | "ready";
|
|
35
35
|
references: EntityRef[];
|
|
36
36
|
onReferencesChange: React.Dispatch<React.SetStateAction<EntityRef[]>>;
|
|
37
37
|
onStop: () => Promise<void>;
|
|
@@ -68,7 +68,7 @@ export declare function useClaudeAgent(options: UseClaudeAgentOptions): {
|
|
|
68
68
|
composer: {
|
|
69
69
|
value: string;
|
|
70
70
|
onChange: React.Dispatch<React.SetStateAction<string>>;
|
|
71
|
-
state: "streaming" | "
|
|
71
|
+
state: "streaming" | "sending" | "ready";
|
|
72
72
|
references: EntityRef[];
|
|
73
73
|
onReferencesChange: React.Dispatch<React.SetStateAction<EntityRef[]>>;
|
|
74
74
|
onStop: () => void;
|
|
@@ -4,6 +4,9 @@ import { Check, Copy, X } from "lucide-react";
|
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
5
|
import { useCopy } from "../lib/use-copy.js";
|
|
6
6
|
import { useStrings } from "./locale.js";
|
|
7
|
+
import { motion } from "../lib/motion.js";
|
|
8
|
+
/** The tick or cross after the clipboard write settles. */
|
|
9
|
+
const tickMotion = motion("copy-field", "tick", "pop");
|
|
7
10
|
/** CopyField's fixed words; override them under `copyField` in LocaleProvider's `strings`. */
|
|
8
11
|
export const copyFieldStrings = {
|
|
9
12
|
copy: "Copy",
|
|
@@ -19,6 +22,6 @@ export const copyFieldStrings = {
|
|
|
19
22
|
function CopyField({ label, value, className, }) {
|
|
20
23
|
const { state, copy } = useCopy();
|
|
21
24
|
const t = useStrings("copyField", copyFieldStrings);
|
|
22
|
-
return (_jsxs("div", { "data-slot": "copy-field", className: cn("flex flex-col gap-1", className), children: [_jsx("span", { className: "text-xs font-medium text-muted-foreground", children: label }), _jsxs("button", { type: "button", onClick: () => copy(value), className: "group/copy flex w-full items-center gap-2 rounded-lg border bg-sunken px-3 py-2 text-start outline-none
|
|
25
|
+
return (_jsxs("div", { "data-slot": "copy-field", className: cn("flex flex-col gap-1", className), children: [_jsx("span", { className: "text-xs font-medium text-muted-foreground", children: label }), _jsxs("button", { type: "button", onClick: () => copy(value), className: "group/copy flex w-full items-center gap-2 rounded-lg border bg-sunken px-3 py-2 text-start outline-none cd-tint hover:border-border-strong focus-visible:ring-[3px] focus-visible:ring-ring/80", children: [_jsx("code", { className: "min-w-0 flex-1 truncate font-mono text-xs", children: value }), _jsx("span", { "aria-live": "polite", "data-copy": state, className: "inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground group-hover/copy:text-foreground", children: state === "copied" ? (_jsxs(_Fragment, { children: [_jsx(Check, { style: tickMotion, className: "size-3 text-success-text cd-pop" }), " ", t("copied")] })) : state === "failed" ? (_jsxs(_Fragment, { children: [_jsx(X, { style: tickMotion, className: "size-3 text-destructive-text cd-pop" }), " ", t("failed")] })) : (_jsxs(_Fragment, { children: [_jsx(Copy, { className: "size-3" }), " ", t("copy")] })) })] })] }));
|
|
23
26
|
}
|
|
24
27
|
export { CopyField };
|
|
@@ -4,6 +4,8 @@ import { CircleAlert, RotateCw } from "lucide-react";
|
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
5
|
import { Button } from "../ui/button.js";
|
|
6
6
|
import { useStrings } from "./locale.js";
|
|
7
|
+
import { motion } from "../lib/motion.js";
|
|
8
|
+
const spinMotion = motion("degraded-panel", "busy", "live", { dur: 1000 });
|
|
7
9
|
/** DegradedPanel's fixed words; override them under `degradedPanel` in LocaleProvider's `strings`. */
|
|
8
10
|
export const degradedPanelStrings = {
|
|
9
11
|
missing: "Missing:",
|
|
@@ -17,6 +19,6 @@ export const degradedPanelStrings = {
|
|
|
17
19
|
*/
|
|
18
20
|
function DegradedPanel({ reason, missing, onRetry, retrying = false, variant = "block", className, }) {
|
|
19
21
|
const t = useStrings("degradedPanel", degradedPanelStrings);
|
|
20
|
-
return (_jsxs("div", { "data-slot": "degraded-panel", role: "status", className: cn("flex gap-2.5 text-sm", variant === "block" ? "items-start rounded-lg border border-warning/40 bg-warning-weak/60 p-3" : "items-center", className), children: [_jsx(CircleAlert, { "aria-hidden": true, className: "mt-0.5 size-4 shrink-0 text-warning-text" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "text-foreground", children: reason }), missing && _jsxs("p", { className: "mt-0.5 text-xs text-muted-foreground", children: [t("missing"), " ", missing] })] }), onRetry && (_jsxs(Button, { size: "xs", variant: "outline", onClick: onRetry, disabled: retrying, "aria-busy": retrying, className: "shrink-0", children: [_jsx(RotateCw, { className: cn(retrying && "
|
|
22
|
+
return (_jsxs("div", { "data-slot": "degraded-panel", role: "status", className: cn("flex gap-2.5 text-sm", variant === "block" ? "items-start rounded-lg border border-warning/40 bg-warning-weak/60 p-3" : "items-center", className), children: [_jsx(CircleAlert, { "aria-hidden": true, className: "mt-0.5 size-4 shrink-0 text-warning-text" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "text-foreground", children: reason }), missing && _jsxs("p", { className: "mt-0.5 text-xs text-muted-foreground", children: [t("missing"), " ", missing] })] }), onRetry && (_jsxs(Button, { size: "xs", variant: "outline", onClick: onRetry, disabled: retrying, "aria-busy": retrying, className: "shrink-0", children: [_jsx(RotateCw, { style: spinMotion, className: cn(retrying && "cd-live-spin") }), " ", retrying ? t("retrying") : t("retry")] }))] }));
|
|
21
23
|
}
|
|
22
24
|
export { DegradedPanel };
|
package/dist/blocks/locked.js
CHANGED
|
@@ -4,6 +4,10 @@ import * as React from "react";
|
|
|
4
4
|
import { LockKeyhole } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
6
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
|
|
7
|
+
import { motion } from "../lib/motion.js";
|
|
8
|
+
/** A press on the locked control: the reason is emphasised, and under expressive the control shakes. */
|
|
9
|
+
const refuseMotion = motion("locked", "refuse", "refuse");
|
|
10
|
+
const reasonMotion = motion("locked", "reason", "tint", { dur: 200 });
|
|
7
11
|
/**
|
|
8
12
|
* The honest-state rule: a control that cannot be used says why.
|
|
9
13
|
*
|
|
@@ -53,6 +57,6 @@ function Locked({ reason, inline = false, children, className, }) {
|
|
|
53
57
|
block(e);
|
|
54
58
|
},
|
|
55
59
|
});
|
|
56
|
-
return (_jsxs("span", { "data-slot": "locked", "data-pressed": held ? "" : undefined, className: cn("relative inline-flex flex-col items-start gap-1.5", shaking && "
|
|
60
|
+
return (_jsxs("span", { "data-slot": "locked", "data-pressed": held ? "" : undefined, style: refuseMotion, className: cn("relative inline-flex flex-col items-start gap-1.5", shaking && "cd-refuse", className), children: [inline ? (control) : (_jsxs(Tooltip, { open: shown || held, onOpenChange: setShown, children: [_jsx(TooltipTrigger, { asChild: true, children: control }), _jsx(TooltipContent, { children: reason })] })), _jsxs("span", { id: id, "data-slot": "locked-reason", style: reasonMotion, className: cn("items-center gap-1 text-xs text-muted-foreground cd-tint", inline ? "inline-flex" : "sr-only", held && "text-foreground"), children: [_jsx(LockKeyhole, { "aria-hidden": true, className: "size-3" }), reason] })] }));
|
|
57
61
|
}
|
|
58
62
|
export { Locked };
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
1
|
import { type RunStatus } from "../lib/status.js";
|
|
3
2
|
/**
|
|
4
3
|
* A small status mark. Running and waiting pulse, since they are the two
|
|
@@ -6,14 +5,26 @@ import { type RunStatus } from "../lib/status.js";
|
|
|
6
5
|
* or failed while on screen, the new dot pops into place (too small for a drawn
|
|
7
6
|
* mark); a dot that mounts finished is still. The label is always available
|
|
8
7
|
* to assistive tech; pass `showLabel` to print it too.
|
|
8
|
+
*
|
|
9
|
+
* Live means heard from: with `lastHeardAt`, a running dot pulses only while the
|
|
10
|
+
* run has been heard from within `staleAfter`, and is still (with its word)
|
|
11
|
+
* once it hasn't, or before the page has a clock. Without it the app isn't
|
|
12
|
+
* tracking freshness, and running pulses as before.
|
|
9
13
|
*/
|
|
10
|
-
declare function StatusDot({ status, showLabel, label, className, }: {
|
|
14
|
+
declare function StatusDot({ status, showLabel, label, lastHeardAt, staleAfter, className, }: {
|
|
11
15
|
/** Which status to show; running and waiting pulse. */
|
|
12
16
|
status: RunStatus;
|
|
13
17
|
/** Print the status name next to the dot. */
|
|
14
18
|
showLabel?: boolean | undefined;
|
|
15
19
|
/** Words to use instead of the status name. */
|
|
16
20
|
label?: string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* When the run was last heard from (an event, a heartbeat). A running dot pulses only while this is within
|
|
23
|
+
* `staleAfter`; past it, or when the time can't be read, the dot is still. Left out, the dot doesn't claim to know.
|
|
24
|
+
*/
|
|
25
|
+
lastHeardAt?: Date | string | undefined;
|
|
26
|
+
/** How long a running run may be silent, in milliseconds, before the dot stops pulsing. Two minutes by default. */
|
|
27
|
+
staleAfter?: number | undefined;
|
|
17
28
|
className?: string | undefined;
|
|
18
|
-
}):
|
|
29
|
+
}): import("react").JSX.Element;
|
|
19
30
|
export { StatusDot };
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import * as React from "react";
|
|
4
3
|
import { cn } from "../lib/utils.js";
|
|
5
|
-
import {
|
|
4
|
+
import { useNow } from "./relative-time.js";
|
|
5
|
+
import { motion, useSettled } from "../lib/motion.js";
|
|
6
|
+
import { runLiveness, STALE_AFTER, useStatusLabel } from "../lib/status.js";
|
|
7
|
+
/** The pulse while a run is heard from. */
|
|
8
|
+
const liveMotion = motion("status-dot", "live", "live");
|
|
9
|
+
/** The dot popping into its final colour when the run settles on screen. */
|
|
10
|
+
const settleMotion = motion("status-dot", "settle", "pop");
|
|
6
11
|
const dotColor = {
|
|
7
12
|
idle: "bg-faint-foreground/60",
|
|
8
13
|
queued: "bg-faint-foreground",
|
|
@@ -14,35 +19,25 @@ const dotColor = {
|
|
|
14
19
|
cancelled: "bg-faint-foreground/60",
|
|
15
20
|
skipped: "bg-faint-foreground/40",
|
|
16
21
|
};
|
|
17
|
-
const finished = (status) => status === "succeeded" || status === "failed";
|
|
18
|
-
const over = (status) => finished(status) || status === "cancelled" || status === "skipped";
|
|
19
|
-
/**
|
|
20
|
-
* How many times the status reached done or failed from an unfinished one while the dot was on screen; zero when it
|
|
21
|
-
* mounted finished, so nothing moves for a change the reader never saw.
|
|
22
|
-
*/
|
|
23
|
-
function useSettled(status) {
|
|
24
|
-
const [settled, setSettled] = React.useState(0);
|
|
25
|
-
const previous = React.useRef(status);
|
|
26
|
-
React.useLayoutEffect(() => {
|
|
27
|
-
const was = previous.current;
|
|
28
|
-
previous.current = status;
|
|
29
|
-
if (was !== status && !over(was) && finished(status))
|
|
30
|
-
setSettled((n) => n + 1);
|
|
31
|
-
}, [status]);
|
|
32
|
-
return settled;
|
|
33
|
-
}
|
|
34
22
|
/**
|
|
35
23
|
* A small status mark. Running and waiting pulse, since they are the two
|
|
36
24
|
* states a person may need to watch or act on. When one of them reaches done
|
|
37
25
|
* or failed while on screen, the new dot pops into place (too small for a drawn
|
|
38
26
|
* mark); a dot that mounts finished is still. The label is always available
|
|
39
27
|
* to assistive tech; pass `showLabel` to print it too.
|
|
28
|
+
*
|
|
29
|
+
* Live means heard from: with `lastHeardAt`, a running dot pulses only while the
|
|
30
|
+
* run has been heard from within `staleAfter`, and is still (with its word)
|
|
31
|
+
* once it hasn't, or before the page has a clock. Without it the app isn't
|
|
32
|
+
* tracking freshness, and running pulses as before.
|
|
40
33
|
*/
|
|
41
|
-
function StatusDot({ status, showLabel = false, label, className, }) {
|
|
34
|
+
function StatusDot({ status, showLabel = false, label, lastHeardAt, staleAfter = STALE_AFTER, className, }) {
|
|
42
35
|
const statusLabel = useStatusLabel();
|
|
43
36
|
const text = label ?? statusLabel(status);
|
|
44
|
-
const
|
|
37
|
+
const now = useNow();
|
|
38
|
+
const liveness = runLiveness({ status, lastHeardAt }, now, staleAfter);
|
|
39
|
+
const live = (status === "running" || status === "waiting") && liveness === "live";
|
|
45
40
|
const settled = useSettled(status);
|
|
46
|
-
return (_jsxs("span", { "data-slot": "status-dot", "data-status": status, "data-settled": settled > 0 ? "" : undefined, className: cn("inline-flex items-center gap-1.5 text-xs text-muted-foreground", className), children: [_jsx("span", { "aria-hidden": true, className: cn("size-2 shrink-0 rounded-full", dotColor[status], live && "
|
|
41
|
+
return (_jsxs("span", { "data-slot": "status-dot", "data-status": status, "data-settled": settled > 0 ? "" : undefined, "data-liveness": lastHeardAt === undefined ? undefined : liveness, className: cn("inline-flex items-center gap-1.5 text-xs text-muted-foreground", className), children: [_jsx("span", { "aria-hidden": true, style: live ? liveMotion : settleMotion, className: cn("size-2 shrink-0 rounded-full", dotColor[status], live && "cd-live-dot", settled > 0 && "cd-pop") }, settled), showLabel ? _jsx("span", { children: text }) : _jsx("span", { className: "sr-only", children: text })] }));
|
|
47
42
|
}
|
|
48
43
|
export { StatusDot };
|
|
@@ -18,7 +18,8 @@ declare function StepMark({ status, }: {
|
|
|
18
18
|
/**
|
|
19
19
|
* An ordered list of steps joined by a rail: an agent's plan, its reasoning,
|
|
20
20
|
* a run's stages. The rail fills to each step as it finishes, never ahead of
|
|
21
|
-
* it, and a step that finishes while on screen draws its mark.
|
|
21
|
+
* it, and a step that finishes while on screen draws its mark. A step added
|
|
22
|
+
* after the list first drew rises in; the steps it loaded with are still.
|
|
22
23
|
*/
|
|
23
24
|
declare function StepList({ steps, className, }: {
|
|
24
25
|
/** The steps in order, each with a status, and optional detail and meta. */
|
package/dist/blocks/step-list.js
CHANGED
|
@@ -3,33 +3,23 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { CircleSlash, Hand, LoaderCircle } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
|
+
import { motion, useArrived, useSettled } from "../lib/motion.js";
|
|
6
7
|
import { useStatusLabel } from "../lib/status.js";
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
function useSettled(status) {
|
|
15
|
-
const [settled, setSettled] = React.useState(0);
|
|
16
|
-
const previous = React.useRef(status);
|
|
17
|
-
React.useLayoutEffect(() => {
|
|
18
|
-
const was = previous.current;
|
|
19
|
-
previous.current = status;
|
|
20
|
-
if (was !== status && !over(was) && finished(status))
|
|
21
|
-
setSettled((n) => n + 1);
|
|
22
|
-
}, [status]);
|
|
23
|
-
return settled;
|
|
24
|
-
}
|
|
8
|
+
/** A step's tick or cross, drawn when its status settles on screen. */
|
|
9
|
+
const markMotion = motion("step-list", "mark", "mark");
|
|
10
|
+
/** The rail growing to a step that finished on screen. */
|
|
11
|
+
const railMotion = motion("step-list", "rail", "fill");
|
|
12
|
+
/** A step added after the list first drew. */
|
|
13
|
+
const arriveMotion = motion("step-list", "arrive", "arrive");
|
|
14
|
+
const workingMotion = motion("step-list", "working", "live", { dur: 1000 });
|
|
25
15
|
/** A tick, drawn along its path when `draw` is above zero (and again each time it changes). */
|
|
26
16
|
function Tick({ draw }) {
|
|
27
|
-
return (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, className: "size-3", children: _jsx("path", { d: "M20 6 9 17l-5-5", pathLength: 1, className: cn(draw > 0 && "
|
|
17
|
+
return (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, className: "size-3", children: _jsx("path", { d: "M20 6 9 17l-5-5", pathLength: 1, style: markMotion, className: cn(draw > 0 && "cd-mark") }) }, draw));
|
|
28
18
|
}
|
|
29
19
|
/** A cross, its two strokes drawn one after the other when `draw` is above zero. */
|
|
30
20
|
function Cross({ draw }) {
|
|
31
|
-
const drawn = draw > 0 && "
|
|
32
|
-
return (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, className: "size-3", children: [_jsx("path", { d: "M18 6 6 18", pathLength: 1, className: cn(drawn) }), _jsx("path", { d: "m6 6 12 12", pathLength: 1, className: cn(drawn, drawn && "[
|
|
21
|
+
const drawn = draw > 0 && "cd-mark";
|
|
22
|
+
return (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, className: "size-3", children: [_jsx("path", { d: "M18 6 6 18", pathLength: 1, style: markMotion, className: cn(drawn) }), _jsx("path", { d: "m6 6 12 12", pathLength: 1, style: markMotion, className: cn(drawn, drawn && "[--cd-mark-step:1]") })] }, draw));
|
|
33
23
|
}
|
|
34
24
|
/**
|
|
35
25
|
* A step's status as a mark. When the status reaches done or failed while the mark is on screen, the spinner
|
|
@@ -37,14 +27,14 @@ function Cross({ draw }) {
|
|
|
37
27
|
*/
|
|
38
28
|
function StepMark({ status, }) {
|
|
39
29
|
const settled = useSettled(status);
|
|
40
|
-
const base = "relative z-10 flex size-5 shrink-0 items-center justify-center rounded-full
|
|
30
|
+
const base = "relative z-10 flex size-5 shrink-0 items-center justify-center rounded-full cd-tint";
|
|
41
31
|
switch (status) {
|
|
42
32
|
case "succeeded":
|
|
43
33
|
return (_jsx("span", { className: cn(base, "bg-success text-on-success"), "data-settled": settled > 0 ? "" : undefined, children: _jsx(Tick, { draw: settled }) }));
|
|
44
34
|
case "failed":
|
|
45
35
|
return (_jsx("span", { className: cn(base, "bg-destructive text-on-destructive"), "data-settled": settled > 0 ? "" : undefined, children: _jsx(Cross, { draw: settled }) }));
|
|
46
36
|
case "running":
|
|
47
|
-
return (_jsx("span", { className: cn(base, "bg-accent-weak text-accent-text"), children: _jsx(LoaderCircle, { className: "size-3.5
|
|
37
|
+
return (_jsx("span", { className: cn(base, "bg-accent-weak text-accent-text"), children: _jsx(LoaderCircle, { style: workingMotion, className: "size-3.5 cd-live-spin", strokeWidth: 2.5 }) }));
|
|
48
38
|
case "waiting":
|
|
49
39
|
return (_jsx("span", { className: cn(base, "bg-warning-weak text-warning-text"), children: _jsx(Hand, { className: "size-3", strokeWidth: 2.5 }) }));
|
|
50
40
|
case "blocked":
|
|
@@ -56,18 +46,20 @@ function StepMark({ status, }) {
|
|
|
56
46
|
return (_jsx("span", { className: cn(base, "bg-background"), children: _jsx("span", { className: "size-2 rounded-full border-[1.5px] border-faint-foreground/70" }) }));
|
|
57
47
|
}
|
|
58
48
|
}
|
|
59
|
-
function StepRow({ step,
|
|
49
|
+
function StepRow({ step, arrived, last, }) {
|
|
60
50
|
const statusLabel = useStatusLabel();
|
|
61
51
|
const done = step.status === "succeeded";
|
|
62
52
|
const settled = useSettled(step.status);
|
|
63
|
-
return (_jsxs("li", { className: "relative flex gap-3 pb-4 last:pb-0
|
|
53
|
+
return (_jsxs("li", { style: arriveMotion, className: cn("relative flex gap-3 pb-4 last:pb-0", arrived && "cd-arrive"), children: [!last && (_jsx("span", { "aria-hidden": true, className: "absolute top-5 bottom-0 start-[9.5px] w-px bg-input", children: done && (_jsx("span", { "data-slot": "step-rail-fill", style: railMotion, className: cn("absolute inset-0 origin-top bg-success/50", settled > 0 && "cd-fill-grow [--cd-fill-from:1_0]") }, settled)) })), _jsx(StepMark, { status: step.status }), _jsxs("div", { className: "min-w-0 flex-1 pt-px", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-3", children: [_jsxs("p", { className: cn("text-sm font-medium", step.status === "idle" && "text-muted-foreground", (step.status === "skipped" || step.status === "cancelled") && "text-faint-foreground line-through"), children: [step.title, _jsxs("span", { className: "sr-only", children: [" \u2014 ", statusLabel(step.status)] })] }), step.meta && _jsx("span", { className: "shrink-0 text-xs tabular-nums text-faint-foreground", children: step.meta })] }), step.detail && _jsx("div", { className: "mt-1 text-[0.8125rem] text-muted-foreground", children: step.detail })] })] }));
|
|
64
54
|
}
|
|
65
55
|
/**
|
|
66
56
|
* An ordered list of steps joined by a rail: an agent's plan, its reasoning,
|
|
67
57
|
* a run's stages. The rail fills to each step as it finishes, never ahead of
|
|
68
|
-
* it, and a step that finishes while on screen draws its mark.
|
|
58
|
+
* it, and a step that finishes while on screen draws its mark. A step added
|
|
59
|
+
* after the list first drew rises in; the steps it loaded with are still.
|
|
69
60
|
*/
|
|
70
61
|
function StepList({ steps, className, }) {
|
|
71
|
-
|
|
62
|
+
const arrived = useArrived(React.useMemo(() => steps.map((s) => s.id), [steps]));
|
|
63
|
+
return (_jsx("ol", { "data-slot": "step-list", className: cn("flex flex-col", className), children: steps.map((step, i) => (_jsx(StepRow, { step: step, arrived: arrived(step.id), last: i === steps.length - 1 }, step.id))) }));
|
|
72
64
|
}
|
|
73
65
|
export { StepList, StepMark };
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "../lib/utils.js";
|
|
3
|
+
import { motion } from "../lib/motion.js";
|
|
4
|
+
const caretMotion = motion("streaming-text", "caret", "live", { dur: 1000 });
|
|
3
5
|
/**
|
|
4
6
|
* Text that is still arriving. Shows a caret while `streaming`, and announces
|
|
5
7
|
* politely so screen readers hear the finished text rather than every token.
|
|
6
8
|
*/
|
|
7
9
|
function StreamingText({ text, streaming = false, className, }) {
|
|
8
|
-
return (_jsxs("div", { "data-slot": "streaming-text", "aria-live": "polite", "aria-busy": streaming, className: cn("whitespace-pre-wrap text-[0.9375rem] leading-relaxed text-foreground", className), children: [text, 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
|
|
10
|
+
return (_jsxs("div", { "data-slot": "streaming-text", "aria-live": "polite", "aria-busy": streaming, className: cn("whitespace-pre-wrap text-[0.9375rem] leading-relaxed text-foreground", className), children: [text, streaming && (_jsx("span", { "aria-hidden": true, style: caretMotion, className: "ms-0.5 inline-block h-[1.05em] w-[2px] translate-y-[0.18em] rounded-full bg-primary cd-live-caret" }))] }));
|
|
9
11
|
}
|
|
10
12
|
/** A label that shimmers while an agent is working ("Thinking", "Reading the docs"). */
|
|
11
13
|
function ShimmerLabel({ children, className, }) {
|
package/dist/blocks/surface.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { type VariantProps } from "class-variance-authority";
|
|
3
3
|
declare const surfaceVariants: (props?: ({
|
|
4
|
-
tone?: "plain" | "
|
|
4
|
+
tone?: "plain" | "subtle" | "muted" | "raised" | "floating" | "accent" | null | undefined;
|
|
5
5
|
radius?: "md" | "lg" | "xl" | null | undefined;
|
|
6
6
|
pad?: "none" | "md" | "sm" | "lg" | null | undefined;
|
|
7
7
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
@@ -4,6 +4,9 @@ import * as React from "react";
|
|
|
4
4
|
import { File, FileArchive, FileCode, FileImage, FileSpreadsheet, FileText, Music, Paperclip, Video, X } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
6
|
import { useFormat, useStrings } from "../blocks/locale.js";
|
|
7
|
+
import { motion } from "../lib/motion.js";
|
|
8
|
+
/** The upload bar moving between measured byte counts: 2.2's 300 ms. */
|
|
9
|
+
const progressMotion = motion("attachment-preview", "progress", "fill", { dur: 300 });
|
|
7
10
|
/** AttachmentPreview's fixed words; override them under `attachmentPreview` in LocaleProvider's `strings`. */
|
|
8
11
|
export const attachmentPreviewStrings = {
|
|
9
12
|
remove: "Remove {name}",
|
|
@@ -58,7 +61,7 @@ function AttachmentChip({ attachment: a, onRemove, className, }) {
|
|
|
58
61
|
const t = useStrings("attachmentPreview", attachmentPreviewStrings);
|
|
59
62
|
const fmt = useFormat();
|
|
60
63
|
const thumb = thumbnail(a);
|
|
61
|
-
return (_jsxs("span", { "data-slot": "attachment-chip", className: cn("relative inline-flex max-w-56 items-center gap-1.5 overflow-hidden rounded-md border bg-card py-1 pe-1 ps-2 text-xs", a.error && "border-destructive/50", className), children: [thumb ? _jsx("img", { src: thumb, alt: "", className: "size-4 shrink-0 rounded-xs object-cover" }) : _jsx(Paperclip, { "aria-hidden": true, className: "size-3 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: a.name }), _jsx("span", { className: cn("shrink-0 tabular-nums", a.error ? "text-destructive-text" : "text-faint-foreground"), children: a.error ? a.error : a.progress !== undefined ? fmt.number(Math.round(a.progress * 100) / 100, { style: "percent" }) : formatFileSize(a.size, fmt.number, t) }), onRemove && (_jsx("button", { type: "button", "aria-label": t("remove", { name: a.name }), onClick: () => onRemove(a.id), className: "relative after:absolute after:-inset-1 rounded-sm p-0.5 text-faint-foreground hover:bg-accent hover:text-foreground", children: _jsx(X, { className: "size-3" }) })), a.progress !== undefined && !a.error && _jsx("span", { "aria-hidden": true, className: "absolute bottom-0 start-0 h-0.5 bg-primary
|
|
64
|
+
return (_jsxs("span", { "data-slot": "attachment-chip", className: cn("relative inline-flex max-w-56 items-center gap-1.5 overflow-hidden rounded-md border bg-card py-1 pe-1 ps-2 text-xs", a.error && "border-destructive/50", className), children: [thumb ? _jsx("img", { src: thumb, alt: "", className: "size-4 shrink-0 rounded-xs object-cover" }) : _jsx(Paperclip, { "aria-hidden": true, className: "size-3 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: a.name }), _jsx("span", { className: cn("shrink-0 tabular-nums", a.error ? "text-destructive-text" : "text-faint-foreground"), children: a.error ? a.error : a.progress !== undefined ? fmt.number(Math.round(a.progress * 100) / 100, { style: "percent" }) : formatFileSize(a.size, fmt.number, t) }), onRemove && (_jsx("button", { type: "button", "aria-label": t("remove", { name: a.name }), onClick: () => onRemove(a.id), className: "relative after:absolute after:-inset-1 rounded-sm p-0.5 text-faint-foreground hover:bg-accent hover:text-foreground", children: _jsx(X, { className: "size-3" }) })), a.progress !== undefined && !a.error && _jsx("span", { "aria-hidden": true, className: "absolute bottom-0 start-0 h-0.5 bg-primary cd-fill", style: { ...progressMotion, width: `${a.progress * 100}%` } })] }));
|
|
62
65
|
}
|
|
63
66
|
/**
|
|
64
67
|
* A sent file, as a tile in a message: an image preview when the app supplied one, else an icon for its type, with
|
|
@@ -72,7 +75,7 @@ function AttachmentPreview({ attachment: a, onOpen, className, }) {
|
|
|
72
75
|
const size = formatFileSize(a.size, fmt.number, t);
|
|
73
76
|
const body = (_jsxs(_Fragment, { children: [thumb ? (_jsx("img", { src: thumb, alt: "", className: "h-24 w-full rounded-md bg-muted object-cover" })) : (_jsx("span", { "aria-hidden": true, className: "flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: _jsx(FileGlyph, { attachment: a }) })), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-sm", children: a.name }), (size || a.error) && _jsx("span", { className: cn("block text-xs tabular-nums", a.error ? "text-destructive-text" : "text-faint-foreground"), children: a.error ?? size })] })] }));
|
|
74
77
|
const frame = cn("flex w-56 max-w-full gap-2.5 rounded-lg border bg-card p-2 text-start", thumb ? "flex-col" : "items-center", className);
|
|
75
|
-
return onOpen ? (_jsx("button", { type: "button", "data-slot": "attachment-preview", "aria-label": t("open", { name: a.name }), onClick: () => onOpen(a), className: cn(frame, "outline-none
|
|
78
|
+
return onOpen ? (_jsx("button", { type: "button", "data-slot": "attachment-preview", "aria-label": t("open", { name: a.name }), onClick: () => onOpen(a), className: cn(frame, "outline-none cd-tint hover:border-border-strong focus-visible:ring-[3px] focus-visible:ring-ring/80"), children: body })) : (_jsx("div", { "data-slot": "attachment-preview", className: frame, children: body }));
|
|
76
79
|
}
|
|
77
80
|
/** Several sent files, as a wrapping row of tiles, announced as a list. */
|
|
78
81
|
function AttachmentList({ attachments, onOpen, className, }) {
|
|
@@ -5,6 +5,8 @@ import { ChevronRight, LayoutTemplate } from "lucide-react";
|
|
|
5
5
|
import { DegradedPanel } from "../blocks/degraded-panel.js";
|
|
6
6
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
|
|
7
7
|
import { useStrings } from "../blocks/locale.js";
|
|
8
|
+
import { motion } from "../lib/motion.js";
|
|
9
|
+
const turnMotion = motion("card-registry", "open", "reveal");
|
|
8
10
|
/** CardRegistry's fixed words; override them under `cardRegistry` in LocaleProvider's `strings`. */
|
|
9
11
|
export const cardRegistryStrings = {
|
|
10
12
|
showAsText: "Show it as text",
|
|
@@ -32,7 +34,7 @@ class CardBoundary extends React.Component {
|
|
|
32
34
|
}
|
|
33
35
|
function Summary({ text, }) {
|
|
34
36
|
const t = useStrings("cardRegistry", cardRegistryStrings);
|
|
35
|
-
return (_jsxs(Collapsible, { className: "group/summary", children: [_jsxs(CollapsibleTrigger, { className: "relative after:absolute after:inset-x-0 after:-inset-y-1 inline-flex items-center gap-1 text-xs text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/80 rounded-sm", children: [_jsx(ChevronRight, { "aria-hidden": true, className: "rtl:-scale-x-100 size-3
|
|
37
|
+
return (_jsxs(Collapsible, { className: "group/summary", children: [_jsxs(CollapsibleTrigger, { className: "relative after:absolute after:inset-x-0 after:-inset-y-1 inline-flex items-center gap-1 text-xs text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/80 rounded-sm", children: [_jsx(ChevronRight, { "aria-hidden": true, style: turnMotion, className: "rtl:-scale-x-100 size-3 cd-reveal-turn group-data-[state=open]/summary:[transform:rotate(90deg)]" }), " ", t("showAsText")] }), _jsx(CollapsibleContent, { style: turnMotion, children: _jsx("p", { className: "mt-1.5 rounded-md bg-sunken px-3 py-2 text-sm whitespace-pre-wrap", children: text }) })] }));
|
|
36
38
|
}
|
|
37
39
|
/**
|
|
38
40
|
* Renders one card from the registry. Three honest fallbacks, each with the
|
package/dist/chat/citation.js
CHANGED
|
@@ -54,7 +54,7 @@ function Citation({ source, marker, }) {
|
|
|
54
54
|
if (!source) {
|
|
55
55
|
return _jsxs("sup", { className: "text-faint-foreground", children: ["[", marker ?? "?", "]"] });
|
|
56
56
|
}
|
|
57
|
-
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": t("sourceLabel", { marker: source.marker, label: source.label }), className: cn("mx-0.5 inline-flex h-4 min-w-4 -translate-y-px items-center justify-center rounded-sm px-1 align-middle text-2xs font-semibold tabular-nums
|
|
57
|
+
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": t("sourceLabel", { marker: source.marker, label: source.label }), className: cn("mx-0.5 inline-flex h-4 min-w-4 -translate-y-px items-center justify-center rounded-sm px-1 align-middle text-2xs font-semibold tabular-nums cd-tint outline-none focus-visible:ring-[3px] focus-visible:ring-ring/80", source.provisional
|
|
58
58
|
? "border border-dashed border-provisional/60 text-provisional hover:bg-muted"
|
|
59
59
|
: "bg-accent-weak text-accent-text hover:bg-primary hover:text-primary-foreground"), children: source.marker }) }), _jsx(PopoverContent, { align: "start", className: "w-80 max-w-[calc(100vw-2rem)] p-3", children: _jsx(SourceDetail, { source: source }) })] }));
|
|
60
60
|
}
|
|
@@ -67,7 +67,7 @@ function SourceList({ sources, className, }) {
|
|
|
67
67
|
.sort((a, b) => a.marker - b.marker)
|
|
68
68
|
.map((s) => {
|
|
69
69
|
const { Icon } = kindOf(s.kind, t);
|
|
70
|
-
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: cn("relative after:absolute after:inset-x-0 after:-inset-y-0.5 inline-flex max-w-56 items-center gap-1.5 rounded-md border bg-card py-0.5 pe-2 ps-1 text-xs text-muted-foreground
|
|
70
|
+
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: cn("relative after:absolute after:inset-x-0 after:-inset-y-0.5 inline-flex max-w-56 items-center gap-1.5 rounded-md border bg-card py-0.5 pe-2 ps-1 text-xs text-muted-foreground cd-tint outline-none hover:border-border-strong hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/80", s.provisional && "border-dashed"), children: [_jsx("span", { className: "flex h-4 min-w-4 items-center justify-center rounded-sm bg-muted px-1 text-2xs font-semibold tabular-nums", children: s.marker }), _jsx(Icon, { "aria-hidden": true, className: "size-3 shrink-0" }), _jsx("span", { className: "truncate", children: s.label })] }) }), _jsx(PopoverContent, { align: "start", className: "w-80 max-w-[calc(100vw-2rem)] p-3", children: _jsx(SourceDetail, { source: s }) })] }, s.id));
|
|
71
71
|
})] }));
|
|
72
72
|
}
|
|
73
73
|
export { Citation, SourceList, SourceDetail };
|
package/dist/chat/composer.js
CHANGED
|
@@ -7,6 +7,14 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
|
|
|
7
7
|
import { EntityChip, useEntityKinds } from "./entity-chip.js";
|
|
8
8
|
import { AttachmentChip } from "./attachment-preview.js";
|
|
9
9
|
import { useStrings } from "../blocks/locale.js";
|
|
10
|
+
import { motion, useArrived, useLeaving } from "../lib/motion.js";
|
|
11
|
+
/** A chip added after the first draw pops in; a removed one pops out as a ghost. */
|
|
12
|
+
const chipMotion = motion("composer", "chip", "pop");
|
|
13
|
+
/** The mention and command menu: 2.2's fade and 4 px rise. */
|
|
14
|
+
const menuMotion = motion("composer", "menu", "overlay", { variant: "slide" });
|
|
15
|
+
/** Send and Stop acknowledge the press: 2.2's scale of 0.95. */
|
|
16
|
+
const sendMotion = motion("composer", "send", "press", { amp: "0.9" });
|
|
17
|
+
const sendingMotion = motion("composer", "sending", "live", { dur: 1000 });
|
|
10
18
|
/** Composer's fixed words; override them under `composer` in LocaleProvider's `strings`. */
|
|
11
19
|
export const composerStrings = {
|
|
12
20
|
placeholder: "Ask a question, or describe a change",
|
|
@@ -42,55 +50,9 @@ const groupOf = (item, commandsLabel = "Commands") => (item.type === "command" ?
|
|
|
42
50
|
const hasFiles = (e) => Array.from(e.dataTransfer?.types ?? []).includes("Files");
|
|
43
51
|
const referenceKey = (r) => `${r.kind}:${r.name}`;
|
|
44
52
|
const attachmentKey = (a) => a.id;
|
|
45
|
-
/**
|
|
46
|
-
* Which chips were added after the composer first drew (they pop in) and which were just removed (they pop out as
|
|
47
|
-
* inert ghosts, then go). Chips present at first render are drawn still: nothing moves for a state the reader
|
|
48
|
-
* already had. The ghosts are cleared by their animation's end, or after 400ms when nothing animates.
|
|
49
|
-
*/
|
|
50
|
-
function useChipMotion(items, keyOf) {
|
|
51
|
-
const [entered, setEntered] = React.useState(() => new Set());
|
|
52
|
-
const [leaving, setLeaving] = React.useState([]);
|
|
53
|
-
const previous = React.useRef(null);
|
|
54
|
-
React.useLayoutEffect(() => {
|
|
55
|
-
const before = previous.current;
|
|
56
|
-
previous.current = items;
|
|
57
|
-
if (!before)
|
|
58
|
-
return;
|
|
59
|
-
const now = new Set(items.map(keyOf));
|
|
60
|
-
const was = new Set(before.map(keyOf));
|
|
61
|
-
const added = items.filter((item) => !was.has(keyOf(item))).map(keyOf);
|
|
62
|
-
const gone = before.filter((item) => !now.has(keyOf(item)));
|
|
63
|
-
if (added.length || gone.length) {
|
|
64
|
-
setEntered((set) => {
|
|
65
|
-
const next = new Set(set);
|
|
66
|
-
for (const item of gone)
|
|
67
|
-
next.delete(keyOf(item));
|
|
68
|
-
for (const key of added)
|
|
69
|
-
next.add(key);
|
|
70
|
-
return next;
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
// A chip added again while its ghost is still leaving takes the ghost's place: the ghost goes at once.
|
|
74
|
-
if (gone.length || added.length) {
|
|
75
|
-
setLeaving((list) => {
|
|
76
|
-
const kept = list.filter((item) => !now.has(keyOf(item)));
|
|
77
|
-
return kept.length === list.length && gone.length === 0 ? list : [...kept, ...gone];
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}, [items, keyOf]);
|
|
81
|
-
React.useEffect(() => {
|
|
82
|
-
if (leaving.length === 0)
|
|
83
|
-
return;
|
|
84
|
-
const timer = window.setTimeout(() => setLeaving([]), 400);
|
|
85
|
-
return () => window.clearTimeout(timer);
|
|
86
|
-
}, [leaving]);
|
|
87
|
-
const entering = (item) => entered.has(keyOf(item));
|
|
88
|
-
const left = (item) => setLeaving((list) => list.filter((x) => x !== item));
|
|
89
|
-
return { entering, leaving, left };
|
|
90
|
-
}
|
|
91
53
|
/** A removed chip, kept for the length of its pop-out and hidden from assistive tech. */
|
|
92
54
|
function LeavingChip({ onDone, children }) {
|
|
93
|
-
return (_jsx("span", { "aria-hidden": true, "data-leaving": true, className: "pointer-events-none inline-flex max-w-full
|
|
55
|
+
return (_jsx("span", { "aria-hidden": true, "data-leaving": true, style: chipMotion, className: "pointer-events-none inline-flex max-w-full cd-pop-out", onAnimationEnd: onDone, children: children }));
|
|
94
56
|
}
|
|
95
57
|
/**
|
|
96
58
|
* Where a person asks. Mentions (@) attach things in the user's system as chips the agent
|
|
@@ -118,8 +80,12 @@ function Composer({ value, onChange, onSubmit, onStop, state = "ready", scope, o
|
|
|
118
80
|
const fileInput = React.useRef(null);
|
|
119
81
|
const [trigger, setTrigger] = React.useState(null);
|
|
120
82
|
const [active, setActive] = React.useState(0);
|
|
121
|
-
|
|
122
|
-
|
|
83
|
+
// Chips added after the composer first drew pop in; chips just removed pop out as inert ghosts, then go. Chips there
|
|
84
|
+
// at first render are drawn still: nothing moves for a state the reader already had.
|
|
85
|
+
const referenceArrived = useArrived(React.useMemo(() => references.map(referenceKey), [references]));
|
|
86
|
+
const attachmentArrived = useArrived(React.useMemo(() => attachments.map(attachmentKey), [attachments]));
|
|
87
|
+
const referenceMotion = useLeaving(references, referenceKey);
|
|
88
|
+
const attachmentMotion = useLeaving(attachments, attachmentKey);
|
|
123
89
|
const held = typeof state === "object" && state.kind === "held" ? state : null;
|
|
124
90
|
const locked = typeof state === "object" && state.kind !== "held" ? state : null;
|
|
125
91
|
const canAttach = Boolean(onAttach) && !locked;
|
|
@@ -243,7 +209,7 @@ function Composer({ value, onChange, onSubmit, onStop, state = "ready", scope, o
|
|
|
243
209
|
const files = Array.from(e.dataTransfer.files);
|
|
244
210
|
if (files.length)
|
|
245
211
|
onAttach(files);
|
|
246
|
-
}, children: [dragDepth > 0 && (_jsx("div", { "aria-hidden": true, className: "pointer-events-none absolute inset-0 z-30 flex items-center justify-center rounded-2xl border-2 border-dashed border-primary bg-card/90 text-sm font-medium text-accent-text", children: t("dropFiles") })), menuOpen && (_jsx("div", { id: listId, role: "listbox", "aria-label": trigger?.char === "/" ? t("commands") : t("mentions"), className: "absolute inset-x-0 bottom-full z-20 mb-2 max-h-72 overflow-y-auto rounded-xl border bg-popover p-1 shadow-float
|
|
212
|
+
}, children: [dragDepth > 0 && (_jsx("div", { "aria-hidden": true, className: "pointer-events-none absolute inset-0 z-30 flex items-center justify-center rounded-2xl border-2 border-dashed border-primary bg-card/90 text-sm font-medium text-accent-text", children: t("dropFiles") })), menuOpen && (_jsx("div", { id: listId, role: "listbox", "aria-label": trigger?.char === "/" ? t("commands") : t("mentions"), style: menuMotion, className: "absolute inset-x-0 bottom-full z-20 mb-2 max-h-72 overflow-y-auto rounded-xl border bg-popover p-1 shadow-float [--cd-overlay-distance:0.25rem] [--cd-side-y:1] cd-overlay", children: items.map((item, i) => {
|
|
247
213
|
const group = groupOf(item, t("commands"));
|
|
248
214
|
const prev = items[i - 1];
|
|
249
215
|
const header = !prev || groupOf(prev, t("commands")) !== group ? group : null;
|
|
@@ -252,7 +218,7 @@ function Composer({ value, onChange, onSubmit, onStop, state = "ready", scope, o
|
|
|
252
218
|
e.preventDefault();
|
|
253
219
|
choose(item);
|
|
254
220
|
}, onMouseMove: () => setActive(i), className: "flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-1.5 text-sm aria-selected:bg-accent", children: item.type === "command" ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "font-mono text-xs text-accent-text", children: ["/", item.command.name] }), item.command.args && _jsx("span", { className: "font-mono text-xs text-faint-foreground", children: item.command.args }), _jsx("span", { className: "ms-auto truncate text-xs text-muted-foreground", children: item.command.description })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex size-6 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: Icon ? _jsx(Icon, { className: "size-3.5" }) : _jsx("span", { className: "text-xs", children: "@" }) }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate", children: item.option.label }), (item.option.description ?? item.option.entity?.name) !== item.option.label && (item.option.description ?? item.option.entity?.name) && (_jsx("span", { className: "block truncate font-mono text-2xs text-faint-foreground", children: item.option.description ?? item.option.entity?.name }))] })] })) })] }, item.type === "command" ? item.command.id : item.option.id));
|
|
255
|
-
}) })), _jsxs("div", { className: cn("rounded-2xl border bg-card shadow-raised
|
|
221
|
+
}) })), _jsxs("div", { className: cn("rounded-2xl border bg-card shadow-raised cd-tint focus-within:border-border-strong focus-within:shadow-float", locked && "bg-muted shadow-none"), children: [(references.length > 0 || attachments.length > 0 || referenceMotion.leaving.length > 0 || attachmentMotion.leaving.length > 0) && (_jsxs("div", { className: "flex flex-wrap gap-1.5 px-3 pt-3", children: [references.map((r) => (_jsx("span", { "data-slot": "composer-chip", style: chipMotion, className: cn("inline-flex max-w-full min-w-0", referenceArrived(referenceKey(r)) && "cd-pop"), children: _jsx(EntityChip, { entity: r, onRemove: (e) => onReferencesChange?.(references.filter((x) => x !== e)) }) }, referenceKey(r)))), referenceMotion.leaving.map((r) => (_jsx(LeavingChip, { onDone: () => referenceMotion.left(r), children: _jsx(EntityChip, { entity: r }) }, `leaving:${referenceKey(r)}`))), attachments.map((a) => (_jsx("span", { "data-slot": "composer-chip", style: chipMotion, className: cn("inline-flex max-w-full min-w-0", attachmentArrived(a.id) && "cd-pop"), children: _jsx(AttachmentChip, { attachment: a, onRemove: onRemoveAttachment }) }, a.id))), attachmentMotion.leaving.map((a) => (_jsx(LeavingChip, { onDone: () => attachmentMotion.left(a), children: _jsx(AttachmentChip, { attachment: a }) }, `leaving:${a.id}`)))] })), _jsx("label", { htmlFor: id, className: "sr-only", children: label }), _jsx("textarea", { ref: textarea, id: id, rows: 1, value: value, disabled: Boolean(locked), placeholder: locked ? undefined : placeholder, onChange: (e) => {
|
|
256
222
|
onChange(e.target.value);
|
|
257
223
|
requestAnimationFrame(syncTrigger);
|
|
258
224
|
}, onKeyDown: onKeyDown, onKeyUp: (e) => {
|
|
@@ -271,8 +237,8 @@ function Composer({ value, onChange, onSubmit, onStop, state = "ready", scope, o
|
|
|
271
237
|
if (files.length)
|
|
272
238
|
onAttach(files);
|
|
273
239
|
e.target.value = "";
|
|
274
|
-
} }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": t("attachFiles"), disabled: Boolean(locked), onClick: () => fileInput.current?.click(), className: "flex size-8 items-center justify-center rounded-lg text-muted-foreground
|
|
240
|
+
} }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": t("attachFiles"), disabled: Boolean(locked), onClick: () => fileInput.current?.click(), className: "flex size-8 items-center justify-center rounded-lg text-muted-foreground cd-tint hover:bg-accent hover:text-foreground disabled:opacity-40", children: _jsx(Paperclip, { className: "size-4" }) }) }), _jsx(TooltipContent, { children: t("attachFiles") })] })] })), tools, scope && (_jsxs("button", { type: "button", onClick: onScopeClick, disabled: !onScopeClick, className: cn("inline-flex min-w-0 items-center gap-1.5 rounded-lg px-2 py-1 text-xs cd-tint enabled:hover:bg-accent", production ? "bg-warning-weak text-warning-text enabled:hover:bg-warning-weak" : "text-muted-foreground"), children: [production ? _jsx(TriangleAlert, { "aria-hidden": true, className: "size-3 shrink-0" }) : _jsx("span", { "aria-hidden": true, className: "size-1.5 shrink-0 rounded-full bg-success" }), _jsxs("span", { className: "truncate", children: [scope.environment, " \u00B7 ", production ? t("production") : (scope.detail ?? t("nonProduction"))] })] })), hints && (
|
|
275
241
|
// Only what this composer can do: @ and / are named only when there's something for them to offer.
|
|
276
|
-
_jsxs("span", { className: "ms-auto hidden text-2xs text-faint-foreground @lg:inline", children: [_jsx("kbd", { className: "font-sans", children: "\u21B5" }), " ", t("hintSend"), " \u00B7 ", _jsx("kbd", { className: "font-sans", children: "\u21E7\u21B5" }), " ", t("hintNewLine"), mentions.length > 0 && (_jsxs(_Fragment, { children: [" · ", _jsx("kbd", { className: "font-sans", children: "@" }), " ", t("hintMention")] })), commands.length > 0 && (_jsxs(_Fragment, { children: [" · ", _jsx("kbd", { className: "font-sans", children: "/" }), " ", t("hintCommand")] }))] })), state === "streaming" && onStop ? (_jsx("button", { type: "button", "aria-label": t("stop"), onClick: onStop, className: cn("ms-auto flex size-8 items-center justify-center bg-foreground text-background
|
|
242
|
+
_jsxs("span", { className: "ms-auto hidden text-2xs text-faint-foreground @lg:inline", children: [_jsx("kbd", { className: "font-sans", children: "\u21B5" }), " ", t("hintSend"), " \u00B7 ", _jsx("kbd", { className: "font-sans", children: "\u21E7\u21B5" }), " ", t("hintNewLine"), mentions.length > 0 && (_jsxs(_Fragment, { children: [" · ", _jsx("kbd", { className: "font-sans", children: "@" }), " ", t("hintMention")] })), commands.length > 0 && (_jsxs(_Fragment, { children: [" · ", _jsx("kbd", { className: "font-sans", children: "/" }), " ", t("hintCommand")] }))] })), state === "streaming" && onStop ? (_jsx("button", { type: "button", "aria-label": t("stop"), onClick: onStop, style: sendMotion, className: cn("ms-auto flex size-8 items-center justify-center bg-foreground text-background cd-press", hints && "@lg:ms-1", sendShape === "round" ? "rounded-full" : "rounded-lg"), children: _jsx(Square, { className: "size-3 fill-current" }) })) : (_jsx("button", { type: "button", "aria-label": t("send"), onClick: submit, disabled: !canSend, "aria-busy": state === "sending", style: sendMotion, className: cn("ms-auto flex size-8 items-center justify-center bg-primary text-primary-foreground cd-press enabled:hover:bg-primary-hover disabled:opacity-30", hints && "@lg:ms-1", sendShape === "round" ? "rounded-full" : "rounded-lg"), children: state === "sending" ? _jsx(LoaderCircle, { style: sendingMotion, className: "size-4 cd-live-spin" }) : _jsx(ArrowUp, { className: "size-4", strokeWidth: 2.5 }) }))] })] })] }));
|
|
277
243
|
}
|
|
278
244
|
export { Composer };
|
|
@@ -4,6 +4,9 @@ import * as React from "react";
|
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
5
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
|
|
6
6
|
import { useFormat, useStrings } from "../blocks/locale.js";
|
|
7
|
+
import { motion } from "../lib/motion.js";
|
|
8
|
+
/** The ring moving to a new measured value, after the first one was drawn in place. */
|
|
9
|
+
const fillMotion = motion("context-meter", "fill", "fill");
|
|
7
10
|
/** ContextMeter's fixed words; override them under `contextMeter` in LocaleProvider's `strings`. */
|
|
8
11
|
export const contextMeterStrings = {
|
|
9
12
|
/** A token count in thousands; `{n}` is already formatted. */
|
|
@@ -46,7 +49,7 @@ function ContextMeter({ segments, limit, className, }) {
|
|
|
46
49
|
// The sweep is a transition, so it runs only for a value that changed after the first one was drawn.
|
|
47
50
|
const [drawn, setDrawn] = React.useState(false);
|
|
48
51
|
React.useEffect(() => setDrawn(true), []);
|
|
49
|
-
return (_jsxs(Popover, { children: [_jsxs(PopoverTrigger, { "aria-label": t("label", { used: tokens(used), limit: tokens(limit) }), "data-warn": warn ? "" : undefined, className: cn("inline-flex items-center gap-1.5 rounded-md px-1.5 py-1 text-2xs tabular-nums outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/80", warn ? "text-warning-text" : "text-muted-foreground", className), children: [_jsxs("svg", { viewBox: "0 0 18 18", className: "size-4 -rotate-90", "aria-hidden": true, children: [_jsx("circle", { cx: "9", cy: "9", r: r, fill: "none", stroke: "var(--muted)", strokeWidth: "2.5" }), _jsx("circle", { "data-slot": "context-meter-ring", cx: "9", cy: "9", r: r, fill: "none", stroke: warn ? "var(--warning)" : "var(--primary)", strokeWidth: "2.5", strokeDasharray: `${pct * c} ${c}`, strokeLinecap: "round", className: cn(drawn && "
|
|
52
|
+
return (_jsxs(Popover, { children: [_jsxs(PopoverTrigger, { "aria-label": t("label", { used: tokens(used), limit: tokens(limit) }), "data-warn": warn ? "" : undefined, className: cn("inline-flex items-center gap-1.5 rounded-md px-1.5 py-1 text-2xs tabular-nums outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/80", warn ? "text-warning-text" : "text-muted-foreground", className), children: [_jsxs("svg", { viewBox: "0 0 18 18", className: "size-4 -rotate-90", "aria-hidden": true, children: [_jsx("circle", { cx: "9", cy: "9", r: r, fill: "none", stroke: "var(--muted)", strokeWidth: "2.5" }), _jsx("circle", { "data-slot": "context-meter-ring", cx: "9", cy: "9", r: r, fill: "none", stroke: warn ? "var(--warning)" : "var(--primary)", strokeWidth: "2.5", strokeDasharray: `${pct * c} ${c}`, strokeLinecap: "round", style: fillMotion, className: cn(drawn && "cd-fill") })] }), fmt.number(Math.round(pct * 100) / 100, { style: "percent" })] }), _jsxs(PopoverContent, { align: "start", className: "w-72 p-3", children: [_jsxs("p", { className: "flex items-baseline justify-between text-sm font-medium", children: [t("context"), " ", _jsx("span", { className: "text-xs font-normal text-muted-foreground tabular-nums", children: t("usage", { used: tokens(used), limit: tokens(limit) }) })] }), _jsx("div", { "aria-hidden": true, className: "mt-2 flex h-2 overflow-hidden rounded-full bg-muted", children: segments.map((s) => (_jsx("span", { style: { width: `${(s.tokens / limit) * 100}%`, background: kindColor[s.kind] } }, s.label))) }), _jsx("ul", { className: "mt-3 flex flex-col gap-1.5", children: [...segments]
|
|
50
53
|
.sort((a, b) => b.tokens - a.tokens)
|
|
51
54
|
.map((s) => (_jsxs("li", { className: "flex items-center gap-2 text-xs", children: [_jsx("span", { "aria-hidden": true, className: "size-2 rounded-xs", style: { background: kindColor[s.kind] } }), _jsx("span", { className: "flex-1", children: s.label }), _jsx("span", { className: "text-muted-foreground tabular-nums", children: tokens(s.tokens) })] }, s.label))) }), warn && _jsx("p", { className: "mt-3 text-xs text-warning-text", children: t("nearlyFull") })] })] }));
|
|
52
55
|
}
|
package/dist/chat/entity-chip.js
CHANGED
|
@@ -81,6 +81,6 @@ function EntityChip({ entity, onOpen, onRemove, className, }) {
|
|
|
81
81
|
: t("title", { noun, name: entity.name });
|
|
82
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, ")"] })] }));
|
|
83
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";
|
|
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
|
|
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 cd-tint hover:border-border-strong hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/80 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: "relative after:absolute after:-inset-1 -ms-px rounded-e-sm border border-s-0 bg-card px-1 py-px text-faint-foreground hover:text-foreground", children: "\u00D7" }))] }));
|
|
85
85
|
}
|
|
86
86
|
export { EntityChip, EntityKindsProvider, useEntityKinds };
|