import { useState } from 'react' import { useNavigate } from 'react-router-dom' import { sessionsApi, stepsApi } from '@/api' import { treesApi } from '@/api' import type { Tree, Session, DecisionRecord, TreeStructure, CustomStep, Step } from '@/types' import type { CustomStepDraft } from '@/components/step-library/CustomStepModal' import type { DescendantNode } from '@/components/session' interface UseCustomStepFlowParams { tree: Tree | null session: Session | null currentNodeId: string pathTaken: string[] decisions: DecisionRecord[] notes: string findNode: (nodeId: string, structure?: TreeStructure) => TreeStructure | null setCurrentNodeId: (id: string) => void setPathTaken: (path: string[]) => void setDecisions: (decisions: DecisionRecord[]) => void setNotes: (notes: string) => void setIsCompleting: (completing: boolean) => void setError: (error: string | null) => void isCompleting: boolean } export function useCustomStepFlow({ tree, session, currentNodeId, pathTaken, decisions, notes, findNode, setCurrentNodeId, setPathTaken, setDecisions, setNotes, setIsCompleting, setError, isCompleting, }: UseCustomStepFlowParams) { const navigate = useNavigate() // Custom steps const [customSteps, setCustomSteps] = useState([]) const [showCustomStepModal, setShowCustomStepModal] = useState(false) // Post-step action flow const [showPostStepModal, setShowPostStepModal] = useState(false) const [pendingStep, setPendingStep] = useState(null) const [pendingStepIsFromLibrary, setPendingStepIsFromLibrary] = useState(false) const [isSavingStep, setIsSavingStep] = useState(false) // Continuation flow const [showContinuationModal, setShowContinuationModal] = useState(false) const [branchOriginNodeId, setBranchOriginNodeId] = useState(null) const [pendingContinuationNodeId, setPendingContinuationNodeId] = useState(null) // Custom branch mode const [customBranchMode, setCustomBranchMode] = useState(false) // Fork flow const [showForkModal, setShowForkModal] = useState(false) const findCustomStep = (nodeId: string): CustomStep | null => { return customSteps.find(cs => cs.id === nodeId) || null } // Get descendant nodes two levels deep (grandchildren) from a decision node's options. const getDescendantNodes = (decisionNodeId: string): DescendantNode[] => { const decisionNode = findNode(decisionNodeId, tree?.tree_structure) if (!decisionNode || decisionNode.type !== 'decision' || !decisionNode.options) { return [] } const descendants: DescendantNode[] = [] for (const option of decisionNode.options) { if (!option.next_node_id) continue const childNode = findNode(option.next_node_id, tree?.tree_structure) if (!childNode) continue if (childNode.type === 'decision' && childNode.options) { for (const childOption of childNode.options) { if (!childOption.next_node_id) continue const grandchild = findNode(childOption.next_node_id, tree?.tree_structure) if (!grandchild) continue descendants.push({ id: grandchild.id, label: grandchild.question || grandchild.title || 'Untitled', type: grandchild.type, parentOptionLabel: `${option.label} \u2192 ${childOption.label}` }) } } else if (childNode.type === 'action' && childNode.next_node_id) { const grandchild = findNode(childNode.next_node_id, tree?.tree_structure) if (grandchild) { descendants.push({ id: grandchild.id, label: grandchild.question || grandchild.title || 'Untitled', type: grandchild.type, parentOptionLabel: `${option.label} \u2192 ${childNode.title || 'Action'}` }) } } } return descendants } // Navigate back to a previously-created custom step from the decision node const handleNavigateToCustomStep = (customStep: CustomStep) => { const newPath = [...pathTaken, customStep.id] setPathTaken(newPath) setCurrentNodeId(customStep.id) } // Called when CustomStepModal submits - show action modal instead of inserting directly const handleStepCreated = (step: Step | CustomStepDraft, isFromLibrary: boolean) => { setPendingStep(step) setPendingStepIsFromLibrary(isFromLibrary) setShowCustomStepModal(false) setShowPostStepModal(true) } const resetPendingStep = () => { setShowPostStepModal(false) setPendingStep(null) setPendingStepIsFromLibrary(false) setIsSavingStep(false) } // Save to library only, don't insert into session const handleSaveForLater = async () => { if (!pendingStep) return setIsSavingStep(true) try { if (!pendingStepIsFromLibrary) { await stepsApi.create({ title: pendingStep.title, step_type: pendingStep.step_type, content: pendingStep.content, visibility: 'private', tags: pendingStep.tags || [] }) } resetPendingStep() } catch (err) { console.error('Failed to save step to library:', err) setIsSavingStep(false) } } // Insert into session and show continuation options const handleUseNow = async () => { if (!pendingStep || !session) return setIsSavingStep(true) try { setBranchOriginNodeId(currentNodeId) const customStep: CustomStep = { id: crypto.randomUUID(), inserted_after_node_id: currentNodeId, step_data: pendingStep, timestamp: new Date().toISOString() } const newDecision: DecisionRecord = { node_id: customStep.id, question: null, answer: null, action_performed: `Custom Step: ${pendingStep.title}`, notes: pendingStep.content.instructions || null, automation_used: false, timestamp: new Date().toISOString(), attachments: [] } const newCustomSteps = [...customSteps, customStep] const newDecisions = [...decisions, newDecision] const newPath = [...pathTaken, customStep.id] setCustomSteps(newCustomSteps) setDecisions(newDecisions) setPathTaken(newPath) setCurrentNodeId(customStep.id) await sessionsApi.update(session.id, { path_taken: newPath, decisions: newDecisions, custom_steps: newCustomSteps }) resetPendingStep() setShowContinuationModal(true) } catch (err) { console.error('Failed to insert custom step:', err) setIsSavingStep(false) } } // Save to library AND insert into session const handleBoth = async () => { if (!pendingStep) return setIsSavingStep(true) try { if (!pendingStepIsFromLibrary) { await stepsApi.create({ title: pendingStep.title, step_type: pendingStep.step_type, content: pendingStep.content, visibility: 'private', tags: pendingStep.tags || [] }) } await handleUseNow() } catch (err) { console.error('Failed to save and insert step:', err) setIsSavingStep(false) } } // Handle selecting a descendant node from continuation modal const handleSelectDescendant = (nodeId: string) => { setShowContinuationModal(false) setBranchOriginNodeId(null) setPendingContinuationNodeId(nodeId) } // Navigate to the previously-selected descendant node const handleContinueToDescendant = async () => { if (!pendingContinuationNodeId || !session) return const newPath = [...pathTaken, pendingContinuationNodeId] setPathTaken(newPath) setCurrentNodeId(pendingContinuationNodeId) setNotes('') setPendingContinuationNodeId(null) try { await sessionsApi.update(session.id, { path_taken: newPath }) } catch (err) { console.error('Failed to update session path:', err) } } // Enter custom branch building mode const handleBuildCustomBranch = () => { setShowContinuationModal(false) setCustomBranchMode(true) } // Complete session from custom branch mode const handleCustomBranchComplete = async () => { if (!session) return setIsCompleting(true) setError(null) try { const completionDecision: DecisionRecord = { node_id: currentNodeId, question: null, answer: null, action_performed: 'Custom Branch Completed', notes: notes || 'Issue resolved via custom troubleshooting steps', automation_used: false, timestamp: new Date().toISOString(), attachments: [] } await sessionsApi.update(session.id, { decisions: [...decisions, completionDecision] }) await sessionsApi.complete(session.id) if (customSteps.length > 0) { setShowForkModal(true) } else { navigate(`/sessions/${session.id}`) } } catch (err) { console.error('Failed to complete session:', err) setError('Failed to complete session. Please try again.') } finally { setIsCompleting(false) } } // Fork tree with custom branch const handleForkTree = async (name: string, description: string) => { if (!tree) return try { const forkedTree = await treesApi.create({ name, description, tree_structure: tree.tree_structure, is_public: false }) navigate(`/trees/${forkedTree.id}/edit`) } catch (err) { console.error('Failed to fork tree:', err) throw err } } // Skip forking and go to session detail const handleSkipFork = () => { setShowForkModal(false) navigate(`/sessions/${session!.id}`) } // Initialize custom steps when resuming a session const initCustomSteps = (steps: CustomStep[]) => { setCustomSteps(steps) } return { // State customSteps, showCustomStepModal, showPostStepModal, pendingStep, pendingStepIsFromLibrary, isSavingStep, showContinuationModal, branchOriginNodeId, pendingContinuationNodeId, customBranchMode, showForkModal, // Derived findCustomStep, getDescendantNodes, // Actions setShowCustomStepModal, setShowContinuationModal, setShowForkModal, initCustomSteps, handleNavigateToCustomStep, handleStepCreated, resetPendingStep, handleSaveForLater, handleUseNow, handleBoth, handleSelectDescendant, handleContinueToDescendant, handleBuildCustomBranch, handleCustomBranchComplete, handleForkTree, handleSkipFork, isCompleting, } }