Merge pull request #6 from Kenearos/claude/fix-latex-form-filling-GbQtu
fix: Simplify PDF filling - use fillable fields directly
This commit is contained in:
commit
dac7677fcf
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 { 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 { generateLatexPdf, isLatexServiceAvailable, detectTemplate, base64ToBlob } from '../services/latexService';
|
||||
import { jsPDF } from "jspdf";
|
||||
|
||||
interface ReviewPanelProps {
|
||||
|
|
@ -14,8 +13,6 @@ interface ReviewPanelProps {
|
|||
onReset: () => void;
|
||||
}
|
||||
|
||||
type PdfMode = 'overlay' | 'fillable' | 'latex';
|
||||
|
||||
export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||
fields: initialFields,
|
||||
formFile,
|
||||
|
|
@ -28,80 +25,28 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
|||
const [activeField, setActiveField] = useState<number | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
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
|
||||
const verifiedCount = fields.filter(f => f.isVerified).length;
|
||||
const totalCount = fields.length;
|
||||
const progressPercent = Math.round((verifiedCount / totalCount) * 100);
|
||||
|
||||
const fieldsRequiresAttention = useMemo(() =>
|
||||
fields.filter(f => f.validation?.status !== 'VALID'),
|
||||
|
||||
const fieldsRequiresAttention = useMemo(() =>
|
||||
fields.filter(f => f.validation?.status !== 'VALID'),
|
||||
[fields]);
|
||||
|
||||
// Generate LaTeX PDF
|
||||
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
|
||||
// Generate preview - fills the original PDF directly
|
||||
useEffect(() => {
|
||||
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') {
|
||||
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 url = URL.createObjectURL(blob);
|
||||
setPreviewUrl(url);
|
||||
return () => URL.revokeObjectURL(url);
|
||||
setPreviewUrl(prev => {
|
||||
if (prev) URL.revokeObjectURL(prev);
|
||||
return url;
|
||||
});
|
||||
} catch (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, 600);
|
||||
const timer = setTimeout(updatePreview, 500);
|
||||
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 newFields = [...fields];
|
||||
newFields[index] = {
|
||||
...newFields[index],
|
||||
newFields[index] = {
|
||||
...newFields[index],
|
||||
value: newValue,
|
||||
// Auto-verify when manually edited
|
||||
isVerified: true,
|
||||
validation: { ...newFields[index].validation!, status: 'VALID', message: 'Manually verified' }
|
||||
};
|
||||
|
|
@ -129,9 +72,9 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
|||
|
||||
const toggleVerify = (index: number) => {
|
||||
const newFields = [...fields];
|
||||
newFields[index] = {
|
||||
...newFields[index],
|
||||
isVerified: !newFields[index].isVerified
|
||||
newFields[index] = {
|
||||
...newFields[index],
|
||||
isVerified: !newFields[index].isVerified
|
||||
};
|
||||
setFields(newFields);
|
||||
};
|
||||
|
|
@ -144,30 +87,6 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
|||
};
|
||||
|
||||
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) {
|
||||
const a = document.createElement('a');
|
||||
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
|
||||
const displayedFields = fields.map((f, i) => ({ ...f, originalIndex: i }))
|
||||
.sort((a, b) => {
|
||||
// Priority 1: Attention needed
|
||||
const aNeedsAttn = a.validation?.status !== 'VALID';
|
||||
const bNeedsAttn = b.validation?.status !== 'VALID';
|
||||
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;
|
||||
|
||||
return a.originalIndex - b.originalIndex;
|
||||
})
|
||||
.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>
|
||||
<p className="text-slate-500 text-sm mt-1">{summary}</p>
|
||||
</div>
|
||||
|
||||
|
||||
<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">
|
||||
<span className="text-xs font-semibold text-slate-600 mb-1">
|
||||
{verifiedCount} / {totalCount} Verified
|
||||
</span>
|
||||
<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"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Start Over
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={isGenerating}
|
||||
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'}`}
|
||||
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"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Download PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="p-3 bg-slate-800 border-b border-slate-700 flex justify-between items-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<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">
|
||||
<FileCode className="w-3 h-3 mr-1" />
|
||||
LaTeX Template Mode
|
||||
<FileCheck className="w-3 h-3 mr-1" />
|
||||
Fillable PDF
|
||||
</span>
|
||||
)}
|
||||
{pdfMode === 'overlay' && (
|
||||
<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">
|
||||
) : (
|
||||
<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">
|
||||
<PenTool className="w-3 h-3 mr-1" />
|
||||
Visual Overlay Mode
|
||||
</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
|
||||
Visual Overlay
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* 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>
|
||||
<span className="text-xs text-slate-400">{formFile.file.name}</span>
|
||||
</div>
|
||||
<div className="flex-1 bg-slate-900 relative">
|
||||
{isGenerating && pdfMode === 'latex' ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-emerald-400">
|
||||
<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' ? (
|
||||
{previewUrl ? (
|
||||
formFile.type === 'application/pdf' ? (
|
||||
<iframe
|
||||
src={previewUrl}
|
||||
title="Form PDF Preview"
|
||||
|
|
@ -341,21 +205,20 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
|||
</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">
|
||||
{/* List Header */}
|
||||
<div className="p-4 border-b border-slate-100 bg-slate-50/50">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="font-semibold text-slate-800">Field Verification</h3>
|
||||
<div className="flex space-x-2 text-xs">
|
||||
<button
|
||||
<button
|
||||
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'}`}
|
||||
>
|
||||
All Fields ({totalCount})
|
||||
</button>
|
||||
{fieldsRequiresAttention.length > 0 && (
|
||||
<button
|
||||
<button
|
||||
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'}`}
|
||||
>
|
||||
|
|
@ -365,37 +228,33 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{pdfMode === 'latex' && detectedTemplate && (
|
||||
<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" />
|
||||
<div>
|
||||
<p className="font-semibold">LaTeX Template Mode: {detectedTemplate}</p>
|
||||
<p>Using precise LaTeX template. All fields will be positioned correctly in the generated PDF.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{pdfMode === 'overlay' && (
|
||||
<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-indigo-600 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-semibold">Visual Overlay Mode</p>
|
||||
<p>AI is visually estimating field positions. Please verify text alignment in the preview.</p>
|
||||
{latexAvailable && detectedTemplate && (
|
||||
<p className="mt-1 text-emerald-700 font-medium">Tip: LaTeX template available! Click "Use LaTeX" for better results.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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">
|
||||
<FileCheck className="w-4 h-4 flex-shrink-0 text-emerald-600 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-semibold">Fillable PDF Mode</p>
|
||||
<p>Fields are filled directly into the original PDF form.</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start space-x-2 bg-amber-50 text-amber-800 p-3 rounded-md border border-amber-100 text-xs">
|
||||
<PenTool className="w-4 h-4 flex-shrink-0 text-amber-600 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-semibold">Visual Overlay Mode</p>
|
||||
<p>This PDF has no fillable fields. Text is overlaid at estimated positions.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Fields List */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-slate-50/30">
|
||||
{displayedFields.map((field) => {
|
||||
const idx = field.originalIndex;
|
||||
const status = field.validation?.status || 'VALID';
|
||||
const isVerified = field.isVerified;
|
||||
|
||||
|
||||
let statusBorder = isVerified ? "border-emerald-200" : "border-slate-200";
|
||||
let statusBg = isVerified ? "bg-emerald-50/30" : "bg-white";
|
||||
|
||||
|
|
@ -410,27 +269,16 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
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}
|
||||
`}
|
||||
<div
|
||||
key={idx}
|
||||
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}`}
|
||||
onFocus={() => setActiveField(idx)}
|
||||
onBlur={() => setActiveField(null)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Verification Checkbox */}
|
||||
<button
|
||||
onClick={() => toggleVerify(idx)}
|
||||
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'
|
||||
}
|
||||
`}
|
||||
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'}`}
|
||||
title={isVerified ? "Mark as unverified" : "Mark as verified"}
|
||||
>
|
||||
<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'}`}>
|
||||
{field.label || field.key || "Unknown Field"}
|
||||
</label>
|
||||
|
||||
{/* Status Badge */}
|
||||
|
||||
{!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'}`}>
|
||||
{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"
|
||||
value={field.value}
|
||||
onChange={(e) => handleUpdate(idx, e.target.value)}
|
||||
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'
|
||||
}
|
||||
`}
|
||||
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'}`}
|
||||
placeholder="Empty"
|
||||
/>
|
||||
{!isVerified && (
|
||||
|
|
@ -478,16 +319,14 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Error/Warning Message */}
|
||||
{(!isVerified && status !== 'VALID' && field.validation?.message) && (
|
||||
<p className={`mt-1.5 text-xs ${status === 'INVALID' ? 'text-red-600' : 'text-amber-600'}`}>
|
||||
{field.validation.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Auto-Fix Button */}
|
||||
{(!isVerified && field.validation?.suggestion && status !== 'VALID') && (
|
||||
<button
|
||||
<button
|
||||
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"
|
||||
>
|
||||
|
|
@ -495,19 +334,25 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
|||
Accept Fix: "{field.validation.suggestion}"
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Source Context Snippet */}
|
||||
|
||||
{!isVerified && field.sourceContext && (
|
||||
<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}"
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
|
||||
|
||||
{displayedFields.length === 0 && (
|
||||
<div className="text-center py-10 text-slate-400">
|
||||
<CheckCircle2 className="w-12 h-12 mx-auto mb-2 opacity-20" />
|
||||
|
|
@ -515,8 +360,8 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Verification Footer */}
|
||||
|
||||
{/* Footer */}
|
||||
<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">
|
||||
<span>{verifiedCount} of {totalCount} fields verified</span>
|
||||
|
|
@ -530,7 +375,6 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
|||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Mobile Progress Bar */}
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { GoogleGenAI, Type, Schema } from "@google/genai";
|
|||
import { FileData, FormResponse } from "../types";
|
||||
import { PdfFieldInfo } from "./pdfService";
|
||||
import { getApiKey } from "./apiKeyService";
|
||||
import { detectTemplate, getExpectedFields } from "./latexService";
|
||||
|
||||
const getAI = () => {
|
||||
const apiKey = getApiKey();
|
||||
|
|
@ -76,98 +75,12 @@ const responseSchema: Schema = {
|
|||
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 (
|
||||
blankForm: FileData,
|
||||
sourceDocument: FileData,
|
||||
pdfFields: PdfFieldInfo[] = []
|
||||
): Promise<FormResponse> => {
|
||||
|
||||
// Detect if we have a known template
|
||||
const detectedTemplate = detectTemplate(blankForm.file?.name || '');
|
||||
const expectedFields = detectedTemplate ? getExpectedFields(detectedTemplate) : [];
|
||||
|
||||
const formPart = {
|
||||
inlineData: {
|
||||
data: blankForm.base64,
|
||||
|
|
@ -183,73 +96,53 @@ export const processDocuments = async (
|
|||
};
|
||||
|
||||
let systemPrompt = `
|
||||
ROLE: Intelligent Document Processing AI (Verification Expert).
|
||||
TASK: Extract data from the SOURCE DOCUMENT and map it to the BLANK TARGET FORM.
|
||||
ROLE: Intelligent Document Processing AI.
|
||||
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
|
||||
if (detectedTemplate === 'G2210-11') {
|
||||
systemPrompt += `
|
||||
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(", ");
|
||||
// PRIORITY 1: If PDF has fillable fields, USE THEM - this is the simplest and best approach
|
||||
if (pdfFields.length > 0) {
|
||||
const fieldList = pdfFields.map(f => `"${f.name}" (${f.type})`).join("\n- ");
|
||||
systemPrompt += `
|
||||
MODE: FILLABLE PDF (AcroForm).
|
||||
The target form has specific embedded fields.
|
||||
Map extracted data to these exact field IDs: [${fieldList}].
|
||||
Return the 'key' property matching the field ID.
|
||||
`;
|
||||
} else if (expectedFields.length > 0) {
|
||||
systemPrompt += `
|
||||
MODE: TEMPLATE-BASED EXTRACTION.
|
||||
Extract the following specific fields: [${expectedFields.join(", ")}].
|
||||
Use these exact label names in your response.
|
||||
|
||||
The target PDF has these EXACT fillable fields:
|
||||
- ${fieldList}
|
||||
|
||||
CRITICAL INSTRUCTIONS:
|
||||
1. For EACH field listed above, extract the corresponding value from the SOURCE DOCUMENT.
|
||||
2. Return the 'key' property with the EXACT field name from the list above.
|
||||
3. The 'label' should be a human-readable description.
|
||||
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 {
|
||||
// FALLBACK: Visual overlay mode for non-fillable PDFs
|
||||
systemPrompt += `
|
||||
MODE: VISUAL FILLING (Flat/XFA/Scan).
|
||||
The target form DOES NOT have accessible digital fields.
|
||||
You must VISUALLY locate where the text should be written.
|
||||
MODE: VISUAL FILLING (Flat PDF/Scan).
|
||||
The target form does NOT have digital form fields.
|
||||
|
||||
For every field you identify on the TARGET FORM:
|
||||
1. Extract the corresponding value from the SOURCE DOCUMENT.
|
||||
2. Estimate the VISUAL COORDINATES [pageIndex, x, y] where the text should start.
|
||||
- 'x' and 'y' are on a scale of 0 to 1000.
|
||||
- (0,0) is the top-left corner of the page.
|
||||
2. Estimate VISUAL COORDINATES [pageIndex, x, y] where the text should be written.
|
||||
- x and y are on a scale of 0 to 1000.
|
||||
- (0,0) is the top-left 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 += `
|
||||
VALIDATION RULES:
|
||||
1. Dates: Ensure format matches the form (e.g. DD.MM.YYYY).
|
||||
2. Checkboxes: Only mark if explicitly supported by source.
|
||||
3. Missing Data: If a field is not found in source, leave 'value' empty and set status 'VALID'. Do not hallucinate.
|
||||
4. Source Context: Always populate 'sourceContext' with the exact text snippet from the source document that justifies your extraction.
|
||||
1. Dates: German format DD.MM.YYYY
|
||||
2. Missing Data: Leave 'value' empty, don't hallucinate.
|
||||
3. Source Context: Include the exact text snippet from source that justifies the extraction.
|
||||
`;
|
||||
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue