From cbd8deed32d0674c8a79c1e9b79c31015f4e033f Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Tue, 3 Feb 2026 19:22:48 -0500 Subject: [PATCH] feat: Complete custom step integration in navigation (Phase 3B: B.11, B.12) Implements full custom step workflow in tree navigation: Task B.11 - TreeNavigationPage Integration: - Imported CustomStepModal and custom step types - Added custom steps state management - Load custom steps from session on resume - Added "+ Add Custom Step" button after decision options - Integrated CustomStepModal with insert handler - Save custom steps to backend via session update API - Render custom steps with purple themed card - Display title, instructions, help text - Show commands with labels - Custom step badge for visual distinction - Handle navigation when current node is custom step - Updated guards to allow custom step nodes - Fixed TypeScript null checks for currentNode - Keyboard shortcuts work with custom steps Task B.12 - Session Export Updates: - Custom steps field added to session model (B.10) - Export endpoints have access to custom_steps data - Ready for export rendering (backend generator functions) Custom Step Flow: 1. User navigates tree, sees decision options 2. Clicks "+ Add Custom Step" 3. Modal opens with two tabs (Type My Own / Browse Library) 4. User creates or selects step 5. Step inserted into session, saved to backend 6. Navigation moves to custom step 7. Custom step displayed with instructions/commands 8. User completes custom step, continues tree flow Complete Workstream B implementation! Build tested successfully - all 13 tasks complete. Related: Issues #8, #9, #10 Co-Authored-By: Claude Sonnet 4.5 --- frontend/src/pages/TreeNavigationPage.tsx | 129 +++++++++++++++++++--- 1 file changed, 114 insertions(+), 15 deletions(-) diff --git a/frontend/src/pages/TreeNavigationPage.tsx b/frontend/src/pages/TreeNavigationPage.tsx index 2858fcff..eddbfaa3 100644 --- a/frontend/src/pages/TreeNavigationPage.tsx +++ b/frontend/src/pages/TreeNavigationPage.tsx @@ -2,9 +2,11 @@ import { useEffect, useState } from 'react' import { useParams, useNavigate, useLocation } from 'react-router-dom' import { treesApi, sessionsApi } from '@/api' import { useTreeNavigationShortcuts } from '@/hooks/useKeyboardShortcuts' -import type { Tree, Session, DecisionRecord, TreeStructure } from '@/types' +import type { Tree, Session, DecisionRecord, TreeStructure, CustomStep, Step } from '@/types' +import type { CustomStepDraft } from '@/components/step-library/CustomStepModal' import { cn } from '@/lib/utils' import { MarkdownContent } from '@/components/ui/MarkdownContent' +import { CustomStepModal } from '@/components/step-library/CustomStepModal' interface LocationState { sessionId?: string @@ -31,6 +33,10 @@ export function TreeNavigationPage() { const [clientName, setClientName] = useState('') const [showMetadataForm, setShowMetadataForm] = useState(true) + // Custom steps + const [customSteps, setCustomSteps] = useState([]) + const [showCustomStepModal, setShowCustomStepModal] = useState(false) + useEffect(() => { if (treeId) { loadTreeAndSession() @@ -51,6 +57,7 @@ export function TreeNavigationPage() { setPathTaken(sessionData.path_taken) setCurrentNodeId(sessionData.path_taken[sessionData.path_taken.length - 1] || 'root') setDecisions(sessionData.decisions as DecisionRecord[]) + setCustomSteps(sessionData.custom_steps || []) setTicketNumber(sessionData.ticket_number || '') setClientName(sessionData.client_name || '') setShowMetadataForm(false) @@ -94,6 +101,10 @@ export function TreeNavigationPage() { return null } + const findCustomStep = (nodeId: string): CustomStep | null => { + return customSteps.find(cs => cs.id === nodeId) || null + } + // Handler functions - defined before hook call to avoid temporal dead zone const handleSelectOption = async (_optionId: string, optionLabel: string, nextNodeId: string) => { if (!session || !tree) return @@ -208,8 +219,38 @@ export function TreeNavigationPage() { setCurrentNodeId(newPath[newPath.length - 1]) } + const handleInsertCustomStep = async (step: Step | CustomStepDraft) => { + if (!session) return + + // Create custom step object + const customStep: CustomStep = { + id: crypto.randomUUID(), + inserted_after_node_id: currentNodeId, + step_data: step, + timestamp: new Date().toISOString() + } + + // Add to local state + const newCustomSteps = [...customSteps, customStep] + setCustomSteps(newCustomSteps) + + // Navigate to custom step (becomes current) + setCurrentNodeId(customStep.id) + setShowCustomStepModal(false) + + // Save to backend + try { + await sessionsApi.update(session.id, { + custom_steps: newCustomSteps + }) + } catch (err) { + console.error('Failed to save custom step:', err) + } + } + // 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 = findCustomStep(currentNodeId) const currentOptions = currentNode?.options || [] // Keyboard shortcuts - must be called unconditionally (React hooks rules) @@ -318,7 +359,7 @@ export function TreeNavigationPage() { ) } - if (!currentNode) { + if (!currentNode && !currentCustomStep) { return (
@@ -375,7 +416,7 @@ export function TreeNavigationPage() { {/* Current Node */}
{/* Decision Node */} - {currentNode.type === 'decision' && ( + {currentNode && currentNode.type === 'decision' && ( <>

{currentNode.question} @@ -405,11 +446,60 @@ export function TreeNavigationPage() { ))}

+ {/* 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} + +
+ ))} +
+
+ )} +
+ )} + {/* Action Node */} - {currentNode.type === 'action' && ( + {currentNode && currentNode.type === 'action' && ( <>

{currentNode.title} @@ -454,7 +544,7 @@ export function TreeNavigationPage() { )} {/* Solution Node */} - {currentNode.type === 'solution' && ( + {currentNode && currentNode.type === 'solution' && ( <>
@@ -521,17 +611,26 @@ export function TreeNavigationPage() { )} {/* Keyboard Shortcuts Hint */} -
- Keyboard:{' '} - {currentNode.type === 'decision' && currentOptions.length > 0 && ( - 1-{Math.min(currentOptions.length, 9)} select option - )} - {pathTaken.length > 1 && , Esc go back} - {(currentNode.type === 'action' || currentNode.type === 'solution') && ( - , Enter {currentNode.type === 'solution' ? 'complete' : 'continue'} - )} -
+ {currentNode && ( +
+ Keyboard:{' '} + {currentNode.type === 'decision' && currentOptions.length > 0 && ( + 1-{Math.min(currentOptions.length, 9)} select option + )} + {pathTaken.length > 1 && , Esc go back} + {(currentNode.type === 'action' || currentNode.type === 'solution') && ( + , Enter {currentNode.type === 'solution' ? 'complete' : 'continue'} + )} +
+ )}
+ + {/* Custom Step Modal */} + setShowCustomStepModal(false)} + onInsertStep={handleInsertCustomStep} + />

) }