fix: Simplify PDF filling - use fillable fields directly
The previous approach was overcomplicated with LaTeX templates. Now the system simply: 1. Detects PDF form fields (AcroForm) 2. Sends field names to Gemini for data extraction 3. Gemini returns 'key' matching exact PDF field names 4. Fields are filled directly in the original PDF Removed: - LaTeX template detection logic - G2210-specific field definitions - Complex mode switching The fillable PDF approach is simpler and more reliable. https://claude.ai/code/session_016pQhdznHZ74Fpkvwr3cLBq
This commit is contained in:
parent
19e96ef59b
commit
f7f899dce7
2 changed files with 104 additions and 367 deletions
|
|
@ -1,8 +1,7 @@
|
||||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { ExtractedField, FileData } from '../types';
|
import { ExtractedField, FileData } from '../types';
|
||||||
import { Check, Edit2, Download, RefreshCw, FileText, AlertTriangle, XCircle, ArrowRight, PenTool, CheckCircle2, Circle, FileCode, Loader2 } from 'lucide-react';
|
import { Check, Edit2, Download, RefreshCw, FileText, AlertTriangle, XCircle, ArrowRight, PenTool, CheckCircle2, Circle, FileCheck } from 'lucide-react';
|
||||||
import { createFilledPdf } from '../services/pdfService';
|
import { createFilledPdf } from '../services/pdfService';
|
||||||
import { generateLatexPdf, isLatexServiceAvailable, detectTemplate, base64ToBlob } from '../services/latexService';
|
|
||||||
import { jsPDF } from "jspdf";
|
import { jsPDF } from "jspdf";
|
||||||
|
|
||||||
interface ReviewPanelProps {
|
interface ReviewPanelProps {
|
||||||
|
|
@ -14,8 +13,6 @@ interface ReviewPanelProps {
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PdfMode = 'overlay' | 'fillable' | 'latex';
|
|
||||||
|
|
||||||
export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
fields: initialFields,
|
fields: initialFields,
|
||||||
formFile,
|
formFile,
|
||||||
|
|
@ -28,80 +25,28 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
const [activeField, setActiveField] = useState<number | null>(null);
|
const [activeField, setActiveField] = useState<number | null>(null);
|
||||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||||
const [filterMode, setFilterMode] = useState<'ALL' | 'ATTENTION'>('ALL');
|
const [filterMode, setFilterMode] = useState<'ALL' | 'ATTENTION'>('ALL');
|
||||||
const [latexAvailable, setLatexAvailable] = useState<boolean | null>(null);
|
|
||||||
const [pdfMode, setPdfMode] = useState<PdfMode>(isFillablePdf ? 'fillable' : 'overlay');
|
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
|
||||||
const [latexPdfBase64, setLatexPdfBase64] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Detect template from form file name
|
|
||||||
const detectedTemplate = useMemo(() => detectTemplate(formFile.file.name), [formFile.file.name]);
|
|
||||||
|
|
||||||
// Check LaTeX service availability on mount
|
|
||||||
useEffect(() => {
|
|
||||||
const checkLatex = async () => {
|
|
||||||
const available = await isLatexServiceAvailable();
|
|
||||||
setLatexAvailable(available);
|
|
||||||
// Auto-switch to LaTeX mode if available and template detected
|
|
||||||
if (available && detectedTemplate && !isFillablePdf) {
|
|
||||||
setPdfMode('latex');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
checkLatex();
|
|
||||||
}, [detectedTemplate, isFillablePdf]);
|
|
||||||
|
|
||||||
// Derived state for progress
|
// Derived state for progress
|
||||||
const verifiedCount = fields.filter(f => f.isVerified).length;
|
const verifiedCount = fields.filter(f => f.isVerified).length;
|
||||||
const totalCount = fields.length;
|
const totalCount = fields.length;
|
||||||
const progressPercent = Math.round((verifiedCount / totalCount) * 100);
|
const progressPercent = Math.round((verifiedCount / totalCount) * 100);
|
||||||
|
|
||||||
const fieldsRequiresAttention = useMemo(() =>
|
const fieldsRequiresAttention = useMemo(() =>
|
||||||
fields.filter(f => f.validation?.status !== 'VALID'),
|
fields.filter(f => f.validation?.status !== 'VALID'),
|
||||||
[fields]);
|
[fields]);
|
||||||
|
|
||||||
// Generate LaTeX PDF
|
// Generate preview - fills the original PDF directly
|
||||||
const generateLatexPreview = useCallback(async () => {
|
|
||||||
if (!detectedTemplate || pdfMode !== 'latex') return;
|
|
||||||
|
|
||||||
setIsGenerating(true);
|
|
||||||
try {
|
|
||||||
const result = await generateLatexPdf(detectedTemplate, fields);
|
|
||||||
if (result.success && result.pdf) {
|
|
||||||
setLatexPdfBase64(result.pdf);
|
|
||||||
const blob = base64ToBlob(result.pdf);
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
setPreviewUrl(url);
|
|
||||||
} else {
|
|
||||||
console.error('LaTeX generation failed:', result.error);
|
|
||||||
// Fallback to overlay mode
|
|
||||||
setPdfMode('overlay');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('LaTeX generation error:', e);
|
|
||||||
setPdfMode('overlay');
|
|
||||||
} finally {
|
|
||||||
setIsGenerating(false);
|
|
||||||
}
|
|
||||||
}, [detectedTemplate, fields, pdfMode]);
|
|
||||||
|
|
||||||
// Generate preview
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updatePreview = async () => {
|
const updatePreview = async () => {
|
||||||
// LaTeX mode - handled separately with button click
|
|
||||||
if (pdfMode === 'latex') {
|
|
||||||
// Only auto-generate if we don't have a preview yet
|
|
||||||
if (!latexPdfBase64) {
|
|
||||||
generateLatexPreview();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formFile.type === 'application/pdf') {
|
if (formFile.type === 'application/pdf') {
|
||||||
try {
|
try {
|
||||||
const filledPdfBytes = await createFilledPdf(formFile.base64, fields, pdfMode === 'fillable');
|
const filledPdfBytes = await createFilledPdf(formFile.base64, fields, isFillablePdf);
|
||||||
const blob = new Blob([filledPdfBytes], { type: 'application/pdf' });
|
const blob = new Blob([filledPdfBytes], { type: 'application/pdf' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
setPreviewUrl(url);
|
setPreviewUrl(prev => {
|
||||||
return () => URL.revokeObjectURL(url);
|
if (prev) URL.revokeObjectURL(prev);
|
||||||
|
return url;
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to generate PDF preview", e);
|
console.error("Failed to generate PDF preview", e);
|
||||||
}
|
}
|
||||||
|
|
@ -110,17 +55,15 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debounce slightly to avoid rapid updates on typing
|
const timer = setTimeout(updatePreview, 500);
|
||||||
const timer = setTimeout(updatePreview, 600);
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [fields, pdfMode, formFile.base64, formFile.type, latexPdfBase64, generateLatexPreview]);
|
}, [fields, isFillablePdf, formFile.base64, formFile.type, formFile.previewUrl]);
|
||||||
|
|
||||||
const handleUpdate = (index: number, newValue: string) => {
|
const handleUpdate = (index: number, newValue: string) => {
|
||||||
const newFields = [...fields];
|
const newFields = [...fields];
|
||||||
newFields[index] = {
|
newFields[index] = {
|
||||||
...newFields[index],
|
...newFields[index],
|
||||||
value: newValue,
|
value: newValue,
|
||||||
// Auto-verify when manually edited
|
|
||||||
isVerified: true,
|
isVerified: true,
|
||||||
validation: { ...newFields[index].validation!, status: 'VALID', message: 'Manually verified' }
|
validation: { ...newFields[index].validation!, status: 'VALID', message: 'Manually verified' }
|
||||||
};
|
};
|
||||||
|
|
@ -129,9 +72,9 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
|
|
||||||
const toggleVerify = (index: number) => {
|
const toggleVerify = (index: number) => {
|
||||||
const newFields = [...fields];
|
const newFields = [...fields];
|
||||||
newFields[index] = {
|
newFields[index] = {
|
||||||
...newFields[index],
|
...newFields[index],
|
||||||
isVerified: !newFields[index].isVerified
|
isVerified: !newFields[index].isVerified
|
||||||
};
|
};
|
||||||
setFields(newFields);
|
setFields(newFields);
|
||||||
};
|
};
|
||||||
|
|
@ -144,30 +87,6 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
// LaTeX mode - regenerate fresh PDF for download
|
|
||||||
if (pdfMode === 'latex' && detectedTemplate) {
|
|
||||||
setIsGenerating(true);
|
|
||||||
try {
|
|
||||||
const result = await generateLatexPdf(detectedTemplate, fields);
|
|
||||||
if (result.success && result.pdf) {
|
|
||||||
const blob = base64ToBlob(result.pdf);
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = `${detectedTemplate}_filled.pdf`;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
} else {
|
|
||||||
alert(`PDF generation failed: ${result.error}`);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setIsGenerating(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formFile.type === 'application/pdf' && previewUrl) {
|
if (formFile.type === 'application/pdf' && previewUrl) {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = previewUrl;
|
a.href = previewUrl;
|
||||||
|
|
@ -187,25 +106,15 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Regenerate LaTeX preview
|
|
||||||
const handleRegenerateLatex = () => {
|
|
||||||
setLatexPdfBase64(null);
|
|
||||||
generateLatexPreview();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sort: Unverified/Issues first, then verified
|
// Sort: Unverified/Issues first, then verified
|
||||||
const displayedFields = fields.map((f, i) => ({ ...f, originalIndex: i }))
|
const displayedFields = fields.map((f, i) => ({ ...f, originalIndex: i }))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
// Priority 1: Attention needed
|
|
||||||
const aNeedsAttn = a.validation?.status !== 'VALID';
|
const aNeedsAttn = a.validation?.status !== 'VALID';
|
||||||
const bNeedsAttn = b.validation?.status !== 'VALID';
|
const bNeedsAttn = b.validation?.status !== 'VALID';
|
||||||
if (aNeedsAttn && !bNeedsAttn) return -1;
|
if (aNeedsAttn && !bNeedsAttn) return -1;
|
||||||
if (!aNeedsAttn && bNeedsAttn) return 1;
|
if (!aNeedsAttn && bNeedsAttn) return 1;
|
||||||
|
|
||||||
// Priority 2: Unverified
|
|
||||||
if (!a.isVerified && b.isVerified) return -1;
|
if (!a.isVerified && b.isVerified) return -1;
|
||||||
if (a.isVerified && !b.isVerified) return 1;
|
if (a.isVerified && !b.isVerified) return 1;
|
||||||
|
|
||||||
return a.originalIndex - b.originalIndex;
|
return a.originalIndex - b.originalIndex;
|
||||||
})
|
})
|
||||||
.filter(f => filterMode === 'ALL' || (f.validation?.status !== 'VALID' && !f.isVerified));
|
.filter(f => filterMode === 'ALL' || (f.validation?.status !== 'VALID' && !f.isVerified));
|
||||||
|
|
@ -218,106 +127,61 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
<h2 className="text-2xl font-bold text-slate-900">Review & Verify</h2>
|
<h2 className="text-2xl font-bold text-slate-900">Review & Verify</h2>
|
||||||
<p className="text-slate-500 text-sm mt-1">{summary}</p>
|
<p className="text-slate-500 text-sm mt-1">{summary}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 w-full md:w-auto">
|
<div className="flex items-center gap-4 w-full md:w-auto">
|
||||||
{/* Verification Progress */}
|
|
||||||
<div className="hidden md:flex flex-col items-end mr-2">
|
<div className="hidden md:flex flex-col items-end mr-2">
|
||||||
<span className="text-xs font-semibold text-slate-600 mb-1">
|
<span className="text-xs font-semibold text-slate-600 mb-1">
|
||||||
{verifiedCount} / {totalCount} Verified
|
{verifiedCount} / {totalCount} Verified
|
||||||
</span>
|
</span>
|
||||||
<div className="w-32 h-2 bg-slate-200 rounded-full overflow-hidden">
|
<div className="w-32 h-2 bg-slate-200 rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-emerald-500 transition-all duration-500 ease-out"
|
className="h-full bg-emerald-500 transition-all duration-500 ease-out"
|
||||||
style={{ width: `${progressPercent}%` }}
|
style={{ width: `${progressPercent}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={onReset}
|
onClick={onReset}
|
||||||
className="flex items-center px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
|
className="flex items-center px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
|
||||||
>
|
>
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
Start Over
|
Start Over
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
disabled={isGenerating}
|
className="flex items-center px-4 py-2 text-sm font-medium text-white rounded-lg transition-colors shadow-sm bg-indigo-600 hover:bg-indigo-700"
|
||||||
className={`flex items-center px-4 py-2 text-sm font-medium text-white rounded-lg transition-colors shadow-sm ${isGenerating ? 'bg-indigo-400 cursor-wait' : 'bg-indigo-600 hover:bg-indigo-700'}`}
|
|
||||||
>
|
>
|
||||||
{isGenerating ? (
|
<Download className="w-4 h-4 mr-2" />
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Download className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
Download PDF
|
Download PDF
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 flex-1 min-h-0">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 flex-1 min-h-0">
|
||||||
{/* Left Column: Visual Reference */}
|
{/* Left Column: PDF Preview */}
|
||||||
<div className="bg-slate-900 rounded-xl overflow-hidden shadow-lg flex flex-col">
|
<div className="bg-slate-900 rounded-xl overflow-hidden shadow-lg flex flex-col">
|
||||||
<div className="p-3 bg-slate-800 border-b border-slate-700 flex justify-between items-center">
|
<div className="p-3 bg-slate-800 border-b border-slate-700 flex justify-between items-center">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-xs font-medium text-slate-300 uppercase tracking-wider">Preview</span>
|
<span className="text-xs font-medium text-slate-300 uppercase tracking-wider">Preview</span>
|
||||||
{pdfMode === 'latex' && (
|
{isFillablePdf ? (
|
||||||
<span className="flex items-center text-[10px] bg-emerald-500/20 text-emerald-300 px-1.5 py-0.5 rounded border border-emerald-500/30">
|
<span className="flex items-center text-[10px] bg-emerald-500/20 text-emerald-300 px-1.5 py-0.5 rounded border border-emerald-500/30">
|
||||||
<FileCode className="w-3 h-3 mr-1" />
|
<FileCheck className="w-3 h-3 mr-1" />
|
||||||
LaTeX Template Mode
|
Fillable PDF
|
||||||
</span>
|
</span>
|
||||||
)}
|
) : (
|
||||||
{pdfMode === 'overlay' && (
|
<span className="flex items-center text-[10px] bg-amber-500/20 text-amber-300 px-1.5 py-0.5 rounded border border-amber-500/30">
|
||||||
<span className="flex items-center text-[10px] bg-indigo-500/20 text-indigo-300 px-1.5 py-0.5 rounded border border-indigo-500/30">
|
|
||||||
<PenTool className="w-3 h-3 mr-1" />
|
<PenTool className="w-3 h-3 mr-1" />
|
||||||
Visual Overlay Mode
|
Visual Overlay
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{pdfMode === 'fillable' && (
|
|
||||||
<span className="flex items-center text-[10px] bg-blue-500/20 text-blue-300 px-1.5 py-0.5 rounded border border-blue-500/30">
|
|
||||||
<FileText className="w-3 h-3 mr-1" />
|
|
||||||
Fillable PDF Mode
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<span className="text-xs text-slate-400">{formFile.file.name}</span>
|
||||||
{/* Mode Switcher */}
|
|
||||||
{latexAvailable && detectedTemplate && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
const newMode = pdfMode === 'latex' ? 'overlay' : 'latex';
|
|
||||||
setPdfMode(newMode);
|
|
||||||
if (newMode === 'latex') {
|
|
||||||
setLatexPdfBase64(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="text-[10px] bg-slate-700 hover:bg-slate-600 text-slate-300 px-2 py-1 rounded transition-colors"
|
|
||||||
>
|
|
||||||
{pdfMode === 'latex' ? 'Use Overlay' : 'Use LaTeX'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{pdfMode === 'latex' && (
|
|
||||||
<button
|
|
||||||
onClick={handleRegenerateLatex}
|
|
||||||
disabled={isGenerating}
|
|
||||||
className="text-[10px] bg-emerald-700 hover:bg-emerald-600 text-emerald-100 px-2 py-1 rounded transition-colors disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isGenerating ? 'Generating...' : 'Regenerate'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<span className="text-xs text-slate-400">{formFile.file.name}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 bg-slate-900 relative">
|
<div className="flex-1 bg-slate-900 relative">
|
||||||
{isGenerating && pdfMode === 'latex' ? (
|
{previewUrl ? (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-emerald-400">
|
formFile.type === 'application/pdf' ? (
|
||||||
<Loader2 className="w-16 h-16 mb-4 animate-spin" />
|
|
||||||
<p className="font-medium">Generating LaTeX PDF...</p>
|
|
||||||
<p className="text-sm text-slate-500 mt-1">Compiling template with your data</p>
|
|
||||||
</div>
|
|
||||||
) : previewUrl ? (
|
|
||||||
formFile.type === 'application/pdf' || pdfMode === 'latex' ? (
|
|
||||||
<iframe
|
<iframe
|
||||||
src={previewUrl}
|
src={previewUrl}
|
||||||
title="Form PDF Preview"
|
title="Form PDF Preview"
|
||||||
|
|
@ -341,21 +205,20 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column: Verification List */}
|
{/* Right Column: Field List */}
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 flex flex-col overflow-hidden">
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 flex flex-col overflow-hidden">
|
||||||
{/* List Header */}
|
|
||||||
<div className="p-4 border-b border-slate-100 bg-slate-50/50">
|
<div className="p-4 border-b border-slate-100 bg-slate-50/50">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h3 className="font-semibold text-slate-800">Field Verification</h3>
|
<h3 className="font-semibold text-slate-800">Field Verification</h3>
|
||||||
<div className="flex space-x-2 text-xs">
|
<div className="flex space-x-2 text-xs">
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilterMode('ALL')}
|
onClick={() => setFilterMode('ALL')}
|
||||||
className={`px-3 py-1 rounded-full border transition-colors ${filterMode === 'ALL' ? 'bg-slate-800 text-white border-slate-800' : 'bg-white text-slate-600 border-slate-200 hover:bg-slate-50'}`}
|
className={`px-3 py-1 rounded-full border transition-colors ${filterMode === 'ALL' ? 'bg-slate-800 text-white border-slate-800' : 'bg-white text-slate-600 border-slate-200 hover:bg-slate-50'}`}
|
||||||
>
|
>
|
||||||
All Fields ({totalCount})
|
All Fields ({totalCount})
|
||||||
</button>
|
</button>
|
||||||
{fieldsRequiresAttention.length > 0 && (
|
{fieldsRequiresAttention.length > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilterMode('ATTENTION')}
|
onClick={() => setFilterMode('ATTENTION')}
|
||||||
className={`px-3 py-1 rounded-full border transition-colors flex items-center ${filterMode === 'ATTENTION' ? 'bg-amber-100 text-amber-800 border-amber-200' : 'bg-white text-amber-600 border-slate-200 hover:bg-amber-50'}`}
|
className={`px-3 py-1 rounded-full border transition-colors flex items-center ${filterMode === 'ATTENTION' ? 'bg-amber-100 text-amber-800 border-amber-200' : 'bg-white text-amber-600 border-slate-200 hover:bg-amber-50'}`}
|
||||||
>
|
>
|
||||||
|
|
@ -365,37 +228,33 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{pdfMode === 'latex' && detectedTemplate && (
|
{isFillablePdf ? (
|
||||||
<div className="flex items-start space-x-2 bg-emerald-50 text-emerald-800 p-3 rounded-md border border-emerald-100 text-xs">
|
<div className="flex items-start space-x-2 bg-emerald-50 text-emerald-800 p-3 rounded-md border border-emerald-100 text-xs">
|
||||||
<FileCode className="w-4 h-4 flex-shrink-0 text-emerald-600 mt-0.5" />
|
<FileCheck className="w-4 h-4 flex-shrink-0 text-emerald-600 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold">LaTeX Template Mode: {detectedTemplate}</p>
|
<p className="font-semibold">Fillable PDF Mode</p>
|
||||||
<p>Using precise LaTeX template. All fields will be positioned correctly in the generated PDF.</p>
|
<p>Fields are filled directly into the original PDF form.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : (
|
||||||
{pdfMode === 'overlay' && (
|
<div className="flex items-start space-x-2 bg-amber-50 text-amber-800 p-3 rounded-md border border-amber-100 text-xs">
|
||||||
<div className="flex items-start space-x-2 bg-indigo-50 text-indigo-800 p-3 rounded-md border border-indigo-100 text-xs">
|
<PenTool className="w-4 h-4 flex-shrink-0 text-amber-600 mt-0.5" />
|
||||||
<PenTool className="w-4 h-4 flex-shrink-0 text-indigo-600 mt-0.5" />
|
<div>
|
||||||
<div>
|
<p className="font-semibold">Visual Overlay Mode</p>
|
||||||
<p className="font-semibold">Visual Overlay Mode</p>
|
<p>This PDF has no fillable fields. Text is overlaid at estimated positions.</p>
|
||||||
<p>AI is visually estimating field positions. Please verify text alignment in the preview.</p>
|
</div>
|
||||||
{latexAvailable && detectedTemplate && (
|
</div>
|
||||||
<p className="mt-1 text-emerald-700 font-medium">Tip: LaTeX template available! Click "Use LaTeX" for better results.</p>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Fields List */}
|
{/* Fields List */}
|
||||||
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-slate-50/30">
|
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-slate-50/30">
|
||||||
{displayedFields.map((field) => {
|
{displayedFields.map((field) => {
|
||||||
const idx = field.originalIndex;
|
const idx = field.originalIndex;
|
||||||
const status = field.validation?.status || 'VALID';
|
const status = field.validation?.status || 'VALID';
|
||||||
const isVerified = field.isVerified;
|
const isVerified = field.isVerified;
|
||||||
|
|
||||||
let statusBorder = isVerified ? "border-emerald-200" : "border-slate-200";
|
let statusBorder = isVerified ? "border-emerald-200" : "border-slate-200";
|
||||||
let statusBg = isVerified ? "bg-emerald-50/30" : "bg-white";
|
let statusBg = isVerified ? "bg-emerald-50/30" : "bg-white";
|
||||||
|
|
||||||
|
|
@ -410,27 +269,16 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
className={`
|
className={`relative group rounded-lg border transition-all duration-200 p-3 shadow-sm ${activeField === idx ? 'ring-1 ring-indigo-500 border-indigo-500 shadow-md z-10' : statusBorder} ${statusBg}`}
|
||||||
relative group rounded-lg border transition-all duration-200 p-3 shadow-sm
|
|
||||||
${activeField === idx ? 'ring-1 ring-indigo-500 border-indigo-500 shadow-md z-10' : statusBorder}
|
|
||||||
${statusBg}
|
|
||||||
`}
|
|
||||||
onFocus={() => setActiveField(idx)}
|
onFocus={() => setActiveField(idx)}
|
||||||
onBlur={() => setActiveField(null)}
|
onBlur={() => setActiveField(null)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
{/* Verification Checkbox */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleVerify(idx)}
|
onClick={() => toggleVerify(idx)}
|
||||||
className={`
|
className={`mt-1 flex-shrink-0 w-5 h-5 rounded border flex items-center justify-center transition-colors ${isVerified ? 'bg-emerald-500 border-emerald-500 text-white' : 'bg-white border-slate-300 text-transparent hover:border-emerald-400'}`}
|
||||||
mt-1 flex-shrink-0 w-5 h-5 rounded border flex items-center justify-center transition-colors
|
|
||||||
${isVerified
|
|
||||||
? 'bg-emerald-500 border-emerald-500 text-white'
|
|
||||||
: 'bg-white border-slate-300 text-transparent hover:border-emerald-400'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
title={isVerified ? "Mark as unverified" : "Mark as verified"}
|
title={isVerified ? "Mark as unverified" : "Mark as verified"}
|
||||||
>
|
>
|
||||||
<Check className="w-3.5 h-3.5" strokeWidth={3} />
|
<Check className="w-3.5 h-3.5" strokeWidth={3} />
|
||||||
|
|
@ -441,8 +289,7 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
<label className={`text-xs font-semibold uppercase tracking-wider truncate ${isVerified ? 'text-emerald-700' : 'text-slate-600'}`}>
|
<label className={`text-xs font-semibold uppercase tracking-wider truncate ${isVerified ? 'text-emerald-700' : 'text-slate-600'}`}>
|
||||||
{field.label || field.key || "Unknown Field"}
|
{field.label || field.key || "Unknown Field"}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{/* Status Badge */}
|
|
||||||
{!isVerified && status !== 'VALID' && (
|
{!isVerified && status !== 'VALID' && (
|
||||||
<span className={`flex items-center text-[10px] font-bold px-1.5 py-0.5 rounded ${status === 'INVALID' ? 'bg-red-100 text-red-700' : 'bg-amber-100 text-amber-700'}`}>
|
<span className={`flex items-center text-[10px] font-bold px-1.5 py-0.5 rounded ${status === 'INVALID' ? 'bg-red-100 text-red-700' : 'bg-amber-100 text-amber-700'}`}>
|
||||||
{status === 'INVALID' ? <XCircle className="w-3 h-3 mr-1"/> : <AlertTriangle className="w-3 h-3 mr-1"/>}
|
{status === 'INVALID' ? <XCircle className="w-3 h-3 mr-1"/> : <AlertTriangle className="w-3 h-3 mr-1"/>}
|
||||||
|
|
@ -462,13 +309,7 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
type="text"
|
type="text"
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={(e) => handleUpdate(idx, e.target.value)}
|
onChange={(e) => handleUpdate(idx, e.target.value)}
|
||||||
className={`
|
className={`block w-full rounded-md px-2.5 py-1.5 text-sm font-medium transition-colors border ${isVerified ? 'border-emerald-200 text-emerald-900 bg-emerald-50/50' : 'border-slate-300 text-slate-900 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500'}`}
|
||||||
block w-full rounded-md px-2.5 py-1.5 text-sm font-medium transition-colors border
|
|
||||||
${isVerified
|
|
||||||
? 'border-emerald-200 text-emerald-900 bg-emerald-50/50'
|
|
||||||
: 'border-slate-300 text-slate-900 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
placeholder="Empty"
|
placeholder="Empty"
|
||||||
/>
|
/>
|
||||||
{!isVerified && (
|
{!isVerified && (
|
||||||
|
|
@ -478,16 +319,14 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error/Warning Message */}
|
|
||||||
{(!isVerified && status !== 'VALID' && field.validation?.message) && (
|
{(!isVerified && status !== 'VALID' && field.validation?.message) && (
|
||||||
<p className={`mt-1.5 text-xs ${status === 'INVALID' ? 'text-red-600' : 'text-amber-600'}`}>
|
<p className={`mt-1.5 text-xs ${status === 'INVALID' ? 'text-red-600' : 'text-amber-600'}`}>
|
||||||
{field.validation.message}
|
{field.validation.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Auto-Fix Button */}
|
|
||||||
{(!isVerified && field.validation?.suggestion && status !== 'VALID') && (
|
{(!isVerified && field.validation?.suggestion && status !== 'VALID') && (
|
||||||
<button
|
<button
|
||||||
onClick={() => applySuggestion(idx)}
|
onClick={() => applySuggestion(idx)}
|
||||||
className="mt-2 flex items-center text-xs font-bold text-indigo-600 hover:text-indigo-800 bg-indigo-50 hover:bg-indigo-100 px-2 py-1 rounded transition-colors w-full sm:w-auto"
|
className="mt-2 flex items-center text-xs font-bold text-indigo-600 hover:text-indigo-800 bg-indigo-50 hover:bg-indigo-100 px-2 py-1 rounded transition-colors w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
|
|
@ -495,19 +334,25 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
Accept Fix: "{field.validation.suggestion}"
|
Accept Fix: "{field.validation.suggestion}"
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Source Context Snippet */}
|
|
||||||
{!isVerified && field.sourceContext && (
|
{!isVerified && field.sourceContext && (
|
||||||
<div className="mt-2 p-2 bg-slate-100 rounded text-[11px] text-slate-500 border border-slate-200">
|
<div className="mt-2 p-2 bg-slate-100 rounded text-[11px] text-slate-500 border border-slate-200">
|
||||||
<span className="font-semibold text-slate-700">Source:</span> "{field.sourceContext}"
|
<span className="font-semibold text-slate-700">Source:</span> "{field.sourceContext}"
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Show PDF field key if available */}
|
||||||
|
{field.key && (
|
||||||
|
<div className="mt-1 text-[10px] text-slate-400">
|
||||||
|
PDF Field: {field.key}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{displayedFields.length === 0 && (
|
{displayedFields.length === 0 && (
|
||||||
<div className="text-center py-10 text-slate-400">
|
<div className="text-center py-10 text-slate-400">
|
||||||
<CheckCircle2 className="w-12 h-12 mx-auto mb-2 opacity-20" />
|
<CheckCircle2 className="w-12 h-12 mx-auto mb-2 opacity-20" />
|
||||||
|
|
@ -515,8 +360,8 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Verification Footer */}
|
{/* Footer */}
|
||||||
<div className="p-4 border-t border-slate-200 bg-white">
|
<div className="p-4 border-t border-slate-200 bg-white">
|
||||||
<div className="flex justify-between items-center text-xs font-medium text-slate-500">
|
<div className="flex justify-between items-center text-xs font-medium text-slate-500">
|
||||||
<span>{verifiedCount} of {totalCount} fields verified</span>
|
<span>{verifiedCount} of {totalCount} fields verified</span>
|
||||||
|
|
@ -530,7 +375,6 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Mobile Progress Bar */}
|
|
||||||
<div className="md:hidden mt-2 w-full h-1.5 bg-slate-100 rounded-full overflow-hidden">
|
<div className="md:hidden mt-2 w-full h-1.5 bg-slate-100 rounded-full overflow-hidden">
|
||||||
<div className="h-full bg-emerald-500 transition-all" style={{ width: `${progressPercent}%` }} />
|
<div className="h-full bg-emerald-500 transition-all" style={{ width: `${progressPercent}%` }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { GoogleGenAI, Type, Schema } from "@google/genai";
|
||||||
import { FileData, FormResponse } from "../types";
|
import { FileData, FormResponse } from "../types";
|
||||||
import { PdfFieldInfo } from "./pdfService";
|
import { PdfFieldInfo } from "./pdfService";
|
||||||
import { getApiKey } from "./apiKeyService";
|
import { getApiKey } from "./apiKeyService";
|
||||||
import { detectTemplate, getExpectedFields } from "./latexService";
|
|
||||||
|
|
||||||
const getAI = () => {
|
const getAI = () => {
|
||||||
const apiKey = getApiKey();
|
const apiKey = getApiKey();
|
||||||
|
|
@ -76,98 +75,12 @@ const responseSchema: Schema = {
|
||||||
required: ["fields", "summary"]
|
required: ["fields", "summary"]
|
||||||
};
|
};
|
||||||
|
|
||||||
// G2210-11 specific field definitions for better extraction
|
|
||||||
const G2210_FIELDS = `
|
|
||||||
REQUIRED FIELDS FOR G2210-11 (Ärztlicher Befundbericht):
|
|
||||||
Extract ALL of the following fields from the source document:
|
|
||||||
|
|
||||||
PATIENT DATA:
|
|
||||||
- Versicherungsnummer (e.g., "12 345678 A 123")
|
|
||||||
- ABT.-Nr. (Aktenzeichen/Abteilungsnummer)
|
|
||||||
- Name, Vorname (Full name: "Nachname, Vorname")
|
|
||||||
- Geburtsdatum (format: DD.MM.YYYY)
|
|
||||||
- Geschlecht (männlich/weiblich/divers)
|
|
||||||
- Straße, Hausnummer
|
|
||||||
- PLZ
|
|
||||||
- Ort
|
|
||||||
- Telefon
|
|
||||||
- Krankenkasse
|
|
||||||
|
|
||||||
EMPLOYMENT:
|
|
||||||
- Derzeitige Tätigkeit (Beruf)
|
|
||||||
- Arbeitgeber
|
|
||||||
- Arbeitsunfähig seit (date: DD.MM.YYYY)
|
|
||||||
- Letzte Arbeitsaufnahme
|
|
||||||
|
|
||||||
DIAGNOSES (up to 6, with ICD-10 codes):
|
|
||||||
- Diagnose 1 + Diagnose 1 ICD
|
|
||||||
- Diagnose 2 + Diagnose 2 ICD
|
|
||||||
- Diagnose 3 + Diagnose 3 ICD
|
|
||||||
- Diagnose 4 + Diagnose 4 ICD
|
|
||||||
- Diagnose 5 + Diagnose 5 ICD
|
|
||||||
- Diagnose 6 + Diagnose 6 ICD
|
|
||||||
|
|
||||||
ANAMNESIS:
|
|
||||||
- Anamnese/Beschwerden (patient symptoms and history)
|
|
||||||
- Krankheitsverlauf (disease progression, previous treatments)
|
|
||||||
- Körperlicher Befund (physical examination findings)
|
|
||||||
|
|
||||||
FUNCTIONAL LIMITATIONS (mark as "keine", "gering", or "erheblich"):
|
|
||||||
- Mobilität keine/gering/erheblich
|
|
||||||
- Selbstversorgung keine/gering/erheblich
|
|
||||||
- Haushaltsführung keine/gering/erheblich
|
|
||||||
- Erwerbstätigkeit keine/gering/erheblich
|
|
||||||
- Kommunikation keine/gering/erheblich
|
|
||||||
- Psychische Belastbarkeit keine/gering/erheblich
|
|
||||||
- Beeinträchtigungen Erläuterung
|
|
||||||
|
|
||||||
MEDICATION (up to 5):
|
|
||||||
- Medikament 1 + Medikament 1 Dosis + Medikament 1 Seit
|
|
||||||
- Medikament 2 + Medikament 2 Dosis + Medikament 2 Seit
|
|
||||||
- Medikament 3 + Medikament 3 Dosis + Medikament 3 Seit
|
|
||||||
- Medikament 4 + Medikament 4 Dosis + Medikament 4 Seit
|
|
||||||
- Medikament 5 + Medikament 5 Dosis + Medikament 5 Seit
|
|
||||||
- Physikalische Therapie
|
|
||||||
|
|
||||||
PREVIOUS REHABILITATION:
|
|
||||||
- Reha 1 Zeitraum + Reha 1 Einrichtung + Reha 1 Erfolg
|
|
||||||
- Reha 2 Zeitraum + Reha 2 Einrichtung + Reha 2 Erfolg
|
|
||||||
|
|
||||||
ASSESSMENT:
|
|
||||||
- Leistungsvermögen (vollschichtig/3-6 Stunden/unter 3 Stunden)
|
|
||||||
- Rehabilitationsbedürftigkeit (reasoning for rehab need)
|
|
||||||
- Rehabilitationsziel
|
|
||||||
- Rehabilitationsform (stationär/ambulant/ganztägig ambulant)
|
|
||||||
- Reha Einrichtung Empfehlung
|
|
||||||
|
|
||||||
TRAVEL:
|
|
||||||
- Reisefähig (ja/nein)
|
|
||||||
- Reisefähig Begründung (if no)
|
|
||||||
- Begleitperson (ja/nein)
|
|
||||||
|
|
||||||
ADDITIONAL:
|
|
||||||
- Ergänzende Angaben
|
|
||||||
|
|
||||||
DOCTOR INFORMATION:
|
|
||||||
- Arzt Name
|
|
||||||
- Facharztbezeichnung
|
|
||||||
- Praxis Anschrift
|
|
||||||
- Praxis Telefon
|
|
||||||
- BSNR
|
|
||||||
- LANR
|
|
||||||
- Unterschrift Datum
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const processDocuments = async (
|
export const processDocuments = async (
|
||||||
blankForm: FileData,
|
blankForm: FileData,
|
||||||
sourceDocument: FileData,
|
sourceDocument: FileData,
|
||||||
pdfFields: PdfFieldInfo[] = []
|
pdfFields: PdfFieldInfo[] = []
|
||||||
): Promise<FormResponse> => {
|
): Promise<FormResponse> => {
|
||||||
|
|
||||||
// Detect if we have a known template
|
|
||||||
const detectedTemplate = detectTemplate(blankForm.file?.name || '');
|
|
||||||
const expectedFields = detectedTemplate ? getExpectedFields(detectedTemplate) : [];
|
|
||||||
|
|
||||||
const formPart = {
|
const formPart = {
|
||||||
inlineData: {
|
inlineData: {
|
||||||
data: blankForm.base64,
|
data: blankForm.base64,
|
||||||
|
|
@ -183,73 +96,53 @@ export const processDocuments = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
let systemPrompt = `
|
let systemPrompt = `
|
||||||
ROLE: Intelligent Document Processing AI (Verification Expert).
|
ROLE: Intelligent Document Processing AI.
|
||||||
TASK: Extract data from the SOURCE DOCUMENT and map it to the BLANK TARGET FORM.
|
TASK: Extract data from the SOURCE DOCUMENT and fill the BLANK TARGET FORM.
|
||||||
|
|
||||||
CRITICAL INSTRUCTION: You must verify every extraction. If a value is ambiguous, plausibility is low, or you are guessing, set validation.status to 'WARNING' and explain why in validation.message.
|
CRITICAL: You must verify every extraction. If uncertain, set validation.status to 'WARNING'.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Add template-specific instructions
|
// PRIORITY 1: If PDF has fillable fields, USE THEM - this is the simplest and best approach
|
||||||
if (detectedTemplate === 'G2210-11') {
|
if (pdfFields.length > 0) {
|
||||||
systemPrompt += `
|
const fieldList = pdfFields.map(f => `"${f.name}" (${f.type})`).join("\n- ");
|
||||||
DETECTED FORM: G2210-11 (Ärztlicher Befundbericht der DRV Westfalen)
|
|
||||||
|
|
||||||
${G2210_FIELDS}
|
|
||||||
|
|
||||||
IMPORTANT INSTRUCTIONS:
|
|
||||||
1. Extract ALL fields listed above, even if they are empty in the source.
|
|
||||||
2. Use the EXACT label names as listed above for each field.
|
|
||||||
3. For multi-value fields like diagnoses and medications, create separate field entries.
|
|
||||||
4. For checkbox fields (Mobilität, Selbstversorgung, etc.), return separate fields for each option.
|
|
||||||
Example: If mobility is "erheblich", return:
|
|
||||||
- "Mobilität keine" with value ""
|
|
||||||
- "Mobilität gering" with value ""
|
|
||||||
- "Mobilität erheblich" with value "true"
|
|
||||||
5. ICD-10 codes must be in standard format (e.g., "M54.5", "F32.1")
|
|
||||||
6. Dates must be in DD.MM.YYYY format.
|
|
||||||
7. For Leistungsvermögen, return separate checkbox fields:
|
|
||||||
- "Leistungsvermögen vollschichtig" (true/false)
|
|
||||||
- "Leistungsvermögen 3-6 Stunden" (true/false)
|
|
||||||
- "Leistungsvermögen unter 3 Stunden" (true/false)
|
|
||||||
`;
|
|
||||||
} else if (pdfFields.length > 0) {
|
|
||||||
const fieldList = pdfFields.map(f => `"${f.name}" (${f.type})`).join(", ");
|
|
||||||
systemPrompt += `
|
systemPrompt += `
|
||||||
MODE: FILLABLE PDF (AcroForm).
|
MODE: FILLABLE PDF (AcroForm).
|
||||||
The target form has specific embedded fields.
|
|
||||||
Map extracted data to these exact field IDs: [${fieldList}].
|
The target PDF has these EXACT fillable fields:
|
||||||
Return the 'key' property matching the field ID.
|
- ${fieldList}
|
||||||
`;
|
|
||||||
} else if (expectedFields.length > 0) {
|
CRITICAL INSTRUCTIONS:
|
||||||
systemPrompt += `
|
1. For EACH field listed above, extract the corresponding value from the SOURCE DOCUMENT.
|
||||||
MODE: TEMPLATE-BASED EXTRACTION.
|
2. Return the 'key' property with the EXACT field name from the list above.
|
||||||
Extract the following specific fields: [${expectedFields.join(", ")}].
|
3. The 'label' should be a human-readable description.
|
||||||
Use these exact label names in your response.
|
4. For checkboxes: use value "true" to check, "false" to uncheck.
|
||||||
|
5. For text fields: use the extracted text value.
|
||||||
|
|
||||||
|
You MUST return a field entry for each PDF field listed above.
|
||||||
|
The 'key' MUST match exactly one of the field names I provided.
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
|
// FALLBACK: Visual overlay mode for non-fillable PDFs
|
||||||
systemPrompt += `
|
systemPrompt += `
|
||||||
MODE: VISUAL FILLING (Flat/XFA/Scan).
|
MODE: VISUAL FILLING (Flat PDF/Scan).
|
||||||
The target form DOES NOT have accessible digital fields.
|
The target form does NOT have digital form fields.
|
||||||
You must VISUALLY locate where the text should be written.
|
|
||||||
|
|
||||||
For every field you identify on the TARGET FORM:
|
For every field you identify on the TARGET FORM:
|
||||||
1. Extract the corresponding value from the SOURCE DOCUMENT.
|
1. Extract the corresponding value from the SOURCE DOCUMENT.
|
||||||
2. Estimate the VISUAL COORDINATES [pageIndex, x, y] where the text should start.
|
2. Estimate VISUAL COORDINATES [pageIndex, x, y] where the text should be written.
|
||||||
- 'x' and 'y' are on a scale of 0 to 1000.
|
- x and y are on a scale of 0 to 1000.
|
||||||
- (0,0) is the top-left corner of the page.
|
- (0,0) is the top-left corner.
|
||||||
- (1000,1000) is the bottom-right corner.
|
- (1000,1000) is the bottom-right corner.
|
||||||
- Align text slightly above lines or inside boxes.
|
|
||||||
|
|
||||||
For checkboxes: If true/yes, the value should be "X" placed inside the box.
|
For checkboxes: value should be "X" if checked.
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
systemPrompt += `
|
systemPrompt += `
|
||||||
VALIDATION RULES:
|
VALIDATION RULES:
|
||||||
1. Dates: Ensure format matches the form (e.g. DD.MM.YYYY).
|
1. Dates: German format DD.MM.YYYY
|
||||||
2. Checkboxes: Only mark if explicitly supported by source.
|
2. Missing Data: Leave 'value' empty, don't hallucinate.
|
||||||
3. Missing Data: If a field is not found in source, leave 'value' empty and set status 'VALID'. Do not hallucinate.
|
3. Source Context: Include the exact text snippet from source that justifies the extraction.
|
||||||
4. Source Context: Always populate 'sourceContext' with the exact text snippet from the source document that justifies your extraction.
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue