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:
83
frontend/src/pages/FlowPilotSessionPage.tsx
Normal file
83
frontend/src/pages/FlowPilotSessionPage.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Sparkles } from 'lucide-react'
|
||||
import { useFlowPilotSession } from '@/hooks/useFlowPilotSession'
|
||||
import { FlowPilotIntake, FlowPilotSession } from '@/components/flowpilot'
|
||||
|
||||
export default function FlowPilotSessionPage() {
|
||||
const { sessionId } = useParams<{ sessionId?: string }>()
|
||||
const fp = useFlowPilotSession()
|
||||
|
||||
// Load existing session if ID in URL
|
||||
useEffect(() => {
|
||||
if (sessionId && !fp.session) {
|
||||
fp.loadSession(sessionId)
|
||||
}
|
||||
}, [sessionId]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Error state
|
||||
if (fp.error && !fp.session) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[50vh]">
|
||||
<div className="glass-card-static p-6 text-center max-w-md">
|
||||
<p className="text-sm text-rose-400">{fp.error}</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="mt-3 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Intake screen (no session yet)
|
||||
if (!fp.session) {
|
||||
return (
|
||||
<div className="h-full p-6">
|
||||
<FlowPilotIntake onSubmit={fp.startSession} isLoading={fp.isLoading} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Active/completed session
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
<div
|
||||
className="flex items-center gap-3 border-b px-5 py-3 shrink-0"
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
>
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary/10">
|
||||
<Sparkles size={14} className="text-primary" />
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="font-heading text-sm font-semibold text-foreground truncate">
|
||||
{fp.session.problem_summary || 'FlowPilot Session'}
|
||||
</h1>
|
||||
</div>
|
||||
<span className="font-label rounded-md bg-card px-2 py-0.5 text-[0.625rem] uppercase tracking-wider text-muted-foreground border border-border">
|
||||
{fp.session.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Session content */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<FlowPilotSession
|
||||
session={fp.session}
|
||||
allSteps={fp.allSteps}
|
||||
currentStep={fp.currentStep}
|
||||
isProcessing={fp.isProcessing}
|
||||
canResolve={fp.canResolve}
|
||||
canEscalate={fp.canEscalate}
|
||||
documentation={fp.documentation}
|
||||
onRespond={fp.respondToStep}
|
||||
onResolve={fp.resolveSession}
|
||||
onEscalate={fp.escalateSession}
|
||||
onRate={fp.rateSession}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,14 +2,16 @@ import { useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { sessionsApi } from '@/api/sessions'
|
||||
import { aiSessionsApi } from '@/api/aiSessions'
|
||||
import { treesApi } from '@/api/trees'
|
||||
import type { Session, TreeListItem, SessionOutcome } from '@/types'
|
||||
import type { Session, TreeListItem, SessionOutcome, AISessionSummary } from '@/types'
|
||||
import type { DateRange } from 'react-day-picker'
|
||||
import { SessionFilters } from '@/components/session/SessionFilters'
|
||||
import type { SessionFilterState } from '@/components/session/SessionFilters'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { EmptyState } from '@/components/common/EmptyState'
|
||||
import { SessionIllustration } from '@/components/common/EmptyStateIllustrations'
|
||||
import { AISessionListItem } from '@/components/flowpilot/AISessionListItem'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { getSessionResumePath } from '@/lib/routing'
|
||||
@@ -18,6 +20,11 @@ export function SessionHistoryPage() {
|
||||
const navigate = useNavigate()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
|
||||
// Top-level tab: flow sessions vs AI sessions
|
||||
const [sessionType, setSessionType] = useState<'flow' | 'ai'>('flow')
|
||||
const [aiSessions, setAiSessions] = useState<AISessionSummary[]>([])
|
||||
const [aiLoading, setAiLoading] = useState(false)
|
||||
|
||||
const [sessions, setSessions] = useState<Session[]>([])
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
const [trees, setTrees] = useState<TreeListItem[]>([])
|
||||
@@ -140,6 +147,25 @@ export function SessionHistoryPage() {
|
||||
setSearchParams(params, { replace: true })
|
||||
}, [filters, setSearchParams])
|
||||
|
||||
// Load AI sessions when tab is active
|
||||
useEffect(() => {
|
||||
if (sessionType !== 'ai') return
|
||||
let cancelled = false
|
||||
const loadAiSessions = async () => {
|
||||
setAiLoading(true)
|
||||
try {
|
||||
const data = await aiSessionsApi.listSessions({ limit: 50 })
|
||||
if (!cancelled) setAiSessions(data)
|
||||
} catch {
|
||||
if (!cancelled) toast.error('Failed to load AI sessions')
|
||||
} finally {
|
||||
if (!cancelled) setAiLoading(false)
|
||||
}
|
||||
}
|
||||
loadAiSessions()
|
||||
return () => { cancelled = true }
|
||||
}, [sessionType])
|
||||
|
||||
const handleFilterChange = (newFilters: SessionFilterState) => {
|
||||
setFilters(newFilters)
|
||||
}
|
||||
@@ -226,7 +252,34 @@ export function SessionHistoryPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
{/* Session type toggle */}
|
||||
<div className="mb-4 flex gap-1 rounded-lg bg-card/50 p-1 w-fit border border-border">
|
||||
<button
|
||||
onClick={() => setSessionType('flow')}
|
||||
className={cn(
|
||||
'rounded-md px-4 py-1.5 text-sm font-medium transition-colors',
|
||||
sessionType === 'flow'
|
||||
? 'bg-primary/10 text-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
Flow Sessions
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSessionType('ai')}
|
||||
className={cn(
|
||||
'rounded-md px-4 py-1.5 text-sm font-medium transition-colors',
|
||||
sessionType === 'ai'
|
||||
? 'bg-primary/10 text-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
AI Sessions
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filter Tabs (flow sessions only) */}
|
||||
{sessionType === 'flow' && (
|
||||
<div className="mb-6 flex gap-2 border-b border-border">
|
||||
{(['active', 'prepared', 'completed', 'all'] as const).map((tab) => (
|
||||
<button
|
||||
@@ -243,8 +296,39 @@ export function SessionHistoryPage() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search and Filter Controls */}
|
||||
{/* AI Sessions view */}
|
||||
{sessionType === 'ai' && (
|
||||
aiLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : aiSessions.length === 0 ? (
|
||||
<EmptyState
|
||||
title="No AI sessions yet"
|
||||
description="Start a FlowPilot session to get AI-guided troubleshooting. Sessions will appear here."
|
||||
action={
|
||||
<Link
|
||||
to="/pilot"
|
||||
className="inline-flex items-center gap-2 rounded-[10px] 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] transition-all"
|
||||
>
|
||||
Start AI Session
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{aiSessions.map((s) => (
|
||||
<AISessionListItem key={s.id} session={s} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Flow Sessions Content */}
|
||||
{sessionType === 'flow' && (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<SessionFilters
|
||||
filters={filters}
|
||||
@@ -474,6 +558,8 @@ export function SessionHistoryPage() {
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user