UI changed, added dashboard

This commit is contained in:
sumona-banerjeee
2026-02-06 17:07:43 +05:30
parent 86c23e01ad
commit ae16cec5ff
20 changed files with 2478 additions and 460 deletions

337
app/dashboard/page.tsx Normal file
View File

@@ -0,0 +1,337 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import {
Plus,
Loader2,
AlertCircle,
TrendingUp,
MapPin as MapPinIcon,
BarChart3,
AlertTriangle,
RectangleHorizontal
} from "lucide-react"
import { SidebarNavigation } from "@/components/sidebar-navigation"
import { GradientStatsCard } from "@/components/dashboard/gradient-stats-card"
import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector"
import { DetectionDonutChart } from "@/components/dashboard/detection-donut-chart"
import { LocationBarChart } from "@/components/dashboard/location-bar-chart"
import { DashboardMap } from "@/components/dashboard/dashboard-map"
import {
fetchProjects,
type Project
} from "@/lib/api"
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8000/api/v1"
interface ProjectSummary {
project: {
id: string
name: string
corridor_name: string | null
state: string | null
}
packages: {
[key: string]: {
package_id: string
region: string | null
locations: {
[key: string]: {
location_id: string
chainage: string | null
detection_count: number
detections: Array<{
id: number
type: string
class: string
confidence: number
latitude: number
longitude: number
}>
}
}
}
}
}
interface DetectionStats {
totalPotholes: number
totalSignboards: number
totalDetections: number
locationData: Array<{
name: string
potholes: number
signboards: number
total: number
}>
}
function calculateStats(summary: ProjectSummary | null): DetectionStats {
if (!summary) {
return { totalPotholes: 0, totalSignboards: 0, totalDetections: 0, locationData: [] }
}
let totalPotholes = 0
let totalSignboards = 0
const locationData: DetectionStats["locationData"] = []
for (const pkg of Object.values(summary.packages || {})) {
for (const [locName, loc] of Object.entries(pkg.locations || {})) {
let locPotholes = 0
let locSignboards = 0
for (const detection of loc.detections || []) {
if (detection.type.toLowerCase().includes("pothole")) {
locPotholes++
totalPotholes++
} else {
locSignboards++
totalSignboards++
}
}
if (locPotholes > 0 || locSignboards > 0) {
const shortName = locName.length > 20 ? locName.substring(0, 20) + "..." : locName
locationData.push({
name: shortName,
potholes: locPotholes,
signboards: locSignboards,
total: locPotholes + locSignboards
})
}
}
}
return {
totalPotholes,
totalSignboards,
totalDetections: totalPotholes + totalSignboards,
locationData
}
}
export default function DashboardPage() {
const router = useRouter()
const [isLoading, setIsLoading] = useState(true)
const [projects, setProjects] = useState<Project[]>([])
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
const [projectSummary, setProjectSummary] = useState<ProjectSummary | null>(null)
const [stats, setStats] = useState<DetectionStats>({
totalPotholes: 0,
totalSignboards: 0,
totalDetections: 0,
locationData: []
})
const [error, setError] = useState<string | null>(null)
// Load projects on mount
useEffect(() => {
const loadProjects = async () => {
try {
setIsLoading(true)
setError(null)
const projectsData = await fetchProjects()
setProjects(projectsData)
if (projectsData.length > 0) {
setSelectedProjectId(projectsData[0].id)
}
} catch (err) {
console.error("Failed to load projects:", err)
setError("Failed to load projects. Please check if the backend is running.")
} finally {
setIsLoading(false)
}
}
loadProjects()
}, [])
// Load project summary when project changes
useEffect(() => {
if (!selectedProjectId) {
setProjectSummary(null)
setStats({ totalPotholes: 0, totalSignboards: 0, totalDetections: 0, locationData: [] })
return
}
const loadProjectSummary = async () => {
try {
setIsLoading(true)
setError(null)
const response = await fetch(`${API_URL}/summary/projects/${selectedProjectId}`, {
headers: {
"Content-Type": "application/json",
"ngrok-skip-browser-warning": "true"
}
})
if (!response.ok) {
throw new Error(`API Error: ${response.status}`)
}
const summary: ProjectSummary = await response.json()
setProjectSummary(summary)
setStats(calculateStats(summary))
} catch (err) {
console.error("Failed to load project summary:", err)
setError("Failed to load project summary.")
setProjectSummary(null)
setStats({ totalPotholes: 0, totalSignboards: 0, totalDetections: 0, locationData: [] })
} finally {
setIsLoading(false)
}
}
loadProjectSummary()
}, [selectedProjectId])
const handleNewAnalysis = () => {
router.push("/")
}
const selectedProject = projects.find(p => p.id === selectedProjectId)
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950">
{/* Sidebar Navigation */}
<SidebarNavigation />
{/* Main Content - offset by sidebar width */}
<main className="ml-16 min-h-screen">
<div className="p-6 max-w-[1600px] mx-auto">
{/* Header */}
<div className="mb-6 animate-in fade-in slide-in-from-top duration-500">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent">
VisionRoad Analytics Dashboard
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
A Comprehensive Overview Of Your Road Infrastructure Analysis
</p>
</div>
<Button
onClick={handleNewAnalysis}
className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white shadow-lg shadow-indigo-500/25 hover:shadow-indigo-500/40 transition-all duration-300"
>
<Plus className="h-4 w-4 mr-2" />
New Analysis
</Button>
</div>
</div>
{/* Error Display */}
{error && (
<div className="mb-4 flex items-center gap-2 p-3 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 text-sm animate-in fade-in duration-300">
<AlertCircle className="h-4 w-4 flex-shrink-0" />
<p>{error}</p>
</div>
)}
{/* Stats Cards - Top Row */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6 animate-in fade-in slide-in-from-bottom duration-500">
<GradientStatsCard
title="Total Detections"
subtitle="All detected objects"
value={stats.totalDetections}
icon={TrendingUp}
gradient="green"
isLoading={isLoading}
/>
<GradientStatsCard
title="Potholes"
subtitle="Road surface damage"
value={stats.totalPotholes}
icon={AlertTriangle}
gradient="coral"
isLoading={isLoading}
/>
<GradientStatsCard
title="Signboards"
subtitle="Traffic signs detected"
value={stats.totalSignboards}
icon={RectangleHorizontal}
gradient="blue"
isLoading={isLoading}
/>
</div>
{/* Project Selector - Above main content */}
<div className="mb-6 animate-in fade-in slide-in-from-bottom duration-500 delay-100">
<CompactProjectSelector
projects={projects}
selectedProjectId={selectedProjectId}
onProjectChange={setSelectedProjectId}
selectedProject={selectedProject}
isLoading={isLoading}
/>
</div>
{/* Main Content Grid - Map Left, Charts Right */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 animate-in fade-in slide-in-from-bottom duration-500 delay-200">
{/* Left Side - Map */}
<div>
<DashboardMap className="h-auto" />
</div>
{/* Right Side - Stacked Charts */}
<div className="space-y-4">
{/* Detection Distribution */}
<Card className="bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm border-0 shadow-lg shadow-gray-200/50 dark:shadow-gray-900/50 rounded-xl overflow-hidden">
<CardHeader className="pb-2 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950/30 dark:to-emerald-950/30">
<CardTitle className="text-base font-bold flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-gradient-to-br from-emerald-400 to-teal-500 shadow-md shadow-emerald-500/30">
<BarChart3 className="h-4 w-4 text-white" />
</div>
<span className="bg-gradient-to-r from-emerald-600 via-teal-500 to-emerald-600 dark:from-emerald-400 dark:via-teal-400 dark:to-emerald-400 bg-clip-text text-transparent">
Detection Distribution
</span>
</CardTitle>
</CardHeader>
<CardContent className="pt-4">
<DetectionDonutChart
potholes={stats.totalPotholes}
signboards={stats.totalSignboards}
isLoading={isLoading}
/>
</CardContent>
</Card>
{/* Location Bar Chart */}
<Card className="bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm border-0 shadow-lg shadow-gray-200/50 dark:shadow-gray-900/50 rounded-xl overflow-hidden">
<CardHeader className="pb-2 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/30 dark:to-indigo-950/30">
<CardTitle className="text-base font-bold flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-gradient-to-br from-blue-400 to-indigo-500 shadow-md shadow-blue-500/30">
<MapPinIcon className="h-4 w-4 text-white" />
</div>
<span className="bg-gradient-to-r from-blue-600 via-indigo-500 to-blue-600 dark:from-blue-400 dark:via-indigo-400 dark:to-blue-400 bg-clip-text text-transparent">
Detections by Location
</span>
</CardTitle>
</CardHeader>
<CardContent className="pt-4">
<LocationBarChart
data={stats.locationData}
isLoading={isLoading}
/>
</CardContent>
</Card>
</div>
</div>
{/* Footer */}
<div className="mt-8 text-center animate-in fade-in duration-700 delay-300">
<p className="text-xs text-gray-400 dark:text-gray-500">
Sentient Geeks Pvt. Ltd.
</p>
</div>
</div>
</main>
</div>
)
}

View File

@@ -4,79 +4,114 @@
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
/* Primary - Indigo */
--primary: oklch(0.55 0.24 264);
--primary-foreground: oklch(0.98 0.01 264);
/* Background & Surfaces */
--background: oklch(0.98 0.01 280);
--foreground: oklch(0.15 0.02 280);
--card: oklch(0.99 0.005 280);
--card-foreground: oklch(0.15 0.02 280);
--popover: oklch(0.99 0.005 280);
--popover-foreground: oklch(0.15 0.02 280);
/* Secondary */
--secondary: oklch(0.95 0.02 264);
--secondary-foreground: oklch(0.25 0.05 264);
/* Muted */
--muted: oklch(0.94 0.02 280);
--muted-foreground: oklch(0.45 0.03 280);
/* Accent - Cyan */
--accent: oklch(0.92 0.04 200);
--accent-foreground: oklch(0.25 0.08 200);
/* Destructive */
--destructive: oklch(0.55 0.22 25);
--destructive-foreground: oklch(0.98 0.01 25);
/* Borders & Inputs */
--border: oklch(0.90 0.02 280);
--input: oklch(0.92 0.02 280);
--ring: oklch(0.55 0.24 264);
/* Chart Colors - Vibrant Palette */
--chart-1: oklch(0.55 0.24 264);
--chart-2: oklch(0.65 0.20 200);
--chart-3: oklch(0.60 0.18 160);
--chart-4: oklch(0.70 0.20 85);
--chart-5: oklch(0.60 0.22 320);
--radius: 0.75rem;
/* Sidebar */
--sidebar: oklch(0.98 0.01 280);
--sidebar-foreground: oklch(0.15 0.02 280);
--sidebar-primary: oklch(0.55 0.24 264);
--sidebar-primary-foreground: oklch(0.98 0.01 264);
--sidebar-accent: oklch(0.94 0.03 264);
--sidebar-accent-foreground: oklch(0.25 0.05 264);
--sidebar-border: oklch(0.90 0.02 280);
--sidebar-ring: oklch(0.55 0.24 264);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
/* Primary - Light Indigo */
--primary: oklch(0.70 0.20 264);
--primary-foreground: oklch(0.15 0.02 264);
/* Background & Surfaces */
--background: oklch(0.12 0.02 280);
--foreground: oklch(0.95 0.01 280);
--card: oklch(0.16 0.02 280);
--card-foreground: oklch(0.95 0.01 280);
--popover: oklch(0.16 0.02 280);
--popover-foreground: oklch(0.95 0.01 280);
/* Secondary */
--secondary: oklch(0.22 0.03 264);
--secondary-foreground: oklch(0.90 0.02 264);
/* Muted */
--muted: oklch(0.20 0.02 280);
--muted-foreground: oklch(0.65 0.02 280);
/* Accent */
--accent: oklch(0.22 0.04 200);
--accent-foreground: oklch(0.85 0.08 200);
/* Destructive */
--destructive: oklch(0.45 0.18 25);
--destructive-foreground: oklch(0.90 0.08 25);
/* Borders & Inputs */
--border: oklch(0.25 0.02 280);
--input: oklch(0.22 0.02 280);
--ring: oklch(0.70 0.20 264);
/* Chart Colors */
--chart-1: oklch(0.65 0.22 264);
--chart-2: oklch(0.70 0.18 200);
--chart-3: oklch(0.65 0.16 160);
--chart-4: oklch(0.75 0.18 85);
--chart-5: oklch(0.65 0.20 320);
/* Sidebar */
--sidebar: oklch(0.14 0.02 280);
--sidebar-foreground: oklch(0.95 0.01 280);
--sidebar-primary: oklch(0.65 0.22 264);
--sidebar-primary-foreground: oklch(0.95 0.01 264);
--sidebar-accent: oklch(0.22 0.03 264);
--sidebar-accent-foreground: oklch(0.90 0.02 264);
--sidebar-border: oklch(0.25 0.02 280);
--sidebar-ring: oklch(0.70 0.20 264);
}
@theme inline {
--font-sans: 'Geist', 'Geist Fallback';
--font-mono: 'Geist Mono', 'Geist Mono Fallback';
--font-sans: 'Geist', 'Geist Fallback', system-ui, sans-serif;
--font-mono: 'Geist Mono', 'Geist Mono Fallback', monospace;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
@@ -122,4 +157,153 @@
body {
@apply bg-background text-foreground;
}
}
/* Premium Background with Animated Mesh Gradient */
@layer utilities {
.bg-mesh-gradient {
background:
radial-gradient(ellipse 80% 50% at 20% 20%, oklch(0.75 0.15 264 / 0.15), transparent),
radial-gradient(ellipse 60% 40% at 80% 80%, oklch(0.70 0.12 200 / 0.12), transparent),
radial-gradient(ellipse 50% 30% at 50% 50%, oklch(0.65 0.10 320 / 0.08), transparent),
var(--background);
}
.dark .bg-mesh-gradient {
background:
radial-gradient(ellipse 80% 50% at 20% 20%, oklch(0.40 0.18 264 / 0.20), transparent),
radial-gradient(ellipse 60% 40% at 80% 80%, oklch(0.35 0.15 200 / 0.15), transparent),
radial-gradient(ellipse 50% 30% at 50% 50%, oklch(0.30 0.12 320 / 0.10), transparent),
var(--background);
}
/* Glassmorphism Card */
.glass-card {
background: oklch(1 0 0 / 0.7);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid oklch(1 0 0 / 0.2);
box-shadow:
0 4px 6px -1px oklch(0 0 0 / 0.05),
0 10px 15px -3px oklch(0 0 0 / 0.08),
0 20px 25px -5px oklch(0 0 0 / 0.05),
inset 0 1px 0 oklch(1 0 0 / 0.5);
}
.dark .glass-card {
background: oklch(0.18 0.02 280 / 0.7);
border: 1px solid oklch(1 0 0 / 0.08);
box-shadow:
0 4px 6px -1px oklch(0 0 0 / 0.15),
0 10px 15px -3px oklch(0 0 0 / 0.20),
0 20px 25px -5px oklch(0 0 0 / 0.15),
inset 0 1px 0 oklch(1 0 0 / 0.05);
}
/* Gradient Text */
.text-gradient {
background: linear-gradient(135deg, oklch(0.55 0.24 264), oklch(0.60 0.20 200));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.dark .text-gradient {
background: linear-gradient(135deg, oklch(0.75 0.20 264), oklch(0.70 0.18 200));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Gradient Button */
.btn-gradient {
background: linear-gradient(135deg, oklch(0.55 0.24 264), oklch(0.50 0.22 280));
box-shadow:
0 4px 14px 0 oklch(0.55 0.24 264 / 0.35),
inset 0 1px 0 oklch(1 0 0 / 0.15);
transition: all 0.3s ease;
}
.btn-gradient:hover {
box-shadow:
0 6px 20px 0 oklch(0.55 0.24 264 / 0.45),
inset 0 1px 0 oklch(1 0 0 / 0.2);
transform: translateY(-1px);
}
.btn-gradient:active {
transform: translateY(0);
box-shadow:
0 2px 8px 0 oklch(0.55 0.24 264 / 0.30),
inset 0 1px 0 oklch(1 0 0 / 0.15);
}
/* Card Glow Border */
.card-glow {
position: relative;
}
.card-glow::before {
content: '';
position: absolute;
inset: -1px;
background: linear-gradient(135deg, oklch(0.55 0.24 264 / 0.3), oklch(0.60 0.20 200 / 0.3));
border-radius: inherit;
z-index: -1;
opacity: 0;
transition: opacity 0.3s ease;
}
.card-glow:hover::before {
opacity: 1;
}
/* Progress Bar Gradient */
.progress-gradient {
background: linear-gradient(90deg, oklch(0.55 0.24 264), oklch(0.60 0.20 200), oklch(0.65 0.18 160));
background-size: 200% 100%;
animation: progress-shimmer 2s ease infinite;
}
@keyframes progress-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Subtle Float Animation */
.float-subtle {
animation: float-subtle 6s ease-in-out infinite;
}
@keyframes float-subtle {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
/* Input Focus Glow */
.input-glow:focus-within {
box-shadow: 0 0 0 3px oklch(0.55 0.24 264 / 0.15);
}
/* Step Indicator */
.step-active {
background: linear-gradient(135deg, oklch(0.55 0.24 264), oklch(0.50 0.22 280));
color: white;
box-shadow: 0 2px 8px oklch(0.55 0.24 264 / 0.3);
}
.step-completed {
background: oklch(0.65 0.18 160);
color: white;
}
.step-pending {
background: oklch(0.90 0.02 280);
color: oklch(0.50 0.02 280);
}
.dark .step-pending {
background: oklch(0.25 0.02 280);
color: oklch(0.60 0.02 280);
}
}

60
app/map/page.tsx Normal file
View File

@@ -0,0 +1,60 @@
"use client"
import { NavigationMenu } from "@/components/navigation-menu"
import { DashboardMap } from "@/components/dashboard/dashboard-map"
export default function MapPage() {
return (
<div className="min-h-screen bg-mesh-gradient relative overflow-hidden">
{/* Navigation Menu */}
<NavigationMenu />
{/* Decorative background elements */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-40 -left-40 w-80 h-80 bg-primary/10 rounded-full blur-3xl float-subtle" />
<div className="absolute -bottom-40 -right-40 w-96 h-96 bg-accent/10 rounded-full blur-3xl float-subtle" style={{ animationDelay: '-3s' }} />
<div className="absolute top-1/2 left-1/4 w-64 h-64 bg-primary/5 rounded-full blur-3xl" />
</div>
<div className="container mx-auto px-4 py-8 max-w-7xl relative z-10">
{/* Header */}
<div className="mb-8 animate-in fade-in slide-in-from-top duration-700">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl md:text-4xl font-bold text-gradient leading-tight">
Detection Map
</h1>
<p className="text-muted-foreground mt-1">
View all detected potholes and signboards on the map
</p>
</div>
</div>
</div>
{/* Map */}
<div className="animate-in fade-in slide-in-from-bottom duration-700 delay-100">
<DashboardMap className="h-[calc(100vh-200px)] min-h-[500px]" />
</div>
{/* Legend */}
<div className="mt-6 flex flex-wrap gap-6 animate-in fade-in duration-700 delay-200">
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded-full bg-red-500 border-2 border-red-600" />
<span className="text-sm text-muted-foreground">Pothole</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded-full bg-blue-500 border-2 border-blue-600" />
<span className="text-sm text-muted-foreground">Signboard</span>
</div>
</div>
{/* Footer */}
<div className="mt-8 text-center animate-in fade-in duration-700 delay-300">
<p className="text-sm text-muted-foreground">
Sentient Geeks Pvt. Ltd.
</p>
</div>
</div>
</div>
)
}

View File

@@ -2,6 +2,7 @@
import { useRouter } from "next/navigation"
import { ProjectSelectionSection } from "@/components/project-selection-section"
import { SidebarNavigation } from "@/components/sidebar-navigation"
import { type SessionContext, saveSession } from "@/lib/api"
export default function SelectionPage() {
@@ -14,23 +15,42 @@ export default function SelectionPage() {
}
return (
<div className="min-h-screen bg-gradient-to-b from-background to-muted/20">
<div className="container mx-auto px-4 py-8 max-w-7xl">
{/* Header */}
<div className="mb-8 animate-in fade-in slide-in-from-top duration-700">
<h1 className="text-4xl font-bold mb-2 text-balance bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text text-transparent">
VisionRoad Detection System
</h1>
<p className="text-muted-foreground">
Select your project location to begin AI-powered road analysis
</p>
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950">
{/* Sidebar Navigation */}
<SidebarNavigation />
{/* Main Content */}
<main className="ml-16 min-h-screen relative overflow-hidden">
{/* Decorative background elements */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-40 -left-40 w-80 h-80 bg-indigo-500/10 rounded-full blur-3xl" />
<div className="absolute -bottom-40 -right-40 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-blue-500/5 rounded-full blur-3xl" />
</div>
{/* Project Selection Section */}
<div className="animate-in fade-in slide-in-from-bottom duration-700 delay-100">
<ProjectSelectionSection onSelectionComplete={handleSelectionComplete} />
<div className="container mx-auto px-4 py-12 max-w-5xl relative z-10">
{/* Premium Header */}
<div className="mb-10 animate-in fade-in slide-in-from-top duration-700">
<div className="text-center max-w-2xl mx-auto">
<h1 className="text-4xl md:text-5xl font-bold mb-4 bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent leading-tight">
VisionRoad Detection System
</h1>
</div>
</div>
{/* Project Selection Section */}
<div className="animate-in fade-in slide-in-from-bottom duration-700 delay-150">
<ProjectSelectionSection onSelectionComplete={handleSelectionComplete} />
</div>
{/* Footer */}
<div className="mt-12 text-center animate-in fade-in duration-700 delay-300">
<p className="text-sm text-gray-400 dark:text-gray-500">
Sentient Geeks Pvt. Ltd.
</p>
</div>
</div>
</div>
</main>
</div>
)
}

View File

@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Loader2, ArrowLeft, MapPin, Package, FolderKanban, RotateCcw } from "lucide-react"
import VideoPlayerSection from "@/components/video-player-section"
import { SidebarNavigation } from "@/components/sidebar-navigation"
import {
type SessionContext,
loadSession,
@@ -157,83 +158,99 @@ export default function ResultsPage() {
if (isLoading) {
return (
<div className="min-h-screen flex flex-col items-center justify-center gap-4">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-muted-foreground">Loading detection results...</p>
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 flex items-center justify-center">
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
<p className="text-gray-500">Loading detection results...</p>
</div>
</div>
)
}
if (error) {
return (
<div className="min-h-screen flex flex-col items-center justify-center gap-4">
<p className="text-destructive">{error}</p>
<Button onClick={handleNewAnalysis}>Start New Analysis</Button>
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 flex items-center justify-center">
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
<p className="text-red-500">{error}</p>
<Button onClick={handleNewAnalysis} className="bg-gradient-to-r from-indigo-500 to-purple-600 text-white">Start New Analysis</Button>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-b from-background to-muted/20">
<div className="container mx-auto px-4 py-8 max-w-7xl">
{/* Header */}
<div className="mb-8 animate-in fade-in slide-in-from-top duration-700">
<h1 className="text-4xl font-bold mb-2 text-balance bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text text-transparent">
{getTitle()}
</h1>
<p className="text-muted-foreground">
View your AI-powered road analysis results
</p>
</div>
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950">
{/* Sidebar Navigation */}
<SidebarNavigation />
{/* Session Info Bar */}
{session && (
<div className="mb-6 animate-in fade-in slide-in-from-top duration-500">
<div className="flex items-center justify-between p-4 rounded-lg bg-gradient-to-r from-primary/10 via-primary/5 to-transparent border border-primary/20">
<div className="flex items-center gap-6 text-sm">
<div className="flex items-center gap-2">
<FolderKanban className="h-4 w-4 text-primary" />
<span className="text-muted-foreground">Project:</span>
<span className="font-medium">{session.projectName}</span>
{/* Main Content */}
<main className="ml-16 min-h-screen">
<div className="container mx-auto px-4 py-8 max-w-7xl">
{/* Header */}
<div className="mb-8 animate-in fade-in slide-in-from-top duration-700">
<h1 className="text-4xl font-bold mb-2 bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent">
{getTitle()}
</h1>
<p className="text-gray-500 dark:text-gray-400">
View your AI-powered road analysis results
</p>
</div>
{/* Session Info Bar */}
{session && (
<div className="mb-6 animate-in fade-in slide-in-from-top duration-500">
<div className="flex items-center justify-between p-4 rounded-xl bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50">
<div className="flex items-center gap-6 text-sm">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-gradient-to-br from-indigo-400 to-purple-500 shadow-md shadow-indigo-500/30">
<FolderKanban className="h-4 w-4 text-white" />
</div>
<span className="text-gray-500 dark:text-gray-400 text-xs uppercase font-semibold tracking-wide">Project:</span>
<span className="font-bold text-gray-900 dark:text-white bg-gradient-to-r from-indigo-600 to-purple-600 dark:from-indigo-400 dark:to-purple-400 bg-clip-text text-transparent">{session.projectName}</span>
</div>
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-gradient-to-br from-emerald-400 to-teal-500 shadow-md shadow-emerald-500/30">
<Package className="h-4 w-4 text-white" />
</div>
<span className="text-gray-500 dark:text-gray-400 text-xs uppercase font-semibold tracking-wide">Package:</span>
<span className="font-bold text-gray-900 dark:text-white bg-gradient-to-r from-emerald-600 to-teal-600 dark:from-emerald-400 dark:to-teal-400 bg-clip-text text-transparent">{session.packageName}</span>
</div>
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-gradient-to-br from-purple-400 to-pink-500 shadow-md shadow-purple-500/30">
<MapPin className="h-4 w-4 text-white" />
</div>
<span className="text-gray-500 dark:text-gray-400 text-xs uppercase font-semibold tracking-wide">Location:</span>
<span className="font-bold text-gray-900 dark:text-white bg-gradient-to-r from-purple-600 to-pink-600 dark:from-purple-400 dark:to-pink-400 bg-clip-text text-transparent">{session.locationName}</span>
</div>
</div>
<div className="flex items-center gap-2">
<Package className="h-4 w-4 text-primary" />
<span className="text-muted-foreground">Package:</span>
<span className="font-medium">{session.packageName}</span>
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={handleBackToUpload} className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Upload
</Button>
<Button variant="outline" size="sm" onClick={handleNewAnalysis} className="border-indigo-200 dark:border-indigo-800 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-900/50">
<RotateCcw className="h-4 w-4 mr-2" />
New Analysis
</Button>
</div>
<div className="flex items-center gap-2">
<MapPin className="h-4 w-4 text-primary" />
<span className="text-muted-foreground">Location:</span>
<span className="font-medium">{session.locationName}</span>
</div>
</div>
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={handleBackToUpload}>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Upload
</Button>
<Button variant="outline" size="sm" onClick={handleNewAnalysis}>
<RotateCcw className="h-4 w-4 mr-2" />
New Analysis
</Button>
</div>
</div>
</div>
)}
)}
{/* Video Player Section */}
{detectionData && videoId && (
<div className="animate-in fade-in slide-in-from-bottom duration-700">
<VideoPlayerSection
data={detectionData}
videoId={videoId}
videoFile={videoFile}
detectionType={detectionType}
projectId={session?.projectId || undefined}
/>
</div>
)}
</div>
{/* Video Player Section */}
{detectionData && videoId && (
<div className="animate-in fade-in slide-in-from-bottom duration-700">
<VideoPlayerSection
data={detectionData}
videoId={videoId}
videoFile={videoFile}
detectionType={detectionType}
projectId={session?.projectId || undefined}
/>
</div>
)}
</div>
</main>
</div>
)
}

View File

@@ -6,9 +6,9 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Progress } from "@/components/ui/progress"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Upload, Loader2, AlertCircle, ArrowLeft, MapPin, Package, FolderKanban } from "lucide-react"
import { Loader2 } from "lucide-react"
import { SidebarNavigation } from "@/components/sidebar-navigation"
import {
type SessionContext,
loadSession,
@@ -71,7 +71,6 @@ export default function UploadPage() {
if (data.type === "complete" || data.status === "completed") {
setStatusMessage("Processing completed! Saving video...")
ws.close()
// Store video file in IndexedDB for results page
if (file) {
try {
await storeVideoFile(videoId, file)
@@ -79,7 +78,6 @@ export default function UploadPage() {
console.error("Failed to store video file:", err)
}
}
// Save video data and navigate to results
saveVideoData({ videoId, detectionType })
setStatusMessage("Redirecting to results...")
setTimeout(() => router.push("/results"), 500)
@@ -154,184 +152,244 @@ export default function UploadPage() {
const getTitle = () => {
return detectionType === "pothole-detection"
? "Pothole Detection System"
: "Signboard Detection System"
? "Pothole Detection"
: "Signboard Detection"
}
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 flex items-center justify-center">
<div className="p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-b from-background to-muted/20">
<div className="container mx-auto px-4 py-8 max-w-7xl">
{/* Header */}
<div className="mb-8 animate-in fade-in slide-in-from-top duration-700">
<h1 className="text-4xl font-bold mb-2 text-balance bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text text-transparent">
{getTitle()}
</h1>
<p className="text-muted-foreground">
Upload a video to detect and analyze with AI-powered processing
</p>
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950">
{/* Sidebar Navigation */}
<SidebarNavigation />
{/* Main Content */}
<main className="ml-16 min-h-screen relative overflow-hidden flex flex-col">
{/* Decorative background elements */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-40 -left-40 w-80 h-80 bg-indigo-500/10 rounded-full blur-3xl" />
<div className="absolute -bottom-40 -right-40 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl" />
</div>
{/* Session Info Bar */}
{session && (
<div className="mb-6 animate-in fade-in slide-in-from-top duration-500">
<div className="flex items-center justify-between p-4 rounded-lg bg-gradient-to-r from-primary/10 via-primary/5 to-transparent border border-primary/20">
<div className="flex items-center gap-6 text-sm">
<div className="flex items-center gap-2">
<FolderKanban className="h-4 w-4 text-primary" />
<span className="text-muted-foreground">Project:</span>
<span className="font-medium">{session.projectName}</span>
</div>
<div className="flex items-center gap-2">
<Package className="h-4 w-4 text-primary" />
<span className="text-muted-foreground">Package:</span>
<span className="font-medium">{session.packageName}</span>
</div>
<div className="flex items-center gap-2">
<MapPin className="h-4 w-4 text-primary" />
<span className="text-muted-foreground">Location:</span>
<span className="font-medium">{session.locationName}</span>
</div>
</div>
<Button variant="ghost" size="sm" onClick={handleBackToSelection}>
<ArrowLeft className="h-4 w-4 mr-2" />
Change Selection
</Button>
<div className="flex-1 container mx-auto px-4 py-6 max-w-6xl relative z-10 flex flex-col">
{/* Compact Header */}
<div className="mb-4 animate-in fade-in slide-in-from-top duration-700">
<div className="text-center">
<h1 className="text-3xl md:text-4xl font-bold bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent leading-tight">
{getTitle()}
</h1>
</div>
</div>
)}
{/* Upload Card */}
<Card className="transition-all hover:shadow-lg animate-in fade-in slide-in-from-bottom duration-700">
<CardHeader>
<CardTitle>Upload Video</CardTitle>
<CardDescription>
Select a video file, detection type, and vehicle speed to start AI-powered analysis
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Video File Input */}
<div className="space-y-2">
<Label htmlFor="video-file">Video File</Label>
<Input
id="video-file"
type="file"
accept="video/*"
onChange={(e) => {
setFile(e.target.files?.[0] || null)
setError(null)
}}
disabled={uploading}
/>
{file && (
<p className="text-sm text-muted-foreground">
Selected: {file.name} ({(file.size / 1024 / 1024).toFixed(2)} MB)
</p>
)}
</div>
{/* JSON File Input */}
<div className="space-y-2">
<Label htmlFor="json-file">GPS JSON File</Label>
<Input
id="json-file"
type="file"
accept=".json,application/json"
onChange={(e) => {
setJsonFile(e.target.files?.[0] || null)
setError(null)
}}
disabled={uploading}
/>
{jsonFile && (
<p className="text-sm text-muted-foreground">
Selected: {jsonFile.name} ({(jsonFile.size / 1024).toFixed(2)} KB)
</p>
)}
</div>
{/* Detection Type */}
<div className="space-y-2">
<Label htmlFor="detection-type">Detection Type</Label>
<Select
value={detectionType}
onValueChange={(v) => setDetectionType(v as DetectionType)}
disabled={uploading}
>
<SelectTrigger id="detection-type">
<SelectValue placeholder="Select detection type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="pothole-detection">Pothole Detection</SelectItem>
<SelectItem value="sign-board-detection">Signboard Detection</SelectItem>
</SelectContent>
</Select>
</div>
{/* Speed Input */}
<div className="space-y-2">
<Label htmlFor="speed">Speed (km/h)</Label>
<Input
id="speed"
type="number"
min={1}
max={200}
value={speed}
onChange={(e) => setSpeed(Number(e.target.value))}
disabled={uploading}
/>
</div>
</div>
{/* Error Display */}
{error && (
<div className="flex items-start gap-2 p-3 rounded-lg bg-destructive/10 text-destructive">
<AlertCircle className="h-5 w-5 mt-0.5 flex-shrink-0" />
<p className="text-sm whitespace-pre-line">{error}</p>
</div>
)}
{/* Upload Button */}
<Button
onClick={handleUpload}
disabled={!file || uploading}
className="w-full"
size="lg"
>
{uploading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Processing...
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
Upload & Process
</>
)}
</Button>
{/* Progress Section */}
{uploading && (
<div className="space-y-3 animate-in fade-in slide-in-from-top duration-500">
<Progress value={progress} className="h-3" />
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">{statusMessage}</span>
<span className="font-semibold">{progress}%</span>
{/* Compact Session Info Bar */}
{session && (
<div className="mb-4 animate-in fade-in slide-in-from-top duration-500 delay-100">
<div className="rounded-xl px-4 py-3 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-6 text-sm">
<div>
<p className="text-[10px] font-medium text-gray-500 uppercase tracking-wide">Project</p>
<p className="font-medium text-gray-900 dark:text-white text-sm">{session.projectName}</p>
</div>
<div className="w-px h-8 bg-gray-300 dark:bg-gray-600" />
<div>
<p className="text-[10px] font-medium text-gray-500 uppercase tracking-wide">Package</p>
<p className="font-medium text-gray-900 dark:text-white text-sm">{session.packageName}</p>
</div>
<div className="w-px h-8 bg-gray-300 dark:bg-gray-600" />
<div>
<p className="text-[10px] font-medium text-gray-500 uppercase tracking-wide">Location</p>
<p className="font-medium text-gray-900 dark:text-white text-sm">{session.locationName}</p>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={handleBackToSelection}
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 text-xs"
>
Change Selection
</Button>
</div>
</div>
)}
</CardContent>
</Card>
</div>
</div>
)}
{/* Upload Card */}
<Card className="bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm border-0 shadow-lg shadow-gray-200/50 dark:shadow-gray-900/50 rounded-xl overflow-hidden animate-in fade-in slide-in-from-bottom duration-700 delay-150 flex-1">
<CardHeader className="pb-4 border-b border-gray-100 dark:border-gray-700 bg-gradient-to-r from-indigo-50 to-purple-50 dark:from-indigo-950/30 dark:to-purple-950/30">
<CardTitle className="text-xl font-bold">
<span className="bg-gradient-to-r from-indigo-600 via-purple-500 to-indigo-600 dark:from-indigo-400 dark:via-purple-400 dark:to-indigo-400 bg-clip-text text-transparent">
Upload Video
</span>
</CardTitle>
<CardDescription className="text-sm">
Select video file, detection type, and vehicle speed for analysis
</CardDescription>
</CardHeader>
<CardContent className="pt-4 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Video File Input */}
<div className="space-y-2">
<Label htmlFor="video-file" className="text-sm font-semibold">
Video File
</Label>
<Input
id="video-file"
type="file"
accept="video/*"
onChange={(e) => {
setFile(e.target.files?.[0] || null)
setError(null)
}}
disabled={uploading}
className="h-10 bg-gray-50 dark:bg-gray-800 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:bg-indigo-100 dark:file:bg-indigo-900/50 file:text-indigo-600 dark:file:text-indigo-400 file:font-medium file:text-xs hover:file:bg-indigo-200"
/>
{file && (
<div className="p-2 rounded-lg bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-200 dark:border-indigo-800">
<p className="text-xs font-medium text-gray-900 dark:text-white">{file.name}</p>
<p className="text-[10px] text-gray-500">
{(file.size / 1024 / 1024).toFixed(2)} MB
</p>
</div>
)}
</div>
{/* JSON File Input */}
<div className="space-y-2">
<Label htmlFor="json-file" className="text-sm font-semibold">
GPS JSON File <span className="text-gray-400 font-normal text-xs">(Optional)</span>
</Label>
<Input
id="json-file"
type="file"
accept=".json,application/json"
onChange={(e) => {
setJsonFile(e.target.files?.[0] || null)
setError(null)
}}
disabled={uploading}
className="h-10 bg-gray-50 dark:bg-gray-800 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:bg-indigo-100 dark:file:bg-indigo-900/50 file:text-indigo-600 dark:file:text-indigo-400 file:font-medium file:text-xs hover:file:bg-indigo-200"
/>
{jsonFile && (
<div className="p-2 rounded-lg bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-200 dark:border-indigo-800">
<p className="text-xs font-medium text-gray-900 dark:text-white">{jsonFile.name}</p>
<p className="text-[10px] text-gray-500">
{(jsonFile.size / 1024).toFixed(2)} KB
</p>
</div>
)}
</div>
{/* Detection Type */}
<div className="space-y-2">
<Label htmlFor="detection-type" className="text-sm font-semibold">
Detection Type
</Label>
<Select
value={detectionType}
onValueChange={(v) => setDetectionType(v as DetectionType)}
disabled={uploading}
>
<SelectTrigger id="detection-type" className="h-10 bg-gray-50 dark:bg-gray-800">
<SelectValue placeholder="Select detection type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="pothole-detection">
<span className="font-medium">Pothole Detection</span>
</SelectItem>
<SelectItem value="sign-board-detection">
<span className="font-medium">Signboard Detection</span>
</SelectItem>
</SelectContent>
</Select>
</div>
{/* Speed Input */}
<div className="space-y-2">
<Label htmlFor="speed" className="text-sm font-semibold">
Vehicle Speed (km/h)
</Label>
<Input
id="speed"
type="number"
min={1}
max={200}
value={speed}
onChange={(e) => setSpeed(Number(e.target.value))}
disabled={uploading}
className="h-10 bg-gray-50 dark:bg-gray-800"
/>
</div>
</div>
{/* Error Display */}
{error && (
<div className="flex items-start gap-2 p-3 rounded-lg bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 animate-in fade-in slide-in-from-top duration-300">
<div className="w-4 h-4 rounded-full bg-red-100 dark:bg-red-900/50 flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="text-[10px] font-bold text-red-500">!</span>
</div>
<p className="text-xs text-red-600 dark:text-red-400 leading-relaxed whitespace-pre-line">{error}</p>
</div>
)}
{/* Upload Button */}
<Button
onClick={handleUpload}
disabled={!file || uploading}
className={`w-full h-12 text-sm font-semibold transition-all rounded-xl ${file && !uploading ? 'bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white shadow-lg shadow-indigo-500/25' : ''
}`}
size="lg"
>
{uploading ? (
<span className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Processing...
</span>
) : (
"Upload and Process"
)}
</Button>
{/* Progress Section */}
{uploading && (
<div className="space-y-3 p-4 rounded-xl bg-indigo-50 dark:bg-indigo-950/30 border border-indigo-200 dark:border-indigo-800 animate-in fade-in slide-in-from-top duration-500">
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="font-medium text-gray-700 dark:text-gray-300 text-xs">Processing Progress</span>
<span className="font-bold text-indigo-600 dark:text-indigo-400 text-xs">{progress}%</span>
</div>
<div className="h-2 rounded-full bg-indigo-100 dark:bg-indigo-900/50 overflow-hidden">
<div
className="h-full bg-gradient-to-r from-indigo-500 to-purple-600 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
</div>
{statusMessage && (
<p className="text-xs text-gray-500">{statusMessage}</p>
)}
</div>
)}
</CardContent>
</Card>
{/* Footer */}
<div className="mt-4 text-center animate-in fade-in duration-700 delay-300">
<p className="text-xs text-gray-400 dark:text-gray-500">
Sentient Geeks Pvt. Ltd.
</p>
</div>
</div>
</main>
</div>
)
}