diff --git a/frontend/src/components/l1/L1WalkTreeVariant.tsx b/frontend/src/components/l1/L1WalkTreeVariant.tsx
new file mode 100644
index 00000000..eaebcf3a
--- /dev/null
+++ b/frontend/src/components/l1/L1WalkTreeVariant.tsx
@@ -0,0 +1,173 @@
+import { useState } from 'react'
+import { ChevronLeft } from 'lucide-react'
+import { Link } from 'react-router-dom'
+import { l1Api } from '@/api/l1'
+import type { WalkSession } from '@/types/l1'
+import { EscalateModal, ResolveModal } from '@/components/l1/WalkModals'
+
+interface Props {
+ session: WalkSession
+ onSessionUpdate: (s: WalkSession) => void
+ onDone: () => void
+}
+
+export function L1WalkTreeVariant({ session, onSessionUpdate, onDone }: Props) {
+ const [showResolve, setShowResolve] = useState(false)
+ const [showEscalate, setShowEscalate] = useState(false)
+ const [note, setNote] = useState('')
+
+ // Phase 1: we don't have the live flow-tree fetch wired up here yet
+ // (the tree-navigation pages have their own loader). The walker shows the
+ // walked-path side panel, advance buttons stubbed for now — Phase 2 wires
+ // the actual flow tree fetching + node advancement against tree data.
+ // The "Yes/No" buttons record a synthetic step so the walked_path JSONB
+ // grows; this gives us a functional roundtrip until Phase 2 wires the tree.
+
+ const handleAnswer = async (answer: 'yes' | 'no') => {
+ const nodeId = session.current_node_id || `step-${session.walked_path.length + 1}`
+ try {
+ const updated = await l1Api.step(session.id, {
+ node_id: nodeId,
+ question: `Step ${session.walked_path.length + 1}`,
+ answer,
+ note: note || null,
+ })
+ onSessionUpdate(updated)
+ setNote('')
+ } catch (err) {
+ // Keep silent for v1 — Phase 2 wires real error UI
+ console.error('step failed', err)
+ }
+ }
+
+ const lastError = (err: unknown): string => {
+ if (typeof err === 'object' && err && 'response' in err) {
+ const detail = (err as any).response?.data?.detail
+ if (typeof detail === 'string') return detail
+ }
+ return 'Unexpected error'
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+ #{session.id.slice(0, 8)}
+ {session.session_kind === 'proposal' && (
+ AI-built
+ )}
+
+
+
+
+
+
+
+ {/* Two-pane body */}
+
+
+
+ Step {session.walked_path.length + 1}
+
+ {session.status !== 'active' ? (
+
+
+ This session is {session.status}.
+
+
+
+ ) : (
+
+
Continue the walk:
+
+
+
+
+
+ )}
+
+
+ {/* Right pane: transcript */}
+
+
+
+ {/* Modals */}
+ {showResolve && (
+
setShowResolve(false)}
+ onConfirm={async (helpful, resolutionNotes) => {
+ try {
+ await l1Api.resolve(session.id, { helpful, resolution_notes: resolutionNotes })
+ onDone()
+ } catch (err) {
+ console.error('resolve failed:', lastError(err))
+ }
+ }}
+ />
+ )}
+ {showEscalate && (
+ setShowEscalate(false)}
+ onConfirm={async (category, reason) => {
+ try {
+ await l1Api.escalate(session.id, { reason, reason_category: category })
+ onDone()
+ } catch (err) {
+ console.error('escalate failed:', lastError(err))
+ }
+ }}
+ />
+ )}
+
+ )
+}
diff --git a/frontend/src/components/l1/WalkModals.tsx b/frontend/src/components/l1/WalkModals.tsx
new file mode 100644
index 00000000..f51033ad
--- /dev/null
+++ b/frontend/src/components/l1/WalkModals.tsx
@@ -0,0 +1,121 @@
+import { useState } from 'react'
+
+export interface ResolveModalProps {
+ defaultNotes?: string
+ onClose: () => void
+ onConfirm: (helpful: boolean, notes: string) => Promise
+}
+
+export function ResolveModal({ defaultNotes = '', onClose, onConfirm }: ResolveModalProps) {
+ const [helpful, setHelpful] = useState(null)
+ const [notes, setNotes] = useState(defaultNotes)
+ const [submitting, setSubmitting] = useState(false)
+
+ return (
+
+
+
Did this resolve it?
+
+
+
+
+
+
+ )
+}
+
+export interface EscalateModalProps {
+ onClose: () => void
+ onConfirm: (category: string, reason: string) => Promise
+}
+
+const REASON_CATEGORIES = [
+ 'Out of L1 scope',
+ 'Customer demanding senior',
+ 'Tree dead-ended',
+ 'AI tree wrong',
+ 'No KB available',
+ 'Other',
+] as const
+
+export function EscalateModal({ onClose, onConfirm }: EscalateModalProps) {
+ const [category, setCategory] = useState(REASON_CATEGORIES[0])
+ const [reason, setReason] = useState('')
+ const [submitting, setSubmitting] = useState(false)
+
+ return (
+
+
+
Escalate to engineering
+
+
+
+
+ )
+}
diff --git a/frontend/src/pages/l1/L1WalkPage.tsx b/frontend/src/pages/l1/L1WalkPage.tsx
index 24007ec7..3144f230 100644
--- a/frontend/src/pages/l1/L1WalkPage.tsx
+++ b/frontend/src/pages/l1/L1WalkPage.tsx
@@ -1,13 +1,68 @@
+import { useEffect, useState } from 'react'
+import { useParams, useNavigate } from 'react-router-dom'
import { PageMeta } from '@/components/common/PageMeta'
+import { l1Api } from '@/api/l1'
+import { L1WalkTreeVariant } from '@/components/l1/L1WalkTreeVariant'
+import type { WalkSession } from '@/types/l1'
export default function L1WalkPage() {
- return (
-
-
-
-
L1 Walk
-
Loading…
+ const { sessionId } = useParams<{ sessionId: string }>()
+ const navigate = useNavigate()
+ const [session, setSession] = useState
(null)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ if (!sessionId) return
+ l1Api.getSession(sessionId)
+ .then(setSession)
+ .catch((err) => {
+ const msg = err?.response?.data?.detail || err?.message || 'Failed to load session'
+ setError(typeof msg === 'string' ? msg : 'Failed to load session')
+ })
+ }, [sessionId])
+
+ if (error) {
+ return (
+
-
+ )
+ }
+ if (!session) {
+ return (
+
+ )
+ }
+
+ const handleDone = () => navigate('/l1')
+
+ // Phase 1: adhoc variant (T23) handles session_kind='adhoc'. Tree variant handles flow/proposal.
+ // For T22, only the tree variant is implemented. Adhoc sessions render a placeholder until T23 lands.
+ if (session.session_kind === 'adhoc') {
+ return (
+
+
+
+ Ad-hoc walker pending (T23).
+
+
+ )
+ }
+
+ return (
+ <>
+
+
+ >
)
}