File size: 5,554 Bytes
16533c4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | import { useState } from "react";
import { ChevronDown, ChevronUp, Eye } from "lucide-react";
import type { ImageResult } from "@/types";
import { getImageUrl } from "@/services/api";
interface ImageResultCardProps {
result: ImageResult;
index: number;
}
export function ImageResultCard({ result, index }: ImageResultCardProps) {
const [expanded, setExpanded] = useState(false);
const hasError = !!result.error;
return (
<div
className={[
"rounded-xl border transition-all duration-200",
hasError
? "border-red-500/20 bg-red-500/5"
: "border-surface-500 bg-surface-700 hover:border-surface-400",
].join(" ")}
>
<div
className="flex items-center gap-4 p-4 cursor-pointer"
onClick={() => setExpanded((e) => !e)}
>
{/* Index badge */}
<div className="w-8 h-8 rounded-lg bg-surface-600 flex items-center justify-center flex-shrink-0">
<span className="text-xs font-mono text-slate-400">{index + 1}</span>
</div>
{/* Preview thumbnail */}
{result.previewUrl && (
<div className="w-12 h-12 rounded-lg overflow-hidden flex-shrink-0 border border-surface-500">
<img
src={result.previewUrl}
alt={result.filename}
className="w-full h-full object-cover"
/>
</div>
)}
{/* Name + status */}
<div className="flex-1 min-w-0">
<p className="font-medium text-white text-sm truncate">{result.filename}</p>
{hasError ? (
<p className="text-red-400 text-xs mt-0.5">
{typeof result.error === "string" ? result.error : JSON.stringify(result.error)}
</p>
) : (
<div className="flex items-center gap-2 mt-1 flex-wrap">
{result.angles.map((angle, i) => (
<span
key={i}
className="text-xs px-2 py-0.5 rounded-full bg-brand-500/15 text-brand-400 font-medium border border-brand-500/20"
>
{angle}
</span>
))}
{result.review && (
<span className="text-xs px-2 py-0.5 rounded-full bg-amber-500/15 text-amber-400 font-medium border border-amber-500/20">
Needs review
</span>
)}
</div>
)}
</div>
{/* Expand */}
<div className="flex-shrink-0 text-slate-500">
{expanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</div>
</div>
{/* Expanded view */}
{expanded && !hasError && (
<div className="px-4 pb-4 border-t border-surface-600 pt-4 animate-fade-in space-y-4">
{/* Local preview always shown */}
{result.previewUrl && (
<div>
<p className="text-xs text-slate-400 mb-2 font-medium uppercase tracking-wider">
Uploaded Image
</p>
<div className="rounded-lg overflow-hidden border border-surface-500 max-w-sm">
<img
src={result.previewUrl}
alt={result.filename}
className="w-full object-contain max-h-64 bg-surface-900"
/>
</div>
</div>
)}
{/* Backend detection overlays (only when available) */}
{result.fileDetails && (
<div>
<p className="text-xs text-slate-400 mb-2 font-medium uppercase tracking-wider">
Detection Outputs
</p>
<div className="grid grid-cols-3 gap-3">
{[
{ label: "Original", path: result.fileDetails.main_img_name },
{ label: "Car Detection", path: result.fileDetails.car_detection },
{ label: "Part Detection", path: result.fileDetails.part_detection },
].map(({ label, path }) => (
<a
key={label}
href={getImageUrl(path)}
target="_blank"
rel="noopener noreferrer"
className="group relative rounded-lg overflow-hidden border border-surface-500 hover:border-brand-500/50 transition-colors block"
>
<div className="aspect-square bg-surface-800">
<img
src={getImageUrl(path)}
alt={label}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
onError={(e) => {
e.currentTarget.style.display = "none";
}}
/>
</div>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex items-end p-2">
<span className="text-white text-xs font-medium flex items-center gap-1">
<Eye className="w-3 h-3" /> {label}
</span>
</div>
<p className="text-center text-xs text-slate-400 py-1.5 bg-surface-800">
{label}
</p>
</a>
))}
</div>
</div>
)}
</div>
)}
</div>
);
}
|