Skip to content

feat(react): add file attachments support to F0AiChat#3671

Open
siguenzaraul wants to merge 1 commit intomainfrom
feat/ai-chat-file-attachments
Open

feat(react): add file attachments support to F0AiChat#3671
siguenzaraul wants to merge 1 commit intomainfrom
feat/ai-chat-file-attachments

Conversation

@siguenzaraul
Copy link
Copy Markdown
Contributor

@siguenzaraul siguenzaraul commented Mar 15, 2026

Summary

  • Add file attachment support to F0AiChat and F0AiChatTextArea. Consumers configure uploads via a new fileAttachments prop on F0AiChatProvider with onUploadFiles, allowedMimeTypes, and maxFiles options.
  • Files are displayed above the textarea with a skeleton loading state during upload and a remove action once uploaded. An attach button (Paperclip icon) appears next to the tool hint selector when onUploadFiles is provided.
  • Uploaded file metadata flows through as CopilotKit multipart BinaryInputContent parts in the message content array (via onSendWithFilessendMessage with content: InputContent[]). In chat history, UserMessage extracts binary parts and renders them as FileItem chips above the user's text bubble — no XML injection or string manipulation needed.
  • Add a lg size variant to FileItem using CVA, used in both the textarea preview and the message history.
  • FileItem prop file widened from File to File | FileDef so chat history can render file chips from metadata without constructing dummy File objects.

Architecture

User attaches files → F0AiChatTextArea (upload via onUploadFiles)
  → onSendWithFiles(text, UploadedFile[])
    → ChatTextarea builds InputContent[] with text + BinaryInputContent parts
      → CopilotKit sendMessage({ content: InputContent[] })
        → AG-UI protocol → Mastra agent → LLM

On the read side:

CopilotKit message.content (InputContent[])
  → UserMessage.getTextContent() extracts text parts
  → UserMessage.getBinaryParts() extracts binary parts
  → FileItem renders each binary part as a chip

New public types

Type Description
UploadedFile Metadata returned by the upload function (url, filename, mimetype, type)
AiChatFileAttachmentConfig Config object: onUploadFiles, allowedMimeTypes?, maxFiles?
AttachedFile Internal state for each file (id, file, status, uploadedFile?)

Files changed

File Change
F0AiChat/types.ts New types + fileAttachments prop + i18n keys
F0AiChat/internal-types.ts fileAttachments in state & provider return
F0AiChat/providers/AiChatStateProvider.tsx Passthrough in context
F0AiChat/F0AiChat.tsx Passthrough to provider
F0AiChat/index.ts Export new types
F0AiChat/components/ChatTextarea.tsx Bridge fileAttachments to textarea; build multipart InputContent[] with BinaryInputContent parts
F0AiChat/components/UserMessage.tsx Extract binary parts from InputContent[] and render as FileItem chips (plain FileDef objects, no dummy File construction)
F0AiChatTextArea/types.ts AttachedFile type + new props; improved JSDoc for onSend/onSendWithFiles
F0AiChatTextArea/F0AiChatTextArea.tsx Attach button, upload logic, preview, MIME validation (filterByMimeType), onSendWithFiles routing
F0AiChatTextArea/index.ts Export AttachedFile
RichText/FileItem/index.tsx Add lg size variant with CVA; widen file prop to File | FileDef
i18n-provider-defaults.ts Default translations (attachFile, removeFile)
ApplicationFrame/index.stories.tsx Mock onUploadFiles in stories

Tests (24 total, all passing)

F0AiChatTextArea.test.tsx (15 tests):

  • Attach button visibility and maxFiles disable
  • Stuck-state fix (server returns fewer items → error, not stuck uploading)
  • Upload failure error handling
  • aria-busy / aria-live during upload
  • onSendWithFiles routing (with files, without files, fallback to onSend)
  • File removal
  • MIME type validation (reject, wildcard match, exact match, mixed filter, no restriction)

UserMessage.test.tsx (9 tests):

  • Plain text, multipart text, tool-context stripping
  • Entity references
  • Binary content parts (single, multiple, missing filename fallback, string-only content)
  • Image messages

