Next.js app — chat interface, auth, settings, PWA manifest
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
'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() {
|
||||
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([]);
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user