diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000..899a157 --- /dev/null +++ b/app/dashboard/page.tsx @@ -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([]) + const [selectedProjectId, setSelectedProjectId] = useState(null) + const [projectSummary, setProjectSummary] = useState(null) + const [stats, setStats] = useState({ + totalPotholes: 0, + totalSignboards: 0, + totalDetections: 0, + locationData: [] + }) + const [error, setError] = useState(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 ( +
+ {/* Sidebar Navigation */} + + + {/* Main Content - offset by sidebar width */} +
+
+ {/* Header */} +
+
+
+

+ VisionRoad Analytics Dashboard +

+

+ A Comprehensive Overview Of Your Road Infrastructure Analysis +

+
+ +
+
+ + {/* Error Display */} + {error && ( +
+ +

{error}

+
+ )} + + {/* Stats Cards - Top Row */} +
+ + + +
+ + {/* Project Selector - Above main content */} +
+ +
+ + {/* Main Content Grid - Map Left, Charts Right */} +
+ {/* Left Side - Map */} +
+ +
+ + {/* Right Side - Stacked Charts */} +
+ {/* Detection Distribution */} + + + +
+ +
+ + Detection Distribution + +
+
+ + + +
+ + {/* Location Bar Chart */} + + + +
+ +
+ + Detections by Location + +
+
+ + + +
+
+
+ + {/* Footer */} +
+

+ Sentient Geeks Pvt. Ltd. +

+
+
+
+
+ ) +} diff --git a/app/globals.css b/app/globals.css index b815a87..b6b6b13 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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); + } } \ No newline at end of file diff --git a/app/map/page.tsx b/app/map/page.tsx new file mode 100644 index 0000000..4bea2b9 --- /dev/null +++ b/app/map/page.tsx @@ -0,0 +1,60 @@ +"use client" + +import { NavigationMenu } from "@/components/navigation-menu" +import { DashboardMap } from "@/components/dashboard/dashboard-map" + +export default function MapPage() { + return ( +
+ {/* Navigation Menu */} + + + {/* Decorative background elements */} +
+
+
+
+
+ +
+ {/* Header */} +
+
+
+

+ Detection Map +

+

+ View all detected potholes and signboards on the map +

+
+
+
+ + {/* Map */} +
+ +
+ + {/* Legend */} +
+
+
+ Pothole +
+
+
+ Signboard +
+
+ + {/* Footer */} +
+

+ Sentient Geeks Pvt. Ltd. +

+
+
+
+ ) +} diff --git a/app/page.tsx b/app/page.tsx index e6aa0a3..53cd430 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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 ( -
-
- {/* Header */} -
-

- VisionRoad Detection System -

-

- Select your project location to begin AI-powered road analysis -

+
+ {/* Sidebar Navigation */} + + + {/* Main Content */} +
+ {/* Decorative background elements */} +
+
+
+
- {/* Project Selection Section */} -
- +
+ {/* Premium Header */} +
+
+

+ VisionRoad Detection System +

+
+
+ + {/* Project Selection Section */} +
+ +
+ + {/* Footer */} +
+

+ Sentient Geeks Pvt. Ltd. +

+
-
+
) } diff --git a/app/results/page.tsx b/app/results/page.tsx index 97ef8f2..eeb89dc 100644 --- a/app/results/page.tsx +++ b/app/results/page.tsx @@ -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 ( -
- -

Loading detection results...

+
+
+ +

Loading detection results...

+
) } if (error) { return ( -
-

{error}

- +
+
+

{error}

+ +
) } return ( -
-
- {/* Header */} -
-

- {getTitle()} -

-

- View your AI-powered road analysis results -

-
+
+ {/* Sidebar Navigation */} + - {/* Session Info Bar */} - {session && ( -
-
-
-
- - Project: - {session.projectName} + {/* Main Content */} +
+
+ {/* Header */} +
+

+ {getTitle()} +

+

+ View your AI-powered road analysis results +

+
+ + {/* Session Info Bar */} + {session && ( +
+
+
+
+
+ +
+ Project: + {session.projectName} +
+
+
+ +
+ Package: + {session.packageName} +
+
+
+ +
+ Location: + {session.locationName} +
-
- - Package: - {session.packageName} +
+ +
-
- - Location: - {session.locationName} -
-
-
- -
-
- )} + )} - {/* Video Player Section */} - {detectionData && videoId && ( -
- -
- )} -
+ {/* Video Player Section */} + {detectionData && videoId && ( +
+ +
+ )} +
+
) } diff --git a/app/upload/page.tsx b/app/upload/page.tsx index a411602..04a18bc 100644 --- a/app/upload/page.tsx +++ b/app/upload/page.tsx @@ -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 ( -
- +
+
+ +
) } return ( -
-
- {/* Header */} -
-

- {getTitle()} -

-

- Upload a video to detect and analyze with AI-powered processing -

+
+ {/* Sidebar Navigation */} + + + {/* Main Content */} +
+ {/* Decorative background elements */} +
+
+
- {/* Session Info Bar */} - {session && ( -
-
-
-
- - Project: - {session.projectName} -
-
- - Package: - {session.packageName} -
-
- - Location: - {session.locationName} -
-
- +
+ {/* Compact Header */} +
+
+

+ {getTitle()} +

- )} - {/* Upload Card */} - - - Upload Video - - Select a video file, detection type, and vehicle speed to start AI-powered analysis - - - -
- {/* Video File Input */} -
- - { - setFile(e.target.files?.[0] || null) - setError(null) - }} - disabled={uploading} - /> - {file && ( -

- Selected: {file.name} ({(file.size / 1024 / 1024).toFixed(2)} MB) -

- )} -
- - {/* JSON File Input */} -
- - { - setJsonFile(e.target.files?.[0] || null) - setError(null) - }} - disabled={uploading} - /> - {jsonFile && ( -

- Selected: {jsonFile.name} ({(jsonFile.size / 1024).toFixed(2)} KB) -

- )} -
- - {/* Detection Type */} -
- - -
- - {/* Speed Input */} -
- - setSpeed(Number(e.target.value))} - disabled={uploading} - /> -
-
- - {/* Error Display */} - {error && ( -
- -

{error}

-
- )} - - {/* Upload Button */} - - - {/* Progress Section */} - {uploading && ( -
- -
- {statusMessage} - {progress}% + {/* Compact Session Info Bar */} + {session && ( +
+
+
+
+
+

Project

+

{session.projectName}

+
+
+
+

Package

+

{session.packageName}

+
+
+
+

Location

+

{session.locationName}

+
+
+
- )} - - -
+
+ )} + + {/* Upload Card */} + + + + + Upload Video + + + + Select video file, detection type, and vehicle speed for analysis + + + +
+ {/* Video File Input */} +
+ + { + 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 && ( +
+

{file.name}

+

+ {(file.size / 1024 / 1024).toFixed(2)} MB +

+
+ )} +
+ + {/* JSON File Input */} +
+ + { + 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 && ( +
+

{jsonFile.name}

+

+ {(jsonFile.size / 1024).toFixed(2)} KB +

+
+ )} +
+ + {/* Detection Type */} +
+ + +
+ + {/* Speed Input */} +
+ + setSpeed(Number(e.target.value))} + disabled={uploading} + className="h-10 bg-gray-50 dark:bg-gray-800" + /> +
+
+ + {/* Error Display */} + {error && ( +
+
+ ! +
+

{error}

+
+ )} + + {/* Upload Button */} + + + {/* Progress Section */} + {uploading && ( +
+
+
+ Processing Progress + {progress}% +
+
+
+
+
+ {statusMessage && ( +

{statusMessage}

+ )} +
+ )} + + + + {/* Footer */} +
+

