/** * ProposalBanner — chat-composer-anchored banner that carries the lifecycle * of a suggested fix from Proposed → Verifying → terminal outcome. * * Replaces the task-lane SuggestedFix card (Phase 8). The banner renders * above the chat composer in AssistantChatPage. Parent owns the fix record * and the outcome mutations; this component renders + dispatches callbacks. * * Visual reference: docs/FlowAssist_Migration/mockups/06-slide-up-banner.html * + 07-verify-states.html. */ import { useState } from 'react' import { Sparkles, Check, ChevronDown, X, MoreHorizontal, Info } from 'lucide-react' import { cn } from '@/lib/utils' import type { SessionSuggestedFix, FixOutcome, } from '@/api/sessionSuggestedFixes' export type BannerMode = | 'proposed' // AI just proposed; engineer hasn't applied yet | 'verifying' // Engineer clicked Apply; awaiting outcome | 'partial' // Applied partially; awaiting finish or terminal outcome | 'ai_confirming' // AI emitted [FIX_OUTCOME]; engineer confirms | 'nudge' // Compact nudge shown after N post-apply messages export interface ProposalBannerProps { fix: SessionSuggestedFix mode: BannerMode onApply: () => void onDismiss: () => void onOutcome: (outcome: FixOutcome, notes?: string) => void onAcceptAIProposal: () => void onRejectAIProposal: () => void /** Collapsed variant shown as a thin single-line strip. */ collapsed?: boolean onToggleCollapsed?: () => void /** Silence the nudge without collapsing it (Task 11 wires this). */ onSilenceNudge: () => void } export function ProposalBanner(props: ProposalBannerProps) { if (props.collapsed) return switch (props.mode) { case 'proposed': return case 'verifying': return case 'partial': return case 'ai_confirming': return case 'nudge': return } } function ProposedBanner({ fix, onApply, onDismiss, onToggleCollapsed }: ProposalBannerProps) { return (
Suggested Fix {fix.confidence_pct}% confidence
{fix.title}
{fix.description}
{fix.script_template_id && (
Matches an existing Script Library template — one-click apply
)}
{onToggleCollapsed && ( )}
) } function VerifyingBanner({ fix, onOutcome }: ProposalBannerProps) { const [showOverflow, setShowOverflow] = useState(false) const appliedLabel = fix.applied_at ? `Applied ${formatRelativeMinutes(fix.applied_at)}` : 'Applied' return (
Verifying {appliedLabel}
Did "{fix.title}" work?
Mark the outcome so the AI can either close the session with this as the resolution, or propose something else.
{showOverflow && (
)}
) } function formatRelativeMinutes(iso: string): string { const then = new Date(iso).getTime() const mins = Math.max(0, Math.round((Date.now() - then) / 60000)) if (mins === 0) return 'just now' if (mins === 1) return '1m ago' return `${mins}m ago` } function PartialBanner({ fix, onOutcome, onApply }: ProposalBannerProps) { return (
Partially applied Parked
{fix.title}
{fix.partial_notes && (
Note {fix.partial_notes}
)}
) } function AIConfirmingBanner({ fix, onAcceptAIProposal, onRejectAIProposal }: ProposalBannerProps) { const proposal = fix.ai_outcome_proposal if (!proposal) return null const isSuccess = proposal.outcome === 'success' const isFailure = proposal.outcome === 'failure' const headlineVerb = isSuccess ? 'resolved the issue' : isFailure ? "didn't work" : 'was partially applied' return (
AI detected outcome {isSuccess ? 'Success' : isFailure ? 'Failure' : 'Partial'}
AI thinks the fix {headlineVerb} — confirm?
{proposal.reason || 'Based on the recent chat. One click either confirms or corrects.'}
) } function NudgeBanner({ fix, onOutcome, onSilenceNudge }: ProposalBannerProps) { return (
Did "{fix.title}" work?
) } function CollapsedBanner({ fix, onToggleCollapsed }: ProposalBannerProps) { return ( ) } export default ProposalBanner