diff --git a/app/page.tsx b/app/page.tsx index 65148c7..dc47c9c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,6 +3,10 @@ import { useState } from "react" import { UploadSection } from "@/components/upload-section" import VideoPlayerSection from "@/components/video-player-section" +import { ProjectSelectionSection } from "@/components/project-selection-section" +import { type SessionContext, emptySessionContext } from "@/lib/api" +import { Button } from "@/components/ui/button" +import { ArrowLeft, MapPin, Package, FolderKanban } from "lucide-react" export type DetectionType = "pothole-detection" | "sign-board-detection" @@ -67,14 +71,17 @@ export type DetectionData = { } export default function DetectionPage() { + const [session, setSession] = useState(emptySessionContext) const [detectionData, setDetectionData] = useState(null) const [videoId, setVideoId] = useState(null) const [videoFile, setVideoFile] = useState(null) const [detectionType, setDetectionType] = useState("pothole-detection") + const isSessionComplete = session.projectId && session.packageId && session.locationId + const getTitle = () => { - return detectionType === "pothole-detection" - ? "Pothole Detection System" + return detectionType === "pothole-detection" + ? "Pothole Detection System" : "Signboard Detection System" } @@ -84,34 +91,85 @@ export default function DetectionPage() { : "Upload a video to detect and identify signboards with AI-powered analysis" } + const handleSelectionComplete = (newSession: SessionContext) => { + setSession(newSession) + } + + const handleBackToSelection = () => { + setSession(emptySessionContext) + setDetectionData(null) + setVideoId(null) + setVideoFile(null) + } + return (
{/* Header */}

- {getTitle()} + {isSessionComplete ? getTitle() : "VisionRoad Detection System"}

-

{getDescription()}

+

+ {isSessionComplete ? getDescription() : "Select your project location to begin AI-powered road analysis"} +

- {/* Upload Section */} -
- { - setDetectionData(data) - setVideoId(vId) - setVideoFile(file) - }} - onDetectionTypeChange={setDetectionType} - /> -
+ {/* Session Info Bar */} + {isSessionComplete && ( +
+
+
+
+ + Project: + {session.projectName} +
+
+ + Package: + {session.packageName} +
+
+ + Location: + {session.locationName} +
+
+ +
+
+ )} + + {/* Project Selection Section - Show when session is not complete */} + {!isSessionComplete && ( +
+ +
+ )} + + {/* Upload Section - Show after session is complete */} + {isSessionComplete && ( +
+ { + setDetectionData(data) + setVideoId(vId) + setVideoFile(file) + }} + onDetectionTypeChange={setDetectionType} + /> +
+ )} {/* Video Player Section */} {detectionData && videoId && videoFile && (
-
) -} \ No newline at end of file +} diff --git a/components/project-selection-section.tsx b/components/project-selection-section.tsx new file mode 100644 index 0000000..919b739 --- /dev/null +++ b/components/project-selection-section.tsx @@ -0,0 +1,312 @@ +"use client" + +import { useState, useEffect } from "react" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Loader2, MapPin, Package, FolderKanban, ArrowRight, AlertCircle } from "lucide-react" +import { + fetchProjects, + fetchPackagesByProject, + fetchLocationsByPackage, + type Project, + type Package as PackageType, + type Location, + type SessionContext +} from "@/lib/api" + +type ProjectSelectionSectionProps = { + onSelectionComplete: (session: SessionContext) => void +} + +export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectionSectionProps) { + // Data states + const [projects, setProjects] = useState([]) + const [packages, setPackages] = useState([]) + const [locations, setLocations] = useState([]) + + // Selection states + const [selectedProject, setSelectedProject] = useState(null) + const [selectedPackage, setSelectedPackage] = useState(null) + const [selectedLocation, setSelectedLocation] = useState(null) + + // Loading states + const [loadingProjects, setLoadingProjects] = useState(true) + const [loadingPackages, setLoadingPackages] = useState(false) + const [loadingLocations, setLoadingLocations] = useState(false) + + // Error state + const [error, setError] = useState(null) + + // Load projects on mount + useEffect(() => { + const loadProjects = async () => { + try { + setLoadingProjects(true) + setError(null) + const data = await fetchProjects() + setProjects(data) + } catch (err) { + console.error("Failed to load projects:", err) + setError("Failed to load projects. Please check if the backend is running.") + } finally { + setLoadingProjects(false) + } + } + loadProjects() + }, []) + + // Load packages when project changes + useEffect(() => { + if (!selectedProject) { + setPackages([]) + setSelectedPackage(null) + return + } + + const loadPackages = async () => { + try { + setLoadingPackages(true) + setError(null) + setSelectedPackage(null) + setSelectedLocation(null) + setLocations([]) + const data = await fetchPackagesByProject(selectedProject.id) + setPackages(data) + } catch (err) { + console.error("Failed to load packages:", err) + setError("Failed to load packages for the selected project.") + } finally { + setLoadingPackages(false) + } + } + loadPackages() + }, [selectedProject]) + + // Load locations when package changes + useEffect(() => { + if (!selectedPackage) { + setLocations([]) + setSelectedLocation(null) + return + } + + const loadLocations = async () => { + try { + setLoadingLocations(true) + setError(null) + setSelectedLocation(null) + const data = await fetchLocationsByPackage(selectedPackage.id) + setLocations(data) + } catch (err) { + console.error("Failed to load locations:", err) + setError("Failed to load locations for the selected package.") + } finally { + setLoadingLocations(false) + } + } + loadLocations() + }, [selectedPackage]) + + const handleProjectChange = (projectId: string) => { + const project = projects.find(p => p.id === projectId) || null + setSelectedProject(project) + } + + const handlePackageChange = (packageId: string) => { + const pkg = packages.find(p => p.id === packageId) || null + setSelectedPackage(pkg) + } + + const handleLocationChange = (locationId: string) => { + const location = locations.find(l => l.id === locationId) || null + setSelectedLocation(location) + } + + const handleProceed = () => { + if (selectedProject && selectedPackage && selectedLocation) { + onSelectionComplete({ + projectId: selectedProject.id, + projectName: selectedProject.name, + packageId: selectedPackage.id, + packageName: selectedPackage.name, + locationId: selectedLocation.id, + locationName: selectedLocation.segment_name + }) + } + } + + const isComplete = selectedProject && selectedPackage && selectedLocation + + return ( + + +
+
+ +
+
+ Select Project Location + + Choose your project, package, and location to begin video analysis + +
+
+
+ + {/* Error Display */} + {error && ( +
+ +

{error}

+
+ )} + +
+ {/* Project Dropdown */} +
+ + + {selectedProject?.state && ( +

+ State: {selectedProject.state} +

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

Selected:

+

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

+
+ )} + + {/* Proceed Button */} + +
+
+ ) +} diff --git a/lib/api.ts b/lib/api.ts new file mode 100644 index 0000000..bf8e575 --- /dev/null +++ b/lib/api.ts @@ -0,0 +1,103 @@ +/** + * API Service Layer for VisionRoad Frontend + * Provides typed functions for interacting with the backend API + */ + +const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8000/api/v1" + +// Type definitions +export interface Project { + id: string + name: string + state: string | null + corridor_name: string | null + start_lat: number | null + start_lng: number | null + end_lat: number | null + end_lng: number | null + created_at: string + updated_at: string +} + +export interface Package { + id: string + project_id: string + name: string + region: string | null + created_at: string + updated_at: string +} + +export interface Location { + id: string + package_id: string + segment_name: string + chainage_start_km: number | null + chainage_end_km: number | null + start_lat: number + start_lng: number + end_lat: number + end_lng: number + created_at: string + updated_at: string +} + +// Helper function for API requests +async function apiRequest(endpoint: string): Promise { + const response = await fetch(`${API_URL}${endpoint}`, { + headers: { + "Content-Type": "application/json", + "ngrok-skip-browser-warning": "true" + } + }) + + if (!response.ok) { + throw new Error(`API Error: ${response.status} ${response.statusText}`) + } + + return response.json() +} + +// API Functions + +/** + * Fetch all projects + */ +export async function fetchProjects(): Promise { + return apiRequest("/projects/") +} + +/** + * Fetch packages filtered by project ID + */ +export async function fetchPackagesByProject(projectId: string): Promise { + return apiRequest(`/packages/?project_id=${projectId}`) +} + +/** + * Fetch locations filtered by package ID + */ +export async function fetchLocationsByPackage(packageId: string): Promise { + return apiRequest(`/locations/?package_id=${packageId}`) +} + +/** + * Session context type for storing user selections + */ +export interface SessionContext { + projectId: string | null + projectName: string | null + packageId: string | null + packageName: string | null + locationId: string | null + locationName: string | null +} + +export const emptySessionContext: SessionContext = { + projectId: null, + projectName: null, + packageId: null, + packageName: null, + locationId: null, + locationName: null +} diff --git a/package-lock.json b/package-lock.json index a69bee4..a2e396f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2480,6 +2480,7 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2490,6 +2491,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2591,6 +2593,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2853,7 +2856,8 @@ "version": "8.5.1", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.1.tgz", "integrity": "sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.5.1", @@ -3300,6 +3304,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz", "integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "16.0.10", "@swc/helpers": "0.5.15", @@ -3425,6 +3430,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3462,6 +3468,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3492,6 +3499,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3504,6 +3512,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz", "integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -3789,7 +3798,8 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tailwindcss-animate": { "version": "1.0.7",