refactor: assistant page — TaskLane UX + ChatSidebar improvements

TaskLane:
- Simplify action buttons: merge "Paste Result"/"Type Answer" into single
  primary button, make "Skip" icon-only (reduces decision points from 3→1)
- Strengthen done state: solid left-border success green + checkmark icon
  instead of faint tint that nearly disappears
- Boost progress bar: 3px→5px, better contrast colors, inline count label
- Differentiate from ChatSidebar: use bg-page instead of bg-sidebar,
  add accent top-border to signal "active workspace"
- Make skipped tasks clickable to un-skip (matching done→reopen pattern)
- Fix slide-in animation: add slide-in-from-right keyframe
- Fix duplicate style props, stray quote from replace_all
- Consolidate 6 grip dot divs to Array.from loop

ChatSidebar:
- Add inline delete confirmation ("Delete? Yes / No") instead of
  immediate destructive action
- Fix text-xs text-[0.625rem] double class conflict on Pinned header

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-29 17:47:58 -04:00
parent cc51d21300
commit 37179096b0
3 changed files with 102 additions and 70 deletions

View File

@@ -1,3 +1,4 @@
import { useState } from 'react'
import { Plus, Pin, Trash2, MessageSquare, History, X } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { ChatListItem } from '@/types/assistant-chat'
@@ -84,7 +85,7 @@ export function ChatSidebar({
<div className="flex-1 overflow-y-auto py-2">
{pinnedChats.length > 0 && (
<div className="px-3 mb-1">
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-muted-foreground">
<span className="font-sans text-[0.625rem] uppercase tracking-widest text-muted-foreground">
Pinned
</span>
</div>
@@ -184,39 +185,65 @@ function ChatItem({
onDelete: () => void
onTogglePin: () => void
}) {
const [confirming, setConfirming] = useState(false)
return (
<div
onClick={onSelect}
className={cn(
'group flex items-center gap-2 px-3 py-2.5 mx-1.5 rounded-lg cursor-pointer transition-colors',
isActive
? 'bg-accent-dim text-foreground'
: 'text-muted-foreground hover:bg-input hover:text-foreground'
confirming
? 'bg-rose-500/10 border border-rose-500/20'
: isActive
? 'bg-accent-dim text-foreground'
: 'text-muted-foreground hover:bg-input hover:text-foreground'
)}
>
<MessageSquare size={14} className="shrink-0" />
<div className="flex-1 min-w-0">
<div className="text-[0.8125rem] font-medium truncate">{chat.title}</div>
<div className="text-[0.6875rem] text-muted-foreground">
{chat.message_count} messages
{confirming ? (
<div className="flex items-center gap-2">
<span className="text-[0.75rem] text-rose-400 font-medium">Delete?</span>
<button
onClick={e => { e.stopPropagation(); onDelete(); setConfirming(false) }}
className="text-[0.6875rem] font-medium text-rose-400 hover:text-rose-300 px-1.5 py-0.5 rounded bg-rose-500/15 hover:bg-rose-500/25 transition-colors"
>
Yes
</button>
<button
onClick={e => { e.stopPropagation(); setConfirming(false) }}
className="text-[0.6875rem] text-muted-foreground hover:text-foreground px-1.5 py-0.5"
>
No
</button>
</div>
) : (
<>
<div className="text-[0.8125rem] font-medium truncate">{chat.title}</div>
<div className="text-[0.6875rem] text-muted-foreground">
{chat.message_count} messages
</div>
</>
)}
</div>
{!confirming && (
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={e => { e.stopPropagation(); onTogglePin() }}
className="p-1 rounded hover:bg-white/[0.08]"
title={chat.pinned ? 'Unpin' : 'Pin'}
>
<Pin size={12} className={chat.pinned ? 'text-primary' : ''} />
</button>
<button
onClick={e => { e.stopPropagation(); setConfirming(true) }}
className="p-1 rounded hover:bg-white/[0.08] text-muted-foreground hover:text-rose-400"
title="Delete"
>
<Trash2 size={12} />
</button>
</div>
</div>
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={e => { e.stopPropagation(); onTogglePin() }}
className="p-1 rounded hover:bg-white/[0.08]"
title={chat.pinned ? 'Unpin' : 'Pin'}
>
<Pin size={12} className={chat.pinned ? 'text-primary' : ''} />
</button>
<button
onClick={e => { e.stopPropagation(); onDelete() }}
className="p-1 rounded hover:bg-white/[0.08] text-muted-foreground hover:text-rose-400"
title="Delete"
>
<Trash2 size={12} />
</button>
</div>
)}
</div>
)
}

View File

@@ -225,8 +225,8 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
return (
<div
className="relative bg-sidebar border-l border-default flex flex-col shrink-0 animate-in slide-in-from-right-4 duration-200"
style={{ width: panelWidth }}
className="relative border-l border-default flex flex-col shrink-0 animate-slide-in-right"
style={{ background: 'var(--color-bg-page)', width: panelWidth }}
>
{/* Resize grip handle */}
<div
@@ -234,16 +234,13 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
className="absolute left-0 top-0 bottom-0 w-[6px] cursor-col-resize z-20 flex items-center justify-center group hover:bg-accent/10 transition-colors"
>
<div className="flex flex-col gap-[3px] opacity-0 group-hover:opacity-100 transition-opacity">
<div className="w-[3px] h-[3px] rounded-full bg-muted-foreground" />
<div className="w-[3px] h-[3px] rounded-full bg-muted-foreground" />
<div className="w-[3px] h-[3px] rounded-full bg-muted-foreground" />
<div className="w-[3px] h-[3px] rounded-full bg-muted-foreground" />
<div className="w-[3px] h-[3px] rounded-full bg-muted-foreground" />
<div className="w-[3px] h-[3px] rounded-full bg-muted-foreground" />
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="w-[3px] h-[3px] rounded-full bg-muted-foreground" />
))}
</div>
</div>
{/* Header */}
<div className="px-4 py-3 border-b border-default flex items-center justify-between shrink-0">
<div className="px-4 py-3 border-b border-default flex items-center justify-between shrink-0" style={{ borderTop: '2px solid var(--color-accent)' }}>
<h3 className="font-heading text-sm font-bold text-heading flex items-center gap-2">
Tasks
<span className={cn(
@@ -266,7 +263,7 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
{/* ── Questions Section ── */}
{questionTasks.length > 0 && (
<section>
<div className="sticky top-0 z-10 bg-sidebar pb-2">
<div className="sticky top-0 z-10 pb-2" style={{ background: 'var(--color-bg-page)' }}>
<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-accent" />
Questions
@@ -282,16 +279,19 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
if (q.state === 'done') {
return (
<div key={idx} className="rounded-lg border border-success/20 bg-success-dim/20 p-3 mb-2 cursor-pointer hover:border-success/40 transition-colors" onClick={() => updateTask(idx, { state: 'active' })}>
<div className="text-[0.8125rem] text-muted-foreground">{q.text}</div>
<div className="text-[0.75rem] text-muted-foreground mt-1 italic">"{q.value}"</div>
<div key={idx} className="rounded-lg border-l-[3px] border-l-success border border-success/25 bg-success-dim/30 p-3 mb-2 cursor-pointer hover:border-success/40 transition-colors" onClick={() => updateTask(idx, { state: 'active' })}>
<div className="flex items-center gap-1.5">
<Check size={12} className="text-success shrink-0" />
<span className="text-[0.8125rem] text-foreground">{q.text}</span>
</div>
<div className="text-[0.75rem] text-muted-foreground mt-1 pl-5 italic truncate">"{q.value}"</div>
</div>
)
}
if (q.state === 'skipped') {
return (
<div key={idx} className="rounded-lg border border-default/50 bg-elevated/20 p-3 mb-2 opacity-50">
<div key={idx} className="rounded-lg border border-default/50 bg-elevated/20 p-3 mb-2 opacity-60 cursor-pointer hover:opacity-80 hover:border-default transition-all" onClick={() => updateTask(idx, { state: 'pending' })} title="Click to restore">
<div className="flex justify-between">
<div className="text-[0.8125rem] text-muted-foreground line-through">{q.text}</div>
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Skipped</span>
@@ -342,9 +342,10 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
</button>
<button
onClick={() => updateTask(idx, { state: 'skipped' })}
className="flex items-center gap-1 rounded-md px-2.5 py-1.5 text-[0.75rem] text-muted-foreground hover:text-heading"
className="p-1.5 rounded-md text-muted-foreground hover:text-heading hover:bg-elevated/50 transition-colors"
title="Skip this question"
>
<SkipForward size={11} /> Skip
<SkipForward size={13} />
</button>
</div>
)}
@@ -357,7 +358,7 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
{/* ── Checks Section ── */}
{actionTasks.length > 0 && (
<section>
<div className="sticky top-0 z-10 bg-sidebar pb-2">
<div className="sticky top-0 z-10 pb-2" style={{ background: 'var(--color-bg-page)' }}>
<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-[#60a5fa]" />
Diagnostic Checks
@@ -401,10 +402,10 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
if (a.state === 'done') {
return (
<div key={idx} className="rounded-lg border border-success/20 bg-success-dim/20 p-3 mb-2 cursor-pointer hover:border-success/40 transition-colors" onClick={() => updateTask(idx, { state: 'active' })}>
<div className="flex justify-between">
<div className="text-[0.8125rem] font-medium text-heading">{a.label}</div>
<span className="text-[10px] font-semibold uppercase tracking-wider text-success"> Done</span>
<div key={idx} className="rounded-lg border-l-[3px] border-l-success border border-success/25 bg-success-dim/30 p-3 mb-2 cursor-pointer hover:border-success/40 transition-colors" onClick={() => updateTask(idx, { state: 'active' })}>
<div className="flex items-center gap-1.5">
<Check size={12} className="text-success shrink-0" />
<span className="text-[0.8125rem] font-medium text-foreground flex-1">{a.label}</span>
</div>
</div>
)
@@ -412,7 +413,7 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
if (a.state === 'skipped') {
return (
<div key={idx} className="rounded-lg border border-default/50 bg-elevated/20 p-3 mb-2 opacity-50">
<div key={idx} className="rounded-lg border border-default/50 bg-elevated/20 p-3 mb-2 opacity-60 cursor-pointer hover:opacity-80 hover:border-default transition-all" onClick={() => updateTask(idx, { state: 'pending' })} title="Click to restore">
<div className="flex justify-between">
<div className="text-[0.8125rem] text-muted-foreground line-through">{a.label}</div>
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Skipped</span>
@@ -464,24 +465,19 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
</div>
</div>
) : (
<div className="mt-2 flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
<div className="mt-2 flex items-center gap-2">
<button
onClick={() => updateTask(idx, { state: 'active' })}
className="flex items-center justify-center gap-1 rounded-md border border-accent/40 bg-accent-dim/30 px-2.5 py-1.5 text-[0.75rem] font-medium text-accent-text hover:bg-accent-dim/50 transition-colors"
className="flex items-center gap-1 rounded-md border border-accent/40 bg-accent-dim/30 px-2.5 py-1.5 text-[0.75rem] font-medium text-accent-text hover:bg-accent-dim/50 transition-colors"
>
<Clipboard size={11} /> Paste Result
</button>
<button
onClick={() => updateTask(idx, { state: 'active' })}
className="flex items-center justify-center gap-1 rounded-md border border-default bg-elevated/50 px-2.5 py-1.5 text-[0.75rem] font-medium text-heading hover:bg-elevated transition-colors"
>
Type Answer
<Clipboard size={11} /> {a.command ? 'Paste Output' : 'Answer'}
</button>
<button
onClick={() => updateTask(idx, { state: 'skipped' })}
className="flex items-center justify-center gap-1 rounded-md px-2.5 py-1.5 text-[0.75rem] text-muted-foreground hover:text-heading"
className="p-1.5 rounded-md text-muted-foreground hover:text-heading hover:bg-elevated/50 transition-colors"
title="Skip this check"
>
<SkipForward size={11} /> Skip
<SkipForward size={13} />
</button>
</div>
)}
@@ -495,19 +491,24 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
{/* Footer */}
<div className="p-3 border-t border-default shrink-0">
{/* Progress bar */}
<div className="flex gap-1 mb-2">
{tasks.map((t, i) => (
<div
key={i}
className={cn(
'flex-1 h-[3px] rounded-full',
t.state === 'done' ? 'bg-success' :
t.state === 'skipped' ? 'bg-muted' :
t.state === 'active' ? 'bg-accent' :
'bg-elevated'
)}
/>
))}
<div className="flex items-center gap-2 mb-2">
<div className="flex gap-1 flex-1">
{tasks.map((t, i) => (
<div
key={i}
className={cn(
'flex-1 h-[5px] rounded-full transition-colors',
t.state === 'done' ? 'bg-success' :
t.state === 'skipped' ? 'bg-muted-foreground/30' :
t.state === 'active' ? 'bg-accent' :
'bg-border-default'
)}
/>
))}
</div>
<span className="text-[0.625rem] font-medium text-muted-foreground tabular-nums shrink-0">
{handledCount}/{totalCount}
</span>
</div>
{/* Collapsible preview */}
{anyHandled && (

View File

@@ -82,6 +82,7 @@
--animate-fade-in: fade-in 200ms ease-out both;
--animate-fade-in-up: fade-in-up 200ms ease-out both;
--animate-slide-in-left: slide-in-from-left 200ms ease-out;
--animate-slide-in-right: slide-in-from-right 200ms ease-out both;
--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;
@@ -95,6 +96,9 @@
@keyframes slide-in-from-left {
from { transform: translateX(-100%); } to { transform: translateX(0); }
}
@keyframes slide-in-from-right {
from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: translateX(0); }
}
@keyframes slide-in-from-bottom {
from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); }
}