217 lines
6.9 KiB
TypeScript
217 lines
6.9 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { FileUpload } from '@/components/file-upload';
|
|
import { SchemaDefinition, type SchemaField } from '@/components/schema-definition';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useToast } from '@/hooks/use-toast';
|
|
|
|
interface ExtractedData {
|
|
company: string;
|
|
address: string;
|
|
total_sum: number;
|
|
items: {
|
|
item: string;
|
|
unit_price: number;
|
|
quantity: number;
|
|
sum: number;
|
|
}[];
|
|
}
|
|
|
|
interface FileWithText {
|
|
file: File;
|
|
text?: string;
|
|
}
|
|
|
|
export default function Home() {
|
|
const [files, setFiles] = useState<FileWithText[]>([]);
|
|
const [schema, setSchema] = useState<SchemaField[]>([]);
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
const [processedData, setProcessedData] = useState<ExtractedData[]>([]);
|
|
const { toast } = useToast();
|
|
|
|
const handleFilesUploaded = (newFiles: File[]) => {
|
|
const filesWithText = newFiles.map(file => ({ file }));
|
|
setFiles(prev => [...prev, ...filesWithText]);
|
|
};
|
|
|
|
const handleTextExtracted = (fileName: string, text: string) => {
|
|
setFiles(prev =>
|
|
prev.map(f =>
|
|
f.file.name === fileName ? { ...f, text } : f
|
|
)
|
|
);
|
|
};
|
|
|
|
const handleSchemaChange = (newSchema: SchemaField[]) => {
|
|
setSchema(newSchema);
|
|
};
|
|
|
|
const handleStartExtraction = async () => {
|
|
setIsProcessing(true);
|
|
const results: ExtractedData[] = [];
|
|
|
|
try {
|
|
for (const { file, text } of files) {
|
|
if (!text) {
|
|
toast({
|
|
title: 'Error',
|
|
description: `Text not yet extracted for ${file.name}`,
|
|
variant: 'destructive',
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const response = await fetch('/api/process', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
text,
|
|
schema,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to process ${file.name}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
results.push(data);
|
|
|
|
toast({
|
|
title: 'Success',
|
|
description: `Processed ${file.name}`,
|
|
});
|
|
}
|
|
|
|
setProcessedData(results);
|
|
} catch (error) {
|
|
console.error('Error during extraction:', error);
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Failed to process files',
|
|
variant: 'destructive',
|
|
});
|
|
} finally {
|
|
setIsProcessing(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen p-8">
|
|
<div className="max-w-4xl mx-auto space-y-8">
|
|
<h1 className="text-3xl font-bold">PDF Data Extractor</h1>
|
|
|
|
<div className="space-y-8">
|
|
<section>
|
|
<h2 className="text-xl font-semibold mb-4">Upload PDF Files</h2>
|
|
<FileUpload
|
|
onFilesUploaded={handleFilesUploaded}
|
|
onTextExtracted={handleTextExtracted}
|
|
/>
|
|
</section>
|
|
|
|
<section>
|
|
<SchemaDefinition onSchemaChange={handleSchemaChange} />
|
|
</section>
|
|
|
|
<section className="flex justify-center space-x-4">
|
|
<Button
|
|
size="lg"
|
|
onClick={handleStartExtraction}
|
|
disabled={files.length === 0 || isProcessing}
|
|
>
|
|
{isProcessing ? 'Processing...' : 'Start Extraction'}
|
|
</Button>
|
|
{processedData.length > 0 && (
|
|
<Button
|
|
size="lg"
|
|
variant="outline"
|
|
onClick={async () => {
|
|
try {
|
|
const response = await fetch('/api/download', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ data: processedData }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to generate Excel file');
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'extracted_data.xlsx';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
toast({
|
|
title: 'Success',
|
|
description: 'Excel file downloaded successfully',
|
|
});
|
|
} catch (error) {
|
|
console.error('Error downloading file:', error);
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Failed to download Excel file',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
Download Excel
|
|
</Button>
|
|
)}
|
|
</section>
|
|
|
|
{processedData.length > 0 && (
|
|
<section>
|
|
<h2 className="text-xl font-semibold mb-4">Extracted Data</h2>
|
|
<div className="space-y-4">
|
|
{processedData.map((data, index) => (
|
|
<div key={index} className="border rounded-lg p-4 space-y-2">
|
|
<h3 className="font-semibold">{data.company}</h3>
|
|
<p className="text-sm text-gray-600">{data.address}</p>
|
|
<p className="font-medium">Total: ${data.total_sum.toFixed(2)}</p>
|
|
<div className="mt-4">
|
|
<h4 className="font-medium mb-2">Items:</h4>
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b">
|
|
<th className="text-left py-2">Item</th>
|
|
<th className="text-right">Unit Price</th>
|
|
<th className="text-right">Quantity</th>
|
|
<th className="text-right">Sum</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.items.map((item, itemIndex) => (
|
|
<tr key={itemIndex} className="border-b">
|
|
<td className="py-2">{item.item}</td>
|
|
<td className="text-right">${item.unit_price.toFixed(2)}</td>
|
|
<td className="text-right">{item.quantity}</td>
|
|
<td className="text-right">${item.sum.toFixed(2)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|