refactor: Migrate to ESM and Vite for modern build
Updates package.json and index.html to use ES Modules and Vite for development and building. This includes migrating dependencies and removing old build scripts and testing configurations. Also, simplifies the Gemini API key handling by directly using environment variables and refactors the Gemini response schema for clearer field definitions. Updates React component imports to use ESM paths for better maintainability.
This commit is contained in:
parent
2ed8e57267
commit
778caa8a45
35 changed files with 562 additions and 10837 deletions
|
|
@ -1,18 +0,0 @@
|
|||
const STORAGE_KEY = 'gemini_api_key';
|
||||
|
||||
export const getApiKey = (): string | null => {
|
||||
return localStorage.getItem(STORAGE_KEY);
|
||||
};
|
||||
|
||||
export const setApiKey = (key: string): void => {
|
||||
localStorage.setItem(STORAGE_KEY, key);
|
||||
};
|
||||
|
||||
export const clearApiKey = (): void => {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
};
|
||||
|
||||
export const hasApiKey = (): boolean => {
|
||||
const key = getApiKey();
|
||||
return key !== null && key.length > 0;
|
||||
};
|
||||
|
|
@ -1,15 +1,8 @@
|
|||
import { GoogleGenAI, Type, Schema } from "@google/genai";
|
||||
import { FileData, FormResponse } from "../types";
|
||||
import { PdfFieldInfo } from "./pdfService";
|
||||
import { getApiKey } from "./apiKeyService";
|
||||
|
||||
const getAI = () => {
|
||||
const apiKey = getApiKey();
|
||||
if (!apiKey) {
|
||||
throw new Error("Kein API Key gesetzt. Bitte gib deinen Gemini API Key ein.");
|
||||
}
|
||||
return new GoogleGenAI({ apiKey });
|
||||
};
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
||||
|
||||
const responseSchema: Schema = {
|
||||
type: Type.OBJECT,
|
||||
|
|
@ -33,11 +26,11 @@ const responseSchema: Schema = {
|
|||
},
|
||||
value: {
|
||||
type: Type.STRING,
|
||||
description: "The value to fill. For checkboxes, use 'true'/'false' or 'X'."
|
||||
description: "The value to fill. For checkboxes, use 'X' if true, otherwise leave empty."
|
||||
},
|
||||
sourceContext: {
|
||||
type: Type.STRING,
|
||||
description: "The exact snippet of text from the source document used to derive this value. Used for user verification."
|
||||
description: "The exact snippet of text from the source document used to derive this value."
|
||||
},
|
||||
coordinates: {
|
||||
type: Type.OBJECT,
|
||||
|
|
@ -58,11 +51,11 @@ const responseSchema: Schema = {
|
|||
},
|
||||
message: {
|
||||
type: Type.STRING,
|
||||
description: "Validation message explaining any issues or uncertainty."
|
||||
description: "Validation message explaining any issues."
|
||||
},
|
||||
suggestion: {
|
||||
type: Type.STRING,
|
||||
description: "Alternative value suggestion if the extracted value is uncertain."
|
||||
description: "Alternative value suggestion."
|
||||
}
|
||||
},
|
||||
required: ["status"]
|
||||
|
|
@ -80,7 +73,7 @@ export const processDocuments = async (
|
|||
sourceDocument: FileData,
|
||||
pdfFields: PdfFieldInfo[] = []
|
||||
): Promise<FormResponse> => {
|
||||
|
||||
|
||||
const formPart = {
|
||||
inlineData: {
|
||||
data: blankForm.base64,
|
||||
|
|
@ -96,67 +89,51 @@ export const processDocuments = async (
|
|||
};
|
||||
|
||||
let systemPrompt = `
|
||||
ROLE: Intelligent Document Processing AI.
|
||||
TASK: Extract data from the SOURCE DOCUMENT and fill the BLANK TARGET FORM.
|
||||
|
||||
CRITICAL: You must verify every extraction. If uncertain, set validation.status to 'WARNING'.
|
||||
ROLE: Intelligent Document Processing AI (German Bureaucracy Expert).
|
||||
TASK: Extract data from the SOURCE DOCUMENT and map it to the TARGET FORM visually or logically.
|
||||
|
||||
STRICT FORMATTING RULES (German Context):
|
||||
1. DATES: Must be formatted as 'DD.MM.YYYY' (e.g., 24.01.1982). Do not use ISO or US formats.
|
||||
2. NUMBERS/CURRENCY: Use comma as decimal separator (e.g., 1.425,00). Do NOT write the currency symbol (€) if the form already has it printed.
|
||||
3. CHECKBOXES: If a condition is met (e.g., "Männlich", "Ja"), the 'value' must be "X". If not met, leave empty.
|
||||
|
||||
CRITICAL: Verify every extraction. If ambiguous, set validation.status to 'WARNING'.
|
||||
`;
|
||||
|
||||
// PRIORITY 1: If PDF has fillable fields, USE THEM - this is the simplest and best approach
|
||||
if (pdfFields.length > 0) {
|
||||
const fieldList = pdfFields.map(f => `"${f.name}" (${f.type})`).join("\n- ");
|
||||
const fieldList = pdfFields.map(f => `"${f.name}" (${f.type})`).join(", ");
|
||||
systemPrompt += `
|
||||
MODE: FILLABLE PDF (AcroForm).
|
||||
|
||||
The target PDF has these EXACT fillable fields:
|
||||
- ${fieldList}
|
||||
|
||||
CRITICAL INSTRUCTIONS:
|
||||
1. For EACH field listed above, extract the corresponding value from the SOURCE DOCUMENT.
|
||||
2. Return the 'key' property with the EXACT field name from the list above.
|
||||
3. The 'label' should be a human-readable description.
|
||||
4. For checkboxes: use value "true" to check, "false" to uncheck.
|
||||
5. For text fields: use the extracted text value.
|
||||
|
||||
You MUST return a field entry for each PDF field listed above.
|
||||
The 'key' MUST match exactly one of the field names I provided.
|
||||
Map extracted data to these exact field IDs: [${fieldList}].
|
||||
`;
|
||||
} else {
|
||||
// FALLBACK: Visual overlay mode for non-fillable PDFs
|
||||
systemPrompt += `
|
||||
MODE: VISUAL FILLING (Flat PDF/Scan).
|
||||
The target form does NOT have digital form fields.
|
||||
|
||||
For every field you identify on the TARGET FORM:
|
||||
1. Extract the corresponding value from the SOURCE DOCUMENT.
|
||||
2. Estimate VISUAL COORDINATES [pageIndex, x, y] where the text should be written.
|
||||
- x and y are on a scale of 0 to 1000.
|
||||
- (0,0) is the top-left corner.
|
||||
- (1000,1000) is the bottom-right corner.
|
||||
|
||||
For checkboxes: value should be "X" if checked.
|
||||
MODE: VISUAL FILLING (Flat Scan/Image).
|
||||
The target form has NO digital fields. You must estimate COORDINATES.
|
||||
|
||||
COORDINATE SYSTEM (0-1000):
|
||||
- x=0, y=0 is Top-Left.
|
||||
- x=1000, y=1000 is Bottom-Right.
|
||||
|
||||
STRATEGY:
|
||||
1. Analyze the blank form image. Identify where user input belongs (lines, boxes).
|
||||
2. For "Reisekosten" (Travel Expenses): Look for columns like "Fahrtkosten", "Übernachtung". accurately place the amounts in the "Betrag" column.
|
||||
3. Place text slightly ABOVE the underline so it looks natural.
|
||||
4. For Checkboxes: Estimate the center of the square box.
|
||||
`;
|
||||
}
|
||||
|
||||
systemPrompt += `
|
||||
VALIDATION RULES:
|
||||
1. Dates: German format DD.MM.YYYY
|
||||
2. Missing Data: Leave 'value' empty, don't hallucinate.
|
||||
3. Source Context: Include the exact text snippet from source that justifies the extraction.
|
||||
`;
|
||||
|
||||
try {
|
||||
const ai = getAI();
|
||||
const modelId = "gemini-2.0-flash";
|
||||
const modelId = "gemini-3-flash-preview";
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: modelId,
|
||||
contents: {
|
||||
parts: [
|
||||
formPart,
|
||||
{ text: "This is the BLANK TARGET FORM." },
|
||||
{ text: "TARGET FORM (Blank)" },
|
||||
sourcePart,
|
||||
{ text: "This is the SOURCE DOCUMENT." },
|
||||
{ text: "SOURCE DATA (Email/Receipts)" },
|
||||
]
|
||||
},
|
||||
config: {
|
||||
|
|
@ -175,4 +152,4 @@ export const processDocuments = async (
|
|||
console.error("Gemini API Error:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
@ -1,251 +0,0 @@
|
|||
/**
|
||||
* 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] || [];
|
||||
};
|
||||
|
|
@ -42,7 +42,7 @@ export const createFilledPdf = async (base64: string, fields: ExtractedField[],
|
|||
if (field instanceof PDFTextField) {
|
||||
field.setText(String(value));
|
||||
} else if (field instanceof PDFCheckBox) {
|
||||
const isChecked = String(value).toLowerCase() === 'true' || String(value).toLowerCase() === 'yes';
|
||||
const isChecked = String(value).toLowerCase() === 'true' || String(value).toLowerCase() === 'yes' || String(value).toLowerCase() === 'x';
|
||||
if (isChecked) field.check();
|
||||
else field.uncheck();
|
||||
}
|
||||
|
|
@ -63,8 +63,9 @@ export const createFilledPdf = async (base64: string, fields: ExtractedField[],
|
|||
|
||||
const { pageIndex, x, y } = field.coordinates;
|
||||
|
||||
// Safety check for page index
|
||||
if (pageIndex < 0 || pageIndex >= pages.length) continue;
|
||||
// Safety check for page index and coordinates
|
||||
if (typeof pageIndex !== 'number' || pageIndex < 0 || pageIndex >= pages.length) continue;
|
||||
if (isNaN(x) || isNaN(y)) continue;
|
||||
|
||||
const page = pages[pageIndex];
|
||||
const { width, height } = page.getSize();
|
||||
|
|
@ -100,36 +101,5 @@ export const createFilledPdf = async (base64: string, fields: ExtractedField[],
|
|||
};
|
||||
|
||||
export const fillPdf = async (base64: string, fieldValues: Record<string, string | boolean>): Promise<Uint8Array> => {
|
||||
const pdfDoc = await PDFDocument.load(base64);
|
||||
|
||||
try {
|
||||
const form = pdfDoc.getForm();
|
||||
|
||||
for (const [fieldName, value] of Object.entries(fieldValues)) {
|
||||
try {
|
||||
const field = form.getField(fieldName);
|
||||
if (!field) continue;
|
||||
|
||||
if (field instanceof PDFTextField) {
|
||||
field.setText(String(value));
|
||||
} else if (field instanceof PDFCheckBox) {
|
||||
const isChecked = typeof value === 'boolean'
|
||||
? value
|
||||
: String(value).toLowerCase() === 'true' || String(value).toLowerCase() === 'yes';
|
||||
if (isChecked) {
|
||||
field.check();
|
||||
} else {
|
||||
field.uncheck();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Field might be read-only or have other issues - continue with other fields
|
||||
console.warn(`Could not fill field "${fieldName}":`, e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Error filling form fields:", e);
|
||||
}
|
||||
|
||||
return await pdfDoc.save();
|
||||
};
|
||||
return new Uint8Array();
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue