From 7fa1d6a32fbe0a1cf9a683443906fc43d32a518c Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Thu, 23 Apr 2026 15:32:02 -0400 Subject: [PATCH] feat(pilot): banner Verifying + Partial states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verifying: amber pulse animation, confidence pill becomes 'Applied Xm ago', three actions (overflow for Mark partial, Didn't work, It worked). window.prompt used for the partial notes + failure reason inputs — good-enough v1 pending an inline composer. Partial: cyan-toned to signal 'parked, outcome unknown', shows saved notes inline, Finish it / Didn't work / It worked actions. Adds pulse-amber to @theme animations alongside slide-up. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/pilot/ProposalBanner.tsx | 150 +++++++++++++++++- frontend/src/index.css | 6 + 2 files changed, 150 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/pilot/ProposalBanner.tsx b/frontend/src/components/pilot/ProposalBanner.tsx index 7a760e79..264213fa 100644 --- a/frontend/src/components/pilot/ProposalBanner.tsx +++ b/frontend/src/components/pilot/ProposalBanner.tsx @@ -9,7 +9,9 @@ * Visual reference: docs/FlowAssist_Migration/mockups/06-slide-up-banner.html * + 07-verify-states.html. */ -import { Sparkles, Check, ChevronDown } from 'lucide-react' +import { useState } from 'react' +import { Sparkles, Check, ChevronDown, X, MoreHorizontal, Info } from 'lucide-react' +import { cn } from '@/lib/utils' import type { SessionSuggestedFix, FixOutcome, @@ -103,11 +105,147 @@ function ProposedBanner({ fix, onApply, onDismiss, onToggleCollapsed }: Proposal ) } -// 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 } +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} +
+ )} +
+
+ + + +
+
+
+ ) +} + +// Placeholder renderers — implemented in Task 9. // eslint-disable-next-line @typescript-eslint/no-unused-vars function AIConfirmingBanner(_: ProposalBannerProps) { return null } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/frontend/src/index.css b/frontend/src/index.css index 4f148cc0..295de510 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -87,11 +87,17 @@ --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; + --animate-pulse-amber: pulse-amber 1.6s infinite; @keyframes slide-up { from { transform: translateY(14px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } + @keyframes pulse-amber { + 0% { box-shadow: 0 0 0 0 rgba(251,191,36,0.45); } + 70% { box-shadow: 0 0 0 10px rgba(251,191,36,0); } + 100% { box-shadow: 0 0 0 0 rgba(251,191,36,0); } + } @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }