Merge pull request #5 from Kenearos/claude/fix-latex-form-filling-GbQtu

Add LaTeX template support for form generation
This commit is contained in:
Kenearos 2026-01-29 19:47:11 +01:00 committed by GitHub
commit e84752fff5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 1557 additions and 32 deletions

67
Dockerfile Normal file
View file

@ -0,0 +1,67 @@
# Multi-stage Dockerfile for AutoForm AI with LaTeX support
# Stage 1: Build Frontend
FROM node:20-alpine AS frontend-builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy source files
COPY . .
# Build the frontend
RUN npm run build
# Stage 2: Production image with Python and LaTeX
FROM python:3.11-slim
# Install TeX Live (minimal installation for form generation)
RUN apt-get update && apt-get install -y --no-install-recommends \
texlive-latex-base \
texlive-latex-recommended \
texlive-latex-extra \
texlive-fonts-recommended \
texlive-lang-german \
texlive-plain-generic \
nodejs \
npm \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy Python requirements and install
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Install serve for static file serving
RUN npm install -g serve
# Copy the built frontend
COPY --from=frontend-builder /app/dist ./dist
# Copy Python backend and LaTeX templates
COPY latex_service.py ./
COPY server.py ./
COPY templates ./templates
# Create startup script
RUN echo '#!/bin/bash\n\
python server.py &\n\
serve dist -l ${PORT:-3000}\n\
' > /app/start.sh && chmod +x /app/start.sh
# Expose ports
EXPOSE 3000 5000
# Environment variables
ENV PORT=3000
ENV FLASK_DEBUG=false
ENV VITE_LATEX_API_URL=http://localhost:5000
# Start both services
CMD ["/app/start.sh"]

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 { 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 { createFilledPdf } from '../services/pdfService';
import { generateLatexPdf, isLatexServiceAvailable, detectTemplate, base64ToBlob } from '../services/latexService';
import { jsPDF } from "jspdf"; import { jsPDF } from "jspdf";
interface ReviewPanelProps { interface ReviewPanelProps {
@ -13,18 +14,40 @@ interface ReviewPanelProps {
onReset: () => void; onReset: () => void;
} }
export const ReviewPanel: React.FC<ReviewPanelProps> = ({ type PdfMode = 'overlay' | 'fillable' | 'latex';
fields: initialFields,
formFile, export const ReviewPanel: React.FC<ReviewPanelProps> = ({
fields: initialFields,
formFile,
sourceFile, sourceFile,
summary, summary,
isFillablePdf, isFillablePdf,
onReset onReset
}) => { }) => {
const [fields, setFields] = useState(initialFields); const [fields, setFields] = useState(initialFields);
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;
@ -35,12 +58,46 @@ 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
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
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, isFillablePdf); const filledPdfBytes = await createFilledPdf(formFile.base64, fields, pdfMode === 'fillable');
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(url);
@ -52,11 +109,11 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
setPreviewUrl(formFile.previewUrl); setPreviewUrl(formFile.previewUrl);
} }
}; };
// Debounce slightly to avoid rapid updates on typing // Debounce slightly to avoid rapid updates on typing
const timer = setTimeout(updatePreview, 600); const timer = setTimeout(updatePreview, 600);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [fields, isFillablePdf, formFile.base64, formFile.type]); }, [fields, pdfMode, formFile.base64, formFile.type, latexPdfBase64, generateLatexPreview]);
const handleUpdate = (index: number, newValue: string) => { const handleUpdate = (index: number, newValue: string) => {
const newFields = [...fields]; const newFields = [...fields];
@ -87,6 +144,30 @@ 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;
@ -106,6 +187,12 @@ 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) => {
@ -154,11 +241,16 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
Start Over Start Over
</button> </button>
<button <button
onClick={handleDownload} 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 Download PDF
</button> </button>
</div> </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="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>
{!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"> <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 Mode
</span> </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> </div>
<span className="text-xs text-slate-400">{formFile.file.name}</span>
</div> </div>
<div className="flex-1 bg-slate-900 relative"> <div className="flex-1 bg-slate-900 relative">
{previewUrl ? ( {isGenerating && pdfMode === 'latex' ? (
formFile.type === 'application/pdf' ? ( <div className="flex flex-col items-center justify-center h-full text-emerald-400">
<iframe <Loader2 className="w-16 h-16 mb-4 animate-spin" />
src={previewUrl} <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" title="Form PDF Preview"
className="w-full h-full border-none" className="w-full h-full border-none"
/> />
) : ( ) : (
<div className="w-full h-full overflow-auto flex items-center justify-center p-4"> <div className="w-full h-full overflow-auto flex items-center justify-center p-4">
<img <img
src={previewUrl} src={previewUrl}
alt="Form Document" alt="Form Document"
className="max-w-full shadow-lg border border-slate-700" className="max-w-full shadow-lg border border-slate-700"
/> />
</div> </div>
) )
@ -230,12 +366,24 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
</div> </div>
</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"> <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" /> <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>AI is visually estimating field positions. Please verify text alignment in the preview.</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>
</div> </div>
)} )}

249
latex_service.py Normal file
View file

@ -0,0 +1,249 @@
#!/usr/bin/env python3
"""
LaTeX Form Generation Service
This service generates filled PDF forms using LaTeX templates.
It takes extracted field data and compiles a LaTeX template into a PDF.
"""
import json
import os
import subprocess
import tempfile
import base64
import sys
import shutil
from pathlib import Path
from typing import Dict, Any, Optional
# Template directory
TEMPLATE_DIR = Path(__file__).parent / "templates"
def escape_latex(text: str) -> str:
"""Escape special LaTeX characters in text."""
if not text:
return ""
# LaTeX special characters that need escaping
replacements = [
('\\', r'\textbackslash{}'),
('&', r'\&'),
('%', r'\%'),
('$', r'\$'),
('#', r'\#'),
('_', r'\_'),
('{', r'\{'),
('}', r'\}'),
('~', r'\textasciitilde{}'),
('^', r'\textasciicircum{}'),
]
result = text
for old, new in replacements:
result = result.replace(old, new)
return result
def checkbox(value: str) -> str:
"""Return LaTeX checkbox symbol based on value."""
if not value:
return r'$\square$'
val_lower = value.lower().strip()
if val_lower in ('true', 'yes', 'ja', 'x', '1', 'checked'):
return r'$\boxtimes$'
return r'$\square$'
def format_date(date_str: str) -> str:
"""Ensure date is in DD.MM.YYYY format."""
if not date_str:
return ""
# Already in correct format
if len(date_str) == 10 and date_str[2] == '.' and date_str[5] == '.':
return escape_latex(date_str)
# Try to parse ISO format
try:
from datetime import datetime
for fmt in ['%Y-%m-%d', '%d/%m/%Y', '%m/%d/%Y']:
try:
dt = datetime.strptime(date_str, fmt)
return dt.strftime('%d.%m.%Y')
except ValueError:
continue
except:
pass
return escape_latex(date_str)
def load_template(template_name: str) -> str:
"""Load a LaTeX template file."""
template_path = TEMPLATE_DIR / f"{template_name}.tex"
if not template_path.exists():
raise FileNotFoundError(f"Template not found: {template_path}")
return template_path.read_text(encoding='utf-8')
def fill_template(template: str, fields: Dict[str, Any]) -> str:
"""
Fill a LaTeX template with field values.
Fields can be accessed in template as:
- {{field_name}} for escaped text values
- {{field_name|raw}} for raw values (no escaping)
- {{field_name|checkbox}} for checkbox symbols
- {{field_name|date}} for date formatting
"""
result = template
# Process each field
for key, value in fields.items():
value_str = str(value) if value is not None else ""
# Replace with different formatters
# Raw (no escaping)
result = result.replace(f'{{{{{key}|raw}}}}', value_str)
# Checkbox
result = result.replace(f'{{{{{key}|checkbox}}}}', checkbox(value_str))
# Date
result = result.replace(f'{{{{{key}|date}}}}', format_date(value_str))
# Default (escaped)
result = result.replace(f'{{{{{key}}}}}', escape_latex(value_str))
# Clean up any remaining placeholders (unfilled fields)
import re
result = re.sub(r'\{\{[^}]+\}\}', '', result)
return result
def compile_latex(latex_content: str, output_format: str = 'pdf') -> bytes:
"""
Compile LaTeX content to PDF.
Returns the PDF as bytes.
"""
with tempfile.TemporaryDirectory() as tmpdir:
tex_file = Path(tmpdir) / "document.tex"
tex_file.write_text(latex_content, encoding='utf-8')
# Copy any additional files (images, etc.) if needed
# For now, we just compile the main document
# Run pdflatex twice (for references)
for _ in range(2):
result = subprocess.run(
['pdflatex', '-interaction=nonstopmode', '-output-directory', tmpdir, str(tex_file)],
capture_output=True,
text=True,
timeout=60
)
if result.returncode != 0:
# Check for common errors
error_log = Path(tmpdir) / "document.log"
if error_log.exists():
log_content = error_log.read_text(encoding='utf-8', errors='ignore')
# Extract error lines
errors = [line for line in log_content.split('\n') if line.startswith('!')]
if errors:
raise RuntimeError(f"LaTeX compilation failed: {'; '.join(errors[:3])}")
raise RuntimeError(f"LaTeX compilation failed: {result.stderr[:500]}")
pdf_file = Path(tmpdir) / "document.pdf"
if not pdf_file.exists():
raise RuntimeError("PDF file was not created")
return pdf_file.read_bytes()
def generate_form(template_name: str, fields: Dict[str, Any]) -> bytes:
"""
Generate a filled PDF form from a template and field data.
Args:
template_name: Name of the template (without .tex extension)
fields: Dictionary of field names to values
Returns:
PDF content as bytes
"""
template = load_template(template_name)
filled = fill_template(template, fields)
return compile_latex(filled)
def list_templates() -> list:
"""List available templates."""
if not TEMPLATE_DIR.exists():
return []
return [f.stem for f in TEMPLATE_DIR.glob("*.tex")]
# CLI Interface
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description='LaTeX Form Generation Service')
parser.add_argument('command', choices=['generate', 'list', 'preview'],
help='Command to execute')
parser.add_argument('--template', '-t', help='Template name')
parser.add_argument('--fields', '-f', help='JSON string or file path with field data')
parser.add_argument('--output', '-o', help='Output file path')
args = parser.parse_args()
if args.command == 'list':
templates = list_templates()
print(json.dumps(templates))
elif args.command == 'preview':
# Output the filled LaTeX source (for debugging)
if not args.template or not args.fields:
print("Error: --template and --fields required", file=sys.stderr)
sys.exit(1)
if args.fields.startswith('{'):
fields = json.loads(args.fields)
else:
with open(args.fields, 'r') as f:
fields = json.load(f)
template = load_template(args.template)
filled = fill_template(template, fields)
print(filled)
elif args.command == 'generate':
if not args.template or not args.fields:
print("Error: --template and --fields required", file=sys.stderr)
sys.exit(1)
# Parse fields
if args.fields.startswith('{'):
fields = json.loads(args.fields)
else:
with open(args.fields, 'r') as f:
fields = json.load(f)
try:
pdf_bytes = generate_form(args.template, fields)
if args.output:
with open(args.output, 'wb') as f:
f.write(pdf_bytes)
print(f"PDF written to {args.output}", file=sys.stderr)
else:
# Output base64 encoded PDF to stdout
print(base64.b64encode(pdf_bytes).decode('ascii'))
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)

