import { useEffect, useMemo, useRef } from 'react' import { AlertTriangle, ArrowRight, Brain, Clock, FileText, Hash, Loader2, Sparkles, Target, User, X, } from 'lucide-react' import type { HandoffResponse } from '@/types/branching' import { cn } from '@/lib/utils' import { timeAgo } from '@/lib/timeAgo' // Magic-moment handoff-context screen. Renders BEFORE the FlowPilot session // view when a senior tech picks up an escalated session, then dissolves on // "Start here". Re-openable via toolbar in FlowPilotSessionPage. // // Four sections per the design plan: // 1. Problem summary (top, Bricolage h2) // 2. What's been tried (left column) — engineer notes + step count. // Full step detail isn't in the handoff snapshot today (snapshot = // problem_summary, problem_domain, status, step_count, confidence_tier // per HandoffManager._generate_snapshot); we surface what's there and // promise the timeline post-pickup. Snapshot expansion is a follow-up. // 3. AI assessment (right column) — likely_cause / suggested_steps / // confidence. Renders gracefully when ai_assessment is null (the 5s // timeout from commit 9bdd995 fired). // 4. Start here (primary CTA, electric-blue, ≥44px) — claims the handoff // and dissolves the screen. type ConfidenceTier = 'low' | 'medium' | 'high' | string interface HandoffContextScreenProps { handoff: HandoffResponse // Pre-claim entry point: one of three choices is made before claiming. // Post-claim re-open (dismissible=true) keeps the legacy onStartHere path. onContinue?: () => Promise | void onAIAnalysis?: () => Promise | void onOwnThing?: () => Promise | void // Legacy single-CTA — used when dismissible=true (post-claim toolbar re-open) onStartHere?: () => Promise | void onDismiss?: () => void // When true, renders an "X" close affordance in the corner. Used when the // screen is re-opened from the FlowPilot toolbar (post-claim re-read). dismissible?: boolean isProcessing?: boolean // Whether the task lane has items — drives the 3-option vs 2-option layout hasTaskLane?: boolean activeOptionKey?: 'continue' | 'ai' | 'own' | null } function ConfidenceBadge({ value }: { value: number | string | null | undefined }) { if (value === null || value === undefined || value === '') return null // Numeric (0..1) or string tier let tier: ConfidenceTier = 'medium' let label = String(value) if (typeof value === 'number') { tier = value >= 0.7 ? 'high' : value >= 0.4 ? 'medium' : 'low' label = `${Math.round(value * 100)}%` } else { const s = String(value).toLowerCase() if (s === 'low' || s === 'medium' || s === 'high') tier = s label = s.charAt(0).toUpperCase() + s.slice(1) } const tone = tier === 'high' ? 'bg-success-dim text-success border border-success/20' : tier === 'low' ? 'bg-warning-dim text-warning border border-warning/20' : 'bg-accent-dim text-accent-text border border-accent/20' return ( {label} ) } export function HandoffContextScreen({ handoff, onContinue, onAIAnalysis, onOwnThing, onDismiss, dismissible = false, isProcessing = false, hasTaskLane = false, activeOptionKey = null, }: HandoffContextScreenProps) { const startBtnRef = useRef(null) const prefersReducedMotion = useMemo(() => { if (typeof window === 'undefined' || !window.matchMedia) return false return window.matchMedia('(prefers-reduced-motion: reduce)').matches }, []) // Esc dismisses when the screen is re-opened post-claim (dismissible mode). // Pre-claim, Esc has no escape hatch — they must Start here or back out via // browser nav. useEffect(() => { if (!dismissible || !onDismiss) return const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onDismiss() } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) }, [dismissible, onDismiss]) // Focus the primary CTA on mount so keyboard users can hit Enter. useEffect(() => { startBtnRef.current?.focus() }, []) const snapshot = handoff.snapshot as Record const problemSummary = (snapshot.problem_summary as string | undefined) || 'Untitled session' const problemDomain = snapshot.problem_domain as string | undefined const stepCount = (snapshot.step_count as number | undefined) ?? 0 const confidenceTier = snapshot.confidence_tier as string | undefined const assessment = handoff.ai_assessment_data const likelyCause = assessment?.likely_cause const whatWeKnow = assessment?.what_we_know ?? [] const suggestedSteps = assessment?.suggested_steps ?? [] const assessmentConfidence = assessment?.confidence const assessmentText = handoff.ai_assessment const enterClass = prefersReducedMotion ? 'animate-fade-in' : 'animate-slide-up' return (
{/* Header */}

Escalation handoff

{problemSummary}

{problemDomain && ( {problemDomain} )} {stepCount} {stepCount === 1 ? 'step' : 'steps'} {confidenceTier && ( Session confidence: {confidenceTier} )} Escalated {timeAgo(handoff.created_at)} {handoff.priority === 'elevated' && ( Elevated )}
{dismissible && onDismiss && ( )}
{/* Two-column body */}
{/* What's been tried */}

What's been tried

{handoff.engineer_notes ? (

Why they escalated

{handoff.engineer_notes}

) : (

No notes from the original engineer.

)}
{stepCount}{' '} diagnostic {stepCount === 1 ? 'step' : 'steps'} on record. Full timeline opens when you start the session.
{/* AI assessment */}

AI assessment

{!assessmentText && !likelyCause && suggestedSteps.length === 0 ? (
AI assessment is still generating. Reopen this view in a few seconds to see it, or pick up the session to investigate directly.
) : ( <> {likelyCause && (

Likely cause

{likelyCause}

)} {whatWeKnow.length > 0 && (

What we know

    {whatWeKnow.map((fact, i) => (
  • {fact}
  • ))}
)} {assessmentText && !likelyCause && (

{assessmentText}

)} {suggestedSteps.length > 0 && (

Suggested next steps

    {suggestedSteps.map((step, i) => (
  • {step}
  • ))}
)} )}
{/* CTA footer */} {dismissible ? ( // Post-claim re-open from toolbar — single close action
) : ( // Pre-claim: 3 options (task lane exists) or 2 options (empty lane)

How would you like to approach this session?

{/* Continue — only when task lane has items */} {hasTaskLane && onContinue && ( )} {/* AI analysis */} {onAIAnalysis && ( )} {/* Own approach */} {onOwnThing && ( )}
)}
) }