+ Sentient Geeks Pvt. Ltd. +

+
+
+
) } diff --git a/components/dashboard/compact-project-selector.tsx b/components/dashboard/compact-project-selector.tsx new file mode 100644 index 0000000..f444159 --- /dev/null +++ b/components/dashboard/compact-project-selector.tsx @@ -0,0 +1,84 @@ +"use client" + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { FolderOpen, MapPin, Building2 } from "lucide-react" +import { type Project } from "@/lib/api" + +interface CompactProjectSelectorProps { + projects: Project[] + selectedProjectId: string | null + onProjectChange: (projectId: string) => void + selectedProject: Project | undefined + isLoading?: boolean +} + +export function CompactProjectSelector({ + projects, + selectedProjectId, + onProjectChange, + selectedProject, + isLoading = false +}: CompactProjectSelectorProps) { + return ( +
+ {/* Project Dropdown */} +
+
+ +
+ +
+ + {/* Corridor Badge */} + {selectedProject?.corridor_name && ( + <> +
+
+
+ +
+ + {selectedProject.corridor_name} + +
+ + )} + + {/* State Badge */} + {selectedProject?.state && ( + <> +
+
+
+ +
+ + {selectedProject.state} + +
+ + )} +
+ ) +} diff --git a/components/dashboard/dashboard-map-content.tsx b/components/dashboard/dashboard-map-content.tsx new file mode 100644 index 0000000..34448b0 --- /dev/null +++ b/components/dashboard/dashboard-map-content.tsx @@ -0,0 +1,130 @@ +"use client" + +import { useEffect } from "react" +import { MapContainer, TileLayer, CircleMarker, Popup, useMap } from "react-leaflet" +import { LatLngBounds, LatLng } from "leaflet" +import "leaflet/dist/leaflet.css" +import { type Detection } from "@/lib/api" + +interface DashboardMapContentProps { + detections: Detection[] +} + +// Component to auto-fit map bounds to show all markers +function FitBounds({ bounds }: { bounds: LatLngBounds }) { + const map = useMap() + + useEffect(() => { + if (bounds.isValid()) { + map.fitBounds(bounds, { padding: [50, 50] }) + } + }, [bounds, map]) + + return null +} + +export default function DashboardMapContent({ detections }: DashboardMapContentProps) { + if (detections.length === 0) { + return ( +
+

