Files
resolutionflow/frontend/src/components/l1/L1WalkTreeVariant.tsx
Michael Chihlas f483196e91 feat(l1): walker renders AI-built nodes via next-node + disclaimer banner
L1WalkTreeVariant drives ai_build sessions node-by-node through POST /next-node:
fetch first node on mount, render question (yes/no) / instruction (acknowledge),
pass node_text on each advance; terminal nodes (resolved/escalate/needs_review)
hand off to the existing Resolve/Escalate modals. Standing AI disclaimer banner on
ai_build walks. L1WalkPage routes ai_build to the tree variant. Published flow/
proposal keep the Phase-1 stub. tsc -b + eslint clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 20:11:40 -04:00

184 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useState } from 'react'
import type { TreeNode, WalkSession } from '@/types/l1'
import { l1Api } from '@/api/l1'
import { WalkModals } from './WalkModals'
interface Props {
session: WalkSession
}
/**
* L1WalkTreeVariant — renders a decision-tree walk.
*
* For `ai_build` sessions it drives real, node-by-node generation via
* POST /l1/sessions/{id}/next-node: fetch the first node on mount, then on each
* answer/acknowledge POST the current node id (+ its text, so the walked path and
* captured tree stay legible) and render the returned node. Terminal nodes
* (resolved / escalate / needs_review) hand off to the existing Resolve/Escalate
* modals. Published `flow` / `proposal` walks keep the Phase-1 stub for now.
*/
export function L1WalkTreeVariant({ session }: Props) {
const isAiBuild = session.session_kind === 'ai_build'
const [showResolve, setShowResolve] = useState(false)
const [showEscalate, setShowEscalate] = useState(false)
const [node, setNode] = useState<TreeNode | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
// Fetch the first node on mount (ai_build only).
useEffect(() => {
if (!isAiBuild) return
let cancelled = false
setLoading(true)
l1Api
.nextNode(session.id, {})
.then((r) => {
if (!cancelled) setNode(r.node)
})
.catch(() => {
if (!cancelled) setError('Could not generate the next step.')
})
.finally(() => {
if (!cancelled) setLoading(false)
})
return () => {
cancelled = true
}
}, [isAiBuild, session.id])
const advance = useCallback(
async (body: { answer?: 'yes' | 'no'; acknowledged?: boolean }) => {
if (!node) return
setLoading(true)
setError(null)
try {
const r = await l1Api.nextNode(session.id, {
node_id: node.id,
node_text: node.text,
...body,
})
setNode(r.node)
} catch {
setError('Could not generate the next step.')
} finally {
setLoading(false)
}
},
[node, session.id],
)
const isTerminal =
node?.node_type === 'resolved' ||
node?.node_type === 'escalate' ||
node?.node_type === 'needs_review'
return (
<div className="space-y-4">
{isAiBuild && (
<div className="rounded-md border border-warning/30 bg-warning/10 px-4 py-2 text-xs text-warning">
These are high-confidence troubleshooting steps, but they come from outside
your organizations knowledge base review them before acting. When in
doubt, escalate early.
</div>
)}
{!isAiBuild && (
<div className="rounded-lg border border-default bg-card p-6">
<p className="text-sm text-muted-foreground">
Walking flow <span className="font-mono">{session.flow_id}</span>
</p>
<p className="mt-2 text-heading">Synthetic step rendering (Phase 1 stub).</p>
</div>
)}
{isAiBuild && (
<div className="rounded-lg border border-default bg-card p-6 space-y-4">
{loading && (
<p className="text-sm text-muted-foreground">Thinking through the next step</p>
)}
{error && <p className="text-sm text-danger">{error}</p>}
{!loading && node?.node_type === 'question' && (
<>
<p className="text-lg text-heading">{node.text}</p>
<div className="flex gap-3">
<button
onClick={() => advance({ answer: 'yes' })}
className="rounded-md bg-accent px-5 py-2 text-sm text-white"
>
Yes
</button>
<button
onClick={() => advance({ answer: 'no' })}
className="rounded-md border border-default px-5 py-2 text-sm"
>
No
</button>
</div>
</>
)}
{!loading && node?.node_type === 'instruction' && (
<>
<p className="text-lg text-heading">{node.text}</p>
<button
onClick={() => advance({ acknowledged: true })}
className="rounded-md bg-accent px-5 py-2 text-sm text-white"
>
Done next step
</button>
</>
)}
{!loading && isTerminal && node && (
<>
<p className="text-lg text-heading">{node.text}</p>
<div className="flex gap-3">
{node.node_type === 'resolved' ? (
<button
onClick={() => setShowResolve(true)}
className="rounded-md bg-accent px-5 py-2 text-sm text-white"
>
Mark resolved
</button>
) : (
<button
onClick={() => setShowEscalate(true)}
className="rounded-md bg-accent px-5 py-2 text-sm text-white"
>
Escalate to engineering
</button>
)}
</div>
</>
)}
</div>
)}
{/* Resolve / Escalate are always reachable during a walk. */}
<div className="flex gap-3">
<button
onClick={() => setShowResolve(true)}
className="rounded-md border border-default px-4 py-2 text-sm"
>
Resolve
</button>
<button
onClick={() => setShowEscalate(true)}
className="rounded-md border border-default px-4 py-2 text-sm"
>
Escalate
</button>
</div>
<WalkModals
session={session}
showResolve={showResolve}
showEscalate={showEscalate}
onCloseResolve={() => setShowResolve(false)}
onCloseEscalate={() => setShowEscalate(false)}
/>
</div>
)
}