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 { Plus, Pin, Trash2, MessageSquare, History, X } from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import type { ChatListItem } from '@/types/assistant-chat'
|
import type { ChatListItem } from '@/types/assistant-chat'
|
||||||
@@ -84,7 +85,7 @@ export function ChatSidebar({
|
|||||||
<div className="flex-1 overflow-y-auto py-2">
|
<div className="flex-1 overflow-y-auto py-2">
|
||||||
{pinnedChats.length > 0 && (
|
{pinnedChats.length > 0 && (
|
||||||
<div className="px-3 mb-1">
|
<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
|
Pinned
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,39 +185,65 @@ function ChatItem({
|
|||||||
onDelete: () => void
|
onDelete: () => void
|
||||||
onTogglePin: () => void
|
onTogglePin: () => void
|
||||||
}) {
|
}) {
|
||||||
|
const [confirming, setConfirming] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group flex items-center gap-2 px-3 py-2.5 mx-1.5 rounded-lg cursor-pointer transition-colors',
|
'group flex items-center gap-2 px-3 py-2.5 mx-1.5 rounded-lg cursor-pointer transition-colors',
|
||||||
isActive
|
confirming
|
||||||
? 'bg-accent-dim text-foreground'
|
? 'bg-rose-500/10 border border-rose-500/20'
|
||||||
: 'text-muted-foreground hover:bg-input hover:text-foreground'
|
: isActive
|
||||||
|
? 'bg-accent-dim text-foreground'
|
||||||
|
: 'text-muted-foreground hover:bg-input hover:text-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<MessageSquare size={14} className="shrink-0" />
|
<MessageSquare size={14} className="shrink-0" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-[0.8125rem] font-medium truncate">{chat.title}</div>
|
{confirming ? (
|
||||||
<div className="text-[0.6875rem] text-muted-foreground">
|
<div className="flex items-center gap-2">
|
||||||
{chat.message_count} messages
|
<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>
|
)}
|
||||||
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,8 +225,8 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative bg-sidebar border-l border-default flex flex-col shrink-0 animate-in slide-in-from-right-4 duration-200"
|
className="relative border-l border-default flex flex-col shrink-0 animate-slide-in-right"
|
||||||
style={{ width: panelWidth }}
|
style={{ background: 'var(--color-bg-page)', width: panelWidth }}
|
||||||
>
|
>
|
||||||
{/* Resize grip handle */}
|
{/* Resize grip handle */}
|
||||||
<div
|
<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"
|
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="flex flex-col gap-[3px] opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<div className="w-[3px] h-[3px] rounded-full bg-muted-foreground" />
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
<div className="w-[3px] h-[3px] rounded-full bg-muted-foreground" />
|
<div key={i} 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Header */}
|
{/* 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">
|
<h3 className="font-heading text-sm font-bold text-heading flex items-center gap-2">
|
||||||
Tasks
|
Tasks
|
||||||
<span className={cn(
|
<span className={cn(
|
||||||
@@ -266,7 +263,7 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
|
|||||||
{/* ── Questions Section ── */}
|
{/* ── Questions Section ── */}
|
||||||
{questionTasks.length > 0 && (
|
{questionTasks.length > 0 && (
|
||||||
<section>
|
<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">
|
<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" />
|
<span className="w-1.5 h-1.5 rounded-full bg-accent" />
|
||||||
Questions
|
Questions
|
||||||
@@ -282,16 +279,19 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
|
|||||||
|
|
||||||
if (q.state === 'done') {
|
if (q.state === 'done') {
|
||||||
return (
|
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 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="text-[0.8125rem] text-muted-foreground">{q.text}</div>
|
<div className="flex items-center gap-1.5">
|
||||||
<div className="text-[0.75rem] text-muted-foreground mt-1 italic">"{q.value}"</div>
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (q.state === 'skipped') {
|
if (q.state === 'skipped') {
|
||||||
return (
|
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="flex justify-between">
|
||||||
<div className="text-[0.8125rem] text-muted-foreground line-through">{q.text}</div>
|
<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>
|
<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>
|
||||||
<button
|
<button
|
||||||
onClick={() => updateTask(idx, { state: 'skipped' })}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -357,7 +358,7 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
|
|||||||
{/* ── Checks Section ── */}
|
{/* ── Checks Section ── */}
|
||||||
{actionTasks.length > 0 && (
|
{actionTasks.length > 0 && (
|
||||||
<section>
|
<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">
|
<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]" />
|
<span className="w-1.5 h-1.5 rounded-full bg-[#60a5fa]" />
|
||||||
Diagnostic Checks
|
Diagnostic Checks
|
||||||
@@ -401,10 +402,10 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
|
|||||||
|
|
||||||
if (a.state === 'done') {
|
if (a.state === 'done') {
|
||||||
return (
|
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 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 justify-between">
|
<div className="flex items-center gap-1.5">
|
||||||
<div className="text-[0.8125rem] font-medium text-heading">{a.label}</div>
|
<Check size={12} className="text-success shrink-0" />
|
||||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-success">✓ Done</span>
|
<span className="text-[0.8125rem] font-medium text-foreground flex-1">{a.label}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -412,7 +413,7 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
|
|||||||
|
|
||||||
if (a.state === 'skipped') {
|
if (a.state === 'skipped') {
|
||||||
return (
|
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="flex justify-between">
|
||||||
<div className="text-[0.8125rem] text-muted-foreground line-through">{a.label}</div>
|
<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>
|
<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>
|
</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
|
<button
|
||||||
onClick={() => updateTask(idx, { state: 'active' })}
|
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
|
<Clipboard size={11} /> {a.command ? 'Paste Output' : 'Answer'}
|
||||||
</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
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => updateTask(idx, { state: 'skipped' })}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -495,19 +491,24 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="p-3 border-t border-default shrink-0">
|
<div className="p-3 border-t border-default shrink-0">
|
||||||
{/* Progress bar */}
|
{/* Progress bar */}
|
||||||
<div className="flex gap-1 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
{tasks.map((t, i) => (
|
<div className="flex gap-1 flex-1">
|
||||||
<div
|
{tasks.map((t, i) => (
|
||||||
key={i}
|
<div
|
||||||
className={cn(
|
key={i}
|
||||||
'flex-1 h-[3px] rounded-full',
|
className={cn(
|
||||||
t.state === 'done' ? 'bg-success' :
|
'flex-1 h-[5px] rounded-full transition-colors',
|
||||||
t.state === 'skipped' ? 'bg-muted' :
|
t.state === 'done' ? 'bg-success' :
|
||||||
t.state === 'active' ? 'bg-accent' :
|
t.state === 'skipped' ? 'bg-muted-foreground/30' :
|
||||||
'bg-elevated'
|
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>
|
</div>
|
||||||
{/* Collapsible preview */}
|
{/* Collapsible preview */}
|
||||||
{anyHandled && (
|
{anyHandled && (
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
--animate-fade-in: fade-in 200ms ease-out both;
|
--animate-fade-in: fade-in 200ms ease-out both;
|
||||||
--animate-fade-in-up: fade-in-up 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-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-slide-in-bottom: slide-in-from-bottom 200ms ease-out both;
|
||||||
--animate-scale-in: scale-in 150ms ease-out both;
|
--animate-scale-in: scale-in 150ms ease-out both;
|
||||||
--animate-fade: fadeIn 300ms ease both;
|
--animate-fade: fadeIn 300ms ease both;
|
||||||
@@ -95,6 +96,9 @@
|
|||||||
@keyframes slide-in-from-left {
|
@keyframes slide-in-from-left {
|
||||||
from { transform: translateX(-100%); } to { transform: translateX(0); }
|
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 {
|
@keyframes slide-in-from-bottom {
|
||||||
from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); }
|
from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user