UI changed, added dashboard
This commit is contained in:
337
app/dashboard/page.tsx
Normal file
337
app/dashboard/page.tsx
Normal file
@@ -0,0 +1,337 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import {
|
||||
Plus,
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
TrendingUp,
|
||||
MapPin as MapPinIcon,
|
||||
BarChart3,
|
||||
AlertTriangle,
|
||||
RectangleHorizontal
|
||||
} from "lucide-react"
|
||||
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
||||
import { GradientStatsCard } from "@/components/dashboard/gradient-stats-card"
|
||||
import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector"
|
||||
import { DetectionDonutChart } from "@/components/dashboard/detection-donut-chart"
|
||||
import { LocationBarChart } from "@/components/dashboard/location-bar-chart"
|
||||
import { DashboardMap } from "@/components/dashboard/dashboard-map"
|
||||
import {
|
||||
fetchProjects,
|
||||
type Project
|
||||
} from "@/lib/api"
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8000/api/v1"
|
||||
|
||||
interface ProjectSummary {
|
||||
project: {
|
||||
id: string
|
||||
name: string
|
||||
corridor_name: string | null
|
||||
state: string | null
|
||||
}
|
||||
packages: {
|
||||
[key: string]: {
|
||||
package_id: string
|
||||
region: string | null
|
||||
locations: {
|
||||
[key: string]: {
|
||||
location_id: string
|
||||
chainage: string | null
|
||||
detection_count: number
|
||||
detections: Array<{
|
||||
id: number
|
||||
type: string
|
||||
class: string
|
||||
confidence: number
|
||||
latitude: number
|
||||
longitude: number
|
||||
}>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface DetectionStats {
|
||||
totalPotholes: number
|
||||
totalSignboards: number
|
||||
totalDetections: number
|
||||
locationData: Array<{
|
||||
name: string
|
||||
potholes: number
|
||||
signboards: number
|
||||
total: number
|
||||
}>
|
||||
}
|
||||
|
||||
function calculateStats(summary: ProjectSummary | null): DetectionStats {
|
||||
if (!summary) {
|
||||
return { totalPotholes: 0, totalSignboards: 0, totalDetections: 0, locationData: [] }
|
||||
}
|
||||
|
||||
let totalPotholes = 0
|
||||
let totalSignboards = 0
|
||||
const locationData: DetectionStats["locationData"] = []
|
||||
|
||||
for (const pkg of Object.values(summary.packages || {})) {
|
||||
for (const [locName, loc] of Object.entries(pkg.locations || {})) {
|
||||
let locPotholes = 0
|
||||
let locSignboards = 0
|
||||
|
||||
for (const detection of loc.detections || []) {
|
||||
if (detection.type.toLowerCase().includes("pothole")) {
|
||||
locPotholes++
|
||||
totalPotholes++
|
||||
} else {
|
||||
locSignboards++
|
||||
totalSignboards++
|
||||
}
|
||||
}
|
||||
|
||||
if (locPotholes > 0 || locSignboards > 0) {
|
||||
const shortName = locName.length > 20 ? locName.substring(0, 20) + "..." : locName
|
||||
locationData.push({
|
||||
name: shortName,
|
||||
potholes: locPotholes,
|
||||
signboards: locSignboards,
|
||||
total: locPotholes + locSignboards
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalPotholes,
|
||||
totalSignboards,
|
||||
totalDetections: totalPotholes + totalSignboards,
|
||||
locationData
|
||||
}
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const router = useRouter()
|
||||
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 [stats, setStats] = useState<DetectionStats>({
|
||||
totalPotholes: 0,
|
||||
totalSignboards: 0,
|
||||
totalDetections: 0,
|
||||
locationData: []
|
||||
})
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Load projects on mount
|
||||
useEffect(() => {
|
||||
const loadProjects = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
const projectsData = await fetchProjects()
|
||||
setProjects(projectsData)
|
||||
|
||||
if (projectsData.length > 0) {
|
||||
setSelectedProjectId(projectsData[0].id)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load projects:", err)
|
||||
setError("Failed to load projects. Please check if the backend is running.")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadProjects()
|
||||
}, [])
|
||||
|
||||
// Load project summary when project changes
|
||||
useEffect(() => {
|
||||
if (!selectedProjectId) {
|
||||
setProjectSummary(null)
|
||||
setStats({ totalPotholes: 0, totalSignboards: 0, totalDetections: 0, locationData: [] })
|
||||
return
|
||||
}
|
||||
|
||||
const loadProjectSummary = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
const response = await fetch(`${API_URL}/summary/projects/${selectedProjectId}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"ngrok-skip-browser-warning": "true"
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API Error: ${response.status}`)
|
||||
}
|
||||
|
||||
const summary: ProjectSummary = await response.json()
|
||||
setProjectSummary(summary)
|
||||
setStats(calculateStats(summary))
|
||||
} catch (err) {
|
||||
console.error("Failed to load project summary:", err)
|
||||
setError("Failed to load project summary.")
|
||||
setProjectSummary(null)
|
||||
setStats({ totalPotholes: 0, totalSignboards: 0, totalDetections: 0, locationData: [] })
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadProjectSummary()
|
||||
}, [selectedProjectId])
|
||||
|
||||
const handleNewAnalysis = () => {
|
||||
router.push("/")
|
||||
}
|
||||
|
||||
const selectedProject = projects.find(p => p.id === selectedProjectId)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950">
|
||||
{/* Sidebar Navigation */}
|
||||
<SidebarNavigation />
|
||||
|
||||
{/* Main Content - offset by sidebar width */}
|
||||
<main className="ml-16 min-h-screen">
|
||||
<div className="p-6 max-w-[1600px] mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-6 animate-in fade-in slide-in-from-top duration-500">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent">
|
||||
VisionRoad Analytics Dashboard
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
A Comprehensive Overview Of Your Road Infrastructure Analysis
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleNewAnalysis}
|
||||
className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white shadow-lg shadow-indigo-500/25 hover:shadow-indigo-500/40 transition-all duration-300"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
New Analysis
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="mb-4 flex items-center gap-2 p-3 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 text-sm animate-in fade-in duration-300">
|
||||
<AlertCircle className="h-4 w-4 flex-shrink-0" />
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats Cards - Top Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6 animate-in fade-in slide-in-from-bottom duration-500">
|
||||
<GradientStatsCard
|
||||
title="Total Detections"
|
||||
subtitle="All detected objects"
|
||||
value={stats.totalDetections}
|
||||
icon={TrendingUp}
|
||||
gradient="green"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<GradientStatsCard
|
||||
title="Potholes"
|
||||
subtitle="Road surface damage"
|
||||
value={stats.totalPotholes}
|
||||
icon={AlertTriangle}
|
||||
gradient="coral"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<GradientStatsCard
|
||||
title="Signboards"
|
||||
subtitle="Traffic signs detected"
|
||||
value={stats.totalSignboards}
|
||||
icon={RectangleHorizontal}
|
||||
gradient="blue"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Project Selector - Above main content */}
|
||||
<div className="mb-6 animate-in fade-in slide-in-from-bottom duration-500 delay-100">
|
||||
<CompactProjectSelector
|
||||
projects={projects}
|
||||
selectedProjectId={selectedProjectId}
|
||||
onProjectChange={setSelectedProjectId}
|
||||
selectedProject={selectedProject}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Content Grid - Map Left, Charts Right */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 animate-in fade-in slide-in-from-bottom duration-500 delay-200">
|
||||
{/* Left Side - Map */}
|
||||
<div>
|
||||
<DashboardMap className="h-auto" />
|
||||
</div>
|
||||
|
||||
{/* Right Side - Stacked Charts */}
|
||||
<div className="space-y-4">
|
||||
{/* Detection Distribution */}
|
||||
<Card className="bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm border-0 shadow-lg shadow-gray-200/50 dark:shadow-gray-900/50 rounded-xl overflow-hidden">
|
||||
<CardHeader className="pb-2 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950/30 dark:to-emerald-950/30">
|
||||
<CardTitle className="text-base font-bold flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-gradient-to-br from-emerald-400 to-teal-500 shadow-md shadow-emerald-500/30">
|
||||
<BarChart3 className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span className="bg-gradient-to-r from-emerald-600 via-teal-500 to-emerald-600 dark:from-emerald-400 dark:via-teal-400 dark:to-emerald-400 bg-clip-text text-transparent">
|
||||
Detection Distribution
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4">
|
||||
<DetectionDonutChart
|
||||
potholes={stats.totalPotholes}
|
||||
signboards={stats.totalSignboards}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Location Bar Chart */}
|
||||
<Card className="bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm border-0 shadow-lg shadow-gray-200/50 dark:shadow-gray-900/50 rounded-xl overflow-hidden">
|
||||
<CardHeader className="pb-2 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/30 dark:to-indigo-950/30">
|
||||
<CardTitle className="text-base font-bold flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-gradient-to-br from-blue-400 to-indigo-500 shadow-md shadow-blue-500/30">
|
||||
<MapPinIcon className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span className="bg-gradient-to-r from-blue-600 via-indigo-500 to-blue-600 dark:from-blue-400 dark:via-indigo-400 dark:to-blue-400 bg-clip-text text-transparent">
|
||||
Detections by Location
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4">
|
||||
<LocationBarChart
|
||||
data={stats.locationData}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-8 text-center animate-in fade-in duration-700 delay-300">
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500">
|
||||
Sentient Geeks Pvt. Ltd.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
318
app/globals.css
318
app/globals.css
@@ -4,79 +4,114 @@
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
/* Primary - Indigo */
|
||||
--primary: oklch(0.55 0.24 264);
|
||||
--primary-foreground: oklch(0.98 0.01 264);
|
||||
|
||||
/* Background & Surfaces */
|
||||
--background: oklch(0.98 0.01 280);
|
||||
--foreground: oklch(0.15 0.02 280);
|
||||
--card: oklch(0.99 0.005 280);
|
||||
--card-foreground: oklch(0.15 0.02 280);
|
||||
--popover: oklch(0.99 0.005 280);
|
||||
--popover-foreground: oklch(0.15 0.02 280);
|
||||
|
||||
/* Secondary */
|
||||
--secondary: oklch(0.95 0.02 264);
|
||||
--secondary-foreground: oklch(0.25 0.05 264);
|
||||
|
||||
/* Muted */
|
||||
--muted: oklch(0.94 0.02 280);
|
||||
--muted-foreground: oklch(0.45 0.03 280);
|
||||
|
||||
/* Accent - Cyan */
|
||||
--accent: oklch(0.92 0.04 200);
|
||||
--accent-foreground: oklch(0.25 0.08 200);
|
||||
|
||||
/* Destructive */
|
||||
--destructive: oklch(0.55 0.22 25);
|
||||
--destructive-foreground: oklch(0.98 0.01 25);
|
||||
|
||||
/* Borders & Inputs */
|
||||
--border: oklch(0.90 0.02 280);
|
||||
--input: oklch(0.92 0.02 280);
|
||||
--ring: oklch(0.55 0.24 264);
|
||||
|
||||
/* Chart Colors - Vibrant Palette */
|
||||
--chart-1: oklch(0.55 0.24 264);
|
||||
--chart-2: oklch(0.65 0.20 200);
|
||||
--chart-3: oklch(0.60 0.18 160);
|
||||
--chart-4: oklch(0.70 0.20 85);
|
||||
--chart-5: oklch(0.60 0.22 320);
|
||||
|
||||
--radius: 0.75rem;
|
||||
|
||||
/* Sidebar */
|
||||
--sidebar: oklch(0.98 0.01 280);
|
||||
--sidebar-foreground: oklch(0.15 0.02 280);
|
||||
--sidebar-primary: oklch(0.55 0.24 264);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.01 264);
|
||||
--sidebar-accent: oklch(0.94 0.03 264);
|
||||
--sidebar-accent-foreground: oklch(0.25 0.05 264);
|
||||
--sidebar-border: oklch(0.90 0.02 280);
|
||||
--sidebar-ring: oklch(0.55 0.24 264);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.145 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.145 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.396 0.141 25.723);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.269 0 0);
|
||||
--input: oklch(0.269 0 0);
|
||||
--ring: oklch(0.439 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(0.269 0 0);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
/* Primary - Light Indigo */
|
||||
--primary: oklch(0.70 0.20 264);
|
||||
--primary-foreground: oklch(0.15 0.02 264);
|
||||
|
||||
/* Background & Surfaces */
|
||||
--background: oklch(0.12 0.02 280);
|
||||
--foreground: oklch(0.95 0.01 280);
|
||||
--card: oklch(0.16 0.02 280);
|
||||
--card-foreground: oklch(0.95 0.01 280);
|
||||
--popover: oklch(0.16 0.02 280);
|
||||
--popover-foreground: oklch(0.95 0.01 280);
|
||||
|
||||
/* Secondary */
|
||||
--secondary: oklch(0.22 0.03 264);
|
||||
--secondary-foreground: oklch(0.90 0.02 264);
|
||||
|
||||
/* Muted */
|
||||
--muted: oklch(0.20 0.02 280);
|
||||
--muted-foreground: oklch(0.65 0.02 280);
|
||||
|
||||
/* Accent */
|
||||
--accent: oklch(0.22 0.04 200);
|
||||
--accent-foreground: oklch(0.85 0.08 200);
|
||||
|
||||
/* Destructive */
|
||||
--destructive: oklch(0.45 0.18 25);
|
||||
--destructive-foreground: oklch(0.90 0.08 25);
|
||||
|
||||
/* Borders & Inputs */
|
||||
--border: oklch(0.25 0.02 280);
|
||||
--input: oklch(0.22 0.02 280);
|
||||
--ring: oklch(0.70 0.20 264);
|
||||
|
||||
/* Chart Colors */
|
||||
--chart-1: oklch(0.65 0.22 264);
|
||||
--chart-2: oklch(0.70 0.18 200);
|
||||
--chart-3: oklch(0.65 0.16 160);
|
||||
--chart-4: oklch(0.75 0.18 85);
|
||||
--chart-5: oklch(0.65 0.20 320);
|
||||
|
||||
/* Sidebar */
|
||||
--sidebar: oklch(0.14 0.02 280);
|
||||
--sidebar-foreground: oklch(0.95 0.01 280);
|
||||
--sidebar-primary: oklch(0.65 0.22 264);
|
||||
--sidebar-primary-foreground: oklch(0.95 0.01 264);
|
||||
--sidebar-accent: oklch(0.22 0.03 264);
|
||||
--sidebar-accent-foreground: oklch(0.90 0.02 264);
|
||||
--sidebar-border: oklch(0.25 0.02 280);
|
||||
--sidebar-ring: oklch(0.70 0.20 264);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: 'Geist', 'Geist Fallback';
|
||||
--font-mono: 'Geist Mono', 'Geist Mono Fallback';
|
||||
--font-sans: 'Geist', 'Geist Fallback', system-ui, sans-serif;
|
||||
--font-mono: 'Geist Mono', 'Geist Mono Fallback', monospace;
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
@@ -122,4 +157,153 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* Premium Background with Animated Mesh Gradient */
|
||||
@layer utilities {
|
||||
.bg-mesh-gradient {
|
||||
background:
|
||||
radial-gradient(ellipse 80% 50% at 20% 20%, oklch(0.75 0.15 264 / 0.15), transparent),
|
||||
radial-gradient(ellipse 60% 40% at 80% 80%, oklch(0.70 0.12 200 / 0.12), transparent),
|
||||
radial-gradient(ellipse 50% 30% at 50% 50%, oklch(0.65 0.10 320 / 0.08), transparent),
|
||||
var(--background);
|
||||
}
|
||||
|
||||
.dark .bg-mesh-gradient {
|
||||
background:
|
||||
radial-gradient(ellipse 80% 50% at 20% 20%, oklch(0.40 0.18 264 / 0.20), transparent),
|
||||
radial-gradient(ellipse 60% 40% at 80% 80%, oklch(0.35 0.15 200 / 0.15), transparent),
|
||||
radial-gradient(ellipse 50% 30% at 50% 50%, oklch(0.30 0.12 320 / 0.10), transparent),
|
||||
var(--background);
|
||||
}
|
||||
|
||||
/* Glassmorphism Card */
|
||||
.glass-card {
|
||||
background: oklch(1 0 0 / 0.7);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid oklch(1 0 0 / 0.2);
|
||||
box-shadow:
|
||||
0 4px 6px -1px oklch(0 0 0 / 0.05),
|
||||
0 10px 15px -3px oklch(0 0 0 / 0.08),
|
||||
0 20px 25px -5px oklch(0 0 0 / 0.05),
|
||||
inset 0 1px 0 oklch(1 0 0 / 0.5);
|
||||
}
|
||||
|
||||
.dark .glass-card {
|
||||
background: oklch(0.18 0.02 280 / 0.7);
|
||||
border: 1px solid oklch(1 0 0 / 0.08);
|
||||
box-shadow:
|
||||
0 4px 6px -1px oklch(0 0 0 / 0.15),
|
||||
0 10px 15px -3px oklch(0 0 0 / 0.20),
|
||||
0 20px 25px -5px oklch(0 0 0 / 0.15),
|
||||
inset 0 1px 0 oklch(1 0 0 / 0.05);
|
||||
}
|
||||
|
||||
/* Gradient Text */
|
||||
.text-gradient {
|
||||
background: linear-gradient(135deg, oklch(0.55 0.24 264), oklch(0.60 0.20 200));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.dark .text-gradient {
|
||||
background: linear-gradient(135deg, oklch(0.75 0.20 264), oklch(0.70 0.18 200));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Gradient Button */
|
||||
.btn-gradient {
|
||||
background: linear-gradient(135deg, oklch(0.55 0.24 264), oklch(0.50 0.22 280));
|
||||
box-shadow:
|
||||
0 4px 14px 0 oklch(0.55 0.24 264 / 0.35),
|
||||
inset 0 1px 0 oklch(1 0 0 / 0.15);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-gradient:hover {
|
||||
box-shadow:
|
||||
0 6px 20px 0 oklch(0.55 0.24 264 / 0.45),
|
||||
inset 0 1px 0 oklch(1 0 0 / 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-gradient:active {
|
||||
transform: translateY(0);
|
||||
box-shadow:
|
||||
0 2px 8px 0 oklch(0.55 0.24 264 / 0.30),
|
||||
inset 0 1px 0 oklch(1 0 0 / 0.15);
|
||||
}
|
||||
|
||||
/* Card Glow Border */
|
||||
.card-glow {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-glow::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
background: linear-gradient(135deg, oklch(0.55 0.24 264 / 0.3), oklch(0.60 0.20 200 / 0.3));
|
||||
border-radius: inherit;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.card-glow:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Progress Bar Gradient */
|
||||
.progress-gradient {
|
||||
background: linear-gradient(90deg, oklch(0.55 0.24 264), oklch(0.60 0.20 200), oklch(0.65 0.18 160));
|
||||
background-size: 200% 100%;
|
||||
animation: progress-shimmer 2s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes progress-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* Subtle Float Animation */
|
||||
.float-subtle {
|
||||
animation: float-subtle 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float-subtle {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
}
|
||||
|
||||
/* Input Focus Glow */
|
||||
.input-glow:focus-within {
|
||||
box-shadow: 0 0 0 3px oklch(0.55 0.24 264 / 0.15);
|
||||
}
|
||||
|
||||
/* Step Indicator */
|
||||
.step-active {
|
||||
background: linear-gradient(135deg, oklch(0.55 0.24 264), oklch(0.50 0.22 280));
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px oklch(0.55 0.24 264 / 0.3);
|
||||
}
|
||||
|
||||
.step-completed {
|
||||
background: oklch(0.65 0.18 160);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-pending {
|
||||
background: oklch(0.90 0.02 280);
|
||||
color: oklch(0.50 0.02 280);
|
||||
}
|
||||
|
||||
.dark .step-pending {
|
||||
background: oklch(0.25 0.02 280);
|
||||
color: oklch(0.60 0.02 280);
|
||||
}
|
||||
}
|
||||
60
app/map/page.tsx
Normal file
60
app/map/page.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
"use client"
|
||||
|
||||
import { NavigationMenu } from "@/components/navigation-menu"
|
||||
import { DashboardMap } from "@/components/dashboard/dashboard-map"
|
||||
|
||||
export default function MapPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-mesh-gradient relative overflow-hidden">
|
||||
{/* Navigation Menu */}
|
||||
<NavigationMenu />
|
||||
|
||||
{/* Decorative background elements */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute -top-40 -left-40 w-80 h-80 bg-primary/10 rounded-full blur-3xl float-subtle" />
|
||||
<div className="absolute -bottom-40 -right-40 w-96 h-96 bg-accent/10 rounded-full blur-3xl float-subtle" style={{ animationDelay: '-3s' }} />
|
||||
<div className="absolute top-1/2 left-1/4 w-64 h-64 bg-primary/5 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 py-8 max-w-7xl relative z-10">
|
||||
{/* Header */}
|
||||
<div className="mb-8 animate-in fade-in slide-in-from-top duration-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-gradient leading-tight">
|
||||
Detection Map
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
View all detected potholes and signboards on the map
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Map */}
|
||||
<div className="animate-in fade-in slide-in-from-bottom duration-700 delay-100">
|
||||
<DashboardMap className="h-[calc(100vh-200px)] min-h-[500px]" />
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mt-6 flex flex-wrap gap-6 animate-in fade-in duration-700 delay-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded-full bg-red-500 border-2 border-red-600" />
|
||||
<span className="text-sm text-muted-foreground">Pothole</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded-full bg-blue-500 border-2 border-blue-600" />
|
||||
<span className="text-sm text-muted-foreground">Signboard</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-8 text-center animate-in fade-in duration-700 delay-300">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Sentient Geeks Pvt. Ltd.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
48
app/page.tsx
48
app/page.tsx
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ProjectSelectionSection } from "@/components/project-selection-section"
|
||||
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
||||
import { type SessionContext, saveSession } from "@/lib/api"
|
||||
|
||||
export default function SelectionPage() {
|
||||
@@ -14,23 +15,42 @@ export default function SelectionPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-background to-muted/20">
|
||||
<div className="container mx-auto px-4 py-8 max-w-7xl">
|
||||
{/* Header */}
|
||||
<div className="mb-8 animate-in fade-in slide-in-from-top duration-700">
|
||||
<h1 className="text-4xl font-bold mb-2 text-balance bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text text-transparent">
|
||||
VisionRoad Detection System
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Select your project location to begin AI-powered road analysis
|
||||
</p>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950">
|
||||
{/* Sidebar Navigation */}
|
||||
<SidebarNavigation />
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="ml-16 min-h-screen relative overflow-hidden">
|
||||
{/* Decorative background elements */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute -top-40 -left-40 w-80 h-80 bg-indigo-500/10 rounded-full blur-3xl" />
|
||||
<div className="absolute -bottom-40 -right-40 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl" />
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-blue-500/5 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
{/* Project Selection Section */}
|
||||
<div className="animate-in fade-in slide-in-from-bottom duration-700 delay-100">
|
||||
<ProjectSelectionSection onSelectionComplete={handleSelectionComplete} />
|
||||
<div className="container mx-auto px-4 py-12 max-w-5xl relative z-10">
|
||||
{/* Premium Header */}
|
||||
<div className="mb-10 animate-in fade-in slide-in-from-top duration-700">
|
||||
<div className="text-center max-w-2xl mx-auto">
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4 bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent leading-tight">
|
||||
VisionRoad Detection System
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project Selection Section */}
|
||||
<div className="animate-in fade-in slide-in-from-bottom duration-700 delay-150">
|
||||
<ProjectSelectionSection onSelectionComplete={handleSelectionComplete} />
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-12 text-center animate-in fade-in duration-700 delay-300">
|
||||
<p className="text-sm text-gray-400 dark:text-gray-500">
|
||||
Sentient Geeks Pvt. Ltd.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Loader2, ArrowLeft, MapPin, Package, FolderKanban, RotateCcw } from "lucide-react"
|
||||
import VideoPlayerSection from "@/components/video-player-section"
|
||||
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
||||
import {
|
||||
type SessionContext,
|
||||
loadSession,
|
||||
@@ -157,83 +158,99 @@ export default function ResultsPage() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center gap-4">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<p className="text-muted-foreground">Loading detection results...</p>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
|
||||
<p className="text-gray-500">Loading detection results...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center gap-4">
|
||||
<p className="text-destructive">{error}</p>
|
||||
<Button onClick={handleNewAnalysis}>Start New Analysis</Button>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
|
||||
<p className="text-red-500">{error}</p>
|
||||
<Button onClick={handleNewAnalysis} className="bg-gradient-to-r from-indigo-500 to-purple-600 text-white">Start New Analysis</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-background to-muted/20">
|
||||
<div className="container mx-auto px-4 py-8 max-w-7xl">
|
||||
{/* Header */}
|
||||
<div className="mb-8 animate-in fade-in slide-in-from-top duration-700">
|
||||
<h1 className="text-4xl font-bold mb-2 text-balance bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text text-transparent">
|
||||
{getTitle()}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
View your AI-powered road analysis results
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950">
|
||||
{/* Sidebar Navigation */}
|
||||
<SidebarNavigation />
|
||||
|
||||
{/* Session Info Bar */}
|
||||
{session && (
|
||||
<div className="mb-6 animate-in fade-in slide-in-from-top duration-500">
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-gradient-to-r from-primary/10 via-primary/5 to-transparent border border-primary/20">
|
||||
<div className="flex items-center gap-6 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<FolderKanban className="h-4 w-4 text-primary" />
|
||||
<span className="text-muted-foreground">Project:</span>
|
||||
<span className="font-medium">{session.projectName}</span>
|
||||
{/* Main Content */}
|
||||
<main className="ml-16 min-h-screen">
|
||||
<div className="container mx-auto px-4 py-8 max-w-7xl">
|
||||
{/* Header */}
|
||||
<div className="mb-8 animate-in fade-in slide-in-from-top duration-700">
|
||||
<h1 className="text-4xl font-bold mb-2 bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent">
|
||||
{getTitle()}
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
View your AI-powered road analysis results
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Session Info Bar */}
|
||||
{session && (
|
||||
<div className="mb-6 animate-in fade-in slide-in-from-top duration-500">
|
||||
<div className="flex items-center justify-between p-4 rounded-xl bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50">
|
||||
<div className="flex items-center gap-6 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-gradient-to-br from-indigo-400 to-purple-500 shadow-md shadow-indigo-500/30">
|
||||
<FolderKanban className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span className="text-gray-500 dark:text-gray-400 text-xs uppercase font-semibold tracking-wide">Project:</span>
|
||||
<span className="font-bold text-gray-900 dark:text-white bg-gradient-to-r from-indigo-600 to-purple-600 dark:from-indigo-400 dark:to-purple-400 bg-clip-text text-transparent">{session.projectName}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-gradient-to-br from-emerald-400 to-teal-500 shadow-md shadow-emerald-500/30">
|
||||
<Package className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span className="text-gray-500 dark:text-gray-400 text-xs uppercase font-semibold tracking-wide">Package:</span>
|
||||
<span className="font-bold text-gray-900 dark:text-white bg-gradient-to-r from-emerald-600 to-teal-600 dark:from-emerald-400 dark:to-teal-400 bg-clip-text text-transparent">{session.packageName}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-gradient-to-br from-purple-400 to-pink-500 shadow-md shadow-purple-500/30">
|
||||
<MapPin className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span className="text-gray-500 dark:text-gray-400 text-xs uppercase font-semibold tracking-wide">Location:</span>
|
||||
<span className="font-bold text-gray-900 dark:text-white bg-gradient-to-r from-purple-600 to-pink-600 dark:from-purple-400 dark:to-pink-400 bg-clip-text text-transparent">{session.locationName}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="h-4 w-4 text-primary" />
|
||||
<span className="text-muted-foreground">Package:</span>
|
||||
<span className="font-medium">{session.packageName}</span>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={handleBackToUpload} className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white">
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Upload
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleNewAnalysis} className="border-indigo-200 dark:border-indigo-800 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-900/50">
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
New Analysis
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="h-4 w-4 text-primary" />
|
||||
<span className="text-muted-foreground">Location:</span>
|
||||
<span className="font-medium">{session.locationName}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={handleBackToUpload}>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Upload
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleNewAnalysis}>
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
New Analysis
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Video Player Section */}
|
||||
{detectionData && videoId && (
|
||||
<div className="animate-in fade-in slide-in-from-bottom duration-700">
|
||||
<VideoPlayerSection
|
||||
data={detectionData}
|
||||
videoId={videoId}
|
||||
videoFile={videoFile}
|
||||
detectionType={detectionType}
|
||||
projectId={session?.projectId || undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Video Player Section */}
|
||||
{detectionData && videoId && (
|
||||
<div className="animate-in fade-in slide-in-from-bottom duration-700">
|
||||
<VideoPlayerSection
|
||||
data={detectionData}
|
||||
videoId={videoId}
|
||||
videoFile={videoFile}
|
||||
detectionType={detectionType}
|
||||
projectId={session?.projectId || undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
||||
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, ArrowLeft, MapPin, Package, FolderKanban } from "lucide-react"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
||||
import {
|
||||
type SessionContext,
|
||||
loadSession,
|
||||
@@ -71,7 +71,6 @@ export default function UploadPage() {
|
||||
if (data.type === "complete" || data.status === "completed") {
|
||||
setStatusMessage("Processing completed! Saving video...")
|
||||
ws.close()
|
||||
// Store video file in IndexedDB for results page
|
||||
if (file) {
|
||||
try {
|
||||
await storeVideoFile(videoId, file)
|
||||
@@ -79,7 +78,6 @@ export default function UploadPage() {
|
||||
console.error("Failed to store video file:", err)
|
||||
}
|
||||
}
|
||||
// Save video data and navigate to results
|
||||
saveVideoData({ videoId, detectionType })
|
||||
setStatusMessage("Redirecting to results...")
|
||||
setTimeout(() => router.push("/results"), 500)
|
||||
@@ -154,184 +152,244 @@ export default function UploadPage() {
|
||||
|
||||
const getTitle = () => {
|
||||
return detectionType === "pothole-detection"
|
||||
? "Pothole Detection System"
|
||||
: "Signboard Detection System"
|
||||
? "Pothole Detection"
|
||||
: "Signboard Detection"
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 flex items-center justify-center">
|
||||
<div className="p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-background to-muted/20">
|
||||
<div className="container mx-auto px-4 py-8 max-w-7xl">
|
||||
{/* Header */}
|
||||
<div className="mb-8 animate-in fade-in slide-in-from-top duration-700">
|
||||
<h1 className="text-4xl font-bold mb-2 text-balance bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text text-transparent">
|
||||
{getTitle()}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Upload a video to detect and analyze with AI-powered processing
|
||||
</p>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950">
|
||||
{/* Sidebar Navigation */}
|
||||
<SidebarNavigation />
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="ml-16 min-h-screen relative overflow-hidden flex flex-col">
|
||||
{/* Decorative background elements */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute -top-40 -left-40 w-80 h-80 bg-indigo-500/10 rounded-full blur-3xl" />
|
||||
<div className="absolute -bottom-40 -right-40 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
{/* Session Info Bar */}
|
||||
{session && (
|
||||
<div className="mb-6 animate-in fade-in slide-in-from-top duration-500">
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-gradient-to-r from-primary/10 via-primary/5 to-transparent border border-primary/20">
|
||||
<div className="flex items-center gap-6 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<FolderKanban className="h-4 w-4 text-primary" />
|
||||
<span className="text-muted-foreground">Project:</span>
|
||||
<span className="font-medium">{session.projectName}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="h-4 w-4 text-primary" />
|
||||
<span className="text-muted-foreground">Package:</span>
|
||||
<span className="font-medium">{session.packageName}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="h-4 w-4 text-primary" />
|
||||
<span className="text-muted-foreground">Location:</span>
|
||||
<span className="font-medium">{session.locationName}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={handleBackToSelection}>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Change Selection
|
||||
</Button>
|
||||
<div className="flex-1 container mx-auto px-4 py-6 max-w-6xl relative z-10 flex flex-col">
|
||||
{/* Compact Header */}
|
||||
<div className="mb-4 animate-in fade-in slide-in-from-top duration-700">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl md:text-4xl font-bold bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent leading-tight">
|
||||
{getTitle()}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upload Card */}
|
||||
<Card className="transition-all hover:shadow-lg animate-in fade-in slide-in-from-bottom duration-700">
|
||||
<CardHeader>
|
||||
<CardTitle>Upload Video</CardTitle>
|
||||
<CardDescription>
|
||||
Select a video file, detection type, and vehicle speed to start AI-powered analysis
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Video File Input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="video-file">Video File</Label>
|
||||
<Input
|
||||
id="video-file"
|
||||
type="file"
|
||||
accept="video/*"
|
||||
onChange={(e) => {
|
||||
setFile(e.target.files?.[0] || null)
|
||||
setError(null)
|
||||
}}
|
||||
disabled={uploading}
|
||||
/>
|
||||
{file && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Selected: {file.name} ({(file.size / 1024 / 1024).toFixed(2)} MB)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* JSON File Input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="json-file">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}
|
||||
/>
|
||||
{jsonFile && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Selected: {jsonFile.name} ({(jsonFile.size / 1024).toFixed(2)} KB)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Detection Type */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="detection-type">Detection Type</Label>
|
||||
<Select
|
||||
value={detectionType}
|
||||
onValueChange={(v) => setDetectionType(v as DetectionType)}
|
||||
disabled={uploading}
|
||||
>
|
||||
<SelectTrigger id="detection-type">
|
||||
<SelectValue placeholder="Select detection type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="pothole-detection">Pothole Detection</SelectItem>
|
||||
<SelectItem value="sign-board-detection">Signboard Detection</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Speed Input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="speed">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>
|
||||
|
||||
{/* Error Display */}
|
||||
{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 flex-shrink-0" />
|
||||
<p className="text-sm whitespace-pre-line">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upload Button */}
|
||||
<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>
|
||||
|
||||
{/* Progress Section */}
|
||||
{uploading && (
|
||||
<div className="space-y-3 animate-in fade-in slide-in-from-top duration-500">
|
||||
<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>
|
||||
{/* Compact Session Info Bar */}
|
||||
{session && (
|
||||
<div className="mb-4 animate-in fade-in slide-in-from-top duration-500 delay-100">
|
||||
<div className="rounded-xl px-4 py-3 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-6 text-sm">
|
||||
<div>
|
||||
<p className="text-[10px] font-medium text-gray-500 uppercase tracking-wide">Project</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white text-sm">{session.projectName}</p>
|
||||
</div>
|
||||
<div className="w-px h-8 bg-gray-300 dark:bg-gray-600" />
|
||||
<div>
|
||||
<p className="text-[10px] font-medium text-gray-500 uppercase tracking-wide">Package</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white text-sm">{session.packageName}</p>
|
||||
</div>
|
||||
<div className="w-px h-8 bg-gray-300 dark:bg-gray-600" />
|
||||
<div>
|
||||
<p className="text-[10px] font-medium text-gray-500 uppercase tracking-wide">Location</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white text-sm">{session.locationName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBackToSelection}
|
||||
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 text-xs"
|
||||
>
|
||||
Change Selection
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upload Card */}
|
||||
<Card className="bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm border-0 shadow-lg shadow-gray-200/50 dark:shadow-gray-900/50 rounded-xl overflow-hidden animate-in fade-in slide-in-from-bottom duration-700 delay-150 flex-1">
|
||||
<CardHeader className="pb-4 border-b border-gray-100 dark:border-gray-700 bg-gradient-to-r from-indigo-50 to-purple-50 dark:from-indigo-950/30 dark:to-purple-950/30">
|
||||
<CardTitle className="text-xl font-bold">
|
||||
<span className="bg-gradient-to-r from-indigo-600 via-purple-500 to-indigo-600 dark:from-indigo-400 dark:via-purple-400 dark:to-indigo-400 bg-clip-text text-transparent">
|
||||
Upload Video
|
||||
</span>
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm">
|
||||
Select video file, detection type, and vehicle speed for analysis
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4 space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-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-10 bg-gray-50 dark:bg-gray-800 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:bg-indigo-100 dark:file:bg-indigo-900/50 file:text-indigo-600 dark:file:text-indigo-400 file:font-medium file:text-xs hover:file:bg-indigo-200"
|
||||
/>
|
||||
{file && (
|
||||
<div className="p-2 rounded-lg bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-200 dark:border-indigo-800">
|
||||
<p className="text-xs font-medium text-gray-900 dark:text-white">{file.name}</p>
|
||||
<p className="text-[10px] text-gray-500">
|
||||
{(file.size / 1024 / 1024).toFixed(2)} MB
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* JSON File Input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="json-file" className="text-sm font-semibold">
|
||||
GPS JSON File <span className="text-gray-400 font-normal text-xs">(Optional)</span>
|
||||
</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-10 bg-gray-50 dark:bg-gray-800 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:bg-indigo-100 dark:file:bg-indigo-900/50 file:text-indigo-600 dark:file:text-indigo-400 file:font-medium file:text-xs hover:file:bg-indigo-200"
|
||||
/>
|
||||
{jsonFile && (
|
||||
<div className="p-2 rounded-lg bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-200 dark:border-indigo-800">
|
||||
<p className="text-xs font-medium text-gray-900 dark:text-white">{jsonFile.name}</p>
|
||||
<p className="text-[10px] text-gray-500">
|
||||
{(jsonFile.size / 1024).toFixed(2)} KB
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 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-10 bg-gray-50 dark:bg-gray-800">
|
||||
<SelectValue placeholder="Select detection type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="pothole-detection">
|
||||
<span className="font-medium">Pothole Detection</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="sign-board-detection">
|
||||
<span className="font-medium">Signboard Detection</span>
|
||||
</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-10 bg-gray-50 dark:bg-gray-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="flex items-start gap-2 p-3 rounded-lg bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 animate-in fade-in slide-in-from-top duration-300">
|
||||
<div className="w-4 h-4 rounded-full bg-red-100 dark:bg-red-900/50 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="text-[10px] font-bold text-red-500">!</span>
|
||||
</div>
|
||||
<p className="text-xs text-red-600 dark:text-red-400 leading-relaxed whitespace-pre-line">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upload Button */}
|
||||
<Button
|
||||
onClick={handleUpload}
|
||||
disabled={!file || uploading}
|
||||
className={`w-full h-12 text-sm font-semibold transition-all rounded-xl ${file && !uploading ? 'bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white shadow-lg shadow-indigo-500/25' : ''
|
||||
}`}
|
||||
size="lg"
|
||||
>
|
||||
{uploading ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Processing...
|
||||
</span>
|
||||
) : (
|
||||
"Upload and Process"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Progress Section */}
|
||||
{uploading && (
|
||||
<div className="space-y-3 p-4 rounded-xl bg-indigo-50 dark:bg-indigo-950/30 border border-indigo-200 dark:border-indigo-800 animate-in fade-in slide-in-from-top duration-500">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300 text-xs">Processing Progress</span>
|
||||
<span className="font-bold text-indigo-600 dark:text-indigo-400 text-xs">{progress}%</span>
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-indigo-100 dark:bg-indigo-900/50 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-indigo-500 to-purple-600 rounded-full transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{statusMessage && (
|
||||
<p className="text-xs text-gray-500">{statusMessage}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-4 text-center animate-in fade-in duration-700 delay-300">
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500">
|
||||
Sentient Geeks Pvt. Ltd.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user