No detections to display

+
+ ) + } + + // Calculate bounds to fit all markers + const firstDetection = detections[0] + const bounds = new LatLngBounds( + new LatLng(firstDetection.latitude!, firstDetection.longitude!), + new LatLng(firstDetection.latitude!, firstDetection.longitude!) + ) + + detections.forEach(d => { + if (d.latitude && d.longitude) { + bounds.extend(new LatLng(d.latitude, d.longitude)) + } + }) + + // Center point + const center: [number, number] = [ + (bounds.getNorth() + bounds.getSouth()) / 2, + (bounds.getEast() + bounds.getWest()) / 2 + ] + + // Get marker color based on detection type + const getMarkerColor = (type: string) => { + if (type.toLowerCase().includes("pothole")) { + return { fill: "#ef4444", stroke: "#dc2626" } // Red for potholes + } + return { fill: "#3b82f6", stroke: "#2563eb" } // Blue for signboards + } + + // Get display name for detection type + const getTypeName = (type: string) => { + if (type.toLowerCase().includes("pothole")) { + return "Pothole" + } + return "Signboard" + } + + return ( + + + + {/* Detection markers */} + {detections.map((detection, idx) => { + const colors = getMarkerColor(detection.type) + const typeName = getTypeName(detection.type) + + return ( + + +
+
+ {typeName} +
+
+
+ Class: + {detection.class.replace(/_/g, " ")} +
+
+ Confidence: + {(detection.confidence * 100).toFixed(1)}% +
+
+
+
Coordinates
+
+
Lat: {detection.latitude!.toFixed(6)}
+
Lng: {detection.longitude!.toFixed(6)}
+
+
+
+
+
+ ) + })} + + {/* Auto-fit bounds */} + +
+ ) +} diff --git a/components/dashboard/dashboard-map.tsx b/components/dashboard/dashboard-map.tsx new file mode 100644 index 0000000..930098f --- /dev/null +++ b/components/dashboard/dashboard-map.tsx @@ -0,0 +1,116 @@ +"use client" + +import { useEffect, useState } from "react" +import dynamic from "next/dynamic" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Loader2, MapPin, AlertTriangle, RectangleHorizontal } from "lucide-react" +import { fetchAllDetections, type Detection } from "@/lib/api" + +// Dynamically import the map to avoid SSR issues with Leaflet +const DashboardMapContent = dynamic( + () => import("./dashboard-map-content"), + { + ssr: false, + loading: () => ( +
+ +
+ ) + } +) + +interface DashboardMapProps { + className?: string +} + +export function DashboardMap({ className }: DashboardMapProps) { + const [detections, setDetections] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + const loadDetections = async () => { + try { + setIsLoading(true) + setError(null) + const data = await fetchAllDetections() + setDetections(data) + } catch (err) { + console.error("Failed to load detections:", err) + setError("Failed to load detection data") + } finally { + setIsLoading(false) + } + } + + loadDetections() + }, []) + + // Filter detections with valid GPS coordinates + const validDetections = detections.filter(d => d.latitude && d.longitude) + + // Count potholes and signboards + const potholeCount = validDetections.filter(d => + d.type?.toLowerCase().includes("pothole") || + d.class?.toLowerCase().includes("pothole") + ).length + const signboardCount = validDetections.length - potholeCount + + return ( + + +
+
+
+ +
+
+ + + Detection Map + + +

