feat(react): add file attachments support to F0AiChat#3671
feat(react): add file attachments support to F0AiChat#3671siguenzaraul wants to merge 1 commit intomainfrom
Conversation
✅ No New Circular DependenciesNo new circular dependencies detected. Current count: 0 |
🔍 Visual review for your branch is published 🔍Here are the links to: |
There was a problem hiding this comment.
Pull request overview
Adds configurable file-attachment uploads to the F0AiChat ecosystem by wiring an upload config through the provider, adding attachment UI + upload state handling in F0AiChatTextArea, and injecting/parsing an invisible <file-attachments> prefix so uploaded files can be displayed in the chat history.
Changes:
- Introduce public file upload types and a new
fileAttachmentsconfig onF0AiChatProvider. - Implement attach button + upload/preview UX in
F0AiChatTextArea, including message prefix injection. - Parse/strip
<file-attachments>inUserMessageand extendFileItemwith a newlgsize variant; add i18n defaults + Storybook mock.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/react/src/sds/ai/F0AiChatTextArea/types.ts | Adds AttachedFile state type and new upload-related props. |
| packages/react/src/sds/ai/F0AiChatTextArea/index.ts | Re-exports AttachedFile type. |
| packages/react/src/sds/ai/F0AiChatTextArea/F0AiChatTextArea.tsx | Implements attach button, upload flow, preview chips/skeletons, and <file-attachments> message prefix injection. |
| packages/react/src/sds/ai/F0AiChat/types.ts | Adds UploadedFile, AiChatFileAttachmentConfig, and provider fileAttachments prop + i18n keys. |
| packages/react/src/sds/ai/F0AiChat/providers/AiChatStateProvider.tsx | Passes fileAttachments through chat context and default hook value. |
| packages/react/src/sds/ai/F0AiChat/internal-types.ts | Extends internal state/return types with fileAttachments. |
| packages/react/src/sds/ai/F0AiChat/index.ts | Exports new public types. |
| packages/react/src/sds/ai/F0AiChat/components/ChatTextarea.tsx | Bridges provider fileAttachments config into F0AiChatTextArea props. |
| packages/react/src/sds/ai/F0AiChat/F0AiChat.tsx | Passes fileAttachments into AiChatStateProvider. |
| packages/react/src/sds/ai/F0AiChat/components/UserMessage.tsx | Parses/strips <file-attachments> and renders attached file chips above the user bubble. |
| packages/react/src/lib/providers/i18n/i18n-provider-defaults.ts | Adds default translations for attach/remove file strings. |
| packages/react/src/experimental/RichText/FileItem/index.tsx | Adds lg size variant (CVA) and exports FileItemSize. |
| packages/react/src/examples/ApplicationFrame/index.stories.tsx | Adds a Storybook mock onUploadFiles and wires fileAttachments in examples. |
You can also share your feedback on Copilot code review. Take the survey.
packages/react/src/sds/ai/F0AiChatTextArea/F0AiChatTextArea.tsx
Outdated
Show resolved
Hide resolved
packages/react/src/sds/ai/F0AiChatTextArea/F0AiChatTextArea.tsx
Outdated
Show resolved
Hide resolved
packages/react/src/sds/ai/F0AiChatTextArea/F0AiChatTextArea.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
After checking with Oriol and asking Ck people, we can try to pass the file differently through sendMessage instead of the XML hack. But before that we need to update CopilotKit to support the AG-UI format for binary type here the PR for it.
Here's the approach on the backend to handle the file upload to Active storage.
The global steps for the frontend:
- Upgrade CopilotKit to get BinaryInputContent support
- Build the frontend upload flow: Active Storage upload + resolveOpenaiChatFile to get the URL
- Wire sendMessage with multipart content (text + binary parts) to pass the file to the agent
- Render file chips on user messages by reading the content array
Here are some changes to explore in the PR:
Change onSend signature to include files:
// Current
onSend: (text: string) => void
// New
onSend: (text: string, files?: UploadedFile[]) => void
In handleSubmit:
const uploaded = uploadedFiles.map((f) => f.uploadedFile).filter(Boolean)
onSend(withToolHint, uploaded.length > 0 ? uploaded : undefined)
The consumer (Factorial monorepo) will use sendMessage to build the multipart
content:
sendMessage({
id: generated(),
role: "user",
content: [
{ type: "text", text: userText },
...files.map((f) => ({
type: "binary" as const,
mimeType: f.mimetype,
url: f.url,
filename: f.filename,
})),
],
})
Change UserMessage.tsx to render files from content array
After the CopilotKit upgrade, message.content will be string | InputContent[]. Replace the XML parsing with:
const content = message?.content
if (Array.isArray(content)) {
const textParts = content.filter((p) => p.type === "text")
const binaryParts = content.filter((p) => p.type === "binary")
// Render binaryParts as <FileItem> chips
// Render textParts as <UserMessageContent>
} else {
// Plain string — render as before
}
📦 Alpha Package Version PublishedUse Use |
There was a problem hiding this comment.
Pull request overview
Adds first-class file attachment support to the React AI chat experience (F0AiChat / F0AiChatTextArea) by introducing upload configuration in the provider, wiring it through the chat textarea, and rendering uploaded files in both the composer and message history.
Changes:
- Introduce new public file upload types/config (
UploadedFile,AiChatFileAttachmentConfig) and provider prop passthrough (fileAttachments). - Implement upload + preview/remove UI in
F0AiChatTextArea, and send multipart CopilotKit messages withbinarycontent parts. - Extend
FileItemwith anlgvariant and use it for attachment chips in composer/history; add i18n keys and Storybook mocks.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/react/src/sds/ai/F0AiChatTextArea/types.ts | Adds attachment-related props/types, incl. onUploadFiles + onSendWithFiles. |
| packages/react/src/sds/ai/F0AiChatTextArea/index.ts | Exports AttachedFile type. |
| packages/react/src/sds/ai/F0AiChatTextArea/F0AiChatTextArea.tsx | Implements attach button, upload flow, preview chips, and send gating while uploading. |
| packages/react/src/sds/ai/F0AiChat/types.ts | Adds new public types + provider fileAttachments prop + new translation keys. |
| packages/react/src/sds/ai/F0AiChat/internal-types.ts | Threads fileAttachments through internal state/return types. |
| packages/react/src/sds/ai/F0AiChat/providers/AiChatStateProvider.tsx | Exposes fileAttachments via context (and fallback return). |
| packages/react/src/sds/ai/F0AiChat/F0AiChat.tsx | Passes fileAttachments into the state provider. |
| packages/react/src/sds/ai/F0AiChat/index.ts | Re-exports new file attachment types. |
| packages/react/src/sds/ai/F0AiChat/components/ChatTextarea.tsx | Bridges uploads to F0AiChatTextArea and sends multipart messages with binary parts. |
| packages/react/src/sds/ai/F0AiChat/components/UserMessage.tsx | Extracts and renders binary attachments as FileItem chips above user messages. |
| packages/react/src/lib/providers/i18n/i18n-provider-defaults.ts | Adds default strings for attach/remove. |
| packages/react/src/experimental/RichText/FileItem/index.tsx | Adds lg size variant using CVA and exports FileItemSize. |
| packages/react/src/examples/ApplicationFrame/index.stories.tsx | Adds Storybook mock onUploadFiles and enables attachments in examples. |
packages/react/src/sds/ai/F0AiChatTextArea/F0AiChatTextArea.tsx
Outdated
Show resolved
Hide resolved
packages/react/src/sds/ai/F0AiChatTextArea/F0AiChatTextArea.tsx
Outdated
Show resolved
Hide resolved
packages/react/src/sds/ai/F0AiChat/components/messages/__tests__/UserMessage.test.tsx
Outdated
Show resolved
Hide resolved
b5c2c1d to
6b8786e
Compare
|
question @siguenzaraul : Is this prepared to support multiple chat files? |
d8a8880 to
56cc88d
Compare
56cc88d to
a1cd675
Compare
| async (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| let files = Array.from(e.target.files ?? []) | ||
| if (files.length === 0 || !onUploadFiles) return | ||
|
|
||
| // Validate MIME types client-side (the <input accept> attribute is not | ||
| // reliable — users can bypass it via drag-and-drop or file picker). | ||
| files = filterByMimeType(files, allowedMimeTypes) | ||
| if (files.length === 0) return |
There was a problem hiding this comment.
handleFileSelect returns early in several cases (e.g. no allowed MIME types after filtering), but the file input value is only cleared at the end of the happy-path. If a user re-selects the same file after an early return, many browsers won’t fire onChange, making it feel “stuck”. Consider clearing e.target.value before those early returns (or in a finally) so the same file can always be re-selected.
packages/react/src/sds/ai/F0AiChat/components/input/ChatTextarea.tsx
Outdated
Show resolved
Hide resolved
packages/react/src/sds/ai/F0AiChat/utils/fetchThreadMessages.ts
Outdated
Show resolved
Hide resolved
| } else if (part.type === "file") { | ||
| if (part.file) { | ||
| fileParts.push(part.file) | ||
| } | ||
| } else { |
There was a problem hiding this comment.
File parts that appear after a user text part will currently be dropped from the reconstructed message content, because the encoding is only applied when a later text part is encountered (and the fallback at the end only runs when messages.length === 0). If the backend can send file parts after text, consider emitting a separate message for file parts (so ordering is preserved) or deferring the merge until all parts are processed.
packages/react/src/sds/ai/F0AiChat/components/input/ChatTextarea.tsx
Outdated
Show resolved
Hide resolved
packages/react/src/sds/ai/F0AiChat/components/input/ChatTextarea.tsx
Outdated
Show resolved
Hide resolved
packages/react/src/sds/ai/F0AiChat/components/messages/UserMessage.tsx
Outdated
Show resolved
Hide resolved
packages/react/src/sds/ai/F0AiChat/utils/__tests__/fetchThreadMessages.test.ts
Show resolved
Hide resolved
packages/react/src/sds/ai/F0AiChat/utils/__tests__/fetchThreadMessages.test.ts
Show resolved
Hide resolved
e2bf915 to
7a69a46
Compare
| mimeType: "application/pdf", | ||
| }, | ||
| ], | ||
| }, | ||
| } as any) |
There was a problem hiding this comment.
Avoid as any here for the same reason as above; keep the test input type-safe against BackendMessage so we don’t introduce new any usage in packages/react.
| {attachedFiles.map((att) => | ||
| att.status === "uploading" ? ( | ||
| <Skeleton key={att.id} className="h-9 w-36 rounded-lg" /> | ||
| ) : ( | ||
| <FileItem | ||
| key={att.id} | ||
| file={att.file} |
There was a problem hiding this comment.
AttachedFile.status === "error" currently renders the same FileItem UI as a successfully uploaded file (anything not "uploading" falls into the same branch), but handleSubmit only includes status === "uploaded" files. This means users can see a file chip and send a message believing it will be attached when it won’t. Render an explicit error state (and/or block send until errored files are removed, or show an error label/action) so the UI matches what will be sent.
f2974c8 to
6a25445
Compare
| ] | ||
|
|
||
| sendMessage({ | ||
| id: crypto.randomUUID(), |
There was a problem hiding this comment.
This message ID is created with crypto.randomUUID(). For consistency with the provider/other calls that use randomId() (and to avoid crypto.randomUUID support issues), consider generating the message id with randomId() instead.
| id: crypto.randomUUID(), | |
| id: randomId(), |
Summary
F0AiChatandF0AiChatTextArea. Consumers configure uploads via a newfileAttachmentsprop onF0AiChatProviderwithonUploadFiles,allowedMimeTypes, andmaxFilesoptions.onUploadFilesis provided.BinaryInputContentparts in the message content array (viaonSendWithFiles→sendMessagewithcontent: InputContent[]). In chat history,UserMessageextracts binary parts and renders them asFileItemchips above the user's text bubble — no XML injection or string manipulation needed.lgsize variant toFileItemusing CVA, used in both the textarea preview and the message history.FileItempropfilewidened fromFiletoFile | FileDefso chat history can render file chips from metadata without constructing dummyFileobjects.Architecture
On the read side:
New public types
UploadedFileurl,filename,mimetype,type)AiChatFileAttachmentConfigonUploadFiles,allowedMimeTypes?,maxFiles?AttachedFileid,file,status,uploadedFile?)Files changed
F0AiChat/types.tsfileAttachmentsprop + i18n keysF0AiChat/internal-types.tsfileAttachmentsin state & provider returnF0AiChat/providers/AiChatStateProvider.tsxF0AiChat/F0AiChat.tsxF0AiChat/index.tsF0AiChat/components/ChatTextarea.tsxfileAttachmentsto textarea; build multipartInputContent[]withBinaryInputContentpartsF0AiChat/components/UserMessage.tsxInputContent[]and render asFileItemchips (plainFileDefobjects, no dummyFileconstruction)F0AiChatTextArea/types.tsAttachedFiletype + new props; improved JSDoc foronSend/onSendWithFilesF0AiChatTextArea/F0AiChatTextArea.tsxfilterByMimeType),onSendWithFilesroutingF0AiChatTextArea/index.tsAttachedFileRichText/FileItem/index.tsxlgsize variant with CVA; widenfileprop toFile | FileDefi18n-provider-defaults.tsattachFile,removeFile)ApplicationFrame/index.stories.tsxonUploadFilesin storiesTests (24 total, all passing)
F0AiChatTextArea.test.tsx(15 tests):maxFilesdisablearia-busy/aria-liveduring uploadonSendWithFilesrouting (with files, without files, fallback toonSend)UserMessage.test.tsx(9 tests):Test plan
pnpm vitest runpasses (24 tests green)pnpm lintpasses (0 warnings, 0 errors)pnpm tsc— only pre-existing errors + expected CopilotKit version mismatch atChatTextarea.tsx:65(resolves when CopilotKit 1.54.0 PR fix(ai): add CopilotKit 1.54.0 devDeps and fix type compatibility #3707 merges)onUploadFilesis configuredfileAttachments, component behaves exactly as beforemaxFilesdisables attach button and truncates selection when limit is reachedDependencies
@ag-ui/core@0.0.47withBinaryInputContentsupport. The TS error atChatTextarea.tsx:65(multipartInputContent[]not assignable tostring) will resolve once that PR merges.