Download src/components/ImageResultCard.tsx from jayvatliq/car-view-classifier-frontend: direct link, hf CLI and curl.
- Browser
- Download file 5.55 kB
-
https://huggingface.co/spaces/jayvatliq/car-view-classifier-frontend/resolve/main/src/components/ImageResultCard.tsx
- Command line
-
hf download hf://spaces/jayvatliq/car-view-classifier-frontend/src/components/ImageResultCard.tsx
-
curl -L -o ImageResultCard.tsx https://huggingface.co/spaces/jayvatliq/car-view-classifier-frontend/resolve/main/src/components/ImageResultCard.tsx
5.55 kB
| 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> | |
| ); | |
| } | |