+ {isLoading ? "Loading..." : `${validDetections.length} detections with GPS coordinates`} +

+
+
+ + {/* Compact Color Legend */} +
+
+
+ Potholes ({potholeCount}) +
+
+
+ Signboards ({signboardCount}) +
+
+
+ + +
+ {isLoading ? ( +
+ +
+ ) : error ? ( +
+

{error}

+
+ ) : validDetections.length === 0 ? ( +
+
+

No detections with GPS coordinates found

+

Process some videos to see detections on the map

+
+
+ ) : ( + + )} +
+
+ + ) +} diff --git a/components/dashboard/detection-chart.tsx b/components/dashboard/detection-chart.tsx new file mode 100644 index 0000000..d8664cf --- /dev/null +++ b/components/dashboard/detection-chart.tsx @@ -0,0 +1,117 @@ +"use client" + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" + +interface DetectionChartProps { + potholes: number + signboards: number + isLoading?: boolean +} + +export function DetectionChart({ potholes, signboards, isLoading }: DetectionChartProps) { + const total = potholes + signboards + const potholePercent = total > 0 ? (potholes / total) * 100 : 50 + const signboardPercent = total > 0 ? (signboards / total) * 100 : 50 + + return ( + + + Detection Distribution + + + {isLoading ? ( +
+
+
+ ) : total === 0 ? ( +
+ No detections yet +
+ ) : ( +
+ {/* Donut Chart */} +
+ + {/* Background circle */} + + {/* Potholes arc */} + + {/* Signboards arc */} + + + + + + + + + + + + +
+
+

{total}

+

Total

+
+
+
+ + {/* Legend */} +
+
+
+
+ Potholes +
+
+

{potholes}

+

{potholePercent.toFixed(0)}%

+
+
+
+
+
+ Signboards +
+
+

{signboards}

+

{signboardPercent.toFixed(0)}%

+
+
+
+
+ )} + + + ) +} diff --git a/components/dashboard/detection-donut-chart.tsx b/components/dashboard/detection-donut-chart.tsx new file mode 100644 index 0000000..8b48499 --- /dev/null +++ b/components/dashboard/detection-donut-chart.tsx @@ -0,0 +1,113 @@ +"use client" + +import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts" +import { Loader2 } from "lucide-react" + +interface DetectionDonutChartProps { + potholes: number + signboards: number + isLoading: boolean +} + +const COLORS = { + pothole: "#ef4444", + signboard: "#3b82f6" +} + +export function DetectionDonutChart({ potholes, signboards, isLoading }: DetectionDonutChartProps) { + if (isLoading) { + return ( +
+ +
+ ) + } + + const total = potholes + signboards + + if (total === 0) { + return ( +
+

No detections found

+

Process videos to see data

+
+ ) + } + + const data = [ + { name: "Potholes", value: potholes, color: COLORS.pothole }, + { name: "Signboards", value: signboards, color: COLORS.signboard } + ] + + return ( +
+ + + + {data.map((entry, index) => ( + + ))} + + { + if (active && payload && payload.length) { + const data = payload[0] + return ( +
+

{data.name}

+

+ Count: {data.value} +

+

+ {((Number(data.value) / total) * 100).toFixed(1)}% of total +

+
+ ) + } + return null + }} + /> + ( +
+ {payload?.map((entry, index) => ( +
+
+ + {entry.value}: {data[index].value} + +
+ ))} +
+ )} + /> + + + {/* Center label */} +
+
+

{total}

+

Total

+
+
+
+ ) +} diff --git a/components/dashboard/gradient-stats-card.tsx b/components/dashboard/gradient-stats-card.tsx new file mode 100644 index 0000000..ba324af --- /dev/null +++ b/components/dashboard/gradient-stats-card.tsx @@ -0,0 +1,95 @@ +"use client" + +import { Card, CardContent } from "@/components/ui/card" +import { LucideIcon } from "lucide-react" + +interface GradientStatsCardProps { + title: string + subtitle?: string + value: number | string + icon: LucideIcon + gradient: "green" | "coral" | "blue" | "purple" + isLoading?: boolean +} + +const gradientStyles = { + green: { + background: "bg-gradient-to-br from-emerald-50 via-emerald-50 to-teal-100 dark:from-emerald-950/40 dark:via-emerald-950/30 dark:to-teal-950/40", + border: "border-l-4 border-l-emerald-500", + iconBg: "bg-gradient-to-br from-emerald-400 to-teal-500", + iconShadow: "shadow-lg shadow-emerald-500/30", + valueGradient: "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" + }, + coral: { + background: "bg-gradient-to-br from-red-50 via-red-50 to-orange-100 dark:from-red-950/40 dark:via-red-950/30 dark:to-orange-950/40", + border: "border-l-4 border-l-red-500", + iconBg: "bg-gradient-to-br from-red-400 to-orange-500", + iconShadow: "shadow-lg shadow-red-500/30", + valueGradient: "bg-gradient-to-r from-red-600 via-orange-500 to-red-600 dark:from-red-400 dark:via-orange-400 dark:to-red-400" + }, + blue: { + background: "bg-gradient-to-br from-blue-50 via-blue-50 to-indigo-100 dark:from-blue-950/40 dark:via-blue-950/30 dark:to-indigo-950/40", + border: "border-l-4 border-l-blue-500", + iconBg: "bg-gradient-to-br from-blue-400 to-indigo-500", + iconShadow: "shadow-lg shadow-blue-500/30", + valueGradient: "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" + }, + purple: { + background: "bg-gradient-to-br from-purple-50 via-purple-50 to-pink-100 dark:from-purple-950/40 dark:via-purple-950/30 dark:to-pink-950/40", + border: "border-l-4 border-l-purple-500", + iconBg: "bg-gradient-to-br from-purple-400 to-pink-500", + iconShadow: "shadow-lg shadow-purple-500/30", + valueGradient: "bg-gradient-to-r from-purple-600 via-pink-500 to-purple-600 dark:from-purple-400 dark:via-pink-400 dark:to-purple-400" + } +} + +export function GradientStatsCard({ + title, + subtitle = "Work level distribution", + value, + icon: Icon, + gradient, + isLoading = false +}: GradientStatsCardProps) { + const styles = gradientStyles[gradient] + + return ( + + +
+ {/* Large gradient icon */} +
+ +
+ + {/* Content */} +
+

+ {title} +

+

+ {subtitle} +

+ + {isLoading ? ( +
+ ) : ( +

+ {value} +

+ )} +
+
+ + + ) +} diff --git a/components/dashboard/location-bar-chart.tsx b/components/dashboard/location-bar-chart.tsx new file mode 100644 index 0000000..5f52813 --- /dev/null +++ b/components/dashboard/location-bar-chart.tsx @@ -0,0 +1,127 @@ +"use client" + +import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts" +import { Loader2 } from "lucide-react" + +interface LocationData { + name: string + potholes: number + signboards: number + total: number +} + +interface LocationBarChartProps { + data: LocationData[] + isLoading: boolean +} + +const COLORS = { + pothole: "#ef4444", + signboard: "#3b82f6" +} + +export function LocationBarChart({ data, isLoading }: LocationBarChartProps) { + if (isLoading) { + return ( +
+ +
+ ) + } + + if (data.length === 0) { + return ( +
+

No location data available

+

Process videos to see detections by location

+
+ ) + } + + return ( +
+ + + + + + { + if (active && payload && payload.length) { + return ( +
+

{label}

+ {payload.map((entry, index) => ( +
+
+ {entry.name}: + {entry.value} +
+ ))} +
+ ) + } + return null + }} + /> + ( +
+ {payload?.map((entry, index) => ( +
+
+ + {entry.value} + +
+ ))} +
+ )} + /> + + + + +
+ ) +} diff --git a/components/dashboard/recent-analyses-table.tsx b/components/dashboard/recent-analyses-table.tsx new file mode 100644 index 0000000..53725a1 --- /dev/null +++ b/components/dashboard/recent-analyses-table.tsx @@ -0,0 +1,114 @@ +"use client" + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Eye, AlertCircle } from "lucide-react" +import { type Video } from "@/lib/api" + +interface RecentAnalysesTableProps { + videos: Video[] + isLoading?: boolean + onViewResults?: (videoId: string, detectionType: string) => void +} + +export function RecentAnalysesTable({ videos, isLoading, onViewResults }: RecentAnalysesTableProps) { + const formatDate = (dateString: string) => { + const date = new Date(dateString) + return date.toLocaleDateString("en-IN", { + day: "2-digit", + month: "short", + year: "numeric", + hour: "2-digit", + minute: "2-digit" + }) + } + + const getStatusBadge = (status: string) => { + switch (status) { + case "completed": + return Completed + case "processing": + return Processing + case "pending": + return Pending + case "failed": + return Failed + default: + return {status} + } + } + + const getDetectionTypeBadge = (type: string) => { + if (type === "pothole-detection") { + return Pothole + } + return Signboard + } + + return ( + + + Recent Analyses + + + {isLoading ? ( +
+ {[1, 2, 3, 4].map((i) => ( +
+ ))} +
+ ) : videos.length === 0 ? ( +
+ +

No analyses found

+

Start a new analysis to see results here

+
+ ) : ( +
+ {videos.slice(0, 8).map((video) => ( +
+
+
+

+ {video.filename || `Video ${video.id.slice(0, 8)}...`} +

+

+ {formatDate(video.created_at)} +

+
+
+ {getDetectionTypeBadge(video.detection_type)} + {getStatusBadge(video.status)} +
+
+

+ {video.detection_type === "pothole-detection" + ? video.unique_potholes ?? 0 + : video.unique_signboards ?? 0} +

+

Detections

+
+
+ {video.status === "completed" && onViewResults && ( + + )} +
+ ))} +
+ )} + + + ) +} diff --git a/components/dashboard/stats-card.tsx b/components/dashboard/stats-card.tsx new file mode 100644 index 0000000..31728a0 --- /dev/null +++ b/components/dashboard/stats-card.tsx @@ -0,0 +1,38 @@ +"use client" + +import { Card, CardContent } from "@/components/ui/card" +import { LucideIcon } from "lucide-react" + +interface StatsCardProps { + title: string + value: string | number + icon: LucideIcon + gradient: string + isLoading?: boolean +} + +export function StatsCard({ title, value, icon: Icon, gradient, isLoading }: StatsCardProps) { + return ( + + +
+
+

+ {title} +

+ {isLoading ? ( +
+ ) : ( +

+ {value} +

+ )} +
+
+ +
+
+ + + ) +} diff --git a/components/navigation-menu.tsx b/components/navigation-menu.tsx new file mode 100644 index 0000000..16d7ebf --- /dev/null +++ b/components/navigation-menu.tsx @@ -0,0 +1,129 @@ +"use client" + +import { useState } from "react" +import { useRouter, usePathname } from "next/navigation" +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet" +import { Button } from "@/components/ui/button" +import { + Menu, + LayoutDashboard, + ChevronRight, + Home, + Map +} from "lucide-react" + +interface NavItem { + title: string + description: string + href: string + icon: React.ElementType +} + +const navItems: NavItem[] = [ + { + title: "Home", + description: "Project selection and analysis setup", + href: "/", + icon: Home + }, + { + title: "Dashboard", + description: "View analytics, statistics, and recent analyses", + href: "/dashboard", + icon: LayoutDashboard + }, + { + title: "Show Map", + description: "View all detections on an interactive map", + href: "/map", + icon: Map + } +] + +export function NavigationMenu() { + const [isOpen, setIsOpen] = useState(false) + const router = useRouter() + const pathname = usePathname() + + const handleNavigation = (href: string) => { + setIsOpen(false) + router.push(href) + } + + return ( + + + + + + + + VisionRoad + +

