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' ? ( -