Files
resolutionflow/frontend/src/components/flowpilot/FlowPilotStepCard.tsx
2026-03-21 16:52:17 -04:00

211 lines
7.9 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 { 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 glass-card-static 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="glass-card-static 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-label 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(
'glass-card-static 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-primary/10 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 && (content.action_type as string) === 'script_generation' && sessionId && (
<InSessionScriptGenerator
templateId={(content.template_id as string) || ''}
preFilledParams={(content.pre_filled_params as Record<string, string>) || {}}
instructions={(content.instructions as string) || stepText}
sessionId={sessionId}
onRespond={onRespond}
/>
)}
{/* Action step buttons */}
{!isResolutionSuggestion && step.step_type === 'action' && (content.action_type as string) !== 'script_generation' && (
<div className="flex flex-col gap-2 sm:flex-row">
<button
onClick={() => handleActionComplete(true)}
className="flex-1 min-h-[44px] rounded-lg bg-primary/10 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-[#5a6170] 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>
)
}