import { useEffect, useState } from 'react' import { useParams, useNavigate, useLocation } from 'react-router-dom' import { treesApi, sessionsApi } from '@/api' import { useTreeNavigationShortcuts } from '@/hooks/useKeyboardShortcuts' import { useCustomStepFlow } from '@/hooks/useCustomStepFlow' import type { Tree, Session, DecisionRecord, TreeStructure } from '@/types' import { cn } from '@/lib/utils' import { MarkdownContent } from '@/components/ui/MarkdownContent' import { CustomStepModal } from '@/components/step-library/CustomStepModal' import { PostStepActionModal, ContinuationModal, ForkTreeModal, ScratchpadSidebar } from '@/components/session' import { Plus, CheckCircle, ArrowRight } from 'lucide-react' interface LocationState { sessionId?: string } export function TreeNavigationPage() { const { id: treeId } = useParams<{ id: string }>() const navigate = useNavigate() const location = useLocation() const locationState = location.state as LocationState | undefined const [tree, setTree] = useState(null) const [session, setSession] = useState(null) const [currentNodeId, setCurrentNodeId] = useState('root') const [pathTaken, setPathTaken] = useState(['root']) const [decisions, setDecisions] = useState([]) const [notes, setNotes] = useState('') const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [isCompleting, setIsCompleting] = useState(false) // Session metadata const [ticketNumber, setTicketNumber] = useState('') const [clientName, setClientName] = useState('') const [showMetadataForm, setShowMetadataForm] = useState(true) // Scratchpad state const [scratchpadOpen, setScratchpadOpen] = useState(() => { return localStorage.getItem('scratchpad-collapsed') === 'false' }) const findNode = (nodeId: string, structure?: TreeStructure): TreeStructure | null => { if (!structure) return null if (structure.id === nodeId) return structure if (structure.children) { for (const child of structure.children) { const found = findNode(nodeId, child) if (found) return found } } return null } // Custom step flow (creation, post-step actions, continuation, branching, forking) const customStepFlow = useCustomStepFlow({ tree, session, currentNodeId, pathTaken, decisions, notes, findNode, setCurrentNodeId, setPathTaken, setDecisions, setNotes, setIsCompleting, setError, isCompleting, }) const handleScratchpadSave = async (content: string) => { if (!session) return await sessionsApi.updateScratchpad(session.id, content) } useEffect(() => { if (treeId) { loadTreeAndSession() } }, [treeId]) const loadTreeAndSession = async () => { setIsLoading(true) setError(null) try { const treeData = await treesApi.get(treeId!) setTree(treeData) // If resuming a session if (locationState?.sessionId) { const sessionData = await sessionsApi.get(locationState.sessionId) setSession(sessionData) setPathTaken(sessionData.path_taken) setCurrentNodeId(sessionData.path_taken[sessionData.path_taken.length - 1] || 'root') setDecisions(sessionData.decisions as DecisionRecord[]) customStepFlow.initCustomSteps(sessionData.custom_steps || []) setTicketNumber(sessionData.ticket_number || '') setClientName(sessionData.client_name || '') setShowMetadataForm(false) } } catch (err) { setError('Failed to load tree') console.error(err) } finally { setIsLoading(false) } } const startSession = async () => { if (!tree) return setIsLoading(true) try { const newSession = await sessionsApi.create({ tree_id: tree.id, ticket_number: ticketNumber || undefined, client_name: clientName || undefined, }) setSession(newSession) setShowMetadataForm(false) } catch (err) { setError('Failed to start session') console.error(err) } finally { setIsLoading(false) } } const handleSelectOption = async (_optionId: string, optionLabel: string, nextNodeId: string) => { if (!session || !tree) return const node = findNode(currentNodeId, tree.tree_structure) if (!node) return const newDecision: DecisionRecord = { node_id: currentNodeId, question: node.question || null, answer: optionLabel, action_performed: null, notes: notes || null, automation_used: false, timestamp: new Date().toISOString(), attachments: [], } const newPath = [...pathTaken, nextNodeId] const newDecisions = [...decisions, newDecision] setPathTaken(newPath) setDecisions(newDecisions) setCurrentNodeId(nextNodeId) setNotes('') try { await sessionsApi.update(session.id, { path_taken: newPath, decisions: newDecisions, }) } catch (err) { console.error('Failed to update session:', err) } } const handleContinue = async (actionPerformed?: string) => { if (!session || !tree) return const node = findNode(currentNodeId, tree.tree_structure) if (!node || !node.next_node_id) return const newDecision: DecisionRecord = { node_id: currentNodeId, question: null, answer: null, action_performed: actionPerformed || node.title || 'Action completed', notes: notes || null, automation_used: false, timestamp: new Date().toISOString(), attachments: [], } const newPath = [...pathTaken, node.next_node_id] const newDecisions = [...decisions, newDecision] setPathTaken(newPath) setDecisions(newDecisions) setCurrentNodeId(node.next_node_id) setNotes('') try { await sessionsApi.update(session.id, { path_taken: newPath, decisions: newDecisions, }) } catch (err) { console.error('Failed to update session:', err) } } const handleComplete = async () => { if (!session || !tree) return setIsCompleting(true) setError(null) try { const node = findNode(currentNodeId, tree.tree_structure) if (node) { const finalDecision: DecisionRecord = { node_id: currentNodeId, question: null, answer: null, action_performed: node.title || 'Session completed', notes: notes || null, automation_used: false, timestamp: new Date().toISOString(), attachments: [], } await sessionsApi.update(session.id, { decisions: [...decisions, finalDecision], }) } await sessionsApi.complete(session.id) navigate(`/sessions/${session.id}`) } catch (err) { console.error('Failed to complete session:', err) setError('Failed to complete session. Check console for details.') } finally { setIsCompleting(false) } } const handleGoBack = () => { if (pathTaken.length <= 1) return const newPath = pathTaken.slice(0, -1) const newDecisions = decisions.slice(0, -1) setPathTaken(newPath) setDecisions(newDecisions) setCurrentNodeId(newPath[newPath.length - 1]) } // Compute current node for keyboard shortcuts (must be before any returns for hooks rules) const currentNode = tree ? findNode(currentNodeId, tree.tree_structure) : null const currentCustomStep = customStepFlow.findCustomStep(currentNodeId) const currentOptions = currentNode?.options || [] // Keyboard shortcuts - must be called unconditionally (React hooks rules) useTreeNavigationShortcuts({ onSelectOption: (index) => { const option = currentOptions[index] if (option && session && tree) { handleSelectOption(option.id, option.label, option.next_node_id) } }, onGoBack: handleGoBack, onContinue: () => { if (currentNode?.type === 'action' && currentNode.next_node_id) { handleContinue() } else if (currentNode?.type === 'solution') { handleComplete() } }, optionCount: currentOptions.length, canGoBack: pathTaken.length > 1 && !showMetadataForm && !isLoading, canContinue: !showMetadataForm && !isLoading && (currentNode?.type === 'action' || currentNode?.type === 'solution'), }) if (isLoading) { return (
) } if (error || !tree) { return (
{error || 'Tree not found'}
) } // Session metadata form if (showMetadataForm) { return (

