feat(session): impeccable session-screen pass + tasklane keyboard flow
Multi-step UX refactor of the assistant chat session screen, run via the $impeccable skill. Heuristic score moved 24/40 → 33/40 (+9), with the biggest gains on Aesthetic & Minimalist (1→3), Consistency & Standards (1→3), and Recognition Rather Than Recall (2→4). Distill — chat region: - Remove the "Suggested checks" chip strip + selected-chip detail card; the TaskLane is the single canonical home for "what to do next" - Add an inline Next steps · N pending cue above the latest action-bearing AI bubble (anchors attention without duplicating the lane's items) - Link banner ↔ script-panel lifecycle: collapsing or dismissing the ProposalBanner now also hides the InlineNoTemplateDialog / TemplateMatchPanel - Drop backdrop-blur on the handoff-context overlay (DESIGN-SYSTEM hard rule) Quieter — drop decoration overshoot: - Remove 3px side stripes on TaskLane done cards, all 6 ProposalBanner modes, WhatWeKnowItem fact rows - Drop bg-gradient surfaces on WhatWeKnow + every ProposalBanner mode - Drop 2px accent borderTop on the TaskLane header - Replace bordered avatar boxes in banners with inline state-colored icons - Each surface now uses a single decoration channel (top border + inline icon) Layout: - Header consolidates to Resolve + Escalate + ⋯ kebab; Context, New Ticket, Update Ticket, Pause now live behind the kebab on desktop, with feature parity in the existing mobile overflow menu - Messages column anchors to max-w-3xl mx-auto to match the composer - Chat bubbles drop from rounded-2xl to rounded-xl for vocabulary alignment Typeset: - Unify text sizing from 14 distinct sizes (with sub-pixel oddities and rem/px duplicates) to a 5-step scale: 10px / 11px / text-xs / 13px / text-sm WhatWeKnow collapsible: - Header is now a toggle; section body hides when collapsed - Auto-collapses on first render when facts ≥ 5 so Questions / Diagnostic Checks stay above the fold - Engineer's choice persists in sessionStorage per session and beats the auto-collapse heuristic on subsequent renders - key=activeChatId on both render sites resets state cleanly across sessions Polish: - Split MessageCircleQuestion into Pencil (question Answer CTA, write affordance) + HelpCircle (per-check Explain toggle, universal help icon) — same icon for two different jobs was a discoverability bug - Drop redundant text-xs from font-sans text-[0.625rem] / text-[0.6875rem] double-class definitions; the more-specific size always wins TaskLane keyboard flow: - Enter submits and auto-advances to the next pending task; Shift+Enter inserts a newline (consistent across question and action textareas — paste events don't fire keydown, so paste-then-Enter still works as expected) - Esc cancels (same as the Cancel button) - After the last pending task is submitted, focus moves to the Send Responses button so the engineer can fire the whole batch with one more keystroke - Subtle hint row under each open input teaches the shortcut Type-check, lint, and build all clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -57,7 +57,7 @@ function TabButton({
|
||||
aria-selected={active}
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'relative px-3 py-[7px] text-[12.5px] font-medium rounded-t-md transition-colors',
|
||||
'relative px-3 py-[7px] text-xs font-medium rounded-t-md transition-colors',
|
||||
'border-b-2 -mb-px',
|
||||
active
|
||||
? 'text-heading border-accent bg-bg-page'
|
||||
|
||||
@@ -54,27 +54,24 @@ export function ProposalBanner(props: ProposalBannerProps) {
|
||||
|
||||
function ProposedBanner({ fix, onApply, onDismiss, onToggleCollapsed }: ProposalBannerProps) {
|
||||
return (
|
||||
<div className="relative border-t border-warning/30 bg-gradient-to-b from-warning-dim/40 to-warning-dim/20 px-5 py-3 animate-slide-up">
|
||||
<div className="absolute left-0 top-0 bottom-0 w-[3px] bg-warning" />
|
||||
<div className="border-t border-warning/30 bg-card px-5 py-3 animate-slide-up">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="shrink-0 mt-0.5 w-7 h-7 rounded-md border border-warning/30 bg-warning-dim flex items-center justify-center text-warning">
|
||||
<Sparkles size={15} />
|
||||
</div>
|
||||
<Sparkles size={16} className="text-warning shrink-0 mt-1" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 font-heading text-[10px] font-semibold uppercase tracking-[1.2px] text-warning">
|
||||
<div className="flex items-center gap-2 font-heading text-[0.625rem] font-semibold uppercase tracking-[1.2px] text-warning">
|
||||
<span>Suggested Fix</span>
|
||||
<span className="tabular-nums px-2 py-[2px] rounded-full bg-warning/20 text-warning text-[10.5px] font-bold">
|
||||
<span className="tabular-nums px-2 py-[2px] rounded-full bg-warning/20 text-warning text-[0.625rem] font-bold">
|
||||
{fix.confidence_pct}% confidence
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-0.5 text-[14px] font-semibold text-heading leading-snug">
|
||||
<div className="mt-0.5 text-sm font-semibold text-heading leading-snug">
|
||||
{fix.title}
|
||||
</div>
|
||||
<div className="mt-1 text-[12.5px] text-muted-foreground leading-relaxed">
|
||||
<div className="mt-1 text-xs text-muted-foreground leading-relaxed">
|
||||
{fix.description}
|
||||
</div>
|
||||
{fix.script_template_id && (
|
||||
<div className="mt-1.5 inline-flex items-center gap-1.5 text-[11.5px] text-success">
|
||||
<div className="mt-1.5 inline-flex items-center gap-1.5 text-[0.6875rem] text-success">
|
||||
<Check size={11} />
|
||||
Matches an existing Script Library template — one-click apply
|
||||
</div>
|
||||
@@ -92,13 +89,13 @@ function ProposedBanner({ fix, onApply, onDismiss, onToggleCollapsed }: Proposal
|
||||
)}
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="px-2.5 py-1.5 rounded text-[12.5px] text-muted-foreground hover:bg-white/[0.08] hover:text-primary"
|
||||
className="px-2.5 py-1.5 rounded text-xs text-muted-foreground hover:bg-white/[0.08] hover:text-primary"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
<button
|
||||
onClick={onApply}
|
||||
className="px-3.5 py-[9px] rounded-lg bg-warning text-[#1a1200] font-semibold text-[12.5px] hover:brightness-110 inline-flex items-center gap-1.5"
|
||||
className="px-3.5 py-[9px] rounded-lg bg-warning text-[#1a1200] font-semibold text-xs hover:brightness-110 inline-flex items-center gap-1.5"
|
||||
>
|
||||
Apply fix
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6" /></svg>
|
||||
@@ -116,27 +113,23 @@ function VerifyingBanner({ fix, onOutcome }: ProposalBannerProps) {
|
||||
: 'Applied'
|
||||
|
||||
return (
|
||||
<div className="relative border-t border-warning/30 bg-gradient-to-b from-warning-dim/40 to-warning-dim/20 px-5 py-3 animate-slide-up">
|
||||
<div className="absolute left-0 top-0 bottom-0 w-[3px] bg-warning" />
|
||||
<div className="border-t border-warning/30 bg-card px-5 py-3 animate-slide-up">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="relative shrink-0 mt-0.5 w-7 h-7 rounded-md border border-warning/30 bg-warning-dim flex items-center justify-center text-warning">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
<span className="absolute inset-[-3px] rounded-lg animate-pulse-amber pointer-events-none" />
|
||||
</div>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-warning shrink-0 mt-1">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 font-heading text-[10px] font-semibold uppercase tracking-[1.2px] text-warning">
|
||||
<div className="flex items-center gap-2 font-heading text-[0.625rem] font-semibold uppercase tracking-[1.2px] text-warning">
|
||||
<span>Verifying</span>
|
||||
<span className="px-2 py-[2px] rounded-full bg-warning/20 text-warning text-[10.5px] font-bold normal-case tracking-normal">
|
||||
<span className="px-2 py-[2px] rounded-full bg-warning/20 text-warning text-[0.625rem] font-bold normal-case tracking-normal">
|
||||
{appliedLabel}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-0.5 text-[14px] font-semibold text-heading leading-snug">
|
||||
<div className="mt-0.5 text-sm font-semibold text-heading leading-snug">
|
||||
Did "{fix.title}" work?
|
||||
</div>
|
||||
<div className="mt-1 text-[12.5px] text-muted-foreground leading-relaxed">
|
||||
<div className="mt-1 text-xs text-muted-foreground leading-relaxed">
|
||||
Mark the outcome so the AI can either close the session with this as the resolution, or propose something else.
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,7 +152,7 @@ function VerifyingBanner({ fix, onOutcome }: ProposalBannerProps) {
|
||||
const notes = window.prompt('What did you run / skip?')
|
||||
if (notes && notes.trim()) onOutcome('applied_partial', notes.trim())
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 text-[12.5px] hover:bg-elevated text-primary"
|
||||
className="w-full text-left px-3 py-2 text-xs hover:bg-elevated text-primary"
|
||||
>
|
||||
Mark partial…
|
||||
</button>
|
||||
@@ -169,7 +162,7 @@ function VerifyingBanner({ fix, onOutcome }: ProposalBannerProps) {
|
||||
const reason = window.prompt('What are you waiting on? (e.g. "client power-cycling router")')
|
||||
if (reason && reason.trim()) onOutcome('applied_pending', reason.trim())
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 text-[12.5px] hover:bg-elevated text-primary inline-flex items-center gap-2"
|
||||
className="w-full text-left px-3 py-2 text-xs hover:bg-elevated text-primary inline-flex items-center gap-2"
|
||||
>
|
||||
<Clock3 size={12} className="text-info" />
|
||||
Waiting to verify…
|
||||
@@ -181,14 +174,14 @@ function VerifyingBanner({ fix, onOutcome }: ProposalBannerProps) {
|
||||
const reason = window.prompt("Why didn't it work? (optional)")
|
||||
onOutcome('applied_failed', reason?.trim() || undefined)
|
||||
}}
|
||||
className="px-3 py-[9px] rounded-lg border border-danger/30 text-danger text-[12.5px] font-medium hover:bg-danger-dim hover:border-danger inline-flex items-center gap-1.5"
|
||||
className="px-3 py-[9px] rounded-lg border border-danger/30 text-danger text-xs font-medium hover:bg-danger-dim hover:border-danger inline-flex items-center gap-1.5"
|
||||
>
|
||||
<X size={12} strokeWidth={2.5} />
|
||||
Didn't work
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onOutcome('applied_success')}
|
||||
className="px-3 py-[9px] rounded-lg bg-success text-[#0a1a12] font-semibold text-[12.5px] hover:brightness-110 inline-flex items-center gap-1.5"
|
||||
className="px-3 py-[9px] rounded-lg bg-success text-[#0a1a12] font-semibold text-xs hover:brightness-110 inline-flex items-center gap-1.5"
|
||||
>
|
||||
<Check size={12} strokeWidth={2.5} />
|
||||
It worked
|
||||
@@ -209,25 +202,22 @@ function formatRelativeMinutes(iso: string): string {
|
||||
|
||||
function PartialBanner({ fix, onOutcome, onApply }: ProposalBannerProps) {
|
||||
return (
|
||||
<div className="relative border-t border-info/30 bg-gradient-to-b from-info-dim/40 to-info-dim/20 px-5 py-3 animate-slide-up">
|
||||
<div className="absolute left-0 top-0 bottom-0 w-[3px] bg-info" />
|
||||
<div className="border-t border-info/30 bg-card px-5 py-3 animate-slide-up">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="shrink-0 mt-0.5 w-7 h-7 rounded-md border border-info/30 bg-info-dim flex items-center justify-center text-info">
|
||||
<Info size={15} />
|
||||
</div>
|
||||
<Info size={16} className="text-info shrink-0 mt-1" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 font-heading text-[10px] font-semibold uppercase tracking-[1.2px] text-info">
|
||||
<div className="flex items-center gap-2 font-heading text-[0.625rem] font-semibold uppercase tracking-[1.2px] text-info">
|
||||
<span>Partially applied</span>
|
||||
<span className="px-2 py-[2px] rounded-full bg-info/20 text-info text-[10.5px] font-bold normal-case tracking-normal">
|
||||
<span className="px-2 py-[2px] rounded-full bg-info/20 text-info text-[0.625rem] font-bold normal-case tracking-normal">
|
||||
Parked
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-0.5 text-[14px] font-semibold text-heading leading-snug">
|
||||
<div className="mt-0.5 text-sm font-semibold text-heading leading-snug">
|
||||
{fix.title}
|
||||
</div>
|
||||
{fix.partial_notes && (
|
||||
<div className="mt-1.5 flex items-center gap-2 px-2.5 py-1.5 rounded-md bg-info/[0.08] border border-info/30 text-[12px] italic text-primary">
|
||||
<span className="not-italic font-bold text-info text-[10.5px] uppercase tracking-[0.6px]">Note</span>
|
||||
<div className="mt-1.5 flex items-center gap-2 px-2.5 py-1.5 rounded-md bg-info/[0.08] border border-info/30 text-xs italic text-primary">
|
||||
<span className="not-italic font-bold text-info text-[0.625rem] uppercase tracking-[0.6px]">Note</span>
|
||||
<span>{fix.partial_notes}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -238,19 +228,19 @@ function PartialBanner({ fix, onOutcome, onApply }: ProposalBannerProps) {
|
||||
const reason = window.prompt("Why didn't it work? (optional)")
|
||||
onOutcome('applied_failed', reason?.trim() || undefined)
|
||||
}}
|
||||
className="px-3 py-[9px] rounded-lg border border-danger/30 text-danger text-[12.5px] font-medium hover:bg-danger-dim hover:border-danger"
|
||||
className="px-3 py-[9px] rounded-lg border border-danger/30 text-danger text-xs font-medium hover:bg-danger-dim hover:border-danger"
|
||||
>
|
||||
Didn't work
|
||||
</button>
|
||||
<button
|
||||
onClick={onApply}
|
||||
className="px-3 py-[9px] rounded-lg bg-card border border-white/10 text-primary text-[12.5px] font-medium hover:bg-elevated"
|
||||
className="px-3 py-[9px] rounded-lg bg-card border border-white/10 text-primary text-xs font-medium hover:bg-elevated"
|
||||
>
|
||||
Finish it ›
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onOutcome('applied_success')}
|
||||
className="px-3 py-[9px] rounded-lg bg-success text-[#0a1a12] font-semibold text-[12.5px] hover:brightness-110"
|
||||
className="px-3 py-[9px] rounded-lg bg-success text-[#0a1a12] font-semibold text-xs hover:brightness-110"
|
||||
>
|
||||
It worked
|
||||
</button>
|
||||
@@ -262,25 +252,22 @@ function PartialBanner({ fix, onOutcome, onApply }: ProposalBannerProps) {
|
||||
|
||||
function PendingBanner({ fix, onOutcome, onDismiss }: ProposalBannerProps) {
|
||||
return (
|
||||
<div className="relative border-t border-info/30 bg-gradient-to-b from-info-dim/40 to-info-dim/20 px-5 py-3 animate-slide-up">
|
||||
<div className="absolute left-0 top-0 bottom-0 w-[3px] bg-info" />
|
||||
<div className="border-t border-info/30 bg-card px-5 py-3 animate-slide-up">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="shrink-0 mt-0.5 w-7 h-7 rounded-md border border-info/30 bg-info-dim flex items-center justify-center text-info">
|
||||
<Clock3 size={15} />
|
||||
</div>
|
||||
<Clock3 size={16} className="text-info shrink-0 mt-1" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 font-heading text-[10px] font-semibold uppercase tracking-[1.2px] text-info">
|
||||
<div className="flex items-center gap-2 font-heading text-[0.625rem] font-semibold uppercase tracking-[1.2px] text-info">
|
||||
<span>Awaiting verification</span>
|
||||
<span className="px-2 py-[2px] rounded-full bg-info/20 text-info text-[10.5px] font-bold normal-case tracking-normal">
|
||||
<span className="px-2 py-[2px] rounded-full bg-info/20 text-info text-[0.625rem] font-bold normal-case tracking-normal">
|
||||
Parked
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-0.5 text-[14px] font-semibold text-heading leading-snug">
|
||||
<div className="mt-0.5 text-sm font-semibold text-heading leading-snug">
|
||||
{fix.title}
|
||||
</div>
|
||||
{fix.pending_reason && (
|
||||
<div className="mt-1.5 flex items-center gap-2 px-2.5 py-1.5 rounded-md bg-info/[0.08] border border-info/30 text-[12px] italic text-primary">
|
||||
<span className="not-italic font-bold text-info text-[10.5px] uppercase tracking-[0.6px]">Waiting on</span>
|
||||
<div className="mt-1.5 flex items-center gap-2 px-2.5 py-1.5 rounded-md bg-info/[0.08] border border-info/30 text-xs italic text-primary">
|
||||
<span className="not-italic font-bold text-info text-[0.625rem] uppercase tracking-[0.6px]">Waiting on</span>
|
||||
<span>{fix.pending_reason}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -288,7 +275,7 @@ function PendingBanner({ fix, onOutcome, onDismiss }: ProposalBannerProps) {
|
||||
<div className="flex items-center gap-2 shrink-0 pt-0.5">
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="px-3 py-[9px] rounded-lg text-muted-foreground text-[12.5px] hover:bg-white/[0.08] hover:text-primary"
|
||||
className="px-3 py-[9px] rounded-lg text-muted-foreground text-xs hover:bg-white/[0.08] hover:text-primary"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
@@ -300,7 +287,7 @@ function PendingBanner({ fix, onOutcome, onDismiss }: ProposalBannerProps) {
|
||||
)
|
||||
if (reason && reason.trim()) onOutcome('applied_pending', reason.trim())
|
||||
}}
|
||||
className="px-3 py-[9px] rounded-lg text-muted-foreground text-[12.5px] hover:bg-white/[0.08] hover:text-primary"
|
||||
className="px-3 py-[9px] rounded-lg text-muted-foreground text-xs hover:bg-white/[0.08] hover:text-primary"
|
||||
>
|
||||
Update reason
|
||||
</button>
|
||||
@@ -309,13 +296,13 @@ function PendingBanner({ fix, onOutcome, onDismiss }: ProposalBannerProps) {
|
||||
const reason = window.prompt("Why didn't it work? (optional)")
|
||||
onOutcome('applied_failed', reason?.trim() || undefined)
|
||||
}}
|
||||
className="px-3 py-[9px] rounded-lg border border-danger/30 text-danger text-[12.5px] font-medium hover:bg-danger-dim hover:border-danger"
|
||||
className="px-3 py-[9px] rounded-lg border border-danger/30 text-danger text-xs font-medium hover:bg-danger-dim hover:border-danger"
|
||||
>
|
||||
Didn't work
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onOutcome('applied_success')}
|
||||
className="px-3 py-[9px] rounded-lg bg-success text-[#0a1a12] font-semibold text-[12.5px] hover:brightness-110 inline-flex items-center gap-1.5"
|
||||
className="px-3 py-[9px] rounded-lg bg-success text-[#0a1a12] font-semibold text-xs hover:brightness-110 inline-flex items-center gap-1.5"
|
||||
>
|
||||
<Check size={12} strokeWidth={2.5} />
|
||||
It worked
|
||||
@@ -339,37 +326,34 @@ function AIConfirmingBanner({ fix, onAcceptAIProposal, onRejectAIProposal }: Pro
|
||||
: 'was partially applied'
|
||||
|
||||
return (
|
||||
<div className="relative border-t border-accent/30 bg-gradient-to-b from-accent-dim/40 to-accent-dim/20 px-5 py-3 animate-slide-up">
|
||||
<div className="absolute left-0 top-0 bottom-0 w-[3px] bg-accent" />
|
||||
<div className="border-t border-accent/30 bg-card px-5 py-3 animate-slide-up">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="shrink-0 mt-0.5 w-7 h-7 rounded-md border border-accent/30 bg-accent-dim flex items-center justify-center text-accent">
|
||||
<Sparkles size={15} />
|
||||
</div>
|
||||
<Sparkles size={16} className="text-accent shrink-0 mt-1" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 font-heading text-[10px] font-semibold uppercase tracking-[1.2px] text-accent">
|
||||
<div className="flex items-center gap-2 font-heading text-[0.625rem] font-semibold uppercase tracking-[1.2px] text-accent">
|
||||
<span>AI detected outcome</span>
|
||||
<span className="px-2 py-[2px] rounded-full bg-accent/20 text-accent text-[10.5px] font-bold normal-case tracking-normal">
|
||||
<span className="px-2 py-[2px] rounded-full bg-accent/20 text-accent text-[0.625rem] font-bold normal-case tracking-normal">
|
||||
{isSuccess ? 'Success' : isFailure ? 'Failure' : 'Partial'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-0.5 text-[14px] font-semibold text-heading leading-snug">
|
||||
<div className="mt-0.5 text-sm font-semibold text-heading leading-snug">
|
||||
AI thinks the fix {headlineVerb} — confirm?
|
||||
</div>
|
||||
<div className="mt-1 text-[12.5px] text-muted-foreground leading-relaxed">
|
||||
<div className="mt-1 text-xs text-muted-foreground leading-relaxed">
|
||||
{proposal.reason || 'Based on the recent chat. One click either confirms or corrects.'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0 pt-0.5">
|
||||
<button
|
||||
onClick={onRejectAIProposal}
|
||||
className="px-3 py-[9px] rounded-lg text-muted-foreground text-[12.5px] hover:bg-white/[0.08] hover:text-primary"
|
||||
className="px-3 py-[9px] rounded-lg text-muted-foreground text-xs hover:bg-white/[0.08] hover:text-primary"
|
||||
>
|
||||
Not yet
|
||||
</button>
|
||||
<button
|
||||
onClick={onAcceptAIProposal}
|
||||
className={cn(
|
||||
'px-3 py-[9px] rounded-lg font-semibold text-[12.5px] inline-flex items-center gap-1.5 hover:brightness-110',
|
||||
'px-3 py-[9px] rounded-lg font-semibold text-xs inline-flex items-center gap-1.5 hover:brightness-110',
|
||||
isSuccess
|
||||
? 'bg-success text-[#0a1a12]'
|
||||
: 'bg-danger text-[#180808]',
|
||||
@@ -386,14 +370,13 @@ function AIConfirmingBanner({ fix, onAcceptAIProposal, onRejectAIProposal }: Pro
|
||||
|
||||
function NudgeBanner({ fix, onOutcome, onSilenceNudge }: ProposalBannerProps) {
|
||||
return (
|
||||
<div className="relative border-t border-warning/30 bg-warning-dim/60 px-5 py-2 flex items-center gap-3">
|
||||
<div className="absolute left-0 top-0 bottom-0 w-[3px] bg-warning" />
|
||||
<div className="border-t border-warning/30 bg-card px-5 py-2 flex items-center gap-3">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-warning shrink-0">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 8v4" />
|
||||
<path d="M12 16h.01" />
|
||||
</svg>
|
||||
<span className="flex-1 text-[12.5px] text-primary">
|
||||
<span className="flex-1 text-xs text-primary">
|
||||
Did <strong className="text-heading">"{fix.title}"</strong> work?
|
||||
</span>
|
||||
<button
|
||||
@@ -407,7 +390,7 @@ function NudgeBanner({ fix, onOutcome, onSilenceNudge }: ProposalBannerProps) {
|
||||
onSilenceNudge()
|
||||
}
|
||||
}}
|
||||
className="px-2.5 py-1 rounded text-[12px] text-muted-foreground hover:bg-white/[0.08] hover:text-primary inline-flex items-center gap-1"
|
||||
className="px-2.5 py-1 rounded text-xs text-muted-foreground hover:bg-white/[0.08] hover:text-primary inline-flex items-center gap-1"
|
||||
>
|
||||
<Clock3 size={11} />
|
||||
Still checking
|
||||
@@ -417,13 +400,13 @@ function NudgeBanner({ fix, onOutcome, onSilenceNudge }: ProposalBannerProps) {
|
||||
const reason = window.prompt("Why didn't it work? (optional)")
|
||||
onOutcome('applied_failed', reason?.trim() || undefined)
|
||||
}}
|
||||
className="px-2.5 py-1 rounded border border-danger/30 text-danger text-[12px] hover:bg-danger-dim"
|
||||
className="px-2.5 py-1 rounded border border-danger/30 text-danger text-xs hover:bg-danger-dim"
|
||||
>
|
||||
No
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onOutcome('applied_success')}
|
||||
className="px-2.5 py-1 rounded bg-success text-[#0a1a12] font-semibold text-[12px] hover:brightness-110"
|
||||
className="px-2.5 py-1 rounded bg-success text-[#0a1a12] font-semibold text-xs hover:brightness-110"
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
@@ -435,15 +418,14 @@ function CollapsedBanner({ fix, onToggleCollapsed }: ProposalBannerProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onToggleCollapsed}
|
||||
className="relative w-full border-t border-warning/30 bg-warning-dim/40 px-5 py-2 flex items-center gap-2.5 hover:bg-warning-dim/60 transition-colors text-left"
|
||||
className="w-full border-t border-warning/30 bg-card px-5 py-2 flex items-center gap-2.5 hover:bg-[var(--color-bg-card-hover)] transition-colors text-left"
|
||||
>
|
||||
<div className="absolute left-0 top-0 bottom-0 w-[3px] bg-warning" />
|
||||
<Sparkles size={12} className="text-warning shrink-0" />
|
||||
<span className="flex-1 text-[12px] font-medium text-heading truncate">{fix.title}</span>
|
||||
<span className="px-1.5 py-[1px] rounded-full bg-warning/20 text-warning text-[10.5px] font-bold tabular-nums">
|
||||
<span className="flex-1 text-xs font-medium text-heading truncate">{fix.title}</span>
|
||||
<span className="px-1.5 py-[1px] rounded-full bg-warning/20 text-warning text-[0.625rem] font-bold tabular-nums">
|
||||
{fix.confidence_pct}%
|
||||
</span>
|
||||
<span className="text-muted-foreground text-[11px]">▸ expand</span>
|
||||
<span className="text-muted-foreground text-[0.6875rem]">▸ expand</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function AddNoteButton({ onAdd }: AddNoteButtonProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="flex items-center gap-1.5 text-[0.75rem] text-muted-foreground hover:text-accent-text transition-colors pl-1 mt-1"
|
||||
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-accent-text transition-colors pl-1 mt-1"
|
||||
>
|
||||
<Plus size={12} />
|
||||
Add a note
|
||||
@@ -62,20 +62,20 @@ export function AddNoteButton({ onAdd }: AddNoteButtonProps) {
|
||||
value={summary}
|
||||
onChange={(e) => setSummary(e.target.value)}
|
||||
placeholder="Short label (optional)"
|
||||
className="mt-1.5 w-full rounded-md border border-default bg-input px-2.5 py-1 text-[0.75rem] text-heading placeholder:text-muted-foreground focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30"
|
||||
className="mt-1.5 w-full rounded-md border border-default bg-input px-2.5 py-1 text-xs text-heading placeholder:text-muted-foreground focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30"
|
||||
/>
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={busy || !text.trim()}
|
||||
className="flex items-center gap-1 rounded-md bg-accent px-2.5 py-1 text-[0.75rem] font-medium text-white disabled:opacity-40 hover:bg-accent-hover transition-colors"
|
||||
className="flex items-center gap-1 rounded-md bg-accent px-2.5 py-1 text-xs font-medium text-white disabled:opacity-40 hover:bg-accent-hover transition-colors"
|
||||
>
|
||||
<Check size={11} /> Add
|
||||
</button>
|
||||
<button
|
||||
onClick={reset}
|
||||
disabled={busy}
|
||||
className="text-[0.75rem] text-muted-foreground hover:text-heading"
|
||||
className="text-xs text-muted-foreground hover:text-heading"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
* and renders the section. Loading/refresh logic lives in the parent
|
||||
* (AssistantChatPage) so it can coordinate with the chat send cycle.
|
||||
*/
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useState } from 'react'
|
||||
import { ChevronDown, ChevronRight, Loader2 } from 'lucide-react'
|
||||
import type { SessionFact } from '@/api/sessionFacts'
|
||||
import { WhatWeKnowItem } from './WhatWeKnowItem'
|
||||
import { AddNoteButton } from './AddNoteButton'
|
||||
@@ -23,32 +23,65 @@ interface WhatWeKnowProps {
|
||||
onUpdateFact: (factId: string, text: string, summary: string | null) => Promise<void> | void
|
||||
onDeleteFact: (factId: string) => Promise<void> | void
|
||||
loading?: boolean
|
||||
/** Used as the sessionStorage key for the engineer's collapse preference.
|
||||
* When the parent re-keys this component on session change, the lazy
|
||||
* initializer reads fresh state for the new session. */
|
||||
sessionId?: string | null
|
||||
}
|
||||
|
||||
const COLLAPSE_STORAGE_KEY = 'rf-whatweknow-collapsed'
|
||||
// First-render auto-collapse threshold. Past this, the section is hidden by
|
||||
// default so Questions / Diagnostic Checks stay above the fold. The engineer's
|
||||
// explicit toggle (stored per-session) always wins over this heuristic.
|
||||
const AUTO_COLLAPSE_THRESHOLD = 5
|
||||
|
||||
export function WhatWeKnow({
|
||||
facts,
|
||||
onAddNote,
|
||||
onUpdateFact,
|
||||
onDeleteFact,
|
||||
loading,
|
||||
sessionId,
|
||||
}: WhatWeKnowProps) {
|
||||
const count = facts.length
|
||||
|
||||
const [collapsed, setCollapsed] = useState<boolean>(() => {
|
||||
if (sessionId) {
|
||||
try {
|
||||
const stored = sessionStorage.getItem(`${COLLAPSE_STORAGE_KEY}:${sessionId}`)
|
||||
if (stored !== null) return stored === '1'
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
return count >= AUTO_COLLAPSE_THRESHOLD
|
||||
})
|
||||
|
||||
const toggle = () => {
|
||||
setCollapsed(prev => {
|
||||
const next = !prev
|
||||
if (sessionId) {
|
||||
try { sessionStorage.setItem(`${COLLAPSE_STORAGE_KEY}:${sessionId}`, next ? '1' : '0') } catch { /* ignore */ }
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
'rounded-lg p-3 -mx-1 mb-1',
|
||||
// Subtle green-to-transparent gradient distinguishes this section
|
||||
// from the rest of the lane (mockup 01-session-primary.png).
|
||||
'bg-gradient-to-b from-success/[0.05] to-transparent',
|
||||
)}
|
||||
>
|
||||
<div className="pb-2">
|
||||
<div className="flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[1.2px] text-muted-foreground pl-0.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-success" />
|
||||
What we know
|
||||
<span className="text-muted-foreground">·</span>
|
||||
<span className="tabular-nums">{count}</span>
|
||||
<section className="rounded-lg p-3 -mx-1 mb-1">
|
||||
<div className={collapsed ? '' : 'pb-2'}>
|
||||
<div className="flex items-center gap-2 pl-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
aria-expanded={!collapsed}
|
||||
aria-label={collapsed ? 'Expand What we know' : 'Collapse What we know'}
|
||||
className="flex items-center gap-2 text-[0.625rem] font-semibold uppercase tracking-[1.2px] text-muted-foreground hover:text-heading transition-colors"
|
||||
>
|
||||
{collapsed ? <ChevronRight size={10} /> : <ChevronDown size={10} />}
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-success" />
|
||||
What we know
|
||||
<span className="text-muted-foreground">·</span>
|
||||
<span className="tabular-nums">{count}</span>
|
||||
</button>
|
||||
{loading && (
|
||||
<span
|
||||
className="ml-auto flex items-center gap-1 text-[0.625rem] font-medium normal-case tracking-normal text-muted-foreground"
|
||||
@@ -61,29 +94,33 @@ export function WhatWeKnow({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{count === 0 && loading && (
|
||||
<div className="space-y-2 px-1 py-2">
|
||||
<div className="h-3 w-3/4 rounded bg-elevated/60 animate-pulse" />
|
||||
<div className="h-3 w-1/2 rounded bg-elevated/60 animate-pulse" />
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<>
|
||||
{count === 0 && loading && (
|
||||
<div className="space-y-2 px-1 py-2">
|
||||
<div className="h-3 w-3/4 rounded bg-elevated/60 animate-pulse" />
|
||||
<div className="h-3 w-1/2 rounded bg-elevated/60 animate-pulse" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{count === 0 && !loading && (
|
||||
<div className="text-xs text-muted-foreground italic px-1 py-2">
|
||||
Nothing confirmed yet — facts appear here as the engineer answers questions and runs checks.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{facts.map((fact) => (
|
||||
<WhatWeKnowItem
|
||||
key={fact.id}
|
||||
fact={fact}
|
||||
onSave={(text, summary) => onUpdateFact(fact.id, text, summary)}
|
||||
onDelete={() => onDeleteFact(fact.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
<AddNoteButton onAdd={onAddNote} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{count === 0 && !loading && (
|
||||
<div className="text-[0.75rem] text-muted-foreground italic px-1 py-2">
|
||||
Nothing confirmed yet — facts appear here as the engineer answers questions and runs checks.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{facts.map((fact) => (
|
||||
<WhatWeKnowItem
|
||||
key={fact.id}
|
||||
fact={fact}
|
||||
onSave={(text, summary) => onUpdateFact(fact.id, text, summary)}
|
||||
onDelete={() => onDeleteFact(fact.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
<AddNoteButton onAdd={onAddNote} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -79,20 +79,20 @@ export function WhatWeKnowItem({ fact, onSave, onDelete }: WhatWeKnowItemProps)
|
||||
value={draftSummary}
|
||||
onChange={(e) => setDraftSummary(e.target.value)}
|
||||
placeholder="Short label (e.g. 'rules out tenant/license')"
|
||||
className="mt-1.5 w-full rounded-md border border-default bg-input px-2.5 py-1 text-[0.75rem] text-heading placeholder:text-muted-foreground focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30"
|
||||
className="mt-1.5 w-full rounded-md border border-default bg-input px-2.5 py-1 text-xs text-heading placeholder:text-muted-foreground focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30"
|
||||
/>
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={busy || !draftText.trim()}
|
||||
className="flex items-center gap-1 rounded-md bg-accent px-2.5 py-1 text-[0.75rem] font-medium text-white disabled:opacity-40 hover:bg-accent-hover transition-colors"
|
||||
className="flex items-center gap-1 rounded-md bg-accent px-2.5 py-1 text-xs font-medium text-white disabled:opacity-40 hover:bg-accent-hover transition-colors"
|
||||
>
|
||||
<Check size={11} /> Save
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
disabled={busy}
|
||||
className="text-[0.75rem] text-muted-foreground hover:text-heading"
|
||||
className="text-xs text-muted-foreground hover:text-heading"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -104,7 +104,7 @@ export function WhatWeKnowItem({ fact, onSave, onDelete }: WhatWeKnowItemProps)
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group rounded-lg border-l-[3px] border-l-success border border-success/25 bg-success-dim/20 p-3 mb-2',
|
||||
'group rounded-lg border border-default/40 p-3 mb-2',
|
||||
busy && 'opacity-60',
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user