import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { Badge } from '@renderer/components/ui/badge'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@renderer/components/ui/dialog'; import { TeamProvisioningPanel } from '@renderer/components/team/TeamProvisioningPanel'; import { StepProgressBar } from '@renderer/components/team/StepProgressBar'; import { useTeamProvisioningPresentation } from '@renderer/components/team/useTeamProvisioningPresentation'; import { DISPLAY_STEPS } from '@renderer/components/team/provisioningSteps'; import { cn } from '@renderer/lib/utils'; import { AlertTriangle, CheckCircle2, ExternalLink, Loader2, X } from 'lucide-react'; import type { TeamProvisioningPresentation } from '@renderer/utils/teamProvisioningPresentation'; import type { CSSProperties } from 'react'; const MINI_STEPS = DISPLAY_STEPS.map((step) => ({ key: step.key, label: step.label })); const HUD_STEPPER_STYLE: CSSProperties = { ['--stepper-done' as string]: '#22c55e', ['--stepper-done-glow' as string]: 'rgba(34, 197, 94, 0.24)', ['--stepper-current' as string]: '#22c55e', ['--stepper-current-ring' as string]: 'rgba(34, 197, 94, 0.18)', ['--stepper-pending' as string]: 'rgba(148, 163, 184, 0.08)', ['--stepper-pending-text' as string]: '#cbd5e1', ['--stepper-pending-border' as string]: 'rgba(148, 163, 184, 0.2)', ['--stepper-line' as string]: 'rgba(148, 163, 184, 0.14)', ['--stepper-line-done' as string]: '#22c55e', ['--stepper-label' as string]: '#94a3b8', ['--stepper-label-active' as string]: '#e2e8f0', ['--stepper-error' as string]: '#ef4444', ['--stepper-error-glow' as string]: 'rgba(239, 68, 68, 0.22)', ['--stepper-label-error' as string]: '#fca5a5', }; function shouldRenderLaunchHud(presentation: TeamProvisioningPresentation | null): boolean { return presentation != null; } function getToneClasses(tone: TeamProvisioningPresentation['compactTone']): { border: string; badge: string; icon: React.ReactNode; iconClassName: string; } { switch (tone) { case 'error': return { border: 'border-red-400/35 bg-[rgba(26,10,16,0.92)]', badge: 'border-red-500/30 text-red-300', icon: , iconClassName: 'text-red-400', }; case 'warning': return { border: 'border-amber-400/35 bg-[rgba(31,18,8,0.92)]', badge: 'border-amber-500/30 text-amber-200', icon: , iconClassName: 'text-amber-400', }; case 'success': return { border: 'border-emerald-400/35 bg-[rgba(8,24,18,0.92)]', badge: 'border-emerald-500/30 text-emerald-200', icon: , iconClassName: 'text-emerald-400', }; default: return { border: 'border-cyan-400/25 bg-[rgba(8,14,26,0.92)]', badge: 'border-cyan-500/20 text-cyan-200', icon: , iconClassName: 'text-cyan-300', }; } } export interface GraphProvisioningHudProps { teamName: string; leadNodeId: string | null; getLaunchAnchorScreenPlacement: ( leadNodeId: string ) => { x: number; y: number; scale: number; visible: boolean } | null; enabled?: boolean; } export function GraphProvisioningHud({ teamName, leadNodeId, getLaunchAnchorScreenPlacement, enabled = true, }: GraphProvisioningHudProps): React.JSX.Element | null { const { presentation, runInstanceKey } = useTeamProvisioningPresentation(teamName); const shellRef = useRef(null); const lastActiveStepRef = useRef(-1); const [detailsOpen, setDetailsOpen] = useState(false); const [dismissed, setDismissed] = useState(false); const shouldRender = enabled && shouldRenderLaunchHud(presentation) && !dismissed && Boolean(leadNodeId); const tone = presentation ? getToneClasses(presentation.compactTone) : null; const errorStepIndex = presentation?.isFailed ? lastActiveStepRef.current >= 0 ? lastActiveStepRef.current : 0 : undefined; useEffect(() => { setDetailsOpen(false); setDismissed(false); lastActiveStepRef.current = -1; }, [runInstanceKey, teamName]); useEffect(() => { if (!shouldRender || !leadNodeId) { setDetailsOpen(false); } }, [leadNodeId, shouldRender]); useEffect(() => { if (presentation && !presentation.isFailed && presentation.currentStepIndex >= 0) { lastActiveStepRef.current = presentation.currentStepIndex; } }, [presentation]); useLayoutEffect(() => { if (!shouldRender || !leadNodeId) { return; } let frameId = 0; const updatePosition = (): void => { const shell = shellRef.current; if (!shell) { frameId = window.requestAnimationFrame(updatePosition); return; } const placement = getLaunchAnchorScreenPlacement(leadNodeId); if (!placement) { shell.style.opacity = '0'; frameId = window.requestAnimationFrame(updatePosition); return; } if (!placement.visible) { shell.style.opacity = '0'; frameId = window.requestAnimationFrame(updatePosition); return; } shell.style.opacity = '1'; shell.style.transform = `translate(${Math.round(placement.x)}px, ${Math.round(placement.y)}px) scale(${placement.scale.toFixed(3)})`; frameId = window.requestAnimationFrame(updatePosition); }; updatePosition(); return () => { window.cancelAnimationFrame(frameId); }; }, [getLaunchAnchorScreenPlacement, leadNodeId, shouldRender]); const compactLabel = useMemo(() => { if (!presentation?.compactDetail) { return null; } return presentation.compactDetail.length > 88 ? `${presentation.compactDetail.slice(0, 88)}...` : presentation.compactDetail; }, [presentation?.compactDetail]); if (!shouldRender || !presentation || !tone) { return null; } return ( {tone.icon} {presentation.compactTitle} {presentation.isFailed ? 'Issue' : presentation.hasMembersStillJoining ? 'Joining' : presentation.isActive ? 'Live' : 'Ready'} {compactLabel ? ( {compactLabel} ) : null} setDetailsOpen(true)} aria-label="Open launch details" > setDismissed(true)} aria-label="Dismiss launch overlay" > setDetailsOpen(true)} aria-label="Open full launch details" > Launch details Detailed team launch progress, live output and CLI logs. ); }