diff --git a/app/results/[videoId]/page.tsx b/app/results/[videoId]/page.tsx new file mode 100644 index 0000000..9bd05c1 --- /dev/null +++ b/app/results/[videoId]/page.tsx @@ -0,0 +1,179 @@ +"use client" + +import { useState, useEffect } from "react" +import { useRouter, useParams } from "next/navigation" +import { Button } from "@/components/ui/button" +import { Loader2, TrendingUp } from "lucide-react" +import VideoPlayerSection from "@/components/video-player-section" +import { SidebarNavigation } from "@/components/sidebar-navigation" +import { + type SessionContext, + loadSession, + clearSession +} from "@/lib/api" +import { getVideoFile, clearVideoFile } from "@/lib/video-storage" +import { DetectionData, DetectionType } from "@/lib/types" + +const API_URL = process.env.NEXT_PUBLIC_API_URL + +export default function VideoResultsPage() { + const router = useRouter() + const { videoId } = useParams() as { videoId: string } + const [session, setSession] = useState(null) + const [detectionData, setDetectionData] = useState(null) + const [detectionType, setDetectionType] = useState("pothole-detection") + const [videoFile, setVideoFile] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + const storedSession = loadSession() + setSession(storedSession) + + const fetchResults = async () => { + try { + // Fetch detection data from backend + const response = await fetch(`${API_URL}/results/${videoId}`, { + headers: { "ngrok-skip-browser-warning": "true" } + }) + + if (!response.ok) { + if (response.status === 404) { + throw new Error("Results not found for this video.") + } + throw new Error(`Failed to load results: ${response.status}`) + } + + const data = await response.json() + setDetectionData(data as any) + + // Try to infer detection type from results if possible + if (data.summary?.unique_signboards !== undefined && data.summary?.unique_signboards > 0) { + setDetectionType("sign-board-detection") + } else if (data.summary?.unique_potholes !== undefined && data.summary?.unique_potholes > 0) { + setDetectionType("pothole-detection") + } + + // Retrieve video file from IndexedDB + const storedVideoFile = await getVideoFile(videoId) + if (storedVideoFile) { + setVideoFile(storedVideoFile) + } + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to load results") + } finally { + setIsLoading(false) + } + } + + if (videoId) { + fetchResults() + } + }, [videoId]) + + const handleNewAnalysis = async () => { + if (videoId) { + try { + await clearVideoFile(videoId) + } catch (err) { + console.error("Failed to clear video file:", err) + } + } + clearSession() + router.push("/new-analysis") + } + + const getTitle = () => { + if (detectionType === "pothole-detection") return "Pothole Detection Results" + if (detectionType === "sign-board-detection") return "Signboard Detection Results" + return "Pothole & Signboard Detection Results" + } + + if (isLoading) { + return ( +
+
+ +

Loading detection results...

+
+
+ ) + } + + if (error) { + return ( +
+
+

{error}

+
+ + +
+
+
+ ) + } + + return ( +
+ +
+
+
+
+ +
+
+

+ {getTitle()} +

+

+ Video ID: {videoId} +

+
+
+ + {session && ( +
+
+
+
+ Project + + {session.projectName} + +
+
+ Package + + {session.packageName} + +
+
+ Location + + {session.locationName} + +
+
+ +
+
+ )} + + {detectionData && ( + + )} +
+
+
+ ) +} diff --git a/app/results/page.tsx b/app/results/page.tsx index 0af3e5b..ddf82b2 100644 --- a/app/results/page.tsx +++ b/app/results/page.tsx @@ -39,38 +39,13 @@ export default function ResultsPage() { return } - setSession(storedSession) - setVideoId(videoData.videoId) - setDetectionType(videoData.detectionType as DetectionType) - - // Fetch detection results and video file - const fetchResults = async () => { - try { - // Fetch detection data - const response = await fetch(`${API_URL}/results/${videoData.videoId}`, { - headers: { "ngrok-skip-browser-warning": "true" } - }) - - if (!response.ok) { - throw new Error(`Failed to load results: ${response.status}`) - } - - const data = await response.json() - setDetectionData(data as any) - - // Retrieve video file from IndexedDB - const storedVideoFile = await getVideoFile(videoData.videoId) - if (storedVideoFile) { - setVideoFile(storedVideoFile) - } - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to load results") - } finally { - setIsLoading(false) - } + // If we have a videoId, redirect to the dynamic results page + if (videoData.videoId) { + router.replace(`/results/${videoData.videoId}`) + return } - fetchResults() + setSession(storedSession) }, [router]) const handleNewAnalysis = async () => { diff --git a/app/upload/[videoId]/page.tsx b/app/upload/[videoId]/page.tsx new file mode 100644 index 0000000..785f03c --- /dev/null +++ b/app/upload/[videoId]/page.tsx @@ -0,0 +1,238 @@ +"use client" + +import { useState, useEffect, useCallback } from "react" +import { useRouter, useParams } from "next/navigation" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Loader2, TrendingUp } from "lucide-react" +import { SidebarNavigation } from "@/components/sidebar-navigation" +import { + type SessionContext, + loadSession, +} from "@/lib/api" +import { getVideoFile } from "@/lib/video-storage" +import { storeVideoFile } from "@/lib/video-storage" + +const API_URL = process.env.NEXT_PUBLIC_API_URL +const WS_URL = API_URL?.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://") + +export default function VideoProcessingPage() { + const router = useRouter() + const { videoId } = useParams() as { videoId: string } + const [session, setSession] = useState(null) + const [isLoading, setIsLoading] = useState(true) + + // Processing states + const [progress, setProgress] = useState(0) + const [statusMessage, setStatusMessage] = useState("Initializing...") + const [error, setError] = useState(null) + const [detectionType, setDetectionType] = useState("pothole-detection") + + const connectWebSocket = useCallback((vid: string) => { + const ws = new WebSocket(`${WS_URL}/ws/${vid}`) + + ws.onmessage = async (event) => { + const data = JSON.parse(event.data) + + if (data.type === "progress" || data.progress !== undefined) { + setProgress(data.progress || 0) + let message = data.message || "Processing..." + 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! Finalizing...") + ws.close() + + // Navigate to results + setTimeout(() => router.push(`/results/${vid}`), 1000) + } + + if (data.type === "error") { + setError("Error: " + data.message) + setStatusMessage("") + ws.close() + } + } + + ws.onerror = () => { + setStatusMessage("Connection lost. Reconnecting...") + setTimeout(() => connectWebSocket(vid), 3000) + } + + return ws + }, [router]) + + useEffect(() => { + const storedSession = loadSession() + setSession(storedSession) + + const checkStatus = async () => { + try { + const response = await fetch(`${API_URL}/status/${videoId}`, { + headers: { "ngrok-skip-browser-warning": "true" } + }) + + if (response.status === 404) { + router.replace("/upload") + return + } + + if (!response.ok) { + throw new Error("Failed to fetch status") + } + + const statusData = await response.json() + + if (statusData.status === "completed") { + router.replace(`/results/${videoId}`) + return + } + + if (statusData.status === "error") { + setError(statusData.message || "An error occurred during processing.") + setIsLoading(false) + return + } + + // If processing, start WebSocket + setProgress(statusData.progress || 0) + setStatusMessage(statusData.message || "Resuming processing...") + connectWebSocket(videoId) + setIsLoading(false) + + } catch (err) { + console.error("Status check failed:", err) + setError("Failed to connect to server.") + setIsLoading(false) + } + } + + if (videoId) { + checkStatus() + } + }, [videoId, router, connectWebSocket]) + + if (isLoading) { + return ( +
+
+ +
+
+ ) + } + + return ( +
+ +
+
+
+
+ +
+
+

+ Processing Analysis +

+
+
+ + {session && ( +
+
+
+
+ Project + + {session.projectName} + +
+
+ Package + + {session.packageName} + +
+
+ Location + + {session.locationName} + +
+
+
+
+ )} + + +
+ {/* Visual Spinner Area */} +
+
+ +
+
+ +
+

+ Processing Video +

+

+ ID: {videoId} +

+
+ +
+
+ Processing Progress + {progress}% +
+
+
+
+
+

+ {statusMessage} +

+
+
+ + {error && ( +
+

{error}

+ +
+ )} + +
+ {/*

+ You can safely refresh this page — progress will resume automatically. +

*/} +
+
+ + +
+

+ Sentient Geeks Pvt. Ltd. +

+
+
+
+
+ ) +} diff --git a/app/upload/page.tsx b/app/upload/page.tsx index 038d793..327ab90 100644 --- a/app/upload/page.tsx +++ b/app/upload/page.tsx @@ -54,50 +54,7 @@ export default function UploadPage() { setIsLoading(false) }, [router]) - const connectWebSocket = (videoId: string) => { - const ws = new WebSocket(`${WS_URL}/ws/${videoId}`) - ws.onmessage = async (event) => { - const data = JSON.parse(event.data) - - if (data.type === "progress" || data.progress !== undefined) { - setProgress(data.progress || 0) - let message = data.message || "Processing..." - 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! Saving video...") - ws.close() - if (file) { - try { - await storeVideoFile(videoId, file) - } catch (err) { - console.error("Failed to store video file:", err) - } - } - saveVideoData({ videoId, detectionType }) - setStatusMessage("Redirecting to results...") - setTimeout(() => router.push("/results"), 500) - } - - if (data.type === "error") { - setError("Error: " + data.message) - setStatusMessage("") - setUploading(false) - ws.close() - } - } - - ws.onerror = () => { - setStatusMessage("Connection error. Retrying...") - } - } const handleUpload = async () => { if (!file) { @@ -132,9 +89,21 @@ export default function UploadPage() { } const result = await response.json() - setStatusMessage("Uploaded! Starting processing...") - setProgress(10) - connectWebSocket(result.video_id) + + // 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) + } + } + + saveVideoData({ videoId: result.video_id, detectionType }) + + // 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") { @@ -384,26 +353,7 @@ export default function UploadPage() { )} - {/* Progress Section */} - {uploading && ( -
-
-
- Processing Progress - {progress}% -
-
-
-
-
- {statusMessage && ( -

{statusMessage}

- )} -
- )} + diff --git a/lib/api.ts b/lib/api.ts index 01f4acc..bedd80e 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -418,7 +418,7 @@ const DETECTION_TYPE_KEY = "visionroad_detection_type" */ export function saveSession(session: SessionContext): void { if (typeof window !== "undefined") { - sessionStorage.setItem(SESSION_KEY, JSON.stringify(session)) + localStorage.setItem(SESSION_KEY, JSON.stringify(session)) } } @@ -427,7 +427,7 @@ export function saveSession(session: SessionContext): void { */ export function loadSession(): SessionContext { if (typeof window !== "undefined") { - const stored = sessionStorage.getItem(SESSION_KEY) + const stored = localStorage.getItem(SESSION_KEY) if (stored) { return JSON.parse(stored) } @@ -440,9 +440,9 @@ export function loadSession(): SessionContext { */ export function clearSession(): void { if (typeof window !== "undefined") { - sessionStorage.removeItem(SESSION_KEY) - sessionStorage.removeItem(VIDEO_DATA_KEY) - sessionStorage.removeItem(DETECTION_TYPE_KEY) + localStorage.removeItem(SESSION_KEY) + localStorage.removeItem(VIDEO_DATA_KEY) + localStorage.removeItem(DETECTION_TYPE_KEY) } } @@ -459,7 +459,7 @@ export interface VideoResultData { */ export function saveVideoData(data: VideoResultData): void { if (typeof window !== "undefined") { - sessionStorage.setItem(VIDEO_DATA_KEY, JSON.stringify(data)) + localStorage.setItem(VIDEO_DATA_KEY, JSON.stringify(data)) } } @@ -468,7 +468,7 @@ export function saveVideoData(data: VideoResultData): void { */ export function loadVideoData(): VideoResultData | null { if (typeof window !== "undefined") { - const stored = sessionStorage.getItem(VIDEO_DATA_KEY) + const stored = localStorage.getItem(VIDEO_DATA_KEY) if (stored) { return JSON.parse(stored) }