+ AI-Powered Road Detection System +

+
+ + {/* Navigation Items */} + + + {/* Footer */} +
+

+ Sentient Geeks Pvt. Ltd. +

+
+
+
+ ) +} diff --git a/components/project-selection-section.tsx b/components/project-selection-section.tsx index 919b739..af94aa8 100644 --- a/components/project-selection-section.tsx +++ b/components/project-selection-section.tsx @@ -5,7 +5,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Button } from "@/components/ui/button" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Loader2, MapPin, Package, FolderKanban, ArrowRight, AlertCircle } from "lucide-react" +import { Loader2 } from "lucide-react" import { fetchProjects, fetchPackagesByProject, @@ -139,153 +139,176 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio const isComplete = selectedProject && selectedPackage && selectedLocation + // Step status helpers + const getStepStatus = (step: number) => { + if (step === 1) return selectedProject ? 'completed' : 'active' + if (step === 2) return selectedPackage ? 'completed' : selectedProject ? 'active' : 'pending' + if (step === 3) return selectedLocation ? 'completed' : selectedPackage ? 'active' : 'pending' + return 'pending' + } + return ( - - -
-
- -
+ + +
- Select Project Location - - Choose your project, package, and location to begin video analysis + Select Project Location + + Select Project, Package & Location to begin intelligent road analysis with advanced computer vision.
+ + {/* Step Progress Indicator */} +
+ {[1, 2, 3].map((step, index) => { + const status = getStepStatus(step) + const labels = ['Project', 'Package', 'Location'] + return ( +
+
+
+ {step} +
+ + {labels[index]} + +
+ {index < 2 && ( +
+ )} +
+ ) + })} +
- + + {/* Error Display */} {error && ( -
- -

{error}

+
+
+ ! +
+

{error}

)}
{/* Project Dropdown */} -
-
{/* Package Dropdown */} -
-
{/* Location Dropdown */} -
-
{/* Selected Summary */} {isComplete && ( -
-

Selected:

-

- {selectedProject?.name} → {selectedPackage?.name} → {selectedLocation?.segment_name} +

+

+ Path: + {selectedProject?.name} + / + {selectedPackage?.name} + / + {selectedLocation?.segment_name}

)} @@ -294,14 +317,12 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio + + {/* Tooltip */} +
+ {item.title}{item.disabled && " (Coming Soon)"} + {/* Arrow */} +
+
+
+ ) + })} + + + {/* Bottom section */} +
+
+ +
+
+ + ) +} diff --git a/components/video-player-section.tsx b/components/video-player-section.tsx index f4a0896..0bfc47d 100644 --- a/components/video-player-section.tsx +++ b/components/video-player-section.tsx @@ -1,12 +1,15 @@ "use client" import { useEffect, useRef, useState, useCallback } from "react" +import dynamic from "next/dynamic" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { ScrollArea } from "@/components/ui/scroll-area" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Target, AlertTriangle, Film, Activity, Gauge, Monitor, SignpostBig, Map as MapIcon } from "lucide-react" -import MapModal from "@/components/map-modal" + +// Dynamically import MapModal with SSR disabled (Leaflet requires window object) +const MapModal = dynamic(() => import("@/components/map-modal"), { ssr: false }) const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8000/api/v1" @@ -194,22 +197,31 @@ function DetailedSummarySection({ return (
{/* Location-based Summary */} - - + +
-
- Detection Locations - - {isPothole ? "Potholes" : "Signboards"} detected across project locations - +
+
+ +
+
+ + + Detection Locations + + + + {isPothole ? "Potholes" : "Signboards"} detected across project locations + +
@@ -245,12 +257,23 @@ function DetailedSummarySection({ {/* All Detections List */} - - - All Detections - - Complete list of {isPothole ? "potholes" : "signboards"} with GPS coordinates - + + +
+
+ +
+
+ + + All Detections + + + + Complete list of {isPothole ? "potholes" : "signboards"} with GPS coordinates + +
+
@@ -345,12 +368,23 @@ function SummarySection({ data, show, detectionType }: { data: DetectionData; sh ] return ( - - - Quick Stats - - Overview of {isPothole ? "pothole" : "signboard"} detection results - + + +
+
+ +
+
+ + + Quick Stats + + + + Overview of {isPothole ? "pothole" : "signboard"} detection results + +
+
@@ -359,14 +393,14 @@ function SummarySection({ data, show, detectionType }: { data: DetectionData; sh return (
-
- +
+
-
{stat.value}
-
{stat.label}
+
{stat.value}
+
{stat.label}
) })} @@ -792,12 +826,23 @@ export default function VideoPlayerSection({ data, videoId, videoFile, detection return (
- - - Video Playback with Detection - - Watch the video with real-time {isPothole ? "pothole" : "signboard"} detection overlays - + + +
+
+ +
+
+ + + Video Playback with Detection + + + + Watch the video with real-time {isPothole ? "pothole" : "signboard"} detection overlays + +
+
diff --git a/lib/api.ts b/lib/api.ts index 126b9f3..84fde1a 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -81,6 +81,111 @@ export async function fetchLocationsByPackage(packageId: string): Promise(`/locations/?package_id=${packageId}`) } +/** + * Fetch all packages (for dashboard) + */ +export async function fetchAllPackages(): Promise { + return apiRequest("/packages/") +} + +/** + * Fetch all locations (for dashboard) + */ +export async function fetchAllLocations(): Promise { + return apiRequest("/locations/") +} + +// Video type for dashboard +export interface Video { + id: string + filename: string + detection_type: "pothole-detection" | "sign-board-detection" + status: "pending" | "processing" | "completed" | "failed" + unique_potholes?: number + unique_signboards?: number + total_detections?: number + created_at: string + updated_at: string +} + +/** + * Fetch all videos (for dashboard) + */ +export async function fetchVideos(): Promise { + const response = await apiRequest<{ videos: Array<{ video_id: string; status: string; progress: number; summary?: { unique_potholes?: number; unique_signboards?: number; total_detections?: number } }> }>("/videos") + + // Transform the response to match our Video interface + return response.videos.map(v => ({ + id: v.video_id, + filename: v.video_id, // Using video_id as filename since backend doesn't provide it + detection_type: v.summary?.unique_potholes !== undefined ? "pothole-detection" : "sign-board-detection" as const, + status: v.status as Video["status"], + unique_potholes: v.summary?.unique_potholes, + unique_signboards: v.summary?.unique_signboards, + total_detections: v.summary?.total_detections, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + })) +} + +// Detection type for map display +export interface Detection { + id: number + video_id: string + type: string + class: string + confidence: number + latitude: number | null + longitude: number | null + frame_number: number + timestamp_ms: number +} + +/** + * Fetch all detections from completed videos (for dashboard map) + * Uses the summary endpoint to get detections for each project + */ +export async function fetchAllDetections(): Promise { + try { + // First get all projects + const projects = await fetchProjects() + + // Then fetch detections for each project + const allDetections: Detection[] = [] + + for (const project of projects) { + try { + const summary = await apiRequest<{ + packages: { + [key: string]: { + locations: { + [key: string]: { + detections: Detection[] + } + } + } + } + }>(`/summary/projects/${project.id}`) + + // Extract detections from the nested structure + for (const pkg of Object.values(summary.packages || {})) { + for (const loc of Object.values(pkg.locations || {})) { + allDetections.push(...(loc.detections || [])) + } + } + } catch (e) { + // Skip projects that fail to load + console.warn(`Failed to load detections for project ${project.id}:`, e) + } + } + + return allDetections + } catch (e) { + console.error("Failed to fetch all detections:", e) + return [] + } +} + /** * Session context type for storing user selections */