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>
292 lines
11 KiB
TypeScript
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>
|
|
)
|
|
}
|