Tesseract / studio /src /components /SessionSidebar.jsx
rkbosamia's picture
fine tuning + UI refinements
d18783e
Raw
History Blame Contribute Delete
8.46 kB
import { useMemo, useState } from 'react'
import useAppStore from '../store/appStore'
import { useSession } from '../hooks/useSession'
function bucketFor(updatedAt) {
const now = new Date()
const t = new Date(updatedAt)
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const startOfYesterday = new Date(startOfToday)
startOfYesterday.setDate(startOfYesterday.getDate() - 1)
const sevenDaysAgo = new Date(startOfToday)
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7)
if (t >= startOfToday) return 'Today'
if (t >= startOfYesterday) return 'Yesterday'
if (t >= sevenDaysAgo) return 'Last 7 days'
return 'Older'
}
const BUCKET_ORDER = ['Today', 'Yesterday', 'Last 7 days', 'Older']
export default function SessionSidebar() {
const { sessions, activeSessionId, sidebarOpen } = useAppStore()
const { createSession, openSession, deleteSession, renameSession } = useSession()
const [renamingId, setRenamingId] = useState(null)
const [renameValue, setRenameValue] = useState('')
const [query, setQuery] = useState('')
const grouped = useMemo(() => {
const filtered = query.trim()
? sessions.filter((s) => s.name.toLowerCase().includes(query.trim().toLowerCase()))
: sessions
const buckets = { Today: [], Yesterday: [], 'Last 7 days': [], Older: [] }
for (const s of filtered) buckets[bucketFor(s.updated_at)].push(s)
return buckets
}, [sessions, query])
if (!sidebarOpen) return null
async function handleNew() {
const session = await createSession('New Session')
if (session) await openSession(session.id)
}
function startRename(s) {
setRenamingId(s.id)
setRenameValue(s.name)
}
async function submitRename(id) {
if (renameValue.trim()) await renameSession(id, renameValue.trim())
setRenamingId(null)
}
return (
<div
className="h-full flex flex-col"
style={{ background: 'var(--surface)', borderRight: '1px solid var(--border)' }}
>
{/* Header */}
<div className="p-3 border-b" style={{ borderColor: 'var(--border)' }}>
<button
onClick={handleNew}
className="w-full h-8 rounded-md text-[12px] font-medium transition-all flex items-center justify-center gap-1.5"
style={{
background: 'linear-gradient(135deg, var(--accent), var(--accent-lo))',
color: '#fff',
boxShadow: '0 2px 10px var(--accent-glow)',
}}
title="New session ⌘N"
>
<span className="text-base leading-none">+</span> New Session
</button>
<div className="mt-2 relative">
<svg
width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5"
className="absolute left-2.5 top-1/2 -translate-y-1/2"
style={{ color: 'var(--text-faint)' }}
>
<circle cx="7" cy="7" r="5" />
<path d="M11 11l3.5 3.5" strokeLinecap="round" />
</svg>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search sessions"
className="w-full h-7 pl-7 pr-2 rounded-md text-[11.5px] outline-none transition-colors"
style={{
background: 'var(--surface-2)',
border: '1px solid var(--border)',
color: 'var(--text)',
}}
onFocus={(e) => (e.target.style.borderColor = 'var(--accent)')}
onBlur={(e) => (e.target.style.borderColor = 'var(--border)')}
/>
</div>
</div>
{/* Session list — grouped */}
<div className="flex-1 overflow-y-auto py-2 px-1">
{sessions.length === 0 && (
<div className="text-center text-[11px] py-4" style={{ color: 'var(--text-faint)' }}>
No sessions yet
</div>
)}
{BUCKET_ORDER.map((bucket) => {
const items = grouped[bucket]
if (!items?.length) return null
return (
<div key={bucket} className="mb-2">
<div
className="px-2.5 py-1 text-[9.5px] uppercase tracking-[0.12em] mono"
style={{ color: 'var(--text-faint)' }}
>
{bucket}
</div>
{items.map((s) => {
const isActive = s.id === activeSessionId
return (
<div
key={s.id}
onClick={() => openSession(s.id)}
className="group relative flex items-center justify-between mx-1 my-0.5 px-2.5 py-1.5 rounded-md cursor-pointer text-[12px] transition-colors"
style={{
background: isActive ? 'var(--accent-bg)' : 'transparent',
color: isActive ? 'var(--text)' : 'var(--text-dim)',
}}
onMouseEnter={(e) => {
if (!isActive) e.currentTarget.style.background = 'var(--surface-2)'
}}
onMouseLeave={(e) => {
if (!isActive) e.currentTarget.style.background = 'transparent'
}}
>
{isActive && (
<span
className="absolute left-0 top-1.5 bottom-1.5 w-[2px] rounded-full"
style={{ background: 'var(--accent)' }}
/>
)}
{renamingId === s.id ? (
<input
autoFocus
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
onBlur={() => submitRename(s.id)}
onKeyDown={(e) => {
if (e.key === 'Enter') submitRename(s.id)
if (e.key === 'Escape') setRenamingId(null)
}}
onClick={(e) => e.stopPropagation()}
className="bg-transparent border-b outline-none w-full text-[12px]"
style={{ borderColor: 'var(--accent)', color: 'var(--text)' }}
/>
) : (
<span className="truncate flex-1 pl-1">{s.name}</span>
)}
<div className="hidden group-hover:flex items-center gap-0.5 ml-1 shrink-0">
<button
onClick={(e) => { e.stopPropagation(); startRename(s) }}
className="w-5 h-5 grid place-items-center rounded transition-colors hover:bg-white/5"
style={{ color: 'var(--text-dim)' }}
title="Rename"
>
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
<path d="M11.5 1.5l3 3-9 9H2.5v-3z" />
</svg>
</button>
<button
onClick={(e) => { e.stopPropagation(); deleteSession(s.id) }}
className="w-5 h-5 grid place-items-center rounded transition-colors hover:bg-red-500/10"
style={{ color: 'var(--text-dim)' }}
title="Delete"
>
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
<path d="M3 4h10M5 4V2.5h6V4M5 4v9.5a1 1 0 001 1h4a1 1 0 001-1V4" />
</svg>
</button>
</div>
</div>
)
})}
</div>
)
})}
{query && Object.values(grouped).every((arr) => arr.length === 0) && (
<div className="text-center text-[11px] py-4" style={{ color: 'var(--text-faint)' }}>
No sessions match "{query}"
</div>
)}
</div>
<div className="px-3 py-2 border-t text-[10px] mono flex items-center justify-between"
style={{ borderColor: 'var(--border)', color: 'var(--text-faint)' }}>
<span>{sessions.length} session{sessions.length === 1 ? '' : 's'}</span>
<span>⌘K</span>
</div>
</div>
)
}