From 778caa8a457392da8bd43d0853ffbc046cfdfd19 Mon Sep 17 00:00:00 2001 From: Kenearos <86194771+Kenearos@users.noreply.github.com> Date: Fri, 6 Feb 2026 14:03:50 +0100 Subject: [PATCH] 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. --- .gitignore | 97 +- App.tsx | 48 +- Dockerfile | 70 - components/ApiKeyModal.tsx | 85 - components/ReviewPanel.tsx | 596 +-- fill_pdf.py | 95 - index.html | 18 +- latex_service.py | 249 -- package-lock.json | 5048 ------------------------- package.json | 40 +- postcss.config.js | 6 + railway.toml | 9 - requirements.txt | 3 - server.py | 339 -- services/apiKeyService.ts | 18 - services/geminiService.ts | 89 +- services/latexService.ts | 251 -- services/pdfService.ts | 42 +- tailwind.config.js | 11 + templates/G2210-11.tex | 353 -- tests/App.test.tsx | 431 --- tests/components/ApiKeyModal.test.tsx | 237 -- tests/components/FileUpload.test.tsx | 331 -- tests/components/ReviewPanel.test.tsx | 370 -- tests/fill_pdf_test.py | 344 -- tests/latex_service_test.py | 489 --- tests/server_test.py | 467 --- tests/services/apiKeyService.test.ts | 125 - tests/services/geminiService.test.ts | 320 -- tests/services/latexService.test.ts | 393 -- tests/services/pdfService.test.ts | 316 -- tests/setup.ts | 21 - tsconfig.json | 36 +- vite.config.ts | 29 +- vitest.config.ts | 23 - 35 files changed, 562 insertions(+), 10837 deletions(-) delete mode 100644 Dockerfile delete mode 100644 components/ApiKeyModal.tsx delete mode 100644 fill_pdf.py delete mode 100644 latex_service.py delete mode 100644 package-lock.json create mode 100644 postcss.config.js delete mode 100644 railway.toml delete mode 100644 requirements.txt delete mode 100644 server.py delete mode 100644 services/apiKeyService.ts delete mode 100644 services/latexService.ts create mode 100644 tailwind.config.js delete mode 100644 templates/G2210-11.tex delete mode 100644 tests/App.test.tsx delete mode 100644 tests/components/ApiKeyModal.test.tsx delete mode 100644 tests/components/FileUpload.test.tsx delete mode 100644 tests/components/ReviewPanel.test.tsx delete mode 100644 tests/fill_pdf_test.py delete mode 100644 tests/latex_service_test.py delete mode 100644 tests/server_test.py delete mode 100644 tests/services/apiKeyService.test.ts delete mode 100644 tests/services/geminiService.test.ts delete mode 100644 tests/services/latexService.test.ts delete mode 100644 tests/services/pdfService.test.ts delete mode 100644 tests/setup.ts delete mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index 0ff731f..1c9279f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,26 +4,85 @@ logs npm-debug.log* yarn-debug.log* yarn-error.log* -pnpm-debug.log* -lerna-debug.log* -# Python -__pycache__/ -*.py[cod] -*.pytest_cache/ +# Runtime data +pids +*.pid +*.seed +*.pid.lock -node_modules -dist -dist-ssr -*.local +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# OS metadata .DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? +Thumbs.db + +# Dist folder +dist/ +dist-ssr/ \ No newline at end of file diff --git a/App.tsx b/App.tsx index 53a67e8..8290833 100644 --- a/App.tsx +++ b/App.tsx @@ -2,11 +2,9 @@ import React, { useState, useEffect } from 'react'; import { AppStatus, FileData, FormResponse } from './types'; import { FileUpload } from './components/FileUpload'; import { ReviewPanel } from './components/ReviewPanel'; -import { ApiKeyModal } from './components/ApiKeyModal'; import { processDocuments } from './services/geminiService'; import { getPdfFields, PdfFieldInfo } from './services/pdfService'; -import { getApiKey, setApiKey, hasApiKey } from './services/apiKeyService'; -import { Bot, Sparkles, ArrowRight, FileCheck2, ScanText, Loader2, AlertTriangle, FileText, Check, Settings } from 'lucide-react'; +import { Bot, Sparkles, ArrowRight, FileCheck2, ScanText, Loader2, AlertTriangle, FileText, Check } from 'lucide-react'; const App: React.FC = () => { const [status, setStatus] = useState(AppStatus.IDLE); @@ -15,12 +13,6 @@ const App: React.FC = () => { const [pdfFields, setPdfFields] = useState([]); const [responseData, setResponseData] = useState(null); const [error, setError] = useState(null); - const [showApiKeyModal, setShowApiKeyModal] = useState(!hasApiKey()); - - const handleSaveApiKey = (key: string) => { - setApiKey(key); - setShowApiKeyModal(false); - }; useEffect(() => { const analyzePdf = async () => { @@ -63,27 +55,14 @@ const App: React.FC = () => { if (status === AppStatus.REVIEW && responseData && formFile && sourceFile) { return (
- setShowApiKeyModal(false)} - currentKey={getApiKey() || ''} - />
-
+
AutoForm AI
-
{ return (
- setShowApiKeyModal(false) : undefined} - currentKey={getApiKey() || ''} - /> {/* Header */}
@@ -118,19 +91,10 @@ const App: React.FC = () => {

Intelligent Document Processing

-
-
- 1. Scan - 2. Extract - 3. Review -
- +
+ 1. Scan + 2. Extract + 3. Review
diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index b4b3d4c..0000000 --- a/Dockerfile +++ /dev/null @@ -1,70 +0,0 @@ -# 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 -# PORT is used by serve for the frontend (Railway will set this) -ENV PORT=3000 -# FLASK_PORT is used by the Python API server (separate from Railway's PORT) -ENV FLASK_PORT=5000 -ENV FLASK_DEBUG=false -ENV VITE_LATEX_API_URL=http://localhost:5000 - -# Start both services -CMD ["/app/start.sh"] diff --git a/components/ApiKeyModal.tsx b/components/ApiKeyModal.tsx deleted file mode 100644 index 298c9de..0000000 --- a/components/ApiKeyModal.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React, { useState } from 'react'; -import { Key, ExternalLink, X } from 'lucide-react'; - -interface ApiKeyModalProps { - isOpen: boolean; - onSave: (key: string) => void; - onClose?: () => void; - currentKey?: string; -} - -export const ApiKeyModal: React.FC = ({ isOpen, onSave, onClose, currentKey }) => { - const [apiKey, setApiKey] = useState(currentKey || ''); - const [error, setError] = useState(''); - - if (!isOpen) return null; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (!apiKey.trim()) { - setError('Bitte gib einen API Key ein'); - return; - } - if (!apiKey.startsWith('AI')) { - setError('Der Key sollte mit "AI" beginnen'); - return; - } - onSave(apiKey.trim()); - }; - - return ( -
-
-
-
- -

Gemini API Key

-
- {onClose && ( - - )} -
- -
-

- Dein API Key wird nur lokal in deinem Browser gespeichert und nie an unsere Server gesendet. -

- -
- - { setApiKey(e.target.value); setError(''); }} - placeholder="AIza..." - className="w-full px-4 py-3 border border-slate-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all" - autoFocus - /> - {error &&

{error}

} -
- - - - API Key bei Google AI Studio holen - - - -
-
-
- ); -}; diff --git a/components/ReviewPanel.tsx b/components/ReviewPanel.tsx index b064188..4636b1a 100644 --- a/components/ReviewPanel.tsx +++ b/components/ReviewPanel.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useMemo, useRef } from 'react'; import { ExtractedField, FileData } from '../types'; -import { Check, Edit2, Download, RefreshCw, FileText, AlertTriangle, XCircle, ArrowRight, PenTool, CheckCircle2, Circle, FileCheck } from 'lucide-react'; +import { Check, Edit2, Download, RefreshCw, FileText, AlertTriangle, XCircle, ArrowRight, PenTool, CheckCircle2, Circle, LayoutTemplate, List, Move } from 'lucide-react'; import { createFilledPdf } from '../services/pdfService'; import { jsPDF } from "jspdf"; @@ -13,38 +13,49 @@ interface ReviewPanelProps { onReset: () => void; } -export const ReviewPanel: React.FC = ({ - fields: initialFields, - formFile, +export const ReviewPanel: React.FC = ({ + fields: initialFields, + formFile, sourceFile, summary, isFillablePdf, - onReset + onReset }) => { const [fields, setFields] = useState(initialFields); const [activeField, setActiveField] = useState(null); const [previewUrl, setPreviewUrl] = useState(null); const [filterMode, setFilterMode] = useState<'ALL' | 'ATTENTION'>('ALL'); + const [viewMode, setViewMode] = useState<'LIST' | 'FORM'>('LIST'); + + // Dragging state + const [draggingField, setDraggingField] = useState(null); + const containerRef = useRef(null); // Derived state for progress const verifiedCount = fields.filter(f => f.isVerified).length; const totalCount = fields.length; const progressPercent = Math.round((verifiedCount / totalCount) * 100); - - const fieldsRequiresAttention = useMemo(() => - fields.filter(f => f.validation?.status !== 'VALID'), + + const fieldsRequiresAttention = useMemo(() => + fields.filter(f => f.validation?.status !== 'VALID'), [fields]); - // Generate preview - fills the original PDF directly + // Generate preview for PDF download useEffect(() => { - const updatePreview = async () => { + let active = true; + let timeoutId: ReturnType; + + const generatePreview = async () => { if (formFile.type === 'application/pdf') { try { const filledPdfBytes = await createFilledPdf(formFile.base64, fields, isFillablePdf); + if (!active) return; + const blob = new Blob([filledPdfBytes], { type: 'application/pdf' }); const url = URL.createObjectURL(blob); + setPreviewUrl(prev => { - if (prev) URL.revokeObjectURL(prev); + if (prev && prev.startsWith('blob:')) URL.revokeObjectURL(prev); return url; }); } catch (e) { @@ -55,26 +66,54 @@ export const ReviewPanel: React.FC = ({ } }; - const timer = setTimeout(updatePreview, 500); - return () => clearTimeout(timer); - }, [fields, isFillablePdf, formFile.base64, formFile.type, formFile.previewUrl]); + // Debounce to avoid excessive PDF generation + timeoutId = setTimeout(generatePreview, 600); + + return () => { + active = false; + clearTimeout(timeoutId); + }; + }, [fields, isFillablePdf, formFile]); + + // Cleanup blob URL on unmount + useEffect(() => { + return () => { + setPreviewUrl(prev => { + if (prev && prev.startsWith('blob:')) URL.revokeObjectURL(prev); + return null; + }); + }; + }, []); const handleUpdate = (index: number, newValue: string) => { const newFields = [...fields]; - newFields[index] = { - ...newFields[index], + newFields[index] = { + ...newFields[index], value: newValue, + // Auto-verify when manually edited isVerified: true, validation: { ...newFields[index].validation!, status: 'VALID', message: 'Manually verified' } }; setFields(newFields); }; + const handleCoordinateUpdate = (index: number, x: number, y: number) => { + const newFields = [...fields]; + if (newFields[index].coordinates) { + newFields[index] = { + ...newFields[index], + coordinates: { ...newFields[index].coordinates!, x, y }, + isVerified: true // Moving it implies verification + }; + setFields(newFields); + } + }; + const toggleVerify = (index: number) => { const newFields = [...fields]; - newFields[index] = { - ...newFields[index], - isVerified: !newFields[index].isVerified + newFields[index] = { + ...newFields[index], + isVerified: !newFields[index].isVerified }; setFields(newFields); }; @@ -106,15 +145,48 @@ export const ReviewPanel: React.FC = ({ } }; + // Drag handlers + const handleDragStart = (e: React.MouseEvent, index: number) => { + e.stopPropagation(); + setDraggingField(index); + setActiveField(index); + }; + + const handleMouseMove = (e: React.MouseEvent) => { + if (draggingField !== null && containerRef.current) { + const rect = containerRef.current.getBoundingClientRect(); + const relativeX = e.clientX - rect.left; + const relativeY = e.clientY - rect.top; + + // Convert pixels to 0-1000 scale + const scaleX = (relativeX / rect.width) * 1000; + const scaleY = (relativeY / rect.height) * 1000; + + // Clamp values + const clampedX = Math.max(0, Math.min(1000, scaleX)); + const clampedY = Math.max(0, Math.min(1000, scaleY)); + + handleCoordinateUpdate(draggingField, clampedX, clampedY); + } + }; + + const handleMouseUp = () => { + setDraggingField(null); + }; + // Sort: Unverified/Issues first, then verified const displayedFields = fields.map((f, i) => ({ ...f, originalIndex: i })) .sort((a, b) => { + // Priority 1: Attention needed const aNeedsAttn = a.validation?.status !== 'VALID'; const bNeedsAttn = b.validation?.status !== 'VALID'; if (aNeedsAttn && !bNeedsAttn) return -1; if (!aNeedsAttn && bNeedsAttn) return 1; + + // Priority 2: Unverified if (!a.isVerified && b.isVerified) return -1; if (a.isVerified && !b.isVerified) return 1; + return a.originalIndex - b.originalIndex; }) .filter(f => filterMode === 'ALL' || (f.validation?.status !== 'VALID' && !f.isVerified)); @@ -127,31 +199,54 @@ export const ReviewPanel: React.FC = ({

Review & Verify

{summary}

- +
+ {/* View Toggle */} +
+ + +
+ +
+ + {/* Verification Progress */}
{verifiedCount} / {totalCount} Verified
-
- - -
-
- {/* Left Column: PDF Preview */} -
-
-
- Preview - {isFillablePdf ? ( - - - Fillable PDF - + {viewMode === 'LIST' ? ( + /* ================= LIST VIEW ================= */ +
+ {/* Left Column: Preview */} +
+
+
+ PDF Preview +
+ {formFile.file.name} +
+
+ {previewUrl ? ( + formFile.type === 'application/pdf' ? ( + +
+

Unable to display PDF directly.

+ Download to view +
+
+ ) : ( +
+ Form Document +
+ ) ) : ( - - - Visual Overlay - - )} -
- {formFile.file.name} -
-
- {previewUrl ? ( - formFile.type === 'application/pdf' ? ( -