feat(l1): AI decision-tree builder — Phase 2A #193
77
frontend/src/components/l1/L1EscalationsSection.tsx
Normal file
77
frontend/src/components/l1/L1EscalationsSection.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { l1Api } from '@/api/l1'
|
||||
import type { WalkSession } from '@/types/l1'
|
||||
|
||||
/**
|
||||
* Engineer-visible list of escalated L1 sessions (the Phase 2A handoff queue).
|
||||
* Backed by GET /l1/escalations (engineer-or-above). Pollable, dependency-free —
|
||||
* each row expands to show the walked path summary. Renders nothing if empty.
|
||||
*/
|
||||
export function L1EscalationsSection() {
|
||||
const [rows, setRows] = useState<WalkSession[]>([])
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const [expanded, setExpanded] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
l1Api
|
||||
.escalations()
|
||||
.then(setRows)
|
||||
.catch(() => setRows([]))
|
||||
.finally(() => setLoaded(true))
|
||||
}, [])
|
||||
|
||||
if (!loaded || rows.length === 0) return null
|
||||
|
||||
return (
|
||||
<section className="space-y-3">
|
||||
<div>
|
||||
<h2 className="font-heading text-lg font-bold text-heading">L1 escalations</h2>
|
||||
<p className="text-sm text-text-muted">
|
||||
Tickets an L1 tech escalated mid-walk — pick one up to continue.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-default bg-card overflow-hidden">
|
||||
{rows.map((s) => {
|
||||
const isOpen = expanded === s.id
|
||||
return (
|
||||
<div key={s.id} className="border-b border-default last:border-b-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded(isOpen ? null : s.id)}
|
||||
className="w-full px-4 py-3 flex items-center justify-between text-left hover:bg-elevated transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<span className="font-mono text-xs text-text-muted">#{s.id.slice(0, 8)}</span>
|
||||
<span className="text-sm text-text-primary truncate">
|
||||
{s.walked_path.length} step{s.walked_path.length === 1 ? '' : 's'} walked
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-text-muted whitespace-nowrap">
|
||||
{new Date(s.last_step_at).toLocaleString()}
|
||||
</span>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="px-4 pb-3 space-y-1.5">
|
||||
{s.walked_path.length === 0 ? (
|
||||
<p className="text-xs text-text-muted">No steps recorded.</p>
|
||||
) : (
|
||||
<ol className="space-y-1.5 text-sm">
|
||||
{s.walked_path.map((step, i) => (
|
||||
<li key={i} className="flex flex-col">
|
||||
<span className="text-text-muted text-xs">{step.question}</span>
|
||||
{step.answer && (
|
||||
<span className="font-medium text-text-primary">→ {step.answer}</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -10,7 +10,10 @@ export interface FlowProposalSummary {
|
||||
supporting_session_count: number
|
||||
status: 'pending' | 'approved' | 'modified' | 'rejected' | 'dismissed' | 'auto_reinforced'
|
||||
target_flow_id: string | null
|
||||
source_session_id: string
|
||||
// Exactly one source is set: source_session_id (FlowPilot ai_session) XOR
|
||||
// l1_session_id (L1 ai_build walk). Both nullable on the backend (Phase 2A).
|
||||
source_session_id: string | null
|
||||
l1_session_id: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user