@copilotkit/react-core 1.54.1 → 1.55.0-next.7

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 (183) hide show
  1. package/CHANGELOG.md +117 -116
  2. package/dist/copilotkit-B3Mb1yVE.cjs +7975 -0
  3. package/dist/copilotkit-B3Mb1yVE.cjs.map +1 -0
  4. package/dist/copilotkit-DBzgOMby.d.cts +2182 -0
  5. package/dist/copilotkit-DBzgOMby.d.cts.map +1 -0
  6. package/dist/copilotkit-DNYSFuz5.mjs +7562 -0
  7. package/dist/copilotkit-DNYSFuz5.mjs.map +1 -0
  8. package/dist/copilotkit-Dy5w3qEV.d.mts +2182 -0
  9. package/dist/copilotkit-Dy5w3qEV.d.mts.map +1 -0
  10. package/dist/index.cjs +27 -28
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +3 -3
  13. package/dist/index.d.cts.map +1 -1
  14. package/dist/index.d.mts +3 -3
  15. package/dist/index.d.mts.map +1 -1
  16. package/dist/index.mjs +4 -5
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/index.umd.js +1941 -35
  19. package/dist/index.umd.js.map +1 -1
  20. package/dist/v2/index.cjs +77 -7
  21. package/dist/v2/index.css +1 -2
  22. package/dist/v2/index.d.cts +6 -4
  23. package/dist/v2/index.d.mts +6 -4
  24. package/dist/v2/index.mjs +7 -4
  25. package/dist/v2/index.umd.js +5725 -24
  26. package/dist/v2/index.umd.js.map +1 -1
  27. package/package.json +37 -9
  28. package/scripts/scope-preflight.mjs +101 -0
  29. package/src/components/CopilotListeners.tsx +2 -6
  30. package/src/components/copilot-provider/copilot-messages.tsx +1 -1
  31. package/src/components/copilot-provider/copilotkit-props.tsx +1 -1
  32. package/src/components/copilot-provider/copilotkit.tsx +4 -4
  33. package/src/context/copilot-messages-context.tsx +1 -1
  34. package/src/hooks/__tests__/use-coagent-config.test.ts +2 -2
  35. package/src/hooks/__tests__/use-coagent-state-render.e2e.test.tsx +2 -2
  36. package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +3 -7
  37. package/src/hooks/__tests__/use-frontend-tool-available.test.tsx +1 -1
  38. package/src/hooks/__tests__/use-frontend-tool-remount.e2e.test.tsx +4 -4
  39. package/src/hooks/use-agent-nodename.ts +1 -1
  40. package/src/hooks/use-coagent-state-render-bridge.tsx +1 -4
  41. package/src/hooks/use-coagent.ts +1 -1
  42. package/src/hooks/use-configure-chat-suggestions.tsx +2 -2
  43. package/src/hooks/use-copilot-chat-suggestions.tsx +2 -2
  44. package/src/hooks/use-copilot-chat_internal.ts +2 -2
  45. package/src/hooks/use-copilot-readable.ts +1 -1
  46. package/src/hooks/use-frontend-tool.ts +2 -2
  47. package/src/hooks/use-human-in-the-loop.ts +2 -2
  48. package/src/hooks/use-langgraph-interrupt.ts +2 -5
  49. package/src/hooks/use-lazy-tool-renderer.tsx +1 -1
  50. package/src/hooks/use-render-tool-call.ts +1 -1
  51. package/src/lib/copilot-task.ts +1 -1
  52. package/src/setupTests.ts +18 -14
  53. package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +176 -0
  54. package/src/v2/__tests__/globalSetup.ts +14 -0
  55. package/src/v2/__tests__/setup.ts +93 -0
  56. package/src/v2/__tests__/utils/test-helpers.tsx +470 -0
  57. package/src/v2/a2ui/A2UIMessageRenderer.tsx +206 -0
  58. package/src/v2/components/CopilotKitInspector.tsx +50 -0
  59. package/src/v2/components/MCPAppsActivityRenderer.tsx +785 -0
  60. package/src/v2/components/WildcardToolCallRender.tsx +86 -0
  61. package/src/v2/components/__tests__/license-warning-banner.test.tsx +46 -0
  62. package/src/v2/components/chat/CopilotChat.tsx +431 -0
  63. package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +375 -0
  64. package/src/v2/components/chat/CopilotChatAudioRecorder.tsx +350 -0
  65. package/src/v2/components/chat/CopilotChatInput.tsx +1302 -0
  66. package/src/v2/components/chat/CopilotChatMessageView.tsx +556 -0
  67. package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +252 -0
  68. package/src/v2/components/chat/CopilotChatSuggestionPill.tsx +59 -0
  69. package/src/v2/components/chat/CopilotChatSuggestionView.tsx +133 -0
  70. package/src/v2/components/chat/CopilotChatToggleButton.tsx +171 -0
  71. package/src/v2/components/chat/CopilotChatToolCallsView.tsx +40 -0
  72. package/src/v2/components/chat/CopilotChatUserMessage.tsx +388 -0
  73. package/src/v2/components/chat/CopilotChatView.tsx +598 -0
  74. package/src/v2/components/chat/CopilotModalHeader.tsx +129 -0
  75. package/src/v2/components/chat/CopilotPopup.tsx +81 -0
  76. package/src/v2/components/chat/CopilotPopupView.tsx +317 -0
  77. package/src/v2/components/chat/CopilotSidebar.tsx +76 -0
  78. package/src/v2/components/chat/CopilotSidebarView.tsx +255 -0
  79. package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +1113 -0
  80. package/src/v2/components/chat/__tests__/CopilotChat.onError.test.tsx +73 -0
  81. package/src/v2/components/chat/__tests__/CopilotChat.slots.e2e.test.tsx +432 -0
  82. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +150 -0
  83. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.tsx +624 -0
  84. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +702 -0
  85. package/src/v2/components/chat/__tests__/CopilotChatCssClasses.test.tsx +107 -0
  86. package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.tsx +929 -0
  87. package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +986 -0
  88. package/src/v2/components/chat/__tests__/CopilotChatMessageView.slots.e2e.test.tsx +1004 -0
  89. package/src/v2/components/chat/__tests__/CopilotChatMessageView.test.tsx +169 -0
  90. package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.tsx +530 -0
  91. package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +782 -0
  92. package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +2413 -0
  93. package/src/v2/components/chat/__tests__/CopilotChatUserMessage.slots.e2e.test.tsx +621 -0
  94. package/src/v2/components/chat/__tests__/CopilotChatView.onClick.e2e.test.tsx +853 -0
  95. package/src/v2/components/chat/__tests__/CopilotChatView.slots.e2e.test.tsx +1050 -0
  96. package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.tsx +484 -0
  97. package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.tsx +612 -0
  98. package/src/v2/components/chat/__tests__/CopilotSidebarView.slots.e2e.test.tsx +502 -0
  99. package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +1011 -0
  100. package/src/v2/components/chat/__tests__/setup.ts +1 -0
  101. package/src/v2/components/chat/index.ts +79 -0
  102. package/src/v2/components/index.ts +7 -0
  103. package/src/v2/components/license-warning-banner.tsx +198 -0
  104. package/src/v2/components/ui/button.tsx +123 -0
  105. package/src/v2/components/ui/dropdown-menu.tsx +258 -0
  106. package/src/v2/components/ui/tooltip.tsx +60 -0
  107. package/src/v2/hooks/__tests__/standard-schema-types.test.tsx +152 -0
  108. package/src/v2/hooks/__tests__/standard-schema.test.tsx +282 -0
  109. package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +132 -0
  110. package/src/v2/hooks/__tests__/use-agent-context.test.tsx +401 -0
  111. package/src/v2/hooks/__tests__/use-agent-error-state.test.tsx +44 -0
  112. package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +205 -0
  113. package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +148 -0
  114. package/src/v2/hooks/__tests__/use-component.test.tsx +123 -0
  115. package/src/v2/hooks/__tests__/use-configure-suggestions.e2e.test.tsx +696 -0
  116. package/src/v2/hooks/__tests__/use-default-render-tool.test.tsx +153 -0
  117. package/src/v2/hooks/__tests__/use-frontend-tool-available.test.tsx +167 -0
  118. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +2129 -0
  119. package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.tsx +1261 -0
  120. package/src/v2/hooks/__tests__/use-interrupt.test.tsx +397 -0
  121. package/src/v2/hooks/__tests__/use-katex-styles.test.tsx +56 -0
  122. package/src/v2/hooks/__tests__/use-keyboard-height.test.tsx +192 -0
  123. package/src/v2/hooks/__tests__/use-render-tool.test.tsx +259 -0
  124. package/src/v2/hooks/__tests__/use-suggestions.e2e.test.tsx +524 -0
  125. package/src/v2/hooks/__tests__/use-threads.test.tsx +433 -0
  126. package/src/v2/hooks/__tests__/zod-regression.test.tsx +311 -0
  127. package/src/v2/hooks/index.ts +18 -0
  128. package/src/v2/hooks/use-agent-context.tsx +45 -0
  129. package/src/v2/hooks/use-agent.tsx +155 -0
  130. package/src/v2/hooks/use-component.tsx +89 -0
  131. package/src/v2/hooks/use-configure-suggestions.tsx +187 -0
  132. package/src/v2/hooks/use-default-render-tool.tsx +254 -0
  133. package/src/v2/hooks/use-frontend-tool.tsx +43 -0
  134. package/src/v2/hooks/use-human-in-the-loop.tsx +81 -0
  135. package/src/v2/hooks/use-interrupt.tsx +305 -0
  136. package/src/v2/hooks/use-keyboard-height.tsx +67 -0
  137. package/src/v2/hooks/use-render-activity-message.tsx +73 -0
  138. package/src/v2/hooks/use-render-custom-messages.tsx +93 -0
  139. package/src/v2/hooks/use-render-tool-call.tsx +175 -0
  140. package/src/v2/hooks/use-render-tool.tsx +181 -0
  141. package/src/v2/hooks/use-suggestions.tsx +91 -0
  142. package/src/v2/hooks/use-threads.tsx +256 -0
  143. package/src/v2/hooks/useKatexStyles.ts +27 -0
  144. package/src/v2/index.css +1 -1
  145. package/src/v2/index.ts +18 -2
  146. package/src/v2/lib/__tests__/completePartialMarkdown.test.ts +495 -0
  147. package/src/v2/lib/__tests__/renderSlot.test.tsx +588 -0
  148. package/src/v2/lib/react-core.ts +156 -0
  149. package/src/v2/lib/slots.tsx +143 -0
  150. package/src/v2/lib/transcription-client.ts +184 -0
  151. package/src/v2/lib/utils.ts +8 -0
  152. package/src/v2/providers/CopilotChatConfigurationProvider.tsx +162 -0
  153. package/src/v2/providers/CopilotKitProvider.tsx +600 -0
  154. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +546 -0
  155. package/src/v2/providers/__tests__/CopilotKitProvider.license.test.tsx +101 -0
  156. package/src/v2/providers/__tests__/CopilotKitProvider.onError.test.tsx +69 -0
  157. package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.tsx +881 -0
  158. package/src/v2/providers/__tests__/CopilotKitProvider.stability.test.tsx +740 -0
  159. package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +642 -0
  160. package/src/v2/providers/__tests__/CopilotKitProvider.wildcard.test.tsx +294 -0
  161. package/src/v2/providers/index.ts +14 -0
  162. package/src/v2/styles/globals.css +230 -0
  163. package/src/v2/types/__tests__/defineToolCallRenderer.test.tsx +525 -0
  164. package/src/v2/types/defineToolCallRenderer.ts +65 -0
  165. package/src/v2/types/frontend-tool.ts +8 -0
  166. package/src/v2/types/human-in-the-loop.ts +33 -0
  167. package/src/v2/types/index.ts +7 -0
  168. package/src/v2/types/interrupt.ts +15 -0
  169. package/src/v2/types/react-activity-message-renderer.ts +27 -0
  170. package/src/v2/types/react-custom-message-renderer.ts +17 -0
  171. package/src/v2/types/react-tool-call-renderer.ts +32 -0
  172. package/tsdown.config.ts +34 -10
  173. package/vitest.config.mjs +4 -3
  174. package/LICENSE +0 -21
  175. package/dist/copilotkit-BRPQ2sqS.d.cts +0 -670
  176. package/dist/copilotkit-BRPQ2sqS.d.cts.map +0 -1
  177. package/dist/copilotkit-C94ayZbs.cjs +0 -2161
  178. package/dist/copilotkit-C94ayZbs.cjs.map +0 -1
  179. package/dist/copilotkit-CwZMFmSK.d.mts +0 -670
  180. package/dist/copilotkit-CwZMFmSK.d.mts.map +0 -1
  181. package/dist/copilotkit-Yh_Ld_FX.mjs +0 -2031
  182. package/dist/copilotkit-Yh_Ld_FX.mjs.map +0 -1
  183. package/dist/v2/index.css.map +0 -1
@@ -0,0 +1,1302 @@
1
+ import React, {
2
+ useState,
3
+ useRef,
4
+ KeyboardEvent,
5
+ ChangeEvent,
6
+ useEffect,
7
+ useLayoutEffect,
8
+ forwardRef,
9
+ useImperativeHandle,
10
+ useCallback,
11
+ useMemo,
12
+ } from "react";
13
+ import { twMerge } from "tailwind-merge";
14
+ import { Plus, Mic, ArrowUp, X, Check, Square, Loader2 } from "lucide-react";
15
+
16
+ import {
17
+ CopilotChatLabels,
18
+ useCopilotChatConfiguration,
19
+ CopilotChatDefaultLabels,
20
+ } from "../../providers/CopilotChatConfigurationProvider";
21
+ import { Button } from "../../components/ui/button";
22
+ import {
23
+ Tooltip,
24
+ TooltipContent,
25
+ TooltipTrigger,
26
+ } from "../../components/ui/tooltip";
27
+ import {
28
+ DropdownMenu,
29
+ DropdownMenuTrigger,
30
+ DropdownMenuContent,
31
+ DropdownMenuItem,
32
+ DropdownMenuSub,
33
+ DropdownMenuSubTrigger,
34
+ DropdownMenuSubContent,
35
+ DropdownMenuSeparator,
36
+ } from "../../components/ui/dropdown-menu";
37
+
38
+ import { CopilotChatAudioRecorder } from "./CopilotChatAudioRecorder";
39
+ import { renderSlot, WithSlots } from "../../lib/slots";
40
+ import { cn } from "../../lib/utils";
41
+
42
+ export type CopilotChatInputMode = "input" | "transcribe" | "processing";
43
+
44
+ export type ToolsMenuItem = {
45
+ label: string;
46
+ } & (
47
+ | {
48
+ action: () => void;
49
+ items?: never;
50
+ }
51
+ | {
52
+ action?: never;
53
+ items: (ToolsMenuItem | "-")[];
54
+ }
55
+ );
56
+
57
+ type CopilotChatInputSlots = {
58
+ textArea: typeof CopilotChatInput.TextArea;
59
+ sendButton: typeof CopilotChatInput.SendButton;
60
+ startTranscribeButton: typeof CopilotChatInput.StartTranscribeButton;
61
+ cancelTranscribeButton: typeof CopilotChatInput.CancelTranscribeButton;
62
+ finishTranscribeButton: typeof CopilotChatInput.FinishTranscribeButton;
63
+ addMenuButton: typeof CopilotChatInput.AddMenuButton;
64
+ audioRecorder: typeof CopilotChatAudioRecorder;
65
+ disclaimer: typeof CopilotChatInput.Disclaimer;
66
+ };
67
+
68
+ type CopilotChatInputRestProps = {
69
+ mode?: CopilotChatInputMode;
70
+ toolsMenu?: (ToolsMenuItem | "-")[];
71
+ autoFocus?: boolean;
72
+ onSubmitMessage?: (value: string) => void;
73
+ onStop?: () => void;
74
+ isRunning?: boolean;
75
+ onStartTranscribe?: () => void;
76
+ onCancelTranscribe?: () => void;
77
+ onFinishTranscribe?: () => void;
78
+ onFinishTranscribeWithAudio?: (audioBlob: Blob) => Promise<void>;
79
+ onAddFile?: () => void;
80
+ value?: string;
81
+ onChange?: (value: string) => void;
82
+ /** Positioning mode for the input container. Default: 'static' */
83
+ positioning?: "static" | "absolute";
84
+ /** Keyboard height in pixels for mobile keyboard handling */
85
+ keyboardHeight?: number;
86
+ /** Ref for the outer positioning container */
87
+ containerRef?: React.Ref<HTMLDivElement>;
88
+ /** Whether to show the disclaimer. Default: true for absolute positioning, false for static */
89
+ showDisclaimer?: boolean;
90
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "onChange">;
91
+
92
+ type CopilotChatInputBaseProps = WithSlots<
93
+ CopilotChatInputSlots,
94
+ CopilotChatInputRestProps
95
+ >;
96
+
97
+ type CopilotChatInputChildrenArgs = CopilotChatInputBaseProps extends {
98
+ children?: infer C;
99
+ }
100
+ ? C extends (props: infer P) => React.ReactNode
101
+ ? P
102
+ : never
103
+ : never;
104
+
105
+ export type CopilotChatInputProps = Omit<
106
+ CopilotChatInputBaseProps,
107
+ "children"
108
+ > & {
109
+ children?: (props: CopilotChatInputChildrenArgs) => React.ReactNode;
110
+ };
111
+
112
+ const SLASH_MENU_MAX_VISIBLE_ITEMS = 5;
113
+ const SLASH_MENU_ITEM_HEIGHT_PX = 40;
114
+
115
+ export function CopilotChatInput({
116
+ mode = "input",
117
+ onSubmitMessage,
118
+ onStop,
119
+ isRunning = false,
120
+ onStartTranscribe,
121
+ onCancelTranscribe,
122
+ onFinishTranscribe,
123
+ onFinishTranscribeWithAudio,
124
+ onAddFile,
125
+ onChange,
126
+ value,
127
+ toolsMenu,
128
+ autoFocus = true,
129
+ positioning = "static",
130
+ keyboardHeight = 0,
131
+ containerRef,
132
+ showDisclaimer,
133
+ textArea,
134
+ sendButton,
135
+ startTranscribeButton,
136
+ cancelTranscribeButton,
137
+ finishTranscribeButton,
138
+ addMenuButton,
139
+ audioRecorder,
140
+ disclaimer,
141
+ children,
142
+ className,
143
+ ...props
144
+ }: CopilotChatInputProps) {
145
+ const isControlled = value !== undefined;
146
+ const [internalValue, setInternalValue] = useState<string>(() => value ?? "");
147
+
148
+ useEffect(() => {
149
+ if (!isControlled && value !== undefined) {
150
+ setInternalValue(value);
151
+ }
152
+ }, [isControlled, value]);
153
+
154
+ const resolvedValue = isControlled ? (value ?? "") : internalValue;
155
+
156
+ const [layout, setLayout] = useState<"compact" | "expanded">("compact");
157
+ const ignoreResizeRef = useRef(false);
158
+ const resizeEvaluationRafRef = useRef<number | null>(null);
159
+ const isExpanded = mode === "input" && layout === "expanded";
160
+ const [commandQuery, setCommandQuery] = useState<string | null>(null);
161
+ const [slashHighlightIndex, setSlashHighlightIndex] = useState(0);
162
+
163
+ const inputRef = useRef<HTMLTextAreaElement>(null);
164
+ const gridRef = useRef<HTMLDivElement>(null);
165
+ const addButtonContainerRef = useRef<HTMLDivElement>(null);
166
+ const actionsContainerRef = useRef<HTMLDivElement>(null);
167
+ const audioRecorderRef =
168
+ useRef<React.ElementRef<typeof CopilotChatAudioRecorder>>(null);
169
+ const slashMenuRef = useRef<HTMLDivElement>(null);
170
+ const config = useCopilotChatConfiguration();
171
+ const labels = config?.labels ?? CopilotChatDefaultLabels;
172
+
173
+ const previousModalStateRef = useRef<boolean | undefined>(undefined);
174
+ const measurementCanvasRef = useRef<HTMLCanvasElement | null>(null);
175
+ const measurementsRef = useRef({
176
+ singleLineHeight: 0,
177
+ maxHeight: 0,
178
+ paddingLeft: 0,
179
+ paddingRight: 0,
180
+ });
181
+
182
+ const commandItems = useMemo(() => {
183
+ const entries: ToolsMenuItem[] = [];
184
+ const seen = new Set<string>();
185
+
186
+ const pushItem = (item: ToolsMenuItem | "-") => {
187
+ if (item === "-") {
188
+ return;
189
+ }
190
+
191
+ if (item.items && item.items.length > 0) {
192
+ for (const nested of item.items) {
193
+ pushItem(nested);
194
+ }
195
+ return;
196
+ }
197
+
198
+ if (!seen.has(item.label)) {
199
+ seen.add(item.label);
200
+ entries.push(item);
201
+ }
202
+ };
203
+
204
+ if (onAddFile) {
205
+ pushItem({
206
+ label: labels.chatInputToolbarAddButtonLabel,
207
+ action: onAddFile,
208
+ });
209
+ }
210
+
211
+ if (toolsMenu && toolsMenu.length > 0) {
212
+ for (const item of toolsMenu) {
213
+ pushItem(item);
214
+ }
215
+ }
216
+
217
+ return entries;
218
+ }, [labels.chatInputToolbarAddButtonLabel, onAddFile, toolsMenu]);
219
+
220
+ const filteredCommands = useMemo(() => {
221
+ if (commandQuery === null) {
222
+ return [] as ToolsMenuItem[];
223
+ }
224
+
225
+ if (commandItems.length === 0) {
226
+ return [] as ToolsMenuItem[];
227
+ }
228
+
229
+ const query = commandQuery.trim().toLowerCase();
230
+ if (query.length === 0) {
231
+ return commandItems;
232
+ }
233
+
234
+ const startsWith: ToolsMenuItem[] = [];
235
+ const contains: ToolsMenuItem[] = [];
236
+ for (const item of commandItems) {
237
+ const label = item.label.toLowerCase();
238
+ if (label.startsWith(query)) {
239
+ startsWith.push(item);
240
+ } else if (label.includes(query)) {
241
+ contains.push(item);
242
+ }
243
+ }
244
+
245
+ return [...startsWith, ...contains];
246
+ }, [commandItems, commandQuery]);
247
+
248
+ useEffect(() => {
249
+ if (!autoFocus) {
250
+ previousModalStateRef.current = config?.isModalOpen;
251
+ return;
252
+ }
253
+
254
+ if (config?.isModalOpen && !previousModalStateRef.current) {
255
+ inputRef.current?.focus();
256
+ }
257
+
258
+ previousModalStateRef.current = config?.isModalOpen;
259
+ }, [config?.isModalOpen, autoFocus]);
260
+
261
+ useEffect(() => {
262
+ if (commandItems.length === 0 && commandQuery !== null) {
263
+ setCommandQuery(null);
264
+ }
265
+ }, [commandItems.length, commandQuery]);
266
+
267
+ const previousCommandQueryRef = useRef<string | null>(null);
268
+
269
+ useEffect(() => {
270
+ if (
271
+ commandQuery !== null &&
272
+ commandQuery !== previousCommandQueryRef.current &&
273
+ filteredCommands.length > 0
274
+ ) {
275
+ setSlashHighlightIndex(0);
276
+ }
277
+
278
+ previousCommandQueryRef.current = commandQuery;
279
+ }, [commandQuery, filteredCommands.length]);
280
+
281
+ useEffect(() => {
282
+ if (commandQuery === null) {
283
+ setSlashHighlightIndex(0);
284
+ return;
285
+ }
286
+
287
+ if (filteredCommands.length === 0) {
288
+ setSlashHighlightIndex(-1);
289
+ } else if (
290
+ slashHighlightIndex < 0 ||
291
+ slashHighlightIndex >= filteredCommands.length
292
+ ) {
293
+ setSlashHighlightIndex(0);
294
+ }
295
+ }, [commandQuery, filteredCommands, slashHighlightIndex]);
296
+
297
+ // Handle recording based on mode changes
298
+ useEffect(() => {
299
+ const recorder = audioRecorderRef.current;
300
+ if (!recorder) {
301
+ return;
302
+ }
303
+
304
+ if (mode === "transcribe") {
305
+ // Start recording when entering transcribe mode
306
+ recorder.start().catch(console.error);
307
+ } else {
308
+ // Stop recording when leaving transcribe mode
309
+ if (recorder.state === "recording") {
310
+ recorder.stop().catch(console.error);
311
+ }
312
+ }
313
+ }, [mode]);
314
+
315
+ useEffect(() => {
316
+ if (mode !== "input") {
317
+ setLayout("compact");
318
+ setCommandQuery(null);
319
+ }
320
+ }, [mode]);
321
+
322
+ const updateSlashState = useCallback(
323
+ (value: string) => {
324
+ if (commandItems.length === 0) {
325
+ setCommandQuery((prev) => (prev === null ? prev : null));
326
+ return;
327
+ }
328
+
329
+ if (value.startsWith("/")) {
330
+ const firstLine = value.split(/\r?\n/, 1)[0] ?? "";
331
+ const query = firstLine.slice(1);
332
+ setCommandQuery((prev) => (prev === query ? prev : query));
333
+ } else {
334
+ setCommandQuery((prev) => (prev === null ? prev : null));
335
+ }
336
+ },
337
+ [commandItems.length],
338
+ );
339
+
340
+ useEffect(() => {
341
+ updateSlashState(resolvedValue);
342
+ }, [resolvedValue, updateSlashState]);
343
+
344
+ // Handlers
345
+ const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
346
+ const nextValue = e.target.value;
347
+ if (!isControlled) {
348
+ setInternalValue(nextValue);
349
+ }
350
+ onChange?.(nextValue);
351
+ updateSlashState(nextValue);
352
+ };
353
+
354
+ const clearInputValue = useCallback(() => {
355
+ if (!isControlled) {
356
+ setInternalValue("");
357
+ }
358
+
359
+ if (onChange) {
360
+ onChange("");
361
+ }
362
+ }, [isControlled, onChange]);
363
+
364
+ const runCommand = useCallback(
365
+ (item: ToolsMenuItem) => {
366
+ clearInputValue();
367
+
368
+ item.action?.();
369
+
370
+ setCommandQuery(null);
371
+ setSlashHighlightIndex(0);
372
+
373
+ requestAnimationFrame(() => {
374
+ inputRef.current?.focus();
375
+ });
376
+ },
377
+ [clearInputValue],
378
+ );
379
+
380
+ const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
381
+ if (commandQuery !== null && mode === "input") {
382
+ if (e.key === "ArrowDown") {
383
+ if (filteredCommands.length > 0) {
384
+ e.preventDefault();
385
+ setSlashHighlightIndex((prev) => {
386
+ if (filteredCommands.length === 0) {
387
+ return prev;
388
+ }
389
+ const next = prev === -1 ? 0 : (prev + 1) % filteredCommands.length;
390
+ return next;
391
+ });
392
+ }
393
+ return;
394
+ }
395
+
396
+ if (e.key === "ArrowUp") {
397
+ if (filteredCommands.length > 0) {
398
+ e.preventDefault();
399
+ setSlashHighlightIndex((prev) => {
400
+ if (filteredCommands.length === 0) {
401
+ return prev;
402
+ }
403
+ if (prev === -1) {
404
+ return filteredCommands.length - 1;
405
+ }
406
+ return prev <= 0 ? filteredCommands.length - 1 : prev - 1;
407
+ });
408
+ }
409
+ return;
410
+ }
411
+
412
+ if (e.key === "Enter") {
413
+ const selected =
414
+ slashHighlightIndex >= 0
415
+ ? filteredCommands[slashHighlightIndex]
416
+ : undefined;
417
+ if (selected) {
418
+ e.preventDefault();
419
+ runCommand(selected);
420
+ return;
421
+ }
422
+ }
423
+
424
+ if (e.key === "Escape") {
425
+ e.preventDefault();
426
+ setCommandQuery(null);
427
+ return;
428
+ }
429
+ }
430
+
431
+ if (e.key === "Enter" && !e.shiftKey) {
432
+ e.preventDefault();
433
+ if (isProcessing) {
434
+ onStop?.();
435
+ } else {
436
+ send();
437
+ }
438
+ }
439
+ };
440
+
441
+ const send = () => {
442
+ if (!onSubmitMessage) {
443
+ return;
444
+ }
445
+ const trimmed = resolvedValue.trim();
446
+ if (!trimmed) {
447
+ return;
448
+ }
449
+
450
+ onSubmitMessage(trimmed);
451
+
452
+ if (!isControlled) {
453
+ setInternalValue("");
454
+ onChange?.("");
455
+ }
456
+
457
+ if (inputRef.current) {
458
+ inputRef.current.focus();
459
+ }
460
+ };
461
+
462
+ const BoundTextArea = renderSlot(textArea, CopilotChatInput.TextArea, {
463
+ ref: inputRef,
464
+ value: resolvedValue,
465
+ onChange: handleChange,
466
+ onKeyDown: handleKeyDown,
467
+ autoFocus: autoFocus,
468
+ className: twMerge(
469
+ "cpk:w-full cpk:py-3",
470
+ isExpanded ? "cpk:px-5" : "cpk:pr-5",
471
+ ),
472
+ });
473
+
474
+ const isProcessing = mode !== "transcribe" && isRunning;
475
+ const canSend = resolvedValue.trim().length > 0 && !!onSubmitMessage;
476
+ const canStop = !!onStop;
477
+
478
+ const handleSendButtonClick = () => {
479
+ if (isProcessing) {
480
+ onStop?.();
481
+ return;
482
+ }
483
+ send();
484
+ };
485
+
486
+ const BoundAudioRecorder = renderSlot(
487
+ audioRecorder,
488
+ CopilotChatAudioRecorder,
489
+ {
490
+ ref: audioRecorderRef,
491
+ },
492
+ );
493
+
494
+ const BoundSendButton = renderSlot(sendButton, CopilotChatInput.SendButton, {
495
+ onClick: handleSendButtonClick,
496
+ disabled: isProcessing ? !canStop : !canSend,
497
+ children:
498
+ isProcessing && canStop ? (
499
+ <Square className="cpk:size-[18px] cpk:fill-current" />
500
+ ) : undefined,
501
+ });
502
+
503
+ const BoundStartTranscribeButton = renderSlot(
504
+ startTranscribeButton,
505
+ CopilotChatInput.StartTranscribeButton,
506
+ {
507
+ onClick: onStartTranscribe,
508
+ },
509
+ );
510
+
511
+ const BoundCancelTranscribeButton = renderSlot(
512
+ cancelTranscribeButton,
513
+ CopilotChatInput.CancelTranscribeButton,
514
+ {
515
+ onClick: onCancelTranscribe,
516
+ },
517
+ );
518
+
519
+ // Handler for finish button - stops recording and passes audio blob
520
+ const handleFinishTranscribe = useCallback(async () => {
521
+ const recorder = audioRecorderRef.current;
522
+ if (recorder && recorder.state === "recording") {
523
+ try {
524
+ const audioBlob = await recorder.stop();
525
+ if (onFinishTranscribeWithAudio) {
526
+ await onFinishTranscribeWithAudio(audioBlob);
527
+ }
528
+ } catch (error) {
529
+ console.error("Failed to stop recording:", error);
530
+ }
531
+ }
532
+ // Always call the original handler to reset mode
533
+ onFinishTranscribe?.();
534
+ }, [onFinishTranscribe, onFinishTranscribeWithAudio]);
535
+
536
+ const BoundFinishTranscribeButton = renderSlot(
537
+ finishTranscribeButton,
538
+ CopilotChatInput.FinishTranscribeButton,
539
+ {
540
+ onClick: handleFinishTranscribe,
541
+ },
542
+ );
543
+
544
+ const BoundAddMenuButton = renderSlot(
545
+ addMenuButton,
546
+ CopilotChatInput.AddMenuButton,
547
+ {
548
+ disabled: mode === "transcribe",
549
+ onAddFile,
550
+ toolsMenu,
551
+ },
552
+ );
553
+
554
+ const BoundDisclaimer = renderSlot(
555
+ disclaimer,
556
+ CopilotChatInput.Disclaimer,
557
+ {},
558
+ );
559
+
560
+ // Determine whether to show disclaimer based on prop or positioning default
561
+ const shouldShowDisclaimer = showDisclaimer ?? positioning === "absolute";
562
+
563
+ if (children) {
564
+ const childProps = {
565
+ textArea: BoundTextArea,
566
+ audioRecorder: BoundAudioRecorder,
567
+ sendButton: BoundSendButton,
568
+ startTranscribeButton: BoundStartTranscribeButton,
569
+ cancelTranscribeButton: BoundCancelTranscribeButton,
570
+ finishTranscribeButton: BoundFinishTranscribeButton,
571
+ addMenuButton: BoundAddMenuButton,
572
+ disclaimer: BoundDisclaimer,
573
+ onSubmitMessage,
574
+ onStop,
575
+ isRunning,
576
+ onStartTranscribe,
577
+ onCancelTranscribe,
578
+ onFinishTranscribe,
579
+ onAddFile,
580
+ mode,
581
+ toolsMenu,
582
+ autoFocus,
583
+ positioning,
584
+ keyboardHeight,
585
+ showDisclaimer: shouldShowDisclaimer,
586
+ containerRef,
587
+ } as CopilotChatInputChildrenArgs;
588
+
589
+ return (
590
+ <div data-copilotkit style={{ display: "contents" }}>
591
+ {children(childProps)}
592
+ </div>
593
+ );
594
+ }
595
+
596
+ const handleContainerClick = (e: React.MouseEvent<HTMLDivElement>) => {
597
+ // Don't focus if clicking on buttons or other interactive elements
598
+ const target = e.target as HTMLElement;
599
+ if (
600
+ target.tagName !== "BUTTON" &&
601
+ !target.closest("button") &&
602
+ inputRef.current &&
603
+ mode === "input"
604
+ ) {
605
+ inputRef.current.focus();
606
+ }
607
+ };
608
+
609
+ const ensureMeasurements = useCallback(() => {
610
+ const textarea = inputRef.current;
611
+ if (!textarea) {
612
+ return;
613
+ }
614
+
615
+ const previousValue = textarea.value;
616
+ const previousHeight = textarea.style.height;
617
+
618
+ textarea.style.height = "auto";
619
+
620
+ const computedStyle = window.getComputedStyle(textarea);
621
+ const paddingLeft = parseFloat(computedStyle.paddingLeft) || 0;
622
+ const paddingRight = parseFloat(computedStyle.paddingRight) || 0;
623
+ const paddingTop = parseFloat(computedStyle.paddingTop) || 0;
624
+ const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0;
625
+
626
+ textarea.value = "";
627
+ const singleLineHeight = textarea.scrollHeight;
628
+ textarea.value = previousValue;
629
+
630
+ const contentHeight = singleLineHeight - paddingTop - paddingBottom;
631
+ const maxHeight = contentHeight * 5 + paddingTop + paddingBottom;
632
+
633
+ measurementsRef.current = {
634
+ singleLineHeight,
635
+ maxHeight,
636
+ paddingLeft,
637
+ paddingRight,
638
+ };
639
+
640
+ textarea.style.height = previousHeight;
641
+ textarea.style.maxHeight = `${maxHeight}px`;
642
+ }, []);
643
+
644
+ const adjustTextareaHeight = useCallback(() => {
645
+ const textarea = inputRef.current;
646
+ if (!textarea) {
647
+ return 0;
648
+ }
649
+
650
+ if (measurementsRef.current.singleLineHeight === 0) {
651
+ ensureMeasurements();
652
+ }
653
+
654
+ const { maxHeight } = measurementsRef.current;
655
+ if (maxHeight) {
656
+ textarea.style.maxHeight = `${maxHeight}px`;
657
+ }
658
+
659
+ textarea.style.height = "auto";
660
+ const scrollHeight = textarea.scrollHeight;
661
+ if (maxHeight) {
662
+ textarea.style.height = `${Math.min(scrollHeight, maxHeight)}px`;
663
+ } else {
664
+ textarea.style.height = `${scrollHeight}px`;
665
+ }
666
+
667
+ return scrollHeight;
668
+ }, [ensureMeasurements]);
669
+
670
+ const updateLayout = useCallback((nextLayout: "compact" | "expanded") => {
671
+ setLayout((prev) => {
672
+ if (prev === nextLayout) {
673
+ return prev;
674
+ }
675
+ ignoreResizeRef.current = true;
676
+ return nextLayout;
677
+ });
678
+ }, []);
679
+
680
+ const evaluateLayout = useCallback(() => {
681
+ if (mode !== "input") {
682
+ updateLayout("compact");
683
+ return;
684
+ }
685
+
686
+ if (
687
+ typeof window !== "undefined" &&
688
+ typeof window.matchMedia === "function"
689
+ ) {
690
+ const isMobileViewport = window.matchMedia("(max-width: 767px)").matches;
691
+ if (isMobileViewport) {
692
+ ensureMeasurements();
693
+ adjustTextareaHeight();
694
+ updateLayout("expanded");
695
+ return;
696
+ }
697
+ }
698
+
699
+ const textarea = inputRef.current;
700
+ const grid = gridRef.current;
701
+ const addContainer = addButtonContainerRef.current;
702
+ const actionsContainer = actionsContainerRef.current;
703
+
704
+ if (!textarea || !grid || !addContainer || !actionsContainer) {
705
+ return;
706
+ }
707
+
708
+ if (measurementsRef.current.singleLineHeight === 0) {
709
+ ensureMeasurements();
710
+ }
711
+
712
+ const scrollHeight = adjustTextareaHeight();
713
+ const baseline = measurementsRef.current.singleLineHeight;
714
+ const hasExplicitBreak = resolvedValue.includes("\n");
715
+ const renderedMultiline =
716
+ baseline > 0 ? scrollHeight > baseline + 1 : false;
717
+ let shouldExpand = hasExplicitBreak || renderedMultiline;
718
+
719
+ if (!shouldExpand) {
720
+ const gridStyles = window.getComputedStyle(grid);
721
+ const paddingLeft = parseFloat(gridStyles.paddingLeft) || 0;
722
+ const paddingRight = parseFloat(gridStyles.paddingRight) || 0;
723
+ const columnGap = parseFloat(gridStyles.columnGap) || 0;
724
+ const gridAvailableWidth = grid.clientWidth - paddingLeft - paddingRight;
725
+
726
+ if (gridAvailableWidth > 0) {
727
+ const addWidth = addContainer.getBoundingClientRect().width;
728
+ const actionsWidth = actionsContainer.getBoundingClientRect().width;
729
+ const compactWidth = Math.max(
730
+ gridAvailableWidth - addWidth - actionsWidth - columnGap * 2,
731
+ 0,
732
+ );
733
+
734
+ const canvas =
735
+ measurementCanvasRef.current ?? document.createElement("canvas");
736
+ if (!measurementCanvasRef.current) {
737
+ measurementCanvasRef.current = canvas;
738
+ }
739
+
740
+ const context = canvas.getContext("2d");
741
+ if (context) {
742
+ const textareaStyles = window.getComputedStyle(textarea);
743
+ const font =
744
+ textareaStyles.font ||
745
+ `${textareaStyles.fontStyle} ${textareaStyles.fontVariant} ${textareaStyles.fontWeight} ${textareaStyles.fontSize}/${textareaStyles.lineHeight} ${textareaStyles.fontFamily}`;
746
+ context.font = font;
747
+
748
+ const compactInnerWidth = Math.max(
749
+ compactWidth -
750
+ (measurementsRef.current.paddingLeft || 0) -
751
+ (measurementsRef.current.paddingRight || 0),
752
+ 0,
753
+ );
754
+
755
+ if (compactInnerWidth > 0) {
756
+ const lines =
757
+ resolvedValue.length > 0 ? resolvedValue.split("\n") : [""];
758
+ let longestWidth = 0;
759
+ for (const line of lines) {
760
+ const metrics = context.measureText(line || " ");
761
+ if (metrics.width > longestWidth) {
762
+ longestWidth = metrics.width;
763
+ }
764
+ }
765
+
766
+ if (longestWidth > compactInnerWidth) {
767
+ shouldExpand = true;
768
+ }
769
+ }
770
+ }
771
+ }
772
+ }
773
+
774
+ const nextLayout = shouldExpand ? "expanded" : "compact";
775
+ updateLayout(nextLayout);
776
+ }, [
777
+ adjustTextareaHeight,
778
+ ensureMeasurements,
779
+ mode,
780
+ resolvedValue,
781
+ updateLayout,
782
+ ]);
783
+
784
+ useLayoutEffect(() => {
785
+ evaluateLayout();
786
+ }, [evaluateLayout]);
787
+
788
+ useEffect(() => {
789
+ if (typeof ResizeObserver === "undefined") {
790
+ return;
791
+ }
792
+
793
+ const textarea = inputRef.current;
794
+ const grid = gridRef.current;
795
+ const addContainer = addButtonContainerRef.current;
796
+ const actionsContainer = actionsContainerRef.current;
797
+
798
+ if (!textarea || !grid || !addContainer || !actionsContainer) {
799
+ return;
800
+ }
801
+
802
+ const scheduleEvaluation = () => {
803
+ if (ignoreResizeRef.current) {
804
+ ignoreResizeRef.current = false;
805
+ return;
806
+ }
807
+
808
+ if (typeof window === "undefined") {
809
+ evaluateLayout();
810
+ return;
811
+ }
812
+
813
+ if (resizeEvaluationRafRef.current !== null) {
814
+ cancelAnimationFrame(resizeEvaluationRafRef.current);
815
+ }
816
+
817
+ resizeEvaluationRafRef.current = window.requestAnimationFrame(() => {
818
+ resizeEvaluationRafRef.current = null;
819
+ evaluateLayout();
820
+ });
821
+ };
822
+
823
+ const observer = new ResizeObserver(() => {
824
+ scheduleEvaluation();
825
+ });
826
+
827
+ observer.observe(grid);
828
+ observer.observe(addContainer);
829
+ observer.observe(actionsContainer);
830
+ observer.observe(textarea);
831
+
832
+ return () => {
833
+ observer.disconnect();
834
+ if (
835
+ typeof window !== "undefined" &&
836
+ resizeEvaluationRafRef.current !== null
837
+ ) {
838
+ cancelAnimationFrame(resizeEvaluationRafRef.current);
839
+ resizeEvaluationRafRef.current = null;
840
+ }
841
+ };
842
+ }, [evaluateLayout]);
843
+
844
+ const slashMenuVisible = commandQuery !== null && commandItems.length > 0;
845
+
846
+ useEffect(() => {
847
+ if (!slashMenuVisible || slashHighlightIndex < 0) {
848
+ return;
849
+ }
850
+
851
+ const active = slashMenuRef.current?.querySelector<HTMLElement>(
852
+ `[data-slash-index="${slashHighlightIndex}"]`,
853
+ );
854
+ active?.scrollIntoView({ block: "nearest" });
855
+ }, [slashMenuVisible, slashHighlightIndex]);
856
+
857
+ const slashMenu = slashMenuVisible ? (
858
+ <div
859
+ data-testid="copilot-slash-menu"
860
+ role="listbox"
861
+ aria-label="Slash commands"
862
+ ref={slashMenuRef}
863
+ className="cpk:absolute cpk:bottom-full cpk:left-0 cpk:right-0 cpk:z-30 cpk:mb-2 cpk:max-h-64 cpk:overflow-y-auto cpk:rounded-lg cpk:border cpk:border-border cpk:bg-white cpk:shadow-lg cpk:dark:border-[#3a3a3a] cpk:dark:bg-[#1f1f1f]"
864
+ style={{
865
+ maxHeight: `${SLASH_MENU_MAX_VISIBLE_ITEMS * SLASH_MENU_ITEM_HEIGHT_PX}px`,
866
+ }}
867
+ >
868
+ {filteredCommands.length === 0 ? (
869
+ <div className="cpk:px-3 cpk:py-2 cpk:text-sm cpk:text-muted-foreground">
870
+ No commands found
871
+ </div>
872
+ ) : (
873
+ filteredCommands.map((item, index) => {
874
+ const isActive = index === slashHighlightIndex;
875
+ return (
876
+ <button
877
+ key={`${item.label}-${index}`}
878
+ type="button"
879
+ role="option"
880
+ aria-selected={isActive}
881
+ data-active={isActive ? "true" : undefined}
882
+ data-slash-index={index}
883
+ className={twMerge(
884
+ "cpk:w-full cpk:px-3 cpk:py-2 cpk:text-left cpk:text-sm cpk:transition-colors",
885
+ "cpk:hover:bg-muted cpk:dark:hover:bg-[#2f2f2f]",
886
+ isActive
887
+ ? "cpk:bg-muted cpk:dark:bg-[#2f2f2f]"
888
+ : "cpk:bg-transparent",
889
+ )}
890
+ onMouseEnter={() => setSlashHighlightIndex(index)}
891
+ onMouseDown={(event) => {
892
+ event.preventDefault();
893
+ runCommand(item);
894
+ }}
895
+ >
896
+ {item.label}
897
+ </button>
898
+ );
899
+ })
900
+ )}
901
+ </div>
902
+ ) : null;
903
+
904
+ // The input pill (inner component)
905
+ const inputPill = (
906
+ <div
907
+ data-testid="copilot-chat-input"
908
+ className={twMerge(
909
+ // V1 compatibility class for custom styling
910
+ "copilotKitInput",
911
+ // Layout
912
+ "cpk:flex cpk:w-full cpk:flex-col cpk:items-center cpk:justify-center",
913
+ // Interaction
914
+ "cpk:cursor-text",
915
+ // Overflow and clipping
916
+ "cpk:overflow-visible cpk:bg-clip-padding cpk:contain-inline-size",
917
+ // Background
918
+ "cpk:bg-white cpk:dark:bg-[#303030]",
919
+ // Visual effects
920
+ "cpk:shadow-[0_4px_4px_0_#0000000a,0_0_1px_0_#0000009e] cpk:rounded-[28px]",
921
+ )}
922
+ onClick={handleContainerClick}
923
+ data-layout={isExpanded ? "expanded" : "compact"}
924
+ >
925
+ <div
926
+ ref={gridRef}
927
+ className={twMerge(
928
+ "cpk:grid cpk:w-full cpk:gap-x-3 cpk:gap-y-3 cpk:px-3 cpk:py-2",
929
+ isExpanded
930
+ ? "cpk:grid-cols-[auto_minmax(0,1fr)_auto] cpk:grid-rows-[auto_auto]"
931
+ : "cpk:grid-cols-[auto_minmax(0,1fr)_auto] cpk:items-center",
932
+ )}
933
+ data-layout={isExpanded ? "expanded" : "compact"}
934
+ >
935
+ <div
936
+ ref={addButtonContainerRef}
937
+ className={twMerge(
938
+ "cpk:flex cpk:items-center",
939
+ isExpanded ? "cpk:row-start-2" : "cpk:row-start-1",
940
+ "cpk:col-start-1",
941
+ )}
942
+ >
943
+ {BoundAddMenuButton}
944
+ </div>
945
+ <div
946
+ className={twMerge(
947
+ "cpk:relative cpk:flex cpk:min-w-0 cpk:flex-col cpk:min-h-[50px] cpk:justify-center",
948
+ isExpanded
949
+ ? "cpk:col-span-3 cpk:row-start-1"
950
+ : "cpk:col-start-2 cpk:row-start-1",
951
+ )}
952
+ >
953
+ {mode === "transcribe" ? (
954
+ BoundAudioRecorder
955
+ ) : mode === "processing" ? (
956
+ <div className="cpk:flex cpk:w-full cpk:items-center cpk:justify-center cpk:py-3 cpk:px-5">
957
+ <Loader2 className="cpk:size-[26px] cpk:animate-spin cpk:text-muted-foreground" />
958
+ </div>
959
+ ) : (
960
+ <>
961
+ {BoundTextArea}
962
+ {slashMenu}
963
+ </>
964
+ )}
965
+ </div>
966
+ <div
967
+ ref={actionsContainerRef}
968
+ className={twMerge(
969
+ "cpk:flex cpk:items-center cpk:justify-end cpk:gap-2",
970
+ isExpanded
971
+ ? "cpk:col-start-3 cpk:row-start-2"
972
+ : "cpk:col-start-3 cpk:row-start-1",
973
+ )}
974
+ >
975
+ {mode === "transcribe" ? (
976
+ <>
977
+ {onCancelTranscribe && BoundCancelTranscribeButton}
978
+ {onFinishTranscribe && BoundFinishTranscribeButton}
979
+ </>
980
+ ) : (
981
+ <>
982
+ {onStartTranscribe && BoundStartTranscribeButton}
983
+ {BoundSendButton}
984
+ </>
985
+ )}
986
+ </div>
987
+ </div>
988
+ </div>
989
+ );
990
+
991
+ return (
992
+ <div
993
+ data-copilotkit
994
+ ref={containerRef}
995
+ className={cn(
996
+ "cpk:pointer-events-none cpk:relative cpk:z-20",
997
+ positioning === "absolute" &&
998
+ "cpk:absolute cpk:bottom-0 cpk:left-0 cpk:right-0",
999
+ className,
1000
+ )}
1001
+ style={{
1002
+ transform:
1003
+ keyboardHeight > 0 ? `translateY(-${keyboardHeight}px)` : undefined,
1004
+ transition: "transform 0.2s ease-out",
1005
+ }}
1006
+ {...props}
1007
+ >
1008
+ <div className="cpk:max-w-3xl cpk:mx-auto cpk:py-0 cpk:px-4 cpk:sm:px-0 cpk:[div[data-sidebar-chat]_&]:px-8 cpk:[div[data-popup-chat]_&]:px-4 cpk:pointer-events-auto">
1009
+ {inputPill}
1010
+ </div>
1011
+ {shouldShowDisclaimer && BoundDisclaimer}
1012
+ </div>
1013
+ );
1014
+ }
1015
+
1016
+ // eslint-disable-next-line @typescript-eslint/no-namespace
1017
+ export namespace CopilotChatInput {
1018
+ export const SendButton: React.FC<
1019
+ React.ButtonHTMLAttributes<HTMLButtonElement>
1020
+ > = ({ className, children, ...props }) => (
1021
+ <div className="cpk:mr-[10px]">
1022
+ <Button
1023
+ type="button"
1024
+ data-testid="copilot-send-button"
1025
+ variant="chatInputToolbarPrimary"
1026
+ size="chatInputToolbarIcon"
1027
+ className={className}
1028
+ {...props}
1029
+ >
1030
+ {children ?? <ArrowUp className="cpk:size-[18px]" />}
1031
+ </Button>
1032
+ </div>
1033
+ );
1034
+
1035
+ export const ToolbarButton: React.FC<
1036
+ React.ButtonHTMLAttributes<HTMLButtonElement> & {
1037
+ icon: React.ReactNode;
1038
+ labelKey: keyof CopilotChatLabels;
1039
+ defaultClassName?: string;
1040
+ }
1041
+ > = ({ icon, labelKey, defaultClassName, className, ...props }) => {
1042
+ const config = useCopilotChatConfiguration();
1043
+ const labels = config?.labels ?? CopilotChatDefaultLabels;
1044
+ return (
1045
+ <Tooltip>
1046
+ <TooltipTrigger asChild>
1047
+ <Button
1048
+ type="button"
1049
+ variant="chatInputToolbarSecondary"
1050
+ size="chatInputToolbarIcon"
1051
+ className={twMerge(defaultClassName, className)}
1052
+ {...props}
1053
+ >
1054
+ {icon}
1055
+ </Button>
1056
+ </TooltipTrigger>
1057
+ <TooltipContent side="bottom">
1058
+ <p>{labels[labelKey]}</p>
1059
+ </TooltipContent>
1060
+ </Tooltip>
1061
+ );
1062
+ };
1063
+
1064
+ export const StartTranscribeButton: React.FC<
1065
+ React.ButtonHTMLAttributes<HTMLButtonElement>
1066
+ > = (props) => (
1067
+ <ToolbarButton
1068
+ data-testid="copilot-start-transcribe-button"
1069
+ icon={<Mic className="cpk:size-[18px]" />}
1070
+ labelKey="chatInputToolbarStartTranscribeButtonLabel"
1071
+ defaultClassName="cpk:mr-2"
1072
+ {...props}
1073
+ />
1074
+ );
1075
+
1076
+ export const CancelTranscribeButton: React.FC<
1077
+ React.ButtonHTMLAttributes<HTMLButtonElement>
1078
+ > = (props) => (
1079
+ <ToolbarButton
1080
+ data-testid="copilot-cancel-transcribe-button"
1081
+ icon={<X className="cpk:size-[18px]" />}
1082
+ labelKey="chatInputToolbarCancelTranscribeButtonLabel"
1083
+ defaultClassName="cpk:mr-2"
1084
+ {...props}
1085
+ />
1086
+ );
1087
+
1088
+ export const FinishTranscribeButton: React.FC<
1089
+ React.ButtonHTMLAttributes<HTMLButtonElement>
1090
+ > = (props) => (
1091
+ <ToolbarButton
1092
+ data-testid="copilot-finish-transcribe-button"
1093
+ icon={<Check className="cpk:size-[18px]" />}
1094
+ labelKey="chatInputToolbarFinishTranscribeButtonLabel"
1095
+ defaultClassName="cpk:mr-[10px]"
1096
+ {...props}
1097
+ />
1098
+ );
1099
+
1100
+ export const AddMenuButton: React.FC<
1101
+ React.ButtonHTMLAttributes<HTMLButtonElement> & {
1102
+ toolsMenu?: (ToolsMenuItem | "-")[];
1103
+ onAddFile?: () => void;
1104
+ }
1105
+ > = ({ className, toolsMenu, onAddFile, disabled, ...props }) => {
1106
+ const config = useCopilotChatConfiguration();
1107
+ const labels = config?.labels ?? CopilotChatDefaultLabels;
1108
+
1109
+ const menuItems = useMemo<(ToolsMenuItem | "-")[]>(() => {
1110
+ const items: (ToolsMenuItem | "-")[] = [];
1111
+
1112
+ if (onAddFile) {
1113
+ items.push({
1114
+ label: labels.chatInputToolbarAddButtonLabel,
1115
+ action: onAddFile,
1116
+ });
1117
+ }
1118
+
1119
+ if (toolsMenu && toolsMenu.length > 0) {
1120
+ if (items.length > 0) {
1121
+ items.push("-");
1122
+ }
1123
+
1124
+ for (const item of toolsMenu) {
1125
+ if (item === "-") {
1126
+ if (items.length === 0 || items[items.length - 1] === "-") {
1127
+ continue;
1128
+ }
1129
+ items.push(item);
1130
+ } else {
1131
+ items.push(item);
1132
+ }
1133
+ }
1134
+
1135
+ while (items.length > 0 && items[items.length - 1] === "-") {
1136
+ items.pop();
1137
+ }
1138
+ }
1139
+
1140
+ return items;
1141
+ }, [onAddFile, toolsMenu, labels.chatInputToolbarAddButtonLabel]);
1142
+
1143
+ const renderMenuItems = useCallback(
1144
+ (items: (ToolsMenuItem | "-")[]): React.ReactNode =>
1145
+ items.map((item, index) => {
1146
+ if (item === "-") {
1147
+ return <DropdownMenuSeparator key={`separator-${index}`} />;
1148
+ }
1149
+
1150
+ if (item.items && item.items.length > 0) {
1151
+ return (
1152
+ <DropdownMenuSub key={`group-${index}`}>
1153
+ <DropdownMenuSubTrigger>{item.label}</DropdownMenuSubTrigger>
1154
+ <DropdownMenuSubContent>
1155
+ {renderMenuItems(item.items)}
1156
+ </DropdownMenuSubContent>
1157
+ </DropdownMenuSub>
1158
+ );
1159
+ }
1160
+
1161
+ return (
1162
+ <DropdownMenuItem key={`item-${index}`} onClick={item.action}>
1163
+ {item.label}
1164
+ </DropdownMenuItem>
1165
+ );
1166
+ }),
1167
+ [],
1168
+ );
1169
+
1170
+ const hasMenuItems = menuItems.length > 0;
1171
+ const isDisabled = disabled || !hasMenuItems;
1172
+
1173
+ return (
1174
+ <DropdownMenu>
1175
+ <Tooltip>
1176
+ <TooltipTrigger asChild>
1177
+ <DropdownMenuTrigger asChild>
1178
+ <Button
1179
+ type="button"
1180
+ data-testid="copilot-add-menu-button"
1181
+ variant="chatInputToolbarSecondary"
1182
+ size="chatInputToolbarIcon"
1183
+ className={twMerge("cpk:ml-1", className)}
1184
+ disabled={isDisabled}
1185
+ {...props}
1186
+ >
1187
+ <Plus className="cpk:size-[20px]" />
1188
+ </Button>
1189
+ </DropdownMenuTrigger>
1190
+ </TooltipTrigger>
1191
+ <TooltipContent side="bottom">
1192
+ <p className="cpk:flex cpk:items-center cpk:gap-1 cpk:text-xs cpk:font-medium">
1193
+ <span>Add files and more</span>
1194
+ <code className="cpk:rounded cpk:bg-[#4a4a4a] cpk:px-1 cpk:py-[1px] cpk:font-mono cpk:text-[11px] cpk:text-white cpk:dark:bg-[#e0e0e0] cpk:dark:text-black">
1195
+ /
1196
+ </code>
1197
+ </p>
1198
+ </TooltipContent>
1199
+ </Tooltip>
1200
+ {hasMenuItems && (
1201
+ <DropdownMenuContent side="top" align="start">
1202
+ {renderMenuItems(menuItems)}
1203
+ </DropdownMenuContent>
1204
+ )}
1205
+ </DropdownMenu>
1206
+ );
1207
+ };
1208
+
1209
+ export type TextAreaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
1210
+
1211
+ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
1212
+ function TextArea(
1213
+ { style, className, autoFocus, placeholder, ...props },
1214
+ ref,
1215
+ ) {
1216
+ const internalTextareaRef = useRef<HTMLTextAreaElement>(null);
1217
+ const config = useCopilotChatConfiguration();
1218
+ const labels = config?.labels ?? CopilotChatDefaultLabels;
1219
+
1220
+ useImperativeHandle(
1221
+ ref,
1222
+ () => internalTextareaRef.current as HTMLTextAreaElement,
1223
+ );
1224
+
1225
+ // Auto-scroll input into view on mobile when focused
1226
+ useEffect(() => {
1227
+ const textarea = internalTextareaRef.current;
1228
+ if (!textarea) return;
1229
+
1230
+ const handleFocus = () => {
1231
+ // Small delay to let the keyboard start appearing
1232
+ setTimeout(() => {
1233
+ textarea.scrollIntoView({ behavior: "smooth", block: "nearest" });
1234
+ }, 300);
1235
+ };
1236
+
1237
+ textarea.addEventListener("focus", handleFocus);
1238
+ return () => textarea.removeEventListener("focus", handleFocus);
1239
+ }, []);
1240
+
1241
+ useEffect(() => {
1242
+ if (autoFocus) {
1243
+ internalTextareaRef.current?.focus();
1244
+ }
1245
+ }, [autoFocus]);
1246
+
1247
+ return (
1248
+ <textarea
1249
+ ref={internalTextareaRef}
1250
+ data-testid="copilot-chat-textarea"
1251
+ placeholder={placeholder ?? labels.chatInputPlaceholder}
1252
+ className={twMerge(
1253
+ "cpk:bg-transparent cpk:outline-none cpk:antialiased cpk:font-regular cpk:leading-relaxed cpk:text-[16px] cpk:placeholder:text-[#00000077] cpk:dark:placeholder:text-[#fffc]",
1254
+ className,
1255
+ )}
1256
+ style={{
1257
+ overflow: "auto",
1258
+ resize: "none",
1259
+ ...style,
1260
+ }}
1261
+ rows={1}
1262
+ {...props}
1263
+ />
1264
+ );
1265
+ },
1266
+ );
1267
+
1268
+ export const AudioRecorder = CopilotChatAudioRecorder;
1269
+
1270
+ export const Disclaimer: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
1271
+ className,
1272
+ ...props
1273
+ }) => {
1274
+ const config = useCopilotChatConfiguration();
1275
+ const labels = config?.labels ?? CopilotChatDefaultLabels;
1276
+ return (
1277
+ <div
1278
+ className={cn(
1279
+ "cpk:text-center cpk:text-xs cpk:text-muted-foreground cpk:py-3 cpk:px-4 cpk:max-w-3xl cpk:mx-auto",
1280
+ className,
1281
+ )}
1282
+ {...props}
1283
+ >
1284
+ {labels.chatDisclaimerText}
1285
+ </div>
1286
+ );
1287
+ };
1288
+ }
1289
+
1290
+ CopilotChatInput.TextArea.displayName = "CopilotChatInput.TextArea";
1291
+ CopilotChatInput.SendButton.displayName = "CopilotChatInput.SendButton";
1292
+ CopilotChatInput.ToolbarButton.displayName = "CopilotChatInput.ToolbarButton";
1293
+ CopilotChatInput.StartTranscribeButton.displayName =
1294
+ "CopilotChatInput.StartTranscribeButton";
1295
+ CopilotChatInput.CancelTranscribeButton.displayName =
1296
+ "CopilotChatInput.CancelTranscribeButton";
1297
+ CopilotChatInput.FinishTranscribeButton.displayName =
1298
+ "CopilotChatInput.FinishTranscribeButton";
1299
+ CopilotChatInput.AddMenuButton.displayName = "CopilotChatInput.AddMenuButton";
1300
+ CopilotChatInput.Disclaimer.displayName = "CopilotChatInput.Disclaimer";
1301
+
1302
+ export default CopilotChatInput;