Files
resolutionflow/frontend/src/components/flowpilot/FlowPilotStepCard.tsx
Michael Chihlas 303a558432 refactor: replace hardcoded hex values with Tailwind semantic tokens
3,200+ hardcoded color values replaced with CSS variable-backed
Tailwind classes (bg-card, text-foreground, border-border, etc.).
Enables light mode via CSS variable swap. Only syntax highlighting
colors and intentional one-offs remain hardcoded (~15 values).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 04:34:35 -04:00

230 lines
8.8 KiB
TypeScript

import { useState } from 'react'
import { MessageSquare, Zap, CheckCircle2, SkipForward, ChevronDown, ChevronUp } 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
}
const STEP_TYPE_ICONS = {
question: MessageSquare,
action: Zap,
intake_analysis: MessageSquare,
verification: CheckCircle2,
info_request: MessageSquare,
script_generation: Zap,
note: MessageSquare,
} as const
export function FlowPilotStepCard({ step, isCurrentStep, isProcessing, sessionId, onRespond }: 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>
)
}
// Current active step
return (
<div
className={cn(
'card-flat p-3 sm:p-4 lg:p-5',
isResolutionSuggestion && 'border-emerald-500/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-emerald-500/10 text-emerald-400' : '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-emerald-500/10 border border-emerald-500/20 px-4 py-2.5 text-sm font-medium text-emerald-400 hover:bg-emerald-500/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>
)
}