Sidebar.tsx
Language TypeScript4.0 KB113 lines
import type { ConversationSummary } from '../ipc/gen/ConversationSummary'import { NANO, relativeDay, usd } from '../lib/format'import { mod } from '../lib/theme'import { useStore } from '../state/store'function groups(list: ConversationSummary[]) {const out: [string, ConversationSummary[]][] = []const pinned = list.filter((c) => c.pinned)if (pinned.length) out.push(['Pinned', pinned])for (const c of list.filter((c) => !c.pinned)) {const label = relativeDay(c.updated_at)const g = out.find(([l]) => l === label)if (g) g[1].push(c)else out.push([label, [c]])}return out}export function SpendMeter() {const spend = useStore((s) => s.spend)const setPanel = useStore((s) => s.setPanel)if (!spend) return nullconst budget = spend.budget_usdconst pct = budget ? Math.min(100, (spend.month_nanousd / NANO / budget) * 100) : 0return (<button className={`spend level-${spend.budget_level}`} onClick={() => setPanel('usage')} data-testid="spend-meter"><span className="spend-row"><span>This month</span><strong>{usd(spend.month_nanousd)}{spend.month_estimated ? '*' : ''}</strong></span>{budget != null && budget > 0 && (<><spanclassName="bar"role="meter"aria-valuemin={0}aria-valuemax={100}aria-valuenow={Math.round(pct)}aria-label="Share of monthly budget"><span style={{ width: `${pct}%` }} /></span><span className="spend-sub">of ${budget.toFixed(2)} budget</span></>)}</button>)}export function Sidebar() {const conversations = useStore((s) => s.conversations)const activeId = useStore((s) => s.view?.conversation.id)const open = useStore((s) => s.open)const newChat = useStore((s) => s.newChat)const setPanel = useStore((s) => s.setPanel)const info = useStore((s) => s.info)return (<aside className="sidebar glass" aria-label="Conversations"><div className={`sidebar-top ${info?.platform === 'macos' ? 'traffic' : ''}`} data-tauri-drag-region><button className="primary new-chat" onClick={newChat} title={`New chat (${mod}N)`}>New chat</button><button className="search-trigger" onClick={() => setPanel('search')} title={`Search (${mod}K)`}><svg viewBox="0 0 20 20" width="14" height="14" aria-hidden><circle cx="8.5" cy="8.5" r="5.5" fill="none" stroke="currentColor" strokeWidth="1.7" /><path d="M13 13l4 4" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" /></svg><span>Search</span><kbd>{mod}K</kbd></button></div><nav className="conv-list">{groups(conversations).map(([label, list]) => (<section key={label}><h2>{label}</h2><ul>{list.map((c) => (<li key={c.id}><buttonclassName={`conv ${c.id === activeId ? 'active' : ''}`}onClick={() => void open(c.id)}aria-current={c.id === activeId}><span className="conv-title">{c.title}</span><span className="conv-cost">{c.cost_nanousd > 0 ? usd(c.cost_nanousd) : c.has_usage ? 'free' : ''}{c.cost_estimated ? '*' : ''}</span></button></li>))}</ul></section>))}{conversations.length === 0 && <p className="muted small pad">No chats yet.</p>}</nav><div className="sidebar-foot"><SpendMeter /><div className="row"><button className="ghost small" onClick={() => setPanel('usage')}>Usage</button><button className="ghost small" onClick={() => setPanel('settings')}>Settings</button></div></div></aside>)}