Test plan

  • pnpm vitest run passes (24 tests green)
  • pnpm lint passes (0 warnings, 0 errors)
  • pnpm tsc — only pre-existing errors + expected CopilotKit version mismatch at ChatTextarea.tsx:65 (resolves when CopilotKit 1.54.0 PR fix(ai): add CopilotKit 1.54.0 devDeps and fix type compatibility #3707 merges)
  • Storybook: attach button only appears when onUploadFiles is configured
  • Without fileAttachments, component behaves exactly as before
  • Send button is disabled while files are uploading
  • Send button requires text input (files alone are not enough)
  • Files show skeleton while uploading, then FileItem with remove action
  • Uploaded files appear as FileItem chips above user message in history
  • maxFiles disables attach button and truncates selection when limit is reached
  • MIME type validation filters disallowed file types client-side

Dependencies

@siguenzaraul siguenzaraul requested a review from a team as a code owner March 15, 2026 23:55
Copilot AI review requested due to automatic review settings March 15, 2026 23:55
@github-actions github-actions bot added the react Changes affect packages/react label Mar 15, 2026
@github-actions github-actions bot added the feat label Mar 15, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 15, 2026

✅ No New Circular Dependencies

No new circular dependencies detected. Current count: 0

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 15, 2026

🔍 Visual review for your branch is published 🔍

Here are the links to:

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 fileAttachments config on F0AiChatProvider.
  • Implement attach button + upload/preview UX in F0AiChatTextArea, including message prefix injection.
  • Parse/strip <file-attachments> in UserMessage and extend FileItem with a new lg size 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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 16, 2026

Coverage Report for packages/react

Status Category Percentage Covered / Total
🔵 Lines 45.35% 11007 / 24271
🔵 Statements 44.61% 11348 / 25437
🔵 Functions 37.28% 2488 / 6673
🔵 Branches 37.12% 7199 / 19392
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/react/src/experimental/RichText/FileItem/index.tsx 54.54% 0% 0% 60% 60-97
packages/react/src/lib/providers/i18n/i18n-provider-defaults.ts 100% 100% 100% 100%
packages/react/src/sds/ai/F0AiChat/F0AiChat.tsx 33.33% 0% 0% 33.33% 42-65, 73-79, 84-113
packages/react/src/sds/ai/F0AiChat/index.ts 100% 100% 100% 100%
packages/react/src/sds/ai/F0AiChat/internal-types.ts 25% 100% 25% 33.33% 203-212
packages/react/src/sds/ai/F0AiChat/types.ts 100% 100% 100% 100%
packages/react/src/sds/ai/F0AiChat/components/input/ChatTextarea.tsx 0.74% 0% 0% 0.87% 59-82, 103-588
packages/react/src/sds/ai/F0AiChat/components/messages/UserMessage.tsx 6.25% 0% 0% 6.66% 31-70, 82-116
packages/react/src/sds/ai/F0AiChat/providers/AiChatStateProvider.tsx 4.95% 0% 0% 5.1% 37-45, 70-312, 319-377
packages/react/src/sds/ai/F0AiChat/utils/fetchThreadMessages.ts 81.39% 72.5% 40% 83.33% 119-125, 198-211
Generated in workflow #12471 for commit 6a25445 by the Vitest Coverage Report Action

Copy link
Copy Markdown

@YaraDebian YaraDebian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Upgrade CopilotKit to get BinaryInputContent support
  2. Build the frontend upload flow: Active Storage upload + resolveOpenaiChatFile to get the URL
  3. Wire sendMessage with multipart content (text + binary parts) to pass the file to the agent
  4. 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
}

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 23, 2026

📦 Alpha Package Version Published

Use pnpm i github:factorialco/f0#npm/alpha-pr-3671 to install the package

Use pnpm i github:factorialco/f0#2816c7ff0a5b8da924e129c700c966cba68bfa71 to install this specific commit

Copilot AI review requested due to automatic review settings March 23, 2026 15:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 with binary content parts.
  • Extend FileItem with an lg variant 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.

Copilot AI review requested due to automatic review settings March 23, 2026 15:58
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.

Copilot AI review requested due to automatic review settings March 24, 2026 08:23
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

@YaraDebian YaraDebian force-pushed the feat/ai-chat-file-attachments branch from b5c2c1d to 6b8786e Compare March 24, 2026 10:41
@DavidRodriguezHidalgo
Copy link
Copy Markdown
Contributor

question @siguenzaraul : Is this prepared to support multiple chat files?

Copilot AI review requested due to automatic review settings March 30, 2026 11:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

@YaraDebian YaraDebian force-pushed the feat/ai-chat-file-attachments branch from d8a8880 to 56cc88d Compare April 1, 2026 10:27
Copilot AI review requested due to automatic review settings April 1, 2026 10:28
@YaraDebian YaraDebian force-pushed the feat/ai-chat-file-attachments branch from 56cc88d to a1cd675 Compare April 1, 2026 10:28
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Comment on lines +126 to +133
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
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 1, 2026 15:06
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 9 comments.

Comment on lines +95 to 99
} else if (part.type === "file") {
if (part.file) {
fileParts.push(part.file)
}
} else {
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 1, 2026 15:29
@YaraDebian YaraDebian force-pushed the feat/ai-chat-file-attachments branch from e2bf915 to 7a69a46 Compare April 1, 2026 15:29
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Comment on lines +301 to +305
mimeType: "application/pdf",
},
],
},
} as any)
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +360 to +366
{attachedFiles.map((att) =>
att.status === "uploading" ? (
<Skeleton key={att.id} className="h-9 w-36 rounded-lg" />
) : (
<FileItem
key={att.id}
file={att.file}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 2, 2026 07:20
@YaraDebian YaraDebian force-pushed the feat/ai-chat-file-attachments branch from f2974c8 to 6a25445 Compare April 2, 2026 07:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

]

sendMessage({
id: crypto.randomUUID(),
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
id: crypto.randomUUID(),
id: randomId(),

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat react Changes affect packages/react

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants