mirror of
https://github.com/run-llama/auto_rfp.git
synced 2026-07-01 21:54:05 -04:00
1151372e5c
* Add agentic mode support for LlamaParse and fix eligibility extraction
- Add agenticMode option to ParseOptions interface and Zod schema
- Enable agentic parsing by default for better multi-sheet Excel support
- Fix eligibility extraction to gracefully handle documents without
eligibility criteria by returning empty array instead of throwing error
* Refactor project index selection to single-select with auto-save
- Change ProjectIndexSelector from multi-select checkboxes to single-select
clickable rows with radio-style indicators
- Implement debounced auto-save (800ms) when selection changes
- Remove Edit/Save/Cancel buttons in favor of automatic persistence
- Add refreshKey prop to ProjectDocuments for efficient refetch on changes
- Update DocumentsSection to wire components with onSaveSuccess callback
- Simplify Questions page index selector to show index name with Active badge
* Fix ESLint configuration and resolve all lint errors
- Add eslint and eslint-config-next as dev dependencies
- Create .eslintrc.json with next/core-web-vitals preset
- Fix 43 unescaped entity errors by replacing quotes and apostrophes
with HTML entities across 14 component files
- Fix 9 react-hooks/exhaustive-deps warnings by wrapping fetch
functions in useCallback with proper dependency arrays
* Add Vitest testing infrastructure with 173 unit tests
- Configure Vitest with coverage reporting and path aliases
- Add test scripts (test, test:run, test:coverage) to package.json
- Install vitest, @vitest/coverage-v8, vite-tsconfig-paths, vitest-mock-extended
Test coverage includes:
- validators: extract-questions, generate-response, llamaparse, multi-step-response
- errors: all API error classes with type guard
- services: FileValidator (file type/size validation), DefaultResponseService
- middleware: apiHandler and withApiHandler request validation
Add mock infrastructure for Prisma, OpenAI, and test fixtures
* Fix missing agenticMode in LlamaParse processing service
Add agentic_mode field to request schema and form data parser to ensure
the agenticMode option is properly passed through the parsing pipeline.
This resolves TypeScript compilation error where agenticMode was required
in LlamaParseOptions but not provided by the processing service.
* Fix Add Manually button routing and improve Vercel preview URL handling
- Fix 404 error when clicking Add Manually button by using correct route path
/projects/{projectId}/questions/create instead of /questions/create?projectId=
- Update magic link auth to use VERCEL_URL for preview deployments
* Simplify IndexSelector to display-only mode
Remove index configuration UI since only single file selection is now supported:
- Remove Configure/Hide toggle button and collapsible panel
- Remove Select All/Deselect All functionality
- Remove checkbox selection grid for indexes
- Clean up handleIndexToggle and handleSelectAllIndexes from provider
- Keep project card display showing active index name and status
379 lines
14 KiB
TypeScript
379 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useRef, useEffect } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { toast } from "@/components/ui/use-toast";
|
|
import { Spinner } from "@/components/ui/spinner";
|
|
import { LlamaParseResult } from "@/types/api";
|
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
|
import { ProcessingModal, ProcessingStatus } from "./ProcessingModal";
|
|
|
|
interface FileUploaderProps {
|
|
onFileProcessed?: (result: LlamaParseResult) => void;
|
|
processingStatus?: ProcessingStatus;
|
|
updateProcessingStatus?: (status: ProcessingStatus) => void;
|
|
}
|
|
|
|
export function FileUploader({
|
|
onFileProcessed,
|
|
processingStatus: externalProcessingStatus,
|
|
updateProcessingStatus: externalUpdateProcessingStatus
|
|
}: FileUploaderProps) {
|
|
const router = useRouter();
|
|
const [file, setFile] = useState<File | null>(null);
|
|
const [parsingMode, setParsingMode] = useState<string>("balanced");
|
|
const [documentName, setDocumentName] = useState<string>("");
|
|
const [isUploading, setIsUploading] = useState<boolean>(false);
|
|
const [dragActive, setDragActive] = useState<boolean>(false);
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
// Processing modal state - use external state if provided
|
|
const [showProcessingModal, setShowProcessingModal] = useState(false);
|
|
const [internalProcessingStatus, setInternalProcessingStatus] = useState<ProcessingStatus>("uploading");
|
|
const [processingProgress, setProcessingProgress] = useState(0);
|
|
const [processedResult, setProcessedResult] = useState<LlamaParseResult | null>(null);
|
|
|
|
// Use external processing status if provided, otherwise use internal
|
|
const processingStatus = externalProcessingStatus || internalProcessingStatus;
|
|
|
|
// Function to update processing status - calls external handler if provided
|
|
const updateProcessingStatus = (status: ProcessingStatus) => {
|
|
if (externalUpdateProcessingStatus) {
|
|
externalUpdateProcessingStatus(status);
|
|
} else {
|
|
setInternalProcessingStatus(status);
|
|
}
|
|
|
|
// If status is "complete", hide the modal after a brief delay
|
|
if (status === "complete") {
|
|
setTimeout(() => {
|
|
setShowProcessingModal(false);
|
|
}, 2000);
|
|
}
|
|
};
|
|
|
|
// Debug effect to monitor modal state
|
|
useEffect(() => {
|
|
console.log("Processing modal state changed:", showProcessingModal);
|
|
if (showProcessingModal) {
|
|
console.log("Modal shown with status:", processingStatus);
|
|
}
|
|
}, [showProcessingModal, processingStatus]);
|
|
|
|
// Handle drag events
|
|
const handleDrag = (e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
if (e.type === "dragenter" || e.type === "dragover") {
|
|
setDragActive(true);
|
|
} else if (e.type === "dragleave") {
|
|
setDragActive(false);
|
|
}
|
|
};
|
|
|
|
// Handle drop event
|
|
const handleDrop = (e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setDragActive(false);
|
|
|
|
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
|
handleFile(e.dataTransfer.files[0]);
|
|
}
|
|
};
|
|
|
|
// Handle file input change
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
e.preventDefault();
|
|
|
|
if (e.target.files && e.target.files[0]) {
|
|
handleFile(e.target.files[0]);
|
|
}
|
|
};
|
|
|
|
// Handle file validation and state update
|
|
const handleFile = (file: File) => {
|
|
const fileExtension = file.name.split('.').pop()?.toLowerCase();
|
|
|
|
if (fileExtension === 'csv' || fileExtension === 'xlsx' || fileExtension === 'xls' || fileExtension === 'pdf') {
|
|
setFile(file);
|
|
if (!documentName) {
|
|
setDocumentName(file.name.split('.')[0]);
|
|
}
|
|
} else {
|
|
toast({
|
|
title: "Unsupported file format",
|
|
description: "Please upload an Excel (.xlsx, .xls), CSV (.csv), or PDF file.",
|
|
variant: "destructive"
|
|
});
|
|
}
|
|
};
|
|
|
|
// Handle file upload to LlamaParse
|
|
const handleUpload = async () => {
|
|
if (!file) {
|
|
toast({
|
|
title: "No file selected",
|
|
description: "Please select a file to upload.",
|
|
variant: "destructive"
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!documentName.trim()) {
|
|
toast({
|
|
title: "Document name required",
|
|
description: "Please provide a name for this document.",
|
|
variant: "destructive"
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Show the processing modal immediately
|
|
updateProcessingStatus("uploading");
|
|
setProcessingProgress(0);
|
|
setShowProcessingModal(true);
|
|
setIsUploading(true);
|
|
|
|
// Set up a timer to automatically progress the UI after a reasonable time
|
|
const progressTimer = setTimeout(() => {
|
|
// If we're still uploading after 3 seconds, assume backend processing has started
|
|
if (processingStatus === "uploading") {
|
|
console.log("Auto-advancing to analyzing state after timeout");
|
|
updateProcessingStatus("analyzing");
|
|
}
|
|
}, 3000);
|
|
|
|
try {
|
|
// Create form data
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('documentName', documentName);
|
|
|
|
// Add parsing mode parameters based on selection
|
|
if (parsingMode === "fast") {
|
|
formData.append('fast_mode', "true");
|
|
} else if (parsingMode === "premium") {
|
|
formData.append('premium_mode', "true");
|
|
} else if (parsingMode === "complexTables") {
|
|
formData.append('preset', "complexTables");
|
|
}
|
|
|
|
// Send the file to our API endpoint
|
|
|
|
const response = await fetch('/api/llamaparse', {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
// Clear the auto-progress timer since the API has responded
|
|
clearTimeout(progressTimer);
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || 'Failed to upload file');
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
// Upload is done, now switch to import progress flow
|
|
setIsUploading(false);
|
|
setProcessedResult(result);
|
|
|
|
// Since API has responded completely, go directly to mapping phase
|
|
// The actual parsing is already done at this point
|
|
updateProcessingStatus("mapping");
|
|
|
|
// Now simulate the mapping progress
|
|
let progress = 0;
|
|
const interval = setInterval(() => {
|
|
progress += 5;
|
|
setProcessingProgress(progress);
|
|
|
|
if (progress >= 100) {
|
|
clearInterval(interval);
|
|
|
|
// When complete, move to parsing status
|
|
updateProcessingStatus("parsing");
|
|
|
|
// Call the onFileProcessed callback to begin question extraction
|
|
if (onFileProcessed) {
|
|
onFileProcessed(result);
|
|
|
|
// Update to extracting status when OpenAI API call begins
|
|
setTimeout(() => {
|
|
updateProcessingStatus("extracting");
|
|
|
|
// Keep modal open until question extraction completes
|
|
// The page component will handle hiding the modal
|
|
}, 1500);
|
|
} else {
|
|
// This path shouldn't typically be used, but just in case
|
|
setShowProcessingModal(false);
|
|
router.push(`/projects/${result.documentId}/questions`);
|
|
}
|
|
}
|
|
}, 200);
|
|
|
|
} catch (error) {
|
|
// Clear the auto-progress timer on error
|
|
clearTimeout(progressTimer);
|
|
|
|
console.error("Error uploading file:", error);
|
|
toast({
|
|
title: "Upload failed",
|
|
description: error instanceof Error ? error.message : "There was an error uploading your file. Please try again.",
|
|
variant: "destructive"
|
|
});
|
|
setIsUploading(false);
|
|
// Hide the modal on error
|
|
setShowProcessingModal(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Upload Document</CardTitle>
|
|
<CardDescription>
|
|
Upload an Excel, CSV, or PDF file to be processed by LlamaParse
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-6">
|
|
{/* File Upload Area with Drag & Drop */}
|
|
<div
|
|
className={`border-2 border-dashed rounded-lg p-10 text-center transition-colors cursor-pointer ${
|
|
dragActive
|
|
? "border-primary bg-primary/5"
|
|
: "border-muted-foreground/25 hover:bg-muted/50"
|
|
}`}
|
|
onDragEnter={handleDrag}
|
|
onDragOver={handleDrag}
|
|
onDragLeave={handleDrag}
|
|
onDrop={handleDrop}
|
|
onClick={() => inputRef.current?.click()}
|
|
>
|
|
<div className="flex flex-col items-center justify-center gap-4">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="40"
|
|
height="40"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className={dragActive ? "text-primary" : "text-muted-foreground"}
|
|
>
|
|
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
|
<polyline points="14 2 14 8 20 8" />
|
|
</svg>
|
|
<div>
|
|
<p className="font-medium">Drag and drop your file here or click to browse</p>
|
|
<p className="text-sm text-muted-foreground">Supports Excel (.xlsx, .xls), CSV (.csv), and PDF files</p>
|
|
</div>
|
|
<Input
|
|
type="file"
|
|
accept=".xlsx,.xls,.csv,.pdf"
|
|
className="hidden"
|
|
ref={inputRef}
|
|
onChange={handleChange}
|
|
/>
|
|
<Button size="sm" type="button">
|
|
Select File
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Advanced Settings Accordion */}
|
|
<Accordion type="single" collapsible className="w-full">
|
|
<AccordionItem value="advanced-settings">
|
|
<AccordionTrigger className="text-sm font-medium">
|
|
Advanced Settings
|
|
</AccordionTrigger>
|
|
<AccordionContent>
|
|
<div className="grid gap-4 pt-2">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">Parsing Mode</label>
|
|
<Select
|
|
defaultValue="balanced"
|
|
value={parsingMode}
|
|
onValueChange={setParsingMode}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select parsing mode" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="fast">Fast (Simple, text-only documents)</SelectItem>
|
|
<SelectItem value="balanced">Balanced (Default for mixed content)</SelectItem>
|
|
<SelectItem value="premium">Premium (Complex documents with tables/images)</SelectItem>
|
|
<SelectItem value="complexTables">Complex Tables (Specialized for tables)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
Choose the appropriate mode based on your document's complexity
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">Document Name</label>
|
|
<Input
|
|
placeholder="Enter a name for this document"
|
|
className="w-full"
|
|
value={documentName}
|
|
onChange={(e) => setDocumentName(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
</Accordion>
|
|
</div>
|
|
</CardContent>
|
|
<CardFooter className="flex justify-between border-t pt-6">
|
|
<div className="space-y-1">
|
|
<p className="text-sm">
|
|
Selected File: {file ? (
|
|
<span className="font-medium">{file.name}</span>
|
|
) : (
|
|
<span className="italic text-muted-foreground">No file selected</span>
|
|
)}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">Powered by LlamaParse</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
onClick={handleUpload}
|
|
disabled={!file || isUploading}
|
|
>
|
|
{isUploading ? (
|
|
<>
|
|
<Spinner className="mr-2" size="sm" />
|
|
Processing...
|
|
</>
|
|
) : (
|
|
"Upload & Process"
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</CardFooter>
|
|
</Card>
|
|
|
|
{/* Processing Modal - simpler implementation without using Dialog */}
|
|
<ProcessingModal
|
|
isOpen={showProcessingModal}
|
|
fileName={file?.name || "Unknown file"}
|
|
status={processingStatus}
|
|
progress={processingProgress}
|
|
/>
|
|
</>
|
|
);
|
|
}
|