@copilotkit/react-ui 1.8.6 → 1.8.7-next.0

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 (149) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/{chunk-Q2YY2NX3.mjs → chunk-24TDU7MY.mjs} +2 -2
  3. package/dist/{chunk-XNQO5AZZ.mjs → chunk-ABHUX6T6.mjs} +2 -2
  4. package/dist/{chunk-QJKMOGWN.mjs → chunk-BDNHZ3GW.mjs} +4 -3
  5. package/dist/chunk-BDNHZ3GW.mjs.map +1 -0
  6. package/dist/{chunk-NMNC4ROZ.mjs → chunk-DSQGQJI4.mjs} +2 -2
  7. package/dist/{chunk-HEIDCT7I.mjs → chunk-HWMFMBJC.mjs} +2 -2
  8. package/dist/chunk-HWMFMBJC.mjs.map +1 -0
  9. package/dist/{chunk-UN2E3HCK.mjs → chunk-IEMQ2SQW.mjs} +6 -4
  10. package/dist/chunk-IEMQ2SQW.mjs.map +1 -0
  11. package/dist/{chunk-ZY25LVYR.mjs → chunk-IJADIQAR.mjs} +20 -2
  12. package/dist/chunk-IJADIQAR.mjs.map +1 -0
  13. package/dist/{chunk-X6EFGEBJ.mjs → chunk-JOL7NS2W.mjs} +2 -2
  14. package/dist/{chunk-PCTCOQK2.mjs → chunk-KENCH7RN.mjs} +2 -2
  15. package/dist/{chunk-ZLRUNNS7.mjs → chunk-O34Z4XM2.mjs} +170 -30
  16. package/dist/chunk-O34Z4XM2.mjs.map +1 -0
  17. package/dist/{chunk-5M7ODWKH.mjs → chunk-OZXUB3V7.mjs} +3 -3
  18. package/dist/chunk-PLHTVHUW.mjs +82 -0
  19. package/dist/chunk-PLHTVHUW.mjs.map +1 -0
  20. package/dist/{chunk-62QMTKMJ.mjs → chunk-POWCBXRY.mjs} +3 -3
  21. package/dist/chunk-PXEVB7IK.mjs +1 -0
  22. package/dist/{chunk-HIORSNVD.mjs → chunk-Q2NFQTCQ.mjs} +2 -2
  23. package/dist/chunk-SLTG4L62.mjs +78 -0
  24. package/dist/chunk-SLTG4L62.mjs.map +1 -0
  25. package/dist/{chunk-SMJ3QQCE.mjs → chunk-T7N77F5Y.mjs} +2 -2
  26. package/dist/{chunk-YOEL33HG.mjs → chunk-UFN2VWSR.mjs} +2 -2
  27. package/dist/{chunk-2OTVZXDX.mjs → chunk-UH2UFL5W.mjs} +3 -3
  28. package/dist/{chunk-D5XIJNXQ.mjs → chunk-VGPQYMKJ.mjs} +8 -8
  29. package/dist/{chunk-WNC6OCIB.mjs → chunk-XFCMZH2H.mjs} +2 -2
  30. package/dist/{chunk-ORSMX3SE.mjs → chunk-XWG3L6QC.mjs} +15 -1
  31. package/dist/{chunk-ORSMX3SE.mjs.map → chunk-XWG3L6QC.mjs.map} +1 -1
  32. package/dist/{chunk-TOQ7P4DO.mjs → chunk-XZNY26GH.mjs} +2 -2
  33. package/dist/{chunk-GOAED4H6.mjs → chunk-Y7UO3RPW.mjs} +10 -10
  34. package/dist/components/chat/Button.js.map +1 -1
  35. package/dist/components/chat/Button.mjs +3 -3
  36. package/dist/components/chat/Chat.d.ts +23 -3
  37. package/dist/components/chat/Chat.js +341 -30
  38. package/dist/components/chat/Chat.js.map +1 -1
  39. package/dist/components/chat/Chat.mjs +16 -14
  40. package/dist/components/chat/ChatContext.d.ts +5 -0
  41. package/dist/components/chat/ChatContext.js +15 -1
  42. package/dist/components/chat/ChatContext.js.map +1 -1
  43. package/dist/components/chat/ChatContext.mjs +2 -2
  44. package/dist/components/chat/CodeBlock.js.map +1 -1
  45. package/dist/components/chat/CodeBlock.mjs +2 -2
  46. package/dist/components/chat/Header.js.map +1 -1
  47. package/dist/components/chat/Header.mjs +5 -5
  48. package/dist/components/chat/Icons.d.ts +2 -1
  49. package/dist/components/chat/Icons.js +17 -2
  50. package/dist/components/chat/Icons.js.map +1 -1
  51. package/dist/components/chat/Icons.mjs +5 -3
  52. package/dist/components/chat/ImageUploadQueue.d.ts +13 -0
  53. package/dist/components/chat/ImageUploadQueue.js +106 -0
  54. package/dist/components/chat/ImageUploadQueue.js.map +1 -0
  55. package/dist/components/chat/ImageUploadQueue.mjs +8 -0
  56. package/dist/components/chat/ImageUploadQueue.mjs.map +1 -0
  57. package/dist/components/chat/Input.d.ts +1 -1
  58. package/dist/components/chat/Input.js +2 -1
  59. package/dist/components/chat/Input.js.map +1 -1
  60. package/dist/components/chat/Input.mjs +3 -3
  61. package/dist/components/chat/Markdown.js.map +1 -1
  62. package/dist/components/chat/Markdown.mjs +3 -3
  63. package/dist/components/chat/Messages.d.ts +1 -1
  64. package/dist/components/chat/Messages.js +18 -0
  65. package/dist/components/chat/Messages.js.map +1 -1
  66. package/dist/components/chat/Messages.mjs +3 -3
  67. package/dist/components/chat/Modal.js +348 -37
  68. package/dist/components/chat/Modal.js.map +1 -1
  69. package/dist/components/chat/Modal.mjs +22 -20
  70. package/dist/components/chat/Popup.js +350 -39
  71. package/dist/components/chat/Popup.js.map +1 -1
  72. package/dist/components/chat/Popup.mjs +23 -21
  73. package/dist/components/chat/Sidebar.js +350 -39
  74. package/dist/components/chat/Sidebar.js.map +1 -1
  75. package/dist/components/chat/Sidebar.mjs +23 -21
  76. package/dist/components/chat/Suggestion.js.map +1 -1
  77. package/dist/components/chat/Suggestion.mjs +2 -2
  78. package/dist/components/chat/Window.js.map +1 -1
  79. package/dist/components/chat/Window.mjs +3 -3
  80. package/dist/components/chat/index.d.ts +1 -0
  81. package/dist/components/chat/index.js +354 -41
  82. package/dist/components/chat/index.js.map +1 -1
  83. package/dist/components/chat/index.mjs +30 -25
  84. package/dist/components/chat/messages/AssistantMessage.js.map +1 -1
  85. package/dist/components/chat/messages/AssistantMessage.mjs +5 -5
  86. package/dist/components/chat/messages/RenderActionExecutionMessage.js.map +1 -1
  87. package/dist/components/chat/messages/RenderActionExecutionMessage.mjs +6 -6
  88. package/dist/components/chat/messages/RenderAgentStateMessage.js.map +1 -1
  89. package/dist/components/chat/messages/RenderAgentStateMessage.mjs +6 -6
  90. package/dist/components/chat/messages/RenderImageMessage.d.ts +7 -0
  91. package/dist/components/chat/messages/RenderImageMessage.js +774 -0
  92. package/dist/components/chat/messages/RenderImageMessage.js.map +1 -0
  93. package/dist/components/chat/messages/RenderImageMessage.mjs +15 -0
  94. package/dist/components/chat/messages/RenderImageMessage.mjs.map +1 -0
  95. package/dist/components/chat/messages/RenderResultMessage.js.map +1 -1
  96. package/dist/components/chat/messages/RenderResultMessage.mjs +6 -6
  97. package/dist/components/chat/messages/RenderTextMessage.js +1 -1
  98. package/dist/components/chat/messages/RenderTextMessage.js.map +1 -1
  99. package/dist/components/chat/messages/RenderTextMessage.mjs +7 -7
  100. package/dist/components/chat/messages/UserMessage.js +1 -1
  101. package/dist/components/chat/messages/UserMessage.js.map +1 -1
  102. package/dist/components/chat/messages/UserMessage.mjs +1 -1
  103. package/dist/components/chat/props.d.ts +3 -0
  104. package/dist/components/chat/props.js.map +1 -1
  105. package/dist/components/dev-console/console.js.map +1 -1
  106. package/dist/components/dev-console/console.mjs +3 -3
  107. package/dist/components/dev-console/index.js.map +1 -1
  108. package/dist/components/dev-console/index.mjs +3 -3
  109. package/dist/components/index.d.ts +1 -0
  110. package/dist/components/index.js +354 -41
  111. package/dist/components/index.js.map +1 -1
  112. package/dist/components/index.mjs +30 -25
  113. package/dist/index.d.ts +1 -0
  114. package/dist/index.js +354 -41
  115. package/dist/index.js.map +1 -1
  116. package/dist/index.mjs +31 -26
  117. package/package.json +6 -6
  118. package/src/components/chat/Chat.tsx +198 -18
  119. package/src/components/chat/ChatContext.tsx +7 -0
  120. package/src/components/chat/Icons.tsx +14 -0
  121. package/src/components/chat/ImageUploadQueue.tsx +77 -0
  122. package/src/components/chat/Input.tsx +8 -1
  123. package/src/components/chat/Messages.tsx +17 -0
  124. package/src/components/chat/index.tsx +1 -0
  125. package/src/components/chat/messages/RenderImageMessage.tsx +64 -0
  126. package/src/components/chat/messages/UserMessage.tsx +5 -1
  127. package/src/components/chat/props.ts +3 -0
  128. package/dist/chunk-HEIDCT7I.mjs.map +0 -1
  129. package/dist/chunk-QJKMOGWN.mjs.map +0 -1
  130. package/dist/chunk-SQMEPWVT.mjs +0 -1
  131. package/dist/chunk-UN2E3HCK.mjs.map +0 -1
  132. package/dist/chunk-ZLRUNNS7.mjs.map +0 -1
  133. package/dist/chunk-ZY25LVYR.mjs.map +0 -1
  134. /package/dist/{chunk-Q2YY2NX3.mjs.map → chunk-24TDU7MY.mjs.map} +0 -0
  135. /package/dist/{chunk-XNQO5AZZ.mjs.map → chunk-ABHUX6T6.mjs.map} +0 -0
  136. /package/dist/{chunk-NMNC4ROZ.mjs.map → chunk-DSQGQJI4.mjs.map} +0 -0
  137. /package/dist/{chunk-X6EFGEBJ.mjs.map → chunk-JOL7NS2W.mjs.map} +0 -0
  138. /package/dist/{chunk-PCTCOQK2.mjs.map → chunk-KENCH7RN.mjs.map} +0 -0
  139. /package/dist/{chunk-5M7ODWKH.mjs.map → chunk-OZXUB3V7.mjs.map} +0 -0
  140. /package/dist/{chunk-62QMTKMJ.mjs.map → chunk-POWCBXRY.mjs.map} +0 -0
  141. /package/dist/{chunk-SQMEPWVT.mjs.map → chunk-PXEVB7IK.mjs.map} +0 -0
  142. /package/dist/{chunk-HIORSNVD.mjs.map → chunk-Q2NFQTCQ.mjs.map} +0 -0
  143. /package/dist/{chunk-SMJ3QQCE.mjs.map → chunk-T7N77F5Y.mjs.map} +0 -0
  144. /package/dist/{chunk-YOEL33HG.mjs.map → chunk-UFN2VWSR.mjs.map} +0 -0
  145. /package/dist/{chunk-2OTVZXDX.mjs.map → chunk-UH2UFL5W.mjs.map} +0 -0
  146. /package/dist/{chunk-D5XIJNXQ.mjs.map → chunk-VGPQYMKJ.mjs.map} +0 -0
  147. /package/dist/{chunk-WNC6OCIB.mjs.map → chunk-XFCMZH2H.mjs.map} +0 -0
  148. /package/dist/{chunk-TOQ7P4DO.mjs.map → chunk-XZNY26GH.mjs.map} +0 -0
  149. /package/dist/{chunk-GOAED4H6.mjs.map → chunk-Y7UO3RPW.mjs.map} +0 -0
