| 'use client'; |
|
|
| import { useEffect, useState } from 'react'; |
| import { ListTodo, Send, Loader2, RefreshCw, Clock, CheckCircle2, XCircle, Zap, AlertTriangle } from 'lucide-react'; |
| import { api, type QueueJob, type QueueStatus } from '@/lib/api'; |
|
|
| const PRIORITY_LABELS = ['CRITICAL', 'HIGH', 'NORMAL', 'LOW']; |
| const PRIORITY_COLORS = [ |
| 'bg-red-500/20 text-red-300 border-red-500/30', |
| 'bg-orange-500/20 text-orange-300 border-orange-500/30', |
| 'bg-blue-500/20 text-blue-300 border-blue-500/30', |
| 'bg-slate-500/20 text-slate-300 border-slate-500/30', |
| ]; |
|
|
| function PriorityBadge({ priority }: { priority: number }) { |
| const cls = PRIORITY_COLORS[priority] ?? PRIORITY_COLORS[2]; |
| return ( |
| <span className={`inline-flex items-center rounded border px-2 py-0.5 text-[10px] font-bold ${cls}`}> |
| {PRIORITY_LABELS[priority] ?? 'NORMAL'} |
| </span> |
| ); |
| } |
|
|
| function JobStatusIcon({ status }: { status: string }) { |
| if (status === 'COMPLETE') return <CheckCircle2 className="h-4 w-4 text-emerald-400" />; |
| if (status === 'FAILED') return <XCircle className="h-4 w-4 text-red-400" />; |
| if (status === 'RUNNING') return <Loader2 className="h-4 w-4 text-blue-400 animate-spin" />; |
| if (status === 'CANCELLED') return <XCircle className="h-4 w-4 text-orange-400" />; |
| return <Clock className="h-4 w-4 text-slate-400" />; |
| } |
|
|
| export default function QueuePanel() { |
| const [status, setStatus] = useState<QueueStatus | null>(null); |
| const [jobs, setJobs] = useState<QueueJob[]>([]); |
| const [loading, setLoading] = useState(false); |
| const [err, setErr] = useState<string | null>(null); |
|
|
| |
| const [prompt, setPrompt] = useState(''); |
| const [priority, setPriority] = useState(2); |
| const [submitting, setSubmitting] = useState(false); |
|
|
| const reload = async () => { |
| setLoading(true); |
| try { |
| const [st, jb] = await Promise.all([api.queueStatus(), api.listQueueJobs()]); |
| setStatus(st); |
| setJobs(jb.jobs); |
| setErr(null); |
| } catch (e: any) { |
| setErr(e.message); |
| } finally { |
| setLoading(false); |
| } |
| }; |
|
|
| useEffect(() => { |
| reload(); |
| const t = setInterval(reload, 3000); |
| return () => clearInterval(t); |
| }, []); |
|
|
| const submit = async (e: React.FormEvent) => { |
| e.preventDefault(); |
| if (!prompt.trim()) return; |
| setSubmitting(true); |
| try { |
| const title = prompt.split('\n')[0].slice(0, 80); |
| await api.queueSubmit(title, prompt, priority); |
| setPrompt(''); |
| await reload(); |
| } catch (e: any) { |
| setErr(e.message); |
| } finally { |
| setSubmitting(false); |
| } |
| }; |
|
|
| return ( |
| <div className="space-y-6"> |
| {/* Stats row */} |
| <div className="grid grid-cols-2 sm:grid-cols-3 gap-4"> |
| <div className="rounded-xl border border-white/10 bg-slate-900/60 p-4"> |
| <p className="text-xs text-slate-400 uppercase tracking-wide mb-1">Queue Depth</p> |
| <p className="text-3xl font-bold">{status?.queue_depth ?? '—'}</p> |
| </div> |
| <div className="rounded-xl border border-white/10 bg-slate-900/60 p-4"> |
| <p className="text-xs text-slate-400 uppercase tracking-wide mb-1">Running</p> |
| <p className="text-3xl font-bold text-blue-300">{status?.running_jobs ?? '—'}</p> |
| </div> |
| <div className="rounded-xl border border-white/10 bg-slate-900/60 p-4"> |
| <p className="text-xs text-slate-400 uppercase tracking-wide mb-1">Backend</p> |
| <div className="flex items-center gap-2 mt-1"> |
| <Zap className="h-4 w-4 text-brand-400" /> |
| <span className="font-semibold capitalize">{status?.backend ?? '—'}</span> |
| </div> |
| </div> |
| </div> |
| |
| {/* Submit to queue */} |
| <section className="rounded-xl border border-white/10 bg-slate-900/60 p-5"> |
| <h2 className="flex items-center gap-2 font-semibold mb-4"> |
| <ListTodo className="h-4 w-4 text-brand-400" /> |
| Submit to Queue |
| </h2> |
| <form onSubmit={submit} className="space-y-3"> |
| <textarea |
| value={prompt} |
| onChange={e => setPrompt(e.target.value)} |
| rows={3} |
| placeholder="Describe the task to queue..." |
| className="w-full rounded-lg border border-white/10 bg-slate-800/60 p-3 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500" |
| /> |
| <div className="flex items-center gap-3 flex-wrap"> |
| <div className="flex items-center gap-2"> |
| <label className="text-xs text-slate-400">Priority:</label> |
| <select |
| value={priority} |
| onChange={e => setPriority(Number(e.target.value))} |
| className="text-xs bg-slate-700 border border-slate-600 text-slate-200 rounded px-2 py-1 focus:outline-none" |
| > |
| <option value={0}>0 — CRITICAL</option> |
| <option value={1}>1 — HIGH</option> |
| <option value={2}>2 — NORMAL</option> |
| <option value={3}>3 — LOW</option> |
| </select> |
| </div> |
| <button |
| type="submit" |
| disabled={submitting || !prompt.trim()} |
| className="flex items-center gap-2 rounded-lg bg-brand-600 px-4 py-2 text-sm font-medium hover:bg-brand-700 disabled:opacity-50 ml-auto" |
| > |
| {submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />} |
| Queue Task |
| </button> |
| </div> |
| </form> |
| </section> |
| |
| {/* Error */} |
| {err && ( |
| <div className="flex items-center gap-2 rounded-lg bg-red-500/10 border border-red-500/20 p-3 text-sm text-red-300"> |
| <AlertTriangle className="h-4 w-4 shrink-0" /> {err} |
| </div> |
| )} |
| |
| {/* Queued jobs */} |
| <section className="rounded-xl border border-white/10 bg-slate-900/60 p-5"> |
| <header className="flex items-center justify-between mb-4"> |
| <h2 className="flex items-center gap-2 font-semibold"> |
| <ListTodo className="h-4 w-4 text-slate-400" /> |
| Queued Jobs |
| {jobs.length > 0 && ( |
| <span className="ml-1 text-xs bg-slate-700 text-slate-300 px-2 py-0.5 rounded-full">{jobs.length}</span> |
| )} |
| </h2> |
| <button |
| onClick={reload} |
| className="flex items-center gap-1 text-xs text-slate-400 hover:text-slate-200 border border-white/10 rounded px-2 py-1 hover:bg-white/5" |
| > |
| <RefreshCw className={`h-3 w-3 ${loading ? 'animate-spin' : ''}`} /> |
| Refresh |
| </button> |
| </header> |
| |
| {jobs.length === 0 ? ( |
| <p className="text-sm text-slate-500 italic">No jobs in queue.</p> |
| ) : ( |
| <div className="space-y-2"> |
| {jobs.map(job => ( |
| <div key={job.job_id} className="flex items-start gap-3 rounded-lg border border-white/5 bg-slate-800/40 p-3"> |
| <JobStatusIcon status={job.status} /> |
| <div className="flex-1 min-w-0"> |
| <div className="flex items-center gap-2 flex-wrap"> |
| <span className="font-mono text-xs text-slate-500">#{job.task_id}</span> |
| <PriorityBadge priority={job.priority} /> |
| <span className="text-xs text-slate-400 truncate">{job.prompt.slice(0, 60)}</span> |
| </div> |
| <p className="text-[10px] text-slate-600 mt-0.5"> |
| job: {job.job_id} · status: {job.status} |
| {job.retry_count > 0 && ` · retries: ${job.retry_count}`} |
| </p> |
| </div> |
| </div> |
| ))} |
| </div> |
| )} |
| </section> |
| </div> |
| ); |
| } |
|
|