@camelai/agent-runtime-react 0.0.0 → 0.10.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 +21 -0
- package/README.md +51 -1
- package/dist/index.d.ts +100 -0
- package/dist/index.js +187 -0
- package/dist/shadcn.css +39 -0
- package/dist/styles.css +209 -0
- package/dist/ui/composer.d.ts +20 -0
- package/dist/ui/composer.js +49 -0
- package/dist/ui/context.d.ts +87 -0
- package/dist/ui/context.js +46 -0
- package/dist/ui/file.d.ts +9 -0
- package/dist/ui/file.js +25 -0
- package/dist/ui/index.d.ts +50 -0
- package/dist/ui/index.js +68 -0
- package/dist/ui/markdown.d.ts +10 -0
- package/dist/ui/markdown.js +71 -0
- package/dist/ui/messages.d.ts +23 -0
- package/dist/ui/messages.js +137 -0
- package/dist/ui/tool.d.ts +13 -0
- package/dist/ui/tool.js +127 -0
- package/package.json +48 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 qaml-ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,53 @@
|
|
|
1
1
|
# @camelai/agent-runtime-react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React hooks and components for a streaming chat with an agent on the
|
|
4
|
+
[camelAI agent runtime](https://agents.camelai.dev). Your server has one route
|
|
5
|
+
(`createAgentHandler` from `@camelai/agent-runtime/server`); these talk to it.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @camelai/agent-runtime @camelai/agent-runtime-react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Prebuilt
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { AgentChat } from "@camelai/agent-runtime-react/ui";
|
|
15
|
+
import "@camelai/agent-runtime-react/styles.css";
|
|
16
|
+
|
|
17
|
+
<AgentChat endpoint="/api/agent" suggestions={["Where is my order?"]} tools={{ get_weather: WeatherCard }} />
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Messages stream in as markdown; tool calls show as cards or your components; the agent's
|
|
21
|
+
questions and approvals are answered in place; files it hands over are previews or downloads; the
|
|
22
|
+
composer stops a run. Props: `endpoint` or `chat`, `thread`, `tools`, `components` (Markdown,
|
|
23
|
+
UserMessage, AssistantMessage, ToolFallback, InputCard, FilePreview, EmptyState), `labels`,
|
|
24
|
+
`placeholder`, `suggestions`, `theme`, `allowImages`, `showReasoning`, `whileRunning`, `data`,
|
|
25
|
+
`header`, `className`, `style`. Theme it with CSS variables (`--agent-accent`, `--agent-radius`, …),
|
|
26
|
+
or import `shadcn.css` too to follow a shadcn/ui theme.
|
|
27
|
+
|
|
28
|
+
## Hooks
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { AgentProvider, useMessages, useSend, useAgentStatus, useInputs } from "@camelai/agent-runtime-react";
|
|
32
|
+
|
|
33
|
+
<AgentProvider endpoint="/api/agent" thread={threadId}>
|
|
34
|
+
<MyChat />
|
|
35
|
+
</AgentProvider>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| Hook | Returns |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `useMessages()` | the messages, as typed parts (`text`, `reasoning`, `tool`, `file`) |
|
|
41
|
+
| `useSend()` | `send(text, { data? })`: shows the message at once, keyed by the id it keeps |
|
|
42
|
+
| `useAgentStatus()` | `{ status, isRunning, error, connected, stop, retry }` |
|
|
43
|
+
| `useInputs()` | `{ inputs, answer(input, value), decline(input) }` |
|
|
44
|
+
| `useLoadOlder()` | `{ hasOlder, loading, loadOlder }` |
|
|
45
|
+
| `useToolRenderer(name, Component)` | draws that tool's calls with your component |
|
|
46
|
+
| `useAgentSelector(select)` | any part of the snapshot, re-rendering only when it changes |
|
|
47
|
+
| `useAgent()` / `useAgentChat(options)` | the chat itself (`@camelai/agent-runtime/chat`) |
|
|
48
|
+
|
|
49
|
+
Each hook re-renders only when what it returns changes, and unchanged messages keep their objects,
|
|
50
|
+
so a streamed token re-renders one message.
|
|
51
|
+
|
|
52
|
+
See the [Frontend guide](../../docs/frontend.md) for the route, security, tools, questions,
|
|
53
|
+
threads and theming.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React bindings for an agent chat: a provider that holds the chat with the user's agent, and hooks
|
|
3
|
+
* that read it. Every hook subscribes to only what it returns, so a streamed token re-renders the
|
|
4
|
+
* message it belongs to, not the page.
|
|
5
|
+
*
|
|
6
|
+
* <AgentProvider endpoint="/api/agent">
|
|
7
|
+
* <MyChat />
|
|
8
|
+
* </AgentProvider>
|
|
9
|
+
*
|
|
10
|
+
* function MyChat() {
|
|
11
|
+
* const messages = useMessages();
|
|
12
|
+
* const send = useSend();
|
|
13
|
+
* const { status, stop } = useAgentStatus();
|
|
14
|
+
* …
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
import { type ComponentType, type ReactNode } from "react";
|
|
18
|
+
import { type AgentChat, type AgentChatOptions, type ChatError, type ChatInput, type ChatMessage, type ChatSnapshot, type ChatStatus, type InputAnswer, type InputValue, type SendOptions, type ToolPart } from "@camelai/agent-runtime/chat";
|
|
19
|
+
export type * from "@camelai/agent-runtime/chat";
|
|
20
|
+
export { answerValue, createAgentChat, projectMessages } from "@camelai/agent-runtime/chat";
|
|
21
|
+
/** What a tool renderer gets: the call, and the means to answer what it waits on. */
|
|
22
|
+
export interface ToolRenderProps {
|
|
23
|
+
part: ToolPart;
|
|
24
|
+
name: string;
|
|
25
|
+
args: Record<string, unknown>;
|
|
26
|
+
state: ToolPart["state"];
|
|
27
|
+
result: ToolPart["result"];
|
|
28
|
+
progress: ToolPart["progress"];
|
|
29
|
+
input: ChatInput | undefined;
|
|
30
|
+
/** Answer the call's input (approve with true, choose a label, fill a form). */
|
|
31
|
+
answer(value: InputValue | InputAnswer): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
export type ToolRenderer = ComponentType<ToolRenderProps>;
|
|
34
|
+
export type ToolRenderers = Record<string, ToolRenderer>;
|
|
35
|
+
export type UseAgentChatOptions = Omit<AgentChatOptions, "autoConnect">;
|
|
36
|
+
/**
|
|
37
|
+
* A chat with the user's agent, connected while the component is mounted. A new `endpoint` or
|
|
38
|
+
* `thread` is a new chat; the other options are read when used, so they may change freely.
|
|
39
|
+
*/
|
|
40
|
+
export declare function useAgentChat(options: UseAgentChatOptions): AgentChat;
|
|
41
|
+
export interface AgentProviderProps extends Partial<UseAgentChatOptions> {
|
|
42
|
+
/** A chat you made yourself (`useAgentChat` or `createAgentChat`), instead of `endpoint`. */
|
|
43
|
+
chat?: AgentChat;
|
|
44
|
+
/** Renderers for tool calls, by tool name (see `useToolRenderer`). */
|
|
45
|
+
tools?: ToolRenderers;
|
|
46
|
+
children?: ReactNode;
|
|
47
|
+
}
|
|
48
|
+
/** Holds the chat with the user's agent for the hooks and components below it. */
|
|
49
|
+
export declare function AgentProvider({ chat, tools, children, ...options }: AgentProviderProps): import("react").JSX.Element;
|
|
50
|
+
/** The chat (its actions and snapshot) from the nearest AgentProvider, or the one given. */
|
|
51
|
+
export declare function useAgent(chat?: AgentChat): AgentChat;
|
|
52
|
+
/**
|
|
53
|
+
* Part of the snapshot, re-rendering only when it changes (compared with `Object.is`, or `equal`).
|
|
54
|
+
* The snapshot's messages and parts keep their identity while unchanged, so selecting them is cheap.
|
|
55
|
+
*/
|
|
56
|
+
export declare function useAgentSelector<T>(select: (snapshot: ChatSnapshot) => T, options?: {
|
|
57
|
+
chat?: AgentChat;
|
|
58
|
+
equal?: (a: T, b: T) => boolean;
|
|
59
|
+
}): T;
|
|
60
|
+
/** The whole snapshot: status, messages, inputs, error. */
|
|
61
|
+
export declare function useAgentSnapshot(chat?: AgentChat): ChatSnapshot;
|
|
62
|
+
/** The chat's messages, oldest first. */
|
|
63
|
+
export declare function useMessages(chat?: AgentChat): ChatMessage[];
|
|
64
|
+
/** One message by id (re-renders only when that message changes). */
|
|
65
|
+
export declare function useMessage(id: string, chat?: AgentChat): ChatMessage | undefined;
|
|
66
|
+
/** Send a message: `send(text, { data })`. It shows at once and resolves once the handler took it. */
|
|
67
|
+
export declare function useSend(chat?: AgentChat): (text: string, options?: SendOptions) => Promise<{
|
|
68
|
+
id: string;
|
|
69
|
+
}>;
|
|
70
|
+
export interface AgentStatus {
|
|
71
|
+
status: ChatStatus;
|
|
72
|
+
/** A turn runs or a message is on its way: show a stop button. */
|
|
73
|
+
isRunning: boolean;
|
|
74
|
+
error: ChatError | null;
|
|
75
|
+
connected: boolean;
|
|
76
|
+
stop(): Promise<void>;
|
|
77
|
+
retry(messageId: string): Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
/** What the agent is doing, the latest error, and `stop`. */
|
|
80
|
+
export declare function useAgentStatus(chat?: AgentChat): AgentStatus;
|
|
81
|
+
/** What the agent waits on from the user, and the means to answer it. */
|
|
82
|
+
export declare function useInputs(chat?: AgentChat): {
|
|
83
|
+
inputs: ChatInput[];
|
|
84
|
+
answer: AgentChat["answer"];
|
|
85
|
+
decline: AgentChat["decline"];
|
|
86
|
+
};
|
|
87
|
+
/** Older history: whether there is more, and `loadOlder()` (with `loading` while it runs). */
|
|
88
|
+
export declare function useLoadOlder(chat?: AgentChat): {
|
|
89
|
+
hasOlder: boolean;
|
|
90
|
+
loading: boolean;
|
|
91
|
+
loadOlder(): Promise<boolean>;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Render calls of the tool `name` with `renderer` wherever this chat is shown (generative UI), while
|
|
95
|
+
* the calling component is mounted. A component's own `tools` prop comes first, then the latest
|
|
96
|
+
* registration, then the provider's `tools`.
|
|
97
|
+
*/
|
|
98
|
+
export declare function useToolRenderer(name: string, renderer: ToolRenderer): void;
|
|
99
|
+
/** The renderer for tool `name`: `extra`'s, else one registered with `useToolRenderer`, else the provider's. */
|
|
100
|
+
export declare function useToolRendererFor(name: string, extra?: ToolRenderers): ToolRenderer | undefined;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* React bindings for an agent chat: a provider that holds the chat with the user's agent, and hooks
|
|
5
|
+
* that read it. Every hook subscribes to only what it returns, so a streamed token re-renders the
|
|
6
|
+
* message it belongs to, not the page.
|
|
7
|
+
*
|
|
8
|
+
* <AgentProvider endpoint="/api/agent">
|
|
9
|
+
* <MyChat />
|
|
10
|
+
* </AgentProvider>
|
|
11
|
+
*
|
|
12
|
+
* function MyChat() {
|
|
13
|
+
* const messages = useMessages();
|
|
14
|
+
* const send = useSend();
|
|
15
|
+
* const { status, stop } = useAgentStatus();
|
|
16
|
+
* …
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
20
|
+
import { createAgentChat, } from "@camelai/agent-runtime/chat";
|
|
21
|
+
export { answerValue, createAgentChat, projectMessages } from "@camelai/agent-runtime/chat";
|
|
22
|
+
function createRegistry() {
|
|
23
|
+
const renderers = new Map();
|
|
24
|
+
const listeners = new Set();
|
|
25
|
+
let version = 0;
|
|
26
|
+
const changed = () => { version++; for (const listener of [...listeners])
|
|
27
|
+
listener(); };
|
|
28
|
+
return {
|
|
29
|
+
get: name => renderers.get(name)?.at(-1),
|
|
30
|
+
set(name, renderer) {
|
|
31
|
+
renderers.set(name, [...renderers.get(name) ?? [], renderer]);
|
|
32
|
+
changed();
|
|
33
|
+
return () => { renderers.set(name, (renderers.get(name) ?? []).filter(item => item !== renderer)); changed(); };
|
|
34
|
+
},
|
|
35
|
+
subscribe(listener) { listeners.add(listener); return () => { listeners.delete(listener); }; },
|
|
36
|
+
version: () => version,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const AgentContext = createContext(null);
|
|
40
|
+
/**
|
|
41
|
+
* A chat with the user's agent, connected while the component is mounted. A new `endpoint` or
|
|
42
|
+
* `thread` is a new chat; the other options are read when used, so they may change freely.
|
|
43
|
+
*/
|
|
44
|
+
export function useAgentChat(options) {
|
|
45
|
+
const latest = useRef(options);
|
|
46
|
+
latest.current = options;
|
|
47
|
+
const key = `${options.endpoint}\u0000${options.thread ?? ""}`;
|
|
48
|
+
const make = () => createAgentChat({
|
|
49
|
+
...options, autoConnect: false,
|
|
50
|
+
headers: () => { const headers = latest.current.headers; return typeof headers === "function" ? headers() : headers ?? {}; },
|
|
51
|
+
onEvent: event => latest.current.onEvent?.(event),
|
|
52
|
+
onError: error => latest.current.onError?.(error),
|
|
53
|
+
});
|
|
54
|
+
const [held, setHeld] = useState(() => ({ key, chat: make() }));
|
|
55
|
+
let chat = held.chat;
|
|
56
|
+
if (held.key !== key) {
|
|
57
|
+
chat = make();
|
|
58
|
+
setHeld({ key, chat });
|
|
59
|
+
}
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
chat.connect();
|
|
62
|
+
return () => chat.disconnect();
|
|
63
|
+
}, [chat]);
|
|
64
|
+
return chat;
|
|
65
|
+
}
|
|
66
|
+
/** Holds the chat with the user's agent for the hooks and components below it. */
|
|
67
|
+
export function AgentProvider({ chat, tools, children, ...options }) {
|
|
68
|
+
if (chat)
|
|
69
|
+
return _jsx(Provide, { chat: chat, tools: tools, children: children });
|
|
70
|
+
if (!options.endpoint)
|
|
71
|
+
throw new Error("AgentProvider needs an endpoint (your agent handler's URL, e.g. \"/api/agent\") or a chat");
|
|
72
|
+
return _jsx(ProvideOwn, { ...options, tools: tools, children: children });
|
|
73
|
+
}
|
|
74
|
+
function ProvideOwn({ tools, children, ...options }) {
|
|
75
|
+
return _jsx(Provide, { chat: useAgentChat(options), tools: tools, children: children });
|
|
76
|
+
}
|
|
77
|
+
function Provide({ chat, tools, children }) {
|
|
78
|
+
const parent = useContext(AgentContext);
|
|
79
|
+
const [registry] = useState(createRegistry);
|
|
80
|
+
const value = useMemo(() => ({ chat, registry, tools: { ...parent?.tools, ...tools } }), [chat, registry, parent?.tools, tools]);
|
|
81
|
+
return _jsx(AgentContext.Provider, { value: value, children: children });
|
|
82
|
+
}
|
|
83
|
+
function useContextValue(hook) {
|
|
84
|
+
const value = useContext(AgentContext);
|
|
85
|
+
if (!value)
|
|
86
|
+
throw new Error(`${hook} must be used inside <AgentProvider>`);
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
/** The chat (its actions and snapshot) from the nearest AgentProvider, or the one given. */
|
|
90
|
+
export function useAgent(chat) {
|
|
91
|
+
const context = useContext(AgentContext);
|
|
92
|
+
const found = chat ?? context?.chat;
|
|
93
|
+
if (!found)
|
|
94
|
+
throw new Error("useAgent must be used inside <AgentProvider> (or given a chat)");
|
|
95
|
+
return found;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Part of the snapshot, re-rendering only when it changes (compared with `Object.is`, or `equal`).
|
|
99
|
+
* The snapshot's messages and parts keep their identity while unchanged, so selecting them is cheap.
|
|
100
|
+
*/
|
|
101
|
+
export function useAgentSelector(select, options = {}) {
|
|
102
|
+
const chat = useAgent(options.chat);
|
|
103
|
+
const cache = useRef(null);
|
|
104
|
+
const read = () => {
|
|
105
|
+
const snapshot = chat.getSnapshot();
|
|
106
|
+
const cached = cache.current;
|
|
107
|
+
if (cached && cached.snapshot === snapshot && cached.select === select)
|
|
108
|
+
return cached.value;
|
|
109
|
+
const value = select(snapshot);
|
|
110
|
+
const kept = cached && (options.equal ?? Object.is)(cached.value, value) ? cached.value : value;
|
|
111
|
+
cache.current = { snapshot, select, value: kept };
|
|
112
|
+
return kept;
|
|
113
|
+
};
|
|
114
|
+
return useSyncExternalStore(chat.subscribe, read, read);
|
|
115
|
+
}
|
|
116
|
+
/** The whole snapshot: status, messages, inputs, error. */
|
|
117
|
+
export function useAgentSnapshot(chat) {
|
|
118
|
+
const found = useAgent(chat);
|
|
119
|
+
return useSyncExternalStore(found.subscribe, found.getSnapshot, found.getSnapshot);
|
|
120
|
+
}
|
|
121
|
+
/** The chat's messages, oldest first. */
|
|
122
|
+
export function useMessages(chat) {
|
|
123
|
+
return useAgentSelector(snapshot => snapshot.messages, { chat });
|
|
124
|
+
}
|
|
125
|
+
/** One message by id (re-renders only when that message changes). */
|
|
126
|
+
export function useMessage(id, chat) {
|
|
127
|
+
return useAgentSelector(snapshot => snapshot.messages.find(message => message.id === id), { chat });
|
|
128
|
+
}
|
|
129
|
+
/** Send a message: `send(text, { data })`. It shows at once and resolves once the handler took it. */
|
|
130
|
+
export function useSend(chat) {
|
|
131
|
+
const found = useAgent(chat);
|
|
132
|
+
return useCallback((text, options) => options ? found.send(text, options) : found.send(text), [found]);
|
|
133
|
+
}
|
|
134
|
+
/** What the agent is doing, the latest error, and `stop`. */
|
|
135
|
+
export function useAgentStatus(chat) {
|
|
136
|
+
const found = useAgent(chat);
|
|
137
|
+
const status = useAgentSelector(snapshot => snapshot.status, { chat: found });
|
|
138
|
+
const error = useAgentSelector(snapshot => snapshot.error, { chat: found });
|
|
139
|
+
const connected = useAgentSelector(snapshot => snapshot.connected, { chat: found });
|
|
140
|
+
return useMemo(() => ({
|
|
141
|
+
status, error, connected, isRunning: status === "submitted" || status === "streaming",
|
|
142
|
+
stop: () => found.stop(), retry: (id) => found.retry(id),
|
|
143
|
+
}), [status, error, connected, found]);
|
|
144
|
+
}
|
|
145
|
+
/** What the agent waits on from the user, and the means to answer it. */
|
|
146
|
+
export function useInputs(chat) {
|
|
147
|
+
const found = useAgent(chat);
|
|
148
|
+
const inputs = useAgentSelector(snapshot => snapshot.inputs, { chat: found });
|
|
149
|
+
return useMemo(() => ({ inputs, answer: found.answer, decline: found.decline }), [inputs, found]);
|
|
150
|
+
}
|
|
151
|
+
/** Older history: whether there is more, and `loadOlder()` (with `loading` while it runs). */
|
|
152
|
+
export function useLoadOlder(chat) {
|
|
153
|
+
const found = useAgent(chat);
|
|
154
|
+
const hasOlder = useAgentSelector(snapshot => snapshot.hasOlder, { chat: found });
|
|
155
|
+
const [loading, setLoading] = useState(false);
|
|
156
|
+
const busy = useRef(false);
|
|
157
|
+
const loadOlder = useCallback(async () => {
|
|
158
|
+
if (busy.current)
|
|
159
|
+
return false;
|
|
160
|
+
busy.current = true;
|
|
161
|
+
setLoading(true);
|
|
162
|
+
try {
|
|
163
|
+
return await found.loadOlder();
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
busy.current = false;
|
|
167
|
+
setLoading(false);
|
|
168
|
+
}
|
|
169
|
+
}, [found]);
|
|
170
|
+
return { hasOlder, loading, loadOlder };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Render calls of the tool `name` with `renderer` wherever this chat is shown (generative UI), while
|
|
174
|
+
* the calling component is mounted. A component's own `tools` prop comes first, then the latest
|
|
175
|
+
* registration, then the provider's `tools`.
|
|
176
|
+
*/
|
|
177
|
+
export function useToolRenderer(name, renderer) {
|
|
178
|
+
const { registry } = useContextValue("useToolRenderer");
|
|
179
|
+
useEffect(() => registry.set(name, renderer), [registry, name, renderer]);
|
|
180
|
+
}
|
|
181
|
+
/** The renderer for tool `name`: `extra`'s, else one registered with `useToolRenderer`, else the provider's. */
|
|
182
|
+
export function useToolRendererFor(name, extra) {
|
|
183
|
+
const context = useContext(AgentContext);
|
|
184
|
+
const subscribe = useCallback((listener) => context?.registry.subscribe(listener) ?? (() => { }), [context]);
|
|
185
|
+
useSyncExternalStore(subscribe, () => context?.registry.version() ?? 0, () => 0);
|
|
186
|
+
return extra?.[name] ?? context?.registry.get(name) ?? context?.tools[name];
|
|
187
|
+
}
|
package/dist/shadcn.css
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @camelai/agent-runtime-react/shadcn.css: <AgentChat> in your shadcn/ui theme. Import it after
|
|
3
|
+
* styles.css; it maps the chat's variables onto shadcn's tokens (--background, --foreground, …), so
|
|
4
|
+
* the chat follows your theme and its .dark class.
|
|
5
|
+
*/
|
|
6
|
+
:where(.agent-chat),
|
|
7
|
+
:where(.dark, [data-theme="dark"]) :where(.agent-chat:not([data-theme="light"])),
|
|
8
|
+
:where(.agent-chat[data-theme="dark"]) {
|
|
9
|
+
--agent-bg: var(--background);
|
|
10
|
+
--agent-fg: var(--foreground);
|
|
11
|
+
--agent-muted: var(--muted);
|
|
12
|
+
--agent-muted-fg: var(--muted-foreground);
|
|
13
|
+
--agent-border: var(--border);
|
|
14
|
+
--agent-accent: var(--primary);
|
|
15
|
+
--agent-accent-fg: var(--primary-foreground);
|
|
16
|
+
--agent-user-bg: var(--secondary);
|
|
17
|
+
--agent-user-fg: var(--secondary-foreground);
|
|
18
|
+
--agent-code-bg: var(--muted);
|
|
19
|
+
--agent-danger: var(--destructive);
|
|
20
|
+
--agent-focus: var(--ring);
|
|
21
|
+
--agent-radius: var(--radius);
|
|
22
|
+
--agent-font: inherit;
|
|
23
|
+
}
|
|
24
|
+
@media (prefers-color-scheme: dark) {
|
|
25
|
+
:where(.agent-chat:not([data-theme="light"], .light .agent-chat, [data-theme="light"] .agent-chat)) {
|
|
26
|
+
--agent-bg: var(--background);
|
|
27
|
+
--agent-fg: var(--foreground);
|
|
28
|
+
--agent-muted: var(--muted);
|
|
29
|
+
--agent-muted-fg: var(--muted-foreground);
|
|
30
|
+
--agent-border: var(--border);
|
|
31
|
+
--agent-accent: var(--primary);
|
|
32
|
+
--agent-accent-fg: var(--primary-foreground);
|
|
33
|
+
--agent-user-bg: var(--secondary);
|
|
34
|
+
--agent-user-fg: var(--secondary-foreground);
|
|
35
|
+
--agent-code-bg: var(--muted);
|
|
36
|
+
--agent-danger: var(--destructive);
|
|
37
|
+
--agent-focus: var(--ring);
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @camelai/agent-runtime-react/styles.css: the default look of <AgentChat>.
|
|
3
|
+
* Theme it with the variables below (on .agent-chat or any ancestor); every rule has zero specificity
|
|
4
|
+
* beyond its class (:where), so your own CSS wins without !important.
|
|
5
|
+
* Dark mode follows the system unless theme="light"/"dark" is set, or an ancestor has .dark/.light
|
|
6
|
+
* (or data-theme="dark"/"light").
|
|
7
|
+
*/
|
|
8
|
+
:where(.agent-chat) {
|
|
9
|
+
--agent-bg: #ffffff;
|
|
10
|
+
--agent-fg: #0a0a0a;
|
|
11
|
+
--agent-muted: #f4f4f5;
|
|
12
|
+
--agent-muted-fg: #52525b;
|
|
13
|
+
--agent-border: #e4e4e7;
|
|
14
|
+
--agent-accent: #18181b;
|
|
15
|
+
--agent-accent-fg: #fafafa;
|
|
16
|
+
--agent-user-bg: #f4f4f5;
|
|
17
|
+
--agent-user-fg: var(--agent-fg);
|
|
18
|
+
--agent-code-bg: #f4f4f5;
|
|
19
|
+
--agent-danger: #b91c1c;
|
|
20
|
+
--agent-success: #15803d;
|
|
21
|
+
--agent-focus: #2563eb;
|
|
22
|
+
--agent-radius: 12px;
|
|
23
|
+
--agent-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
24
|
+
--agent-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
25
|
+
--agent-font-size: 15px;
|
|
26
|
+
--agent-line-height: 1.6;
|
|
27
|
+
--agent-max-width: 760px;
|
|
28
|
+
--agent-composer-max-height: 200px;
|
|
29
|
+
}
|
|
30
|
+
:where(.dark, [data-theme="dark"]) :where(.agent-chat:not([data-theme="light"])),
|
|
31
|
+
:where(.agent-chat[data-theme="dark"]) {
|
|
32
|
+
--agent-bg: #0a0a0a;
|
|
33
|
+
--agent-fg: #fafafa;
|
|
34
|
+
--agent-muted: #18181b;
|
|
35
|
+
--agent-muted-fg: #a1a1aa;
|
|
36
|
+
--agent-border: #27272a;
|
|
37
|
+
--agent-accent: #fafafa;
|
|
38
|
+
--agent-accent-fg: #0a0a0a;
|
|
39
|
+
--agent-user-bg: #27272a;
|
|
40
|
+
--agent-code-bg: #18181b;
|
|
41
|
+
--agent-danger: #f87171;
|
|
42
|
+
--agent-success: #4ade80;
|
|
43
|
+
--agent-focus: #60a5fa;
|
|
44
|
+
}
|
|
45
|
+
@media (prefers-color-scheme: dark) {
|
|
46
|
+
:where(.agent-chat:not([data-theme="light"], .light .agent-chat, [data-theme="light"] .agent-chat)) {
|
|
47
|
+
--agent-bg: #0a0a0a;
|
|
48
|
+
--agent-fg: #fafafa;
|
|
49
|
+
--agent-muted: #18181b;
|
|
50
|
+
--agent-muted-fg: #a1a1aa;
|
|
51
|
+
--agent-border: #27272a;
|
|
52
|
+
--agent-accent: #fafafa;
|
|
53
|
+
--agent-accent-fg: #0a0a0a;
|
|
54
|
+
--agent-user-bg: #27272a;
|
|
55
|
+
--agent-code-bg: #18181b;
|
|
56
|
+
--agent-danger: #f87171;
|
|
57
|
+
--agent-success: #4ade80;
|
|
58
|
+
--agent-focus: #60a5fa;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
:where(.agent-chat) {
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
height: 100%;
|
|
66
|
+
min-height: 0;
|
|
67
|
+
background: var(--agent-bg);
|
|
68
|
+
color: var(--agent-fg);
|
|
69
|
+
font-family: var(--agent-font);
|
|
70
|
+
font-size: var(--agent-font-size);
|
|
71
|
+
line-height: var(--agent-line-height);
|
|
72
|
+
color-scheme: light dark;
|
|
73
|
+
box-sizing: border-box;
|
|
74
|
+
}
|
|
75
|
+
:where(.agent-chat) *, :where(.agent-chat) *::before, :where(.agent-chat) *::after { box-sizing: inherit; }
|
|
76
|
+
:where(.agent-chat :focus-visible) { outline: 2px solid var(--agent-focus); outline-offset: 2px; }
|
|
77
|
+
:where(.agent-chat button) { font: inherit; color: inherit; cursor: pointer; }
|
|
78
|
+
:where(.agent-chat button:disabled) { cursor: not-allowed; opacity: 0.5; }
|
|
79
|
+
|
|
80
|
+
:where(.agent-chat__viewport) { position: relative; flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
|
|
81
|
+
:where(.agent-chat__messages) { flex: 1 1 auto; overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable; }
|
|
82
|
+
:where(.agent-chat__messages):focus-visible { outline-offset: -2px; }
|
|
83
|
+
:where(.agent-chat__content) { max-width: var(--agent-max-width); margin: 0 auto; padding: 16px; display: flex; flex-direction: column; gap: 16px; }
|
|
84
|
+
|
|
85
|
+
:where(.agent-chat__message) { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
|
|
86
|
+
:where(.agent-chat__message--user) { align-items: flex-end; }
|
|
87
|
+
:where(.agent-chat__bubble) { max-width: 85%; background: var(--agent-user-bg); color: var(--agent-user-fg); padding: 8px 14px; border-radius: var(--agent-radius); overflow-wrap: anywhere; }
|
|
88
|
+
:where(.agent-chat__message--user[data-status="sending"]) :where(.agent-chat__bubble) { opacity: 0.7; }
|
|
89
|
+
:where(.agent-chat__user-text) { margin: 0; white-space: pre-wrap; }
|
|
90
|
+
:where(.agent-chat__user-image) { max-width: 240px; border-radius: calc(var(--agent-radius) - 4px); display: block; }
|
|
91
|
+
:where(.agent-chat__author) { font-size: 0.8em; color: var(--agent-muted-fg); }
|
|
92
|
+
:where(.agent-chat__note) { font-size: 0.85em; color: var(--agent-muted-fg); }
|
|
93
|
+
:where(.agent-chat__note--error) { color: var(--agent-danger); }
|
|
94
|
+
:where(.agent-chat__link-button) { background: none; border: 0; padding: 0; text-decoration: underline; }
|
|
95
|
+
|
|
96
|
+
/* Markdown */
|
|
97
|
+
:where(.agent-chat__markdown) { overflow-wrap: anywhere; }
|
|
98
|
+
:where(.agent-chat__markdown) > :first-child { margin-top: 0; }
|
|
99
|
+
:where(.agent-chat__markdown) > :last-child { margin-bottom: 0; }
|
|
100
|
+
:where(.agent-chat__markdown) :where(p, ul, ol, blockquote, .agent-chat__code, .agent-chat__table) { margin: 0 0 0.75em; }
|
|
101
|
+
:where(.agent-chat__markdown) :where(h1, h2, h3, h4, h5, h6) { margin: 1em 0 0.5em; line-height: 1.3; }
|
|
102
|
+
:where(.agent-chat__markdown) h1 { font-size: 1.4em; }
|
|
103
|
+
:where(.agent-chat__markdown) h2 { font-size: 1.25em; }
|
|
104
|
+
:where(.agent-chat__markdown) h3 { font-size: 1.1em; }
|
|
105
|
+
:where(.agent-chat__markdown) :where(ul, ol) { padding-left: 1.5em; }
|
|
106
|
+
:where(.agent-chat__markdown) li + li { margin-top: 0.25em; }
|
|
107
|
+
:where(.agent-chat__markdown) blockquote { border-left: 3px solid var(--agent-border); padding-left: 12px; color: var(--agent-muted-fg); }
|
|
108
|
+
:where(.agent-chat__markdown) a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
|
|
109
|
+
:where(.agent-chat__markdown) img { max-width: 100%; border-radius: calc(var(--agent-radius) - 4px); }
|
|
110
|
+
:where(.agent-chat__markdown) hr { border: 0; border-top: 1px solid var(--agent-border); margin: 1em 0; }
|
|
111
|
+
:where(.agent-chat__markdown) :not(pre) > code { font-family: var(--agent-mono); font-size: 0.9em; background: var(--agent-code-bg); padding: 0.1em 0.35em; border-radius: 4px; }
|
|
112
|
+
:where(.agent-chat__code) { border: 1px solid var(--agent-border); border-radius: calc(var(--agent-radius) - 4px); overflow: hidden; background: var(--agent-code-bg); }
|
|
113
|
+
:where(.agent-chat__code-bar) { display: flex; justify-content: space-between; align-items: center; padding: 4px 10px; font-size: 0.8em; color: var(--agent-muted-fg); border-bottom: 1px solid var(--agent-border); }
|
|
114
|
+
:where(.agent-chat__code-copy) { background: none; border: 0; padding: 2px 6px; border-radius: 4px; color: var(--agent-muted-fg); }
|
|
115
|
+
:where(.agent-chat__code-copy):hover { color: var(--agent-fg); }
|
|
116
|
+
:where(.agent-chat pre) { margin: 0; padding: 10px 12px; overflow-x: auto; font-family: var(--agent-mono); font-size: 0.85em; line-height: 1.5; }
|
|
117
|
+
:where(.agent-chat__table) { overflow-x: auto; }
|
|
118
|
+
:where(.agent-chat__table) table { border-collapse: collapse; font-size: 0.95em; }
|
|
119
|
+
:where(.agent-chat__table) :where(th, td) { border: 1px solid var(--agent-border); padding: 4px 10px; text-align: left; }
|
|
120
|
+
:where(.agent-chat__table) th { background: var(--agent-muted); }
|
|
121
|
+
|
|
122
|
+
/* Streaming caret after the last block of the message being written */
|
|
123
|
+
:where(.agent-chat__message[data-streaming]) > :where(.agent-chat__markdown):last-child > :last-child::after {
|
|
124
|
+
content: ""; display: inline-block; width: 0.5em; height: 1em; margin-left: 2px; vertical-align: -0.15em;
|
|
125
|
+
background: currentColor; opacity: 0.6; animation: agent-chat-blink 1s steps(2) infinite;
|
|
126
|
+
}
|
|
127
|
+
@keyframes agent-chat-blink { 50% { opacity: 0; } }
|
|
128
|
+
|
|
129
|
+
/* Reasoning, tools */
|
|
130
|
+
:where(.agent-chat__reasoning, .agent-chat__tool) { border: 1px solid var(--agent-border); border-radius: calc(var(--agent-radius) - 4px); font-size: 0.9em; }
|
|
131
|
+
:where(.agent-chat__reasoning, .agent-chat__tool) > summary { cursor: pointer; padding: 6px 10px; color: var(--agent-muted-fg); display: flex; align-items: center; gap: 8px; list-style: none; }
|
|
132
|
+
:where(.agent-chat__reasoning, .agent-chat__tool) > summary::-webkit-details-marker { display: none; }
|
|
133
|
+
:where(.agent-chat__reasoning, .agent-chat__tool) > summary::after { content: ""; margin-left: auto; width: 6px; height: 6px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(-45deg); transition: transform 0.15s; }
|
|
134
|
+
:where(.agent-chat__reasoning, .agent-chat__tool)[open] > summary::after { transform: rotate(45deg); }
|
|
135
|
+
:where(.agent-chat__reasoning-text) { padding: 0 10px 10px; white-space: pre-wrap; color: var(--agent-muted-fg); }
|
|
136
|
+
:where(.agent-chat__tool-body) { padding: 0 10px 10px; display: flex; flex-direction: column; gap: 8px; }
|
|
137
|
+
:where(.agent-chat__tool-body) pre { background: var(--agent-code-bg); border-radius: 6px; max-height: 320px; }
|
|
138
|
+
:where(.agent-chat__tool-heading) { font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.04em; color: var(--agent-muted-fg); margin-bottom: 4px; }
|
|
139
|
+
:where(.agent-chat__tool-progress-text) { font-size: 0.9em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
140
|
+
:where(.agent-chat__tool) progress { width: 100%; accent-color: var(--agent-accent); }
|
|
141
|
+
:where(.agent-chat__tool-icon) { width: 12px; height: 12px; border-radius: 50%; flex: none; border: 2px solid var(--agent-muted-fg); border-top-color: transparent; animation: agent-chat-spin 0.8s linear infinite; }
|
|
142
|
+
:where(.agent-chat__tool[data-state="done"]) :where(.agent-chat__tool-icon) { animation: none; border: 0; background: var(--agent-success); }
|
|
143
|
+
:where(.agent-chat__tool[data-state="error"]) :where(.agent-chat__tool-icon) { animation: none; border: 0; background: var(--agent-danger); }
|
|
144
|
+
:where(.agent-chat__tool[data-state="input_required"]) :where(.agent-chat__tool-icon) { animation: none; border-color: var(--agent-focus); }
|
|
145
|
+
@keyframes agent-chat-spin { to { transform: rotate(360deg); } }
|
|
146
|
+
|
|
147
|
+
/* Inputs: approvals, questions, forms */
|
|
148
|
+
:where(.agent-chat__input) { border: 1px solid var(--agent-border); border-radius: calc(var(--agent-radius) - 2px); padding: 12px; display: flex; flex-direction: column; gap: 10px; background: var(--agent-bg); }
|
|
149
|
+
:where(.agent-chat__input) pre { background: var(--agent-code-bg); border-radius: 6px; max-height: 240px; }
|
|
150
|
+
:where(.agent-chat__input-message) { margin: 0; font-weight: 500; }
|
|
151
|
+
:where(.agent-chat__input-error) { margin: 0; color: var(--agent-danger); font-size: 0.9em; }
|
|
152
|
+
:where(.agent-chat__input) fieldset { border: 0; padding: 0; margin: 0 0 10px; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
153
|
+
:where(.agent-chat__input) legend { font-weight: 500; padding: 0; margin-bottom: 6px; }
|
|
154
|
+
:where(.agent-chat__question-header) { display: inline-block; font-size: 0.75em; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--agent-muted-fg); margin-right: 6px; }
|
|
155
|
+
:where(.agent-chat__option) { display: flex; gap: 8px; align-items: flex-start; padding: 6px 8px; border: 1px solid var(--agent-border); border-radius: 8px; cursor: pointer; }
|
|
156
|
+
:where(.agent-chat__option):has(input:checked) { border-color: var(--agent-accent); }
|
|
157
|
+
:where(.agent-chat__option) input { margin-top: 0.3em; accent-color: var(--agent-accent); }
|
|
158
|
+
:where(.agent-chat__option-description) { display: block; font-size: 0.85em; color: var(--agent-muted-fg); }
|
|
159
|
+
:where(.agent-chat__field) { display: flex; flex-direction: column; gap: 4px; }
|
|
160
|
+
:where(.agent-chat__field-description) { font-size: 0.85em; color: var(--agent-muted-fg); }
|
|
161
|
+
:where(.agent-chat__text-input, .agent-chat__input select) { font: inherit; color: inherit; background: var(--agent-bg); border: 1px solid var(--agent-border); border-radius: 8px; padding: 6px 10px; }
|
|
162
|
+
:where(.agent-chat__input-actions) { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
163
|
+
:where(.agent-chat__button) { display: inline-flex; align-items: center; justify-content: center; padding: 6px 14px; border-radius: 8px; border: 1px solid var(--agent-border); background: var(--agent-bg); color: var(--agent-fg); text-decoration: none; font-size: 0.95em; }
|
|
164
|
+
:where(.agent-chat__button):hover:not(:disabled) { background: var(--agent-muted); }
|
|
165
|
+
:where(.agent-chat__button--primary) { background: var(--agent-accent); color: var(--agent-accent-fg); border-color: var(--agent-accent); }
|
|
166
|
+
:where(.agent-chat__button--primary):hover:not(:disabled) { background: var(--agent-accent); opacity: 0.9; }
|
|
167
|
+
:where(.agent-chat__loose-inputs) { max-width: var(--agent-max-width); width: 100%; margin: 0 auto; padding: 0 16px; }
|
|
168
|
+
|
|
169
|
+
/* Files */
|
|
170
|
+
:where(.agent-chat__file) { margin: 0; display: flex; flex-direction: column; gap: 6px; max-width: 420px; }
|
|
171
|
+
:where(.agent-chat__file) img { max-width: 100%; max-height: 360px; border-radius: calc(var(--agent-radius) - 4px); border: 1px solid var(--agent-border); display: block; }
|
|
172
|
+
:where(.agent-chat__file-link) { display: flex; flex-direction: column; padding: 8px 12px; border: 1px solid var(--agent-border); border-radius: calc(var(--agent-radius) - 4px); color: inherit; text-decoration: none; }
|
|
173
|
+
:where(.agent-chat__file[data-kind="image"]) :where(.agent-chat__file-link) { border: 0; padding: 0; flex-direction: row; gap: 8px; font-size: 0.85em; }
|
|
174
|
+
:where(.agent-chat__file-link):hover { background: var(--agent-muted); }
|
|
175
|
+
:where(.agent-chat__file-link[aria-disabled]) { pointer-events: none; opacity: 0.6; }
|
|
176
|
+
:where(.agent-chat__file-name) { font-weight: 500; overflow-wrap: anywhere; }
|
|
177
|
+
:where(.agent-chat__file-meta, .agent-chat__file-caption) { font-size: 0.85em; color: var(--agent-muted-fg); }
|
|
178
|
+
|
|
179
|
+
/* Waiting, empty, older, jump, errors */
|
|
180
|
+
:where(.agent-chat__thinking) { display: flex; align-items: center; gap: 4px; color: var(--agent-muted-fg); }
|
|
181
|
+
:where(.agent-chat__thinking) > span:not(.agent-chat__thinking-text) { width: 6px; height: 6px; border-radius: 50%; background: currentColor; animation: agent-chat-pulse 1.2s ease-in-out infinite; }
|
|
182
|
+
:where(.agent-chat__thinking) > span:nth-child(2) { animation-delay: 0.15s; }
|
|
183
|
+
:where(.agent-chat__thinking) > span:nth-child(3) { animation-delay: 0.3s; }
|
|
184
|
+
:where(.agent-chat__thinking-text) { margin-left: 6px; font-size: 0.9em; }
|
|
185
|
+
@keyframes agent-chat-pulse { 0%, 100% { opacity: 0.25; } 50% { opacity: 1; } }
|
|
186
|
+
:where(.agent-chat__empty) { margin: auto; padding: 48px 0 24px; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 16px; }
|
|
187
|
+
:where(.agent-chat__empty-title) { margin: 0; font-size: 1.25em; font-weight: 600; }
|
|
188
|
+
:where(.agent-chat__suggestions) { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
|
|
189
|
+
:where(.agent-chat__suggestion) { border: 1px solid var(--agent-border); background: var(--agent-bg); border-radius: 999px; padding: 6px 14px; font-size: 0.9em; }
|
|
190
|
+
:where(.agent-chat__suggestion):hover { background: var(--agent-muted); }
|
|
191
|
+
:where(.agent-chat__older) { align-self: center; border: 1px solid var(--agent-border); background: var(--agent-bg); border-radius: 999px; padding: 4px 12px; font-size: 0.85em; color: var(--agent-muted-fg); }
|
|
192
|
+
:where(.agent-chat__jump) { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); border: 1px solid var(--agent-border); background: var(--agent-bg); border-radius: 999px; padding: 4px 12px; font-size: 0.85em; box-shadow: 0 2px 8px rgb(0 0 0 / 0.12); }
|
|
193
|
+
:where(.agent-chat__error) { max-width: var(--agent-max-width); width: calc(100% - 32px); margin: 0 auto 8px; padding: 8px 12px; border-radius: 8px; border: 1px solid var(--agent-danger); color: var(--agent-danger); font-size: 0.9em; }
|
|
194
|
+
:where(.agent-chat__error--soft) { border-color: var(--agent-border); color: var(--agent-muted-fg); }
|
|
195
|
+
|
|
196
|
+
/* Composer */
|
|
197
|
+
:where(.agent-chat__composer) { max-width: var(--agent-max-width); width: calc(100% - 32px); margin: 0 auto 16px; display: flex; align-items: flex-end; gap: 8px; border: 1px solid var(--agent-border); border-radius: calc(var(--agent-radius) + 4px); padding: 8px 8px 8px 14px; background: var(--agent-bg); }
|
|
198
|
+
:where(.agent-chat__composer):focus-within { border-color: var(--agent-muted-fg); }
|
|
199
|
+
:where(.agent-chat__composer) textarea { flex: 1; resize: none; border: 0; outline: none; background: transparent; color: inherit; font: inherit; line-height: 1.5; padding: 6px 0; max-height: var(--agent-composer-max-height); field-sizing: content; min-height: 1.5em; }
|
|
200
|
+
:where(.agent-chat__composer) textarea:focus-visible { outline: none; }
|
|
201
|
+
:where(.agent-chat__send) { flex: none; width: 34px; height: 34px; border-radius: 50%; border: 0; background: var(--agent-accent); color: var(--agent-accent-fg); display: grid; place-items: center; }
|
|
202
|
+
:where(.agent-chat__send-icon) { width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-bottom: 9px solid currentColor; }
|
|
203
|
+
:where(.agent-chat__send--stop) :where(.agent-chat__send-icon) { width: 11px; height: 11px; border: 0; border-radius: 2px; background: currentColor; }
|
|
204
|
+
|
|
205
|
+
:where(.agent-chat__sr-only) { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
|
206
|
+
|
|
207
|
+
@media (prefers-reduced-motion: reduce) {
|
|
208
|
+
:where(.agent-chat) *, :where(.agent-chat) *::before, :where(.agent-chat) *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; }
|
|
209
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ComposerProps {
|
|
2
|
+
placeholder?: string;
|
|
3
|
+
autoFocus?: boolean;
|
|
4
|
+
/** What a message sent while the agent works does (the chat's own setting by default). */
|
|
5
|
+
whileRunning?: "queue" | "steer";
|
|
6
|
+
/** JSON sent with each message, for your handler's `onSend`. */
|
|
7
|
+
data?: unknown | (() => unknown);
|
|
8
|
+
className?: string;
|
|
9
|
+
/** Called after a message is sent. */
|
|
10
|
+
onSend?: (text: string) => void;
|
|
11
|
+
}
|
|
12
|
+
export interface ComposerHandle {
|
|
13
|
+
focus(): void;
|
|
14
|
+
setText(text: string): void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The message box: Enter sends, Shift+Enter makes a new line, Escape stops the agent. While the agent
|
|
18
|
+
* works, an empty box offers Stop; typing turns it back into Send.
|
|
19
|
+
*/
|
|
20
|
+
export declare const Composer: import("react").ForwardRefExoticComponent<ComposerProps & import("react").RefAttributes<ComposerHandle>>;
|