package/dist/index.mjs CHANGED
@@ -1,55 +1,59 @@
1
1
  "use client";
2
2
  import "./chunk-EFZPSZWO.mjs";
3
+ import "./chunk-IU3WTXLQ.mjs";
3
4
  import "./chunk-MMVDU6DF.mjs";
4
- import "./chunk-SQMEPWVT.mjs";
5
+ import "./chunk-PXEVB7IK.mjs";
6
+ import {
7
+ CopilotPopup
8
+ } from "./chunk-T7N77F5Y.mjs";
5
9
  import {
6
10
  CopilotSidebar
7
- } from "./chunk-X6EFGEBJ.mjs";
11
+ } from "./chunk-JOL7NS2W.mjs";
8
12
  import "./chunk-WB3YULQ4.mjs";
9
- import {
10
- CopilotPopup
11
- } from "./chunk-SMJ3QQCE.mjs";
12
- import "./chunk-GOAED4H6.mjs";
13
- import "./chunk-HIORSNVD.mjs";
14
- import "./chunk-2OTVZXDX.mjs";
13
+ import "./chunk-Y7UO3RPW.mjs";
14
+ import "./chunk-Q2NFQTCQ.mjs";
15
+ import "./chunk-UH2UFL5W.mjs";
15
16
  import "./chunk-V7W6IM2V.mjs";
