|
|
import React, { useState, useEffect, useRef } from 'react'; |
|
|
import { SlidersHorizontal, Sparkles, Brain } from 'lucide-react'; |
|
|
|
|
|
const ImagePreview = ({ imageData, fileName, onResolutionChange, onEnhanceToggle, isEnhanced, onReasoningModeToggle, useReasoning }) => { |
|
|
const [resolution, setResolution] = useState(100); |
|
|
const canvasRef = useRef(null); |
|
|
const [originalDimensions, setOriginalDimensions] = useState({ width: 0, height: 0 }); |
|
|
const [currentDimensions, setCurrentDimensions] = useState({ width: 0, height: 0 }); |
|
|
|
|
|
useEffect(() => { |
|
|
if (!imageData || !canvasRef.current) return; |
|
|
|
|
|
const canvas = canvasRef.current; |
|
|
const ctx = canvas.getContext('2d'); |
|
|
const img = new Image(); |
|
|
|
|
|
img.onload = () => { |
|
|
if (!originalDimensions.width) { |
|
|
setOriginalDimensions({ width: img.width, height: img.height }); |
|
|
} |
|
|
|
|
|
|
|
|
const newWidth = Math.floor(img.width * (resolution / 100)); |
|
|
const newHeight = Math.floor(img.height * (resolution / 100)); |
|
|
|
|
|
setCurrentDimensions({ width: newWidth, height: newHeight }); |
|
|
|
|
|
|
|
|
const displayWidth = Math.min(400, newWidth); |
|
|
const displayHeight = Math.floor(newHeight * (displayWidth / newWidth)); |
|
|
|
|
|
canvas.width = displayWidth; |
|
|
canvas.height = displayHeight; |
|
|
|
|
|
|
|
|
ctx.drawImage(img, 0, 0, displayWidth, displayHeight); |
|
|
|
|
|
|
|
|
if (onResolutionChange) { |
|
|
const resizedCanvas = document.createElement('canvas'); |
|
|
resizedCanvas.width = newWidth; |
|
|
resizedCanvas.height = newHeight; |
|
|
const resizedCtx = resizedCanvas.getContext('2d'); |
|
|
resizedCtx.drawImage(img, 0, 0, newWidth, newHeight); |
|
|
const resizedDataUrl = resizedCanvas.toDataURL('image/jpeg', 0.95); |
|
|
onResolutionChange(resizedDataUrl, resolution); |
|
|
} |
|
|
}; |
|
|
|
|
|
img.src = imageData; |
|
|
}, [imageData, resolution]); |
|
|
|
|
|
const handleResolutionChange = (e) => { |
|
|
const newResolution = parseInt(e.target.value); |
|
|
setResolution(newResolution); |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<div className="bg-white rounded-lg shadow-md p-4 space-y-3"> |
|
|
<div className="flex items-center justify-between"> |
|
|
<h4 className="text-sm font-semibold text-gray-700">Preview: {fileName}</h4> |
|
|
<span className="text-xs text-gray-500"> |
|
|
{currentDimensions.width} × {currentDimensions.height}px |
|
|
</span> |
|
|
</div> |
|
|
|
|
|
<div className="bg-gray-50 rounded-lg p-3 flex justify-center"> |
|
|
<canvas ref={canvasRef} className="rounded shadow-sm" /> |
|
|
</div> |
|
|
|
|
|
{/* Enhance Button */} |
|
|
<button |
|
|
onClick={() => onEnhanceToggle && onEnhanceToggle()} |
|
|
className={`w-full py-2 px-4 rounded-lg font-medium transition-all flex items-center justify-center gap-2 ${ |
|
|
isEnhanced |
|
|
? 'bg-purple-600 hover:bg-purple-700 text-white shadow-lg' |
|
|
: 'bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white shadow-md' |
|
|
}`} |
|
|
> |
|
|
<Sparkles className="w-4 h-4" /> |
|
|
{isEnhanced ? 'Enhanced ✓' : 'Enhance Image'} |
|
|
</button> |
|
|
|
|
|
{isEnhanced && ( |
|
|
<div className="bg-purple-50 border border-purple-200 rounded p-2 text-xs text-purple-700"> |
|
|
✨ Image will be enhanced with OpenCV (CLAHE, denoising, sharpening) before processing |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{/* Reasoning Mode Toggle */} |
|
|
<button |
|
|
onClick={() => onReasoningModeToggle && onReasoningModeToggle()} |
|
|
className={`w-full py-2 px-4 rounded-lg font-medium transition-all flex items-center justify-center gap-2 ${ |
|
|
useReasoning |
|
|
? 'bg-blue-600 hover:bg-blue-700 text-white shadow-lg' |
|
|
: 'bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600 text-white shadow-md' |
|
|
}`} |
|
|
> |
|
|
<Brain className="w-4 h-4" /> |
|
|
{useReasoning ? 'Reasoning ✓' : 'Simple Mode'} |
|
|
</button> |
|
|
|
|
|
{useReasoning && ( |
|
|
<div className="bg-blue-50 border border-blue-200 rounded p-2 text-xs text-blue-700"> |
|
|
🧠 VLM will use 2-step reasoning: first analyze document structure, then extract fields |
|
|
</div> |
|
|
)} |
|
|
|
|
|
<div className="space-y-2"> |
|
|
<div className="flex items-center justify-between"> |
|
|
<label className="text-sm font-medium text-gray-700 flex items-center gap-2"> |
|
|
<SlidersHorizontal className="w-4 h-4" /> |
|
|
Resolution |
|
|
</label> |
|
|
<span className="text-sm font-bold text-primary-600">{resolution}%</span> |
|
|
</div> |
|
|
|
|
|
<input |
|
|
type="range" |
|
|
min="10" |
|
|
max="100" |
|
|
value={resolution} |
|
|
onChange={handleResolutionChange} |
|
|
className="w-full h-2 rounded-lg cursor-pointer" |
|
|
style={{ accentColor: '#2563eb' }} // Tailwind blue-600 |
|
|
/> |
|
|
|
|
|
|
|
|
<div className="flex justify-between text-xs text-gray-500"> |
|
|
<span>Low Quality</span> |
|
|
<span>Original</span> |
|
|
</div> |
|
|
|
|
|
{resolution < 100 && ( |
|
|
<div className="bg-blue-50 border border-blue-200 rounded p-2 text-xs text-blue-700"> |
|
|
💡 Lower resolution = faster processing & lower cost |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
|
|
|
<div className="text-xs text-gray-500 space-y-1"> |
|
|
<div className="flex justify-between"> |
|
|
<span>Original:</span> |
|
|
<span className="font-medium">{originalDimensions.width} × {originalDimensions.height}px</span> |
|
|
</div> |
|
|
<div className="flex justify-between"> |
|
|
<span>Processing:</span> |
|
|
<span className="font-medium text-primary-600">{currentDimensions.width} × {currentDimensions.height}px</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default ImagePreview; |
|
|
|