feat: create FallbackSteps UI component (Task 17)
Collapsible component supporting edit and execute modes. Edit mode provides title/description inputs with add/remove controls. Execute mode shows "This worked" / "Didn't help" action buttons with emerald/ rose styling. Amber accent styling throughout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
153
frontend/src/components/procedural/FallbackSteps.tsx
Normal file
153
frontend/src/components/procedural/FallbackSteps.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { useState } from 'react'
|
||||
import { AlertCircle, ChevronDown, ChevronRight, Plus, Trash2, Check, X } from 'lucide-react'
|
||||
import type { ProceduralStep } from '@/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface FallbackStepsProps {
|
||||
fallbackSteps: ProceduralStep[]
|
||||
mode: 'edit' | 'execute'
|
||||
// Edit mode
|
||||
onAdd?: () => void
|
||||
onRemove?: (index: number) => void
|
||||
onUpdate?: (index: number, updates: Partial<ProceduralStep>) => void
|
||||
// Execute mode
|
||||
onComplete?: (stepId: string, notes: string | null, outcome: 'resolved' | 'not_resolved' | 'skipped') => void
|
||||
completedIds?: Set<string>
|
||||
}
|
||||
|
||||
export function FallbackSteps({
|
||||
fallbackSteps,
|
||||
mode,
|
||||
onAdd,
|
||||
onRemove,
|
||||
onUpdate,
|
||||
onComplete,
|
||||
completedIds,
|
||||
}: FallbackStepsProps) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
// In execute mode, hide if no fallback steps
|
||||
if (mode === 'execute' && fallbackSteps.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const toggleLabel =
|
||||
mode === 'execute'
|
||||
? "Didn't work?"
|
||||
: `Fallback branches (${fallbackSteps.length})`
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
{/* Toggle button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-amber-400/80 transition-colors"
|
||||
>
|
||||
<AlertCircle className="h-4 w-4 text-amber-400/80 shrink-0" />
|
||||
<span>{toggleLabel}</span>
|
||||
{expanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="mt-3 border-l-2 border-amber-400/20 pl-4">
|
||||
<div className="space-y-2">
|
||||
{fallbackSteps.map((fbStep, index) => {
|
||||
const isCompleted = completedIds?.has(fbStep.id)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={fbStep.id}
|
||||
className={cn(
|
||||
'rounded-lg border p-3 transition-colors',
|
||||
'bg-white/[0.02] border-border/50',
|
||||
isCompleted && 'border-emerald-500/30 bg-emerald-500/5'
|
||||
)}
|
||||
>
|
||||
{mode === 'edit' ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={fbStep.title}
|
||||
onChange={(e) => onUpdate?.(index, { title: e.target.value })}
|
||||
placeholder="Fallback step title"
|
||||
className="flex-1 rounded border border-border bg-card px-2.5 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRemove?.(index)}
|
||||
className="shrink-0 rounded p-1.5 text-muted-foreground hover:bg-rose-500/10 hover:text-rose-400 transition-colors"
|
||||
title="Remove fallback step"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
value={fbStep.description || ''}
|
||||
onChange={(e) =>
|
||||
onUpdate?.(index, { description: e.target.value || undefined })
|
||||
}
|
||||
placeholder="Describe this alternative approach..."
|
||||
rows={2}
|
||||
className="w-full rounded border border-border bg-card px-2.5 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// Execute mode
|
||||
<div>
|
||||
<p className={cn('text-sm font-medium', isCompleted ? 'text-emerald-400' : 'text-foreground')}>
|
||||
{fbStep.title}
|
||||
</p>
|
||||
{fbStep.description && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">{fbStep.description}</p>
|
||||
)}
|
||||
{!isCompleted && (
|
||||
<div className="mt-3 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onComplete?.(fbStep.id, null, 'resolved')}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-emerald-500/30 bg-emerald-500/10 px-3 py-1.5 text-xs font-medium text-emerald-400 hover:bg-emerald-500/20 transition-colors"
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
This worked
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onComplete?.(fbStep.id, null, 'not_resolved')}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-rose-500/30 bg-rose-500/10 px-3 py-1.5 text-xs font-medium text-rose-400 hover:bg-rose-500/20 transition-colors"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
Didn't help
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{isCompleted && (
|
||||
<p className="mt-2 text-xs text-emerald-400/70">Resolved via this fallback</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{mode === 'edit' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAdd}
|
||||
className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-dashed border-amber-400/20 px-3 py-2 text-xs text-amber-400/60 hover:border-amber-400/40 hover:text-amber-400/80 transition-colors"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
Add fallback step
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user