Indrajit Ari commited on
Commit
93108c0
·
1 Parent(s): 76b632a

fix: restore full content and wrap in Suspense for static build

Browse files
frontend/src/app/processing/page.tsx ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useRef, Suspense } from 'react'
4
+ import { useRouter, useSearchParams } from 'next/navigation'
5
+
6
+ const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8000'
7
+
8
+ const VOC_COLORS: Record<string, string> = {
9
+ aeroplane:'#87CEEB', bicycle:'#FFA500', bird:'#FFD700', boat:'#00BFFF',
10
+ bottle:'#9400D3', bus:'#FF1493', car:'#DC143C', cat:'#FF8C00',
11
+ chair:'#8B4513', cow:'#D4A017', diningtable:'#D2691E', dog:'#BA55D3',
12
+ horse:'#FF69B4', motorbike:'#22c55e', person:'#FF4500',
13
+ 'potted plant':'#228B22', sheep:'#B8A40A', sofa:'#00CED1',
14
+ train:'#3b82f6', 'tv/monitor':'#0D9488',
15
+ }
16
+
17
+ const STEPS = ['Queued', 'Inferring Frames', 'Encoding H.264', 'Complete']
18
+
19
+ function ProcessingContent() {
20
+ const router = useRouter()
21
+ const searchParams = useSearchParams()
22
+ const jobId = searchParams?.get('id') ?? ''
23
+ const cardRef = useRef<HTMLDivElement>(null)
24
+
25
+ const [pct, setPct] = useState(0)
26
+ const [status, setStatus] = useState('queued')
27
+ const [detected, setDetected] = useState<string[]>([])
28
+ const [error, setError] = useState<string | null>(null)
29
+ const [elapsed, setElapsed] = useState(0)
30
+ const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
31
+
32
+ useEffect(() => {
33
+ setTimeout(() => cardRef.current?.classList.add('scroll-visible'), 50)
34
+ }, [])
35
+
36
+ useEffect(() => {
37
+ if (!jobId) return
38
+ const start = Date.now()
39
+ timerRef.current = setInterval(() => setElapsed(Math.floor((Date.now()-start)/1000)), 1000)
40
+
41
+ const wsUrl = `${API_BASE.replace('https','wss').replace('http','ws')}/ws/${jobId}`
42
+ const ws = new WebSocket(wsUrl)
43
+
44
+ ws.onmessage = (evt) => {
45
+ const data = JSON.parse(evt.data)
46
+ setStatus(data.status)
47
+ if (data.pct !== undefined) setPct(data.pct)
48
+ if (data.detected) setDetected(data.detected)
49
+ if (data.status === 'done') {
50
+ setPct(100); clearInterval(timerRef.current!)
51
+ setTimeout(() => router.push(`/result?id=${jobId}`), 1200)
52
+ }
53
+ if (data.status === 'error') { setError(data.error ?? 'Failed'); clearInterval(timerRef.current!) }
54
+ }
55
+ ws.onerror = () => pollFallback()
56
+ return () => { ws.close(); clearInterval(timerRef.current!) }
57
+ }, [jobId, router])
58
+
59
+ const pollFallback = () => {
60
+ const iv = setInterval(async () => {
61
+ try {
62
+ const d = await fetch(`${API_BASE}/api/status/${jobId}`).then(r=>r.json())
63
+ setStatus(d.status)
64
+ if (d.pct !== undefined) setPct(d.pct)
65
+ if (d.detected) setDetected(d.detected)
66
+ if (d.status === 'done') {
67
+ clearInterval(iv); clearInterval(timerRef.current!)
68
+ setTimeout(() => router.push(`/result?id=${jobId}`), 1200)
69
+ }
70
+ if (d.status === 'error') { setError(d.error); clearInterval(iv) }
71
+ } catch {}
72
+ }, 1200)
73
+ }
74
+
75
+ const fmtTime = (s: number) => `${Math.floor(s/60)}:${String(s%60).padStart(2,'0')}`
76
+ const currentStep = status==='queued' ? 0 : status==='processing' ? 1 : status==='done' ? 3 : 2
77
+
78
+ return (
79
+ <div className="max-w-xl mx-auto px-5 py-20">
80
+ <div
81
+ ref={cardRef}
82
+ className="scroll-hidden card p-8 border border-slate-200 shadow-sm"
83
+ style={{ borderRadius: '20px' }}
84
+ >
85
+ <div className="text-center mb-8">
86
+ <div className={`w-20 h-20 rounded-2xl mx-auto mb-5 flex items-center justify-center
87
+ ${status==='done' ? 'bg-green-50 border border-green-200'
88
+ : status==='error' ? 'bg-red-50 border border-red-200'
89
+ : 'bg-orange-50 border border-orange-200'}`}>
90
+ {status==='done' ? (
91
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2.5"><polyline points="20 6 9 17 4 12"/></svg>
92
+ ) : status==='error' ? (
93
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="2">
94
+ <circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>
95
+ </svg>
96
+ ) : (
97
+ <svg className="animate-spin" width="30" height="30" viewBox="0 0 24 24" fill="none" strokeWidth="2">
98
+ <defs>
99
+ <linearGradient id="spin-g" x1="0%" y1="0%" x2="100%" y2="100%">
100
+ <stop offset="0%" stopColor="#f97316"/>
101
+ <stop offset="100%" stopColor="#fbbf24"/>
102
+ </linearGradient>
103
+ </defs>
104
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" stroke="url(#spin-g)"/>
105
+ </svg>
106
+ )}
107
+ </div>
108
+
109
+ <h1 className="text-xl font-bold text-slate-900 mb-1">
110
+ {status==='queued' ? 'In Queue'
111
+ : status==='processing' ? 'Segmenting…'
112
+ : status==='done' ? 'Complete!'
113
+ : status==='error' ? 'Failed' : status}
114
+ </h1>
115
+ <p className="text-sm text-slate-400">
116
+ Job <code className="text-orange-500 font-mono text-xs">{jobId?.slice(0,8)}…</code>
117
+ {status==='processing' && <span className="ml-2 text-slate-400">· {fmtTime(elapsed)}</span>}
118
+ </p>
119
+ </div>
120
+
121
+ {status !== 'error' && (
122
+ <div className="mb-7">
123
+ <div className="flex justify-between text-xs font-medium text-slate-500 mb-2">
124
+ <span>Progress</span>
125
+ <span className={pct>=100 ? 'text-green-600' : 'text-orange-500'}>{pct.toFixed(1)}%</span>
126
+ </div>
127
+ <div className="progress-track h-2">
128
+ <div className="progress-fill h-full" style={{ width:`${pct}%` }} />
129
+ </div>
130
+ </div>
131
+ )}
132
+
133
+ {status !== 'error' && (
134
+ <div className="mb-7">
135
+ <div className="flex items-center gap-0">
136
+ {STEPS.map((s, i) => (
137
+ <div key={i} className="flex items-center flex-1">
138
+ <div className="flex flex-col items-center gap-1">
139
+ <div className={`step-dot ${i < currentStep ? 'done' : i === currentStep ? 'active' : 'pending'}`}>
140
+ {i < currentStep
141
+ ? <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#15803d" strokeWidth="3"><polyline points="20 6 9 17 4 12"/></svg>
142
+ : i+1}
143
+ </div>
144
+ <p className={`text-[9px] font-semibold uppercase tracking-wider whitespace-nowrap
145
+ ${i===currentStep ? 'text-orange-500' : i<currentStep ? 'text-green-600' : 'text-slate-300'}`}>
146
+ {s}
147
+ </p>
148
+ </div>
149
+ {i < STEPS.length-1 && (
150
+ <div className={`h-px flex-1 mx-1 mb-4 ${i < currentStep ? 'bg-green-300' : 'bg-slate-200'}`} />
151
+ )}
152
+ </div>
153
+ ))}
154
+ </div>
155
+ </div>
156
+ )}
157
+
158
+ {error && (
159
+ <div className="p-4 rounded-xl bg-red-50 border border-red-200 text-red-600 text-sm mb-6">
160
+ <strong>Error:</strong> {error}
161
+ </div>
162
+ )}
163
+
164
+ {status === 'processing' && (
165
+ <div className="grid grid-cols-3 gap-3 mb-7">
166
+ {[
167
+ { label:'Progress', val:`${pct.toFixed(0)}%`, color:'text-orange-500' },
168
+ { label:'Objects', val:`${detected.length}`, color:'text-slate-800' },
169
+ { label:'Elapsed', val:fmtTime(elapsed), color:'text-slate-800' },
170
+ ].map(s => (
171
+ <div key={s.label} className="text-center p-4 rounded-xl bg-slate-50 border border-slate-100">
172
+ <p className="text-[10px] text-slate-400 uppercase tracking-widest mb-1">{s.label}</p>
173
+ <p className={`text-lg font-bold ${s.color}`} style={{fontVariantNumeric:'tabular-nums'}}>{s.val}</p>
174
+ </div>
175
+ ))}
176
+ </div>
177
+ )}
178
+
179
+ {status === 'queued' && (
180
+ <div className="flex items-center gap-3 p-4 rounded-xl bg-orange-50 border border-orange-100 mb-6">
181
+ <div className="flex gap-1.5">
182
+ <span className="bounce-dot bg-orange-400" />
183
+ <span className="bounce-dot bg-amber-400" />
184
+ <span className="bounce-dot bg-yellow-400" />
185
+ </div>
186
+ <p className="text-sm text-orange-700">Waiting for a worker to pick up this job…</p>
187
+ </div>
188
+ )}
189
+
190
+ {detected.length > 0 && (
191
+ <div className="pt-4 border-t border-slate-100">
192
+ <p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-3">
193
+ Detected Objects · {detected.length}
194
+ </p>
195
+ <div className="flex flex-wrap gap-1.5">
196
+ {detected.map(cls => (
197
+ <span key={cls} className="class-pill">
198
+ <span className="w-2 h-2 rounded-full" style={{ backgroundColor: VOC_COLORS[cls]??'#888' }} />
199
+ {cls}
200
+ </span>
201
+ ))}
202
+ </div>
203
+ </div>
204
+ )}
205
+
206
+ <a href="/" className="mt-8 flex items-center justify-center gap-1.5 text-xs text-slate-400 hover:text-slate-600 transition-colors">
207
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="15 18 9 12 15 6"/></svg>
208
+ Back to upload
209
+ </a>
210
+ </div>
211
+ </div>
212
+ )
213
+ }
214
+
215
+ export default function ProcessingPage() {
216
+ return (
217
+ <Suspense fallback={<div className="p-20 text-center text-slate-400">Loading process…</div>}>
218
+ <ProcessingContent />
219
+ </Suspense>
220
+ )
221
+ }
frontend/src/app/result/page.tsx ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useRef, Suspense } from 'react'
4
+ import { useRouter, useSearchParams } from 'next/navigation'
5
+
6
+ const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8000'
7
+
8
+ function ResultContent() {
9
+ const router = useRouter()
10
+ const searchParams = useSearchParams()
11
+ const jobId = searchParams?.get('id') ?? ''
12
+ const videoRef = useRef<HTMLVideoElement>(null)
13
+
14
+ const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading')
15
+ const [videoUrl, setVideoUrl] = useState<string | null>(null)
16
+ const [detected, setDetected] = useState<string[]>([])
17
+ const [isPlaying, setIsPlaying] = useState(false)
18
+ const [copied, setCopied] = useState(false)
19
+ const [retries, setRetries] = useState(0)
20
+
21
+ useEffect(() => {
22
+ if (!jobId) { setStatus('error'); return }
23
+
24
+ const checkStatus = async () => {
25
+ try {
26
+ // 1. Check API status
27
+ const res = await fetch(`${API_BASE}/api/status/${jobId}`)
28
+ if (!res.ok) throw new Error('Not found')
29
+ const data = await res.json()
30
+
31
+ if (data.status === 'done') {
32
+ setDetected(data.detected || [])
33
+ const url = `${API_BASE}/api/video/${jobId}`
34
+
35
+ // 2. Verify file exists with HEAD request
36
+ const head = await fetch(url, { method: 'HEAD' })
37
+ if (head.ok) {
38
+ setVideoUrl(url)
39
+ setStatus('ready')
40
+ } else {
41
+ throw new Error('Video file missing')
42
+ }
43
+ } else if (data.status === 'error') {
44
+ setStatus('error')
45
+ } else {
46
+ // Still processing? Wait and retry
47
+ if (retries < 10) {
48
+ setTimeout(() => setRetries(r => r + 1), 2000)
49
+ } else {
50
+ setStatus('error')
51
+ }
52
+ }
53
+ } catch (err) {
54
+ if (retries < 5) {
55
+ setTimeout(() => setRetries(r => r + 1), 2000)
56
+ } else {
57
+ setStatus('error')
58
+ }
59
+ }
60
+ }
61
+
62
+ checkStatus()
63
+ }, [jobId, retries])
64
+
65
+ const togglePlay = () => {
66
+ const v = videoRef.current; if (!v) return
67
+ if (v.paused) { v.play(); setIsPlaying(true) }
68
+ else { v.pause(); setIsPlaying(false) }
69
+ }
70
+
71
+ const fmtTime = (s: number) =>
72
+ `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`
73
+
74
+ const copyLink = async () => {
75
+ await navigator.clipboard.writeText(window.location.href)
76
+ setCopied(true)
77
+ setTimeout(() => setCopied(false), 2000)
78
+ }
79
+
80
+ if (status === 'loading') return (
81
+ <div className="max-w-4xl mx-auto px-5 py-32 text-center">
82
+ <div className="w-16 h-16 rounded-2xl bg-orange-50 border border-orange-200 flex items-center justify-center mx-auto mb-5">
83
+ <svg className="animate-spin" width="24" height="24" viewBox="0 0 24 24" fill="none" strokeWidth="2">
84
+ <defs>
85
+ <linearGradient id="sg-spin" x1="0%" y1="0%" x2="100%" y2="100%">
86
+ <stop offset="0%" stopColor="#f97316"/>
87
+ <stop offset="100%" stopColor="#fbbf24"/>
88
+ </linearGradient>
89
+ </defs>
90
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" stroke="url(#sg-spin)"/>
91
+ </svg>
92
+ </div>
93
+ <p className="text-sm font-medium text-slate-600">Loading your result…</p>
94
+ </div>
95
+ )
96
+
97
+ if (status === 'error') return (
98
+ <div className="max-w-4xl mx-auto px-5 py-32 text-center">
99
+ <div className="w-16 h-16 rounded-2xl bg-red-50 border border-red-200 flex items-center justify-center mx-auto mb-5">
100
+ <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="2">
101
+ <circle cx="12" cy="12" r="10"/>
102
+ <line x1="15" y1="9" x2="9" y2="15"/>
103
+ <line x1="9" y1="9" x2="15" y2="15"/>
104
+ </svg>
105
+ </div>
106
+ <p className="font-semibold text-slate-800 mb-1">Result not available</p>
107
+ <p className="text-sm text-slate-500 mb-6">The job might still be processing or the file has expired.</p>
108
+ <div className="flex items-center justify-center gap-3">
109
+ <button onClick={() => { setStatus('loading'); setRetries(0) }} className="btn-outline px-5 py-2.5 text-sm">Retry</button>
110
+ <a href="/" className="btn-primary px-5 py-2.5 text-sm">New Upload</a>
111
+ </div>
112
+ </div>
113
+ )
114
+
115
+ return (
116
+ <div className="bg-white max-w-5xl mx-auto px-5 py-12">
117
+ <div className="flex items-start justify-between mb-10 flex-wrap gap-4">
118
+ <div>
119
+ <div className="flex items-center gap-2 mb-2">
120
+ <span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
121
+ <span className="text-xs font-bold text-green-600 uppercase tracking-widest">Segmentation Finished</span>
122
+ </div>
123
+ <h1 className="text-3xl font-bold text-slate-900 tracking-tight">Your AI Result</h1>
124
+ <p className="text-sm text-slate-500 mt-1">
125
+ Job ID: <code className="text-orange-500 font-mono">{jobId?.slice(0, 12)}</code>
126
+ </p>
127
+ </div>
128
+
129
+ <div className="flex gap-2">
130
+ <button onClick={copyLink} className="btn-outline px-4 py-2.5 text-sm flex items-center gap-2">
131
+ {copied ? 'Link Copied!' : 'Copy Result Link'}
132
+ </button>
133
+ <a href={videoUrl!} download className="btn-primary px-5 py-2.5 text-sm flex items-center gap-2">
134
+ Download MP4
135
+ </a>
136
+ </div>
137
+ </div>
138
+
139
+ <div className="bg-slate-900 rounded-[32px] overflow-hidden shadow-2xl border border-slate-800 relative group aspect-video lg:aspect-auto">
140
+ <video
141
+ ref={videoRef}
142
+ src={videoUrl!}
143
+ className="w-full h-full max-h-[70vh]"
144
+ onPlay={() => setIsPlaying(true)}
145
+ onPause={() => setIsPlaying(false)}
146
+ onClick={togglePlay}
147
+ />
148
+
149
+ <div className="absolute inset-x-0 bottom-0 p-6 bg-gradient-to-t from-black/80 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
150
+ <div className="flex items-center justify-between">
151
+ <button onClick={togglePlay} className="w-12 h-12 rounded-full bg-white/10 backdrop-blur-md flex items-center justify-center hover:bg-white/20 transition-all">
152
+ {isPlaying ? (
153
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="white"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
154
+ ) : (
155
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="white"><polygon points="5 3 19 12 5 21 5 3"/></svg>
156
+ )}
157
+ </button>
158
+ <div className="px-4 py-2 rounded-full bg-black/40 backdrop-blur-md border border-white/10 text-[10px] font-bold text-white uppercase tracking-widest">
159
+ DeepLabV3+ Output
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+
165
+ {detected.length > 0 && (
166
+ <div className="mt-12">
167
+ <h2 className="text-sm font-bold text-slate-400 uppercase tracking-[0.2em] mb-6 flex items-center gap-3">
168
+ <span className="h-px bg-slate-100 flex-1" />
169
+ Detected Objects
170
+ <span className="h-px bg-slate-100 flex-1" />
171
+ </h2>
172
+ <div className="flex flex-wrap justify-center gap-2">
173
+ {detected.map(cls => (
174
+ <span key={cls} className="px-4 py-2 rounded-xl bg-slate-50 border border-slate-100 text-sm font-medium text-slate-700 capitalize">
175
+ {cls}
176
+ </span>
177
+ ))}
178
+ </div>
179
+ </div>
180
+ )}
181
+
182
+ <div className="mt-16 text-center">
183
+ <a href="/" className="inline-flex items-center gap-2 text-sm text-slate-400 hover:text-orange-500 transition-colors">
184
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
185
+ Back to upload new video
186
+ </a>
187
+ </div>
188
+ </div>
189
+ )
190
+ }
191
+
192
+ export default function ResultPage() {
193
+ return (
194
+ <Suspense fallback={<div className="p-20 text-center">Loading...</div>}>
195
+ <ResultContent />
196
+ </Suspense>
197
+ )
198
+ }