| 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> |
| ); |
| } |
|
|