diff --git a/frontend/src/components/pilot/ProposalBanner.tsx b/frontend/src/components/pilot/ProposalBanner.tsx new file mode 100644 index 00000000..7a760e79 --- /dev/null +++ b/frontend/src/components/pilot/ProposalBanner.tsx @@ -0,0 +1,118 @@ +/** + * 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 { Sparkles, Check, ChevronDown } from 'lucide-react' +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 +} + +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 && ( + + )} + + +
+
+
+ ) +} + +// Placeholder renderers — implemented in Tasks 8 & 9. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function VerifyingBanner(_: ProposalBannerProps) { return null } +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function PartialBanner(_: ProposalBannerProps) { return null } +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function AIConfirmingBanner(_: ProposalBannerProps) { return null } +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function NudgeBanner(_: ProposalBannerProps) { return null } +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function CollapsedBanner(_: ProposalBannerProps) { return null } + +export default ProposalBanner diff --git a/frontend/src/index.css b/frontend/src/index.css index 607ee349..4f148cc0 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -86,7 +86,12 @@ --animate-slide-in-bottom: slide-in-from-bottom 200ms ease-out both; --animate-scale-in: scale-in 150ms ease-out both; --animate-fade: fadeIn 300ms ease both; + --animate-slide-up: slide-up 320ms cubic-bezier(.22,.9,.28,1) both; + @keyframes slide-up { + from { transform: translateY(14px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } + } @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }