Multi-file upload support

- Upload accepts multiple files at once (hold Ctrl/Cmd to select)
- All files extracted and combined into one document for AI parsing
- Each file clearly labelled with filename separator in combined text
- Progress shows "Extracting text from file1.xlsx..." per file
- Source filename stores comma-separated list of all uploaded files
- Works with both Normal and Deep extraction modes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
DJP 2026-04-10 09:52:14 -04:00
parent 714ab98388
commit f01774e6f3
2 changed files with 45 additions and 25 deletions

View file

@ -88,46 +88,63 @@ async def _background_parse(project_id: int, filename: str, text: str, metadata:
async def upload_client_document(
project_id: int,
background_tasks: BackgroundTasks,
file: UploadFile = File(...),
files: list[UploadFile] = File(...),
mode: str = "normal",
db: AsyncSession = Depends(get_db),
):
"""Upload a client document and extract assets using AI."""
"""Upload one or more client documents and extract assets using AI."""
project = await _get_project(project_id, db)
# Stage 1: Read file and save to data dir
import os
from app.config import settings
content = await file.read()
save_path = os.path.join(settings.data_dir, file.filename)
with open(save_path, "wb") as f:
f.write(content)
project.source_filename = file.filename
filenames = []
all_text_parts = []
total_chars = 0
total_sheets = 0
project.status = ProjectStatus.PARSING
project.parse_stage = f"Uploading {file.filename}..."
project.parse_stage = f"Uploading {len(files)} file(s)..."
await db.commit()
# Stage 2: Extract text (fast, synchronous)
project.parse_stage = "Extracting text from document..."
await db.commit()
# Stage 1+2: Read and extract text from each file
for file in files:
content = await file.read()
save_path = os.path.join(settings.data_dir, file.filename)
with open(save_path, "wb") as f:
f.write(content)
filenames.append(file.filename)
try:
text, metadata = extract_text_from_file(content, file.filename)
except Exception as e:
project.parse_stage = f"Extracting text from {file.filename}..."
await db.commit()
try:
text, metadata = extract_text_from_file(content, file.filename)
all_text_parts.append(f"\n{'='*60}\nFILE: {file.filename}\n{'='*60}\n{text}")
total_chars += metadata["char_count"]
total_sheets += metadata.get("sheet_count", 0)
except Exception as e:
logger.warning(f"Failed to extract text from {file.filename}: {e}")
continue
if not all_text_parts:
project.status = ProjectStatus.DRAFT
project.parse_stage = None
await db.commit()
raise HTTPException(status_code=400, detail=f"Failed to extract text: {str(e)}")
raise HTTPException(status_code=400, detail="Failed to extract text from any uploaded file.")
sheets_info = f" ({metadata['sheet_count']} sheets)" if metadata['sheet_count'] else ""
project.parse_stage = f"Extracted {metadata['char_count']:,} characters{sheets_info}. Sending to AI..."
combined_text = "\n".join(all_text_parts)
project.source_filename = ", ".join(filenames)
sheets_info = f" ({total_sheets} sheets)" if total_sheets else ""
project.parse_stage = f"Extracted {total_chars:,} characters from {len(filenames)} file(s){sheets_info}. Sending to AI..."
await db.commit()
# Stage 3+4: AI parsing runs in background — return 202 immediately
background_tasks.add_task(_background_parse, project_id, file.filename, text, metadata, mode)
background_tasks.add_task(_background_parse, project_id, ", ".join(filenames), combined_text, {"char_count": total_chars, "sheet_count": total_sheets}, mode)
return {
"message": f"Document received. AI parsing started for {file.filename}.",
"message": f"{len(filenames)} file(s) received. AI parsing started.",
"status": "parsing",
}

View file

@ -144,14 +144,17 @@ export default function ProjectView() {
}, []);
async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
const files = e.target.files;
if (!files || files.length === 0) return;
setUploading(true);
setUploadStage(`Uploading ${file.name}...`);
const names = Array.from(files).map(f => f.name).join(', ');
setUploadStage(`Uploading ${files.length} file(s): ${names}...`);
try {
const form = new FormData();
form.append('file', file);
for (let i = 0; i < files.length; i++) {
form.append('files', files[i]);
}
await api.post(`/projects/${id}/upload?mode=${extractionMode}`, form);
} catch (err: any) {
alert(`Upload failed: ${err.response?.data?.detail || err.message}`);
@ -524,7 +527,7 @@ export default function ProjectView() {
<label className="btn btn-primary upload-btn">
Choose File
<input type="file" accept=".docx,.xlsx,.txt" onChange={handleUpload} hidden />
<input type="file" accept=".docx,.xlsx,.txt" onChange={handleUpload} hidden multiple />
</label>
{project.source_filename && (
<p className="upload-file">Current: {project.source_filename}</p>