16
17
  import {
17
18
  CopilotDevConsole
18
- } from "./chunk-D5XIJNXQ.mjs";
19
+ } from "./chunk-VGPQYMKJ.mjs";
20
+ import "./chunk-Q5V6S67N.mjs";
19
21
  import {
20
22
  shouldShowDevConsole
21
23
  } from "./chunk-6TCUJ3B7.mjs";
22
24
  import "./chunk-KXE2JCUH.mjs";
23
25
  import "./chunk-NRA3CFEE.mjs";
24
26
  import "./chunk-BH6PCAAL.mjs";
25
- import "./chunk-Q5V6S67N.mjs";
26
- import "./chunk-YOEL33HG.mjs";
27
+ import "./chunk-UFN2VWSR.mjs";
27
28
  import {
28
29
  CopilotChat
29
- } from "./chunk-ZLRUNNS7.mjs";
30
- import "./chunk-Q2YY2NX3.mjs";
31
- import "./chunk-WNC6OCIB.mjs";
32
- import "./chunk-62QMTKMJ.mjs";
30
+ } from "./chunk-O34Z4XM2.mjs";
31
+ import "./chunk-DSQGQJI4.mjs";
32
+ import "./chunk-24TDU7MY.mjs";
33
+ import {
34
+ RenderImageMessage
35
+ } from "./chunk-SLTG4L62.mjs";
36
+ import "./chunk-XFCMZH2H.mjs";
37
+ import "./chunk-POWCBXRY.mjs";
33
38
  import {
34
39
  UserMessage
35
- } from "./chunk-HEIDCT7I.mjs";
36
- import "./chunk-NMNC4ROZ.mjs";
40
+ } from "./chunk-HWMFMBJC.mjs";
37
41
  import {
38
42
  AssistantMessage
39
- } from "./chunk-5M7ODWKH.mjs";
40
- import "./chunk-TOQ7P4DO.mjs";
41
- import "./chunk-QJKMOGWN.mjs";
43
+ } from "./chunk-OZXUB3V7.mjs";
44
+ import "./chunk-XZNY26GH.mjs";
45
+ import "./chunk-PLHTVHUW.mjs";
46
+ import "./chunk-BDNHZ3GW.mjs";
42
47
  import "./chunk-YQFVRDNC.mjs";
43
48
  import {
44
49
  Markdown
45
- } from "./chunk-XNQO5AZZ.mjs";
46
- import "./chunk-PCTCOQK2.mjs";
47
- import "./chunk-ZY25LVYR.mjs";
50
+ } from "./chunk-ABHUX6T6.mjs";
51
+ import "./chunk-KENCH7RN.mjs";
52
+ import "./chunk-IJADIQAR.mjs";
48
53
  import {
49
54
  useChatContext
50
- } from "./chunk-UN2E3HCK.mjs";
51
- import "./chunk-ORSMX3SE.mjs";
52
- import "./chunk-IU3WTXLQ.mjs";
55
+ } from "./chunk-IEMQ2SQW.mjs";
56
+ import "./chunk-XWG3L6QC.mjs";
53
57
  import "./chunk-T26KLXLH.mjs";
54
58
  import {
55
59
  useCopilotChatSuggestions
@@ -65,6 +69,7 @@ export {
65
69
  CopilotPopup,
66
70
  CopilotSidebar,
67
71
  Markdown,
72
+ RenderImageMessage,
68
73
  UserMessage,
69
74
  shouldShowDevConsole,
70
75
  useChatContext,
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "publishConfig": {
10
10
  "access": "public"
11
11
  },
12
- "version": "1.8.6",
12
+ "version": "1.8.7-next.0",
13
13
  "sideEffects": [
14
14
  "**/*.css"
15
15
  ],
@@ -40,9 +40,9 @@
40
40
  "ts-jest": "^29.1.1",
41
41
  "tsup": "^6.7.0",
42
42
  "typescript": "^5.2.3",
43
+ "eslint-config-custom": "1.4.6",
43
44
  "tailwind-config": "1.4.6",
44
- "tsconfig": "1.4.6",
45
- "eslint-config-custom": "1.4.6"
45
+ "tsconfig": "1.4.6"
46
46
  },
47
47
  "dependencies": {
48
48
  "@headlessui/react": "^2.1.3",
@@ -50,9 +50,9 @@
50
50
  "react-syntax-highlighter": "^15.5.0",
51
51
  "remark-gfm": "^3.0.1",
52
52
  "remark-math": "^5.1.1",
53
- "@copilotkit/react-core": "1.8.6",
54
- "@copilotkit/runtime-client-gql": "1.8.6",
55
- "@copilotkit/shared": "1.8.6"
53
+ "@copilotkit/react-core": "1.8.7-next.0",
54
+ "@copilotkit/runtime-client-gql": "1.8.7-next.0",
55
+ "@copilotkit/shared": "1.8.7-next.0"
56
56
  },
57
57
  "keywords": [
58
58
  "copilotkit",
@@ -57,6 +57,7 @@ import { RenderTextMessage as DefaultRenderTextMessage } from "./messages/Render
57
57
  import { RenderActionExecutionMessage as DefaultRenderActionExecutionMessage } from "./messages/RenderActionExecutionMessage";
58
58
  import { RenderResultMessage as DefaultRenderResultMessage } from "./messages/RenderResultMessage";
59
59
  import { RenderAgentStateMessage as DefaultRenderAgentStateMessage } from "./messages/RenderAgentStateMessage";
60
+ import { RenderImageMessage as DefaultRenderImageMessage } from "./messages/RenderImageMessage";
60
61
  import { AssistantMessage as DefaultAssistantMessage } from "./messages/AssistantMessage";
61
62
  import { UserMessage as DefaultUserMessage } from "./messages/UserMessage";
62
63
  import { Suggestion } from "./Suggestion";
@@ -69,7 +70,7 @@ import {
69
70
  } from "@copilotkit/react-core";
70
71
  import { reloadSuggestions } from "./Suggestion";
71
72
  import { CopilotChatSuggestion } from "../../types/suggestions";
72
- import { Message, Role, TextMessage } from "@copilotkit/runtime-client-gql";
73
+ import { Message, Role, TextMessage, ImageMessage } from "@copilotkit/runtime-client-gql";
73
74
  import { randomId } from "@copilotkit/shared";
74
75
  import {
75
76
  AssistantMessageProps,
@@ -80,6 +81,7 @@ import {
80
81
  } from "./props";
81
82
 
82
83
  import { HintFunction, runAgent, stopAgent } from "@copilotkit/react-core";
84
+ import { ImageUploadQueue } from "./ImageUploadQueue";
83
85
 
84
86
  /**
85
87
  * Props for CopilotChat component.
@@ -145,6 +147,17 @@ export interface CopilotChatProps {
145
147
  */
146
148
  labels?: CopilotChatLabels;
147
149
 
150
+ /**
151
+ * Enable image upload button (image inputs only supported on some models)
152
+ */
153
+ imageUploadsEnabled?: boolean;
154
+
155
+ /**
156
+ * The 'accept' attribute for the file input used for image uploads.
157
+ * Defaults to "image/*".
158
+ */
159
+ inputFileAccept?: string;
160
+
148
161
  /**
149
162
  * A function that takes in context string and instructions and returns
150
163
  * the system message to include in the chat request.
@@ -188,6 +201,11 @@ export interface CopilotChatProps {
188
201
  */
189
202
  RenderResultMessage?: React.ComponentType<RenderMessageProps>;
190
203
 
204
+ /**
205
+ * A custom RenderImageMessage component to use instead of the default.
206
+ */
207
+ RenderImageMessage?: React.ComponentType<RenderMessageProps>;
208
+
191
209
  /**
192
210
  * A custom Input component to use instead of the default.
193
211
  */
@@ -257,6 +275,11 @@ export type OnStopGeneration = (args: OnStopGenerationArguments) => void;
257
275
 
258
276
  export type OnReloadMessages = (args: OnReloadMessagesArguments) => void;
259
277
 
278
+ export type ImageUpload = {
279
+ contentType: string;
280
+ bytes: string;
281
+ };
282
+
260
283
  export function CopilotChat({
261
284
  instructions,
262
285
  onSubmitMessage,
@@ -273,14 +296,69 @@ export function CopilotChat({
273
296
  RenderActionExecutionMessage = DefaultRenderActionExecutionMessage,
274
297
  RenderAgentStateMessage = DefaultRenderAgentStateMessage,
275
298
  RenderResultMessage = DefaultRenderResultMessage,
299
+ RenderImageMessage = DefaultRenderImageMessage,
276
300
  Input = DefaultInput,
277
301
  className,
278
302
  icons,
279
303
  labels,
280
304
  AssistantMessage = DefaultAssistantMessage,
281
305
  UserMessage = DefaultUserMessage,
306
+ imageUploadsEnabled,
307
+ inputFileAccept = "image/*",
282
308
  }: CopilotChatProps) {
283
309
  const { additionalInstructions, setChatInstructions } = useCopilotContext();
310
+ const [selectedImages, setSelectedImages] = useState<Array<ImageUpload>>([]);
311
+ const fileInputRef = useRef<HTMLInputElement>(null);
312
+
313
+ // Clipboard paste handler
314
+ useEffect(() => {
315
+ if (!imageUploadsEnabled) return;
316
+
317
+ const handlePaste = async (e: ClipboardEvent) => {
318
+ const target = e.target as HTMLElement;
319
+ if (!target.parentElement?.classList.contains("copilotKitInput")) return;
320
+
321
+ const items = Array.from(e.clipboardData?.items || []);
322
+ const imageItems = items.filter((item) => item.type.startsWith("image/"));
323
+
324
+ if (imageItems.length === 0) return;
325
+
326
+ e.preventDefault(); // Prevent default paste behavior for images
327
+
328
+ const imagePromises: Promise<ImageUpload | null>[] = imageItems.map((item) => {
329
+ const file = item.getAsFile();
330
+ if (!file) return Promise.resolve(null);
331
+
332
+ return new Promise<ImageUpload | null>((resolve, reject) => {
333
+ const reader = new FileReader();
334
+ reader.onload = (e) => {
335
+ const base64String = (e.target?.result as string)?.split(",")[1];
336
+ if (base64String) {
337
+ resolve({
338
+ contentType: file.type,
339
+ bytes: base64String,
340
+ });
341
+ } else {
342
+ resolve(null);
343
+ }
344
+ };
345
+ reader.onerror = reject;
346
+ reader.readAsDataURL(file);
347
+ });
348
+ });
349
+
350
+ try {
351
+ const loadedImages = (await Promise.all(imagePromises)).filter((img) => img !== null);
352
+ setSelectedImages((prev) => [...prev, ...loadedImages]);
353
+ } catch (error) {
354
+ // TODO: Show an error message to the user
355
+ console.error("Error processing pasted images:", error);
356
+ }
357
+ };
358
+
359
+ document.addEventListener("paste", handlePaste);
360
+ return () => document.removeEventListener("paste", handlePaste);
361
+ }, [imageUploadsEnabled]);
284
362
 
285
363
  useEffect(() => {
286
364
  if (!additionalInstructions?.length) {
@@ -322,6 +400,17 @@ export function CopilotChat({
322
400
  onReloadMessages,
323
401
  );
324
402
 
403
+ // Wrapper for sendMessage to clear selected images
404
+ const handleSendMessage = (text: string) => {
405
+ const images = selectedImages;
406
+ setSelectedImages([]);
407
+ if (fileInputRef.current) {
408
+ fileInputRef.current.value = "";
409
+ }
410
+
411
+ return sendMessage(text, images);
412
+ };
413
+
325
414
  const chatContext = React.useContext(ChatContext);
326
415
  const isVisible = chatContext ? chatContext.open : true;
327
416
 
@@ -339,6 +428,44 @@ export function CopilotChat({
339
428
  }
340
429
  };
341
430
 
431
+ const handleImageUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
432
+ if (!event.target.files || event.target.files.length === 0) {
433
+ return;
434
+ }
435
+
436
+ const files = Array.from(event.target.files).filter((file) => file.type.startsWith("image/"));
437
+ if (files.length === 0) return;
438
+
439
+ const fileReadPromises = files.map((file) => {
440
+ return new Promise<{ contentType: string; bytes: string }>((resolve, reject) => {
441
+ const reader = new FileReader();
442
+ reader.onload = (e) => {
443
+ const base64String = (e.target?.result as string)?.split(",")[1] || "";
444
+ if (base64String) {
445
+ resolve({
446
+ contentType: file.type,
447
+ bytes: base64String,
448
+ });
449
+ }
450
+ };
451
+ reader.onerror = reject;
452
+ reader.readAsDataURL(file);
453
+ });
454
+ });
455
+
456
+ try {
457
+ const loadedImages = await Promise.all(fileReadPromises);
458
+ setSelectedImages((prev) => [...prev, ...loadedImages]);
459
+ } catch (error) {
460
+ // TODO: Show an error message to the user
461
+ console.error("Error reading files:", error);
462
+ }
463
+ };
464
+
465
+ const removeSelectedImage = (index: number) => {
466
+ setSelectedImages((prev) => prev.filter((_, i) => i !== index));
467
+ };
468
+
342
469
  return (
343
470
  <WrappedCopilotChat icons={icons} labels={labels} className={className}>
344
471
  <Messages
@@ -348,6 +475,7 @@ export function CopilotChat({
348
475
  RenderActionExecutionMessage={RenderActionExecutionMessage}
349
476
  RenderAgentStateMessage={RenderAgentStateMessage}
350
477
  RenderResultMessage={RenderResultMessage}
478
+ RenderImageMessage={RenderImageMessage}
351
479
  messages={visibleMessages}
352
480
  inProgress={isLoading}
353
481
  onRegenerate={handleRegenerate}
@@ -364,17 +492,33 @@ export function CopilotChat({
364
492
  message={suggestion.message}
365
493
  partial={suggestion.partial}
366
494
  className={suggestion.className}
367
- onClick={(message) => sendMessage(message)}
495
+ onClick={(message) => handleSendMessage(message)}
368
496
  />
369
497
  ))}
370
498
  </div>
371
499
  )}
372
500
  </Messages>
501
+
502
+ {imageUploadsEnabled && (
503
+ <>
504
+ <ImageUploadQueue images={selectedImages} onRemoveImage={removeSelectedImage} />
505
+ <input
506
+ type="file"
507
+ multiple
508
+ ref={fileInputRef}
509
+ onChange={handleImageUpload}
510
+ accept={inputFileAccept}
511
+ style={{ display: "none" }}
512
+ />
513
+ </>
514
+ )}
515
+
373
516
  <Input
374
517
  inProgress={isLoading}
375
- onSend={sendMessage}
518
+ onSend={handleSendMessage}
376
519
  isVisible={isVisible}
377
520
  onStop={stopGeneration}
521
+ onUpload={imageUploadsEnabled ? () => fileInputRef.current?.click() : undefined}
378
522
  />
379
523
  </WrappedCopilotChat>
380
524
  );
@@ -467,28 +611,64 @@ export const useCopilotChatLogic = (
467
611
  visibleMessages.length == 0,
468
612
  ]);
469
613
 
470
- const sendMessage = async (messageContent: string) => {
614
+ const sendMessage = async (
615
+ messageContent: string,
616
+ imagesToUse?: Array<{ contentType: string; bytes: string }>,
617
+ ) => {
618
+ // Use images passed in the call OR the ones from the state (passed via props)
619
+ const images = imagesToUse || [];
620
+
471
621
  abortSuggestions();
472
622
  setCurrentSuggestions([]);
473
623
 
474
- const message: Message = new TextMessage({
475
- content: messageContent,
476
- role: Role.User,
477
- });
624
+ let firstMessage: Message | null = null;
478
625
 
479
- if (onSubmitMessage) {
480
- try {
481
- await onSubmitMessage(messageContent);
482
- } catch (error) {
483
- console.error("Error in onSubmitMessage:", error);
626
+ // If there's text content, send a text message first
627
+ if (messageContent.trim().length > 0) {
628
+ const textMessage = new TextMessage({
629
+ content: messageContent,
630
+ role: Role.User,
631
+ });
632
+
633
+ if (onSubmitMessage) {
634
+ try {
635
+ // Call onSubmitMessage only with text, as image handling is internal right now
636
+ await onSubmitMessage(messageContent);
637
+ } catch (error) {
638
+ console.error("Error in onSubmitMessage:", error);
639
+ }
640
+ }
641
+
642
+ await appendMessage(textMessage, { followUp: images.length === 0 });
643
+
644
+ if (!firstMessage) {
645
+ firstMessage = textMessage;
484
646
  }
485
647
  }
486
- // this needs to happen after onSubmitMessage, because it will trigger submission
487
- // of the message to the endpoint. Some users depend on performing some actions
488
- // before the message is submitted.
489
- appendMessage(message);
490
648
 
491
- return message;
649
+ // Send image messages
650
+ if (images.length > 0) {
651
+ for (let i = 0; i < images.length; i++) {
652
+ const imageMessage = new ImageMessage({
653
+ format: images[i].contentType.replace("image/", ""),
654
+ bytes: images[i].bytes,
655
+ role: Role.User,
656
+ });
657
+ await appendMessage(imageMessage, { followUp: i === images.length - 1 });
658
+ if (!firstMessage) {
659
+ firstMessage = imageMessage;
660
+ }
661
+ }
662
+ }
663
+
664
+ if (!firstMessage) {
665
+ // Should not happen if send button is properly disabled, but handle just in case
666
+ return new TextMessage({ content: "", role: Role.User }); // Return a dummy message
667
+ }
668
+
669
+ // The hook implicitly triggers API call on appendMessage.
670
+ // We return the first message sent (either text or first image)
671
+ return firstMessage;
492
672
  };
493
673
 
494
674
  const messages = visibleMessages;
@@ -81,6 +81,12 @@ export interface CopilotChatIcons {
81
81
  */
82
82
 
83
83
  thumbsDownIcon?: React.ReactNode;
84
+
85
+ /**
86
+ * The icon to use for the upload button.
87
+ * @default <UploadIcon />
88
+ */
89
+ uploadIcon?: React.ReactNode;
84
90
  }
85
91
 
86
92
  /**
@@ -221,6 +227,7 @@ export const ChatContextProvider = ({
221
227
  copyIcon: DefaultIcons.CopyIcon,
222
228
  thumbsUpIcon: DefaultIcons.ThumbsUpIcon,
223
229
  thumbsDownIcon: DefaultIcons.ThumbsDownIcon,
230
+ uploadIcon: DefaultIcons.UploadIcon,
224
231
  },
225
232
  ...icons,
226
233
  }),
@@ -207,6 +207,20 @@ export const DownloadIcon = (
207
207
  </svg>
208
208
  );
209
209
 
210
+ export const UploadIcon = (
211
+ <svg
212
+ xmlns="http://www.w3.org/2000/svg"
213
+ fill="none"
214
+ viewBox="0 0 24 24"
215
+ strokeWidth="1.5"
216
+ stroke="currentColor"
217
+ width="24"
218
+ height="24"
219
+ >
220
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
221
+ </svg>
222
+ );
223
+
210
224
  export const CheckIcon = (
211
225
  <svg
212
226
  xmlns="http://www.w3.org/2000/svg"
@@ -0,0 +1,77 @@
1
+ import React from "react";
2
+
3
+ interface ImageUploadQueueProps {
4
+ images: Array<{ contentType: string; bytes: string }>;
5
+ onRemoveImage: (index: number) => void;
6
+ className?: string;
7
+ }
8
+
9
+ export const ImageUploadQueue: React.FC<ImageUploadQueueProps> = ({
10
+ images,
11
+ onRemoveImage,
12
+ className = "",
13
+ }) => {
14
+ if (images.length === 0) return null;
15
+
16
+ return (
17
+ <div
18
+ className={`copilotKitImageUploadQueue ${className}`}
19
+ style={{
20
+ display: "flex",
21
+ flexWrap: "wrap",
22
+ gap: "8px",
23
+ margin: "8px",
24
+ padding: "8px",
25
+ }}
26
+ >
27
+ {images.map((image, index) => (
28
+ <div
29
+ key={index}
30
+ className="copilotKitImageUploadQueueItem"
31
+ style={{
32
+ position: "relative",
33
+ display: "inline-block",
34
+ width: "60px",
35
+ height: "60px",
36
+ borderRadius: "4px",
37
+ overflow: "hidden",
38
+ }}
39
+ >
40
+ {/* eslint-disable-next-line @next/next/no-img-element */}
41
+ <img
42
+ src={`data:${image.contentType};base64,${image.bytes}`}
43
+ alt={`Selected image ${index + 1}`}
44
+ style={{
45
+ width: "100%",
46
+ height: "100%",
47
+ objectFit: "cover",
48
+ }}
49
+ />
50
+ <button
51
+ onClick={() => onRemoveImage(index)}
52
+ className="copilotKitImageUploadQueueRemoveButton"
53
+ style={{
54
+ position: "absolute",
55
+ top: "2px",
56
+ right: "2px",
57
+ background: "rgba(0,0,0,0.6)",
58
+ color: "white",
59
+ border: "none",
60
+ borderRadius: "50%",
61
+ width: "18px",
62
+ height: "18px",
63
+ display: "flex",
64
+ alignItems: "center",
65
+ justifyContent: "center",
66
+ cursor: "pointer",
67
+ fontSize: "10px",
68
+ padding: 0,
69
+ }}
70
+ >
71
+
72
+ </button>
73
+ </div>
74
+ ))}
75
+ </div>
76
+ );
77
+ };
@@ -5,7 +5,7 @@ import AutoResizingTextarea from "./Textarea";
5
5
  import { usePushToTalk } from "../../hooks/use-push-to-talk";
6
6
  import { useCopilotContext } from "@copilotkit/react-core";
7
7
 
8
- export const Input = ({ inProgress, onSend, isVisible = false, onStop }: InputProps) => {
8
+ export const Input = ({ inProgress, onSend, isVisible = false, onStop, onUpload }: InputProps) => {
9
9
  const context = useChatContext();
10
10
  const copilotContext = useCopilotContext();
11
11
 
@@ -89,7 +89,14 @@ export const Input = ({ inProgress, onSend, isVisible = false, onStop }: InputPr
89
89
  }}
90
90
  />
91
91
  <div className="copilotKitInputControls">
92
+ {onUpload && (
93
+ <button onClick={onUpload} className="copilotKitInputControlButton">
94
+ {context.icons.uploadIcon}
95
+ </button>
96
+ )}
97
+
92
98
  <div style={{ flexGrow: 1 }} />
99
+
93
100
  {showPushToTalk && (
94
101
  <button
95
102
  onClick={() =>
@@ -12,6 +12,7 @@ export const Messages = ({
12
12
  RenderActionExecutionMessage,
13
13
  RenderAgentStateMessage,
14
14
  RenderResultMessage,
15
+ RenderImageMessage,
15
16
  AssistantMessage,
16
17
  UserMessage,
17
18
  onRegenerate,
@@ -105,6 +106,22 @@ export const Messages = ({
105
106
  UserMessage={UserMessage}
106
107
  />
107
108
  );
109
+ } else if (message.isImageMessage && message.isImageMessage()) {
110
+ return (
111
+ <RenderImageMessage
112
+ key={index}
113
+ message={message}
114
+ inProgress={inProgress}
115
+ index={index}
116
+ isCurrentMessage={isCurrentMessage}
117
+ AssistantMessage={AssistantMessage}
118
+ UserMessage={UserMessage}
119
+ onRegenerate={onRegenerate}
120
+ onCopy={onCopy}
121
+ onThumbsUp={onThumbsUp}
122
+ onThumbsDown={onThumbsDown}
123
+ />
124
+ );
108
125
  }
109
126
  })}
110
127
  {interrupt}
@@ -6,3 +6,4 @@ export { Markdown } from "./Markdown";
6
6
  export { AssistantMessage } from "./messages/AssistantMessage";
7
7
  export { UserMessage } from "./messages/UserMessage";
8
8
  export { useChatContext } from "./ChatContext";
9
+ export { RenderImageMessage } from "./messages/RenderImageMessage";
@@ -0,0 +1,64 @@
1
+ import { RenderMessageProps } from "../props";
2
+ import { UserMessage as DefaultUserMessage } from "./UserMessage";
3
+ import { AssistantMessage as DefaultAssistantMessage } from "./AssistantMessage";
4
+
5
+ export function RenderImageMessage({
6
+ UserMessage = DefaultUserMessage,
7
+ AssistantMessage = DefaultAssistantMessage,
8
+ ...props
9
+ }: RenderMessageProps) {
10
+ const {
11
+ message,
12
+ inProgress,
13
+ index,
14
+ isCurrentMessage,
15
+ onRegenerate,
16
+ onCopy,
17
+ onThumbsUp,
18
+ onThumbsDown,
19
+ } = props;
20
+
21
+ if (message.isImageMessage()) {
22
+ const imageData = `data:${message.format};base64,${message.bytes}`;
23
+ const imageComponent = (
24
+ <div className="copilotKitImage">
25
+ <img
26
+ src={imageData}
27
+ alt="User uploaded image"
28
+ style={{ maxWidth: "100%", maxHeight: "300px", borderRadius: "8px" }}
29
+ />
30
+ </div>
31
+ );
32
+
33
+ if (message.role === "user") {
34
+ return (
35
+ <UserMessage
36
+ key={index}
37
+ data-message-role="user"
38
+ message=""
39
+ rawData={message}
40
+ subComponent={imageComponent}
41
+ />
42
+ );
43
+ } else if (message.role === "assistant") {
44
+ return (
45
+ <AssistantMessage
46
+ key={index}
47
+ data-message-role="assistant"
48
+ message=""
49
+ rawData={message}
50
+ subComponent={imageComponent}
51
+ isLoading={inProgress && isCurrentMessage && !message.bytes}
52
+ isGenerating={inProgress && isCurrentMessage && !!message.bytes}
53
+ isCurrentMessage={isCurrentMessage}
54
+ onRegenerate={() => onRegenerate?.(message.id)}
55
+ onCopy={onCopy}
56
+ onThumbsUp={onThumbsUp}
57
+ onThumbsDown={onThumbsDown}
58
+ />
59
+ );
60
+ }
61
+ }
62
+
63
+ return null;
64
+ }