Implements Phase 1 of the FlowPilot-First pivot — the core AI session experience where engineers describe a problem and FlowPilot guides them through structured diagnosis with selectable options, free-text escape hatches, and auto-generated documentation on resolution. Backend: AISession + AISessionStep models, FlowPilot Engine (LLM orchestration with structured JSON output), Flow Matching Engine v1 (semantic + keyword + recency scoring), 8 API endpoints with auth, rate limiting, and AI quota enforcement. Frontend: Intake screen, conversational session view with sidebar, step cards with options/actions/resolution suggestions, resolve/escalate modals, documentation view with rating, session history integration, and /pilot route with sidebar navigation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
246 lines
9.0 KiB
TypeScript
246 lines
9.0 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 { FlowPilotOptions } from './FlowPilotOptions'
|
|
|
|
interface FlowPilotStepCardProps {
|
|
step: AISessionStepResponse
|
|
isCurrentStep: boolean
|
|
isProcessing: boolean
|
|
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, onRespond }: FlowPilotStepCardProps) {
|
|
const [freeText, setFreeText] = useState('')
|
|
const [showFreeText, setShowFreeText] = useState(false)
|
|
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 handleFreeTextSubmit = () => {
|
|
if (!freeText.trim()) return
|
|
onRespond({ free_text_input: freeText.trim() })
|
|
setFreeText('')
|
|
setShowFreeText(false)
|
|
}
|
|
|
|
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-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-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>
|
|
<p className="text-sm text-foreground">{stepText}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Current active step
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'glass-card-static 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">
|
|
<p className="text-xs text-muted-foreground">{step.context_message}</p>
|
|
</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>
|
|
<p className="text-sm font-medium text-foreground">{stepText}</p>
|
|
{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 gap-2">
|
|
<button
|
|
onClick={() => handleResolutionResponse(true)}
|
|
className="flex-1 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 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}
|
|
/>
|
|
)}
|
|
|
|
{/* Action step buttons */}
|
|
{!isResolutionSuggestion && step.step_type === 'action' && (
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => handleActionComplete(true)}
|
|
className="flex-1 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 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>
|
|
)}
|
|
|
|
{/* Free text escape hatch */}
|
|
{!isResolutionSuggestion && step.allow_free_text && (
|
|
<>
|
|
{!showFreeText ? (
|
|
<button
|
|
onClick={() => setShowFreeText(true)}
|
|
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
None of these — let me describe
|
|
</button>
|
|
) : (
|
|
<div className="space-y-2">
|
|
<textarea
|
|
value={freeText}
|
|
onChange={(e) => setFreeText(e.target.value)}
|
|
placeholder="Describe what you're seeing..."
|
|
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none resize-none"
|
|
rows={3}
|
|
autoFocus
|
|
/>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={handleFreeTextSubmit}
|
|
disabled={!freeText.trim()}
|
|
className="rounded-lg bg-gradient-brand px-4 py-1.5 text-sm font-semibold text-[#101114] hover:opacity-90 disabled:opacity-50 transition-opacity"
|
|
>
|
|
Submit
|
|
</button>
|
|
<button
|
|
onClick={() => { setShowFreeText(false); setFreeText('') }}
|
|
className="rounded-lg px-4 py-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</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>
|
|
)
|
|
}
|