PRISM2.0 / frontend /components /SingleAnalysis.tsx
devranx's picture
Modified CPU usage message
5ac57f3
raw
history blame
12.2 kB
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 {
// Call backend to copy sample to uploads folder
await useSample(filename, 'single');
// Set preview
setPreview(`/static/samples/${filename}`);
setImage(null); // Clear file input
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;