feat(ai-session): add FlowPilot AI-powered troubleshooting sessions

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>
This commit is contained in:
2026-03-18 14:27:36 +00:00
parent 44eb48e457
commit 5494816b06
29 changed files with 3647 additions and 5 deletions

View File

@@ -0,0 +1,60 @@
import { Link } from 'react-router-dom'
import { Clock, CheckCircle2, ArrowUpRight, AlertCircle, Pause } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { AISessionSummary } from '@/types/ai-session'
interface AISessionListItemProps {
session: AISessionSummary
}
const STATUS_CONFIG = {
active: { icon: Clock, color: 'text-primary', label: 'Active' },
paused: { icon: Pause, color: 'text-amber-400', label: 'Paused' },
resolved: { icon: CheckCircle2, color: 'text-emerald-400', label: 'Resolved' },
escalated: { icon: ArrowUpRight, color: 'text-amber-400', label: 'Escalated' },
abandoned: { icon: AlertCircle, color: 'text-[#5a6170]', label: 'Abandoned' },
} as const
export function AISessionListItem({ session }: AISessionListItemProps) {
const config = STATUS_CONFIG[session.status as keyof typeof STATUS_CONFIG] ?? STATUS_CONFIG.active
const StatusIcon = config.icon
return (
<Link
to={`/pilot/${session.id}`}
className="glass-card block p-4 transition-all"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">
{session.problem_summary || 'Untitled session'}
</p>
<div className="mt-1.5 flex items-center gap-3 flex-wrap">
{session.problem_domain && (
<span className="font-label rounded-md bg-primary/10 px-2 py-0.5 text-[0.625rem] uppercase tracking-wider text-primary">
{session.problem_domain}
</span>
)}
<span className={cn('flex items-center gap-1 text-xs', config.color)}>
<StatusIcon size={12} />
{config.label}
</span>
<span className="text-xs text-muted-foreground">
{session.step_count} steps
</span>
<span className="text-xs text-[#5a6170]">
{new Date(session.created_at).toLocaleDateString(undefined, {
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
})}
</span>
</div>
</div>
{session.session_rating && (
<span className="font-label text-xs text-amber-400">
{'★'.repeat(session.session_rating)}
</span>
)}
</div>
</Link>
)
}

View File

