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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user