refactor: remove unused files

This commit is contained in:
2026-03-18 19:39:13 +05:30
parent 598098d7e8
commit 42624cae94
10 changed files with 156 additions and 1092 deletions

View File

@@ -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<typeof Sidebar>) {
const pathname = usePathname();
const pathname = usePathname();
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<div className="flex items-center gap-2 py-2">
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<Package className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
<span className="truncate font-semibold">Vision Road</span>
<span className="truncate text-xs">Analytics Platform</span>
</div>
</div>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarMenu>
{data.navMain.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
tooltip={item.title}
isActive={pathname === item.url}
>
<Link href={item.url}>
{item.icon && <item.icon />}
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
<SidebarRail />
</Sidebar>
);
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<div className="flex items-center gap-2 py-2">
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<Package className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
<span className="truncate font-semibold">Vision Road</span>
<span className="truncate text-xs">Analytics Platform</span>
</div>
</div>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarMenu>
{data.navMain.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild tooltip={item.title} isActive={pathname === item.url}>
<Link href={item.url}>
{item.icon && <item.icon />}
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
<SidebarRail />
</Sidebar>
);
}

View File

@@ -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 (
<div className="flex flex-wrap items-center gap-3 p-3 rounded-md border">
{/* Project Dropdown */}
<div className="flex flex-col min-w-[120px]">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">Project</span>
<Select
value={selectedProjectId || ""}
onValueChange={onProjectChange}
disabled={isLoading || projects.length === 0}
>
<SelectTrigger className="h-auto p-0 border-0 shadow-none focus:ring-0 text-sm font-bold bg-transparent text-left">
<SelectValue placeholder="Select project" />
</SelectTrigger>
<SelectContent>
{projects.map((project) => (
<SelectItem key={project.id} value={project.id}>
{project.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Corridor Badge */}
{selectedProject?.corridor_name && (
<>
<div className="h-8 w-px bg-border mx-2" />
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">Corridor</span>
<span className="text-xs font-bold text-muted-foreground">
{selectedProject.corridor_name}
</span>
</div>
</>
)}
{/* State Badge */}
{selectedProject?.state && (
<>
<div className="h-8 w-px bg-border mx-2" />
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">State</span>
<span className="text-xs font-bold text-muted-foreground">
{selectedProject.state}
</span>
</div>
</>
)}
</div>
)
}

View File

@@ -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 (
<Card className="overflow-hidden border shadow-sm">
<CardContent className="p-6 flex items-center justify-between gap-4">
<div className="flex flex-col gap-1 min-w-0">
<h3 className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest">
{title}
</h3>
<div className="flex flex-col">
{isLoading ? (
<div className="h-10 w-24 bg-muted animate-pulse rounded-md mt-1" />
) : (
<>
<p className="text-3xl font-bold tracking-tight">
{value}
</p>
{subtitle && (
<p className="text-[10px] font-semibold text-muted-foreground mt-0.5">
{subtitle}
</p>
)}
</>
)}
</div>
</div>
<div className="p-3 rounded-lg bg-secondary shrink-0">
<Icon className="h-6 w-6 text-primary" />
</div>
</CardContent>
</Card>
)
}

View File

@@ -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 (
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={item.isActive}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
{item.icon && <item.icon />}
<span>{item.title}</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<a href={subItem.url}>
<span>{subItem.title}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
)
}

View File

@@ -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 (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
{projects.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.name}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover>
<MoreHorizontal />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48 rounded-lg"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
<DropdownMenuItem>
<Folder className="text-muted-foreground" />
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Forward className="text-muted-foreground" />
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 className="text-muted-foreground" />
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton className="text-sidebar-foreground/70">
<MoreHorizontal className="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
)
}

View File