@@ -0,0 +1,44 @@
import { cn } from '@/lib/utils'
interface ConfidenceIndicatorProps {
tier: string
score: number
className?: string
}
const TIER_CONFIG = {
guided: {
color: 'bg-emerald-400',
label: 'Proven path',
description: 'FlowPilot is following a known resolution path with high confidence.',
},
exploring: {
color: 'bg-amber-400',
label: 'Investigating',
description: 'FlowPilot is narrowing down the issue based on your responses.',
},
discovery: {
color: 'bg-violet-400',
label: 'New territory',
description: 'FlowPilot is exploring this issue — your responses help build the knowledge base.',
},
} as const
export function ConfidenceIndicator({ tier, score, className }: ConfidenceIndicatorProps) {
const config = TIER_CONFIG[tier as keyof typeof TIER_CONFIG] ?? TIER_CONFIG.discovery
return (
<div className={cn('group relative inline-flex items-center gap-2', className)}>
<span className={cn('h-2 w-2 rounded-full', config.color)} />
<span className="font-label text-xs text-muted-foreground">{config.label}</span>
{/* Tooltip */}
<div className="pointer-events-none absolute left-0 top-full z-50 mt-2 w-56 rounded-lg border border-border bg-card p-3 opacity-0 shadow-lg transition-opacity group-hover:pointer-events-auto group-hover:opacity-100">
<p className="text-xs text-muted-foreground">{config.description}</p>
<p className="mt-1 font-label text-[0.625rem] text-[#5a6170]">
Confidence: {Math.round(score * 100)}%
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,140 @@
import { useState } from 'react'
import { CheckCircle2, ArrowUpRight } from 'lucide-react'
import type { ResolveSessionRequest, EscalateSessionRequest, SessionDocumentation } from '@/types/ai-session'
interface FlowPilotActionBarProps {
canResolve: boolean
canEscalate: boolean
isProcessing: boolean
onResolve: (data: ResolveSessionRequest) => Promise<SessionDocumentation>
onEscalate: (data: EscalateSessionRequest) => Promise<SessionDocumentation>
}
export function FlowPilotActionBar({
canResolve,
canEscalate,
isProcessing,
onResolve,
onEscalate,
}: FlowPilotActionBarProps) {
const [showResolve, setShowResolve] = useState(false)
const [showEscalate, setShowEscalate] = useState(false)
const [resolutionSummary, setResolutionSummary] = useState('')
const [escalationReason, setEscalationReason] = useState('')
const [submitting, setSubmitting] = useState(false)
const handleResolve = async () => {
if (!resolutionSummary.trim() || resolutionSummary.length < 5) return
setSubmitting(true)
try {
await onResolve({ resolution_summary: resolutionSummary })
setShowResolve(false)
} finally {
setSubmitting(false)
}
}
const handleEscalate = async () => {
if (!escalationReason.trim() || escalationReason.length < 5) return
setSubmitting(true)
try {
await onEscalate({ escalation_reason: escalationReason })
setShowEscalate(false)
} finally {
setSubmitting(false)
}
}
return (
<>
{/* Bottom bar */}
<div
className="flex items-center gap-3 border-t px-5 py-3"
style={{ borderColor: 'var(--glass-border)', background: 'rgba(16, 17, 20, 0.8)', backdropFilter: 'blur(12px)' }}
>
<button
onClick={() => { setShowResolve(true); setShowEscalate(false) }}
disabled={!canResolve || isProcessing}
className="flex items-center gap-2 rounded-lg bg-emerald-500/10 border border-emerald-500/20 px-4 py-2 text-sm font-medium text-emerald-400 hover:bg-emerald-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
>
<CheckCircle2 size={16} />
Resolve
</button>
<button
onClick={() => { setShowEscalate(true); setShowResolve(false) }}
disabled={!canEscalate || isProcessing}
className="flex items-center gap-2 rounded-lg bg-amber-500/10 border border-amber-500/20 px-4 py-2 text-sm font-medium text-amber-400 hover:bg-amber-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
>
<ArrowUpRight size={16} />
Escalate
</button>
</div>
{/* Resolve modal */}
{showResolve && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="glass-card-static w-full max-w-lg p-6">
<h3 className="font-heading text-lg font-semibold text-foreground mb-1">Resolve Session</h3>
<p className="text-sm text-muted-foreground mb-4">Summarize what fixed the issue. This will be included in the auto-generated documentation.</p>
<textarea
value={resolutionSummary}
onChange={(e) => setResolutionSummary(e.target.value)}
placeholder="What resolved the issue?"
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={4}
autoFocus
/>
<div className="mt-4 flex justify-end gap-2">
<button
onClick={() => setShowResolve(false)}
className="rounded-lg px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Cancel
</button>
<button
onClick={handleResolve}
disabled={resolutionSummary.length < 5 || submitting}
className="rounded-lg bg-emerald-500/20 border border-emerald-500/30 px-4 py-2 text-sm font-medium text-emerald-400 hover:bg-emerald-500/30 disabled:opacity-50 transition-colors"
>
{submitting ? 'Resolving...' : 'Resolve Session'}
</button>
</div>
</div>
</div>
)}
{/* Escalate modal */}
{showEscalate && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="glass-card-static w-full max-w-lg p-6">
<h3 className="font-heading text-lg font-semibold text-foreground mb-1">Escalate Session</h3>
<p className="text-sm text-muted-foreground mb-4">Explain why this needs escalation. FlowPilot will package the context for the next engineer.</p>
<textarea
value={escalationReason}
onChange={(e) => setEscalationReason(e.target.value)}
placeholder="Why does this need to be escalated?"
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={4}
autoFocus
/>
<div className="mt-4 flex justify-end gap-2">
<button
onClick={() => setShowEscalate(false)}
className="rounded-lg px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Cancel
</button>
<button
onClick={handleEscalate}
disabled={escalationReason.length < 5 || submitting}
className="rounded-lg bg-amber-500/20 border border-amber-500/30 px-4 py-2 text-sm font-medium text-amber-400 hover:bg-amber-500/30 disabled:opacity-50 transition-colors"
>
{submitting ? 'Escalating...' : 'Escalate Session'}
</button>
</div>
</div>
</div>
)}
</>
)
}

View File

@@ -0,0 +1,119 @@
import { useState } from 'react'
import { Sparkles, FileText, Terminal } from 'lucide-react'
import type { AISessionCreateRequest } from '@/types/ai-session'
interface FlowPilotIntakeProps {
onSubmit: (request: AISessionCreateRequest) => void
isLoading: boolean
}
export function FlowPilotIntake({ onSubmit, isLoading }: FlowPilotIntakeProps) {
const [text, setText] = useState('')
const [showLogs, setShowLogs] = useState(false)
const [logContent, setLogContent] = useState('')
const handleSubmit = () => {
if (!text.trim() && !logContent.trim()) return
const intake_content: Record<string, unknown> = {}
if (text.trim()) intake_content.text = text.trim()
if (logContent.trim()) intake_content.log_content = logContent.trim()
const intake_type = logContent.trim()
? text.trim() ? 'combined' : 'log_paste'
: 'free_text'
onSubmit({ intake_type, intake_content })
}
const hasContent = text.trim() || logContent.trim()
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[50vh]">
<div className="text-center">
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/10">
<Sparkles size={24} className="text-primary animate-pulse" />
</div>
<p className="text-sm font-medium text-foreground">Analyzing your issue...</p>
<p className="mt-1 text-xs text-muted-foreground">FlowPilot is classifying the problem and searching for relevant flows</p>
</div>
</div>
)
}
return (
<div className="flex items-start justify-center pt-[10vh]">
<div className="w-full max-w-2xl">
<div className="text-center mb-6">
<h1 className="font-heading text-2xl font-bold tracking-tight text-foreground">
What are you troubleshooting?
</h1>
<p className="mt-2 text-sm text-muted-foreground">
Describe the issue, paste an error message, or paste log output
</p>
</div>
<div className="glass-card-static p-5 space-y-4">
{/* Main text area */}
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="e.g. User can't access shared drive after password reset, getting 'Access Denied' in Event Viewer..."
className="w-full rounded-lg border border-border bg-card px-4 py-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none resize-none"
rows={5}
autoFocus
/>
{/* Input type toggles */}
<div className="flex items-center gap-2">
<button
onClick={() => setShowLogs(!showLogs)}
className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition-colors ${
showLogs
? 'bg-primary/10 text-primary border border-primary/20'
: 'bg-card/50 text-muted-foreground border border-border hover:text-foreground'
}`}
>
<Terminal size={12} />
Paste Logs
</button>
<button
disabled
className="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium bg-card/50 text-[#5a6170] border border-border opacity-50 cursor-not-allowed"
title="Coming in Phase 2"
>
<FileText size={12} />
Pull from Ticket
</button>
</div>
{/* Log paste area */}
{showLogs && (
<textarea
value={logContent}
onChange={(e) => setLogContent(e.target.value)}
placeholder="Paste log output, error messages, or Event Viewer entries here..."
className="w-full rounded-lg border border-border bg-card px-4 py-3 font-mono text-xs text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none resize-none"
rows={6}
/>
)}
{/* Submit */}
<div className="flex items-center justify-between">
<p className="text-[0.6875rem] text-[#5a6170]">
FlowPilot will analyze your input and guide you through diagnosis
</p>
<button
onClick={handleSubmit}
disabled={!hasContent}
className="rounded-lg bg-gradient-brand px-5 py-2.5 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] disabled:opacity-40 disabled:shadow-none transition-all"
>
Start Session
</button>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,58 @@
import { useState } from 'react'
import { Check } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { StepOptionSchema } from '@/types/ai-session'
interface FlowPilotOptionsProps {
options: StepOptionSchema[]
onSelect: (value: string) => void
disabled?: boolean
}
export function FlowPilotOptions({ options, onSelect, disabled }: FlowPilotOptionsProps) {
const [selected, setSelected] = useState<string | null>(null)
const handleSelect = (value: string) => {
if (disabled) return
setSelected(value)
onSelect(value)
}
return (
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
{options.map((option) => {
const isSelected = selected === option.value
return (
<button
key={option.value}
onClick={() => handleSelect(option.value)}
disabled={disabled}
className={cn(
'group relative rounded-xl border p-4 text-left transition-all',
'hover:border-[rgba(6,182,212,0.3)] hover:shadow-[0_0_20px_rgba(6,182,212,0.08)]',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40',
isSelected
? 'border-primary/40 bg-primary/10'
: 'border-border bg-card/50',
disabled && 'pointer-events-none opacity-60'
)}
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<p className="text-sm font-medium text-foreground">{option.label}</p>
{option.followup_hint && (
<p className="mt-1 text-xs text-muted-foreground">{option.followup_hint}</p>
)}
</div>
{isSelected && (
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary/20 text-primary">
<Check size={12} />
</span>
)}
</div>
</button>
)
})}
</div>
)
}

View File

@@ -0,0 +1,171 @@
import { useEffect, useRef } from 'react'
import { Network, Clock, Hash } from 'lucide-react'
import type {
AISessionDetail,
AISessionStepResponse,
StepResponseRequest,
ResolveSessionRequest,
EscalateSessionRequest,
SessionDocumentation,
} from '@/types/ai-session'
import { ConfidenceIndicator } from './ConfidenceIndicator'
import { FlowPilotStepCard } from './FlowPilotStepCard'
import { FlowPilotActionBar } from './FlowPilotActionBar'
import { SessionDocView } from './SessionDocView'
interface FlowPilotSessionProps {
session: AISessionDetail
allSteps: AISessionStepResponse[]
currentStep: AISessionStepResponse | null
isProcessing: boolean
canResolve: boolean
canEscalate: boolean
documentation: SessionDocumentation | null
onRespond: (response: StepResponseRequest) => void
onResolve: (data: ResolveSessionRequest) => Promise<SessionDocumentation>
onEscalate: (data: EscalateSessionRequest) => Promise<SessionDocumentation>
onRate: (rating: number) => void
}
export function FlowPilotSession({
session,
allSteps,
currentStep,
isProcessing,
canResolve,
canEscalate,
documentation,
onRespond,
onResolve,
onEscalate,
onRate,
}: FlowPilotSessionProps) {
const scrollRef = useRef<HTMLDivElement>(null)
// Auto-scroll to latest step
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
}
}, [allSteps.length])
const isCompleted = session.status === 'resolved' || session.status === 'escalated'
// Show documentation view for completed sessions
if (isCompleted && documentation) {
return (
<div className="flex h-full flex-col">
<div className="flex-1 overflow-y-auto p-6">
<SessionDocView
documentation={documentation}
onRate={onRate}
currentRating={session.session_rating}
/>
</div>
</div>
)
}
return (
<div className="flex h-full flex-col">
{/* Main content area: conversation + sidebar */}
<div className="flex flex-1 min-h-0">
{/* Conversation column */}
<div ref={scrollRef} className="flex-1 overflow-y-auto p-6">
<div className="mx-auto max-w-2xl space-y-3">
{allSteps.map((step) => (
<FlowPilotStepCard
key={step.step_id}
step={step}
isCurrentStep={currentStep?.step_id === step.step_id}
isProcessing={isProcessing && currentStep?.step_id === step.step_id}
onRespond={onRespond}
/>
))}
</div>
</div>
{/* Sidebar */}
<div
className="hidden w-72 shrink-0 overflow-y-auto border-l p-4 lg:block"
style={{ borderColor: 'var(--glass-border)' }}
>
<div className="space-y-4">
{/* Problem summary */}
{session.problem_summary && (
<div>
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-1">
Problem
</h4>
<p className="text-sm text-foreground">{session.problem_summary}</p>
</div>
)}
{/* Domain */}
{session.problem_domain && (
<div>
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-1">
Domain
</h4>
<span className="font-label rounded-md bg-primary/10 px-2 py-0.5 text-[0.625rem] uppercase tracking-wider text-primary">
{session.problem_domain}
</span>
</div>
)}
{/* Confidence */}
<div>
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-1">
Confidence
</h4>
<ConfidenceIndicator
tier={session.confidence_tier}
score={currentStep?.confidence_score ?? 0}
/>
</div>
{/* Matched flow */}
{session.matched_flow_id && (
<div>
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-1">
Matched flow
</h4>
<div className="flex items-center gap-2">
<Network size={14} className="text-muted-foreground" />
<span className="text-xs text-foreground">
{session.match_score ? `${Math.round(session.match_score * 100)}% match` : 'Match found'}
</span>
</div>
</div>
)}
{/* Steps */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-1.5">
<Hash size={12} className="text-muted-foreground" />
<span className="text-xs text-muted-foreground">{session.step_count} steps</span>
</div>
<div className="flex items-center gap-1.5">
<Clock size={12} className="text-muted-foreground" />
<span className="text-xs text-muted-foreground">
{new Date(session.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
</div>
</div>
</div>
</div>
{/* Action bar */}
{session.status === 'active' && (
<FlowPilotActionBar
canResolve={canResolve}
canEscalate={canEscalate}
isProcessing={isProcessing}
onResolve={onResolve}
onEscalate={onEscalate}
/>
)}
</div>
)
}

View File

@@ -0,0 +1,245 @@
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>
)
}

View File

@@ -0,0 +1,120 @@
import { FileText, Clock, CheckCircle2, ArrowUpRight, Star } from 'lucide-react'
import type { SessionDocumentation } from '@/types/ai-session'
interface SessionDocViewProps {
documentation: SessionDocumentation
onRate?: (rating: number) => void
currentRating?: number | null
}
export function SessionDocView({ documentation, onRate, currentRating }: SessionDocViewProps) {
return (
<div className="space-y-5">
{/* Header */}
<div className="glass-card-static p-5">
<div className="flex items-start gap-3">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
<FileText size={16} />
</span>
<div className="flex-1">
<h3 className="font-heading text-lg font-semibold text-foreground">Session Documentation</h3>
<p className="mt-1 text-sm text-muted-foreground">{documentation.problem_summary}</p>
<div className="mt-2 flex items-center gap-3 flex-wrap">
{documentation.problem_domain && (
<span className="font-label rounded-md bg-primary/10 px-2 py-0.5 text-[0.625rem] uppercase tracking-wider text-primary">
{documentation.problem_domain}
</span>
)}
{documentation.duration_display && (
<span className="flex items-center gap-1 text-xs text-muted-foreground">
<Clock size={12} />
{documentation.duration_display}
</span>
)}
<span className="text-xs text-muted-foreground">
{documentation.total_steps} steps
</span>
</div>
</div>
</div>
</div>
{/* Outcome */}
{(documentation.resolution_summary || documentation.escalation_reason) && (
<div className={`glass-card-static p-4 border-l-2 ${documentation.resolution_summary ? 'border-l-emerald-500' : 'border-l-amber-500'}`}>
<div className="flex items-center gap-2 mb-1">
{documentation.resolution_summary ? (
<CheckCircle2 size={14} className="text-emerald-400" />
) : (
<ArrowUpRight size={14} className="text-amber-400" />
)}
<span className="font-label text-[0.625rem] uppercase tracking-wider text-muted-foreground">
{documentation.resolution_summary ? 'Resolved' : 'Escalated'}
</span>
</div>
<p className="text-sm text-foreground">
{documentation.resolution_summary || documentation.escalation_reason}
</p>
</div>
)}
{/* Intake summary */}
<div className="glass-card-static p-4">
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-muted-foreground mb-2">
Original intake
</h4>
<p className="text-sm text-foreground whitespace-pre-wrap">{documentation.intake_summary}</p>
</div>
{/* Diagnostic steps */}
<div className="space-y-2">
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-muted-foreground px-1">
Diagnostic trail
</h4>
{documentation.diagnostic_steps.map((step) => (
<div key={step.step_number} className="glass-card-static p-4">
<div className="flex items-start gap-3">
<span className="font-label flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-card text-[0.625rem] text-muted-foreground border border-border">
{step.step_number}
</span>
<div className="flex-1">
<p className="text-sm text-foreground">{step.description}</p>
{step.engineer_response && (
<p className="mt-1 text-xs text-primary"> {step.engineer_response}</p>
)}
{step.outcome && (
<p className="mt-1 text-xs text-muted-foreground">Outcome: {step.outcome}</p>
)}
</div>
</div>
</div>
))}
</div>
{/* Rating */}
{onRate && (
<div className="glass-card-static p-4 text-center">
<p className="text-sm text-muted-foreground mb-2">How helpful was this session?</p>
<div className="flex items-center justify-center gap-1">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
onClick={() => onRate(star)}
className="p-1 transition-colors"
>
<Star
size={20}
className={
(currentRating ?? 0) >= star
? 'fill-amber-400 text-amber-400'
: 'text-muted-foreground hover:text-amber-400'
}
/>
</button>
))}
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,8 @@
export { FlowPilotIntake } from './FlowPilotIntake'
export { FlowPilotSession } from './FlowPilotSession'
export { FlowPilotStepCard } from './FlowPilotStepCard'
export { FlowPilotOptions } from './FlowPilotOptions'
export { FlowPilotActionBar } from './FlowPilotActionBar'
export { ConfidenceIndicator } from './ConfidenceIndicator'
export { SessionDocView } from './SessionDocView'
export { AISessionListItem } from './AISessionListItem'