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:
parent
714ab98388
commit
f01774e6f3
2 changed files with 45 additions and 25 deletions
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue