/**
* 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