feat: integrate FallbackSteps into editor and session runner (Task 18)

Wire FallbackSteps edit mode into StepEditor for procedure_step type
with add/remove/update handlers using crypto.randomUUID(). Add execute
mode rendering in ProceduralNavigationPage with fallbackDecisions state
tracking per parent step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-16 01:13:46 -04:00
parent 7359ec8222
commit 06229a8f69
2 changed files with 64 additions and 1 deletions

View File

@@ -2,6 +2,7 @@ import { useState } from 'react'
import { ChevronUp, ChevronDown, AlertTriangle, Clock, ExternalLink, CheckSquare, Terminal, Settings2 } from 'lucide-react'
import type { ProceduralStep, StepContentType, IntakeFormField } from '@/types'
import { cn } from '@/lib/utils'
import { FallbackSteps } from '@/components/procedural/FallbackSteps'
const CONTENT_TYPE_OPTIONS: { value: StepContentType; label: string; color: string }[] = [
{ value: 'action', label: 'Action', color: 'text-blue-400' },
@@ -278,6 +279,32 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa
</div>}
</div>
)}
{/* Fallback Steps — procedure_step only */}
{step.type === 'procedure_step' && (
<FallbackSteps
fallbackSteps={step.fallback_steps ?? []}
mode="edit"
onAdd={() => {
const newFallback: ProceduralStep = {
id: crypto.randomUUID(),
type: 'procedure_step',
title: '',
}
onUpdate({ fallback_steps: [...(step.fallback_steps ?? []), newFallback] })
}}
onRemove={(index) => {
const updated = (step.fallback_steps ?? []).filter((_, i) => i !== index)
onUpdate({ fallback_steps: updated.length > 0 ? updated : undefined })
}}
onUpdate={(index, updates) => {
const updated = (step.fallback_steps ?? []).map((fb, i) =>
i === index ? { ...fb, ...updates } : fb
)
onUpdate({ fallback_steps: updated })
}}
/>
)}
</div>
</div>
)

View File

@@ -5,7 +5,8 @@ import { treesApi } from '@/api/trees'
import { sessionsApi } from '@/api/sessions'
import { stepsApi } from '@/api/steps'
import type { Tree, Session, ProceduralStep, DecisionRecord, RuntimeStep, CustomProceduralStep, IntakeFormField } from '@/types'
import type { CustomStep } from '@/types/session'
import type { CustomStep, FallbackStepRecord } from '@/types/session'
import { FallbackSteps } from '@/components/procedural/FallbackSteps'
import type { Step } from '@/types/step'
import { StepChecklist } from '@/components/procedural/StepChecklist'
import { StepDetail } from '@/components/procedural/StepDetail'
@@ -84,6 +85,9 @@ export function ProceduralNavigationPage() {
const [batchProgress, setBatchProgress] = useState<{ completed: number; total: number } | null>(null)
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
// Fallback step decisions
const [fallbackDecisions, setFallbackDecisions] = useState<FallbackStepRecord[]>([])
// Custom step state
const [runtimeSteps, setRuntimeSteps] = useState<RuntimeStep[]>([])
const [sessionCustomSteps, setSessionCustomSteps] = useState<CustomStep[]>([])
@@ -434,6 +438,22 @@ export function ProceduralNavigationPage() {
setShowCsatModal(false)
}
const handleFallbackComplete = (
parentStepId: string,
fallbackStepId: string,
notes: string | null,
outcome: 'resolved' | 'not_resolved' | 'skipped'
) => {
const record: FallbackStepRecord = {
parent_step_id: parentStepId,
fallback_step_id: fallbackStepId,
completed_at: new Date().toISOString(),
notes,
outcome,
}
setFallbackDecisions((prev) => [...prev, record])
}
const handleStepCreated = (step: Step | CustomStepDraft, isFromLibrary: boolean) => {
setPendingCustomStep(step)
setPendingIsFromLibrary(isFromLibrary)
@@ -734,6 +754,22 @@ export function ProceduralNavigationPage() {
/>
)}
{/* Fallback steps — shown when step has fallback alternatives */}
{currentStep && !('isCustom' in currentStep && currentStep.isCustom) && 'fallback_steps' in currentStep && (
<FallbackSteps
fallbackSteps={(currentStep as ProceduralStep).fallback_steps ?? []}
mode="execute"
completedIds={new Set(
fallbackDecisions
.filter((d) => d.parent_step_id === currentStep.id && d.outcome === 'resolved')
.map((d) => d.fallback_step_id)
)}
onComplete={(fallbackStepId, notes, outcome) =>
handleFallbackComplete(currentStep.id, fallbackStepId, notes, outcome)
}
/>
)}
{/* Add custom step — only on current active incomplete non-custom step */}
{currentStep && !completedStepIds.has(currentStep.id) && !('isCustom' in currentStep && currentStep.isCustom) && (
<div className="mt-4">