setup husky prettier eslint
This commit is contained in:
@@ -1,336 +1,341 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Loader2, TrendingUp } from "lucide-react"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { PoweredBy } from "@/components/powered-by"
|
||||
import { sessionService, videoService } from "@/services/api"
|
||||
import { SessionContext } from "@/types"
|
||||
import { ROUTES } from "@/utils/routes"
|
||||
import { storeVideoFile } from "@/lib/video-storage"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Loader2, TrendingUp } from 'lucide-react';
|
||||
import { PageHeader } from '@/components/page-header';
|
||||
import { PoweredBy } from '@/components/powered-by';
|
||||
import { sessionService, videoService } from '@/services/api';
|
||||
import { SessionContext } from '@/types';
|
||||
import { ROUTES } from '@/utils/routes';
|
||||
import { storeVideoFile } from '@/lib/video-storage';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
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
|
||||
{ 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 Detection Model" },
|
||||
{ value: "yolo_vl", label: "YOLO with Vision-Language Model" },
|
||||
{ value: "sam3", label: "OpenAI SAM 3 Segmentation Model" },
|
||||
{ value: "yoloe", label: "YOLOE Open-Vocabulary Detection" },
|
||||
{ value: "yoloe_trained_vl", label: "YOLOE With Vision Language Model" },
|
||||
] as const
|
||||
{ value: 'yolo', label: 'YOLO Detection Model' },
|
||||
{ value: 'yolo_vl', label: 'YOLO with Vision-Language Model' },
|
||||
{ value: 'sam3', label: 'OpenAI SAM 3 Segmentation Model' },
|
||||
{ value: 'yoloe', label: 'YOLOE Open-Vocabulary Detection' },
|
||||
{ value: 'yoloe_trained_vl', label: 'YOLOE With Vision Language Model' },
|
||||
] as const;
|
||||
|
||||
type DetectionType = "pothole-detection" | "sign-board-detection" | "pot-sign-detection"
|
||||
type DetectionType = 'pothole-detection' | 'sign-board-detection' | 'pot-sign-detection';
|
||||
|
||||
export default function UploadPage() {
|
||||
const router = useRouter()
|
||||
const [session, setSession] = useState<SessionContext | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const router = useRouter();
|
||||
const [session, setSession] = useState<SessionContext | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Form states
|
||||
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")
|
||||
// Form states
|
||||
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');
|
||||
|
||||
// Upload states
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [progress, setProgress] = useState(0)
|
||||
const [statusMessage, setStatusMessage] = useState("")
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
// Upload states
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [statusMessage, setStatusMessage] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Load session on mount
|
||||
useEffect(() => {
|
||||
const storedSession = sessionService.loadSession()
|
||||
if (!sessionService.isSessionValid(storedSession)) {
|
||||
router.replace(ROUTES.NEW_ANALYSIS)
|
||||
return
|
||||
}
|
||||
setSession(storedSession)
|
||||
setIsLoading(false)
|
||||
}, [router])
|
||||
// Load session on mount
|
||||
useEffect(() => {
|
||||
const storedSession = sessionService.loadSession();
|
||||
if (!sessionService.isSessionValid(storedSession)) {
|
||||
router.replace(ROUTES.NEW_ANALYSIS);
|
||||
return;
|
||||
}
|
||||
setSession(storedSession);
|
||||
setIsLoading(false);
|
||||
}, [router]);
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!file) {
|
||||
setError("Please select a video file")
|
||||
return
|
||||
}
|
||||
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)
|
||||
if (jsonFile) {
|
||||
formData.append("json_file", jsonFile)
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('detection_type', detectionType);
|
||||
formData.append('speed_kmh', speed.toString());
|
||||
formData.append('detection_mode', selectMethod);
|
||||
if (jsonFile) {
|
||||
formData.append('json_file', jsonFile);
|
||||
}
|
||||
|
||||
setUploading(true)
|
||||
setProgress(0)
|
||||
setStatusMessage("Uploading...")
|
||||
setError(null)
|
||||
setUploading(true);
|
||||
setProgress(0);
|
||||
setStatusMessage('Uploading...');
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await videoService.uploadVideo(formData);
|
||||
|
||||
// Store file locally for potential recovery/results display
|
||||
if (file) {
|
||||
try {
|
||||
const result = await videoService.uploadVideo(formData);
|
||||
|
||||
// Store file locally for potential recovery/results display
|
||||
if (file) {
|
||||
try {
|
||||
await storeVideoFile(result.video_id, file)
|
||||
} catch (err) {
|
||||
console.error("Failed to store video file:", err)
|
||||
}
|
||||
}
|
||||
|
||||
sessionService.saveVideoData({ videoId: result.video_id, detectionType })
|
||||
|
||||
// Redirect to the dynamic processing page
|
||||
router.push(`/upload/${result.video_id}`)
|
||||
|
||||
await storeVideoFile(result.video_id, file);
|
||||
} catch (err) {
|
||||
let errorMessage = "Upload failed"
|
||||
if (err instanceof TypeError && err.message === "Failed to fetch") {
|
||||
errorMessage = "Cannot connect to server. Please check if backend is running."
|
||||
} else if (err instanceof Error) {
|
||||
errorMessage = err.message
|
||||
}
|
||||
setError(errorMessage)
|
||||
setStatusMessage("")
|
||||
setUploading(false)
|
||||
setProgress(0)
|
||||
console.error('Failed to store video file:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleBackToSelection = () => {
|
||||
sessionService.clearSession()
|
||||
router.push(ROUTES.NEW_ANALYSIS)
|
||||
}
|
||||
sessionService.saveVideoData({ videoId: result.video_id, detectionType });
|
||||
|
||||
const getTitle = () => {
|
||||
if (detectionType === "pothole-detection") return "Pothole Detection"
|
||||
if (detectionType === "sign-board-detection") return "Signboard Detection"
|
||||
return "Pothole & Signboard Detection"
|
||||
// Redirect to the dynamic processing page
|
||||
router.push(`/upload/${result.video_id}`);
|
||||
} catch (err) {
|
||||
let errorMessage = 'Upload failed';
|
||||
if (err instanceof TypeError && err.message === 'Failed to fetch') {
|
||||
errorMessage = 'Cannot connect to server. Please check if backend is running.';
|
||||
} else if (err instanceof Error) {
|
||||
errorMessage = err.message;
|
||||
}
|
||||
setError(errorMessage);
|
||||
setStatusMessage('');
|
||||
setUploading(false);
|
||||
setProgress(0);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<Card className="p-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const handleBackToSelection = () => {
|
||||
sessionService.clearSession();
|
||||
router.push(ROUTES.NEW_ANALYSIS);
|
||||
};
|
||||
|
||||
const getTitle = () => {
|
||||
if (detectionType === 'pothole-detection') return 'Pothole Detection';
|
||||
if (detectionType === 'sign-board-detection') return 'Signboard Detection';
|
||||
return 'Pothole & Signboard Detection';
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
{/* Main Content */}
|
||||
<main className="min-h-screen flex flex-col">
|
||||
<div className="flex-1 container mx-auto px-6 py-6 max-w-340 flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<PageHeader
|
||||
title={getTitle()}
|
||||
description="Upload video file and fill in required details to start the road analysis"
|
||||
icon={TrendingUp}
|
||||
/>
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<Card className="p-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
{/* Main Content */}
|
||||
<main className="min-h-screen flex flex-col">
|
||||
<div className="flex-1 container mx-auto px-6 py-6 max-w-340 flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<PageHeader
|
||||
title={getTitle()}
|
||||
description="Upload video file and fill in required details to start the road analysis"
|
||||
icon={TrendingUp}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Compact Session Info Bar */}
|
||||
{session && (
|
||||
<div className="mb-6">
|
||||
<Card className="p-0 border shadow-sm overflow-hidden">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between py-4 px-6 gap-6 bg-card">
|
||||
<div className="flex flex-wrap items-center gap-x-12 gap-y-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">
|
||||
Project
|
||||
</span>
|
||||
<span className="text-base font-bold leading-tight">
|
||||
{session.projectName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Compact Session Info Bar */}
|
||||
{session && (
|
||||
<div className="mb-6">
|
||||
<Card className="p-0 border shadow-sm overflow-hidden">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between py-4 px-6 gap-6 bg-card">
|
||||
<div className="flex flex-wrap items-center gap-x-12 gap-y-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Project</span>
|
||||
<span className="text-base font-bold leading-tight">
|
||||
{session.projectName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col border-l pl-12 border-border/60">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Package</span>
|
||||
<span className="text-sm font-semibold text-muted-foreground leading-tight">
|
||||
{session.packageName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col border-l pl-12 border-border/60">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Chainage</span>
|
||||
<span className="text-sm font-semibold text-muted-foreground leading-tight">
|
||||
{session.chainageName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleBackToSelection}
|
||||
className="font-semibold px-6 shrink-0 h-9"
|
||||
>
|
||||
Change Selection
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upload Card */}
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-xl font-bold">
|
||||
Upload Video
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm">
|
||||
Select video file, detection type, vehicle speed, and method for analysis
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4 space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
|
||||
{/* Video File Input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="video-file" className="text-sm font-semibold">
|
||||
Video File
|
||||
</Label>
|
||||
<Input
|
||||
id="video-file"
|
||||
type="file"
|
||||
accept="video/*"
|
||||
onChange={(e) => {
|
||||
setFile(e.target.files?.[0] || null)
|
||||
setError(null)
|
||||
}}
|
||||
disabled={uploading}
|
||||
className="h-11 bg-muted/20 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:bg-primary/10 file:text-primary file:font-medium file:text-xs hover:file:bg-primary/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* JSON File Input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="json-file" className="text-sm font-semibold">
|
||||
GPS JSON File
|
||||
</Label>
|
||||
<Input
|
||||
id="json-file"
|
||||
type="file"
|
||||
accept=".json,application/json"
|
||||
onChange={(e) => {
|
||||
setJsonFile(e.target.files?.[0] || null)
|
||||
setError(null)
|
||||
}}
|
||||
disabled={uploading}
|
||||
className="h-11 bg-muted/20 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:bg-primary/10 file:text-primary file:font-medium file:text-xs hover:file:bg-primary/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detection, Speed, and Method Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* Detection Type */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="detection-type" className="text-sm font-semibold">
|
||||
Detection Type
|
||||
</Label>
|
||||
<Select
|
||||
value={detectionType}
|
||||
onValueChange={(v) => setDetectionType(v as DetectionType)}
|
||||
disabled={uploading}
|
||||
>
|
||||
<SelectTrigger id="detection-type" className="h-11">
|
||||
<SelectValue placeholder="Select detection type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DETECTION_TYPES.map((type) => (
|
||||
<SelectItem key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Speed Input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="speed" className="text-sm font-semibold">
|
||||
Vehicle Speed (km/h)
|
||||
</Label>
|
||||
<Input
|
||||
id="speed"
|
||||
type="number"
|
||||
min={1}
|
||||
max={200}
|
||||
value={speed}
|
||||
onChange={(e) => setSpeed(Number(e.target.value))}
|
||||
disabled={uploading}
|
||||
className="h-11"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Select Method */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="select-method" className="text-sm font-semibold">
|
||||
Select Method
|
||||
</Label>
|
||||
<Select
|
||||
value={selectMethod}
|
||||
onValueChange={setSelectMethod}
|
||||
disabled={uploading}
|
||||
>
|
||||
<SelectTrigger id="select-method" className="h-11">
|
||||
<SelectValue placeholder="Select method" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DETECTION_METHODS.map((method) => (
|
||||
<SelectItem key={method.value} value={method.value}>
|
||||
{method.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="p-4 rounded-md bg-destructive/10 border border-destructive/20 text-destructive text-sm leading-relaxed whitespace-pre-line">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upload Button */}
|
||||
<Button
|
||||
onClick={handleUpload}
|
||||
disabled={!file || uploading}
|
||||
className="w-full h-14 text-base font-bold uppercase tracking-wide"
|
||||
size="lg"
|
||||
>
|
||||
{uploading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
<span>Processing...</span>
|
||||
</div>
|
||||
) : (
|
||||
"Upload and Process"
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<PoweredBy />
|
||||
<div className="flex flex-col border-l pl-12 border-border/60">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">
|
||||
Package
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-muted-foreground leading-tight">
|
||||
{session.packageName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col border-l pl-12 border-border/60">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">
|
||||
Chainage
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-muted-foreground leading-tight">
|
||||
{session.chainageName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleBackToSelection}
|
||||
className="font-semibold px-6 shrink-0 h-9"
|
||||
>
|
||||
Change Selection
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upload Card */}
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-xl font-bold">Upload Video</CardTitle>
|
||||
<CardDescription className="text-sm">
|
||||
Select video file, detection type, vehicle speed, and method for analysis
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4 space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
|
||||
{/* Video File Input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="video-file" className="text-sm font-semibold">
|
||||
Video File
|
||||
</Label>
|
||||
<Input
|
||||
id="video-file"
|
||||
type="file"
|
||||
accept="video/*"
|
||||
onChange={(e) => {
|
||||
setFile(e.target.files?.[0] || null);
|
||||
setError(null);
|
||||
}}
|
||||
disabled={uploading}
|
||||
className="h-11 bg-muted/20 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:bg-primary/10 file:text-primary file:font-medium file:text-xs hover:file:bg-primary/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* JSON File Input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="json-file" className="text-sm font-semibold">
|
||||
GPS JSON File
|
||||
</Label>
|
||||
<Input
|
||||
id="json-file"
|
||||
type="file"
|
||||
accept=".json,application/json"
|
||||
onChange={(e) => {
|
||||
setJsonFile(e.target.files?.[0] || null);
|
||||
setError(null);
|
||||
}}
|
||||
disabled={uploading}
|
||||
className="h-11 bg-muted/20 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:bg-primary/10 file:text-primary file:font-medium file:text-xs hover:file:bg-primary/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detection, Speed, and Method Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* Detection Type */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="detection-type" className="text-sm font-semibold">
|
||||
Detection Type
|
||||
</Label>
|
||||
<Select
|
||||
value={detectionType}
|
||||
onValueChange={(v) => setDetectionType(v as DetectionType)}
|
||||
disabled={uploading}
|
||||
>
|
||||
<SelectTrigger id="detection-type" className="h-11">
|
||||
<SelectValue placeholder="Select detection type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DETECTION_TYPES.map((type) => (
|
||||
<SelectItem key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Speed Input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="speed" className="text-sm font-semibold">
|
||||
Vehicle Speed (km/h)
|
||||
</Label>
|
||||
<Input
|
||||
id="speed"
|
||||
type="number"
|
||||
min={1}
|
||||
max={200}
|
||||
value={speed}
|
||||
onChange={(e) => setSpeed(Number(e.target.value))}
|
||||
disabled={uploading}
|
||||
className="h-11"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Select Method */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="select-method" className="text-sm font-semibold">
|
||||
Select Method
|
||||
</Label>
|
||||
<Select value={selectMethod} onValueChange={setSelectMethod} disabled={uploading}>
|
||||
<SelectTrigger id="select-method" className="h-11">
|
||||
<SelectValue placeholder="Select method" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DETECTION_METHODS.map((method) => (
|
||||
<SelectItem key={method.value} value={method.value}>
|
||||
{method.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="p-4 rounded-md bg-destructive/10 border border-destructive/20 text-destructive text-sm leading-relaxed whitespace-pre-line">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upload Button */}
|
||||
<Button
|
||||
onClick={handleUpload}
|
||||
disabled={!file || uploading}
|
||||
className="w-full h-14 text-base font-bold uppercase tracking-wide"
|
||||
size="lg"
|
||||
>
|
||||
{uploading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
<span>Processing...</span>
|
||||
</div>
|
||||
) : (
|
||||
'Upload and Process'
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<PoweredBy />
|
||||
</div>
|
||||
)
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user