github-actions[bot]
Sync from GitHub: 167b65b6ddc3de7f29548d9d6ba060e50653b352
4dbceee
import React, { useRef, useEffect, useState } from 'react';
import { SlidersHorizontal, ChevronDown, ChevronUp, Brain } from 'lucide-react';
const ResultCard = ({ result, imageData, processedImageData, onReprocess, isProcessing }) => {
const canvasRef = useRef(null);
const previewCanvasRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const [signatureCrop, setSignatureCrop] = useState(null);
const [stampCrop, setStampCrop] = useState(null);
const [resolution, setResolution] = useState(result.processedResolution || 100);
const [adjustedDataUrl, setAdjustedDataUrl] = useState(null);
const [previewDimensions, setPreviewDimensions] = useState({ width: 0, height: 0 });
const [currentImageData, setCurrentImageData] = useState(processedImageData || imageData);
const [showReasoning, setShowReasoning] = useState(false);
// Function to crop image regions
const cropRegion = (img, coords, scaleX, scaleY) => {
if (!coords || coords.length === 0) return null;
const [x1, y1, x2, y2] = coords[0];
const width = (x2 - x1) * scaleX;
const height = (y2 - y1) * scaleY;
const cropCanvas = document.createElement('canvas');
cropCanvas.width = width;
cropCanvas.height = height;
const cropCtx = cropCanvas.getContext('2d');
// Draw the cropped region
cropCtx.drawImage(
img,
x1, y1, x2 - x1, y2 - y1,
0, 0, width, height
);
return cropCanvas.toDataURL();
};
// Initialize currentImageData with processedImageData when available
useEffect(() => {
if (processedImageData) {
setCurrentImageData(processedImageData);
}
}, [processedImageData]);
// Main effect to draw image with bounding boxes using currentImageData
useEffect(() => {
if (!currentImageData || !canvasRef.current) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
// Set canvas size to match container while maintaining aspect ratio
const maxWidth = 800;
const maxHeight = 600;
let width = img.width;
let height = img.height;
if (width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}
if (height > maxHeight) {
width = (width * maxHeight) / height;
height = maxHeight;
}
canvas.width = width;
canvas.height = height;
setDimensions({ width, height });
// Draw image
ctx.drawImage(img, 0, 0, width, height);
// Calculate scale factors
const scaleX = width / img.width;
const scaleY = height / img.height;
// Create cropped images for signature and stamp from the CURRENT (processed) image
// This ensures crops match the resolution that was sent to the API
if (result.signature_coords && result.signature_coords.length > 0) {
const sigCrop = cropRegion(img, result.signature_coords, 1, 1);
setSignatureCrop(sigCrop);
}
if (result.stamp_coords && result.stamp_coords.length > 0) {
const stCrop = cropRegion(img, result.stamp_coords, 1, 1);
setStampCrop(stCrop);
}
// Draw bounding boxes for signature
if (result.signature_coords && result.signature_coords.length > 0) {
ctx.strokeStyle = '#ef4444';
ctx.lineWidth = 3;
ctx.setLineDash([5, 5]);
result.signature_coords.forEach(coords => {
const [x1, y1, x2, y2] = coords;
ctx.strokeRect(
x1 * scaleX,
y1 * scaleY,
(x2 - x1) * scaleX,
(y2 - y1) * scaleY
);
});
// Add label
ctx.fillStyle = '#ef4444';
ctx.font = 'bold 14px Arial';
ctx.fillText('Signature', result.signature_coords[0][0] * scaleX, result.signature_coords[0][1] * scaleY - 5);
}
// Draw bounding boxes for stamp
if (result.stamp_coords && result.stamp_coords.length > 0) {
ctx.strokeStyle = '#3b82f6';
ctx.lineWidth = 3;
ctx.setLineDash([5, 5]);
result.stamp_coords.forEach(coords => {
const [x1, y1, x2, y2] = coords;
ctx.strokeRect(
x1 * scaleX,
y1 * scaleY,
(x2 - x1) * scaleX,
(y2 - y1) * scaleY
);
});
// Add label
ctx.fillStyle = '#3b82f6';
ctx.font = 'bold 14px Arial';
ctx.fillText('Stamp', result.stamp_coords[0][0] * scaleX, result.stamp_coords[0][1] * scaleY - 5);
}
};
img.src = currentImageData;
}, [currentImageData, imageData, result]);
// Handle resolution adjustment for preview and update currentImageData
useEffect(() => {
if (!imageData || !previewCanvasRef.current) return;
const canvas = previewCanvasRef.current;
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
const scale = resolution / 100;
const newWidth = Math.floor(img.width * scale);
const newHeight = Math.floor(img.height * scale);
canvas.width = newWidth;
canvas.height = newHeight;
setPreviewDimensions({ width: newWidth, height: newHeight });
ctx.drawImage(img, 0, 0, newWidth, newHeight);
// Generate adjusted data URL
const adjustedUrl = canvas.toDataURL('image/jpeg', 0.95);
setAdjustedDataUrl(adjustedUrl);
// Update the current image data to reflect resolution change
setCurrentImageData(adjustedUrl);
};
img.src = imageData;
}, [imageData, resolution]);
if (!result.success) {
return (
<div className="bg-red-50 border-2 border-red-200 rounded-xl p-6 mb-4">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">Processing Error</h3>
<div className="mt-2 text-sm text-red-700">
<p><strong>File:</strong> {result.filename}</p>
<p><strong>Error:</strong> {result.error}</p>
</div>
</div>
</div>
</div>
);
}
return (
<div className="bg-white rounded-xl shadow-lg overflow-hidden mb-6 border border-gray-200">
{/* Header */}
<div className="bg-gradient-to-r from-primary-500 to-primary-600 px-6 py-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-white flex items-center">
<svg className="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
{result.filename}
</h3>
{result.pageNumber && (
<p className="text-primary-100 text-sm mt-1">Page {result.pageNumber}</p>
)}
</div>
{onReprocess && (
<button
onClick={() => onReprocess(result, resolution, adjustedDataUrl)}
disabled={isProcessing}
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Reprocess
</button>
)}
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 p-6">
{/* Image with bounding boxes */}
<div className="space-y-4">
<h4 className="text-md font-semibold text-gray-700 flex items-center">
<svg className="w-5 h-5 mr-2 text-primary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Document Preview
</h4>
{/* Resolution Slider */}
<div className="bg-white rounded-lg p-4 shadow-sm border border-gray-200">
<div className="flex items-center justify-between mb-3">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<SlidersHorizontal className="w-4 h-4 text-primary-500" />
Adjust Resolution for Reprocessing
</label>
<span className="text-sm font-semibold text-primary-600">{resolution}%</span>
</div>
<input
type="range"
min="10"
max="100"
step="5"
value={resolution}
onChange={(e) => setResolution(parseInt(e.target.value))}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb"
disabled={isProcessing}
/>
<div className="flex justify-between text-xs text-gray-500 mt-2">
<span>10% (Fast)</span>
<span className="text-gray-400">Dimensions: {previewDimensions.width} × {previewDimensions.height}px</span>
<span>100% (Best)</span>
</div>
</div>
<div className="relative bg-gray-50 rounded-lg p-4 flex justify-center items-center">
<canvas ref={canvasRef} className="max-w-full h-auto rounded shadow-md" />
{isProcessing && (
<div className="absolute inset-0 flex items-center justify-center bg-black/10 rounded-lg">
<div className="scanning-line"></div>
</div>
)}
</div>
<div className="flex gap-4 text-sm">
{result.signature_coords && result.signature_coords.length > 0 && (
<div className="flex items-center">
<div className="w-4 h-4 border-2 border-red-500 mr-2"></div>
<span className="text-gray-600">Signature Detected</span>
</div>
)}
{result.stamp_coords && result.stamp_coords.length > 0 && (
<div className="flex items-center">
<div className="w-4 h-4 border-2 border-blue-500 mr-2"></div>
<span className="text-gray-600">Stamp Detected</span>
</div>
)}
</div>
</div>
{/* Extracted Information */}
<div className="space-y-4">
<h4 className="text-md font-semibold text-gray-700 flex items-center">
<svg className="w-5 h-5 mr-2 text-primary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Extracted Information
</h4>
<div className="bg-gray-50 rounded-lg p-4 space-y-3">
{/* Performance Metrics */}
<div className="grid grid-cols-3 gap-3">
{result.processing_time !== undefined && (
<div className="bg-white rounded-lg p-3 shadow-sm text-center">
<div className="text-xs text-gray-500 mb-1">Processing Time</div>
<div className="text-lg font-bold text-blue-600">{result.processing_time.toFixed(2)}s</div>
</div>
)}
{result.confidence !== undefined && (
<div className="bg-white rounded-lg p-3 shadow-sm text-center">
<div className="text-xs text-gray-500 mb-1">Confidence</div>
<div className="text-lg font-bold text-green-600">{(result.confidence * 100).toFixed(1)}%</div>
</div>
)}
{result.cost_estimate_usd !== undefined && (
<div className="bg-white rounded-lg p-3 shadow-sm text-center">
<div className="text-xs text-gray-500 mb-1">Cost</div>
<div className="text-lg font-bold text-purple-600">${result.cost_estimate_usd.toFixed(4)}</div>
</div>
)}
</div>
<div className="bg-white rounded-lg p-4 shadow-sm">
<h5 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">Extracted Text</h5>
<div className="text-sm text-gray-800 whitespace-pre-wrap max-h-96 overflow-y-auto font-mono bg-gray-50 p-3 rounded border border-gray-200">
{result.extracted_text || 'No text extracted'}
</div>
</div>
{/* Reasoning Output (Chain of Thought) */}
{result.timing_breakdown?.reasoning_output && (
<div className="bg-blue-50 rounded-lg border border-blue-200 overflow-hidden">
<button
onClick={() => setShowReasoning(!showReasoning)}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-blue-100 transition-colors"
>
<div className="flex items-center gap-2">
<Brain className="w-5 h-5 text-blue-600" />
<h5 className="text-sm font-semibold text-blue-700 uppercase tracking-wide">
Reasoning Output
</h5>
</div>
{showReasoning ? (
<ChevronUp className="w-5 h-5 text-blue-600" />
) : (
<ChevronDown className="w-5 h-5 text-blue-600" />
)}
</button>
{showReasoning && (
<div className="px-4 pb-4">
<div className="text-xs text-blue-600 mb-2">
This is the model's reasoning before extracting structured fields
</div>
<div className="text-sm text-gray-800 whitespace-pre-wrap max-h-96 overflow-y-auto font-mono bg-white p-3 rounded border border-blue-300">
{result.timing_breakdown.reasoning_output}
</div>
</div>
)}
</div>
)}
{/* Detection Status */}
<div className="grid grid-cols-2 gap-3">
<div className="bg-white rounded-lg p-4 shadow-sm">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">Signature</span>
{result.signature_coords && result.signature_coords.length > 0 ? (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<svg className="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
Detected
</span>
) : (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
Not Found
</span>
)}
</div>
</div>
<div className="bg-white rounded-lg p-4 shadow-sm">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">Stamp</span>
{result.stamp_coords && result.stamp_coords.length > 0 ? (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<svg className="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
Detected
</span>
) : (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
Not Found
</span>
)}
</div>
</div>
</div>
{/* Cropped Signature and Stamp */}
{(signatureCrop || stampCrop) && (
<div className="bg-white rounded-lg p-4 shadow-sm">
<h5 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">Extracted Elements</h5>
<div className="grid grid-cols-2 gap-4">
{signatureCrop && (
<div className="space-y-2">
<div className="text-xs font-medium text-red-600 uppercase">Signature</div>
<div className="border-2 border-red-200 rounded-lg p-2 bg-gray-50">
<img src={signatureCrop} alt="Signature" className="w-full h-auto" />
</div>
</div>
)}
{stampCrop && (
<div className="space-y-2">
<div className="text-xs font-medium text-blue-600 uppercase">Stamp</div>
<div className="border-2 border-blue-200 rounded-lg p-2 bg-gray-50">
<img src={stampCrop} alt="Stamp" className="w-full h-auto" />
</div>
</div>
)}
</div>
</div>
)}
{/* Coordinates Info */}
{(result.signature_coords?.length > 0 || result.stamp_coords?.length > 0) && (
<div className="bg-white rounded-lg p-4 shadow-sm">
<h5 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">Coordinates</h5>
<div className="text-xs space-y-2">
{result.signature_coords?.length > 0 && (
<div>
<span className="font-medium text-red-600">Signature:</span>
<code className="ml-2 text-gray-700">
{JSON.stringify(result.signature_coords)}
</code>
</div>
)}
{result.stamp_coords?.length > 0 && (
<div>
<span className="font-medium text-blue-600">Stamp:</span>
<code className="ml-2 text-gray-700">
{JSON.stringify(result.stamp_coords)}
</code>
</div>
)}
</div>
</div>
)}
</div>
</div>
</div>
{/* Hidden canvas for resolution preview generation */}
<canvas ref={previewCanvasRef} style={{ display: 'none' }} />
</div>
);
};
export default ResultCard;