@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,40 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { UIMessage } from "ai";
|
|
3
|
+
import type { UseChatHelpers } from "@ai-sdk/react";
|
|
4
|
+
import type { EntityRef, Message } from "../lib/chat.js";
|
|
5
|
+
import { type FromAiSdkOptions } from "./ai-sdk.js";
|
|
6
|
+
export type PendingApproval = {
|
|
7
|
+
id: string;
|
|
8
|
+
toolCallId: string;
|
|
9
|
+
tool: string;
|
|
10
|
+
input: unknown;
|
|
11
|
+
reason?: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Wires a `useChat()` result to Candor: messages for <Thread>/<Message>, props
|
|
15
|
+
* for <Composer>, and the tool calls waiting on a person. Configure useChat
|
|
16
|
+
* however you like (transport, tools, onFinish); this only reads it.
|
|
17
|
+
*
|
|
18
|
+
* Mentions in the composer are sent as structured references in the request
|
|
19
|
+
* body (`references`), not pasted into the text, so the server gets ids.
|
|
20
|
+
*
|
|
21
|
+
* const chat = useChat({ transport })
|
|
22
|
+
* const { messages, composer, approvals, respond } = useCandorChat(chat, { agent })
|
|
23
|
+
* <Thread footer={<Composer {...composer} />}>{messages.map((m) => <Message key={m.id} message={m} />)}</Thread>
|
|
24
|
+
*/
|
|
25
|
+
export declare function useCandorChat(chat: UseChatHelpers<UIMessage>, options: Omit<FromAiSdkOptions, "chatStatus">): {
|
|
26
|
+
messages: Message[];
|
|
27
|
+
approvals: PendingApproval[];
|
|
28
|
+
respond: (approvalId: string, approved: boolean, reason?: string) => void | PromiseLike<void>;
|
|
29
|
+
/** The request failed; say so, and offer to try again with `chat.regenerate()`. */
|
|
30
|
+
error: string | undefined;
|
|
31
|
+
composer: {
|
|
32
|
+
value: string;
|
|
33
|
+
onChange: React.Dispatch<React.SetStateAction<string>>;
|
|
34
|
+
state: "streaming" | "ready" | "sending";
|
|
35
|
+
references: EntityRef[];
|
|
36
|
+
onReferencesChange: React.Dispatch<React.SetStateAction<EntityRef[]>>;
|
|
37
|
+
onStop: () => Promise<void>;
|
|
38
|
+
onSubmit: (text: string, refs: EntityRef[]) => void;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { fromAiSdkMessages } from "./ai-sdk.js";
|
|
3
|
+
/**
|
|
4
|
+
* Wires a `useChat()` result to Candor: messages for <Thread>/<Message>, props
|
|
5
|
+
* for <Composer>, and the tool calls waiting on a person. Configure useChat
|
|
6
|
+
* however you like (transport, tools, onFinish); this only reads it.
|
|
7
|
+
*
|
|
8
|
+
* Mentions in the composer are sent as structured references in the request
|
|
9
|
+
* body (`references`), not pasted into the text, so the server gets ids.
|
|
10
|
+
*
|
|
11
|
+
* const chat = useChat({ transport })
|
|
12
|
+
* const { messages, composer, approvals, respond } = useCandorChat(chat, { agent })
|
|
13
|
+
* <Thread footer={<Composer {...composer} />}>{messages.map((m) => <Message key={m.id} message={m} />)}</Thread>
|
|
14
|
+
*/
|
|
15
|
+
export function useCandorChat(chat, options) {
|
|
16
|
+
const [value, setValue] = React.useState("");
|
|
17
|
+
const [references, setReferences] = React.useState([]);
|
|
18
|
+
const { agent, createdAt, toolTitle, summarize } = options;
|
|
19
|
+
const messages = React.useMemo(() => fromAiSdkMessages(chat.messages, { agent, createdAt, toolTitle, summarize, chatStatus: chat.status }), [chat.messages, chat.status, agent, createdAt, toolTitle, summarize]);
|
|
20
|
+
const state = chat.status === "submitted" ? "sending" : chat.status === "streaming" ? "streaming" : "ready";
|
|
21
|
+
const approvals = React.useMemo(() => {
|
|
22
|
+
const last = chat.messages.findLast((m) => m.role === "assistant");
|
|
23
|
+
if (!last)
|
|
24
|
+
return [];
|
|
25
|
+
return last.parts.flatMap((p) => {
|
|
26
|
+
const t = p;
|
|
27
|
+
if (t.state !== "approval-requested" || !t.approval || !t.toolCallId)
|
|
28
|
+
return [];
|
|
29
|
+
return [{ id: t.approval.id, toolCallId: t.toolCallId, tool: t.toolName ?? t.type.replace(/^tool-/, ""), input: t.input, reason: t.approval.requestReason }];
|
|
30
|
+
});
|
|
31
|
+
}, [chat.messages]);
|
|
32
|
+
const { sendMessage, stop, addToolApprovalResponse } = chat;
|
|
33
|
+
const respond = React.useCallback((approvalId, approved, reason) => addToolApprovalResponse({ id: approvalId, approved, reason }), [addToolApprovalResponse]);
|
|
34
|
+
return {
|
|
35
|
+
messages,
|
|
36
|
+
approvals,
|
|
37
|
+
respond,
|
|
38
|
+
/** The request failed; say so, and offer to try again with `chat.regenerate()`. */
|
|
39
|
+
error: chat.error?.message,
|
|
40
|
+
composer: {
|
|
41
|
+
value,
|
|
42
|
+
onChange: setValue,
|
|
43
|
+
state,
|
|
44
|
+
references,
|
|
45
|
+
onReferencesChange: setReferences,
|
|
46
|
+
onStop: stop,
|
|
47
|
+
onSubmit: (text, refs) => {
|
|
48
|
+
if (!text.trim())
|
|
49
|
+
return;
|
|
50
|
+
void sendMessage({ text }, refs.length ? { body: { references: refs } } : undefined);
|
|
51
|
+
setValue("");
|
|
52
|
+
setReferences([]);
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { EntityRef } from "../lib/chat.js";
|
|
3
|
+
import { type FromClaudeAgentOptions } from "./claude-agent-sdk.js";
|
|
4
|
+
export type UseClaudeAgentOptions = Omit<FromClaudeAgentOptions, "running"> & {
|
|
5
|
+
/** Your route that runs the agent with streamClaudeAgent. It receives `{ prompt, references, sessionId, ...body }`. */
|
|
6
|
+
api: string;
|
|
7
|
+
/** Your route that calls answerClaudeAgentApproval. It receives `{ id, approved, reason }`. */
|
|
8
|
+
approveApi: string;
|
|
9
|
+
/** Extra fields for every request, e.g. which project the agent works on. */
|
|
10
|
+
body?: Record<string, unknown>;
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
/** Replaces the global fetch, e.g. to add auth or to play a script in tests and demos. */
|
|
13
|
+
fetch?: typeof fetch;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Wires a Claude Agent SDK server route to Candor: messages for <Thread>/<Message>,
|
|
17
|
+
* props for <Composer>, and the tool calls waiting on a person. The same shape as
|
|
18
|
+
* useCandorChat, so screens work with either.
|
|
19
|
+
*
|
|
20
|
+
* const { messages, composer, approvals, respond } = useClaudeAgent({ api: "/api/agent", approveApi: "/api/agent/approve", agent })
|
|
21
|
+
* <Thread footer={<Composer {...composer} />}>{messages.map((m) => <Message key={m.id} message={m} />)}</Thread>
|
|
22
|
+
*
|
|
23
|
+
* Each message continues the same agent session (the SDK's `resume`). Mentions are
|
|
24
|
+
* sent as structured `references`, not pasted into the text.
|
|
25
|
+
*/
|
|
26
|
+
export declare function useClaudeAgent(options: UseClaudeAgentOptions): {
|
|
27
|
+
messages: import("../index.js").MessageData[];
|
|
28
|
+
approvals: import("./claude-agent-sdk.js").ClaudeApproval[];
|
|
29
|
+
respond: (approvalId: string, approved: boolean, reason?: string) => Promise<void>;
|
|
30
|
+
/** What the SDK measured for the session: cost (its estimate), tokens and turns. */
|
|
31
|
+
usage: import("./claude-agent-sdk.js").ClaudeAgentUsage | undefined;
|
|
32
|
+
/** A passing problem the agent is handling itself, such as a retried API call. */
|
|
33
|
+
notice: string | undefined;
|
|
34
|
+
/** The request failed; say so, and let the person send again. */
|
|
35
|
+
error: string | undefined;
|
|
36
|
+
sessionId: string | undefined;
|
|
37
|
+
/** Forget the conversation and start a new session. */
|
|
38
|
+
reset: () => void;
|
|
39
|
+
composer: {
|
|
40
|
+
value: string;
|
|
41
|
+
onChange: React.Dispatch<React.SetStateAction<string>>;
|
|
42
|
+
state: "streaming" | "ready" | "sending";
|
|
43
|
+
references: EntityRef[];
|
|
44
|
+
onReferencesChange: React.Dispatch<React.SetStateAction<EntityRef[]>>;
|
|
45
|
+
onStop: () => void | undefined;
|
|
46
|
+
onSubmit: (text: string, refs: EntityRef[]) => void;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { fromClaudeAgentEvents } from "./claude-agent-sdk.js";
|
|
3
|
+
/**
|
|
4
|
+
* Wires a Claude Agent SDK server route to Candor: messages for <Thread>/<Message>,
|
|
5
|
+
* props for <Composer>, and the tool calls waiting on a person. The same shape as
|
|
6
|
+
* useCandorChat, so screens work with either.
|
|
7
|
+
*
|
|
8
|
+
* const { messages, composer, approvals, respond } = useClaudeAgent({ api: "/api/agent", approveApi: "/api/agent/approve", agent })
|
|
9
|
+
* <Thread footer={<Composer {...composer} />}>{messages.map((m) => <Message key={m.id} message={m} />)}</Thread>
|
|
10
|
+
*
|
|
11
|
+
* Each message continues the same agent session (the SDK's `resume`). Mentions are
|
|
12
|
+
* sent as structured `references`, not pasted into the text.
|
|
13
|
+
*/
|
|
14
|
+
export function useClaudeAgent(options) {
|
|
15
|
+
const { api, approveApi, body, headers, agent, toolTitle, fetch: fetcher } = options;
|
|
16
|
+
const [events, setEvents] = React.useState([]);
|
|
17
|
+
const [running, setRunning] = React.useState(false);
|
|
18
|
+
const [error, setError] = React.useState();
|
|
19
|
+
const [value, setValue] = React.useState("");
|
|
20
|
+
const [references, setReferences] = React.useState([]);
|
|
21
|
+
const run = React.useRef(null);
|
|
22
|
+
const state = React.useMemo(() => fromClaudeAgentEvents(events, { agent, toolTitle, running }), [events, agent, toolTitle, running]);
|
|
23
|
+
const session = React.useRef(undefined);
|
|
24
|
+
React.useEffect(() => {
|
|
25
|
+
session.current = state.sessionId;
|
|
26
|
+
}, [state.sessionId]);
|
|
27
|
+
// Stop the agent if the component goes away mid-run.
|
|
28
|
+
React.useEffect(() => () => run.current?.abort(), []);
|
|
29
|
+
const send = React.useCallback(async (text, refs) => {
|
|
30
|
+
if (run.current)
|
|
31
|
+
return;
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
run.current = controller;
|
|
34
|
+
setError(undefined);
|
|
35
|
+
setRunning(true);
|
|
36
|
+
setEvents((e) => [...e, { type: "candor_user", id: crypto.randomUUID(), text, references: refs.length ? refs : undefined }]);
|
|
37
|
+
// Streamed text arrives a few characters at a time; apply it once per frame.
|
|
38
|
+
const pending = { events: [], frame: 0 };
|
|
39
|
+
const flush = () => {
|
|
40
|
+
pending.frame = 0;
|
|
41
|
+
const batch = pending.events.splice(0);
|
|
42
|
+
if (batch.length)
|
|
43
|
+
setEvents((e) => [...e, ...batch]);
|
|
44
|
+
};
|
|
45
|
+
try {
|
|
46
|
+
const res = await (fetcher ?? fetch)(api, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { "content-type": "application/json", ...headers },
|
|
49
|
+
body: JSON.stringify({ ...body, prompt: text, references: refs, sessionId: session.current }),
|
|
50
|
+
signal: controller.signal,
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok || !res.body)
|
|
53
|
+
throw new Error(`The agent request failed (${res.status}).`);
|
|
54
|
+
for await (const event of readEvents(res.body)) {
|
|
55
|
+
pending.events.push(event);
|
|
56
|
+
pending.frame ||= requestAnimationFrame(flush);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (!controller.signal.aborted) {
|
|
61
|
+
const message = err instanceof Error ? err.message : "The agent request failed.";
|
|
62
|
+
pending.events.push({ type: "candor_error", message });
|
|
63
|
+
setError(message);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
cancelAnimationFrame(pending.frame);
|
|
68
|
+
flush();
|
|
69
|
+
run.current = null;
|
|
70
|
+
setRunning(false);
|
|
71
|
+
}
|
|
72
|
+
}, [api, body, headers, fetcher]);
|
|
73
|
+
const respond = React.useCallback(async (approvalId, approved, reason) => {
|
|
74
|
+
try {
|
|
75
|
+
const res = await (fetcher ?? fetch)(approveApi, { method: "POST", headers: { "content-type": "application/json", ...headers }, body: JSON.stringify({ id: approvalId, approved, reason }) });
|
|
76
|
+
const ok = res.ok && (await res.json()).ok;
|
|
77
|
+
if (!ok)
|
|
78
|
+
setError("Your answer didn’t reach the agent. The request may have ended; try sending the message again.");
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
setError("Your answer didn’t reach the agent. Check your connection and try again.");
|
|
82
|
+
}
|
|
83
|
+
}, [approveApi, headers, fetcher]);
|
|
84
|
+
const stop = React.useCallback(() => run.current?.abort(), []);
|
|
85
|
+
const composerState = running ? (state.messages.at(-1)?.author.role === "agent" ? "streaming" : "sending") : "ready";
|
|
86
|
+
return {
|
|
87
|
+
messages: state.messages,
|
|
88
|
+
approvals: state.approvals,
|
|
89
|
+
respond,
|
|
90
|
+
/** What the SDK measured for the session: cost (its estimate), tokens and turns. */
|
|
91
|
+
usage: state.usage,
|
|
92
|
+
/** A passing problem the agent is handling itself, such as a retried API call. */
|
|
93
|
+
notice: state.notice,
|
|
94
|
+
/** The request failed; say so, and let the person send again. */
|
|
95
|
+
error,
|
|
96
|
+
sessionId: state.sessionId,
|
|
97
|
+
/** Forget the conversation and start a new session. */
|
|
98
|
+
reset: () => {
|
|
99
|
+
run.current?.abort();
|
|
100
|
+
setEvents([]);
|
|
101
|
+
setError(undefined);
|
|
102
|
+
},
|
|
103
|
+
composer: {
|
|
104
|
+
value,
|
|
105
|
+
onChange: setValue,
|
|
106
|
+
state: composerState,
|
|
107
|
+
references,
|
|
108
|
+
onReferencesChange: setReferences,
|
|
109
|
+
onStop: stop,
|
|
110
|
+
onSubmit: (text, refs) => {
|
|
111
|
+
if (!text.trim() || running)
|
|
112
|
+
return;
|
|
113
|
+
void send(text, refs);
|
|
114
|
+
setValue("");
|
|
115
|
+
setReferences([]);
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Newline-delimited JSON events from the server; a malformed line is skipped. */
|
|
121
|
+
async function* readEvents(stream) {
|
|
122
|
+
const reader = stream.getReader();
|
|
123
|
+
const decoder = new TextDecoder();
|
|
124
|
+
let buffer = "";
|
|
125
|
+
for (;;) {
|
|
126
|
+
const { done, value } = await reader.read();
|
|
127
|
+
buffer += decoder.decode(value, { stream: !done });
|
|
128
|
+
const lines = buffer.split("\n");
|
|
129
|
+
buffer = done ? "" : (lines.pop() ?? "");
|
|
130
|
+
for (const line of lines) {
|
|
131
|
+
if (!line.trim())
|
|
132
|
+
continue;
|
|
133
|
+
try {
|
|
134
|
+
yield JSON.parse(line);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
/* a partial or corrupt line; the next one carries on */
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (done)
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
package/dist/ag-ui.d.ts
ADDED
package/dist/ag-ui.js
ADDED
package/dist/ai-sdk.d.ts
ADDED
package/dist/ai-sdk.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A value a person copies somewhere else (a callback URL, a scope list). The
|
|
3
|
+
* whole field is the copy target; it says "Copied" only once the clipboard
|
|
4
|
+
* took the value.
|
|
5
|
+
*/
|
|
6
|
+
declare function CopyField({ label, value, className, }: {
|
|
7
|
+
/** What the value is, shown above it. */
|
|
8
|
+
label: string;
|
|
9
|
+
/** The text that is shown and copied. */
|
|
10
|
+
value: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
13
|
+
export { CopyField };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Check, Copy, X } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { useCopy } from "../lib/use-copy.js";
|
|
6
|
+
/**
|
|
7
|
+
* A value a person copies somewhere else (a callback URL, a scope list). The
|
|
8
|
+
* whole field is the copy target; it says "Copied" only once the clipboard
|
|
9
|
+
* took the value.
|
|
10
|
+
*/
|
|
11
|
+
function CopyField({ label, value, className, }) {
|
|
12
|
+
const { state, copy } = useCopy();
|
|
13
|
+
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-left outline-none transition-colors duration-150 hover:border-border-strong focus-visible:ring-[3px] focus-visible:ring-ring/40", children: [_jsx("code", { className: "min-w-0 flex-1 truncate font-mono text-xs", children: value }), _jsx("span", { "aria-live": "polite", 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, { className: "size-3 text-success-text" }), " Copied"] })) : state === "failed" ? (_jsxs(_Fragment, { children: [_jsx(X, { className: "size-3 text-destructive-text" }), " Couldn\u2019t copy"] })) : (_jsxs(_Fragment, { children: [_jsx(Copy, { className: "size-3" }), " Copy"] })) })] })] }));
|
|
14
|
+
}
|
|
15
|
+
export { CopyField };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Something couldn't be done. Says what went wrong, what is missing because of
|
|
3
|
+
* it, and offers a retry only when the caller can actually retry (Candor's
|
|
4
|
+
* DegradedPanel: a degraded state names what's missing, never just looks sad).
|
|
5
|
+
*/
|
|
6
|
+
declare function DegradedPanel({ reason, missing, onRetry, retrying, variant, className, }: {
|
|
7
|
+
/** What went wrong, in a sentence. */
|
|
8
|
+
reason: React.ReactNode;
|
|
9
|
+
/** What the person won't see because of it. */
|
|
10
|
+
missing?: React.ReactNode;
|
|
11
|
+
/** Shows a Try again button that calls this. */
|
|
12
|
+
onRetry?: () => void;
|
|
13
|
+
/** Shows the retry as in progress. */
|
|
14
|
+
retrying?: boolean;
|
|
15
|
+
/** A boxed panel, or one line inside other content. */
|
|
16
|
+
variant?: "block" | "inline";
|
|
17
|
+
className?: string;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
19
|
+
export { DegradedPanel };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { CircleAlert, RotateCw } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { Button } from "../ui/button.js";
|
|
6
|
+
/**
|
|
7
|
+
* Something couldn't be done. Says what went wrong, what is missing because of
|
|
8
|
+
* it, and offers a retry only when the caller can actually retry (Candor's
|
|
9
|
+
* DegradedPanel: a degraded state names what's missing, never just looks sad).
|
|
10
|
+
*/
|
|
11
|
+
function DegradedPanel({ reason, missing, onRetry, retrying = false, variant = "block", className, }) {
|
|
12
|
+
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: ["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 && "animate-spin") }), " ", retrying ? "Retrying" : "Retry"] }))] }));
|
|
13
|
+
}
|
|
14
|
+
export { DegradedPanel };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
type Lockable = React.ReactElement<{
|
|
3
|
+
"aria-disabled"?: boolean | "true" | "false";
|
|
4
|
+
"aria-describedby"?: string;
|
|
5
|
+
onClick?: React.MouseEventHandler;
|
|
6
|
+
onKeyDown?: React.KeyboardEventHandler;
|
|
7
|
+
"data-locked"?: string;
|
|
8
|
+
}>;
|
|
9
|
+
/**
|
|
10
|
+
* The honest-state rule: a control that cannot be used says why.
|
|
11
|
+
*
|
|
12
|
+
* Wrap any single control. With a `reason`, the control stays focusable but is
|
|
13
|
+
* marked `aria-disabled`, its activation is blocked (click, Enter, Space), and
|
|
14
|
+
* it is described by the reason — so keyboard and screen-reader users reach
|
|
15
|
+
* the explanation on the control itself. The reason shows as a tooltip, or
|
|
16
|
+
* printed under the control with `inline`.
|
|
17
|
+
*/
|
|
18
|
+
declare function Locked({ reason, inline, children, className, }: {
|
|
19
|
+
/** Why the control can't be used. Without one, the control renders as normal. */
|
|
20
|
+
reason?: string | null;
|
|
21
|
+
/** Print the reason under the control instead of in a tooltip. */
|
|
22
|
+
inline?: boolean;
|
|
23
|
+
children: Lockable;
|
|
24
|
+
className?: string;
|
|
25
|
+
}): React.JSX.Element;
|
|
26
|
+
export { Locked };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { LockKeyhole } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
|
|
7
|
+
/**
|
|
8
|
+
* The honest-state rule: a control that cannot be used says why.
|
|
9
|
+
*
|
|
10
|
+
* Wrap any single control. With a `reason`, the control stays focusable but is
|
|
11
|
+
* marked `aria-disabled`, its activation is blocked (click, Enter, Space), and
|
|
12
|
+
* it is described by the reason — so keyboard and screen-reader users reach
|
|
13
|
+
* the explanation on the control itself. The reason shows as a tooltip, or
|
|
14
|
+
* printed under the control with `inline`.
|
|
15
|
+
*/
|
|
16
|
+
function Locked({ reason, inline = false, children, className, }) {
|
|
17
|
+
const id = React.useId();
|
|
18
|
+
if (!reason)
|
|
19
|
+
return children;
|
|
20
|
+
const block = (e) => {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
e.stopPropagation();
|
|
23
|
+
};
|
|
24
|
+
const control = React.cloneElement(children, {
|
|
25
|
+
"aria-disabled": true,
|
|
26
|
+
"aria-describedby": id,
|
|
27
|
+
"data-locked": "",
|
|
28
|
+
onClick: block,
|
|
29
|
+
onKeyDown: (e) => {
|
|
30
|
+
if (e.key === "Enter" || e.key === " ")
|
|
31
|
+
block(e);
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
return (_jsxs("span", { "data-slot": "locked", className: cn("relative inline-flex flex-col items-start gap-1.5", className), children: [inline ? (control) : (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: control }), _jsx(TooltipContent, { children: reason })] })), _jsxs("span", { id: id, className: cn("items-center gap-1 text-xs text-muted-foreground", inline ? "inline-flex" : "sr-only"), children: [_jsx(LockKeyhole, { "aria-hidden": true, className: "size-3" }), reason] })] }));
|
|
35
|
+
}
|
|
36
|
+
export { Locked };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export declare function formatRelative(date: Date, now?: Date): string;
|
|
3
|
+
/** The current time in milliseconds, updated every 30 seconds; null while rendering on the server. */
|
|
4
|
+
export declare function useNow(): number | null;
|
|
5
|
+
/** "3 minutes ago" / "in 6 days", kept fresh, with the exact time on hover. Shows the date until the clock is known. */
|
|
6
|
+
declare function RelativeTime({ date, bare, className, }: {
|
|
7
|
+
/** When it happened. Shown as "5 minutes ago", with the exact time on hover. */
|
|
8
|
+
date: Date | string;
|
|
9
|
+
/** Drop "ago" and "in", for tight spaces: "5 minutes". */
|
|
10
|
+
bare?: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
}): React.JSX.Element;
|
|
13
|
+
export { RelativeTime };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
const units = [
|
|
5
|
+
["year", 31_536_000],
|
|
6
|
+
["month", 2_592_000],
|
|
7
|
+
["week", 604_800],
|
|
8
|
+
["day", 86_400],
|
|
9
|
+
["hour", 3_600],
|
|
10
|
+
["minute", 60],
|
|
11
|
+
["second", 1],
|
|
12
|
+
];
|
|
13
|
+
const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" });
|
|
14
|
+
export function formatRelative(date, now = new Date()) {
|
|
15
|
+
const seconds = Math.round((date.getTime() - now.getTime()) / 1000);
|
|
16
|
+
if (Math.abs(seconds) < 10)
|
|
17
|
+
return "just now";
|
|
18
|
+
for (const [unit, size] of units) {
|
|
19
|
+
if (Math.abs(seconds) >= size || unit === "second")
|
|
20
|
+
return rtf.format(Math.round(seconds / size), unit);
|
|
21
|
+
}
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
/*
|
|
25
|
+
* One shared clock for every relative time on the page. During server rendering (and React's hydration pass) there's
|
|
26
|
+
* no "now" to read, so useNow returns null and components show a fixed date until the browser takes over. That keeps
|
|
27
|
+
* server and browser output identical, and lets frameworks prerender pages without reading the clock.
|
|
28
|
+
*/
|
|
29
|
+
const listeners = new Set();
|
|
30
|
+
let now = 0;
|
|
31
|
+
let timer;
|
|
32
|
+
function subscribe(onChange) {
|
|
33
|
+
listeners.add(onChange);
|
|
34
|
+
if (!timer) {
|
|
35
|
+
now = Date.now();
|
|
36
|
+
timer = setInterval(() => {
|
|
37
|
+
now = Date.now();
|
|
38
|
+
listeners.forEach((l) => l());
|
|
39
|
+
}, 30_000);
|
|
40
|
+
}
|
|
41
|
+
return () => {
|
|
42
|
+
listeners.delete(onChange);
|
|
43
|
+
if (!listeners.size) {
|
|
44
|
+
clearInterval(timer);
|
|
45
|
+
timer = undefined;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** The current time in milliseconds, updated every 30 seconds; null while rendering on the server. */
|
|
50
|
+
export function useNow() {
|
|
51
|
+
return React.useSyncExternalStore(subscribe, () => now || (now = Date.now()), () => null);
|
|
52
|
+
}
|
|
53
|
+
/** "3 minutes ago" / "in 6 days", kept fresh, with the exact time on hover. Shows the date until the clock is known. */
|
|
54
|
+
function RelativeTime({ date, bare = false, className, }) {
|
|
55
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
56
|
+
const current = useNow();
|
|
57
|
+
let text = current === null ? d.toISOString().slice(0, 10) : formatRelative(d, new Date(current));
|
|
58
|
+
if (bare)
|
|
59
|
+
text = text.replace(/ ago$/, "").replace(/^in /, "");
|
|
60
|
+
return (_jsx("time", { dateTime: d.toISOString(), title: current === null ? undefined : d.toLocaleString(), className: className, children: text }));
|
|
61
|
+
}
|
|
62
|
+
export { RelativeTime };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type RunStatus } from "../lib/status.js";
|
|
2
|
+
/**
|
|
3
|
+
* A small status mark. Running and waiting pulse, since they are the two
|
|
4
|
+
* states a person may need to watch or act on. The label is always available
|
|
5
|
+
* to assistive tech; pass `showLabel` to print it too.
|
|
6
|
+
*/
|
|
7
|
+
declare function StatusDot({ status, showLabel, label, className, }: {
|
|
8
|
+
/** Which status to show; running and waiting pulse. */
|
|
9
|
+
status: RunStatus;
|
|
10
|
+
/** Print the status name next to the dot. */
|
|
11
|
+
showLabel?: boolean;
|
|
12
|
+
/** Words to use instead of the status name. */
|
|
13
|
+
label?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
16
|
+
export { StatusDot };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
import { statusLabel } from "../lib/status.js";
|
|
4
|
+
const dotColor = {
|
|
5
|
+
idle: "bg-faint-foreground/60",
|
|
6
|
+
queued: "bg-faint-foreground",
|
|
7
|
+
running: "bg-primary text-accent-text",
|
|
8
|
+
waiting: "bg-warning text-warning-text",
|
|
9
|
+
blocked: "bg-transparent ring-[1.5px] ring-inset ring-faint-foreground",
|
|
10
|
+
succeeded: "bg-success",
|
|
11
|
+
failed: "bg-destructive",
|
|
12
|
+
cancelled: "bg-faint-foreground/60",
|
|
13
|
+
skipped: "bg-faint-foreground/40",
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A small status mark. Running and waiting pulse, since they are the two
|
|
17
|
+
* states a person may need to watch or act on. The label is always available
|
|
18
|
+
* to assistive tech; pass `showLabel` to print it too.
|
|
19
|
+
*/
|
|
20
|
+
function StatusDot({ status, showLabel = false, label, className, }) {
|
|
21
|
+
const text = label ?? statusLabel[status];
|
|
22
|
+
const live = status === "running" || status === "waiting";
|
|
23
|
+
return (_jsxs("span", { "data-slot": "status-dot", "data-status": status, 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 && "animate-pulse-dot") }), showLabel ? _jsx("span", { children: text }) : _jsx("span", { className: "sr-only", children: text })] }));
|
|
24
|
+
}
|
|
25
|
+
export { StatusDot };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type RunStatus } from "../lib/status.js";
|
|
3
|
+
export type Step = {
|
|
4
|
+
id: string;
|
|
5
|
+
title: React.ReactNode;
|
|
6
|
+
status: RunStatus;
|
|
7
|
+
detail?: React.ReactNode;
|
|
8
|
+
meta?: React.ReactNode;
|
|
9
|
+
};
|
|
10
|
+
declare function StepMark({ status, }: {
|
|
11
|
+
/** The step's status, which picks its mark. */
|
|
12
|
+
status: RunStatus;
|
|
13
|
+
}): React.JSX.Element;
|
|
14
|
+
/**
|
|
15
|
+
* An ordered list of steps joined by a rail: an agent's plan, its reasoning,
|
|
16
|
+
* a run's stages. The rail fills as steps finish.
|
|
17
|
+
*/
|
|
18
|
+
declare function StepList({ steps, className, }: {
|
|
19
|
+
/** The steps in order, each with a status, and optional detail and meta. */
|
|
20
|
+
steps: Step[];
|
|
21
|
+
className?: string;
|
|
22
|
+
}): React.JSX.Element;
|
|
23
|
+
export { StepList, StepMark };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Check, CircleSlash, Hand, LoaderCircle, X } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { statusLabel } from "../lib/status.js";
|
|
6
|
+
function StepMark({ status, }) {
|
|
7
|
+
const base = "relative z-10 flex size-5 shrink-0 items-center justify-center rounded-full";
|
|
8
|
+
switch (status) {
|
|
9
|
+
case "succeeded":
|
|
10
|
+
return (_jsx("span", { className: cn(base, "bg-success text-on-success"), children: _jsx(Check, { className: "size-3", strokeWidth: 3 }) }));
|
|
11
|
+
case "failed":
|
|
12
|
+
return (_jsx("span", { className: cn(base, "bg-destructive text-on-destructive"), children: _jsx(X, { className: "size-3", strokeWidth: 3 }) }));
|
|
13
|
+
case "running":
|
|
14
|
+
return (_jsx("span", { className: cn(base, "bg-accent-weak text-accent-text"), children: _jsx(LoaderCircle, { className: "size-3.5 animate-spin", strokeWidth: 2.5 }) }));
|
|
15
|
+
case "waiting":
|
|
16
|
+
return (_jsx("span", { className: cn(base, "bg-warning-weak text-warning-text"), children: _jsx(Hand, { className: "size-3", strokeWidth: 2.5 }) }));
|
|
17
|
+
case "blocked":
|
|
18
|
+
return (_jsx("span", { className: cn(base, "border-[1.5px] border-dashed border-faint-foreground bg-card") }));
|
|
19
|
+
case "cancelled":
|
|
20
|
+
case "skipped":
|
|
21
|
+
return (_jsx("span", { className: cn(base, "bg-muted text-faint-foreground"), children: _jsx(CircleSlash, { className: "size-3", strokeWidth: 2.5 }) }));
|
|
22
|
+
default:
|
|
23
|
+
return (_jsx("span", { className: cn(base, "bg-background"), children: _jsx("span", { className: "size-2 rounded-full border-[1.5px] border-faint-foreground/70" }) }));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* An ordered list of steps joined by a rail: an agent's plan, its reasoning,
|
|
28
|
+
* a run's stages. The rail fills as steps finish.
|
|
29
|
+
*/
|
|
30
|
+
function StepList({ steps, className, }) {
|
|
31
|
+
return (_jsx("ol", { "data-slot": "step-list", className: cn("flex flex-col", className), children: steps.map((step, i) => {
|
|
32
|
+
const last = i === steps.length - 1;
|
|
33
|
+
const done = step.status === "succeeded";
|
|
34
|
+
return (_jsxs("li", { className: "relative flex gap-3 pb-4 last:pb-0 animate-rise-in", style: { animationDelay: `${i * 40}ms` }, children: [!last && (_jsx("span", { "aria-hidden": true, className: cn("absolute top-5 bottom-0 left-[9.5px] w-px transition-colors duration-500", done ? "bg-success/50" : "bg-input") })), _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 })] })] }, step.id));
|
|
35
|
+
}) }));
|
|
36
|
+
}
|
|
37
|
+
export { StepList, StepMark };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text that is still arriving. Shows a caret while `streaming`, and announces
|
|
3
|
+
* politely so screen readers hear the finished text rather than every token.
|
|
4
|
+
*/
|
|
5
|
+
declare function StreamingText({ text, streaming, className, }: {
|
|
6
|
+
/** The text so far. */
|
|
7
|
+
text: string;
|
|
8
|
+
/** Show a caret while more is arriving; announced once when it stops. */
|
|
9
|
+
streaming?: boolean;
|
|
10
|
+
className?: string;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
12
|
+
/** A label that shimmers while an agent is working ("Thinking", "Reading the docs"). */
|
|
13
|
+
declare function ShimmerLabel({ children, className, }: {
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
17
|
+
export { StreamingText, ShimmerLabel };
|