Skip to content

Conversation

@Ericzhou-ez
Copy link
Collaborator

File Upload Implementation

Overview

This PR implements a comprehensive file upload system for the Witely chat application, enabling users to attach and process multiple file types (images, PDFs, and text files) with intelligent model-based compatibility checking and robust security measures.

🎯 Key Features

File Type Support

  • Images: JPEG, PNG, HEIC (vision-capable models only)
  • Documents: PDF (models with PDF understanding capability)
  • Text Files: TXT, CSV, Markdown (supported by all models)

Model-Based Compatibility

  • Automatic validation of file types against selected model capabilities
  • Real-time compatibility checking when switching models
  • User-friendly error messages for incompatible files
  • Smart model filtering based on attached files

Drag & Drop Interface

  • Intuitive drag-and-drop file attachment
  • Visual feedback during drag operations
  • Multiple file selection support
  • File preview with icons based on type

Security & Validation

  • ✅ Session-based authentication for all uploads
  • ✅ Filename sanitization to prevent path traversal attacks
  • ✅ User-isolated storage with timestamp-based organization
  • ✅ File size limits:
    • Images: 5MB max
    • PDFs: 10MB max
    • Text files: 0.5MB max
    • Total attachments: 50MB per message
  • ✅ Maximum 8 attachments per message
  • ✅ MIME type validation on client and server

📊 Changes Summary

26 files changed, 1569 insertions(+), 272 deletions(-)

New Files

  • lib/ai/file-compatibility.ts - Model compatibility checking utilities
  • lib/ai/file-upload.ts - Shared file validation and upload logic
  • lib/ai/FILE_UPLOAD_GUIDE.md - Comprehensive implementation documentation
  • components/attachment-loader.tsx - Loading state for file attachments
  • components/drag-drop-wrapper.tsx - Drag-and-drop functionality
  • components/file-drop-overlay.tsx - Visual feedback during drag operations
  • public/icons/*.svg - File type icons (CSV, PDF, TXT, folder, uploaded-file)

Modified Files

  • app/(chat)/api/chat/route.ts - Enhanced to process file attachments
  • app/(chat)/api/files/upload/route.ts - Improved security and validation
  • components/chat.tsx - Integrated file upload functionality
  • components/multimodal-input.tsx - Added file selection UI
  • components/message.tsx - Enhanced attachment display
  • components/messages.tsx - Better attachment handling
  • components/model-selector.tsx - Model compatibility filtering
  • components/preview-attachment.tsx - Enhanced preview with type-specific icons
  • And more...

🏗️ Architecture

File Upload Flow

1. User Selection/Drop
   ↓
2. Client-Side Validation
   - Check file type compatibility with selected model
   - Validate file sizes
   - Check attachment count limits
   ↓
3. Upload to Vercel Blob
   - Secure, authenticated upload
   - User-isolated storage
   - Filename sanitization
   ↓
4. Attach to Message
   ↓
5. Server Processing
   - Images: Passed directly to model
   - PDFs: Passed to model (if supported)
   - Text files: Content fetched and appended to message

Type Safety

All file operations use strict TypeScript types:

type MediaType =
   | "image/jpeg"
   | "image/png"
   | "image/heic"
   | "application/pdf"
   | "text/plain"
   | "text/csv"
   | "text/markdown";

type FileAttachment = {
   name: string;
   url: string;
   mediaType: MediaType;
};

🔒 Security Measures

  1. Authentication: All upload endpoints require valid session
  2. Sanitization: Filenames are sanitized using sanitize-filename library
  3. Isolation: Files stored in user-specific directories with timestamps
  4. Validation:
    • File type validation on client and server
    • MIME type checking
    • Size limit enforcement
    • Maximum attachment count
  5. Error Handling: No internal errors exposed to users

🎨 UI/UX Improvements

  • Visual drag-and-drop overlay with clear feedback
  • File type icons for better recognition (PDF, CSV, TXT, images)
  • Loading states during upload
  • Clear error messages for incompatible files
  • Attachment persistence when switching compatible models
  • Automatic cleanup UI for attachment management

🧪 Testing Considerations

  • File type validation for all supported formats
  • Size limit enforcement
  • Model compatibility checking
  • Drag-and-drop functionality
  • Error handling for various failure scenarios
  • Authentication and authorization

📝 Documentation

Comprehensive implementation guide included in lib/ai/FILE_UPLOAD_GUIDE.md covering:

  • File type support details
  • Security measures
  • Type safety patterns
  • Error handling strategies
  • Architecture overview
  • Best practices
  • Known limitations
  • Future improvements

🚀 Performance

  • Parallel file uploads using Promise.all
  • Efficient client-side validation before upload
  • 30-second timeout for text file fetching
  • Content size limits to prevent context overflow

🐛 Bug Fixes

  • Fixed attachment persistence when changing models
  • Improved type safety in file handling
  • Enhanced error handling throughout the upload pipeline

📋 Commits

eb185ab fix: UI in artifact
6d40823 improv: type-safety, file-sanitization&validation, error handling
c4b1e2a fix: attatchment persistence upon model change
1543e2b feat: pdf/csv/txt/jpeg/png/heic model-permission based upload

⚠️ Known Limitations

  1. Text files fetched from URLs have a 30-second timeout
  2. Text file content limited to 0.5MB to prevent context overflow
  3. Files stored permanently on Vercel Blob (cleanup strategy needed)
  4. Maximum 8 attachments per message

🔮 Future Improvements

  • Automatic file cleanup for deleted chats
  • Progress indicators for large file uploads
  • Batch file processing optimization
  • File preview before upload
  • Image compression for large files
  • Support for additional file types (e.g., DOCX, XLSX)
  • Drag-and-drop file reordering

✅ Checklist

  • Type safety implemented throughout
  • Security measures in place
  • Client and server validation
  • Error handling with user feedback
  • Documentation completed
  • UI/UX polished
  • Code follows project conventions
  • No linter errors

🔗 Related Issues

Ready for review

@gemini-code-assist
Copy link

Summary of Changes

Hello @Ericzhou-ez, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers a complete file upload solution for the Witely chat application, significantly enhancing user interaction by allowing them to attach diverse media and document types. The system is designed with a strong emphasis on security, ensuring all uploads are authenticated, sanitized, and stored securely. It also provides a seamless user experience through an intuitive drag-and-drop interface and intelligent model compatibility features, which guide users in selecting appropriate AI models for their attached content.

Highlights

  • Comprehensive File Upload System: Introduced a robust file upload system supporting images (JPEG, PNG, HEIC), PDFs, and various text files (TXT, CSV, Markdown) with intelligent model-based compatibility checking.
  • Enhanced Security and Validation: Implemented session-based authentication, filename sanitization, user-isolated storage, and strict client-side and server-side validation for file types, sizes, and attachment limits (max 8 files, 50MB total).
  • Intuitive Drag & Drop Interface: Added a user-friendly drag-and-drop interface with visual feedback, multiple file selection, and file previews featuring type-specific icons.
  • Dynamic Model Compatibility: Models are now automatically validated against attached file types, with real-time compatibility checking when switching models and user-friendly error messages for incompatible selections.
  • Server-Side File Processing: Text files are now fetched and appended to messages, while images are converted to a Gateway AI format, and PDFs are passed directly to supported models.
  • Chat Cleanup and Persistence: Implemented logic to delete associated files from Vercel Blob when a chat is deleted, and fixed attachment persistence issues when changing models.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is an impressive pull request that adds a comprehensive file upload system. The code is well-structured, with new logic for file handling, validation, and compatibility checking correctly separated into dedicated utility files. The UI changes, including the drag-and-drop functionality and model compatibility feedback, significantly improve the user experience.

I've identified a few areas for improvement, including a critical issue in components/message.tsx that prevents component updates, and some high-severity issues related to type safety and security. I've also left some medium-severity comments on maintainability and documentation consistency. Overall, great work on this complex feature.

}

return false;
return true;

Choose a reason for hiding this comment

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

critical

The comparison function for React.memo always returns true, which will prevent the PreviewMessage component from ever re-rendering, even when its props change. This is a critical bug that will cause the UI to become stale and not reflect new data.

You should either remove the custom comparison function to default to a shallow comparison, or implement a proper deep comparison. Since fast-deep-equal is already imported in this file, you can use it for a correct deep comparison.

Suggested change
return true;
return equal(prevProps, nextProps);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

no this fix would prevent rerenders during streaming


// Check if content is too large (max 0.5MB for text files)
const maxSize = 0.5 * 1024 * 1024;
if (content.length > maxSize) {

Choose a reason for hiding this comment

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

high

Using content.length to check the file size is not reliable for strings containing multi-byte characters (e.g., emojis, non-ASCII characters). This could lead to incorrect size validation. To get the accurate byte length of a UTF-8 string, you should use new TextEncoder().encode(content).length.

Suggested change
if (content.length > maxSize) {
if (new TextEncoder().encode(content).length > maxSize) {

}

// Sanitize filename to prevent path traversal
const sanitizedFilename = filename.replace(/[^a-zA-Z0-9._-]/g, "_");

Choose a reason for hiding this comment

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

high

The PR description mentions using the sanitize-filename library for sanitization, but the implementation uses a simple regex replacement. While this provides basic protection, a dedicated library is more robust as it handles many more edge cases, invalid characters across different operating systems, and other security considerations. I recommend using the sanitize-filename library as intended to improve security and robustness.

You would need to add import sanitize from "sanitize-filename"; at the top of the file.

Suggested change
const sanitizedFilename = filename.replace(/[^a-zA-Z0-9._-]/g, "_");
const sanitizedFilename = sanitize(filename);

Comment on lines 139 to 142
key={
fileAttachment.url ??
`attachment-${Math.random()}`
}

Choose a reason for hiding this comment

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

high

Using Math.random() as a fallback for a React key is an anti-pattern. Keys must be stable across renders for React to correctly identify and update elements. A random key will cause the component to be re-mounted on every render, which can lead to performance degradation and state-related bugs.

Since you are mapping over attachmentsFromMessage, you can get the attachmentIndex and use it to create a stable key.

Suggested change
key={
fileAttachment.url ??
`attachment-${Math.random()}`
}
key={
fileAttachment.url ??
`attachment-${index}-${attachmentIndex}`
}

Comment on lines 138 to 141
.filter(
(p: any): p is { type: "file"; url: string; name: string } =>
p.type === "file"
);

Choose a reason for hiding this comment

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

high

The type guard (p: any): p is ... is unsafe. The p parameter comes from a jsonb field and could be anything (e.g., null, a string, a number), not just an object. Accessing p.type directly could throw a runtime error if p is not an object. You should add checks to ensure p is a non-null object before accessing its properties.

Suggested change
.filter(
(p: any): p is { type: "file"; url: string; name: string } =>
p.type === "file"
);
.filter(
(p: any): p is { type: "file"; url: string; name: string } =>
typeof p === "object" && p !== null && p.type === "file"
);

const content = await response.text();

// Check if content is too large (max 0.5MB for text files)
const maxSize = 0.5 * 1024 * 1024;

Choose a reason for hiding this comment

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

medium

The max size for text files (0.5 * 1024 * 1024) is hardcoded here. It would be better to define this as a named constant, for example MAX_TEXT_FILE_SIZE_BYTES, at the top of the file or in a shared constants file. This improves maintainability and makes the code easier to understand.

Comment on lines 112 to 115
attachments={attachments.map((att: any) => ({
name: att.name,
mediaType: att.mediaType,
}))}

Choose a reason for hiding this comment

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

medium

The att parameter is typed as any, which bypasses TypeScript's type checking and reduces type safety. A more specific type should be used. Since the attachments array is filtered to contain only file parts, you can use a type assertion to safely access the name and mediaType properties.

Suggested change
attachments={attachments.map((att: any) => ({
name: att.name,
mediaType: att.mediaType,
}))}
attachments={attachments.map((att) => ({
name: (att as { name?: string }).name,
mediaType: (att as { mediaType?: string }).mediaType,
}))}

Comment on lines +101 to 107
className="absolute top-0.5 right-0.5 size-3 rounded-full p-0 opacity-0 transition-opacity group-hover:opacity-100"
onClick={onRemove}
size="sm"
variant="destructive"
variant={"outline"}
>
<CrossSmallIcon size={8} />
<CrossSmallIcon size={2} />
</Button>

Choose a reason for hiding this comment

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

medium

The remove button for attachments is quite small (size-3) and uses an outline variant, which might make it difficult to see and click, especially on smaller screens. Consider making the button slightly larger (e.g., size-4) and using a more prominent variant like destructive to improve visibility and usability.

Suggested change
className="absolute top-0.5 right-0.5 size-3 rounded-full p-0 opacity-0 transition-opacity group-hover:opacity-100"
onClick={onRemove}
size="sm"
variant="destructive"
variant={"outline"}
>
<CrossSmallIcon size={8} />
<CrossSmallIcon size={2} />
</Button>
className="absolute top-0.5 right-0.5 size-4 rounded-full p-0 opacity-0 transition-opacity group-hover:opacity-100"
onClick={onRemove}
size="sm"
variant="destructive"
>
<CrossSmallIcon size={8} />


1. Text files fetched from URLs have a 30-second timeout
2. Text file content is limited to 0.5MB to prevent context overflow
3. Maximum 10 attachments per message

Choose a reason for hiding this comment

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

medium

The documentation states a maximum of 10 attachments per message, but the implementation in lib/ai/file-upload.ts (MAX_ATTACHMENTS) and the PR description specify a limit of 8. Please update the documentation to be consistent with the code.

Suggested change
3. Maximum 10 attachments per message
3. Maximum 8 attachments per message

},
{
protocol: "https",
hostname: "**.public.blob.vercel-storage.com",

Choose a reason for hiding this comment

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

medium

The hostname **.public.blob.vercel-storage.com uses a multi-level wildcard (**). Vercel Blob storage URLs typically follow a pattern like <app-id>.public.blob.vercel-storage.com. A single-level wildcard (*) would be more specific and secure here. I recommend changing this to *.public.blob.vercel-storage.com to follow the principle of least privilege.

Suggested change
hostname: "**.public.blob.vercel-storage.com",
hostname: "*.public.blob.vercel-storage.com",

Dashboard-User-Id: 4a95fa0f-db5a-4a0d-b46b-558f51207d70
@paragon-review
Copy link

⚠️ Optimization workflow failed for commit c0af931.

@Ericzhou-ez
Copy link
Collaborator Author

⚠️ Optimization workflow failed for commit c0af931.

What does that even mean bro...

Dashboard-User-Id: 4a95fa0f-db5a-4a0d-b46b-558f51207d70
paragon-review bot pushed a commit that referenced this pull request Oct 10, 2025
@paragon-review
Copy link

✅ Optimization complete for commit c67fa7a.

A new optimization PR is available: #4 targeting file-upload.

@paragon-review paragon-review bot mentioned this pull request Oct 10, 2025
@vercel
Copy link

vercel bot commented Oct 11, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
witely Ready Ready Preview Comment Oct 11, 2025 2:32am

@Ericzhou-ez Ericzhou-ez merged commit 382374d into main Oct 11, 2025
1 of 4 checks passed
@Ericzhou-ez Ericzhou-ez deleted the file-upload branch October 15, 2025 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants