|
|
import React, { useState, useRef, useEffect } from 'react'; |
|
|
import { useNavigate } from 'react-router-dom'; |
|
|
import { CheckCircleIcon, XCircleIcon, UploadIcon, ScanIcon, ArrowLeftIcon } from './Icons'; |
|
|
import { SingleAnalysisReport } from '../types'; |
|
|
import { uploadSingle, classifySingle, getSamples, useSample } from '../services/apiService'; |
|
|
|
|
|
interface SingleAnalysisProps { |
|
|
onBack: () => void; |
|
|
} |
|
|
|
|
|
const SingleAnalysis: React.FC = () => { |
|
|
const navigate = useNavigate(); |
|
|
const [image, setImage] = useState<File | null>(null); |
|
|
const [selectedSample, setSelectedSample] = useState<string | null>(null); |
|
|
const [preview, setPreview] = useState<string | null>(null); |
|
|
const [loading, setLoading] = useState(false); |
|
|
const [report, setReport] = useState<SingleAnalysisReport | null>(null); |
|
|
const [samples, setSamples] = useState<{ id: number, url: string, filename: string }[]>([]); |
|
|
const fileInputRef = useRef<HTMLInputElement>(null); |
|
|
|
|
|
useEffect(() => { |
|
|
const fetchSamples = async () => { |
|
|
try { |
|
|
const data = await getSamples(); |
|
|
if (Array.isArray(data)) { |
|
|
setSamples(data); |
|
|
} |
|
|
} catch (err) { |
|
|
console.error("Failed to fetch samples", err); |
|
|
} |
|
|
}; |
|
|
fetchSamples(); |
|
|
}, []); |
|
|
|
|
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
|
|
if (e.target.files && e.target.files[0]) { |
|
|
const file = e.target.files[0]; |
|
|
setImage(file); |
|
|
setSelectedSample(null); |
|
|
setPreview(URL.createObjectURL(file)); |
|
|
setReport(null); |
|
|
} |
|
|
}; |
|
|
|
|
|
const runClassification = async (filename: string) => { |
|
|
setLoading(true); |
|
|
try { |
|
|
const result = await classifySingle(filename); |
|
|
setReport(result); |
|
|
} catch (err) { |
|
|
console.error(err); |
|
|
alert("Analysis failed. Please try again."); |
|
|
} finally { |
|
|
setLoading(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
const handleRunAnalysis = async () => { |
|
|
if (image) { |
|
|
setLoading(true); |
|
|
try { |
|
|
const filename = await uploadSingle(image); |
|
|
await runClassification(filename); |
|
|
} catch (err) { |
|
|
console.error(err); |
|
|
alert("Upload failed."); |
|
|
setLoading(false); |
|
|
} |
|
|
} else if (selectedSample) { |
|
|
await runClassification(selectedSample); |
|
|
} |
|
|
}; |
|
|
|
|
|
const handleSampleSelect = async (filename: string) => { |
|
|
setLoading(true); |
|
|
try { |
|
|
|
|
|
await useSample(filename, 'single'); |
|
|
|
|
|
|
|
|
setPreview(`/static/samples/${filename}`); |
|
|
setImage(null); |
|
|
setSelectedSample(filename); |
|
|
setReport(null); |
|
|
} catch (err) { |
|
|
console.error("Failed to use sample", err); |
|
|
alert("Failed to load sample."); |
|
|
} finally { |
|
|
setLoading(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
const reset = () => { |
|
|
setImage(null); |
|
|
setSelectedSample(null); |
|
|
setPreview(null); |
|
|
setReport(null); |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<div className="min-h-screen flex flex-col p-4 md:p-8 max-w-7xl mx-auto"> |
|
|
<header className="flex items-center justify-between mb-8"> |
|
|
<h2 className="text-2xl font-light tracking-wide">Single Image <span className="font-bold text-cyan-400">Analysis</span></h2> |
|
|
</header> |
|
|
|
|
|
<div className="flex flex-col lg:flex-row gap-8 flex-1"> |
|
|
{/* Left: Upload / Preview */} |
|
|
<div className="flex-1 flex flex-col gap-6"> |
|
|
<div className="glass-panel rounded-3xl p-8 flex flex-col items-center justify-center min-h-[400px] relative overflow-hidden group"> |
|
|
<input |
|
|
type="file" |
|
|
ref={fileInputRef} |
|
|
onChange={handleFileChange} |
|
|
className="hidden" |
|
|
accept="image/*" |
|
|
/> |
|
|
|
|
|
{preview ? ( |
|
|
<div className="relative w-full h-full flex items-center justify-center"> |
|
|
<img |
|
|
src={preview} |
|
|
alt="Preview" |
|
|
className="max-h-[500px] w-auto object-contain rounded-lg shadow-2xl" |
|
|
/> |
|
|
<button |
|
|
onClick={() => { |
|
|
setPreview(null); |
|
|
setImage(null); |
|
|
setSelectedSample(null); |
|
|
setReport(null); |
|
|
}} |
|
|
className="absolute top-4 right-4 p-2 bg-black/50 hover:bg-black/70 rounded-full text-white transition-colors" |
|
|
> |
|
|
<XCircleIcon /> |
|
|
</button> |
|
|
</div> |
|
|
) : ( |
|
|
<div |
|
|
onClick={() => fileInputRef.current?.click()} |
|
|
className="cursor-pointer flex flex-col items-center text-center p-8 border-2 border-dashed border-slate-700 hover:border-cyan-500 rounded-2xl transition-colors w-full h-full justify-center" |
|
|
> |
|
|
<div className="w-20 h-20 bg-slate-800 rounded-full flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"> |
|
|
<UploadIcon /> |
|
|
</div> |
|
|
<h3 className="text-xl font-medium text-white mb-2">Upload Image</h3> |
|
|
<p className="text-slate-400 max-w-xs">Drag & drop or click to browse</p> |
|
|
<p className="text-xs text-slate-500 mt-4">Supports PNG, JPG, JPEG</p> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
|
|
|
{/* Sample Gallery */} |
|
|
<div className="glass-panel rounded-2xl p-6 flex flex-col max-h-[600px] min-h-0"> |
|
|
<p className="text-sm text-slate-400 mb-4 uppercase tracking-wider font-semibold flex-shrink-0">Or try a sample</p> |
|
|
<div className="grid grid-cols-2 gap-4 overflow-y-auto custom-scrollbar pr-2 flex-1"> |
|
|
{samples.map((sample) => { |
|
|
const isSelected = preview === sample.url; |
|
|
return ( |
|
|
<div |
|
|
key={sample.id} |
|
|
className={`group relative w-full h-64 rounded-xl overflow-hidden cursor-pointer border-2 transition-all duration-300 ${isSelected ? 'border-cyan-400 ring-2 ring-cyan-400/50' : 'border-slate-700 hover:border-cyan-500' |
|
|
}`} |
|
|
onClick={() => handleSampleSelect(sample.filename)} |
|
|
> |
|
|
<img |
|
|
src={sample.url} |
|
|
alt={`Sample ${sample.id}`} |
|
|
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" |
|
|
/> |
|
|
<div className={`absolute inset-0 transition-colors duration-300 ${isSelected ? 'bg-cyan-500/20' : 'bg-black/0 group-hover:bg-black/20' |
|
|
}`}> |
|
|
{isSelected && ( |
|
|
<div className="absolute top-2 right-2 bg-cyan-500 rounded-full p-1"> |
|
|
<CheckCircleIcon className="w-4 h-4 text-white" /> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
})} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{/* Right: Report Area */} |
|
|
<div className="flex-1 flex flex-col"> |
|
|
{loading && ( |
|
|
<div className="flex-1 flex flex-col items-center justify-center glass-panel rounded-3xl p-12 text-center animate-pulse"> |
|
|
<div className="relative w-24 h-24 mb-8"> |
|
|
<div className="absolute inset-0 border-4 border-cyan-500/30 rounded-full"></div> |
|
|
<div className="absolute inset-0 border-4 border-t-cyan-400 border-r-transparent border-b-transparent border-l-transparent rounded-full animate-spin"></div> |
|
|
<div className="absolute inset-0 flex items-center justify-center"> |
|
|
<ScanIcon className="w-8 h-8 text-cyan-400 animate-pulse" /> |
|
|
</div> |
|
|
</div> |
|
|
<h3 className="text-2xl font-bold text-white mb-2">Analyzing Image</h3> |
|
|
<p className="text-cyan-400/80 animate-pulse mb-4">Running AI classification models...</p> |
|
|
<div className="bg-yellow-500/10 border border-yellow-500/20 rounded-lg px-4 py-2 max-w-xs mx-auto"> |
|
|
<p className="text-yellow-200/80 text-xs"> |
|
|
⚠️ Note: Classification runs on CPU and may take up to a minute or two. |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{!report && !loading && ( |
|
|
<div className="flex-1 flex flex-col items-center justify-center glass-panel rounded-3xl p-12 text-center"> |
|
|
{!image && !selectedSample && ( |
|
|
<> |
|
|
<div className="w-16 h-16 bg-slate-800 rounded-2xl flex items-center justify-center mb-4 text-slate-500"> |
|
|
<ScanIcon /> |
|
|
</div> |
|
|
<p className="text-slate-400 text-lg">Upload an image or select a sample to generate a compliance report.</p> |
|
|
</> |
|
|
)} |
|
|
|
|
|
{(image || selectedSample) && ( |
|
|
<button |
|
|
onClick={handleRunAnalysis} |
|
|
className="mt-8 bg-cyan-500 hover:bg-cyan-400 text-black font-bold py-4 px-12 rounded-xl transition-all hover:shadow-[0_0_30px_rgba(34,211,238,0.4)]" |
|
|
> |
|
|
Run Classification |
|
|
</button> |
|
|
)} |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{report && ( |
|
|
<div className="flex-1 glass-panel rounded-3xl p-8 overflow-y-auto animate-fade-in flex flex-col"> |
|
|
<div className="flex items-center justify-between mb-6 pb-4 border-b border-white/10"> |
|
|
<h3 className="text-xl font-semibold text-white">Compliance Report</h3> |
|
|
<div className="px-3 py-1 rounded-full bg-cyan-500/10 text-cyan-400 text-sm border border-cyan-500/20"> |
|
|
AI Verified |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{/* DEBUG: Check what we are receiving */} |
|
|
{/* <pre className="text-xs text-slate-500 mb-4 overflow-auto max-h-20">{JSON.stringify(report, null, 2)}</pre> */} |
|
|
|
|
|
{/* Render Tailwind Table */} |
|
|
<div className="flex-1 overflow-x-auto"> |
|
|
<table className="w-full text-left border-collapse"> |
|
|
<thead> |
|
|
<tr className="border-b border-white/10 text-slate-400 text-sm uppercase tracking-wider"> |
|
|
<th className="py-3 px-4 font-medium">Label</th> |
|
|
<th className="py-3 px-4 font-medium text-right">Result</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody className="divide-y divide-white/5"> |
|
|
{report.detailed_results && report.detailed_results.map(([label, result], index) => { |
|
|
const isFail = String(result).startsWith('1') || result === 1; |
|
|
return ( |
|
|
<tr key={index} className="hover:bg-white/5 transition-colors"> |
|
|
<td className="py-3 px-4 text-slate-300 font-medium">{label}</td> |
|
|
<td className="py-3 px-4 text-right"> |
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${isFail |
|
|
? 'bg-red-500/10 text-red-400 border border-red-500/20' |
|
|
: 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' |
|
|
}`}> |
|
|
{isFail ? 'Fail' : 'Pass'} |
|
|
</span> |
|
|
</td> |
|
|
</tr> |
|
|
); |
|
|
})} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
|
|
|
<div className="mt-8 pt-6 border-t border-white/10"> |
|
|
<button onClick={reset} className="w-full py-4 rounded-xl bg-white/5 hover:bg-white/10 text-white font-medium transition-colors border border-white/10"> |
|
|
Analyze Another Image |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default SingleAnalysis; |