@candor-ui/react 0.0.0-stage → 1.0.1
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 +129 -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 +122 -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 +241 -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 +12 -0
- package/dist/lib/safe-markdown.js +98 -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 +90 -4
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
/**
|
|
4
|
+
* Text that is still arriving. Shows a caret while `streaming`, and announces
|
|
5
|
+
* politely so screen readers hear the finished text rather than every token.
|
|
6
|
+
*/
|
|
7
|
+
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: "ml-0.5 inline-block h-[1.05em] w-[2px] translate-y-[0.18em] rounded-full bg-primary animate-caret" }))] }));
|
|
9
|
+
}
|
|
10
|
+
/** A label that shimmers while an agent is working ("Thinking", "Reading the docs"). */
|
|
11
|
+
function ShimmerLabel({ children, className, }) {
|
|
12
|
+
return _jsx("span", { className: cn("shimmer-text text-sm font-medium", className), children: children });
|
|
13
|
+
}
|
|
14
|
+
export { StreamingText, ShimmerLabel };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type VariantProps } from "class-variance-authority";
|
|
3
|
+
declare const surfaceVariants: (props?: ({
|
|
4
|
+
tone?: "plain" | "muted" | "subtle" | "raised" | "floating" | "accent" | null | undefined;
|
|
5
|
+
radius?: "lg" | "md" | "xl" | null | undefined;
|
|
6
|
+
pad?: "none" | "sm" | "lg" | "md" | null | undefined;
|
|
7
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
|
+
declare function Surface({ className, tone, radius, pad, ...props }: React.ComponentProps<"div"> & VariantProps<typeof surfaceVariants>): React.JSX.Element;
|
|
9
|
+
export { Surface, surfaceVariants };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cva } from "class-variance-authority";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
// Candor's cards are borderless grey grounds with large radii; in an app they
|
|
6
|
+
// also need a bordered and a floating form. One component carries all three so
|
|
7
|
+
// no screen hand-rolls its own card.
|
|
8
|
+
const surfaceVariants = cva("relative text-card-foreground", {
|
|
9
|
+
variants: {
|
|
10
|
+
tone: {
|
|
11
|
+
plain: "bg-card border",
|
|
12
|
+
muted: "bg-muted",
|
|
13
|
+
subtle: "bg-sunken border",
|
|
14
|
+
raised: "bg-card border shadow-raised",
|
|
15
|
+
floating: "bg-popover border shadow-float",
|
|
16
|
+
accent: "bg-accent-weak text-foreground",
|
|
17
|
+
},
|
|
18
|
+
radius: {
|
|
19
|
+
md: "rounded-lg",
|
|
20
|
+
lg: "rounded-xl",
|
|
21
|
+
xl: "rounded-2xl",
|
|
22
|
+
},
|
|
23
|
+
pad: {
|
|
24
|
+
none: "",
|
|
25
|
+
sm: "p-3",
|
|
26
|
+
md: "p-4",
|
|
27
|
+
lg: "p-6",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: { tone: "plain", radius: "xl", pad: "md" },
|
|
31
|
+
});
|
|
32
|
+
function Surface({ className, tone, radius, pad, ...props }) {
|
|
33
|
+
return _jsx("div", { "data-slot": "surface", className: cn(surfaceVariants({ tone, radius, pad }), className), ...props });
|
|
34
|
+
}
|
|
35
|
+
export { Surface, surfaceVariants };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The app's words for its own concepts, so components read like the product:
|
|
4
|
+
* one app says "org", another "project" or "workspace".
|
|
5
|
+
*/
|
|
6
|
+
export type Terms = {
|
|
7
|
+
/** One place agents act in. */
|
|
8
|
+
environment: string;
|
|
9
|
+
environments: string;
|
|
10
|
+
/** The paying customer that owns the environments. */
|
|
11
|
+
account: string;
|
|
12
|
+
/** The product's own name, for labels like "In the app" and "What the app will read". */
|
|
13
|
+
product: string;
|
|
14
|
+
};
|
|
15
|
+
declare function TermsProvider({ terms, children, }: {
|
|
16
|
+
/** The words to use; any left out fall back to the ones above. */
|
|
17
|
+
terms: Partial<Terms>;
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
}): React.JSX.Element;
|
|
20
|
+
/** The app's terms; defaults are "environment" and "account". */
|
|
21
|
+
declare function useTerms(): Terms;
|
|
22
|
+
/** "Environment" from "environment": for headings and the start of a sentence. */
|
|
23
|
+
declare const capitalise: (s: string) => string;
|
|
24
|
+
export { TermsProvider, useTerms, capitalise };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
const defaults = { environment: "environment", environments: "environments", account: "account", product: "the app" };
|
|
5
|
+
const TermsContext = React.createContext(defaults);
|
|
6
|
+
function TermsProvider({ terms, children, }) {
|
|
7
|
+
const parent = React.useContext(TermsContext);
|
|
8
|
+
const value = React.useMemo(() => ({ ...parent, ...terms }), [parent, terms]);
|
|
9
|
+
return _jsx(TermsContext.Provider, { value: value, children: children });
|
|
10
|
+
}
|
|
11
|
+
/** The app's terms; defaults are "environment" and "account". */
|
|
12
|
+
function useTerms() {
|
|
13
|
+
return React.useContext(TermsContext);
|
|
14
|
+
}
|
|
15
|
+
/** "Environment" from "environment": for headings and the start of a sentence. */
|
|
16
|
+
const capitalise = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
17
|
+
export { TermsProvider, useTerms, capitalise };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type LucideIcon } from "lucide-react";
|
|
3
|
+
import type { AgentIdentity } from "../lib/chat.js";
|
|
4
|
+
export type AgentRoleInfo = {
|
|
5
|
+
Icon: LucideIcon;
|
|
6
|
+
noun: string;
|
|
7
|
+
hue: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const baseAgentRoles: Record<string, AgentRoleInfo>;
|
|
10
|
+
/** The app's agent roles; nested providers add to (and can override) the ones above. */
|
|
11
|
+
declare function AgentRolesProvider({ roles, children, }: {
|
|
12
|
+
/** Role ids mapped to an icon, a noun and a hue. */
|
|
13
|
+
roles: Record<string, AgentRoleInfo>;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}): React.JSX.Element;
|
|
16
|
+
/** Look up a role's glyph, noun and hue; unknown roles read as "Assistant". */
|
|
17
|
+
declare function useAgentRoles(): (role: string) => AgentRoleInfo;
|
|
18
|
+
/** An agent's mark: its role glyph on a tile tinted with the role's hue. */
|
|
19
|
+
declare function AgentAvatar({ agent, size, className, }: {
|
|
20
|
+
/** Who the agent is: id, display name and role. */
|
|
21
|
+
agent: AgentIdentity;
|
|
22
|
+
/** md for message headers, sm for lists and chips. */
|
|
23
|
+
size?: "sm" | "md";
|
|
24
|
+
className?: string;
|
|
25
|
+
}): React.JSX.Element;
|
|
26
|
+
/** Avatar plus name, with the role spelled out for screen readers. */
|
|
27
|
+
declare function AgentBadge({ agent, size, className, }: {
|
|
28
|
+
/** Who the agent is: id, display name and role. */
|
|
29
|
+
agent: AgentIdentity;
|
|
30
|
+
/** md for message headers, sm for lists and chips. */
|
|
31
|
+
size?: "sm" | "md";
|
|
32
|
+
className?: string;
|
|
33
|
+
}): React.JSX.Element;
|
|
34
|
+
export { AgentAvatar, AgentBadge, AgentRolesProvider, useAgentRoles };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Bot, ClipboardList, CodeXml, Compass, FlaskConical, PenTool, ScanSearch, SearchCheck, Workflow } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
// Each role keeps one glyph and one chart hue everywhere it appears, so a
|
|
7
|
+
// person learns "the green flask is the tester" once.
|
|
8
|
+
export const baseAgentRoles = {
|
|
9
|
+
assistant: { Icon: Bot, noun: "Assistant", hue: "var(--chart-1)" },
|
|
10
|
+
orchestrator: { Icon: Workflow, noun: "Orchestrator", hue: "var(--chart-1)" },
|
|
11
|
+
planner: { Icon: Compass, noun: "Planner", hue: "var(--chart-3)" },
|
|
12
|
+
researcher: { Icon: ScanSearch, noun: "Researcher", hue: "var(--chart-2)" },
|
|
13
|
+
analyst: { Icon: ClipboardList, noun: "Analyst", hue: "var(--chart-2)" },
|
|
14
|
+
designer: { Icon: PenTool, noun: "Designer", hue: "var(--chart-5)" },
|
|
15
|
+
coder: { Icon: CodeXml, noun: "Coder", hue: "var(--chart-1)" },
|
|
16
|
+
reviewer: { Icon: SearchCheck, noun: "Reviewer", hue: "var(--chart-3)" },
|
|
17
|
+
tester: { Icon: FlaskConical, noun: "Tester", hue: "var(--chart-4)" },
|
|
18
|
+
};
|
|
19
|
+
const unknownRole = { Icon: Bot, noun: "Assistant", hue: "var(--chart-1)" };
|
|
20
|
+
const RolesContext = React.createContext(baseAgentRoles);
|
|
21
|
+
/** The app's agent roles; nested providers add to (and can override) the ones above. */
|
|
22
|
+
function AgentRolesProvider({ roles, children, }) {
|
|
23
|
+
const parent = React.useContext(RolesContext);
|
|
24
|
+
const value = React.useMemo(() => ({ ...parent, ...roles }), [parent, roles]);
|
|
25
|
+
return _jsx(RolesContext.Provider, { value: value, children: children });
|
|
26
|
+
}
|
|
27
|
+
/** Look up a role's glyph, noun and hue; unknown roles read as "Assistant". */
|
|
28
|
+
function useAgentRoles() {
|
|
29
|
+
const roles = React.useContext(RolesContext);
|
|
30
|
+
return React.useCallback((role) => roles[role] ?? unknownRole, [roles]);
|
|
31
|
+
}
|
|
32
|
+
/** An agent's mark: its role glyph on a tile tinted with the role's hue. */
|
|
33
|
+
function AgentAvatar({ agent, size = "md", className, }) {
|
|
34
|
+
const { Icon, hue } = useAgentRoles()(agent.role);
|
|
35
|
+
return (_jsx("span", { "data-slot": "agent-avatar", "aria-hidden": true, className: cn("inline-flex shrink-0 items-center justify-center rounded-md", size === "md" ? "size-7 [&_svg]:size-3.5" : "size-5 rounded-sm [&_svg]:size-3", className), style: { color: hue, background: `color-mix(in oklab, ${hue} 14%, var(--card))`, boxShadow: `inset 0 0 0 1px color-mix(in oklab, ${hue} 22%, transparent)` }, children: _jsx(Icon, { strokeWidth: 2.25 }) }));
|
|
36
|
+
}
|
|
37
|
+
/** Avatar plus name, with the role spelled out for screen readers. */
|
|
38
|
+
function AgentBadge({ agent, size = "md", className, }) {
|
|
39
|
+
const { noun } = useAgentRoles()(agent.role);
|
|
40
|
+
return (_jsxs("span", { "data-slot": "agent-badge", className: cn("inline-flex min-w-0 items-center gap-2", className), children: [_jsx(AgentAvatar, { agent: agent, size: size }), _jsx("span", { className: cn("truncate font-medium", size === "sm" ? "text-xs" : "text-sm"), children: agent.name }), _jsxs("span", { className: "sr-only", children: [", ", noun, " agent"] })] }));
|
|
41
|
+
}
|
|
42
|
+
export { AgentAvatar, AgentBadge, AgentRolesProvider, useAgentRoles };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A card an agent's reply can carry. `check` decides whether the props are
|
|
4
|
+
* the shape `render` needs: it returns null when they are, or a sentence
|
|
5
|
+
* saying what's wrong. Cards are data from a model; nothing renders until the
|
|
6
|
+
* check passes.
|
|
7
|
+
*/
|
|
8
|
+
export type CardDefinition<P = unknown> = {
|
|
9
|
+
/** What it is, in a few words, for fallbacks: "Query results". */
|
|
10
|
+
label: string;
|
|
11
|
+
check: (props: unknown) => string | null;
|
|
12
|
+
render: (props: P) => React.ReactNode;
|
|
13
|
+
};
|
|
14
|
+
type AnyCard = CardDefinition<any>;
|
|
15
|
+
/** Define a card with typed props; the check is what makes the cast safe. */
|
|
16
|
+
export declare function defineCard<P>(card: CardDefinition<P>): CardDefinition<P>;
|
|
17
|
+
/** Makes cards available to every message below it. Cards not listed here fall back to their summary. */
|
|
18
|
+
declare function CardRegistryProvider({ cards, children, }: {
|
|
19
|
+
/** Your app's cards by name, made with defineCard. */
|
|
20
|
+
cards: Record<string, AnyCard>;
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
}): React.JSX.Element;
|
|
23
|
+
/**
|
|
24
|
+
* Renders one card from the registry. Three honest fallbacks, each with the
|
|
25
|
+
* service's text summary: the app doesn't know this card; its data doesn't
|
|
26
|
+
* fit (and how); or it failed while drawing.
|
|
27
|
+
*/
|
|
28
|
+
declare function Card({ name, props, summary, }: {
|
|
29
|
+
/** Which card to draw, from the registry. */
|
|
30
|
+
name: string;
|
|
31
|
+
/** The card's data, checked before it draws. */
|
|
32
|
+
props: unknown;
|
|
33
|
+
/** The plain-text version, shown when the card can't be. */
|
|
34
|
+
summary: string;
|
|
35
|
+
}): React.JSX.Element;
|
|
36
|
+
export declare const isObject: (v: unknown) => v is Record<string, unknown>;
|
|
37
|
+
export declare const isArrayOf: <T>(v: unknown, item: (x: unknown) => x is T) => v is T[];
|
|
38
|
+
export declare const isString: (v: unknown) => v is string;
|
|
39
|
+
export { Card, CardRegistryProvider };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ChevronRight, LayoutTemplate } from "lucide-react";
|
|
5
|
+
import { DegradedPanel } from "../blocks/degraded-panel.js";
|
|
6
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
|
|
7
|
+
/** Define a card with typed props; the check is what makes the cast safe. */
|
|
8
|
+
export function defineCard(card) {
|
|
9
|
+
return card;
|
|
10
|
+
}
|
|
11
|
+
const Registry = React.createContext({});
|
|
12
|
+
/** Makes cards available to every message below it. Cards not listed here fall back to their summary. */
|
|
13
|
+
function CardRegistryProvider({ cards, children, }) {
|
|
14
|
+
return _jsx(Registry.Provider, { value: cards, children: children });
|
|
15
|
+
}
|
|
16
|
+
class CardBoundary extends React.Component {
|
|
17
|
+
state = { failed: false };
|
|
18
|
+
static getDerivedStateFromError() {
|
|
19
|
+
return { failed: true };
|
|
20
|
+
}
|
|
21
|
+
render() {
|
|
22
|
+
return this.state.failed ? this.props.fallback : this.props.children;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function Summary({ text, }) {
|
|
26
|
+
return (_jsxs(Collapsible, { className: "group/summary", children: [_jsxs(CollapsibleTrigger, { className: "inline-flex items-center gap-1 text-xs text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/40 rounded-sm", children: [_jsx(ChevronRight, { "aria-hidden": true, className: "size-3 transition-transform group-data-[state=open]/summary:rotate-90" }), " Show it as text"] }), _jsx(CollapsibleContent, { children: _jsx("p", { className: "mt-1.5 rounded-md bg-sunken px-3 py-2 text-sm whitespace-pre-wrap", children: text }) })] }));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Renders one card from the registry. Three honest fallbacks, each with the
|
|
30
|
+
* service's text summary: the app doesn't know this card; its data doesn't
|
|
31
|
+
* fit (and how); or it failed while drawing.
|
|
32
|
+
*/
|
|
33
|
+
function Card({ name, props, summary, }) {
|
|
34
|
+
const cards = React.useContext(Registry);
|
|
35
|
+
const def = cards[name];
|
|
36
|
+
if (!def) {
|
|
37
|
+
return (_jsxs("div", { "data-slot": "card", "data-card": name, "data-state": "unknown", className: "flex flex-col gap-1.5 rounded-lg border border-dashed px-3 py-2.5", children: [_jsxs("p", { className: "flex items-center gap-1.5 text-sm text-muted-foreground", children: [_jsx(LayoutTemplate, { "aria-hidden": true, className: "size-3.5" }), " This reply includes a \u201C", name, "\u201D card this app can\u2019t show."] }), _jsx(Summary, { text: summary })] }));
|
|
38
|
+
}
|
|
39
|
+
const problem = def.check(props);
|
|
40
|
+
if (problem) {
|
|
41
|
+
return (_jsxs("div", { "data-slot": "card", "data-card": name, "data-state": "invalid", className: "flex flex-col gap-1.5", children: [_jsx(DegradedPanel, { reason: `The ${def.label.toLowerCase()} couldn’t be shown: ${problem}` }), _jsx(Summary, { text: summary })] }));
|
|
42
|
+
}
|
|
43
|
+
return (_jsx("div", { "data-slot": "card", "data-card": name, "data-state": "ready", children: _jsx(CardBoundary, { fallback: _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(DegradedPanel, { reason: `The ${def.label.toLowerCase()} failed while drawing.` }), _jsx(Summary, { text: summary })] }), children: def.render(props) }) }));
|
|
44
|
+
}
|
|
45
|
+
/* ---------------------------------------------------------------- small checking helpers */
|
|
46
|
+
export const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
47
|
+
export const isArrayOf = (v, item) => Array.isArray(v) && v.every(item);
|
|
48
|
+
export const isString = (v) => typeof v === "string";
|
|
49
|
+
export { Card, CardRegistryProvider };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type LucideIcon } from "lucide-react";
|
|
2
|
+
import type { MessageSource } from "../lib/chat.js";
|
|
3
|
+
/** Kinds of evidence most apps cite. Others read as "Source". */
|
|
4
|
+
export declare const sourceKinds: Record<string, {
|
|
5
|
+
Icon: LucideIcon;
|
|
6
|
+
noun: string;
|
|
7
|
+
}>;
|
|
8
|
+
declare function SourceDetail({ source, }: {
|
|
9
|
+
/** The source this marker points to; without it the marker says it's unknown. */
|
|
10
|
+
source: MessageSource;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
12
|
+
/**
|
|
13
|
+
* An inline [n]. Opens the source in a popover on click or Enter, so evidence is
|
|
14
|
+
* one step away without leaving the thread. A source the message doesn't list
|
|
15
|
+
* renders as a plain bracket rather than a guess.
|
|
16
|
+
*/
|
|
17
|
+
declare function Citation({ source, marker, }: {
|
|
18
|
+
/** The source this marker points to; without it the marker says it's unknown. */
|
|
19
|
+
source?: MessageSource;
|
|
20
|
+
/** The number to show when there's no source. */
|
|
21
|
+
marker?: number;
|
|
22
|
+
}): import("react").JSX.Element;
|
|
23
|
+
/** The sources under a reply, as chips that open the same detail. */
|
|
24
|
+
declare function SourceList({ sources, className, }: {
|
|
25
|
+
/** Every source the reply used, in marker order. */
|
|
26
|
+
sources: MessageSource[];
|
|
27
|
+
className?: string;
|
|
28
|
+
}): import("react").JSX.Element | null;
|
|
29
|
+
export { Citation, SourceList, SourceDetail };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { BookOpen, Braces, Database, FileText, Globe, Table2, Waypoints } from "lucide-react";
|
|
3
|
+
import { cn } from "../lib/utils.js";
|
|
4
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
|
|
5
|
+
/** Kinds of evidence most apps cite. Others read as "Source". */
|
|
6
|
+
export const sourceKinds = {
|
|
7
|
+
document: { Icon: FileText, noun: "Document" },
|
|
8
|
+
web: { Icon: Globe, noun: "Web page" },
|
|
9
|
+
query: { Icon: Database, noun: "Query" },
|
|
10
|
+
data: { Icon: Table2, noun: "Data" },
|
|
11
|
+
code: { Icon: Braces, noun: "Code" },
|
|
12
|
+
workflow: { Icon: Waypoints, noun: "Workflow" },
|
|
13
|
+
knowledge: { Icon: BookOpen, noun: "Knowledge" },
|
|
14
|
+
};
|
|
15
|
+
const kindOf = (kind) => sourceKinds[kind] ?? { Icon: FileText, noun: "Source" };
|
|
16
|
+
function SourceDetail({ source, }) {
|
|
17
|
+
const { Icon, noun } = kindOf(source.kind);
|
|
18
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-sm bg-muted text-muted-foreground", children: _jsx(Icon, { className: "size-3" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-sm font-medium leading-snug", children: source.label }), _jsx("p", { className: "text-xs text-muted-foreground", children: source.origin ?? noun })] })] }), source.snippet && (_jsx("blockquote", { className: "max-h-40 overflow-auto rounded-md bg-sunken px-2.5 py-2 text-xs leading-relaxed whitespace-pre-wrap text-muted-foreground", children: source.snippet })), source.provisional && (_jsx("p", { className: "text-xs text-provisional", children: "The agent cites this but hasn\u2019t verified it." }))] }));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* An inline [n]. Opens the source in a popover on click or Enter, so evidence is
|
|
22
|
+
* one step away without leaving the thread. A source the message doesn't list
|
|
23
|
+
* renders as a plain bracket rather than a guess.
|
|
24
|
+
*/
|
|
25
|
+
function Citation({ source, marker, }) {
|
|
26
|
+
if (!source) {
|
|
27
|
+
return _jsxs("sup", { className: "text-faint-foreground", children: ["[", marker ?? "?", "]"] });
|
|
28
|
+
}
|
|
29
|
+
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": `Source ${source.marker}: ${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 transition-colors duration-150 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40", source.provisional
|
|
30
|
+
? "border border-dashed border-provisional/60 text-provisional hover:bg-muted"
|
|
31
|
+
: "bg-accent-weak text-accent-text hover:bg-primary hover:text-primary-foreground"), children: source.marker }) }), _jsx(PopoverContent, { align: "start", className: "w-80 p-3", children: _jsx(SourceDetail, { source: source }) })] }));
|
|
32
|
+
}
|
|
33
|
+
/** The sources under a reply, as chips that open the same detail. */
|
|
34
|
+
function SourceList({ sources, className, }) {
|
|
35
|
+
if (sources.length === 0)
|
|
36
|
+
return null;
|
|
37
|
+
return (_jsxs("div", { "data-slot": "source-list", className: cn("flex flex-wrap items-center gap-1.5", className), children: [_jsx("span", { className: "mr-0.5 text-xs text-faint-foreground", children: "Sources" }), [...sources]
|
|
38
|
+
.sort((a, b) => a.marker - b.marker)
|
|
39
|
+
.map((s) => {
|
|
40
|
+
const { Icon } = kindOf(s.kind);
|
|
41
|
+
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: cn("inline-flex max-w-56 items-center gap-1.5 rounded-md border bg-card py-0.5 pr-2 pl-1 text-xs 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", 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 p-3", children: _jsx(SourceDetail, { source: s }) })] }, s.id));
|
|
42
|
+
})] }));
|
|
43
|
+
}
|
|
44
|
+
export { Citation, SourceList, SourceDetail };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { Attachment, ComposerScope, EntityRef, MentionOption, SlashCommand } from "../lib/chat.js";
|
|
3
|
+
export type ComposerState = "ready" | "sending" | "streaming" | {
|
|
4
|
+
kind: "locked";
|
|
5
|
+
reason: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "offline";
|
|
8
|
+
reason?: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Where a person asks. Mentions (@) attach things in the user's system as chips the agent
|
|
12
|
+
* receives as references, not as text it has to parse; slash commands (/) start
|
|
13
|
+
* a known action. The scope pill says which environment the ask runs
|
|
14
|
+
* against, and turns to a warning when that is production.
|
|
15
|
+
*
|
|
16
|
+
* Enter sends, Shift+Enter breaks a line. While an agent replies the send button
|
|
17
|
+
* becomes Stop. A locked composer says why; an offline one says so.
|
|
18
|
+
*/
|
|
19
|
+
declare function Composer({ value, onChange, onSubmit, onStop, state, scope, onScopeClick, mentions, commands, references, onReferencesChange, attachments, onAttach, onRemoveAttachment, placeholder, label, className, }: {
|
|
20
|
+
/** The text in the box. */
|
|
21
|
+
value: string;
|
|
22
|
+
/** Called with the new text as the person types. */
|
|
23
|
+
onChange: (value: string) => void;
|
|
24
|
+
/** Called on send with the text and the entities mentioned. */
|
|
25
|
+
onSubmit: (value: string, references: EntityRef[]) => void;
|
|
26
|
+
/** Shown as Stop while a reply is streaming. */
|
|
27
|
+
onStop?: () => void;
|
|
28
|
+
/** ready, sending, streaming, locked (with a reason) or offline. */
|
|
29
|
+
state?: ComposerState;
|
|
30
|
+
/** Where the next message's work lands; production turns the pill to a warning. */
|
|
31
|
+
scope?: ComposerScope;
|
|
32
|
+
/** Makes the scope pill a button, e.g. to change environment. */
|
|
33
|
+
onScopeClick?: () => void;
|
|
34
|
+
/** What @ can insert, grouped under their headings. */
|
|
35
|
+
mentions?: MentionOption[];
|
|
36
|
+
/** What / can run, only at the start of a message. */
|
|
37
|
+
commands?: SlashCommand[];
|
|
38
|
+
/** Entities already attached, shown as chips. */
|
|
39
|
+
references?: EntityRef[];
|
|
40
|
+
/** Called when a chip is added or removed. */
|
|
41
|
+
onReferencesChange?: (references: EntityRef[]) => void;
|
|
42
|
+
/** Files attached, with upload progress or an error. */
|
|
43
|
+
attachments?: Attachment[];
|
|
44
|
+
/** Shows an attach button; called with the chosen files. */
|
|
45
|
+
onAttach?: (files: File[]) => void;
|
|
46
|
+
/** Called when a file is removed. */
|
|
47
|
+
onRemoveAttachment?: (id: string) => void;
|
|
48
|
+
/** Shown in the empty box. */
|
|
49
|
+
placeholder?: string;
|
|
50
|
+
/** The accessible name of the text box. */
|
|
51
|
+
label?: string;
|
|
52
|
+
className?: string;
|
|
53
|
+
}): React.JSX.Element;
|
|
54
|
+
export { Composer };
|
|
@@ -0,0 +1,168 @@
|
|
|
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 { ArrowUp, LoaderCircle, Paperclip, Square, TriangleAlert, X } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
|
|
7
|
+
import { EntityChip, useEntityKinds } from "./entity-chip.js";
|
|
8
|
+
/** The @ or / token the caret sits in, if any. A / only counts at the start of the message. */
|
|
9
|
+
function findTrigger(value, caret) {
|
|
10
|
+
const before = value.slice(0, caret);
|
|
11
|
+
const m = /(^|\s)([@/])([\w.-]*)$/.exec(before);
|
|
12
|
+
if (!m)
|
|
13
|
+
return null;
|
|
14
|
+
const char = m[2];
|
|
15
|
+
const start = before.length - m[3].length - 1;
|
|
16
|
+
if (char === "/" && before.slice(0, start).trim() !== "")
|
|
17
|
+
return null;
|
|
18
|
+
return { char, start, query: m[3] };
|
|
19
|
+
}
|
|
20
|
+
const groupOf = (item) => (item.type === "command" ? "Commands" : item.option.group);
|
|
21
|
+
function formatSize(bytes) {
|
|
22
|
+
if (bytes === undefined)
|
|
23
|
+
return null;
|
|
24
|
+
if (bytes < 1024)
|
|
25
|
+
return `${bytes} B`;
|
|
26
|
+
if (bytes < 1024 * 1024)
|
|
27
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
28
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Where a person asks. Mentions (@) attach things in the user's system as chips the agent
|
|
32
|
+
* receives as references, not as text it has to parse; slash commands (/) start
|
|
33
|
+
* a known action. The scope pill says which environment the ask runs
|
|
34
|
+
* against, and turns to a warning when that is production.
|
|
35
|
+
*
|
|
36
|
+
* Enter sends, Shift+Enter breaks a line. While an agent replies the send button
|
|
37
|
+
* becomes Stop. A locked composer says why; an offline one says so.
|
|
38
|
+
*/
|
|
39
|
+
function Composer({ value, onChange, onSubmit, onStop, state = "ready", scope, onScopeClick, mentions = [], commands = [], references = [], onReferencesChange, attachments = [], onAttach, onRemoveAttachment, placeholder = "Ask a question, or describe a change", label = "Message", className, }) {
|
|
40
|
+
const kindOf = useEntityKinds();
|
|
41
|
+
const id = React.useId();
|
|
42
|
+
const textarea = React.useRef(null);
|
|
43
|
+
const fileInput = React.useRef(null);
|
|
44
|
+
const [trigger, setTrigger] = React.useState(null);
|
|
45
|
+
const [active, setActive] = React.useState(0);
|
|
46
|
+
const locked = typeof state === "object" ? state : null;
|
|
47
|
+
const busy = state === "sending" || state === "streaming";
|
|
48
|
+
const canSend = !locked && !busy && (value.trim().length > 0 || references.length > 0);
|
|
49
|
+
const items = React.useMemo(() => {
|
|
50
|
+
if (!trigger)
|
|
51
|
+
return [];
|
|
52
|
+
const q = trigger.query.toLowerCase();
|
|
53
|
+
if (trigger.char === "/") {
|
|
54
|
+
return commands
|
|
55
|
+
.filter((c) => c.name.toLowerCase().startsWith(q))
|
|
56
|
+
.map((command) => ({ type: "command", command }));
|
|
57
|
+
}
|
|
58
|
+
const taken = new Set(references.map((r) => `${r.kind}:${r.name}`));
|
|
59
|
+
return mentions
|
|
60
|
+
.filter((o) => !o.entity || !taken.has(`${o.entity.kind}:${o.entity.name}`))
|
|
61
|
+
.filter((o) => o.label.toLowerCase().includes(q) || o.entity?.name.toLowerCase().includes(q))
|
|
62
|
+
.slice(0, 8)
|
|
63
|
+
.map((option) => ({ type: "mention", option }));
|
|
64
|
+
}, [trigger, commands, mentions, references]);
|
|
65
|
+
const menuOpen = trigger !== null && items.length > 0;
|
|
66
|
+
React.useEffect(() => setActive(0), [trigger?.query, trigger?.char]);
|
|
67
|
+
const syncTrigger = () => {
|
|
68
|
+
const el = textarea.current;
|
|
69
|
+
if (!el)
|
|
70
|
+
return;
|
|
71
|
+
setTrigger(findTrigger(el.value, el.selectionStart));
|
|
72
|
+
};
|
|
73
|
+
// Where the caret goes once a replaced value renders. Applied in a layout
|
|
74
|
+
// effect on that render, so keystrokes typed afterwards are never moved.
|
|
75
|
+
const pendingCaret = React.useRef(null);
|
|
76
|
+
React.useLayoutEffect(() => {
|
|
77
|
+
const el = textarea.current;
|
|
78
|
+
if (el && pendingCaret.current !== null) {
|
|
79
|
+
el.setSelectionRange(pendingCaret.current, pendingCaret.current);
|
|
80
|
+
pendingCaret.current = null;
|
|
81
|
+
}
|
|
82
|
+
}, [value, references]);
|
|
83
|
+
const replaceTrigger = (text) => {
|
|
84
|
+
if (!trigger)
|
|
85
|
+
return;
|
|
86
|
+
const el = textarea.current;
|
|
87
|
+
const caret = el?.selectionStart ?? value.length;
|
|
88
|
+
const next = value.slice(0, trigger.start) + text + value.slice(caret);
|
|
89
|
+
pendingCaret.current = trigger.start + text.length;
|
|
90
|
+
onChange(next);
|
|
91
|
+
setTrigger(null);
|
|
92
|
+
el?.focus();
|
|
93
|
+
};
|
|
94
|
+
const choose = (item) => {
|
|
95
|
+
if (item.type === "command") {
|
|
96
|
+
replaceTrigger(`/${item.command.name} `);
|
|
97
|
+
}
|
|
98
|
+
else if (item.option.entity) {
|
|
99
|
+
onReferencesChange?.([...references, item.option.entity]);
|
|
100
|
+
replaceTrigger("");
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
replaceTrigger(`@${item.option.label} `);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const submit = () => {
|
|
107
|
+
if (!canSend)
|
|
108
|
+
return;
|
|
109
|
+
onSubmit(value.trim(), references);
|
|
110
|
+
};
|
|
111
|
+
const onKeyDown = (e) => {
|
|
112
|
+
if (menuOpen) {
|
|
113
|
+
if (e.key === "ArrowDown") {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
setActive((a) => (a + 1) % items.length);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (e.key === "ArrowUp") {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
setActive((a) => (a - 1 + items.length) % items.length);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (e.key === "Enter" || e.key === "Tab") {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
choose(items[active]);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (e.key === "Escape") {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
setTrigger(null);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (e.key === "Backspace" && value === "" && references.length > 0) {
|
|
135
|
+
onReferencesChange?.(references.slice(0, -1));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
submit();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const production = !!scope?.production;
|
|
144
|
+
const listId = `${id}-menu`;
|
|
145
|
+
const optionId = (i) => `${id}-opt-${i}`;
|
|
146
|
+
return (_jsxs("div", { "data-slot": "composer", className: cn("@container relative", className), children: [menuOpen && (_jsx("div", { id: listId, role: "listbox", "aria-label": trigger?.char === "/" ? "Commands" : "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 animate-in fade-in-0 slide-in-from-bottom-1", children: items.map((item, i) => {
|
|
147
|
+
const group = groupOf(item);
|
|
148
|
+
const prev = items[i - 1];
|
|
149
|
+
const header = !prev || groupOf(prev) !== group ? group : null;
|
|
150
|
+
const Icon = item.type === "mention" && item.option.entity ? kindOf(item.option.entity.kind).Icon : null;
|
|
151
|
+
return (_jsxs(React.Fragment, { children: [header && _jsx("p", { className: "px-2 pt-2 pb-1 text-2xs font-medium text-faint-foreground", children: header }), _jsx("div", { id: optionId(i), role: "option", tabIndex: -1, "aria-selected": i === active, onMouseDown: (e) => {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
choose(item);
|
|
154
|
+
}, 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: "ml-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));
|
|
155
|
+
}) })), _jsxs("div", { className: cn("rounded-2xl border bg-card shadow-raised transition-[border-color,box-shadow] duration-150 focus-within:border-border-strong focus-within:shadow-float", locked && "bg-muted shadow-none"), children: [(references.length > 0 || attachments.length > 0) && (_jsxs("div", { className: "flex flex-wrap gap-1.5 px-3 pt-3", children: [references.map((r) => (_jsx(EntityChip, { entity: r, onRemove: (e) => onReferencesChange?.(references.filter((x) => x !== e)) }, `${r.kind}:${r.name}`))), attachments.map((a) => (_jsxs("span", { className: cn("relative inline-flex max-w-56 items-center gap-1.5 overflow-hidden rounded-md border bg-card py-1 pr-1 pl-2 text-xs", a.error && "border-destructive/50"), children: [_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 ? `${Math.round(a.progress * 100)}%` : formatSize(a.size) }), onRemoveAttachment && (_jsx("button", { type: "button", "aria-label": `Remove ${a.name}`, onClick: () => onRemoveAttachment(a.id), className: "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 left-0 h-0.5 bg-primary transition-[width] duration-300", style: { width: `${a.progress * 100}%` } }))] }, 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) => {
|
|
156
|
+
onChange(e.target.value);
|
|
157
|
+
requestAnimationFrame(syncTrigger);
|
|
158
|
+
}, onKeyDown: onKeyDown, onKeyUp: (e) => {
|
|
159
|
+
if (e.key === "ArrowLeft" || e.key === "ArrowRight")
|
|
160
|
+
syncTrigger();
|
|
161
|
+
}, onClick: syncTrigger, onBlur: () => setTrigger(null), "aria-controls": menuOpen ? listId : undefined, "aria-activedescendant": menuOpen ? optionId(active) : undefined, "aria-autocomplete": "list", "aria-describedby": locked ? `${id}-locked` : undefined, className: cn("block max-h-60 min-h-12 w-full resize-none bg-transparent px-4 pt-3 pb-1 text-base leading-relaxed outline-none [field-sizing:content] placeholder:text-faint-foreground disabled:cursor-not-allowed", locked && !value && "min-h-0 h-3 pt-3 pb-0") }), locked && (_jsx("p", { id: `${id}-locked`, className: "flex items-center gap-1.5 px-4 pb-1 text-sm text-muted-foreground", children: locked.kind === "offline" ? locked.reason ?? "You’re offline. Your message will wait here." : locked.reason })), _jsxs("div", { className: "flex items-center gap-1.5 px-2 pt-1 pb-2", children: [onAttach && (_jsxs(_Fragment, { children: [_jsx("input", { ref: fileInput, type: "file", multiple: true, tabIndex: -1, "aria-label": "Attach files", className: "hidden", onChange: (e) => {
|
|
162
|
+
const files = Array.from(e.target.files ?? []);
|
|
163
|
+
if (files.length)
|
|
164
|
+
onAttach(files);
|
|
165
|
+
e.target.value = "";
|
|
166
|
+
} }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": "Attach files", disabled: Boolean(locked), onClick: () => fileInput.current?.click(), className: "flex size-8 items-center justify-center rounded-lg text-muted-foreground transition-colors duration-150 hover:bg-accent hover:text-foreground disabled:opacity-40", children: _jsx(Paperclip, { className: "size-4" }) }) }), _jsx(TooltipContent, { children: "Attach files" })] })] })), 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 transition-colors duration-150 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 ? "Production" : (scope.detail ?? "Non-production")] })] })), _jsxs("span", { className: "ml-auto hidden text-2xs text-faint-foreground @lg:inline", children: [_jsx("kbd", { className: "font-sans", children: "\u21B5" }), " send \u00B7 ", _jsx("kbd", { className: "font-sans", children: "\u21E7\u21B5" }), " new line \u00B7 ", _jsx("kbd", { className: "font-sans", children: "@" }), " mention"] }), state === "streaming" && onStop ? (_jsx("button", { type: "button", "aria-label": "Stop", onClick: onStop, className: "ml-auto flex size-8 items-center justify-center rounded-lg bg-foreground text-background transition-transform duration-150 active:scale-95 @lg:ml-1", children: _jsx(Square, { className: "size-3 fill-current" }) })) : (_jsx("button", { type: "button", "aria-label": "Send", onClick: submit, disabled: !canSend, "aria-busy": state === "sending", className: "ml-auto flex size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground transition-[background-color,opacity,transform] duration-150 enabled:hover:bg-primary-hover active:scale-95 disabled:opacity-30 @lg:ml-1", children: state === "sending" ? _jsx(LoaderCircle, { className: "size-4 animate-spin" }) : _jsx(ArrowUp, { className: "size-4", strokeWidth: 2.5 }) }))] })] })] }));
|
|
167
|
+
}
|
|
168
|
+
export { Composer };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ContextSegment } from "../lib/know.js";
|
|
2
|
+
/**
|
|
3
|
+
* What fills the model's context budget, as a ring in the composer that
|
|
4
|
+
* opens to a breakdown: system, instructions, Know, the thread, tools, files.
|
|
5
|
+
* Counts are the service's own token counts; the ring warns past 80%.
|
|
6
|
+
*/
|
|
7
|
+
declare function ContextMeter({ segments, limit, className, }: {
|
|
8
|
+
/** What fills the context window, by kind, in tokens. */
|
|
9
|
+
segments: ContextSegment[];
|
|
10
|
+
/** The model's context window, in tokens. */
|
|
11
|
+
limit: number;
|
|
12
|
+
className?: string;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
14
|
+
export { ContextMeter };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
|
|
4
|
+
const kindColor = {
|
|
5
|
+
system: "var(--chart-5)",
|
|
6
|
+
instructions: "var(--chart-1)",
|
|
7
|
+
knowledge: "var(--chart-2)",
|
|
8
|
+
thread: "var(--chart-4)",
|
|
9
|
+
tools: "var(--chart-3)",
|
|
10
|
+
files: "var(--faint-foreground)",
|
|
11
|
+
};
|
|
12
|
+
function k(n) {
|
|
13
|
+
return n >= 1000 ? `${(n / 1000).toFixed(n >= 10_000 ? 0 : 1)}k` : String(n);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* What fills the model's context budget, as a ring in the composer that
|
|
17
|
+
* opens to a breakdown: system, instructions, Know, the thread, tools, files.
|
|
18
|
+
* Counts are the service's own token counts; the ring warns past 80%.
|
|
19
|
+
*/
|
|
20
|
+
function ContextMeter({ segments, limit, className, }) {
|
|
21
|
+
const used = segments.reduce((n, s) => n + s.tokens, 0);
|
|
22
|
+
const pct = Math.min(1, used / limit);
|
|
23
|
+
const warn = pct > 0.8;
|
|
24
|
+
const r = 7;
|
|
25
|
+
const c = 2 * Math.PI * r;
|
|
26
|
+
return (_jsxs(Popover, { children: [_jsxs(PopoverTrigger, { "aria-label": `Context: ${k(used)} of ${k(limit)} tokens used`, className: cn("inline-flex items-center gap-1.5 rounded-md px-1.5 py-1 text-2xs tabular-nums outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/40", warn ? "text-warning-text" : "text-muted-foreground", className), children: [_jsxs("svg", { viewBox: "0 0 18 18", className: "size-4 -rotate-90", "aria-hidden": true, children: [_jsx("circle", { cx: "9", cy: "9", r: r, fill: "none", stroke: "var(--muted)", strokeWidth: "2.5" }), _jsx("circle", { cx: "9", cy: "9", r: r, fill: "none", stroke: warn ? "var(--warning)" : "var(--primary)", strokeWidth: "2.5", strokeDasharray: `${pct * c} ${c}`, strokeLinecap: "round" })] }), Math.round(pct * 100), "%"] }), _jsxs(PopoverContent, { align: "start", className: "w-72 p-3", children: [_jsxs("p", { className: "flex items-baseline justify-between text-sm font-medium", children: ["Context ", _jsxs("span", { className: "text-xs font-normal text-muted-foreground tabular-nums", children: [k(used), " / ", k(limit), " tokens"] })] }), _jsx("div", { "aria-hidden": true, className: "mt-2 flex h-2 overflow-hidden rounded-full bg-muted", children: segments.map((s) => (_jsx("span", { style: { width: `${(s.tokens / limit) * 100}%`, background: kindColor[s.kind] } }, s.label))) }), _jsx("ul", { className: "mt-3 flex flex-col gap-1.5", children: [...segments]
|
|
27
|
+
.sort((a, b) => b.tokens - a.tokens)
|
|
28
|
+
.map((s) => (_jsxs("li", { className: "flex items-center gap-2 text-xs", children: [_jsx("span", { "aria-hidden": true, className: "size-2 rounded-xs", style: { background: kindColor[s.kind] } }), _jsx("span", { className: "flex-1", children: s.label }), _jsx("span", { className: "text-muted-foreground tabular-nums", children: k(s.tokens) })] }, s.label))) }), warn && _jsx("p", { className: "mt-3 text-xs text-warning-text", children: "Nearly full. Older thread turns will be summarised to make room." })] })] }));
|
|
29
|
+
}
|
|
30
|
+
export { ContextMeter };
|