@@ -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 (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<activeTeam.logo className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{activeTeam.name}
</span>
<span className="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="start"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-xs text-muted-foreground">
Teams
</DropdownMenuLabel>
{teams.map((team, index) => (
<DropdownMenuItem
key={team.name}
onClick={() => setActiveTeam(team)}
className="gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-sm border">
<team.logo className="size-4 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
<Plus className="size-4" />
</div>
<div className="font-medium text-muted-foreground">Add team</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}

View File

@@ -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 (
<div
role="list"
data-slot="item-group"
className={cn('group/item-group flex flex-col', className)}
{...props}
/>
)
}
function ItemSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="item-separator"
orientation="horizontal"
className={cn('my-0', className)}
{...props}
/>
)
}
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<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'div'
return (
<Comp
data-slot="item"
data-variant={variant}
data-size={size}
className={cn(itemVariants({ variant, size, className }))}
{...props}
/>
)
}
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<typeof itemMediaVariants>) {
return (
<div
data-slot="item-media"
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
)
}
function ItemContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-content"
className={cn(
'flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none',
className,
)}
{...props}
/>
)
}
function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-title"
className={cn(
'flex w-fit items-center gap-2 text-sm leading-snug font-medium',
className,
)}
{...props}
/>
)
}
function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
return (
<p
data-slot="item-description"
className={cn(
'text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance',
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
className,
)}
{...props}
/>
)
}
function ItemActions({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-actions"
className={cn('flex items-center gap-2', className)}
{...props}
/>
)
}
function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-header"
className={cn(
'flex basis-full items-center justify-between gap-2',
className,
)}
{...props}
/>
)
}
function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-footer"
className={cn(
'flex basis-full items-center justify-between gap-2',
className,
)}
{...props}
/>
)
}
export {
Item,
ItemMedia,
ItemContent,
ItemActions,
ItemGroup,
ItemSeparator,
ItemTitle,
ItemDescription,
ItemHeader,
ItemFooter,
}

View File

@@ -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<File | null>(null)
const [jsonFile, setJsonFile] = useState<File | null>(null)
const [speed, setSpeed] = useState(30)
const [detectionType, setDetectionType] = useState<DetectionType>("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<string | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const jsonFileInputRef = useRef<HTMLInputElement>(null)
const wsRef = useRef<WebSocket | null>(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 (
<Card>
<CardHeader>
<CardTitle>Upload Video</CardTitle>
<CardDescription>
Select video file, detection type, vehicle speed, and method for analysis
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div className="space-y-2">
<Label htmlFor="video-file">Video File</Label>
<div className="flex gap-2">
<Input
ref={fileInputRef}
id="video-file"
type="file"
accept="video/*"
onChange={(e) => {
setFile(e.target.files?.[0] || null)
setError(null)
}}
disabled={uploading}
className="flex-1"
/>
</div>
{file && (
<p className="text-sm text-muted-foreground">
Selected: {file.name} ({(file.size / 1024 / 1024).toFixed(2)} MB)
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="json-file">GPS JSON File</Label>
<div className="flex gap-2">
<Input
ref={jsonFileInputRef}
id="json-file"
type="file"
accept=".json,application/json"
onChange={(e) => {
setJsonFile(e.target.files?.[0] || null)
setError(null)
}}
disabled={uploading}
className="flex-1"
/>
</div>
{jsonFile && (
<p className="text-sm text-muted-foreground">
Selected: {jsonFile.name} ({(jsonFile.size / 1024).toFixed(2)} KB)
</p>
)}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="detection-type">Detection Type</Label>
<Select
value={detectionType}
onValueChange={handleDetectionTypeChange}
disabled={uploading}
>
<SelectTrigger id="detection-type">
<SelectValue placeholder="Select detection type" />
</SelectTrigger>
<SelectContent>
{DETECTION_TYPES.map((type) => (
<SelectItem key={type.value} value={type.value}>
<span>{type.label}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="speed">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}
/>
</div>
<div className="space-y-2">
<Label htmlFor="select-method">Select Method</Label>
<Select
value={selectMethod}
onValueChange={setSelectMethod}
disabled={uploading}
>
<SelectTrigger id="select-method">
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
{DETECTION_METHODS.map((method) => (
<SelectItem key={method.value} value={method.value}>
<span>{method.label}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{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 shrink-0" />
<p className="text-sm whitespace-pre-line">{error}</p>
</div>
)}
<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>
{uploading && (
<div className="space-y-3">
<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>
</div>
</div>
)}
</CardContent>
</Card>
)
}