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} + />

) }