mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-03-26 07:13:09 +00:00
feat: add document file upload support for spec creation and project expansion
Add support for uploading Markdown, Text, Word (.docx), CSV, Excel (.xlsx), PDF, and PowerPoint (.pptx) files in addition to existing JPEG/PNG image uploads in the spec creation and project expansion chat interfaces. Backend changes: - New server/utils/document_extraction.py: in-memory text extraction for all document formats using python-docx, openpyxl, PyPDF2, python-pptx (no disk persistence) - Rename ImageAttachment to FileAttachment across schemas, routers, and chat session services - Add build_attachment_content_blocks() helper in chat_constants.py to route images as image content blocks and documents as extracted text blocks - Separate size limits: 5MB for images, 20MB for documents - Handle extraction errors (corrupt files, encrypted PDFs) gracefully Frontend changes: - Widen accepted MIME types and file extensions in both chat components - Add resolveMimeType() fallback for browsers that don't set MIME on .md files - Document attachments display with FileText icon instead of image thumbnail - ChatMessage renders documents as compact pills with filename and size - Update help text from "attach images" to "attach files" Dependencies added: python-docx, openpyxl, PyPDF2, python-pptx Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,16 +11,18 @@ import { useSpecChat } from '../hooks/useSpecChat'
|
||||
import { ChatMessage } from './ChatMessage'
|
||||
import { QuestionOptions } from './QuestionOptions'
|
||||
import { TypingIndicator } from './TypingIndicator'
|
||||
import type { ImageAttachment } from '../lib/types'
|
||||
import type { FileAttachment } from '../lib/types'
|
||||
import { ALL_ALLOWED_MIME_TYPES, IMAGE_MIME_TYPES, isImageAttachment, resolveMimeType } from '../lib/types'
|
||||
import { isSubmitEnter } from '../lib/keyboard'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
|
||||
// Image upload validation constants
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5 MB
|
||||
const ALLOWED_TYPES = ['image/jpeg', 'image/png']
|
||||
// File upload validation constants
|
||||
const MAX_IMAGE_SIZE = 5 * 1024 * 1024 // 5 MB for images
|
||||
const MAX_DOCUMENT_SIZE = 20 * 1024 * 1024 // 20 MB for documents
|
||||
const ALLOWED_EXTENSIONS = ['md', 'txt', 'csv', 'docx', 'xlsx', 'pdf', 'pptx', 'jpg', 'jpeg', 'png']
|
||||
|
||||
// Sample prompt for quick testing
|
||||
const SAMPLE_PROMPT = `Let's call it Simple Todo. This is a really simple web app that I can use to track my to-do items using a Kanban board. I should be able to add to-dos and then drag and drop them through the Kanban board. The different columns in the Kanban board are:
|
||||
@@ -64,7 +66,7 @@ export function SpecCreationChat({
|
||||
const [input, setInput] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [yoloEnabled, setYoloEnabled] = useState(false)
|
||||
const [pendingAttachments, setPendingAttachments] = useState<ImageAttachment[]>([])
|
||||
const [pendingAttachments, setPendingAttachments] = useState<FileAttachment[]>([])
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -138,20 +140,33 @@ export function SpecCreationChat({
|
||||
sendAnswer(answers)
|
||||
}
|
||||
|
||||
// File handling for image attachments
|
||||
// File handling for attachments (images and documents)
|
||||
const handleFileSelect = useCallback((files: FileList | null) => {
|
||||
if (!files) return
|
||||
|
||||
Array.from(files).forEach((file) => {
|
||||
// Validate file type
|
||||
if (!ALLOWED_TYPES.includes(file.type)) {
|
||||
setError(`Invalid file type: ${file.name}. Only JPEG and PNG are supported.`)
|
||||
return
|
||||
// Resolve MIME type (browsers may not set it for .md files)
|
||||
let mimeType = file.type
|
||||
if (!mimeType || !ALL_ALLOWED_MIME_TYPES.includes(mimeType)) {
|
||||
mimeType = resolveMimeType(file.name)
|
||||
}
|
||||
|
||||
// Validate file size
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
setError(`File too large: ${file.name}. Maximum size is 5 MB.`)
|
||||
// Validate file type
|
||||
if (!ALL_ALLOWED_MIME_TYPES.includes(mimeType)) {
|
||||
const ext = file.name.split('.').pop()?.toLowerCase()
|
||||
if (!ext || !ALLOWED_EXTENSIONS.includes(ext)) {
|
||||
setError(`Unsupported file type: ${file.name}. Supported: images (JPEG, PNG) and documents (MD, TXT, CSV, DOCX, XLSX, PDF, PPTX).`)
|
||||
return
|
||||
}
|
||||
mimeType = resolveMimeType(file.name)
|
||||
}
|
||||
|
||||
// Validate size based on type
|
||||
const isImage = (IMAGE_MIME_TYPES as readonly string[]).includes(mimeType)
|
||||
const maxSize = isImage ? MAX_IMAGE_SIZE : MAX_DOCUMENT_SIZE
|
||||
const maxLabel = isImage ? '5 MB' : '20 MB'
|
||||
if (file.size > maxSize) {
|
||||
setError(`File too large: ${file.name}. Maximum size is ${maxLabel}.`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -159,15 +174,14 @@ export function SpecCreationChat({
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const dataUrl = e.target?.result as string
|
||||
// dataUrl is "data:image/png;base64,XXXXXX"
|
||||
const base64Data = dataUrl.split(',')[1]
|
||||
|
||||
const attachment: ImageAttachment = {
|
||||
const attachment: FileAttachment = {
|
||||
id: `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
filename: file.name,
|
||||
mimeType: file.type as 'image/jpeg' | 'image/png',
|
||||
mimeType: mimeType as FileAttachment['mimeType'],
|
||||
base64Data,
|
||||
previewUrl: dataUrl,
|
||||
previewUrl: isImage ? dataUrl : '',
|
||||
size: file.size,
|
||||
}
|
||||
|
||||
@@ -364,11 +378,17 @@ export function SpecCreationChat({
|
||||
key={attachment.id}
|
||||
className="relative group border-2 border-border p-1 bg-card rounded shadow-sm"
|
||||
>
|
||||
<img
|
||||
src={attachment.previewUrl}
|
||||
alt={attachment.filename}
|
||||
className="w-16 h-16 object-cover rounded"
|
||||
/>
|
||||
{isImageAttachment(attachment) ? (
|
||||
<img
|
||||
src={attachment.previewUrl}
|
||||
alt={attachment.filename}
|
||||
className="w-16 h-16 object-cover rounded"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-16 h-16 flex items-center justify-center bg-muted rounded">
|
||||
<FileText size={24} className="text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleRemoveAttachment(attachment.id)}
|
||||
className="absolute -top-2 -right-2 bg-destructive text-destructive-foreground rounded-full p-0.5 border-2 border-border hover:scale-110 transition-transform"
|
||||
@@ -391,7 +411,7 @@ export function SpecCreationChat({
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/jpeg,image/png"
|
||||
accept="image/jpeg,image/png,.md,.txt,.csv,.docx,.xlsx,.pdf,.pptx"
|
||||
multiple
|
||||
onChange={(e) => handleFileSelect(e.target.files)}
|
||||
className="hidden"
|
||||
@@ -403,7 +423,7 @@ export function SpecCreationChat({
|
||||
disabled={connectionStatus !== 'connected'}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Attach image (JPEG, PNG - max 5MB)"
|
||||
title="Attach files (images: JPEG/PNG up to 5MB, documents: MD, TXT, CSV, DOCX, XLSX, PDF, PPTX up to 20MB)"
|
||||
>
|
||||
<Paperclip size={18} />
|
||||
</Button>
|
||||
@@ -444,7 +464,7 @@ export function SpecCreationChat({
|
||||
|
||||
{/* Help text */}
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Press Enter to send, Shift+Enter for new line. Drag & drop or click <Paperclip size={12} className="inline" /> to attach images (JPEG/PNG, max 5MB).
|
||||
Press Enter to send, Shift+Enter for new line. Drag & drop or click <Paperclip size={12} className="inline" /> to attach files.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user