import { useState, useCallback } from "react"; import { Car, Scan, RefreshCw, FileArchive, ChevronRight, Layers, CheckCircle2, XCircle, } from "lucide-react"; import { ZipUploader } from "@/components/ZipUploader"; import { CarAngleDiagram } from "@/components/CarAngleDiagram"; import { AngleCard } from "@/components/AngleCard"; import { ImageResultCard } from "@/components/ImageResultCard"; import { ProgressBar } from "@/components/ProgressBar"; import { classifyImage } from "@/services/api"; import type { ImageResult, AngleSummary, ClassificationSession, ProcessingStatus, AngleLabel, } from "@/types"; import { ALL_ANGLES } from "@/types"; export default function Index() { const [status, setStatus] = useState("idle"); const [session, setSession] = useState(null); const [progress, setProgress] = useState({ current: 0, total: 0 }); const [activeTab, setActiveTab] = useState<"diagram" | "images">("diagram"); const [errorMsg, setErrorMsg] = useState(null); const buildSummary = (results: ImageResult[]): AngleSummary[] => { return ALL_ANGLES.map((angle) => { const matching = results.filter((r) => r.angles.includes(angle)); const bestScore = matching.length > 0 ? Math.max( ...matching.map((r) => { const idx = r.angles.indexOf(angle); return idx >= 0 ? (r.scores[idx] ?? 0) : 0; }) ) : 0; return { angle, detected: matching.length > 0, bestScore, detectedIn: matching.map((r) => r.filename), previewUrls: matching.map((r) => r.previewUrl).filter((u): u is string => !!u), }; }); }; const handleFilesReady = useCallback( async (files: File[], zipName: string) => { setStatus("processing"); setErrorMsg(null); setProgress({ current: 0, total: files.length }); const results: ImageResult[] = []; for (let i = 0; i < files.length; i++) { const file = files[i]; const previewUrl = URL.createObjectURL(file); try { const result = await classifyImage(file); results.push({ ...result, previewUrl }); } catch (err) { results.push({ filename: file.name, angles: [], scores: [], review: false, error: err instanceof Error ? err.message : "Request failed", fileDetails: null, previewUrl, }); } setProgress({ current: i + 1, total: files.length }); } const angleSummary = buildSummary(results); setSession({ zipFilename: zipName, processedAt: Date.now(), totalImages: files.length, angleSummary, imageResults: results, }); setStatus("done"); }, [] ); const handleReset = () => { setStatus("idle"); setSession(null); setProgress({ current: 0, total: 0 }); setErrorMsg(null); }; const detectedCount = session?.angleSummary.filter((s) => s.detected).length ?? 0; const missingCount = (session?.angleSummary.length ?? 0) - detectedCount; // Dynamic header stats const allScores = session?.imageResults.flatMap((r) => r.scores).filter((s) => s > 0) ?? []; const avgScore = allScores.length > 0 ? allScores.reduce((a, b) => a + b, 0) / allScores.length : 0; const accuracyLabel = !session ? "—" : avgScore >= 0.85 ? "High" : avgScore >= 0.65 ? "Medium" : "Low"; const accuracyColor = !session ? "text-slate-500" : avgScore >= 0.85 ? "text-emerald-400" : avgScore >= 0.65 ? "text-amber-400" : "text-red-400"; const headerStats = [ { label: session ? "Angles Found" : "Angles Tracked", value: session ? `${detectedCount}/8` : "8", color: session ? detectedCount === 8 ? "text-emerald-400" : detectedCount > 0 ? "text-amber-400" : "text-red-400" : "text-brand-400", }, { label: "Images Processed", value: session ? String(session.totalImages) : "—", color: session ? "text-brand-400" : "text-slate-500", }, { label: "Avg Confidence", value: session ? `${Math.round(avgScore * 100)}%` : "—", color: accuracyColor, }, ]; return (
{/* ── Hero Header ── */}
{/* Logo */}

CarLens AI

Vehicle Angle Classifier

{/* Stats strip */}
{headerStats.map(({ label, value, color }) => (

{value}

{label}

))}
{/* Reset button */} {session && ( )}
{/* ── Upload Section ── */} {status === "idle" && (
{/* Hero copy */}
AI-Powered Angle Detection

Which car angles are{" "} missing?

Upload a ZIP of car photos. Our AI scans each image, classifies its viewing angle, and tells you exactly which angles you still need to capture.

{/* How it works */}
{[ { step: "01", title: "Upload ZIP", desc: "Drop a ZIP archive containing all your car images", icon: FileArchive, color: "text-brand-400", bg: "bg-brand-500/10 border-brand-500/20", }, { step: "02", title: "AI Analysis", desc: "YOLO models detect parts and determine viewing angle per image", icon: Scan, color: "text-cyan-400", bg: "bg-cyan-500/10 border-cyan-500/20", }, { step: "03", title: "Gap Report", desc: "See a visual map of detected vs. missing angles at a glance", icon: Layers, color: "text-emerald-400", bg: "bg-emerald-500/10 border-emerald-500/20", }, ].map(({ step, title, desc, icon: Icon, color, bg }) => (
{step}
{title}

{desc}

))}
)} {/* ── Processing ── */} {status === "processing" && (

Classifying Images

Running YOLO detection on each photo in your ZIP…

)} {/* ── Results ── */} {status === "done" && session && (
{/* Summary header */}
{session.zipFilename}

Analysis Complete

Processed {session.totalImages} image{session.totalImages !== 1 ? "s" : ""} {" "}·{" "} {new Date(session.processedAt).toLocaleString()}

{/* Quick badges */}
{detectedCount} detected
{missingCount > 0 && (
{missingCount} missing
)}
{/* Tabs */}
{(["diagram", "images"] as const).map((tab) => ( ))}
{/* Tab content */} {activeTab === "diagram" && (
{/* Diagram */}

Car Coverage Map

{/* Angle list */}

Angle Breakdown

{session.angleSummary.map((s) => ( ))}
)} {activeTab === "images" && (

Individual Image Results

{session.imageResults.map((result, i) => ( ))}
)} {/* Missing angles callout */} {missingCount > 0 && (

Missing Angles

The following views were not found in any of the uploaded images:

{session.angleSummary .filter((s) => !s.detected) .map((s) => ( {s.angle} ))}
)}
)}
{/* Footer */}
); }