Bundles four fixes from the live debugging session: 1. AssistantChatPage: replace urlSessionId === activeChatId gate with a loadedChatIdsRef. After8914391made activeChatId initialize from urlSessionId, the gate short-circuited fresh mounts and selectChat never fired. Symptom: senior picks up an escalation, lands on a blank chat surface with no conversation history and no sidebar entry. Fix also adds loadChats() in handleStartHere so the picked-up session appears in the sidebar (its escalated_to_id is null pre-claim, so listSessions doesn't return it until claim_session sets it). 2. config: bump ESCALATION_AI_ASSESSMENT_TIMEOUT_SECONDS 15s → 45s. Sonnet was hitting tail latency at 15s in the field, leaving the magic-moment placeholder permanent. Background-task architecture (e8ba74e) means this no longer blocks the user; it's just the budget before publishing has_assessment=false. NOTE: live test still shows assessment not populating — see HANDOFF for the consolidation plan that supersedes this. 3. Enter-to-submit: chat-input convention (Enter submits, Shift+Enter inserts newline) on the escalate-flow forms. RichTextInput gains an optional onSubmit prop; EscalateModal wires it to handleSubmit; ConcludeSessionModal gets the same handler on its plain textarea. 4. PendingEscalations: each row is now expandable. Click row body to reveal the engineer's escalation reason, step count on record, confidence tier, and PSA ticket number. Pick Up still clicks through directly. Single-expand-at-a-time keeps the dashboard compact. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
155 lines
6.3 KiB
TypeScript
155 lines
6.3 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { Link, useNavigate } from 'react-router-dom'
|
|
import { AlertTriangle, ChevronDown, ChevronRight, Hash } from 'lucide-react'
|
|
import { aiSessionsApi } from '@/api/aiSessions'
|
|
import type { AISessionSummary } from '@/types/ai-session'
|
|
import { timeAgo } from '@/lib/timeAgo'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
export function PendingEscalations() {
|
|
const [escalations, setEscalations] = useState<AISessionSummary[]>([])
|
|
// Single expansion at a time — keeps the dashboard compact even when
|
|
// multiple escalations are pending. Click a row again to collapse.
|
|
const [expandedId, setExpandedId] = useState<string | null>(null)
|
|
const navigate = useNavigate()
|
|
|
|
useEffect(() => {
|
|
aiSessionsApi.getEscalationQueue()
|
|
.then(setEscalations)
|
|
.catch(() => {})
|
|
}, [])
|
|
|
|
if (escalations.length === 0) return null
|
|
|
|
return (
|
|
<div
|
|
className="card-flat overflow-hidden"
|
|
style={{ borderColor: 'rgba(234, 179, 8, 0.2)' }}
|
|
>
|
|
<div
|
|
className="flex items-center justify-between px-5 py-3"
|
|
style={{ borderBottom: '1px solid var(--color-border-default)' }}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<AlertTriangle size={14} className="text-amber-400" />
|
|
<h3 className="font-heading text-sm font-bold text-foreground">
|
|
Pending Escalations
|
|
<span className="ml-2 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-amber-400/10 px-1.5 text-[0.625rem] font-bold text-amber-400">
|
|
{escalations.length}
|
|
</span>
|
|
</h3>
|
|
</div>
|
|
<Link
|
|
to="/escalations"
|
|
className="text-[0.6875rem] text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
View all
|
|
</Link>
|
|
</div>
|
|
<div>
|
|
{escalations.slice(0, 3).map((esc, i) => {
|
|
const isExpanded = expandedId === esc.id
|
|
const isLast = i >= Math.min(escalations.length, 3) - 1
|
|
return (
|
|
<div
|
|
key={esc.id}
|
|
style={{
|
|
borderBottom: !isLast
|
|
? '1px solid var(--color-border-default)'
|
|
: undefined,
|
|
}}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={() => setExpandedId(isExpanded ? null : esc.id)}
|
|
aria-expanded={isExpanded}
|
|
className="w-full flex items-center gap-3 px-5 py-3 text-left hover:bg-elevated/30 transition-colors"
|
|
>
|
|
<span className="h-2 w-2 shrink-0 rounded-full bg-amber-400 animate-pulse" />
|
|
{isExpanded ? (
|
|
<ChevronDown size={12} className="shrink-0 text-muted-foreground" />
|
|
) : (
|
|
<ChevronRight size={12} className="shrink-0 text-muted-foreground" />
|
|
)}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm text-foreground truncate">
|
|
{esc.problem_summary || 'Escalated session'}
|
|
</div>
|
|
<div className="text-[0.6875rem] text-muted-foreground">
|
|
{esc.problem_domain || 'General'}
|
|
<span className="mx-1.5 text-[var(--text-dimmed)]">·</span>
|
|
<span className="font-sans text-xs">{timeAgo(esc.created_at)}</span>
|
|
{esc.psa_ticket_id && (
|
|
<>
|
|
<span className="mx-1.5 text-[var(--text-dimmed)]">·</span>
|
|
<span className="inline-flex items-center gap-0.5 font-mono text-[0.625rem] text-accent-text">
|
|
<Hash size={9} />
|
|
{esc.psa_ticket_id}
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<span
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
navigate(`/pilot/${esc.id}?pickup=true`)
|
|
}}
|
|
role="button"
|
|
tabIndex={0}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
navigate(`/pilot/${esc.id}?pickup=true`)
|
|
}
|
|
}}
|
|
className="shrink-0 rounded-lg border border-amber-400/30 bg-amber-400/10 px-3 py-1 text-[0.6875rem] font-medium text-amber-400 hover:bg-amber-400/20 transition-colors cursor-pointer"
|
|
>
|
|
Pick up
|
|
</span>
|
|
</button>
|
|
|
|
{isExpanded && (
|
|
<div
|
|
className={cn(
|
|
'px-5 pb-3 pl-12 space-y-2 text-xs animate-fade-in'
|
|
)}
|
|
>
|
|
{esc.escalation_reason && (
|
|
<div>
|
|
<p className="font-sans text-[0.5625rem] uppercase tracking-wider text-muted-foreground mb-0.5">
|
|
Why escalated
|
|
</p>
|
|
<p className="text-foreground whitespace-pre-wrap leading-snug">
|
|
{esc.escalation_reason}
|
|
</p>
|
|
</div>
|
|
)}
|
|
<div className="flex flex-wrap gap-x-3 gap-y-1 text-muted-foreground">
|
|
<span>
|
|
<span className="font-medium text-foreground">{esc.step_count}</span>{' '}
|
|
diagnostic {esc.step_count === 1 ? 'step' : 'steps'} on record
|
|
</span>
|
|
{esc.confidence_tier && (
|
|
<span className="font-sans uppercase tracking-wider text-[0.5625rem]">
|
|
Confidence: {esc.confidence_tier}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{!esc.escalation_reason && (
|
|
<p className="italic text-muted-foreground">
|
|
No reason note from the original engineer. Pick up to see the full session
|
|
context and AI assessment.
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|