feat: Add LaTeX template-based form filling for G2210-11

Replace unreliable visual overlay mode with precise LaTeX templates:

- Add LaTeX template for G2210-11 (Ärztlicher Befundbericht der WAG)
- Create Python Flask backend for LaTeX compilation (latex_service.py, server.py)
- Add frontend latexService.ts for API communication
- Update ReviewPanel with LaTeX mode toggle and preview
- Enhance Gemini prompts with G2210-11 specific field extraction
- Add Dockerfile with TeX Live for Railway deployment
- Update railway.toml to use Docker builder

The LaTeX approach ensures accurate field placement and proper
formatting for German medical/insurance forms.

https://claude.ai/code/session_016pQhdznHZ74Fpkvwr3cLBq
This commit is contained in:
Claude 2026-01-29 18:29:22 +00:00
parent adf8f0240e
commit 19e96ef59b
No known key found for this signature in database
10 changed files with 1557 additions and 32 deletions

View file

@ -1,7 +1,8 @@
import React, { useState, useEffect, useMemo } from 'react';
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { ExtractedField, FileData } from '../types';
import { Check, Edit2, Download, RefreshCw, FileText, AlertTriangle, XCircle, ArrowRight, PenTool, CheckCircle2, Circle } from 'lucide-react';
import { Check, Edit2, Download, RefreshCw, FileText, AlertTriangle, XCircle, ArrowRight, PenTool, CheckCircle2, Circle, FileCode, Loader2 } from 'lucide-react';
import { createFilledPdf } from '../services/pdfService';
import { generateLatexPdf, isLatexServiceAvailable, detectTemplate, base64ToBlob } from '../services/latexService';
import { jsPDF } from "jspdf";
interface ReviewPanelProps {
@ -13,18 +14,40 @@ interface ReviewPanelProps {
onReset: () => void;
}
export const ReviewPanel: React.FC<ReviewPanelProps> = ({
fields: initialFields,
formFile,
type PdfMode = 'overlay' | 'fillable' | 'latex';
export const ReviewPanel: React.FC<ReviewPanelProps> = ({
fields: initialFields,
formFile,
sourceFile,
summary,
isFillablePdf,
onReset
onReset
}) => {
const [fields, setFields] = useState(initialFields);
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;
@ -35,12 +58,46 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
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
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, isFillablePdf);
const filledPdfBytes = await createFilledPdf(formFile.base64, fields, pdfMode === 'fillable');
const blob = new Blob([filledPdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
setPreviewUrl(url);
@ -52,11 +109,11 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
setPreviewUrl(formFile.previewUrl);
}
};
// Debounce slightly to avoid rapid updates on typing
const timer = setTimeout(updatePreview, 600);
return () => clearTimeout(timer);
}, [fields, isFillablePdf, formFile.base64, formFile.type]);
}, [fields, pdfMode, formFile.base64, formFile.type, latexPdfBase64, generateLatexPreview]);
const handleUpdate = (index: number, newValue: string) => {
const newFields = [...fields];
@ -87,6 +144,30 @@ 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;
@ -106,6 +187,12 @@ 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) => {
@ -154,11 +241,16 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
Start Over
</button>
<button
<button
onClick={handleDownload}
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`}
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'}`}
>
<Download className="w-4 h-4 mr-2" />
{isGenerating ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Download className="w-4 h-4 mr-2" />
)}
Download PDF
</button>
</div>
@ -170,29 +262,73 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
<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>
{!isFillablePdf && (
{pdfMode === 'latex' && (
<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
</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">
<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
</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">
{previewUrl ? (
formFile.type === 'application/pdf' ? (
<iframe
src={previewUrl}
{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' ? (
<iframe
src={previewUrl}
title="Form PDF Preview"
className="w-full h-full border-none"
/>
) : (
<div className="w-full h-full overflow-auto flex items-center justify-center p-4">
<img
src={previewUrl}
alt="Form Document"
className="max-w-full shadow-lg border border-slate-700"
<img
src={previewUrl}
alt="Form Document"
className="max-w-full shadow-lg border border-slate-700"
/>
</div>
)
@ -230,12 +366,24 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
</div>
</div>
{!isFillablePdf && (
{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>
)}