3,200+ hardcoded color values replaced with CSS variable-backed Tailwind classes (bg-card, text-foreground, border-border, etc.). Enables light mode via CSS variable swap. Only syntax highlighting colors and intentional one-offs remain hardcoded (~15 values). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
141 lines
4.4 KiB
TypeScript
141 lines
4.4 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { AlertTriangle, Clock, Hash, Ticket, Loader2, RefreshCw } from 'lucide-react'
|
|
import { aiSessionsApi } from '@/api'
|
|
import type { AISessionSummary } from '@/types/ai-session'
|
|
|
|
interface EscalationQueueProps {
|
|
onPickup?: (sessionId: string) => void
|
|
}
|
|
|
|
export function EscalationQueue({ onPickup }: EscalationQueueProps) {
|
|
const navigate = useNavigate()
|
|
const [sessions, setSessions] = useState<AISessionSummary[]>([])
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
const loadQueue = async () => {
|
|
setIsLoading(true)
|
|
setError(null)
|
|
try {
|
|
const data = await aiSessionsApi.getEscalationQueue()
|
|
setSessions(data)
|
|
} catch {
|
|
setError('Failed to load escalation queue')
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadQueue()
|
|
}, [])
|
|
|
|
const handlePickup = (sessionId: string) => {
|
|
if (onPickup) {
|
|
onPickup(sessionId)
|
|
} else {
|
|
navigate(`/pilot/${sessionId}?pickup=true`)
|
|
}
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 size={20} className="animate-spin text-muted-foreground" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="py-12 text-center">
|
|
<p className="text-sm text-rose-400">{error}</p>
|
|
<button
|
|
onClick={loadQueue}
|
|
className="mt-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
Try again
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (sessions.length === 0) {
|
|
return (
|
|
<div className="py-12 text-center">
|
|
<AlertTriangle size={24} className="mx-auto mb-2 text-muted-foreground/40" />
|
|
<p className="text-sm text-muted-foreground">No sessions awaiting escalation</p>
|
|
<button
|
|
onClick={loadQueue}
|
|
className="mt-3 flex items-center gap-1.5 mx-auto text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
<RefreshCw size={12} />
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between px-1">
|
|
<h3 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted">
|
|
Awaiting pickup ({sessions.length})
|
|
</h3>
|
|
<button
|
|
onClick={loadQueue}
|
|
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
<RefreshCw size={10} />
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
|
|
{sessions.map((session) => (
|
|
<div key={session.id} className="card-interactive p-3 sm:p-4 space-y-3">
|
|
<div>
|
|
<p className="text-sm font-semibold text-foreground">
|
|
{session.problem_summary || 'Untitled session'}
|
|
</p>
|
|
{session.escalation_reason && (
|
|
<p className="mt-1 text-xs text-amber-400 line-clamp-2">
|
|
Reason: {session.escalation_reason}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
|
{session.problem_domain && (
|
|
<span className="font-sans text-xs rounded-md bg-accent-dim px-1.5 py-0.5 text-[0.5625rem] uppercase tracking-wider text-primary">
|
|
{session.problem_domain}
|
|
</span>
|
|
)}
|
|
<span className="flex items-center gap-1">
|
|
<Hash size={10} />
|
|
{session.step_count} steps
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<Clock size={10} />
|
|
{new Date(session.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
</span>
|
|
{session.psa_ticket_id && (
|
|
<span className="flex items-center gap-1 text-primary">
|
|
<Ticket size={10} />
|
|
#{session.psa_ticket_id}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => handlePickup(session.id)}
|
|
className="w-full min-h-[44px] rounded-lg bg-primary text-white px-4 py-2 text-sm font-semibold hover:brightness-110 active:scale-[0.98] transition-all"
|
|
>
|
|
Pick Up Session
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|