@copilotkitnext/react 0.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.
Files changed (60) hide show
  1. package/.turbo/turbo-build$colon$css.log +9 -0
  2. package/.turbo/turbo-build.log +28 -0
  3. package/.turbo/turbo-check-types.log +0 -0
  4. package/.turbo/turbo-lint.log +78 -0
  5. package/.turbo/turbo-test.log +79 -0
  6. package/LICENSE +11 -0
  7. package/components.json +20 -0
  8. package/dist/index.d.mts +363 -0
  9. package/dist/index.d.ts +363 -0
  10. package/dist/index.js +2322 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/index.mjs +2291 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/styles.css +2 -0
  15. package/eslint.config.mjs +11 -0
  16. package/package.json +84 -0
  17. package/postcss.config.js +7 -0
  18. package/src/__tests__/setup.ts +2 -0
  19. package/src/components/chat/CopilotChat.tsx +90 -0
  20. package/src/components/chat/CopilotChatAssistantMessage.tsx +478 -0
  21. package/src/components/chat/CopilotChatAudioRecorder.tsx +157 -0
  22. package/src/components/chat/CopilotChatInput.tsx +596 -0
  23. package/src/components/chat/CopilotChatMessageView.tsx +85 -0
  24. package/src/components/chat/CopilotChatToolCallsView.tsx +43 -0
  25. package/src/components/chat/CopilotChatUserMessage.tsx +337 -0
  26. package/src/components/chat/CopilotChatView.tsx +385 -0
  27. package/src/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +684 -0
  28. package/src/components/chat/__tests__/CopilotChatInput.test.tsx +531 -0
  29. package/src/components/chat/__tests__/setup.ts +1 -0
  30. package/src/components/chat/index.ts +35 -0
  31. package/src/components/index.ts +4 -0
  32. package/src/components/ui/button.tsx +123 -0
  33. package/src/components/ui/dropdown-menu.tsx +257 -0
  34. package/src/components/ui/tooltip.tsx +59 -0
  35. package/src/hooks/index.ts +6 -0
  36. package/src/hooks/use-agent-context.tsx +17 -0
  37. package/src/hooks/use-agent.tsx +48 -0
  38. package/src/hooks/use-frontend-tool.tsx +46 -0
  39. package/src/hooks/use-human-in-the-loop.tsx +76 -0
  40. package/src/hooks/use-render-tool-call.tsx +81 -0
  41. package/src/index.ts +4 -0
  42. package/src/lib/__tests__/completePartialMarkdown.test.ts +495 -0
  43. package/src/lib/__tests__/renderSlot.test.tsx +610 -0
  44. package/src/lib/slots.tsx +55 -0
  45. package/src/lib/utils.ts +6 -0
  46. package/src/providers/CopilotChatConfigurationProvider.tsx +81 -0
  47. package/src/providers/CopilotKitProvider.tsx +269 -0
  48. package/src/providers/__tests__/CopilotKitProvider.test.tsx +487 -0
  49. package/src/providers/__tests__/CopilotKitProvider.wildcard.test.tsx +261 -0
  50. package/src/providers/index.ts +14 -0
  51. package/src/styles/globals.css +302 -0
  52. package/src/types/frontend-tool.ts +8 -0
  53. package/src/types/human-in-the-loop.ts +33 -0
  54. package/src/types/index.ts +3 -0
  55. package/src/types/react-tool-call-render.ts +29 -0
  56. package/tailwind.config.js +9 -0
  57. package/test.css +2355 -0
  58. package/tsconfig.json +23 -0
  59. package/tsup.config.ts +19 -0
  60. package/vitest.config.mjs +15 -0
