feat(l1): proposal L1-source block + engineer L1-escalations section
- flow-proposal.ts: source_session_id nullable + add l1_session_id (matches backend
FlowProposalSummary).
- ProposalDetail.tsx: render an 'AI L1 walk (outcome-validated)' note when
l1_session_id is set instead of the /pilot/{source_session_id} link; fall back to
the link for ai_session-sourced proposals.
- New L1EscalationsSection.tsx (GET /l1/escalations) — expandable rows with walked-path
summary; renders nothing if empty. Mounted below the FlowPilot queue on
EscalationQueuePage. tsc -b + eslint clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
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
|
supporting_session_count: number
|
||||||
status: 'pending' | 'approved' | 'modified' | 'rejected' | 'dismissed' | 'auto_reinforced'
|
status: 'pending' | 'approved' | 'modified' | 'rejected' | 'dismissed' | 'auto_reinforced'
|
||||||
target_flow_id: string | null
|
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
|
created_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user