126 lines
4.8 KiB
TypeScript
126 lines
4.8 KiB
TypeScript
'use client';
|
||
|
||
import { useStore } from '@/lib/store';
|
||
import { api } from '@/lib/api';
|
||
|
||
function formatDate(iso: string): string {
|
||
const d = new Date(iso);
|
||
const now = new Date();
|
||
const diff = (now.getTime() - d.getTime()) / 1000;
|
||
if (diff < 60) return 'just now';
|
||
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
||
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
||
if (diff < 172800) return 'yesterday';
|
||
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||
}
|
||
|
||
function groupConversations(convs: { id: string; title: string; updated_at: string; created_at: string; message_count: number }[]) {
|
||
const now = new Date();
|
||
const groups: Record<string, typeof convs> = { today: [], yesterday: [], week: [], older: [] };
|
||
convs.forEach(c => {
|
||
const diff = (now.getTime() - new Date(c.updated_at).getTime()) / 86400000;
|
||
if (diff < 1) groups.today.push(c);
|
||
else if (diff < 2) groups.yesterday.push(c);
|
||
else if (diff < 7) groups.week.push(c);
|
||
else groups.older.push(c);
|
||
});
|
||
return groups;
|
||
}
|
||
|
||
export default function Sidebar({ onClose }: { onClose?: () => void }) {
|
||
const { conversations, currentId, setCurrentId, setMessages, setConversations, setSidebarOpen } = useStore();
|
||
|
||
async function newConversation() {
|
||
const conv = await api.newConversation();
|
||
const updated = await api.getConversations();
|
||
setConversations(updated);
|
||
setCurrentId(conv.id);
|
||
setMessages([]);
|
||
if (onClose) onClose();
|
||
}
|
||
|
||
async function loadConversation(id: string) {
|
||
setCurrentId(id);
|
||
setSidebarOpen(false);
|
||
}
|
||
|
||
async function deleteConv(e: React.MouseEvent, id: string) {
|
||
e.stopPropagation();
|
||
if (!confirm('Delete this conversation?')) return;
|
||
await api.deleteConversation(id);
|
||
const updated = await api.getConversations();
|
||
setConversations(updated);
|
||
if (currentId === id) { setCurrentId(null); setMessages([]); }
|
||
}
|
||
|
||
const groups = groupConversations(conversations);
|
||
const labels: Record<string, string> = { today: 'Today', yesterday: 'Yesterday', week: 'This week', older: 'Older' };
|
||
|
||
return (
|
||
<div className="flex flex-col h-full">
|
||
{/* Header */}
|
||
<div className="p-3" style={{ borderBottom: '1px solid var(--border)' }}>
|
||
<button
|
||
onClick={newConversation}
|
||
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-opacity hover:opacity-85"
|
||
style={{ background: 'var(--accent)', color: '#e8f5ed', border: 'none', cursor: 'pointer' }}
|
||
>
|
||
<span style={{ fontSize: '16px', lineHeight: 1 }}>+</span>
|
||
New conversation
|
||
</button>
|
||
</div>
|
||
|
||
{/* Conversation list */}
|
||
<div className="flex-1 overflow-y-auto py-1 px-2">
|
||
{Object.entries(labels).map(([key, label]) => {
|
||
const group = groups[key];
|
||
if (!group.length) return null;
|
||
return (
|
||
<div key={key}>
|
||
<div
|
||
className="px-2 py-2 text-xs uppercase tracking-wider"
|
||
style={{ color: 'var(--text3)', letterSpacing: '0.06em' }}
|
||
>
|
||
{label}
|
||
</div>
|
||
{group.map(c => (
|
||
<div
|
||
key={c.id}
|
||
onClick={() => loadConversation(c.id)}
|
||
className="flex items-center gap-2 px-2 py-2 rounded-lg cursor-pointer mb-0.5 group"
|
||
style={{
|
||
background: c.id === currentId ? 'var(--bg3)' : 'transparent',
|
||
}}
|
||
onMouseEnter={e => { if (c.id !== currentId) (e.currentTarget as HTMLElement).style.background = 'var(--bg3)'; }}
|
||
onMouseLeave={e => { if (c.id !== currentId) (e.currentTarget as HTMLElement).style.background = 'transparent'; }}
|
||
>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="text-sm truncate" style={{ color: 'var(--text)' }} title={c.title}>
|
||
{c.title}
|
||
</div>
|
||
<div className="text-xs mt-0.5" style={{ color: 'var(--text3)' }}>
|
||
{formatDate(c.updated_at)}
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={e => deleteConv(e, c.id)}
|
||
className="opacity-0 group-hover:opacity-100 px-1 py-0.5 rounded text-sm transition-opacity"
|
||
style={{ background: 'none', border: 'none', color: 'var(--text3)', cursor: 'pointer', minWidth: '24px', minHeight: '24px' }}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
})}
|
||
{conversations.length === 0 && (
|
||
<div className="px-3 py-4 text-sm" style={{ color: 'var(--text3)' }}>
|
||
No conversations yet
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|