@@ -0,0 +1,90 @@
1
+ import { useAgent } from "@/hooks/use-agent";
2
+ import { CopilotChatView, CopilotChatViewProps } from "./CopilotChatView";
3
+ import { CopilotChatConfigurationProvider } from "@/providers/CopilotChatConfigurationProvider";
4
+ import { DEFAULT_AGENT_ID, randomUUID } from "@copilotkitnext/shared";
5
+ import { useCallback, useState, useEffect, useMemo } from "react";
6
+ import { merge } from "ts-deepmerge";
7
+
8
+ export type CopilotChatProps = Omit<CopilotChatViewProps, "messages"> & {
9
+ agentId?: string;
10
+ threadId?: string;
11
+ };
12
+
13
+ export function CopilotChat({
14
+ agentId = DEFAULT_AGENT_ID,
15
+ threadId,
16
+ ...props
17
+ }: CopilotChatProps) {
18
+ const { agent } = useAgent({ agentId });
19
+ const [isLoading, setIsLoading] = useState(false);
20
+ threadId = threadId ?? useMemo(() => randomUUID(), []);
21
+
22
+ const subscriber = {
23
+ onTextMessageStartEvent: () => setIsLoading(false),
24
+ onToolCallStartEvent: () => setIsLoading(false),
25
+ };
26
+
27
+ useEffect(() => {
28
+ const connect = async () => {
29
+ setIsLoading(true);
30
+ await agent?.runAgent(
31
+ {
32
+ forwardedProps: { __copilotkitConnect: true },
33
+ },
34
+ subscriber
35
+ );
36
+ setIsLoading(false);
37
+ };
38
+ if (agent) {
39
+ agent.threadId = threadId;
40
+ if ("isCopilotKitAgent" in agent) {
41
+ connect();
42
+ } else {
43
+ setIsLoading(false);
44
+ }
45
+ }
46
+ return () => {};
47
+ }, [threadId, agent]);
48
+
49
+ const [inputValue, setInputValue] = useState("");
50
+ const onSubmitInput = useCallback(
51
+ async (value: string) => {
52
+ setInputValue("");
53
+ agent?.addMessage({
54
+ id: randomUUID(),
55
+ role: "user",
56
+ content: value,
57
+ });
58
+ setIsLoading(true);
59
+ await agent?.runAgent({}, subscriber);
60
+ setIsLoading(false);
61
+ },
62
+ [agent]
63
+ );
64
+
65
+ const mergedProps = merge(
66
+ {
67
+ messageView: { isLoading },
68
+ },
69
+ {
70
+ ...props,
71
+ ...(typeof props.messageView === "string"
72
+ ? { messageView: { className: props.messageView } }
73
+ : props.messageView !== undefined
74
+ ? { messageView: props.messageView }
75
+ : {}),
76
+ }
77
+ );
78
+
79
+ return (
80
+ <CopilotChatConfigurationProvider
81
+ inputValue={inputValue}
82
+ onSubmitInput={onSubmitInput}
83
+ onChangeInput={setInputValue}
84
+ >
85
+ <CopilotChatView
86
+ {...{ messages: agent?.messages ?? [], ...mergedProps }}
87
+ />
88
+ </CopilotChatConfigurationProvider>
89
+ );
90
+ }
@@ -0,0 +1,478 @@
1
+ import { AssistantMessage, Message } from "@ag-ui/core";
2
+ import { MarkdownHooks } from "react-markdown";
3
+ import remarkGfm from "remark-gfm";
4
+ import remarkMath from "remark-math";
5
+ import rehypePrettyCode from "rehype-pretty-code";
6
+ import rehypeKatex from "rehype-katex";
7
+ import { useState } from "react";
8
+ import {
9
+ Copy,
10
+ Check,
11
+ ThumbsUp,
12
+ ThumbsDown,
13
+ Volume2,
14
+ RefreshCw,
15
+ } from "lucide-react";
16
+ import { cn } from "@/lib/utils";
17
+ import { useCopilotChatConfiguration } from "@/providers/CopilotChatConfigurationProvider";
18
+ import { twMerge } from "tailwind-merge";
19
+ import { Button } from "@/components/ui/button";
20
+ import {
21
+ Tooltip,
22
+ TooltipContent,
23
+ TooltipTrigger,
24
+ } from "@/components/ui/tooltip";
25
+ import "katex/dist/katex.min.css";
26
+ import { WithSlots, renderSlot } from "@/lib/slots";
27
+ import { completePartialMarkdown } from "@copilotkitnext/core";
28
+ import CopilotChatToolCallsView from "./CopilotChatToolCallsView";
29
+
30
+ export type CopilotChatAssistantMessageProps = WithSlots<
31
+ {
32
+ markdownRenderer: typeof CopilotChatAssistantMessage.MarkdownRenderer;
33
+ toolbar: typeof CopilotChatAssistantMessage.Toolbar;
34
+ copyButton: typeof CopilotChatAssistantMessage.CopyButton;
35
+ thumbsUpButton: typeof CopilotChatAssistantMessage.ThumbsUpButton;
36
+ thumbsDownButton: typeof CopilotChatAssistantMessage.ThumbsDownButton;
37
+ readAloudButton: typeof CopilotChatAssistantMessage.ReadAloudButton;
38
+ regenerateButton: typeof CopilotChatAssistantMessage.RegenerateButton;
39
+ toolCallsView: typeof CopilotChatToolCallsView;
40
+ },
41
+ {
42
+ onThumbsUp?: (message: AssistantMessage) => void;
43
+ onThumbsDown?: (message: AssistantMessage) => void;
44
+ onReadAloud?: (message: AssistantMessage) => void;
45
+ onRegenerate?: (message: AssistantMessage) => void;
46
+ message: AssistantMessage;
47
+ messages?: Message[];
48
+ isLoading?: boolean;
49
+ additionalToolbarItems?: React.ReactNode;
50
+ toolbarVisible?: boolean;
51
+ } & React.HTMLAttributes<HTMLDivElement>
52
+ >;
53
+
54
+ export function CopilotChatAssistantMessage({
55
+ message,
56
+ messages,
57
+ isLoading,
58
+ onThumbsUp,
59
+ onThumbsDown,
60
+ onReadAloud,
61
+ onRegenerate,
62
+ additionalToolbarItems,
63
+ toolbarVisible = true,
64
+ markdownRenderer,
65
+ toolbar,
66
+ copyButton,
67
+ thumbsUpButton,
68
+ thumbsDownButton,
69
+ readAloudButton,
70
+ regenerateButton,
71
+ toolCallsView,
72
+ children,
73
+ className,
74
+ ...props
75
+ }: CopilotChatAssistantMessageProps) {
76
+ const boundMarkdownRenderer = renderSlot(
77
+ markdownRenderer,
78
+ CopilotChatAssistantMessage.MarkdownRenderer,
79
+ {
80
+ content: message.content || "",
81
+ }
82
+ );
83
+
84
+ const boundCopyButton = renderSlot(
85
+ copyButton,
86
+ CopilotChatAssistantMessage.CopyButton,
87
+ {
88
+ onClick: async () => {
89
+ if (message.content) {
90
+ try {
91
+ await navigator.clipboard.writeText(message.content);
92
+ } catch (err) {
93
+ console.error("Failed to copy message:", err);
94
+ }
95
+ }
96
+ },
97
+ }
98
+ );
99
+
100
+ const boundThumbsUpButton = renderSlot(
101
+ thumbsUpButton,
102
+ CopilotChatAssistantMessage.ThumbsUpButton,
103
+ {
104
+ onClick: onThumbsUp,
105
+ }
106
+ );
107
+
108
+ const boundThumbsDownButton = renderSlot(
109
+ thumbsDownButton,
110
+ CopilotChatAssistantMessage.ThumbsDownButton,
111
+ {
112
+ onClick: onThumbsDown,
113
+ }
114
+ );
115
+
116
+ const boundReadAloudButton = renderSlot(
117
+ readAloudButton,
118
+ CopilotChatAssistantMessage.ReadAloudButton,
119
+ {
120
+ onClick: onReadAloud,
121
+ }
122
+ );
123
+
124
+ const boundRegenerateButton = renderSlot(
125
+ regenerateButton,
126
+ CopilotChatAssistantMessage.RegenerateButton,
127
+ {
128
+ onClick: onRegenerate,
129
+ }
130
+ );
131
+
132
+ const boundToolbar = renderSlot(
133
+ toolbar,
134
+ CopilotChatAssistantMessage.Toolbar,
135
+ {
136
+ children: (
137
+ <div className="flex items-center gap-1">
138
+ {boundCopyButton}
139
+ {(onThumbsUp || thumbsUpButton) && boundThumbsUpButton}
140
+ {(onThumbsDown || thumbsDownButton) && boundThumbsDownButton}
141
+ {(onReadAloud || readAloudButton) && boundReadAloudButton}
142
+ {(onRegenerate || regenerateButton) && boundRegenerateButton}
143
+ {additionalToolbarItems}
144
+ </div>
145
+ ),
146
+ }
147
+ );
148
+
149
+ const boundToolCallsView = renderSlot(
150
+ toolCallsView,
151
+ CopilotChatToolCallsView,
152
+ {
153
+ message,
154
+ messages,
155
+ isLoading,
156
+ }
157
+ );
158
+
159
+ if (children) {
160
+ return (
161
+ <>
162
+ {children({
163
+ markdownRenderer: boundMarkdownRenderer,
164
+ toolbar: boundToolbar,
165
+ toolCallsView: boundToolCallsView,
166
+ copyButton: boundCopyButton,
167
+ thumbsUpButton: boundThumbsUpButton,
168
+ thumbsDownButton: boundThumbsDownButton,
169
+ readAloudButton: boundReadAloudButton,
170
+ regenerateButton: boundRegenerateButton,
171
+ message,
172
+ messages,
173
+ isLoading,
174
+ onThumbsUp,
175
+ onThumbsDown,
176
+ onReadAloud,
177
+ onRegenerate,
178
+ additionalToolbarItems,
179
+ toolbarVisible,
180
+ })}
181
+ </>
182
+ );
183
+ }
184
+
185
+ return (
186
+ <div
187
+ className={twMerge(
188
+ "prose max-w-full break-words dark:prose-invert",
189
+ className
190
+ )}
191
+ {...props}
192
+ data-message-id={message.id}
193
+ >
194
+ {boundMarkdownRenderer}
195
+ {boundToolCallsView}
196
+ {toolbarVisible && boundToolbar}
197
+ </div>
198
+ );
199
+ }
200
+
201
+ // eslint-disable-next-line @typescript-eslint/no-namespace
202
+ export namespace CopilotChatAssistantMessage {
203
+ const InlineCode = ({
204
+ children,
205
+ ...props
206
+ }: React.HTMLAttributes<HTMLElement>) => {
207
+ return (
208
+ <code
209
+ className="px-[4.8px] py-[2.5px] bg-[rgb(236,236,236)] dark:bg-gray-800 rounded text-sm font-mono font-medium! text-foreground!"
210
+ {...props}
211
+ >
212
+ {children}
213
+ </code>
214
+ );
215
+ };
216
+
217
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
218
+ const CodeBlock = ({ children, className, onClick, ...props }: any) => {
219
+ const { labels } = useCopilotChatConfiguration();
220
+ const [copied, setCopied] = useState(false);
221
+
222
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
223
+ const getCodeContent = (node: any): string => {
224
+ if (typeof node === "string") return node;
225
+ if (Array.isArray(node)) return node.map(getCodeContent).join("");
226
+ if (node?.props?.children) return getCodeContent(node.props.children);
227
+ return "";
228
+ };
229
+
230
+ const codeContent = getCodeContent(children);
231
+ const language = props["data-language"] as string | undefined;
232
+
233
+ const copyToClipboard = async () => {
234
+ if (!codeContent.trim()) return;
235
+
236
+ try {
237
+ setCopied(true);
238
+ setTimeout(() => setCopied(false), 2000);
239
+ if (onClick) {
240
+ onClick();
241
+ }
242
+ } catch (err) {
243
+ console.error("Failed to copy code:", err);
244
+ }
245
+ };
246
+
247
+ return (
248
+ <div className="relative">
249
+ <div className="flex items-center justify-between px-4 pr-3 py-3 text-xs">
250
+ {language && (
251
+ <span className="font-regular text-muted-foreground dark:text-white">
252
+ {language}
253
+ </span>
254
+ )}
255
+
256
+ <button
257
+ className={cn(
258
+ "px-2 gap-0.5 text-xs flex items-center cursor-pointer text-muted-foreground dark:text-white"
259
+ )}
260
+ onClick={copyToClipboard}
261
+ title={
262
+ copied
263
+ ? labels.assistantMessageToolbarCopyCodeCopiedLabel
264
+ : `${labels.assistantMessageToolbarCopyCodeLabel} code`
265
+ }
266
+ >
267
+ {copied ? (
268
+ <Check className="h-[10px]! w-[10px]!" />
269
+ ) : (
270
+ <Copy className="h-[10px]! w-[10px]!" />
271
+ )}
272
+ <span className="text-[11px]">
273
+ {copied
274
+ ? labels.assistantMessageToolbarCopyCodeCopiedLabel
275
+ : labels.assistantMessageToolbarCopyCodeLabel}
276
+ </span>
277
+ </button>
278
+ </div>
279
+
280
+ <pre
281
+ className={cn(
282
+ className,
283
+ "rounded-2xl bg-transparent border-t-0 my-1!"
284
+ )}
285
+ {...props}
286
+ >
287
+ {children}
288
+ </pre>
289
+ </div>
290
+ );
291
+ };
292
+
293
+ export const MarkdownRenderer: React.FC<
294
+ React.HTMLAttributes<HTMLDivElement> & { content: string }
295
+ > = ({ content, className }) => (
296
+ <div className={className}>
297
+ <MarkdownHooks
298
+ /* async plugins are now fine ✨ */
299
+ remarkPlugins={[remarkGfm, remarkMath]}
300
+ rehypePlugins={[
301
+ [
302
+ rehypePrettyCode,
303
+ {
304
+ keepBackground: false,
305
+ theme: {
306
+ dark: "one-dark-pro",
307
+ light: "one-light",
308
+ },
309
+ bypassInlineCode: true,
310
+ },
311
+ ],
312
+ rehypeKatex,
313
+ ]}
314
+ components={{
315
+ pre: CodeBlock,
316
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
317
+ code: ({ className, children, ...props }: any) => {
318
+ // For inline code, use custom styling
319
+ if (typeof children === "string") {
320
+ return <InlineCode {...props}>{children}</InlineCode>;
321
+ }
322
+
323
+ // For code blocks, just return the code element as-is
324
+ return (
325
+ <code className={className} {...props}>
326
+ {children}
327
+ </code>
328
+ );
329
+ },
330
+ }}
331
+ >
332
+ {completePartialMarkdown(content || "")}
333
+ </MarkdownHooks>
334
+ </div>
335
+ );
336
+
337
+ export const Toolbar: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
338
+ className,
339
+ ...props
340
+ }) => (
341
+ <div
342
+ className={twMerge(
343
+ "w-full bg-transparent flex items-center -ml-[5px] -mt-[0px]",
344
+ className
345
+ )}
346
+ {...props}
347
+ />
348
+ );
349
+
350
+ export const ToolbarButton: React.FC<
351
+ React.ButtonHTMLAttributes<HTMLButtonElement> & {
352
+ title: string;
353
+ children: React.ReactNode;
354
+ }
355
+ > = ({ title, children, ...props }) => {
356
+ return (
357
+ <Tooltip>
358
+ <TooltipTrigger asChild>
359
+ <Button
360
+ type="button"
361
+ variant="assistantMessageToolbarButton"
362
+ aria-label={title}
363
+ {...props}
364
+ >
365
+ {children}
366
+ </Button>
367
+ </TooltipTrigger>
368
+ <TooltipContent side="bottom">
369
+ <p>{title}</p>
370
+ </TooltipContent>
371
+ </Tooltip>
372
+ );
373
+ };
374
+
375
+ export const CopyButton: React.FC<
376
+ React.ButtonHTMLAttributes<HTMLButtonElement>
377
+ > = ({ className, title, onClick, ...props }) => {
378
+ const { labels } = useCopilotChatConfiguration();
379
+ const [copied, setCopied] = useState(false);
380
+
381
+ const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
382
+ setCopied(true);
383
+ setTimeout(() => setCopied(false), 2000);
384
+
385
+ if (onClick) {
386
+ onClick(event);
387
+ }
388
+ };
389
+
390
+ return (
391
+ <ToolbarButton
392
+ title={title || labels.assistantMessageToolbarCopyMessageLabel}
393
+ onClick={handleClick}
394
+ className={className}
395
+ {...props}
396
+ >
397
+ {copied ? (
398
+ <Check className="size-[18px]" />
399
+ ) : (
400
+ <Copy className="size-[18px]" />
401
+ )}
402
+ </ToolbarButton>
403
+ );
404
+ };
405
+
406
+ export const ThumbsUpButton: React.FC<
407
+ React.ButtonHTMLAttributes<HTMLButtonElement>
408
+ > = ({ title, ...props }) => {
409
+ const { labels } = useCopilotChatConfiguration();
410
+ return (
411
+ <ToolbarButton
412
+ title={title || labels.assistantMessageToolbarThumbsUpLabel}
413
+ {...props}
414
+ >
415
+ <ThumbsUp className="size-[18px]" />
416
+ </ToolbarButton>
417
+ );
418
+ };
419
+
420
+ export const ThumbsDownButton: React.FC<
421
+ React.ButtonHTMLAttributes<HTMLButtonElement>
422
+ > = ({ title, ...props }) => {
423
+ const { labels } = useCopilotChatConfiguration();
424
+ return (
425
+ <ToolbarButton
426
+ title={title || labels.assistantMessageToolbarThumbsDownLabel}
427
+ {...props}
428
+ >
429
+ <ThumbsDown className="size-[18px]" />
430
+ </ToolbarButton>
431
+ );
432
+ };
433
+
434
+ export const ReadAloudButton: React.FC<
435
+ React.ButtonHTMLAttributes<HTMLButtonElement>
436
+ > = ({ title, ...props }) => {
437
+ const { labels } = useCopilotChatConfiguration();
438
+ return (
439
+ <ToolbarButton
440
+ title={title || labels.assistantMessageToolbarReadAloudLabel}
441
+ {...props}
442
+ >
443
+ <Volume2 className="size-[20px]" />
444
+ </ToolbarButton>
445
+ );
446
+ };
447
+
448
+ export const RegenerateButton: React.FC<
449
+ React.ButtonHTMLAttributes<HTMLButtonElement>
450
+ > = ({ title, ...props }) => {
451
+ const { labels } = useCopilotChatConfiguration();
452
+ return (
453
+ <ToolbarButton
454
+ title={title || labels.assistantMessageToolbarRegenerateLabel}
455
+ {...props}
456
+ >
457
+ <RefreshCw className="size-[18px]" />
458
+ </ToolbarButton>
459
+ );
460
+ };
461
+ }
462
+
463
+ CopilotChatAssistantMessage.MarkdownRenderer.displayName =
464
+ "CopilotChatAssistantMessage.MarkdownRenderer";
465
+ CopilotChatAssistantMessage.Toolbar.displayName =
466
+ "CopilotChatAssistantMessage.Toolbar";
467
+ CopilotChatAssistantMessage.CopyButton.displayName =
468
+ "CopilotChatAssistantMessage.CopyButton";
469
+ CopilotChatAssistantMessage.ThumbsUpButton.displayName =
470
+ "CopilotChatAssistantMessage.ThumbsUpButton";
471
+ CopilotChatAssistantMessage.ThumbsDownButton.displayName =
472
+ "CopilotChatAssistantMessage.ThumbsDownButton";
473
+ CopilotChatAssistantMessage.ReadAloudButton.displayName =
474
+ "CopilotChatAssistantMessage.ReadAloudButton";
475
+ CopilotChatAssistantMessage.RegenerateButton.displayName =
476
+ "CopilotChatAssistantMessage.RegenerateButton";
477
+
478
+ export default CopilotChatAssistantMessage;