View file

@ -11,7 +11,9 @@
"test": "vitest", "test": "vitest",
"test:ui": "vitest --ui", "test:ui": "vitest --ui",
"test:coverage": "vitest --coverage", "test:coverage": "vitest --coverage",
"test:run": "vitest run" "test:run": "vitest run",
"latex:server": "python3 server.py",
"dev:all": "concurrently \"npm run dev\" \"npm run latex:server\""
}, },
"dependencies": { "dependencies": {
"react-dom": "^19.2.4", "react-dom": "^19.2.4",

View file

@ -1,9 +1,9 @@
[build] [build]
builder = "nixpacks" builder = "dockerfile"
dockerfilePath = "Dockerfile"
[deploy] [deploy]
startCommand = "npm start"
healthcheckPath = "/" healthcheckPath = "/"
healthcheckTimeout = 100 healthcheckTimeout = 300
restartPolicyType = "on_failure" restartPolicyType = "on_failure"
restartPolicyMaxRetries = 3 restartPolicyMaxRetries = 3

View file

@ -1 +1,3 @@
pypdf>=4.0.0 pypdf>=4.0.0
flask>=3.0.0
flask-cors>=4.0.0

337
server.py Normal file
View file

@ -0,0 +1,337 @@
#!/usr/bin/env python3
"""
Flask server for LaTeX form generation.
Provides API endpoints for compiling LaTeX templates with field data.
"""
from flask import Flask, request, jsonify, send_file
from flask_cors import CORS
import base64
import io
import json
import os
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent))
from latex_service import generate_form, list_templates, load_template, fill_template, escape_latex
app = Flask(__name__)
CORS(app) # Enable CORS for frontend access
# Field mapping for G2210-11 template
# Maps extracted field labels to LaTeX template variables
G2210_FIELD_MAPPING = {
# Patient data
"versicherungsnummer": ["versicherungsnummer", "vers.nr.", "vers-nr", "rentenversicherungsnummer", "rvnr"],
"abt_nr": ["abt.-nr.", "abt-nr", "abteilungsnummer", "aktenzeichen"],
"name_vorname": ["name, vorname", "name vorname", "patient", "patientenname", "name des versicherten"],
"geburtsdatum": ["geburtsdatum", "geb.", "geb.datum", "geboren am", "geburtstag"],
"geschlecht": ["geschlecht", "sex", "m/w/d"],
"strasse": ["straße", "strasse", "anschrift", "adresse"],
"plz": ["plz", "postleitzahl"],
"ort": ["ort", "wohnort", "stadt"],
"telefon": ["telefon", "tel.", "tel", "telefonnummer", "rufnummer"],
"krankenkasse": ["krankenkasse", "krankenversicherung", "kk", "versicherung"],
# Employment
"beruf_taetigkeit": ["beruf", "tätigkeit", "derzeitige tätigkeit", "beschäftigung", "arbeit"],
"arbeitgeber": ["arbeitgeber", "firma", "unternehmen"],
"au_seit": ["arbeitsunfähig seit", "au seit", "arbeitsunfähigkeit seit", "krankgeschrieben seit"],
"letzte_arbeit": ["letzte arbeitsaufnahme", "letzter arbeitstag", "zuletzt gearbeitet"],
# Diagnoses
"diagnose_1": ["diagnose 1", "hauptdiagnose", "1. diagnose"],
"diagnose_1_icd": ["icd 1", "icd-10 1", "diagnose 1 icd"],
"diagnose_2": ["diagnose 2", "nebendiagnose 1", "2. diagnose"],
"diagnose_2_icd": ["icd 2", "icd-10 2", "diagnose 2 icd"],
"diagnose_3": ["diagnose 3", "nebendiagnose 2", "3. diagnose"],
"diagnose_3_icd": ["icd 3", "icd-10 3", "diagnose 3 icd"],
"diagnose_4": ["diagnose 4", "nebendiagnose 3", "4. diagnose"],
"diagnose_4_icd": ["icd 4", "icd-10 4", "diagnose 4 icd"],
"diagnose_5": ["diagnose 5", "nebendiagnose 4", "5. diagnose"],
"diagnose_5_icd": ["icd 5", "icd-10 5", "diagnose 5 icd"],
"diagnose_6": ["diagnose 6", "nebendiagnose 5", "6. diagnose"],
"diagnose_6_icd": ["icd 6", "icd-10 6", "diagnose 6 icd"],
# Anamnesis
"anamnese_beschwerden": ["anamnese", "beschwerden", "eigenanamnese", "aktuelle beschwerden", "symptome"],
"krankheitsverlauf": ["krankheitsverlauf", "verlauf", "bisherige behandlung", "behandlungsverlauf"],
"koerperlicher_befund": ["befund", "körperlicher befund", "aktueller befund", "untersuchungsbefund"],
# Functional limitations (checkboxes)
"mobilitaet_keine": ["mobilität keine", "mobilität: keine"],
"mobilitaet_gering": ["mobilität gering", "mobilität: gering"],
"mobilitaet_erheblich": ["mobilität erheblich", "mobilität: erheblich"],
"selbstversorgung_keine": ["selbstversorgung keine"],
"selbstversorgung_gering": ["selbstversorgung gering"],
"selbstversorgung_erheblich": ["selbstversorgung erheblich"],
"haushalt_keine": ["haushaltsführung keine", "haushalt keine"],
"haushalt_gering": ["haushaltsführung gering", "haushalt gering"],
"haushalt_erheblich": ["haushaltsführung erheblich", "haushalt erheblich"],
"erwerb_keine": ["erwerbstätigkeit keine", "erwerb keine"],
"erwerb_gering": ["erwerbstätigkeit gering", "erwerb gering"],
"erwerb_erheblich": ["erwerbstätigkeit erheblich", "erwerb erheblich"],
"kommunikation_keine": ["kommunikation keine"],
"kommunikation_gering": ["kommunikation gering"],
"kommunikation_erheblich": ["kommunikation erheblich"],
"psyche_keine": ["psychische belastbarkeit keine", "psyche keine"],
"psyche_gering": ["psychische belastbarkeit gering", "psyche gering"],
"psyche_erheblich": ["psychische belastbarkeit erheblich", "psyche erheblich"],
"beeintraechtigungen_erlaeuterung": ["beeinträchtigungen erläuterung", "erläuterungen beeinträchtigungen"],
# Medication
"medikament_1": ["medikament 1", "medikation 1"],
"medikament_1_dosis": ["dosis 1", "medikament 1 dosis"],
"medikament_1_seit": ["seit 1", "medikament 1 seit"],
"medikament_2": ["medikament 2", "medikation 2"],
"medikament_2_dosis": ["dosis 2", "medikament 2 dosis"],
"medikament_2_seit": ["seit 2", "medikament 2 seit"],
"medikament_3": ["medikament 3", "medikation 3"],
"medikament_3_dosis": ["dosis 3", "medikament 3 dosis"],
"medikament_3_seit": ["seit 3", "medikament 3 seit"],
"medikament_4": ["medikament 4", "medikation 4"],
"medikament_4_dosis": ["dosis 4", "medikament 4 dosis"],
"medikament_4_seit": ["seit 4", "medikament 4 seit"],
"medikament_5": ["medikament 5", "medikation 5"],
"medikament_5_dosis": ["dosis 5", "medikament 5 dosis"],
"medikament_5_seit": ["seit 5", "medikament 5 seit"],
"physikalische_therapie": ["physikalische therapie", "heilmittel", "physiotherapie", "krankengymnastik"],
# Previous rehab
"reha_1_zeitraum": ["reha 1 zeitraum", "frühere reha 1 zeitraum"],
"reha_1_einrichtung": ["reha 1 einrichtung", "frühere reha 1 einrichtung"],
"reha_1_erfolg": ["reha 1 erfolg", "frühere reha 1 erfolg"],
"reha_2_zeitraum": ["reha 2 zeitraum", "frühere reha 2 zeitraum"],
"reha_2_einrichtung": ["reha 2 einrichtung", "frühere reha 2 einrichtung"],
"reha_2_erfolg": ["reha 2 erfolg", "frühere reha 2 erfolg"],
# Assessment
"leistungsvermoegen_checkbox_vollschichtig": ["vollschichtig", "leistungsvermögen vollschichtig", "6 stunden und mehr"],
"leistungsvermoegen_checkbox_teilschichtig": ["teilschichtig", "leistungsvermögen 3-6", "3-6 stunden"],
"leistungsvermoegen_checkbox_unter3": ["unter 3 stunden", "leistungsvermögen unter 3"],
"reha_beduerftig_begruendung": ["rehabilitationsbedürftigkeit", "reha begründung", "reha bedürftigkeit"],
"reha_ziel": ["rehabilitationsziel", "reha ziel", "therapieziel"],
"reha_stationaer": ["stationär", "stationäre reha"],
"reha_ambulant": ["ambulant", "ambulante reha"],
"reha_ganztaegig": ["ganztägig ambulant", "teilstationär"],
"reha_einrichtung_empfehlung": ["empfohlene einrichtung", "reha einrichtung", "klinikempfehlung"],
# Travel capability
"reisefaehig_ja": ["reisefähig ja", "öffentliche verkehrsmittel ja"],
"reisefaehig_nein": ["reisefähig nein", "öffentliche verkehrsmittel nein"],
"reisefaehig_begruendung": ["reisefähigkeit begründung", "nicht reisefähig weil"],
"begleitperson_ja": ["begleitperson ja", "begleitperson erforderlich"],
"begleitperson_nein": ["begleitperson nein", "keine begleitperson"],
# Additional
"ergaenzende_angaben": ["ergänzende angaben", "zusätzliche informationen", "bemerkungen", "sonstiges"],
# Attachments
"anlage_laborbefunde": ["anlage laborbefunde", "laborbefunde"],
"anlage_roentgen": ["anlage röntgen", "bildgebende befunde", "röntgenbefunde"],
"anlage_arztbriefe": ["anlage arztbriefe", "arztbriefe"],
"anlage_krankenhausberichte": ["anlage krankenhausberichte", "krankenhausberichte", "entlassungsberichte"],
"anlage_sonstige": ["anlage sonstige", "sonstige anlagen"],
"anlage_sonstige_text": ["sonstige anlagen text", "anlage sonstige bezeichnung"],
# Signature
"unterschrift_datum": ["unterschrift datum", "datum unterschrift", "ausstellungsdatum"],
"arzt_name": ["arzt name", "name des arztes", "behandelnder arzt"],
"arzt_fachrichtung": ["facharztbezeichnung", "fachrichtung", "facharzt"],
"praxis_anschrift": ["praxis anschrift", "praxisadresse", "arztpraxis"],
"praxis_telefon": ["praxis telefon", "praxis tel"],
"bsnr": ["bsnr", "betriebsstättennummer"],
"lanr": ["lanr", "lebenslange arztnummer"],
}
def normalize_label(label: str) -> str:
"""Normalize a label for matching."""
return label.lower().strip().replace(':', '').replace('_', ' ')
def map_fields_to_template(extracted_fields: list, template_mapping: dict) -> dict:
"""
Map extracted fields to template variables using fuzzy matching.
Args:
extracted_fields: List of {label, value, ...} dicts from AI extraction
template_mapping: Dict mapping template vars to possible label variations
Returns:
Dict of template variables to values
"""
result = {}
# Build reverse mapping: normalized label -> template var
reverse_map = {}
for template_var, possible_labels in template_mapping.items():
for label in possible_labels:
reverse_map[normalize_label(label)] = template_var
# Map each extracted field
for field in extracted_fields:
label = normalize_label(field.get('label', ''))
value = field.get('value', '')
if not label or not value:
continue
# Direct match
if label in reverse_map:
result[reverse_map[label]] = value
continue
# Fuzzy match: check if any mapping label is contained in the extracted label
for possible_label, template_var in reverse_map.items():
if possible_label in label or label in possible_label:
result[template_var] = value
break
return result
@app.route('/api/health', methods=['GET'])
def health_check():
"""Health check endpoint."""
return jsonify({'status': 'ok', 'service': 'latex-form-generator'})
@app.route('/api/templates', methods=['GET'])
def get_templates():
"""List available LaTeX templates."""
templates = list_templates()
return jsonify({'templates': templates})
@app.route('/api/generate', methods=['POST'])
def generate_pdf():
"""
Generate a filled PDF from a template and field data.
Request body:
{
"template": "G2210-11",
"fields": [
{"label": "Name, Vorname", "value": "Müller, Hans"},
{"label": "Geburtsdatum", "value": "01.01.1970"},
...
]
}
Returns: PDF file or base64-encoded PDF
"""
try:
data = request.get_json()
if not data:
return jsonify({'error': 'No JSON data provided'}), 400
template_name = data.get('template', 'G2210-11')
extracted_fields = data.get('fields', [])
return_format = data.get('format', 'base64') # 'base64' or 'file'
# Get the appropriate field mapping
if template_name == 'G2210-11':
field_mapping = G2210_FIELD_MAPPING
else:
# For other templates, try direct field names
field_mapping = {}
# Map extracted fields to template variables
if field_mapping:
template_fields = map_fields_to_template(extracted_fields, field_mapping)
else:
# Direct mapping: use label as key
template_fields = {normalize_label(f['label']).replace(' ', '_'): f['value']
for f in extracted_fields if f.get('value')}
# Generate PDF
pdf_bytes = generate_form(template_name, template_fields)
if return_format == 'file':
return send_file(
io.BytesIO(pdf_bytes),
mimetype='application/pdf',
as_attachment=True,
download_name=f'{template_name}_filled.pdf'
)
else:
# Return base64
pdf_base64 = base64.b64encode(pdf_bytes).decode('ascii')
return jsonify({
'success': True,
'pdf': pdf_base64,
'mapped_fields': template_fields
})
except FileNotFoundError as e:
return jsonify({'error': str(e)}), 404
except Exception as e:
return jsonify({'error': f'Generation failed: {str(e)}'}), 500
@app.route('/api/preview', methods=['POST'])
def preview_latex():
"""
Preview the filled LaTeX source (for debugging).
Same request format as /api/generate.
Returns the LaTeX source instead of compiled PDF.
"""
try:
data = request.get_json()
if not data:
return jsonify({'error': 'No JSON data provided'}), 400
template_name = data.get('template', 'G2210-11')
extracted_fields = data.get('fields', [])
# Get the appropriate field mapping
if template_name == 'G2210-11':
field_mapping = G2210_FIELD_MAPPING
else:
field_mapping = {}
# Map extracted fields to template variables
if field_mapping:
template_fields = map_fields_to_template(extracted_fields, field_mapping)
else:
template_fields = {normalize_label(f['label']).replace(' ', '_'): f['value']
for f in extracted_fields if f.get('value')}
# Load and fill template
template = load_template(template_name)
filled = fill_template(template, template_fields)
return jsonify({
'success': True,
'latex': filled,
'mapped_fields': template_fields
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/field-mapping/<template_name>', methods=['GET'])
def get_field_mapping(template_name):
"""Get the field mapping for a specific template."""
if template_name == 'G2210-11':
return jsonify({
'template': template_name,
'fields': list(G2210_FIELD_MAPPING.keys()),
'mapping': G2210_FIELD_MAPPING
})
else:
return jsonify({'error': 'Unknown template'}), 404
if __name__ == '__main__':
port = int(os.environ.get('PORT', 5000))
debug = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true'
app.run(host='0.0.0.0', port=port, debug=debug)

View file

@ -2,6 +2,7 @@ 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();
@ -75,12 +76,98 @@ 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,
@ -98,11 +185,34 @@ export const processDocuments = async (
let systemPrompt = ` let systemPrompt = `
ROLE: Intelligent Document Processing AI (Verification Expert). ROLE: Intelligent Document Processing AI (Verification Expert).
TASK: Extract data from the SOURCE DOCUMENT and map it to the BLANK TARGET FORM. TASK: Extract data from the SOURCE DOCUMENT and map it to 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 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.
`; `;
if (pdfFields.length > 0) { // 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(", "); const fieldList = pdfFields.map(f => `"${f.name}" (${f.type})`).join(", ");
systemPrompt += ` systemPrompt += `
MODE: FILLABLE PDF (AcroForm). MODE: FILLABLE PDF (AcroForm).
@ -110,12 +220,18 @@ export const processDocuments = async (
Map extracted data to these exact field IDs: [${fieldList}]. Map extracted data to these exact field IDs: [${fieldList}].
Return the 'key' property matching the field ID. 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.
`;
} else { } else {
systemPrompt += ` systemPrompt += `
MODE: VISUAL FILLING (Flat/XFA/Scan). MODE: VISUAL FILLING (Flat/XFA/Scan).
The target form DOES NOT have accessible digital fields. The target form DOES NOT have accessible digital fields.
You must VISUALLY locate where the text should be written. 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 the VISUAL COORDINATES [pageIndex, x, y] where the text should start.
@ -123,7 +239,7 @@ export const processDocuments = async (
- (0,0) is the top-left corner of the page. - (0,0) is the top-left corner of the page.
- (1000,1000) is the bottom-right corner. - (1000,1000) is the bottom-right corner.
- Align text slightly above lines or inside boxes. - Align text slightly above lines or inside boxes.
For checkboxes: If true/yes, the value should be "X" placed inside the box. For checkboxes: If true/yes, the value should be "X" placed inside the box.
`; `;
} }

251
services/latexService.ts Normal file
View file

@ -0,0 +1,251 @@
/**
* LaTeX Form Generation Service
*
* This service communicates with the Python LaTeX backend to generate
* filled PDF forms using LaTeX templates.
*/
import { ExtractedField } from '../types';
// Backend API URL - can be configured via environment variable
const API_BASE_URL = import.meta.env.VITE_LATEX_API_URL || 'http://localhost:5000';
export interface LatexGenerationResult {
success: boolean;
pdf?: string; // base64 encoded PDF
mappedFields?: Record<string, string>;
error?: string;
}
export interface TemplateInfo {
name: string;
fields: string[];
}
/**
* Check if the LaTeX backend is available
*/
export const isLatexServiceAvailable = async (): Promise<boolean> => {
try {
const response = await fetch(`${API_BASE_URL}/api/health`, {
method: 'GET',
signal: AbortSignal.timeout(3000),
});
return response.ok;
} catch {
return false;
}
};
/**
* Get list of available LaTeX templates
*/
export const getAvailableTemplates = async (): Promise<string[]> => {
try {
const response = await fetch(`${API_BASE_URL}/api/templates`);
if (!response.ok) {
throw new Error('Failed to fetch templates');
}
const data = await response.json();
return data.templates || [];
} catch (error) {
console.warn('Could not fetch templates:', error);
return [];
}
};
/**
* Get field mapping for a specific template
*/
export const getTemplateFieldMapping = async (templateName: string): Promise<Record<string, string[]> | null> => {
try {
const response = await fetch(`${API_BASE_URL}/api/field-mapping/${templateName}`);
if (!response.ok) {
return null;
}
const data = await response.json();
return data.mapping || null;
} catch (error) {
console.warn('Could not fetch field mapping:', error);
return null;
}
};
/**
* Generate a filled PDF using LaTeX template
*/
export const generateLatexPdf = async (
templateName: string,
fields: ExtractedField[]
): Promise<LatexGenerationResult> => {
try {
const response = await fetch(`${API_BASE_URL}/api/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
template: templateName,
fields: fields.map(f => ({
label: f.label,
value: f.value,
key: f.key,
})),
format: 'base64',
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP ${response.status}`);
}
const data = await response.json();
return {
success: true,
pdf: data.pdf,
mappedFields: data.mapped_fields,
};
} catch (error) {
console.error('LaTeX PDF generation failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
};
/**
* Preview the filled LaTeX source (for debugging)
*/
export const previewLatexSource = async (
templateName: string,
fields: ExtractedField[]
): Promise<{ latex?: string; error?: string }> => {
try {
const response = await fetch(`${API_BASE_URL}/api/preview`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
template: templateName,
fields: fields.map(f => ({
label: f.label,
value: f.value,
key: f.key,
})),
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP ${response.status}`);
}
const data = await response.json();
return { latex: data.latex };
} catch (error) {
return { error: error instanceof Error ? error.message : 'Unknown error' };
}
};
/**
* Convert base64 PDF to Blob URL for preview/download
*/
export const base64ToBlob = (base64: string, mimeType: string = 'application/pdf'): Blob => {
const byteCharacters = atob(base64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
return new Blob([byteArray], { type: mimeType });
};
/**
* Detect which template to use based on form file name or content
*/
export const detectTemplate = (fileName: string): string | null => {
const lowerName = fileName.toLowerCase();
// G2210-11 Ärztlicher Befundbericht
if (lowerName.includes('g2210') ||
lowerName.includes('befundbericht') ||
lowerName.includes('aerztlicher') ||
lowerName.includes('ärztlicher')) {
return 'G2210-11';
}
// Add more template detection patterns here
// if (lowerName.includes('s0051')) return 'S0051';
return null;
};
/**
* Get expected fields for a known form type
* This helps the AI extraction know what fields to look for
*/
export const getExpectedFields = (templateName: string): string[] => {
const fieldMappings: Record<string, string[]> = {
'G2210-11': [
'Versicherungsnummer',
'ABT.-Nr.',
'Name, Vorname',
'Geburtsdatum',
'Geschlecht',
'Straße, Hausnummer',
'PLZ',
'Ort',
'Telefon',
'Krankenkasse',
'Derzeitige Tätigkeit',
'Arbeitgeber',
'Arbeitsunfähig seit',
'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',
'Anamnese/Beschwerden',
'Krankheitsverlauf',
'Körperlicher Befund',
'Mobilität (keine/gering/erheblich)',
'Selbstversorgung (keine/gering/erheblich)',
'Haushaltsführung (keine/gering/erheblich)',
'Erwerbstätigkeit (keine/gering/erheblich)',
'Medikament 1',
'Medikament 1 Dosis',
'Medikament 2',
'Medikament 2 Dosis',
'Medikament 3',
'Medikament 3 Dosis',
'Physikalische Therapie',
'Frühere Reha Zeitraum',
'Frühere Reha Einrichtung',
'Leistungsvermögen',
'Rehabilitationsbedürftigkeit',
'Rehabilitationsziel',
'Rehabilitationsform (stationär/ambulant)',
'Reisefähig (ja/nein)',
'Begleitperson erforderlich (ja/nein)',
'Ergänzende Angaben',
'Arzt Name',
'Facharztbezeichnung',
'Praxis Anschrift',
'Praxis Telefon',
'BSNR',
'LANR',
],
};
return fieldMappings[templateName] || [];
};

353
templates/G2210-11.tex Normal file
View file

@ -0,0 +1,353 @@
\documentclass[a4paper,10pt]{article}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage[ngerman]{babel}
\usepackage[margin=1.5cm,top=1cm,bottom=1.5cm]{geometry}
\usepackage{array}
\usepackage{tabularx}
\usepackage{booktabs}
\usepackage{multirow}
\usepackage{graphicx}
\usepackage{xcolor}
\usepackage{helvet}
\usepackage{amssymb}
\usepackage{tikz}
\usepackage{fancyhdr}
\usepackage{lastpage}
\usepackage{enumitem}
\renewcommand{\familydefault}{\sfdefault}
\setlength{\parindent}{0pt}
\setlength{\parskip}{0.3em}
% Custom colors
\definecolor{drvblue}{RGB}{0,51,102}
\definecolor{lightgray}{RGB}{240,240,240}
\definecolor{bordergray}{RGB}{180,180,180}
% Header and Footer
\pagestyle{fancy}
\fancyhf{}
\renewcommand{\headrulewidth}{0pt}
\fancyfoot[C]{\footnotesize Seite \thepage\ von \pageref{LastPage}}
\fancyfoot[R]{\footnotesize G2210-11 (01.2025)}
% Custom commands
\newcommand{\formfield}[2]{\textbf{#1:} #2}
\newcommand{\formbox}[1]{\fbox{\parbox{0.95\linewidth}{#1}}}
\newcommand{\sectionheader}[1]{\colorbox{lightgray}{\parbox{\dimexpr\linewidth-2\fboxsep}{\textbf{#1}}}}
\begin{document}
% === HEADER ===
\begin{center}
{\Large\bfseries\color{drvblue} Deutsche Rentenversicherung Westfalen}\\[0.3em]
{\large Ärztlicher Befundbericht}\\[0.2em]
{\small zum Antrag auf Leistungen zur medizinischen Rehabilitation}\\[0.5em]
\hrule
\end{center}
\vspace{0.5em}
% === PATIENT DATA ===
\sectionheader{1. Angaben zur versicherten Person}
\vspace{0.3em}
\begin{tabularx}{\linewidth}{|p{4cm}|X|p{4cm}|X|}
\hline
\textbf{Versicherungsnummer:} & {{versicherungsnummer}} & \textbf{ABT.-Nr.:} & {{abt_nr}} \\
\hline
\textbf{Name, Vorname:} & \multicolumn{3}{l|}{{{name_vorname}}} \\
\hline
\textbf{Geburtsdatum:} & {{geburtsdatum|date}} & \textbf{Geschlecht:} & {{geschlecht}} \\
\hline
\textbf{Straße, Hausnummer:} & \multicolumn{3}{l|}{{{strasse}}} \\
\hline
\textbf{PLZ, Ort:} & \multicolumn{3}{l|}{{{plz}} {{ort}}} \\
\hline
\textbf{Telefon:} & {{telefon}} & \textbf{Krankenkasse:} & {{krankenkasse}} \\
\hline
\end{tabularx}
\vspace{0.5em}
% === EMPLOYMENT ===
\sectionheader{2. Angaben zur beruflichen Situation}
\vspace{0.3em}
\begin{tabularx}{\linewidth}{|p{5cm}|X|}
\hline
\textbf{Derzeitige Tätigkeit:} & {{beruf_taetigkeit}} \\
\hline
\textbf{Arbeitgeber:} & {{arbeitgeber}} \\
\hline
\textbf{Arbeitsunfähig seit:} & {{au_seit|date}} \\
\hline
\textbf{Letzte Arbeitsaufnahme:} & {{letzte_arbeit|date}} \\
\hline
\end{tabularx}
\vspace{0.5em}
% === DIAGNOSES ===
\sectionheader{3. Diagnosen (mit ICD-10-Schlüssel)}
\vspace{0.3em}
\begin{tabularx}{\linewidth}{|p{1cm}|X|p{2.5cm}|}
\hline
\textbf{Nr.} & \textbf{Diagnose} & \textbf{ICD-10} \\
\hline
1. & {{diagnose_1}} & {{diagnose_1_icd}} \\
\hline
2. & {{diagnose_2}} & {{diagnose_2_icd}} \\
\hline
3. & {{diagnose_3}} & {{diagnose_3_icd}} \\
\hline
4. & {{diagnose_4}} & {{diagnose_4_icd}} \\
\hline
5. & {{diagnose_5}} & {{diagnose_5_icd}} \\
\hline
6. & {{diagnose_6}} & {{diagnose_6_icd}} \\
\hline
\end{tabularx}
\vspace{0.5em}
% === ANAMNESIS ===
\sectionheader{4. Anamnese und aktueller Befund}
\vspace{0.3em}
\textbf{4.1 Eigenanamnese / Beschwerden des Patienten:}
\begin{tabularx}{\linewidth}{|X|}
\hline
{{anamnese_beschwerden}} \\[4em]
\hline
\end{tabularx}
\vspace{0.3em}
\textbf{4.2 Krankheitsverlauf / bisherige Behandlungen:}
\begin{tabularx}{\linewidth}{|X|}
\hline
{{krankheitsverlauf}} \\[4em]
\hline
\end{tabularx}
\vspace{0.3em}
\textbf{4.3 Aktueller körperlicher Befund:}
\begin{tabularx}{\linewidth}{|X|}
\hline
{{koerperlicher_befund}} \\[4em]
\hline
\end{tabularx}
\newpage
% === FUNCTIONAL LIMITATIONS ===
\sectionheader{5. Nicht nur vorübergehende Beeinträchtigungen der Aktivitäten und Teilhabe}
\vspace{0.3em}
\begin{tabularx}{\linewidth}{|X|c|c|c|}
\hline
\textbf{Beeinträchtigungsbereich} & \textbf{Keine} & \textbf{Gering} & \textbf{Erheblich} \\
\hline
Mobilität (Gehen, Treppensteigen, Fortbewegung) & {{mobilitaet_keine|checkbox}} & {{mobilitaet_gering|checkbox}} & {{mobilitaet_erheblich|checkbox}} \\
\hline
Selbstversorgung (Waschen, Ankleiden, Essen) & {{selbstversorgung_keine|checkbox}} & {{selbstversorgung_gering|checkbox}} & {{selbstversorgung_erheblich|checkbox}} \\
\hline
Haushaltsführung & {{haushalt_keine|checkbox}} & {{haushalt_gering|checkbox}} & {{haushalt_erheblich|checkbox}} \\
\hline
Erwerbstätigkeit & {{erwerb_keine|checkbox}} & {{erwerb_gering|checkbox}} & {{erwerb_erheblich|checkbox}} \\
\hline
Kommunikation & {{kommunikation_keine|checkbox}} & {{kommunikation_gering|checkbox}} & {{kommunikation_erheblich|checkbox}} \\
\hline
Psychische Belastbarkeit & {{psyche_keine|checkbox}} & {{psyche_gering|checkbox}} & {{psyche_erheblich|checkbox}} \\
\hline
\end{tabularx}
\vspace{0.3em}
\textbf{Erläuterungen zu den Beeinträchtigungen:}
\begin{tabularx}{\linewidth}{|X|}
\hline
{{beeintraechtigungen_erlaeuterung}} \\[3em]
\hline
\end{tabularx}
\vspace{0.5em}
% === THERAPY ===
\sectionheader{6. Bisherige und aktuelle Therapie}
\vspace{0.3em}
\textbf{6.1 Aktuelle Medikation:}
\begin{tabularx}{\linewidth}{|X|p{2cm}|p{2.5cm}|}
\hline
\textbf{Medikament} & \textbf{Dosis} & \textbf{Seit wann} \\
\hline
{{medikament_1}} & {{medikament_1_dosis}} & {{medikament_1_seit}} \\
\hline
{{medikament_2}} & {{medikament_2_dosis}} & {{medikament_2_seit}} \\
\hline
{{medikament_3}} & {{medikament_3_dosis}} & {{medikament_3_seit}} \\
\hline
{{medikament_4}} & {{medikament_4_dosis}} & {{medikament_4_seit}} \\
\hline
{{medikament_5}} & {{medikament_5_dosis}} & {{medikament_5_seit}} \\
\hline
\end{tabularx}
\vspace{0.3em}
\textbf{6.2 Physikalische Therapie / Heilmittel:}
\begin{tabularx}{\linewidth}{|X|}
\hline
{{physikalische_therapie}} \\[2em]
\hline
\end{tabularx}
\vspace{0.5em}
% === PREVIOUS REHAB ===
\sectionheader{7. Frühere Rehabilitationsmaßnahmen}
\vspace{0.3em}
\begin{tabularx}{\linewidth}{|p{3cm}|X|p{2.5cm}|}
\hline
\textbf{Zeitraum} & \textbf{Einrichtung / Art} & \textbf{Erfolg} \\
\hline
{{reha_1_zeitraum}} & {{reha_1_einrichtung}} & {{reha_1_erfolg}} \\
\hline
{{reha_2_zeitraum}} & {{reha_2_einrichtung}} & {{reha_2_erfolg}} \\
\hline
\end{tabularx}
\vspace{0.5em}
% === ASSESSMENT ===
\sectionheader{8. Ärztliche Beurteilung}
\vspace{0.3em}
\textbf{8.1 Leistungsvermögen im bisherigen Beruf:}\\
{{leistungsvermoegen_checkbox_vollschichtig|checkbox}} Vollschichtig (6 Std. und mehr) \quad
{{leistungsvermoegen_checkbox_teilschichtig|checkbox}} 3-6 Stunden \quad
{{leistungsvermoegen_checkbox_unter3|checkbox}} Unter 3 Stunden
\vspace{0.3em}
\textbf{8.2 Rehabilitationsbedürftigkeit:}
\begin{tabularx}{\linewidth}{|X|}
\hline
{{reha_beduerftig_begruendung}} \\[3em]
\hline
\end{tabularx}
\vspace{0.3em}
\textbf{8.3 Rehabilitationsziel:}
\begin{tabularx}{\linewidth}{|X|}
\hline
{{reha_ziel}} \\[2em]
\hline
\end{tabularx}
\vspace{0.3em}
\textbf{8.4 Empfohlene Rehabilitationsform:}\\
{{reha_stationaer|checkbox}} Stationär \quad
{{reha_ambulant|checkbox}} Ambulant \quad
{{reha_ganztaegig|checkbox}} Ganztägig ambulant
\vspace{0.3em}
\textbf{8.5 Empfohlene Rehabilitationseinrichtung (falls bekannt):}
\begin{tabularx}{\linewidth}{|X|}
\hline
{{reha_einrichtung_empfehlung}} \\[1em]
\hline
\end{tabularx}
\newpage
% === TRAVEL CAPABILITY ===
\sectionheader{9. Reisefähigkeit}
\vspace{0.3em}
\textbf{Kann der Patient mit öffentlichen Verkehrsmitteln reisen?}\\
{{reisefaehig_ja|checkbox}} Ja \quad
{{reisefaehig_nein|checkbox}} Nein
\vspace{0.2em}
\textbf{Falls nein, Begründung:}
\begin{tabularx}{\linewidth}{|X|}
\hline
{{reisefaehig_begruendung}} \\[2em]
\hline
\end{tabularx}
\vspace{0.3em}
\textbf{Begleitperson erforderlich?}\\
{{begleitperson_ja|checkbox}} Ja \quad
{{begleitperson_nein|checkbox}} Nein
\vspace{0.5em}
% === ADDITIONAL INFO ===
\sectionheader{10. Ergänzende Angaben}
\vspace{0.3em}
\begin{tabularx}{\linewidth}{|X|}
\hline
{{ergaenzende_angaben}} \\[4em]
\hline
\end{tabularx}
\vspace{0.5em}
% === ATTACHMENTS ===
\sectionheader{11. Anlagen}
\vspace{0.3em}
{{anlage_laborbefunde|checkbox}} Laborbefunde \quad
{{anlage_roentgen|checkbox}} Röntgen-/Bildgebende Befunde \quad
{{anlage_arztbriefe|checkbox}} Arztbriefe\\
{{anlage_krankenhausberichte|checkbox}} Krankenhausberichte \quad
{{anlage_sonstige|checkbox}} Sonstige: {{anlage_sonstige_text}}
\vspace{1em}
% === SIGNATURE ===
\sectionheader{12. Unterschrift des behandelnden Arztes}
\vspace{0.5em}
\begin{tabularx}{\linewidth}{|p{5cm}|X|}
\hline
\textbf{Datum:} & {{unterschrift_datum|date}} \\
\hline
\textbf{Name des Arztes:} & {{arzt_name}} \\
\hline
\textbf{Facharztbezeichnung:} & {{arzt_fachrichtung}} \\
\hline
\textbf{Anschrift Praxis:} & {{praxis_anschrift}} \\
\hline
\textbf{Telefon:} & {{praxis_telefon}} \\
\hline
\textbf{BSNR/LANR:} & {{bsnr}} / {{lanr}} \\
\hline
\end{tabularx}
\vspace{1.5em}
\begin{tabularx}{\linewidth}{X X}
\hrulefill & \hrulefill \\
Ort, Datum & Unterschrift und Stempel \\
\end{tabularx}
\vspace{1em}
\footnotesize
\textit{Hinweis: Die Angaben werden vertraulich behandelt und unterliegen dem Sozialgeheimnis nach § 35 SGB I.}
\end{document}