Files
resolutionflow/frontend/src/components/flowpilot/FlowPilotStepCard.tsx
Michael Chihlas cef853d7ea refactor: normalize FlowPilot/Assistant/ScriptBuilder to design system tokens
Replace hardcoded Tailwind color utilities with semantic CSS variable tokens
across 31 files in the FlowPilot, Assistant Chat, and Script Builder feature
communities — the areas graphify identified as design-system-free.

- text-blue-400 → text-accent, bg-blue-500/10 → bg-accent-dim, border-blue-500/20 → border-accent/20
- text-amber-400 → text-warning, bg-amber-400/10 → bg-warning-dim, border-l-amber-500 → border-l-warning
- text-rose-400/500 → text-danger, bg-rose-500/10 → bg-danger-dim
- text-emerald-400 → text-success, bg-emerald-500/10 → bg-success-dim, border-l-emerald-500 → border-l-success
- bg-white/[0.08] → bg-elevated (opacity hack → semantic surface token)
- bg-gradient-to-r from-blue-500 to-blue-400 → bg-accent (no gradient surfaces)
- bg-[#60a5fa] → bg-accent (hard-coded hex removed)

Also adds graphify-out/ to .gitignore.

Theme resilience: accent color has changed twice in 5 weeks. Semantic tokens
mean the next change is a 1-line edit in index.css, not 110 grep-and-replace.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 20:20:07 -04:00

292 lines
11 KiB
TypeScript

import { useState } from 'react'
import { MessageSquare, Zap, CheckCircle2, SkipForward, ChevronDown, ChevronUp, GitFork } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { AISessionStepResponse, StepResponseRequest } from '@/types/ai-session'
import { isScriptGenerationAction, isScriptBuilderAction, getActionType } from '@/types/ai-session'
import { MarkdownContent } from '@/components/ui/MarkdownContent'
import { FlowPilotOptions } from './FlowPilotOptions'
import { InSessionScriptGenerator } from './InSessionScriptGenerator'
interface FlowPilotStepCardProps {
step: AISessionStepResponse
isCurrentStep: boolean
isProcessing: boolean
sessionId?: string
onRespond: (response: StepResponseRequest) => void
onBranchSwitch?: (branchId: string) => void
activeBranchId?: string | null
}
const STEP_TYPE_ICONS = {
question: MessageSquare,
action: Zap,
intake_analysis: MessageSquare,
verification: CheckCircle2,
info_request: MessageSquare,
script_generation: Zap,
note: MessageSquare,
fork: GitFork,
} as const
export function FlowPilotStepCard({ step, isCurrentStep, isProcessing, sessionId, onRespond, onBranchSwitch, activeBranchId }: FlowPilotStepCardProps) {
const [isCollapsed, setIsCollapsed] = useState(!isCurrentStep)
const content = step.content as Record<string, unknown>
const stepText = (content.text as string) || ''
const contentType = (content.type as string) || step.step_type
const isResolutionSuggestion = contentType === 'resolution_suggestion'
const Icon = STEP_TYPE_ICONS[step.step_type as keyof typeof STEP_TYPE_ICONS] ?? MessageSquare
const handleOptionSelect = (value: string) => {
onRespond({ selected_option: value })
}
const handleSkip = () => {
onRespond({ was_skipped: true })
}
const handleActionComplete = (success: boolean) => {
onRespond({
action_result: { success, details: '' },
})
}
const handleResolutionResponse = (accepted: boolean) => {
if (accepted) {
// Parent will handle opening the resolve modal
onRespond({ selected_option: 'resolution_accepted' })
} else {
onRespond({ free_text_input: 'No, keep investigating. The issue is not resolved.' })
}
}
// Completed step (collapsed view)
if (!isCurrentStep && isCollapsed) {
return (
<button
onClick={() => setIsCollapsed(false)}
className="w-full text-left card-flat p-3 sm:p-4 opacity-70 hover:opacity-90 transition-opacity"
>
<div className="flex items-center gap-3">
<Icon size={16} className="shrink-0 text-muted-foreground" />
<p className="text-sm text-foreground truncate flex-1">{stepText}</p>
<ChevronDown size={14} className="shrink-0 text-muted-foreground" />
</div>
</button>
)
}
// Expanded completed step
if (!isCurrentStep && !isCollapsed) {
return (
<div className="card-flat p-3 sm:p-4 opacity-80">
<button
onClick={() => setIsCollapsed(true)}
className="mb-2 flex w-full items-center justify-between text-left"
>
<div className="flex items-center gap-2">
<Icon size={16} className="text-muted-foreground" />
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-muted-foreground">
Step {step.step_order + 1}
</span>
</div>
<ChevronUp size={14} className="text-muted-foreground" />
</button>
<MarkdownContent content={stepText} className="text-sm text-foreground" />
</div>
)
}
// Fork step — special rendering with branch options
if (contentType === 'fork') {
const forkReason = (content.fork_reason as string) || stepText
const forkBranches = (content.fork_branches as Array<{ branch_id: string; label: string }>) || []
return (
<div className="card-flat p-3 sm:p-4 lg:p-5 border-accent/30">
{/* Context message */}
{step.context_message && (
<div className="mb-3 rounded-lg bg-primary/5 px-3 py-2 border border-primary/10">
<MarkdownContent content={step.context_message} className="text-xs text-muted-foreground" />
</div>
)}
{/* Fork header */}
<div className="flex items-center gap-2 mb-3">
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-accent-dim">
<GitFork size={14} className="text-accent" />
</span>
<span className="text-[10px] font-semibold uppercase tracking-wider text-accent-text">
Diagnostic Fork
</span>
</div>
{/* Fork reason */}
<MarkdownContent content={forkReason} className="text-sm mb-4" />
{/* Branch options */}
{forkBranches.length > 0 && onBranchSwitch && (
<div className="flex flex-col gap-2">
{forkBranches.map((branch) => {
const isActive = branch.branch_id === activeBranchId
return (
<button
key={branch.branch_id}
onClick={() => onBranchSwitch(branch.branch_id)}
className={cn(
'w-full text-left rounded-[5px] border px-3 py-2.5 transition-colors',
'hover:bg-elevated',
isActive
? 'border-accent bg-accent-dim'
: 'border-default bg-elevated/50'
)}
>
<p className={cn(
'text-sm font-medium',
isActive ? 'text-accent-text' : 'text-heading'
)}>
{branch.label}
</p>
</button>
)
})}
</div>
)}
</div>
)
}
// Current active step
return (
<div
className={cn(
'card-flat p-3 sm:p-4 lg:p-5',
isResolutionSuggestion && 'border-success/30'
)}
>
{/* Context message */}
{step.context_message && (
<div className="mb-3 rounded-lg bg-primary/5 px-3 py-2 border border-primary/10">
<MarkdownContent content={step.context_message} className="text-xs text-muted-foreground" />
</div>
)}
{/* Step content */}
<div className="flex items-start gap-3 mb-4">
<span className={cn(
'mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-lg',
isResolutionSuggestion ? 'bg-success-dim text-success' : 'bg-accent-dim text-primary'
)}>
<Icon size={14} />
</span>
<div className="min-w-0 flex-1">
<MarkdownContent content={stepText} className="text-sm" />
{isResolutionSuggestion && typeof content.resolution_summary === 'string' && (
<p className="mt-2 text-sm text-muted-foreground">
{content.resolution_summary}
</p>
)}
</div>
</div>
{/* Interactive area */}
{isCurrentStep && !isProcessing && (
<div className="space-y-3">
{/* Resolution suggestion buttons */}
{isResolutionSuggestion && (
<div className="flex flex-col gap-2 sm:flex-row">
<button
onClick={() => handleResolutionResponse(true)}
className="flex-1 min-h-[44px] rounded-lg bg-success-dim border border-success/20 px-4 py-2.5 text-sm font-medium text-success hover:bg-success/20 transition-colors"
>
Yes, this is resolved
</button>
<button
onClick={() => handleResolutionResponse(false)}
className="flex-1 min-h-[44px] rounded-lg bg-card/50 border border-border px-4 py-2.5 text-sm font-medium text-foreground hover:bg-card transition-colors"
>
No, keep investigating
</button>
</div>
)}
{/* Options for question steps */}
{!isResolutionSuggestion && step.options.length > 0 && (
<FlowPilotOptions
options={step.options}
onSelect={handleOptionSelect}
disabled={isProcessing}
/>
)}
{/* In-session script generator */}
{!isResolutionSuggestion && isScriptGenerationAction(content) && sessionId && (
<InSessionScriptGenerator
templateId={content.template_id || ''}
preFilledParams={content.pre_filled_params || {}}
instructions={content.instructions || stepText}
sessionId={sessionId}
onRespond={onRespond}
/>
)}
{/* Script Builder handoff */}
{!isResolutionSuggestion && step.step_type === 'action' && isScriptBuilderAction(content) && (
<button
onClick={() => {
sessionStorage.setItem('scriptBuilderContext', JSON.stringify({
from_session: sessionId,
prompt: content.script_prompt || '',
language: content.script_language || 'powershell',
}))
window.open('/script-builder?from=flowpilot', '_blank')
onRespond({ action_result: { success: true, details: 'Opened Script Builder' } })
}}
className="flex-1 min-h-[44px] rounded-lg bg-primary text-white px-4 py-2.5 text-sm font-semibold hover:brightness-110 active:scale-[0.98] transition-all"
>
Open Script Builder
</button>
)}
{/* Action step buttons */}
{!isResolutionSuggestion && step.step_type === 'action' && getActionType(content) !== 'script_generation' && getActionType(content) !== 'open_script_builder' && (
<div className="flex flex-col gap-2 sm:flex-row">
<button
onClick={() => handleActionComplete(true)}
className="flex-1 min-h-[44px] rounded-lg bg-accent-dim border border-primary/20 px-4 py-2.5 text-sm font-medium text-primary hover:bg-primary/20 transition-colors"
>
I've completed this action
</button>
<button
onClick={() => handleActionComplete(false)}
className="flex-1 min-h-[44px] rounded-lg bg-card/50 border border-border px-4 py-2.5 text-sm font-medium text-foreground hover:bg-card transition-colors"
>
This didn't work
</button>
</div>
)}
{/* Skip option */}
{!isResolutionSuggestion && step.allow_skip && (
<button
onClick={handleSkip}
className="flex items-center gap-1.5 text-xs text-text-muted hover:text-muted-foreground transition-colors"
>
<SkipForward size={12} />
I can't check this right now
</button>
)}
</div>
)}
{/* Processing indicator */}
{isProcessing && (
<div className="flex items-center gap-2 pt-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary animate-pulse" />
<span className="text-xs text-muted-foreground">FlowPilot is thinking...</span>
</div>
)}
</div>
)
}