{tree.name}

{tree.description}

Session Details

Optional: Add ticket and client info for easier tracking

setTicketNumber(e.target.value)} placeholder="e.g., INC0012345" className={cn( 'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2', 'text-foreground placeholder:text-muted-foreground', 'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary' )} />
setClientName(e.target.value)} placeholder="e.g., Acme Corp" className={cn( 'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2', 'text-foreground placeholder:text-muted-foreground', 'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary' )} />
) } if (!currentNode && !currentCustomStep) { return (
Invalid tree structure
) } return (
{/* Main Content */}
{/* Header */}

{tree.name}

{(ticketNumber || clientName) && (

{ticketNumber && `Ticket: ${ticketNumber}`} {ticketNumber && clientName && ' · '} {clientName && `Client: ${clientName}`}

)}
{/* Breadcrumb */}
{pathTaken.map((nodeId, index) => { const node = findNode(nodeId, tree?.tree_structure) const customStep = customStepFlow.findCustomStep(nodeId) const label = node?.question || node?.title || customStep?.step_data.title || nodeId return ( {index > 0 && } {label.length > 30 ? `${label.slice(0, 30)}...` : label} ) })}
{/* Current Node */}
{/* Decision Node */} {currentNode && currentNode.type === 'decision' && ( <>

{currentNode.question}

{currentNode.help_text && (
)}
{currentNode.options?.map((option, index) => ( ))}
{/* Previously-created custom steps at this node */} {customStepFlow.customSteps.filter(cs => cs.inserted_after_node_id === currentNodeId).length > 0 && (

Your Custom Steps

{customStepFlow.customSteps .filter(cs => cs.inserted_after_node_id === currentNodeId) .map(cs => ( ))}
)} {/* Add Custom Step Button */} )} {/* Custom Step Node */} {currentCustomStep && (
{/* Custom Step Badge */} Custom Step

{currentCustomStep.step_data.title}

{currentCustomStep.step_data.content.instructions && (
)} {currentCustomStep.step_data.content.help_text && (
)} {currentCustomStep.step_data.content.commands && currentCustomStep.step_data.content.commands.length > 0 && (

Commands:

{currentCustomStep.step_data.content.commands.map((cmd, index) => (

{cmd.label}

{cmd.command}
))}
)} {/* Continue to selected descendant */} {customStepFlow.pendingContinuationNodeId && !customStepFlow.customBranchMode && (() => { const targetNode = findNode(customStepFlow.pendingContinuationNodeId, tree?.tree_structure) const targetLabel = targetNode?.question || targetNode?.title || 'next step' return (
) })()} {/* Custom Branch Controls */} {customStepFlow.customBranchMode && (

Building custom branch - add steps until the issue is resolved

)}
)} {/* Action Node */} {currentNode && currentNode.type === 'action' && ( <>

{currentNode.title}

{currentNode.description && (
)} {currentNode.commands && currentNode.commands.length > 0 && (

Commands:

{currentNode.commands.map((cmd, index) => ( {cmd} ))}
)} {currentNode.expected_outcome && (

Expected outcome: {currentNode.expected_outcome}

)} {currentNode.next_node_id && ( )} )} {/* Solution Node */} {currentNode && currentNode.type === 'solution' && ( <>
Solution

{currentNode.title}

{currentNode.description && (
)} {currentNode.resolution_steps && currentNode.resolution_steps.length > 0 && (

Resolution steps:

    {currentNode.resolution_steps.map((step, index) => (
  1. {step}
  2. ))}
)} )} {/* Notes */}