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:
Kenearos 2026-01-29 20:31:17 +01:00 committed by GitHub
commit dac7677fcf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 104 additions and 367 deletions

View file

@ -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,26 +25,6 @@ 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;
@ -58,50 +35,18 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
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' }
}; };
@ -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));
@ -220,7 +129,6 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
</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
@ -243,81 +151,37 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
<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,9 +205,8 @@ 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>
@ -366,27 +229,23 @@ 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 */}
@ -412,25 +271,14 @@ 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} />
@ -442,7 +290,6 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
{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,14 +319,12 @@ 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)}
@ -496,12 +335,18 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
</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>
@ -516,7 +361,7 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
)} )}
</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>

View file

@ -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 {