refactor: redesign Session History with tabs + Load More, improve Escalation Queue urgency
Session History: - Split into AI Sessions / Flow Sessions tabs (AI default) - Load More pagination (25 per page) instead of 50-item hard cap - Dynamic problem domain filter from actual session data - Fix all blue focus rings to ember orange - Fix badge colors to use design system tokens Escalation Queue: - Add wait-time color coding (muted <1h, amber 1-4h, red >4h) - Sort oldest-first for triage urgency - Compact right-aligned pickup button - Widen container, dynamic session count in subtitle - Fix typos and non-system color tokens Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,12 +3,21 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
import { AlertTriangle, Clock, Hash, Ticket, Loader2, RefreshCw } from 'lucide-react'
|
import { AlertTriangle, Clock, Hash, Ticket, Loader2, RefreshCw } from 'lucide-react'
|
||||||
import { aiSessionsApi } from '@/api'
|
import { aiSessionsApi } from '@/api'
|
||||||
import type { AISessionSummary } from '@/types/ai-session'
|
import type { AISessionSummary } from '@/types/ai-session'
|
||||||
|
import { timeAgo } from '@/lib/timeAgo'
|
||||||
|
|
||||||
interface EscalationQueueProps {
|
interface EscalationQueueProps {
|
||||||
onPickup?: (sessionId: string) => void
|
onPickup?: (sessionId: string) => void
|
||||||
|
onCountChange?: (count: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EscalationQueue({ onPickup }: EscalationQueueProps) {
|
function waitTimeColor(createdAt: string): string {
|
||||||
|
const hours = (Date.now() - new Date(createdAt).getTime()) / 3_600_000
|
||||||
|
if (hours >= 4) return '#f87171' // danger
|
||||||
|
if (hours >= 1) return '#fbbf24' // warning/amber
|
||||||
|
return '#848b9b' // muted
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EscalationQueue({ onPickup, onCountChange }: EscalationQueueProps) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [sessions, setSessions] = useState<AISessionSummary[]>([])
|
const [sessions, setSessions] = useState<AISessionSummary[]>([])
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
@@ -19,7 +28,12 @@ export function EscalationQueue({ onPickup }: EscalationQueueProps) {
|
|||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
const data = await aiSessionsApi.getEscalationQueue()
|
const data = await aiSessionsApi.getEscalationQueue()
|
||||||
setSessions(data)
|
// Sort oldest-first — longest waiting = most urgent
|
||||||
|
const sorted = [...data].sort(
|
||||||
|
(a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
||||||
|
)
|
||||||
|
setSessions(sorted)
|
||||||
|
onCountChange?.(sorted.length)
|
||||||
} catch {
|
} catch {
|
||||||
setError('Failed to load escalation queue')
|
setError('Failed to load escalation queue')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -29,6 +43,7 @@ export function EscalationQueue({ onPickup }: EscalationQueueProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadQueue()
|
loadQueue()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- load once on mount
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handlePickup = (sessionId: string) => {
|
const handlePickup = (sessionId: string) => {
|
||||||
@@ -50,7 +65,7 @@ export function EscalationQueue({ onPickup }: EscalationQueueProps) {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="py-12 text-center">
|
<div className="py-12 text-center">
|
||||||
<p className="text-sm text-rose-400">{error}</p>
|
<p className="text-sm text-danger">{error}</p>
|
||||||
<button
|
<button
|
||||||
onClick={loadQueue}
|
onClick={loadQueue}
|
||||||
className="mt-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
className="mt-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||||
@@ -80,7 +95,7 @@ export function EscalationQueue({ onPickup }: EscalationQueueProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between px-1">
|
<div className="flex items-center justify-between px-1">
|
||||||
<h3 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted">
|
<h3 className="font-sans text-[0.625rem] uppercase tracking-wider text-muted-foreground">
|
||||||
Awaiting pickup ({sessions.length})
|
Awaiting pickup ({sessions.length})
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
@@ -93,13 +108,13 @@ export function EscalationQueue({ onPickup }: EscalationQueueProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sessions.map((session) => (
|
{sessions.map((session) => (
|
||||||
<div key={session.id} className="card-interactive p-3 sm:p-4 space-y-3">
|
<div key={session.id} className="card-flat p-3 sm:p-4 space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-foreground">
|
<p className="text-sm font-semibold text-foreground">
|
||||||
{session.problem_summary || 'Untitled session'}
|
{session.problem_summary || 'Untitled session'}
|
||||||
</p>
|
</p>
|
||||||
{session.escalation_reason && (
|
{session.escalation_reason && (
|
||||||
<p className="mt-1 text-xs text-amber-400 line-clamp-2">
|
<p className="mt-1 text-xs text-warning line-clamp-2">
|
||||||
Reason: {session.escalation_reason}
|
Reason: {session.escalation_reason}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -107,7 +122,7 @@ export function EscalationQueue({ onPickup }: EscalationQueueProps) {
|
|||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
||||||
{session.problem_domain && (
|
{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">
|
<span className="font-sans rounded-md bg-accent-dim px-1.5 py-0.5 text-[0.5625rem] uppercase tracking-wider text-accent-text">
|
||||||
{session.problem_domain}
|
{session.problem_domain}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -115,24 +130,29 @@ export function EscalationQueue({ onPickup }: EscalationQueueProps) {
|
|||||||
<Hash size={10} />
|
<Hash size={10} />
|
||||||
{session.step_count} steps
|
{session.step_count} steps
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span
|
||||||
|
className="flex items-center gap-1 font-medium"
|
||||||
|
style={{ color: waitTimeColor(session.created_at) }}
|
||||||
|
>
|
||||||
<Clock size={10} />
|
<Clock size={10} />
|
||||||
{new Date(session.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
{timeAgo(session.created_at)}
|
||||||
</span>
|
</span>
|
||||||
{session.psa_ticket_id && (
|
{session.psa_ticket_id && (
|
||||||
<span className="flex items-center gap-1 text-primary">
|
<span className="flex items-center gap-1 text-accent-text">
|
||||||
<Ticket size={10} />
|
<Ticket size={10} />
|
||||||
#{session.psa_ticket_id}
|
#{session.psa_ticket_id}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div className="flex justify-end">
|
||||||
onClick={() => handlePickup(session.id)}
|
<button
|
||||||
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"
|
onClick={() => handlePickup(session.id)}
|
||||||
>
|
className="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>
|
Pick Up
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
import { AlertTriangle } from 'lucide-react'
|
import { AlertTriangle } from 'lucide-react'
|
||||||
import { EscalationQueue } from '@/components/flowpilot'
|
import { EscalationQueue } from '@/components/flowpilot'
|
||||||
|
|
||||||
export default function EscalationQueuePage() {
|
export default function EscalationQueuePage() {
|
||||||
|
const [count, setCount] = useState<number | null>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-3xl p-6">
|
<div className="mx-auto max-w-4xl p-6">
|
||||||
<div className="flex items-center gap-3 mb-6">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-amber-500/10">
|
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-warning-dim">
|
||||||
<AlertTriangle size={16} className="text-amber-400" />
|
<AlertTriangle size={16} className="text-warning" />
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="font-heading text-xl font-bold text-foreground">Escalation Queue</h1>
|
<h1 className="font-heading text-xl font-bold text-foreground">Escalation Queue</h1>
|
||||||
<p className="text-sm text-muted-foreground">Sessions from your team waiting for pickup</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{count !== null && count > 0
|
||||||
|
? `${count} session${count !== 1 ? 's' : ''} waiting for pickup`
|
||||||
|
: 'Sessions from your team waiting for pickup'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EscalationQueue />
|
<EscalationQueue onCountChange={setCount} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user