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:
Kenearos 2026-02-06 14:03:50 +01:00
parent 2ed8e57267
commit 778caa8a45
35 changed files with 562 additions and 10837 deletions

View file

@ -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<ReviewPanelProps> = ({
fields: initialFields,
formFile,
export const ReviewPanel: React.FC<ReviewPanelProps> = ({
fields: initialFields,
formFile,
sourceFile,
summary,
isFillablePdf,
onReset
onReset
}) => {
const [fields, setFields] = useState(initialFields);
const [activeField, setActiveField] = useState<number | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [filterMode, setFilterMode] = useState<'ALL' | 'ATTENTION'>('ALL');
const [viewMode, setViewMode] = useState<'LIST' | 'FORM'>('LIST');
// Dragging state
const [draggingField, setDraggingField] = useState<number | null>(null);
const containerRef = useRef<HTMLDivElement>(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<typeof setTimeout>;
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<ReviewPanelProps> = ({
}
};
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<ReviewPanelProps> = ({
}
};
// 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<ReviewPanelProps> = ({
<h2 className="text-2xl font-bold text-slate-900">Review & Verify</h2>
<p className="text-slate-500 text-sm mt-1">{summary}</p>
</div>
<div className="flex items-center gap-4 w-full md:w-auto">
{/* View Toggle */}
<div className="flex bg-slate-100 p-1 rounded-lg border border-slate-200">
<button
onClick={() => setViewMode('LIST')}
className={`p-1.5 rounded-md flex items-center space-x-2 text-sm font-medium transition-all ${viewMode === 'LIST' ? 'bg-white shadow text-slate-900' : 'text-slate-500 hover:text-slate-700'}`}
title="List View"
>
<List className="w-4 h-4" />
<span className="hidden sm:inline">List</span>
</button>
<button
onClick={() => setViewMode('FORM')}
className={`p-1.5 rounded-md flex items-center space-x-2 text-sm font-medium transition-all ${viewMode === 'FORM' ? 'bg-white shadow text-slate-900' : 'text-slate-500 hover:text-slate-700'}`}
title="Form Overlay View"
>
<LayoutTemplate className="w-4 h-4" />
<span className="hidden sm:inline">Form View</span>
</button>
</div>
<div className="h-6 w-px bg-slate-300 mx-2 hidden md:block"></div>
{/* Verification Progress */}
<div className="hidden md:flex flex-col items-end mr-2">
<span className="text-xs font-semibold text-slate-600 mb-1">
{verifiedCount} / {totalCount} Verified
</span>
<div className="w-32 h-2 bg-slate-200 rounded-full overflow-hidden">
<div
<div
className="h-full bg-emerald-500 transition-all duration-500 ease-out"
style={{ width: `${progressPercent}%` }}
/>
</div>
</div>
<button
<button
onClick={onReset}
className="flex items-center px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
>
<RefreshCw className="w-4 h-4 mr-2" />
Start Over
</button>
<button
<button
onClick={handleDownload}
className="flex items-center px-4 py-2 text-sm font-medium text-white rounded-lg transition-colors shadow-sm bg-indigo-600 hover:bg-indigo-700"
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`}
>
<Download className="w-4 h-4 mr-2" />
Download PDF
@ -159,228 +254,271 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 flex-1 min-h-0">
{/* Left Column: PDF Preview */}
<div className="bg-slate-900 rounded-xl overflow-hidden shadow-lg flex flex-col">
<div className="p-3 bg-slate-800 border-b border-slate-700 flex justify-between items-center">
<div className="flex items-center space-x-2">
<span className="text-xs font-medium text-slate-300 uppercase tracking-wider">Preview</span>
{isFillablePdf ? (
<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">
<FileCheck className="w-3 h-3 mr-1" />
Fillable PDF
</span>
{viewMode === 'LIST' ? (
/* ================= LIST VIEW ================= */
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 flex-1 min-h-0">
{/* Left Column: Preview */}
<div className="bg-slate-900 rounded-xl overflow-hidden shadow-lg flex flex-col">
<div className="p-3 bg-slate-800 border-b border-slate-700 flex justify-between items-center">
<div className="flex items-center space-x-2">
<span className="text-xs font-medium text-slate-300 uppercase tracking-wider">PDF Preview</span>
</div>
<span className="text-xs text-slate-400">{formFile.file.name}</span>
</div>
<div className="flex-1 bg-slate-900 relative">
{previewUrl ? (
formFile.type === 'application/pdf' ? (
<object
data={previewUrl}
type="application/pdf"
className="w-full h-full block"
aria-label="PDF Preview"
>
<div className="flex flex-col items-center justify-center h-full text-white/70">
<p>Unable to display PDF directly.</p>
<a href={previewUrl} download className="text-indigo-400 underline mt-2">Download to view</a>
</div>
</object>
) : (
<div className="w-full h-full overflow-auto flex items-center justify-center p-4">
<img
src={previewUrl}
alt="Form Document"
className="max-w-full shadow-lg border border-slate-700"
/>
</div>
)
) : (
<span className="flex items-center text-[10px] bg-amber-500/20 text-amber-300 px-1.5 py-0.5 rounded border border-amber-500/30">
<PenTool className="w-3 h-3 mr-1" />
Visual Overlay
</span>
)}
</div>
<span className="text-xs text-slate-400">{formFile.file.name}</span>
</div>
<div className="flex-1 bg-slate-900 relative">
{previewUrl ? (
formFile.type === 'application/pdf' ? (
<iframe
src={previewUrl}
title="Form PDF Preview"
className="w-full h-full border-none"
/>
) : (
<div className="w-full h-full overflow-auto flex items-center justify-center p-4">
<img
src={previewUrl}
alt="Form Document"
className="max-w-full shadow-lg border border-slate-700"
/>
<div className="flex flex-col items-center justify-center h-full text-slate-500">
<FileText className="w-16 h-16 mb-4 opacity-50" />
<p>Preview not available</p>
</div>
)
) : (
<div className="flex flex-col items-center justify-center h-full text-slate-500">
<FileText className="w-16 h-16 mb-4 opacity-50" />
<p>Preview not available</p>
</div>
)}
)}
</div>
</div>
</div>
{/* Right Column: Field List */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 flex flex-col overflow-hidden">
<div className="p-4 border-b border-slate-100 bg-slate-50/50">
<div className="flex justify-between items-center mb-4">
<h3 className="font-semibold text-slate-800">Field Verification</h3>
<div className="flex space-x-2 text-xs">
<button
onClick={() => setFilterMode('ALL')}
className={`px-3 py-1 rounded-full border transition-colors ${filterMode === 'ALL' ? 'bg-slate-800 text-white border-slate-800' : 'bg-white text-slate-600 border-slate-200 hover:bg-slate-50'}`}
>
All Fields ({totalCount})
</button>
{fieldsRequiresAttention.length > 0 && (
<button
onClick={() => setFilterMode('ATTENTION')}
className={`px-3 py-1 rounded-full border transition-colors flex items-center ${filterMode === 'ATTENTION' ? 'bg-amber-100 text-amber-800 border-amber-200' : 'bg-white text-amber-600 border-slate-200 hover:bg-amber-50'}`}
{/* Right Column: Verification List */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 flex flex-col overflow-hidden">
<div className="p-4 border-b border-slate-100 bg-slate-50/50">
<div className="flex justify-between items-center mb-4">
<h3 className="font-semibold text-slate-800">Field Verification</h3>
<div className="flex space-x-2 text-xs">
<button
onClick={() => setFilterMode('ALL')}
className={`px-3 py-1 rounded-full border transition-colors ${filterMode === 'ALL' ? 'bg-slate-800 text-white border-slate-800' : 'bg-white text-slate-600 border-slate-200 hover:bg-slate-50'}`}
>
<AlertTriangle className="w-3 h-3 mr-1" />
Needs Review ({fieldsRequiresAttention.length})
All ({totalCount})
</button>
)}
{fieldsRequiresAttention.length > 0 && (
<button
onClick={() => setFilterMode('ATTENTION')}
className={`px-3 py-1 rounded-full border transition-colors flex items-center ${filterMode === 'ATTENTION' ? 'bg-amber-100 text-amber-800 border-amber-200' : 'bg-white text-amber-600 border-slate-200 hover:bg-amber-50'}`}
>
<AlertTriangle className="w-3 h-3 mr-1" />
Needs Review ({fieldsRequiresAttention.length})
</button>
)}
</div>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-slate-50/30">
{displayedFields.map((field) => {
const idx = field.originalIndex;
const status = field.validation?.status || 'VALID';
const isVerified = field.isVerified;
let statusBorder = isVerified ? "border-emerald-200" : "border-slate-200";
let statusBg = isVerified ? "bg-emerald-50/30" : "bg-white";
{isFillablePdf ? (
<div className="flex items-start space-x-2 bg-emerald-50 text-emerald-800 p-3 rounded-md border border-emerald-100 text-xs">
<FileCheck className="w-4 h-4 flex-shrink-0 text-emerald-600 mt-0.5" />
<div>
<p className="font-semibold">Fillable PDF Mode</p>
<p>Fields are filled directly into the original PDF form.</p>
</div>
</div>
) : (
<div className="flex items-start space-x-2 bg-amber-50 text-amber-800 p-3 rounded-md border border-amber-100 text-xs">
<PenTool className="w-4 h-4 flex-shrink-0 text-amber-600 mt-0.5" />
<div>
<p className="font-semibold">Visual Overlay Mode</p>
<p>This PDF has no fillable fields. Text is overlaid at estimated positions.</p>
</div>
</div>
)}
</div>
{/* Fields List */}
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-slate-50/30">
{displayedFields.map((field) => {
const idx = field.originalIndex;
const status = field.validation?.status || 'VALID';
const isVerified = field.isVerified;
let statusBorder = isVerified ? "border-emerald-200" : "border-slate-200";
let statusBg = isVerified ? "bg-emerald-50/30" : "bg-white";
if (!isVerified) {
if (status === 'INVALID') {
statusBorder = "border-red-200";
statusBg = "bg-red-50/50";
} else if (status === 'WARNING') {
statusBorder = "border-amber-200";
statusBg = "bg-amber-50/50";
if (!isVerified) {
if (status === 'INVALID') {
statusBorder = "border-red-200";
statusBg = "bg-red-50/50";
} else if (status === 'WARNING') {
statusBorder = "border-amber-200";
statusBg = "bg-amber-50/50";
}
}
}
return (
<div
key={idx}
className={`relative group rounded-lg border transition-all duration-200 p-3 shadow-sm ${activeField === idx ? 'ring-1 ring-indigo-500 border-indigo-500 shadow-md z-10' : statusBorder} ${statusBg}`}
onFocus={() => setActiveField(idx)}
onBlur={() => setActiveField(null)}
>
<div className="flex items-start gap-3">
<button
onClick={() => toggleVerify(idx)}
className={`mt-1 flex-shrink-0 w-5 h-5 rounded border flex items-center justify-center transition-colors ${isVerified ? 'bg-emerald-500 border-emerald-500 text-white' : 'bg-white border-slate-300 text-transparent hover:border-emerald-400'}`}
title={isVerified ? "Mark as unverified" : "Mark as verified"}
>
<Check className="w-3.5 h-3.5" strokeWidth={3} />
</button>
return (
<div
key={idx}
className={`relative group rounded-lg border transition-all duration-200 p-3 shadow-sm ${activeField === idx ? 'ring-1 ring-indigo-500 border-indigo-500 shadow-md z-10' : statusBorder} ${statusBg}`}
onFocus={() => setActiveField(idx)}
onBlur={() => setActiveField(null)}
>
<div className="flex items-start gap-3">
<button
onClick={() => toggleVerify(idx)}
className={`mt-1 flex-shrink-0 w-5 h-5 rounded border flex items-center justify-center transition-colors ${isVerified ? 'bg-emerald-500 border-emerald-500 text-white' : 'bg-white border-slate-300 text-transparent hover:border-emerald-400'}`}
title={isVerified ? "Mark as unverified" : "Mark as verified"}
>
<Check className="w-3.5 h-3.5" strokeWidth={3} />
</button>
<div className="flex-1 min-w-0">
<div className="flex justify-between items-center mb-1">
<label className={`text-xs font-semibold uppercase tracking-wider truncate ${isVerified ? 'text-emerald-700' : 'text-slate-600'}`}>
{field.label || field.key || "Unknown Field"}
</label>
<div className="flex-1 min-w-0">
<div className="flex justify-between items-center mb-1">
<label className={`text-xs font-semibold uppercase tracking-wider truncate ${isVerified ? 'text-emerald-700' : 'text-slate-600'}`}>
{field.label || field.key || "Unknown Field"}
</label>
{!isVerified && status !== 'VALID' && (
<span className={`flex items-center text-[10px] font-bold px-1.5 py-0.5 rounded ${status === 'INVALID' ? 'bg-red-100 text-red-700' : 'bg-amber-100 text-amber-700'}`}>
{status === 'INVALID' ? <XCircle className="w-3 h-3 mr-1"/> : <AlertTriangle className="w-3 h-3 mr-1"/>}
{status}
</span>
)}
</div>
{!isVerified && status !== 'VALID' && (
<span className={`flex items-center text-[10px] font-bold px-1.5 py-0.5 rounded ${status === 'INVALID' ? 'bg-red-100 text-red-700' : 'bg-amber-100 text-amber-700'}`}>
{status === 'INVALID' ? <XCircle className="w-3 h-3 mr-1"/> : <AlertTriangle className="w-3 h-3 mr-1"/>}
{status}
</span>
)}
{isVerified && (
<span className="flex items-center text-[10px] font-bold px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-700">
<CheckCircle2 className="w-3 h-3 mr-1"/>
VERIFIED
</span>
<div className="relative">
<input
type="text"
value={field.value}
onChange={(e) => handleUpdate(idx, e.target.value)}
className={`block w-full rounded-md px-2.5 py-1.5 text-sm font-medium transition-colors border ${isVerified ? 'border-emerald-200 text-emerald-900 bg-emerald-50/50' : 'border-slate-300 text-slate-900 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500'}`}
placeholder="Empty"
/>
</div>
{(!isVerified && field.validation?.suggestion && status !== 'VALID') && (
<button
onClick={() => applySuggestion(idx)}
className="mt-2 flex items-center text-xs font-bold text-indigo-600 hover:text-indigo-800 bg-indigo-50 hover:bg-indigo-100 px-2 py-1 rounded transition-colors"
>
<ArrowRight className="w-3 h-3 mr-1" />
Accept: "{field.validation.suggestion}"
</button>
)}
</div>
<div className="relative">
<input
type="text"
value={field.value}
onChange={(e) => handleUpdate(idx, e.target.value)}
className={`block w-full rounded-md px-2.5 py-1.5 text-sm font-medium transition-colors border ${isVerified ? 'border-emerald-200 text-emerald-900 bg-emerald-50/50' : 'border-slate-300 text-slate-900 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500'}`}
placeholder="Empty"
/>
{!isVerified && (
<div className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none text-slate-400">
<Edit2 className="w-3.5 h-3.5 opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
)}
</div>
{(!isVerified && status !== 'VALID' && field.validation?.message) && (
<p className={`mt-1.5 text-xs ${status === 'INVALID' ? 'text-red-600' : 'text-amber-600'}`}>
{field.validation.message}
</p>
)}
{(!isVerified && field.validation?.suggestion && status !== 'VALID') && (
<button
onClick={() => applySuggestion(idx)}
className="mt-2 flex items-center text-xs font-bold text-indigo-600 hover:text-indigo-800 bg-indigo-50 hover:bg-indigo-100 px-2 py-1 rounded transition-colors w-full sm:w-auto"
>
<ArrowRight className="w-3 h-3 mr-1" />
Accept Fix: "{field.validation.suggestion}"
</button>
)}
{!isVerified && field.sourceContext && (
<div className="mt-2 p-2 bg-slate-100 rounded text-[11px] text-slate-500 border border-slate-200">
<span className="font-semibold text-slate-700">Source:</span> "{field.sourceContext}"
</div>
)}
{/* Show PDF field key if available */}
{field.key && (
<div className="mt-1 text-[10px] text-slate-400">
PDF Field: {field.key}
</div>
)}
</div>
</div>
</div>
);
})}
{displayedFields.length === 0 && (
<div className="text-center py-10 text-slate-400">
<CheckCircle2 className="w-12 h-12 mx-auto mb-2 opacity-20" />
<p>All fields verified!</p>
</div>
)}
</div>
{/* Footer */}
<div className="p-4 border-t border-slate-200 bg-white">
<div className="flex justify-between items-center text-xs font-medium text-slate-500">
<span>{verifiedCount} of {totalCount} fields verified</span>
{progressPercent === 100 ? (
<span className="text-emerald-600 font-bold flex items-center">
<CheckCircle2 className="w-4 h-4 mr-1"/> Ready to Download
</span>
) : (
<span className="text-amber-600 flex items-center">
<Circle className="w-4 h-4 mr-1 fill-amber-100"/> Review in progress
</span>
)}
</div>
<div className="md:hidden mt-2 w-full h-1.5 bg-slate-100 rounded-full overflow-hidden">
<div className="h-full bg-emerald-500 transition-all" style={{ width: `${progressPercent}%` }} />
</div>
);
})}
</div>
</div>
</div>
</div>
) : (
/* ================= FORM VIEW (Overlay with Drag & Drop) ================= */
<div className="bg-slate-200 rounded-xl overflow-auto shadow-inner flex-1 border border-slate-300 relative p-8 flex justify-center">
<div
ref={containerRef}
className="relative bg-white shadow-2xl transition-cursor"
style={{
width: '794px',
minHeight: '1123px',
cursor: draggingField !== null ? 'grabbing' : 'default'
}}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
{/* Background Image/PDF */}
{formFile.previewUrl && (
<img
src={formFile.previewUrl}
className="absolute inset-0 w-full h-full object-contain pointer-events-none opacity-90 select-none"
alt="Form Background"
/>
)}
{/* Fallback info if not visual mode compatible */}
{!fields.some(f => f.coordinates) && (
<div className="absolute inset-0 flex items-center justify-center bg-white/80 z-20 backdrop-blur-sm">
<div className="text-center p-6 max-w-md">
<AlertTriangle className="w-12 h-12 text-amber-500 mx-auto mb-4" />
<h3 className="text-lg font-bold text-slate-900">Visual Mode Not Available</h3>
<p className="text-slate-600 mt-2">
Coordinates were not extracted for this document. Please use the List View to edit fields.
</p>
<button
onClick={() => setViewMode('LIST')}
className="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
Switch to List View
</button>
</div>
</div>
)}
{/* Overlay Inputs */}
{fields.map((field, idx) => {
if (!field.coordinates) return null;
// Coordinate conversion (0-1000 scale to percentage)
const left = (field.coordinates.x / 1000) * 100;
const top = (field.coordinates.y / 1000) * 100;
const status = field.validation?.status || 'VALID';
let borderColor = 'border-indigo-400 bg-white/60';
if (field.isVerified) borderColor = 'border-emerald-500 bg-emerald-50/70';
else if (status === 'INVALID') borderColor = 'border-red-500 bg-red-50/70';
else if (status === 'WARNING') borderColor = 'border-amber-500 bg-amber-50/70';
const isDragging = draggingField === idx;
const isCheckbox = field.value === 'X';
return (
<div
key={idx}
className={`absolute group hover:z-50 ${isDragging ? 'z-50 cursor-grabbing' : 'z-10'}`}
style={{
left: `${left}%`,
top: `${top}%`,
width: isCheckbox ? '20px' : '200px',
transform: 'translateY(-50%)', // Center vertically
}}
onMouseDown={(e) => handleDragStart(e, idx)}
>
<div className="relative">
{/* Drag Handle (Visible on Hover) */}
<div className={`absolute -top-3 -left-3 cursor-grab p-1 bg-slate-800 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity ${isDragging ? 'opacity-100' : ''}`}>
<Move className="w-3 h-3" />
</div>
{isCheckbox ? (
<div className={`w-6 h-6 border-2 flex items-center justify-center font-bold text-black ${borderColor}`}>
X
</div>
) : (
<input
type="text"
value={field.value}
onChange={(e) => handleUpdate(idx, e.target.value)}
onFocus={() => setActiveField(idx)}
className={`
w-full px-1 py-0.5 text-xs font-medium border-2 rounded transition-all shadow-sm
focus:ring-2 focus:ring-offset-1 focus:z-10 focus:bg-white
${borderColor}
`}
style={{
fontFamily: 'Courier, monospace',
color: 'black',
background: 'rgba(255, 255, 255, 0.7)'
}}
/>
)}
{/* Validation Icon Overlay */}
{!field.isVerified && status !== 'VALID' && (
<div className="absolute -top-2 -right-2 bg-white rounded-full shadow-md z-20 pointer-events-none">
{status === 'INVALID'
? <XCircle className="w-4 h-4 text-red-500" />
: <AlertTriangle className="w-4 h-4 text-amber-500" />
}
</div>
)}
{/* Tooltip on Hover */}
<div className="absolute opacity-0 group-hover:opacity-100 bottom-full left-0 mb-1 bg-slate-800 text-white text-[10px] px-2 py-1 rounded whitespace-nowrap pointer-events-none z-30 transition-opacity">
{field.label}
</div>
</div>
</div>
);
})}
</div>
</div>
)}
</div>
);
};