refactor: remove unused files
This commit is contained in:
BIN
public/avatars/profile.jpg
Normal file
BIN
public/avatars/profile.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -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,41 +105,40 @@ 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 ||
|
||||
@@ -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<Project[]>([]);
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [projectSummary, setProjectSummary] = useState<ProjectSummary | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
|
||||
const [projectSummary, setProjectSummary] = useState<ProjectSummary | null>(null);
|
||||
const [stats, setStats] = useState<DetectionStats>({
|
||||
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<string | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedChainageId, setSelectedChainageId] = useState<string | null>( // Changed 'selectedLocationId' to 'selectedChainageId'
|
||||
null,
|
||||
);
|
||||
const [selectedPackageId, setSelectedPackageId] = useState<string | null>(null);
|
||||
const [selectedChainageId, setSelectedChainageId] = useState<string | null>(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() {
|
||||
<Milestone className="h-4 w-4 text-primary" />
|
||||
Filter Analysis
|
||||
</h3>
|
||||
<p className="text-xs mt-1 text-muted-foreground">Refine your view by project, package, or chainage</p>
|
||||
<p className="text-xs mt-1 text-muted-foreground">
|
||||
Refine your view by project, package, or chainage
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<FilterSelector
|
||||
@@ -446,7 +432,6 @@ export default function DashboardPage() {
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
|
||||
{isLoading && !projectSummary ? (
|
||||
<DashboardSkeleton />
|
||||
) : (
|
||||
@@ -532,13 +517,12 @@ export default function DashboardPage() {
|
||||
<Card className="flex-1 h-full">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle className="text-base font-bold">Severity by Chainage</CardTitle>
|
||||
<CardDescription className="text-xs">Detections grouped by road segment</CardDescription>
|
||||
<CardDescription className="text-xs">
|
||||
Detections grouped by road segment
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[300px]">
|
||||
<ChainageBarChart
|
||||
data={stats.chainageData || []}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<ChainageBarChart data={stats.chainageData || []} isLoading={isLoading} />
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="leading-none text-muted-foreground">
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
"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";
|
||||
'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 {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -21,40 +14,40 @@ import {
|
||||
SidebarMenuButton,
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
} from "@/components/ui/sidebar";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
} 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",
|
||||
name: 'Vision Admin',
|
||||
email: 'admin@visionroad.ai',
|
||||
avatar: '/avatars/profile.jpg',
|
||||
},
|
||||
navMain: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
title: 'Dashboard',
|
||||
url: ROUTES.DASHBOARD,
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{
|
||||
title: "New Analysis",
|
||||
title: 'New Analysis',
|
||||
url: ROUTES.NEW_ANALYSIS,
|
||||
icon: Plus,
|
||||
},
|
||||
{
|
||||
title: "Project",
|
||||
title: 'Project',
|
||||
url: ROUTES.PROJECT,
|
||||
icon: Layers,
|
||||
},
|
||||
{
|
||||
title: "Package",
|
||||
title: 'Package',
|
||||
url: ROUTES.PACKAGE,
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
title: "Chainage",
|
||||
title: 'Chainage',
|
||||
url: ROUTES.CHAINAGE,
|
||||
icon: Milestone,
|
||||
},
|
||||
@@ -82,11 +75,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<SidebarMenu>
|
||||
{data.navMain.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
tooltip={item.title}
|
||||
isActive={pathname === item.url}
|
||||
>
|
||||
<SidebarMenuButton asChild tooltip={item.title} isActive={pathname === item.url}>
|
||||
<Link href={item.url}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user