forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCopilotChat.tsx
More file actions
295 lines (267 loc) · 8.48 KB
/
Copy pathCopilotChat.tsx
File metadata and controls
295 lines (267 loc) · 8.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
import React, {
createContext,
useCallback,
useContext,
useEffect,
useRef,
} from "react";
import type { ReactNode } from "react";
import { useAgent } from "@copilotkit/react-core/v2/headless";
import { useCopilotKit } from "@copilotkit/react-core/v2/context";
import { DEFAULT_AGENT_ID, randomUUID } from "@copilotkit/shared";
import type { InputContent } from "@copilotkit/shared";
import type { CopilotKitCoreErrorCode } from "@copilotkit/core";
import { useAttachments } from "./hooks/use-attachments";
import type { NativeAttachmentsConfig } from "./hooks/use-attachments";
import type { Attachment } from "@copilotkit/shared";
// ---------------------------------------------------------------------------
// Context
// ---------------------------------------------------------------------------
export interface CopilotChatContextValue {
/** The resolved agent instance. */
agent: any;
/** Whether the agent is currently running. */
isRunning: boolean;
/** Current messages in the conversation. */
messages: any[];
/** Currently selected attachments (uploading + ready). */
attachments: Attachment[];
/** Whether attachments are enabled. */
attachmentsEnabled: boolean;
/** Open the native document picker to add files. */
openPicker: () => Promise<void>;
/** Remove an attachment by ID. */
removeAttachment: (id: string) => void;
/**
* Submit a message with optional attachments.
* Handles consuming ready attachments, building InputContent[],
* calling agent.addMessage, and running the agent.
*/
submitMessage: (text: string) => Promise<void>;
}
const CopilotChatCtx = createContext<CopilotChatContextValue | null>(null);
/**
* Hook to access the CopilotChat context from child components.
* Must be called inside a `<CopilotChat>` component tree.
*/
export function useCopilotChatContext(): CopilotChatContextValue {
const ctx = useContext(CopilotChatCtx);
if (!ctx) {
throw new Error(
"useCopilotChatContext must be used within a <CopilotChat> component",
);
}
return ctx;
}
// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
export interface CopilotChatBaseProps {
/**
* The agent ID to use for this chat session.
* Matches the web SDK's CopilotChat `agentId` prop.
*
* Resolution order: `agentId` > `agentName` > `"default"`
*/
agentId?: string;
/**
* @deprecated Use `agentId` instead. `agentName` is kept for backwards
* compatibility and will be removed in a future release.
*/
agentName?: string;
/**
* Thread ID for this chat session. When provided, the chat will resume
* the specified thread. Matches the web SDK's CopilotChat `threadId` prop.
*/
threadId?: string;
/**
* Error handler scoped to this chat's agent. Fires in addition to the
* provider-level onError (does not suppress it). Receives only errors
* whose context.agentId matches this chat's agent.
*/
onError?: (event: {
error: Error;
code: CopilotKitCoreErrorCode;
context: Record<string, any>;
}) => void | Promise<void>;
/**
* Throttle interval (in milliseconds) for re-renders triggered by message
* change notifications. Overrides the provider-level `defaultThrottleMs`
* for this chat instance. Forwarded to the internal `useAgent()` hook.
*
* @default undefined -- inherits from provider `defaultThrottleMs`;
* if that is also unset, re-renders are unthrottled.
*/
throttleMs?: number;
/**
* Enable multimodal file attachments (images, audio, video, documents).
* Pass a NativeAttachmentsConfig object to configure file picking behavior.
*/
attachments?: NativeAttachmentsConfig;
/**
* Optional children rendered inside the chat context.
*/
children?: ReactNode;
}
export interface CopilotChatProps extends CopilotChatBaseProps {
/** Passthrough props are forwarded to consumers via the agent context. */
[key: string]: unknown;
}
/**
* Headless CopilotChat component for React Native.
*
* Wires up the `useAgent` hook with `agentId` resolution and renders children.
* Unlike the web SDK's CopilotChat, this component does not render any UI
* elements -- consumers provide their own React Native views.
*
* Children can access chat state via `useCopilotChatContext()`.
*
* ```tsx
* import { CopilotChat, useCopilotChatContext } from "@copilotkit/react-native";
*
* function MyChatUI() {
* const { messages, submitMessage, attachments, openPicker } = useCopilotChatContext();
* // ... render your UI
* }
*
* <CopilotChat agentId="my-agent" attachments={{ enabled: true }}>
* <MyChatUI />
* </CopilotChat>
* ```
*/
export function CopilotChat({
agentId,
agentName,
threadId,
onError,
throttleMs,
attachments: attachmentsConfig,
children,
..._rest
}: CopilotChatProps) {
const resolvedAgentId = agentId ?? agentName ?? DEFAULT_AGENT_ID;
// Deprecation warning (dev only, fires once per mount)
const warnedRef = useRef(false);
useEffect(() => {
if (
agentName !== undefined &&
agentId === undefined &&
!warnedRef.current
) {
warnedRef.current = true;
if (typeof __DEV__ === "undefined" || __DEV__) {
console.warn(
"[CopilotKit] agentName is deprecated, use agentId instead",
);
}
}
}, [agentName, agentId]);
const { agent } = useAgent({ agentId: resolvedAgentId, throttleMs });
// Set threadId on the agent when provided
useEffect(() => {
if (threadId) {
agent.threadId = threadId;
}
}, [agent, threadId]);
// onError subscription -- forward core errors scoped to this chat's agent
const { copilotkit } = useCopilotKit();
const onErrorRef = useRef(onError);
useEffect(() => {
onErrorRef.current = onError;
}, [onError]);
useEffect(() => {
if (!onErrorRef.current) return;
const subscription = copilotkit.subscribe({
onError: (event) => {
// Only forward errors that match this chat's agent
if (
event.context?.agentId === resolvedAgentId ||
!event.context?.agentId
) {
onErrorRef.current?.({
error: event.error,
code: event.code,
context: event.context,
});
}
},
});
return () => {
subscription.unsubscribe();
};
}, [copilotkit, resolvedAgentId]);
// Attachments
const {
attachments: selectedAttachments,
enabled: attachmentsEnabled,
openPicker,
removeAttachment,
consumeAttachments,
} = useAttachments({ config: attachmentsConfig });
// Submit handler -- mirrors web CopilotChat.tsx lines 234-288
const submitMessage = useCallback(
async (value: string) => {
// Block if uploads in progress
const hasUploading = selectedAttachments.some(
(a) => a.status === "uploading",
);
if (hasUploading) {
console.error(
"[CopilotKit] Cannot send while attachments are uploading",
);
return;
}
const readyAttachments = consumeAttachments();
if (readyAttachments.length > 0) {
const contentParts: InputContent[] = [];
if (value.trim()) {
contentParts.push({ type: "text", text: value });
}
for (const att of readyAttachments) {
contentParts.push({
type: att.type,
source: att.source,
metadata: {
...(att.filename ? { filename: att.filename } : {}),
...att.metadata,
},
} as InputContent);
}
agent.addMessage({
id: randomUUID(),
role: "user",
content: contentParts,
});
} else {
agent.addMessage({
id: randomUUID(),
role: "user",
content: value,
});
}
try {
await copilotkit.runAgent({ agent });
} catch (error) {
console.error("CopilotChat: runAgent failed", error);
}
},
// copilotkit is intentionally excluded -- it is a stable ref that never changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
[agent, selectedAttachments, consumeAttachments],
);
const contextValue: CopilotChatContextValue = {
agent,
isRunning: agent.isRunning,
messages: agent.messages,
attachments: selectedAttachments,
attachmentsEnabled,
openPicker,
removeAttachment,
submitMessage,
};
return (
<CopilotChatCtx.Provider value={contextValue}>
{children}
</CopilotChatCtx.Provider>
);
}