diff --git a/public/avatars/profile.jpg b/public/avatars/profile.jpg new file mode 100644 index 0000000..c3d5590 Binary files /dev/null and b/public/avatars/profile.jpg differ diff --git a/src/app/(modules)/dashboard/page.tsx b/src/app/(modules)/dashboard/page.tsx index fc200a2..9b5362a 100644 --- a/src/app/(modules)/dashboard/page.tsx +++ b/src/app/(modules)/dashboard/page.tsx @@ -1,7 +1,14 @@ -"use client"; +'use client'; -import { useState, useEffect } from "react"; -import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"; +import { useState, useEffect } from 'react'; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, + CardFooter, +} from '@/components/ui/card'; import { AlertCircle, Activity, @@ -13,26 +20,21 @@ import { PencilLine, CheckCircle2, Settings2, -} from "lucide-react"; -import { StatsCard } from "@/components/dashboard/stats-card"; -import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector"; -import { FilterSelector } from "@/components/dashboard/filter-selector"; -import { DetectionDonutChart } from "@/components/dashboard/detection-donut-chart"; -import { ChainageBarChart } from "@/components/dashboard/chainage-bar-chart" -import { DashboardMap } from "@/components/dashboard/dashboard-map"; -import { PageHeader } from "@/components/page-header"; -import { projectService } from "@/services/api"; -import { Project } from "@/types"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { Button } from "@/components/ui/button"; -import { DashboardSkeleton } from "@/components/dashboard/dashboard-skeleton" -import { PoweredBy } from "@/components/powered-by"; -import { toast } from "sonner"; -import { Reveal, StaggerContainer, StaggerItem } from "@/components/ui/reveal"; +} from 'lucide-react'; +import { StatsCard } from '@/components/dashboard/stats-card'; +import { FilterSelector } from '@/components/dashboard/filter-selector'; +import { DetectionDonutChart } from '@/components/dashboard/detection-donut-chart'; +import { ChainageBarChart } from '@/components/dashboard/chainage-bar-chart'; +import { DashboardMap } from '@/components/dashboard/dashboard-map'; +import { PageHeader } from '@/components/page-header'; +import { projectService } from '@/services/api'; +import { Project } from '@/types'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Button } from '@/components/ui/button'; +import { DashboardSkeleton } from '@/components/dashboard/dashboard-skeleton'; +import { PoweredBy } from '@/components/powered-by'; +import { toast } from 'sonner'; +import { Reveal, StaggerContainer, StaggerItem } from '@/components/ui/reveal'; const API_URL = process.env.NEXT_PUBLIC_API_URL; @@ -103,42 +105,41 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats { let total_damaged_road_marking = 0; let total_good_sign_board = 0; let total_road_damage = 0; - const chainageData: DetectionStats["chainageData"] = []; + const chainageData: DetectionStats['chainageData'] = []; for (const pkg of Object.values(summary.packages || {})) { - for (const [chnName, chn] of Object.entries(pkg.chainages || {})) { // Changed 'locName, loc' to 'chnName, chn' and 'pkg.locations' to 'pkg.chainages' + for (const [chnName, chn] of Object.entries(pkg.chainages || {})) { + // Changed 'locName, loc' to 'chnName, chn' and 'pkg.locations' to 'pkg.chainages' let chnDefectedSignboard = 0; // Changed 'locDefectedSignboard' to 'chnDefectedSignboard' let chnPothole = 0; // Changed 'locPothole' to 'chnPothole' let chnRoadCrack = 0; // Changed 'locRoadCrack' to 'chnRoadCrack' let chnDamagedRoadMarking = 0; // Changed 'locDamagedRoadMarking' to 'chnDamagedRoadMarking' let chnGoodSignboard = 0; // Changed 'locGoodSignboard' to 'chnGoodSignboard' - for (const detection of chn.detections || []) { // Changed 'loc.detections' to 'chn.detections' + for (const detection of chn.detections || []) { + // Changed 'loc.detections' to 'chn.detections' const type = detection.type.toLowerCase(); - if (type === "defected_sign_board") { + if (type === 'defected_sign_board') { chnDefectedSignboard++; total_defected_sign_board++; - } else if (type === "pothole") { + } else if (type === 'pothole') { chnPothole++; total_pothole++; - } else if (type === "road_crack") { + } else if (type === 'road_crack') { chnRoadCrack++; total_road_crack++; - } else if (type === "damaged_road_marking") { + } else if (type === 'damaged_road_marking') { chnDamagedRoadMarking++; total_damaged_road_marking++; - } else if (type === "good_sign_board") { + } else if (type === 'good_sign_board') { chnGoodSignboard++; total_good_sign_board++; } } const chnTotalDamage = // Changed 'locTotalDamage' to 'chnTotalDamage' - chnDefectedSignboard + - chnPothole + - chnRoadCrack + - chnDamagedRoadMarking; - + chnDefectedSignboard + chnPothole + chnRoadCrack + chnDamagedRoadMarking; + if ( chnDefectedSignboard > 0 || chnPothole > 0 || @@ -146,9 +147,9 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats { chnDamagedRoadMarking > 0 || chnGoodSignboard > 0 ) { - const shortName = - chnName.length > 20 ? chnName.substring(0, 20) + "..." : chnName; - chainageData.push({ // Changed 'locationData.push' to 'chainageData.push' + const shortName = chnName.length > 20 ? chnName.substring(0, 20) + '...' : chnName; + chainageData.push({ + // Changed 'locationData.push' to 'chainageData.push' name: shortName, defected_sign_board: chnDefectedSignboard, pothole: chnPothole, @@ -167,10 +168,7 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats { } total_road_damage = - total_defected_sign_board + - total_pothole + - total_road_crack + - total_damaged_road_marking; + total_defected_sign_board + total_pothole + total_road_crack + total_damaged_road_marking; return { totalDefectedSignboard: total_defected_sign_board, @@ -186,12 +184,8 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats { export default function DashboardPage() { const [isLoading, setIsLoading] = useState(true); const [projects, setProjects] = useState([]); - const [selectedProjectId, setSelectedProjectId] = useState( - null, - ); - const [projectSummary, setProjectSummary] = useState( - null, - ); + const [selectedProjectId, setSelectedProjectId] = useState(null); + const [projectSummary, setProjectSummary] = useState(null); const [stats, setStats] = useState({ totalDefectedSignboard: 0, totalPothole: 0, @@ -214,9 +208,9 @@ export default function DashboardPage() { setSelectedProjectId(projectsData.items[0].id); } } catch (err) { - console.error("Failed to load projects:", err); - toast.error("Failed to load projects", { - description: "Please check if the backend is running." + console.error('Failed to load projects:', err); + toast.error('Failed to load projects', { + description: 'Please check if the backend is running.', }); } finally { setTimeout(() => { @@ -239,18 +233,12 @@ export default function DashboardPage() { : []; // Extract chainages from selected package - const [selectedPackageId, setSelectedPackageId] = useState( - null, - ); - const [selectedChainageId, setSelectedChainageId] = useState( // Changed 'selectedLocationId' to 'selectedChainageId' - null, - ); + const [selectedPackageId, setSelectedPackageId] = useState(null); + const [selectedChainageId, setSelectedChainageId] = useState(null); // Changed 'selectedLocationId' to 'selectedChainageId' const chainages = - projectSummary && selectedPackageId && selectedPackageId !== "all" - ? Object.keys( - projectSummary.packages[selectedPackageId]?.chainages || {}, - ).map((chnName) => ({ + projectSummary && selectedPackageId && selectedPackageId !== 'all' + ? Object.keys(projectSummary.packages[selectedPackageId]?.chainages || {}).map((chnName) => ({ id: chnName, name: chnName, })) @@ -270,8 +258,8 @@ export default function DashboardPage() { const summary: ProjectSummary = await projectService.getProjectSummary(selectedProjectId); setProjectSummary(summary); } catch (err) { - console.error("Failed to load project summary:", err); - toast.error("Failed to load project summary"); + console.error('Failed to load project summary:', err); + toast.error('Failed to load project summary'); setProjectSummary(null); } finally { setTimeout(() => { @@ -314,16 +302,16 @@ export default function DashboardPage() { let totalRoadCrack = 0; let totalDamagedRoadMarking = 0; let totalGoodSignboard = 0; - const chainageData: DetectionStats["chainageData"] = []; + const chainageData: DetectionStats['chainageData'] = []; const packagesToProcess = - selectedPackageId && selectedPackageId !== "all" + selectedPackageId && selectedPackageId !== 'all' ? { [selectedPackageId]: projectSummary.packages[selectedPackageId] } : projectSummary.packages || {}; for (const [pkgName, pkg] of Object.entries(packagesToProcess)) { const chainagesToProcess = - selectedChainageId && selectedChainageId !== "all" + selectedChainageId && selectedChainageId !== 'all' ? { [selectedChainageId]: pkg.chainages[selectedChainageId] } : pkg.chainages || {}; @@ -338,19 +326,19 @@ export default function DashboardPage() { for (const detection of chn.detections || []) { const type = detection.type.toLowerCase(); - if (type === "defected_sign_board") { + if (type === 'defected_sign_board') { chnDefectedSignboard++; totalDefectedSignboard++; - } else if (type === "pothole") { + } else if (type === 'pothole') { chnPothole++; totalPothole++; - } else if (type === "road_crack") { + } else if (type === 'road_crack') { chnRoadCrack++; totalRoadCrack++; - } else if (type === "damaged_road_marking") { + } else if (type === 'damaged_road_marking') { chnDamagedRoadMarking++; totalDamagedRoadMarking++; - } else if (type === "good_sign_board") { + } else if (type === 'good_sign_board') { chnGoodSignboard++; totalGoodSignboard++; } @@ -363,8 +351,7 @@ export default function DashboardPage() { chnDamagedRoadMarking > 0 || chnGoodSignboard > 0 ) { - const shortName = - chnName.length > 20 ? chnName.substring(0, 20) + "..." : chnName; + const shortName = chnName.length > 20 ? chnName.substring(0, 20) + '...' : chnName; chainageData.push({ name: shortName, defected_sign_board: chnDefectedSignboard, @@ -390,10 +377,7 @@ export default function DashboardPage() { totalDamagedRoadMarking, totalGoodSignboard, totalRoadDamage: - totalDefectedSignboard + - totalPothole + - totalRoadCrack + - totalDamagedRoadMarking, + totalDefectedSignboard + totalPothole + totalRoadCrack + totalDamagedRoadMarking, chainageData, }); }, [projectSummary, selectedPackageId, selectedChainageId]); @@ -425,7 +409,9 @@ export default function DashboardPage() { Filter Analysis -

Refine your view by project, package, or chainage

+

+ Refine your view by project, package, or chainage +

- {isLoading && !projectSummary ? ( ) : ( @@ -532,13 +517,12 @@ export default function DashboardPage() { Severity by Chainage - Detections grouped by road segment + + Detections grouped by road segment + - +
@@ -552,14 +536,14 @@ export default function DashboardPage() { {/* Map - Full Width Below Charts */}
-
-
+ )} diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index 8c0b1dd..295c57d 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -1,106 +1,95 @@ -"use client"; -import * as React from "react"; +'use client'; +import * as React from 'react'; +import { LayoutDashboard, Plus, Layers, Package, MapPin, Milestone } from 'lucide-react'; +import { NavUser } from '@/components/nav-user'; +import { ROUTES } from '@/utils/routes'; import { - LayoutDashboard, - Plus, - Layers, - Package, - MapPin, - Milestone, -} from "lucide-react"; -import { NavUser } from "@/components/nav-user"; -import { ROUTES } from "@/utils/routes"; -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarHeader, - SidebarRail, - SidebarMenu, - SidebarMenuItem, - SidebarMenuButton, - SidebarGroup, - SidebarGroupLabel, -} from "@/components/ui/sidebar"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarRail, + SidebarMenu, + SidebarMenuItem, + SidebarMenuButton, + SidebarGroup, + SidebarGroupLabel, +} from '@/components/ui/sidebar'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; // This is the navigation data const data = { - user: { - name: "shadcn", - email: "m@example.com", - avatar: "/avatars/shadcn.jpg", + user: { + name: 'Vision Admin', + email: 'admin@visionroad.ai', + avatar: '/avatars/profile.jpg', + }, + navMain: [ + { + title: 'Dashboard', + url: ROUTES.DASHBOARD, + icon: LayoutDashboard, }, - navMain: [ - { - title: "Dashboard", - url: ROUTES.DASHBOARD, - icon: LayoutDashboard, - }, - { - title: "New Analysis", - url: ROUTES.NEW_ANALYSIS, - icon: Plus, - }, - { - title: "Project", - url: ROUTES.PROJECT, - icon: Layers, - }, - { - title: "Package", - url: ROUTES.PACKAGE, - icon: Package, - }, - { - title: "Chainage", - url: ROUTES.CHAINAGE, - icon: Milestone, - }, - ], + { + title: 'New Analysis', + url: ROUTES.NEW_ANALYSIS, + icon: Plus, + }, + { + title: 'Project', + url: ROUTES.PROJECT, + icon: Layers, + }, + { + title: 'Package', + url: ROUTES.PACKAGE, + icon: Package, + }, + { + title: 'Chainage', + url: ROUTES.CHAINAGE, + icon: Milestone, + }, + ], }; export function AppSidebar({ ...props }: React.ComponentProps) { - const pathname = usePathname(); + const pathname = usePathname(); - return ( - - -
-
- -
-
- Vision Road - Analytics Platform -
-
-
- - - - {data.navMain.map((item) => ( - - - - {item.icon && } - {item.title} - - - - ))} - - - - - - - -
- ); + return ( + + +
+
+ +
+
+ Vision Road + Analytics Platform +
+
+
+ + + + {data.navMain.map((item) => ( + + + + {item.icon && } + {item.title} + + + + ))} + + + + + + + +
+ ); } diff --git a/src/components/dashboard/compact-project-selector.tsx b/src/components/dashboard/compact-project-selector.tsx deleted file mode 100644 index 57b8d8d..0000000 --- a/src/components/dashboard/compact-project-selector.tsx +++ /dev/null @@ -1,77 +0,0 @@ -"use client" - -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" -import { Project } from "@/types" - -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 */} -
- Project - -
- - {/* Corridor Badge */} - {selectedProject?.corridor_name && ( - <> -
-
- Corridor - - {selectedProject.corridor_name} - -
- - )} - - {/* State Badge */} - {selectedProject?.state && ( - <> -
-
- State - - {selectedProject.state} - -
- - )} -
- ) -} diff --git a/src/components/dashboard/gradient-stats-card.tsx b/src/components/dashboard/gradient-stats-card.tsx deleted file mode 100644 index 633eba1..0000000 --- a/src/components/dashboard/gradient-stats-card.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client" - -import { Card, CardContent } from "@/components/ui/card" -import { LucideIcon } from "lucide-react" -import { cn } from "@/lib/utils" - -interface GradientStatsCardProps { - title: string - subtitle?: string - value: number | string - icon: LucideIcon - gradient?: "green" | "coral" | "blue" | "purple" | "orange" | "indigo" | "emerald" - isLoading?: boolean -} - -export function GradientStatsCard({ - title, - subtitle, - value, - icon: Icon, - isLoading = false -}: GradientStatsCardProps) { - return ( - - -
-

- {title} -

-
- {isLoading ? ( -
- ) : ( - <> -

- {value} -

- {subtitle && ( -

- {subtitle} -

- )} - - )} -
-
- -
- -
- - - ) -} diff --git a/src/components/nav-main.tsx b/src/components/nav-main.tsx deleted file mode 100644 index 1d71af1..0000000 --- a/src/components/nav-main.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"use client" - -import { ChevronRight, type LucideIcon } from "lucide-react" - -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible" -import { - SidebarGroup, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, -} from "@/components/ui/sidebar" - -export function NavMain({ - items, -}: { - items: { - title: string - url: string - icon?: LucideIcon - isActive?: boolean - items?: { - title: string - url: string - }[] - }[] -}) { - return ( - - Platform - - {items.map((item) => ( - - - - - {item.icon && } - {item.title} - - - - - - {item.items?.map((subItem) => ( - - - - {subItem.title} - - - - ))} - - - - - ))} - - - ) -} diff --git a/src/components/nav-projects.tsx b/src/components/nav-projects.tsx deleted file mode 100644 index f50b20d..0000000 --- a/src/components/nav-projects.tsx +++ /dev/null @@ -1,89 +0,0 @@ -"use client" - -import { - Folder, - Forward, - MoreHorizontal, - Trash2, - type LucideIcon, -} from "lucide-react" - -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { - SidebarGroup, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuAction, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@/components/ui/sidebar" - -export function NavProjects({ - projects, -}: { - projects: { - name: string - url: string - icon: LucideIcon - }[] -}) { - const { isMobile } = useSidebar() - - return ( - - Projects - - {projects.map((item) => ( - - - - - {item.name} - - - - - - - More - - - - - - View Project - - - - Share Project - - - - - Delete Project - - - - - ))} - - - - More - - - - - ) -} diff --git a/src/components/team-switcher.tsx b/src/components/team-switcher.tsx deleted file mode 100644 index 2808e0a..0000000 --- a/src/components/team-switcher.tsx +++ /dev/null @@ -1,89 +0,0 @@ -"use client" - -import * as React from "react" -import { ChevronsUpDown, Plus } from "lucide-react" - -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@/components/ui/sidebar" - -export function TeamSwitcher({ - teams, -}: { - teams: { - name: string - logo: React.ElementType - plan: string - }[] -}) { - const { isMobile } = useSidebar() - const [activeTeam, setActiveTeam] = React.useState(teams[0]) - - return ( - - - - - -
- -
-
- - {activeTeam.name} - - {activeTeam.plan} -
- -
-
- - - Teams - - {teams.map((team, index) => ( - setActiveTeam(team)} - className="gap-2 p-2" - > -
- -
- {team.name} - ⌘{index + 1} -
- ))} - - -
- -
-
Add team
-
-
-
-
-
- ) -} diff --git a/src/components/ui/item.tsx b/src/components/ui/item.tsx deleted file mode 100644 index efdf58c..0000000 --- a/src/components/ui/item.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import * as React from 'react' -import { Slot } from '@radix-ui/react-slot' -import { cva, type VariantProps } from 'class-variance-authority' - -import { cn } from '@/lib/utils' -import { Separator } from '@/components/ui/separator' - -function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ) -} - -function ItemSeparator({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -const itemVariants = cva( - 'group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a&]:hover:bg-accent/50 [a&]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]', - { - variants: { - variant: { - default: 'bg-transparent', - outline: 'border-border', - muted: 'bg-muted/50', - }, - size: { - default: 'p-4 gap-4 ', - sm: 'py-3 px-4 gap-2.5', - }, - }, - defaultVariants: { - variant: 'default', - size: 'default', - }, - }, -) - -function Item({ - className, - variant = 'default', - size = 'default', - asChild = false, - ...props -}: React.ComponentProps<'div'> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : 'div' - return ( - - ) -} - -const itemMediaVariants = cva( - 'flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5', - { - variants: { - variant: { - default: 'bg-transparent', - icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4", - image: - 'size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover', - }, - }, - defaultVariants: { - variant: 'default', - }, - }, -) - -function ItemMedia({ - className, - variant = 'default', - ...props -}: React.ComponentProps<'div'> & VariantProps) { - return ( -
- ) -} - -function ItemContent({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ) -} - -function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ) -} - -function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) { - return ( -

a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4', - className, - )} - {...props} - /> - ) -} - -function ItemActions({ className, ...props }: React.ComponentProps<'div'>) { - return ( -

- ) -} - -function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ) -} - -function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ) -} - -export { - Item, - ItemMedia, - ItemContent, - ItemActions, - ItemGroup, - ItemSeparator, - ItemTitle, - ItemDescription, - ItemHeader, - ItemFooter, -} diff --git a/src/components/upload-section.tsx b/src/components/upload-section.tsx deleted file mode 100644 index 0a9d4df..0000000 --- a/src/components/upload-section.tsx +++ /dev/null @@ -1,334 +0,0 @@ -"use client" - -import { useState, useRef } from "react" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -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 } from "lucide-react" -import type { DetectionData, DetectionType } from "@/types" - -import { videoService } from "@/services/api" -const API_URL = process.env.NEXT_PUBLIC_API_URL -const WS_URL = API_URL?.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://") - -const DETECTION_TYPES = [ - { value: "pothole-detection", label: "Pothole Detection" }, - { value: "sign-board-detection", label: "Signboard Detection" }, - { value: "pot-sign-detection", label: "Pothole & Signboard Detection" }, -] as const - -const DETECTION_METHODS = [ - { value: "yolo", label: "YOLO" }, - { value: "yolo_vl", label: "YOLO with VL" }, - { value: "sam3", label: "SAM 3" }, - { value: "yoloe", label: "YOLOE" }, - { value: "yoloe_trained_vl", label: "YOLOE Trained" }, -] as const - -type UploadSectionProps = { - onDetectionComplete: (data: DetectionData, videoId: string, file: File) => void - onDetectionTypeChange: (type: DetectionType) => void -} - -export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: UploadSectionProps) { - const [file, setFile] = useState(null) - const [jsonFile, setJsonFile] = useState(null) - const [speed, setSpeed] = useState(30) - const [detectionType, setDetectionType] = useState("pothole-detection") - const [selectMethod, setSelectMethod] = useState("yolo_vl") - const [uploading, setUploading] = useState(false) - const [progress, setProgress] = useState(0) - const [statusMessage, setStatusMessage] = useState("") - const [error, setError] = useState(null) - const fileInputRef = useRef(null) - const jsonFileInputRef = useRef(null) - const wsRef = useRef(null) - - const handleDetectionTypeChange = (value: DetectionType) => { - setDetectionType(value) - onDetectionTypeChange(value) - } - - const connectWebSocket = (videoId: string) => { - console.log("[Upload] Connecting WebSocket for video:", videoId) - - const ws = new WebSocket(`${WS_URL}/ws/${videoId}`) - wsRef.current = ws - - ws.onopen = () => { - console.log("[Upload] WebSocket connected") - setError(null) - } - - ws.onmessage = (event) => { - const data = JSON.parse(event.data) - console.log("[Upload] WebSocket message:", data) - - if (data.type === "progress" || data.progress !== undefined) { - const progressValue = data.progress || 0 - setProgress(progressValue) - - let message = data.message || "Processing..." - - // Handle both pothole and signboard progress messages - if (data.unique_potholes !== undefined) { - message += ` | Unique: ${data.unique_potholes} | Total: ${data.total_detections || 0}` - } else if (data.unique_signboards !== undefined) { - message += ` | Unique: ${data.unique_signboards} | Total: ${data.total_detections || 0}` - } - - setStatusMessage(message) - } - - if (data.type === "complete" || data.status === "completed") { - setStatusMessage("Processing completed! Loading results...") - ws.close() - setTimeout(() => loadResults(videoId), 500) - } - - if (data.type === "error") { - setError("Error: " + data.message) - setStatusMessage("") - setUploading(false) - ws.close() - } - } - - ws.onerror = (error) => { - console.error("[Upload] WebSocket error:", error) - setStatusMessage("Connection error. Retrying...") - } - - ws.onclose = () => { - console.log("[Upload] WebSocket closed") - } - } - - const loadResults = async (videoId: string) => { - try { - console.log("[Upload] Loading results for:", videoId) - const detectionData: DetectionData = await videoService.getVideoResults(videoId); - console.log("[Upload] Results loaded:", detectionData) - - setUploading(false) - setProgress(100) - setStatusMessage("✓ Complete!") - setError(null) - - // Pass detection data, video_id, and the original file - if (file) { - onDetectionComplete(detectionData, videoId, file) - } - } catch (error) { - console.error("[Upload] Failed to load results:", error) - setError("Failed to load results. Please try again.") - setStatusMessage("") - setUploading(false) - } - } - - const handleUpload = async () => { - if (!file) { - setError("Please select a video file") - return - } - - const formData = new FormData() - formData.append("file", file) - formData.append("detection_type", detectionType) - formData.append("speed_kmh", speed.toString()) - formData.append("detection_mode", selectMethod) - - // Add JSON file if provided - if (jsonFile) { - formData.append("json_file", jsonFile) - } - - setUploading(true) - setProgress(0) - setStatusMessage("Uploading...") - setError(null) - - try { - const result = await videoService.uploadVideo(formData); - const videoId = result.video_id - - console.log("[Upload] Video uploaded successfully, ID:", videoId) - console.log("[Upload] Response:", result) - setStatusMessage("Uploaded! Starting processing...") - setProgress(10) - - // Connect WebSocket for progress updates - connectWebSocket(videoId) - } catch (error) { - console.error("[Upload] Upload error:", error) - - let errorMessage = "Upload failed" - - if (error instanceof TypeError && error.message === "Failed to fetch") { - errorMessage = "Cannot connect to server. Please check if backend is running." - } else if (error instanceof Error) { - errorMessage = error.message - } - - setError(errorMessage) - setStatusMessage("") - setUploading(false) - setProgress(0) - } - } - - return ( - - - Upload Video - - Select video file, detection type, vehicle speed, and method for analysis - - - -
-
- -
- { - setFile(e.target.files?.[0] || null) - setError(null) - }} - disabled={uploading} - className="flex-1" - /> -
- {file && ( -

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

- )} -
- -
- -
- { - setJsonFile(e.target.files?.[0] || null) - setError(null) - }} - disabled={uploading} - className="flex-1" - /> -
- {jsonFile && ( -

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

- )} -
-
- -
-
- - -
- -
- - setSpeed(Number(e.target.value))} - disabled={uploading} - /> -
- -
- - -
-
- - {error && ( -
- -

{error}

-
- )} - - - - {uploading && ( -
- -
- {statusMessage} - {progress}% -
-
- )} -
-
- ) -} \ No newline at end of file