import { useEffect, useRef, useState } from 'react'
import { useParams, useSearchParams, useLocation, useBlocker, useNavigate } from 'react-router-dom'
import { Sparkles, Loader2, AlertTriangle, CheckCircle2, ArrowUpRight, FileText, MoreHorizontal, Pause, X } from 'lucide-react'
import { useFlowPilotSession } from '@/hooks/useFlowPilotSession'
import { useBranching } from '@/hooks/useBranching'
import { FlowPilotIntake, FlowPilotSession, SessionBriefing } from '@/components/flowpilot'
import { EscalateModal } from '@/components/flowpilot/EscalateModal'
import { StatusUpdateModal } from '@/components/flowpilot/StatusUpdateModal'
import { HandoffModal } from '@/components/session/HandoffModal'
import { handoffsApi } from '@/api/handoffs'
import { aiSessionsApi } from '@/api'
import { toast } from '@/lib/toast'
export default function FlowPilotSessionPage() {
const { sessionId } = useParams<{ sessionId?: string }>()
const [searchParams, setSearchParams] = useSearchParams()
const navigate = useNavigate()
const location = useLocation()
const prefill = (location.state as { prefill?: string } | null)?.prefill || ''
const isPickup = searchParams.get('pickup') === 'true'
const fp = useFlowPilotSession()
const branching = useBranching()
const prefillHandledRef = useRef(false)
const [showOverflow, setShowOverflow] = useState(false)
const [showResolve, setShowResolve] = useState(false)
const [showEscalate, setShowEscalate] = useState(false)
const [showAbandon, setShowAbandon] = useState(false)
const [showStatusUpdate, setShowStatusUpdate] = useState(false)
const [showHandoff, setShowHandoff] = useState(false)
const [resolutionSummary, setResolutionSummary] = useState('')
const [submitting, setSubmitting] = useState(false)
// Block navigation when session is active
const isActiveSession = fp.session?.status === 'active'
const blocker = useBlocker(
({ currentLocation, nextLocation }) =>
!!isActiveSession && currentLocation.pathname !== nextLocation.pathname
)
// Auto-submit when navigating from dashboard with prefilled problem
useEffect(() => {
if (prefill && !prefillHandledRef.current && !sessionId && !fp.session && !fp.isLoading) {
prefillHandledRef.current = true
fp.startSession({ intake_type: 'free_text', intake_content: { text: prefill } })
}
}, [prefill, sessionId, fp.session, fp.isLoading]) // eslint-disable-line react-hooks/exhaustive-deps
const [pickingUp, setPickingUp] = useState(false)
// Load existing session if ID in URL
useEffect(() => {
if (sessionId && !fp.session) {
fp.loadSession(sessionId)
}
}, [sessionId]) // eslint-disable-line react-hooks/exhaustive-deps
// Load branches when session is branching
useEffect(() => {
if (fp.session?.is_branching && fp.session.id) {
branching.loadBranches(fp.session.id)
}
}, [fp.session?.is_branching, fp.session?.id]) // eslint-disable-line react-hooks/exhaustive-deps
const handlePickupContinue = async () => {
if (!sessionId) return
setPickingUp(true)
try {
await aiSessionsApi.pickupSession(sessionId, { resume_mode: 'continue' })
// Clear pickup param and reload the session as active
setSearchParams({})
await fp.loadSession(sessionId)
} catch (e: unknown) {
const message = e instanceof Error ? e.message : 'Failed to pick up session'
toast.error(message)
} finally {
setPickingUp(false)
}
}
const handleBranchSwitch = async (branchId: string) => {
if (!fp.session) return
const result = await branching.switchBranch(fp.session.id, branchId)
if (result) {
// Reload session to get updated steps for the switched branch
await fp.loadSession(fp.session.id)
}
}
const handlePickupFresh = async (context: string) => {
if (!sessionId) return
setPickingUp(true)
try {
await aiSessionsApi.pickupSession(sessionId, {
resume_mode: 'fresh',
additional_context: context,
})
setSearchParams({})
await fp.loadSession(sessionId)
} catch (e: unknown) {
const message = e instanceof Error ? e.message : 'Failed to pick up session'
toast.error(message)
} finally {
setPickingUp(false)
}
}
// Error state
if (fp.error && !fp.session) {
return (
{fp.error}
window.location.reload()}
className="mt-3 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
Try again
)
}
// Loading
if (fp.isLoading && !fp.session) {
return (
)
}
// Intake screen (no session yet)
if (!fp.session) {
return (
)
}
// Escalation pickup briefing
if (isPickup && fp.session.status === 'requesting_escalation' && fp.session.escalation_reason) {
// Build escalation package from session detail
// The escalation_package is in the session but not directly on AISessionDetail —
// we use what's available from the session fields
const escalationPackage = {
problem_summary: fp.session.problem_summary ?? undefined,
escalation_reason: fp.session.escalation_reason ?? undefined,
// Steps are available from the session detail
steps_tried: fp.allSteps.map(step => ({
step_type: step.step_type,
description: (step.content as Record)?.text || '',
})),
}
return (
{/* Header */}
Escalation Pickup — {fp.session.problem_summary || 'FlowPilot Session'}
Awaiting pickup
{/* Briefing */}
)
}
// Active/completed session
return (
{/* Navigation guard modal */}
{blocker.state === 'blocked' && (
You have an active troubleshooting session. If you leave, your session will be paused and you can resume it later.
blocker.reset()}
className="flex-1 rounded-lg bg-gradient-to-r from-blue-500 to-blue-400 px-4 py-2.5 text-sm font-semibold text-white hover:brightness-110 active:scale-[0.98] transition-all"
>
Stay in Session
{
fp.pauseSession()
blocker.proceed()
}}
className="flex-1 rounded-lg bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] px-4 py-2.5 text-sm font-medium text-foreground hover:border-[rgba(255,255,255,0.12)] transition-all"
>
Pause & Leave
)}
{/* Header with actions */}
{fp.session.problem_summary || 'FlowPilot Session'}
{/* Action buttons — desktop inline, mobile overflow menu */}
{fp.session.status === 'active' && (
<>
{/* Desktop actions */}
setShowResolve(true)}
disabled={!fp.canResolve || fp.isProcessing}
className="flex items-center gap-1.5 rounded-lg bg-emerald-500/10 border border-emerald-500/20 px-3 py-1.5 text-xs font-medium text-emerald-400 hover:bg-emerald-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
>
Resolve
setShowEscalate(true)}
disabled={!fp.canEscalate || fp.isProcessing}
className="flex items-center gap-1.5 rounded-lg bg-amber-500/10 border border-amber-500/20 px-3 py-1.5 text-xs font-medium text-amber-400 hover:bg-amber-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
>
Escalate
{fp.allSteps.length >= 2 && (
setShowStatusUpdate(true)}
disabled={fp.isProcessing}
className="flex items-center gap-1.5 rounded-lg bg-blue-500/10 border border-blue-500/20 px-3 py-1.5 text-xs font-medium text-blue-400 hover:bg-blue-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
title="Update"
>
Update
)}
{/* Overflow: Pause / Close */}
setShowOverflow(!showOverflow)}
className="flex items-center justify-center rounded-lg px-2 py-1.5 text-muted-foreground hover:text-foreground hover:bg-[rgba(255,255,255,0.06)] transition-colors"
>
{showOverflow && (
<>
setShowOverflow(false)} />
{ setShowOverflow(false); fp.pauseSession() }}
className="flex w-full items-center gap-2 px-3 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-[rgba(255,255,255,0.06)] transition-colors"
>
Pause
{ setShowOverflow(false); setShowAbandon(true) }}
className="flex w-full items-center gap-2 px-3 py-2 text-xs text-muted-foreground hover:text-rose-400 hover:bg-rose-500/10 transition-colors"
>
Close
>
)}
{/* Mobile: single overflow menu */}
setShowOverflow(!showOverflow)}
className="flex items-center justify-center rounded-lg px-2 py-1.5 text-muted-foreground hover:text-foreground hover:bg-[rgba(255,255,255,0.06)] transition-colors"
>
{showOverflow && (
<>
setShowOverflow(false)} />
{ setShowOverflow(false); setShowResolve(true) }}
disabled={!fp.canResolve}
className="flex w-full items-center gap-2 px-3 py-2.5 text-xs text-emerald-400 hover:bg-emerald-500/10 transition-colors disabled:opacity-40"
>
Resolve
{ setShowOverflow(false); setShowEscalate(true) }}
disabled={!fp.canEscalate}
className="flex w-full items-center gap-2 px-3 py-2.5 text-xs text-amber-400 hover:bg-amber-500/10 transition-colors disabled:opacity-40"
>
Escalate
{fp.allSteps.length >= 2 && (
{ setShowOverflow(false); setShowStatusUpdate(true) }}
className="flex w-full items-center gap-2 px-3 py-2.5 text-xs text-blue-400 hover:bg-blue-500/10 transition-colors"
>
Update
)}
{ setShowOverflow(false); fp.pauseSession() }}
className="flex w-full items-center gap-2 px-3 py-2.5 text-xs text-muted-foreground hover:text-foreground hover:bg-[rgba(255,255,255,0.06)] transition-colors"
>
Pause
{ setShowOverflow(false); setShowAbandon(true) }}
className="flex w-full items-center gap-2 px-3 py-2.5 text-xs text-muted-foreground hover:text-rose-400 hover:bg-rose-500/10 transition-colors"
>
Close
>
)}
>
)}
{/* Status badge — non-active states */}
{fp.session.status !== 'active' && (
{fp.session.status}
)}
{/* Session content */}
fp.loadSession(fp.session!.id)}
onGenerateStatusUpdate={(audience, length, context) => fp.generateStatusUpdate({ audience, length, context })}
branches={branching.branches}
activeBranchId={branching.activeBranchId}
onBranchSwitch={handleBranchSwitch}
/>
{/* ── Page-level modals (moved from action bar) ── */}
{/* Resolve modal */}
{showResolve && (
Resolve Session
Summarize what fixed the issue. This will be included in the auto-generated documentation.
)}
{/* Close/Abandon confirmation */}
{showAbandon && (
Close
Are you sure you want to close this session? The session history will be kept but it won't count as resolved.
setShowAbandon(false)}
className="rounded-lg px-4 py-2 min-h-[44px] text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Cancel
{
setSubmitting(true)
try {
await fp.abandonSession()
setShowAbandon(false)
navigate('/sessions')
} finally {
setSubmitting(false)
}
}}
disabled={submitting}
className="rounded-lg bg-rose-500/20 border border-rose-500/30 px-4 py-2 min-h-[44px] text-sm font-medium text-rose-400 hover:bg-rose-500/30 disabled:opacity-50 transition-colors"
>
{submitting ? 'Closing...' : 'Close'}
)}
{/* Escalate modal */}
setShowEscalate(false)}
onEscalate={fp.escalateSession}
isProcessing={fp.isProcessing || submitting}
hasPsaTicket={!!fp.session.psa_ticket_id}
sessionId={fp.session.id}
/>
{/* Status Update modal */}
setShowStatusUpdate(false)}
onGenerate={(audience, length, context) => fp.generateStatusUpdate({ audience, length, context })}
context="status"
hasPsaTicket={!!fp.session.psa_ticket_id}
/>
{/* Handoff modal (branching sessions) */}
{fp.session.is_branching && showHandoff && (
setShowHandoff(false)}
onSubmit={async (data) => {
await handoffsApi.createHandoff(fp.session!.id, data)
setShowHandoff(false)
toast.success('Handoff created')
}}
/>
)}
)
}