159 lines
6.0 KiB
TypeScript
159 lines
6.0 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect } from 'react';
|
|
import { useStore } from '@/lib/store';
|
|
import { api } from '@/lib/api';
|
|
import Sidebar from '@/components/Sidebar';
|
|
import MessageList from '@/components/MessageList';
|
|
import MessageInput from '@/components/MessageInput';
|
|
import SettingsPanel from '@/components/SettingsPanel';
|
|
|
|
export default function Home() {
|
|
const {
|
|
settings,
|
|
setSettings,
|
|
setConversations,
|
|
setCurrentId,
|
|
setMessages,
|
|
currentId,
|
|
settingsOpen,
|
|
setSettingsOpen,
|
|
sidebarOpen,
|
|
setSidebarOpen,
|
|
} = useStore();
|
|
|
|
// SSE — listen for dream notifications
|
|
const sseRef = useRef<EventSource | null>(null);
|
|
useEffect(() => {
|
|
function connect() {
|
|
const es = new EventSource('/api/events');
|
|
sseRef.current = es;
|
|
es.onmessage = (e) => {
|
|
try {
|
|
const data = JSON.parse(e.data);
|
|
if (data.type === 'dream') {
|
|
if ('Notification' in window && Notification.permission === 'granted') {
|
|
new Notification('Bird dreamed', {
|
|
body: `${data.mode?.toUpperCase()} dream delivered — ${data.filename}`,
|
|
icon: '/icon-192.png',
|
|
});
|
|
}
|
|
}
|
|
} catch {}
|
|
};
|
|
es.onerror = () => {
|
|
es.close();
|
|
setTimeout(connect, 10000);
|
|
};
|
|
}
|
|
if ('Notification' in window) {
|
|
Notification.requestPermission();
|
|
}
|
|
connect();
|
|
return () => sseRef.current?.close();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
api.getSettings().then(s => {
|
|
setSettings(s);
|
|
document.documentElement.setAttribute('data-theme', s.theme || 'light');
|
|
document.documentElement.setAttribute('data-font', s.font_size || 'medium');
|
|
}).catch(console.error);
|
|
api.getConversations().then(setConversations).catch(console.error);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
document.documentElement.setAttribute('data-theme', settings.theme || 'light');
|
|
document.documentElement.setAttribute('data-font', settings.font_size || 'medium');
|
|
}, [settings.theme, settings.font_size]);
|
|
|
|
useEffect(() => {
|
|
if (currentId) {
|
|
api.getMessages(currentId).then(setMessages).catch(console.error);
|
|
} else {
|
|
setMessages([]);
|
|
}
|
|
}, [currentId]);
|
|
|
|
return (
|
|
<div className="flex h-dvh overflow-hidden" style={{ background: 'var(--bg)' }}>
|
|
|
|
{/* Mobile sidebar overlay */}
|
|
{sidebarOpen && (
|
|
<div
|
|
className="fixed inset-0 z-20 md:hidden"
|
|
style={{ background: 'rgba(0,0,0,0.4)' }}
|
|
onClick={() => setSidebarOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Sidebar — desktop: toggleable, mobile: slide-over */}
|
|
<div
|
|
className={`
|
|
fixed inset-y-0 left-0 z-30 flex-shrink-0 transition-all duration-200
|
|
md:relative md:translate-x-0
|
|
${sidebarOpen ? 'translate-x-0 w-64' : '-translate-x-full md:translate-x-0'}
|
|
${sidebarOpen ? 'md:w-64' : 'md:w-0 md:overflow-hidden'}
|
|
`}
|
|
style={{
|
|
background: 'var(--sidebar-bg)',
|
|
borderRight: sidebarOpen ? '1px solid var(--border)' : 'none',
|
|
width: undefined,
|
|
}}
|
|
>
|
|
<div className="w-64 h-full">
|
|
<Sidebar onClose={() => setSidebarOpen(false)} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main — shrinks when settings opens */}
|
|
<div
|
|
className="flex flex-col flex-1 min-w-0 transition-all duration-200"
|
|
style={{ marginRight: settingsOpen ? '340px' : '0' }}
|
|
>
|
|
{/* Topbar */}
|
|
<div
|
|
className="flex items-center justify-between px-4 flex-shrink-0"
|
|
style={{ borderBottom: '1px solid var(--border)', paddingTop: 'max(12px, env(safe-area-inset-top))', paddingBottom: '12px' }}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
{/* Sidebar toggle */}
|
|
<button
|
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
|
style={{ color: 'var(--text3)', background: 'none', border: 'none', cursor: 'pointer', padding: '4px', borderRadius: '6px', lineHeight: 1 }}
|
|
aria-label="Toggle sidebar"
|
|
>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
{sidebarOpen
|
|
? <polyline points="15 18 9 12 15 6" />
|
|
: <polyline points="9 18 15 12 9 6" />
|
|
}
|
|
</svg>
|
|
</button>
|
|
<span style={{ fontSize: '13px', color: 'var(--text3)' }}>
|
|
<strong style={{ color: 'var(--text)', fontWeight: 500 }}>Aaron AI</strong>
|
|
{' '}— personal knowledge assistant
|
|
</span>
|
|
</div>
|
|
<button
|
|
onClick={() => setSettingsOpen(!settingsOpen)}
|
|
style={{ color: settingsOpen ? 'var(--accent)' : 'var(--text3)', background: 'none', border: 'none', cursor: 'pointer', padding: '6px', borderRadius: '6px', lineHeight: 1 }}
|
|
aria-label="Settings"
|
|
>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<circle cx="12" cy="12" r="3"/>
|
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<MessageList />
|
|
<MessageInput />
|
|
</div>
|
|
|
|
{/* Settings panel — pushes content, doesn't cover */}
|
|
<SettingsPanel />
|
|
</div>
|
|
);
|
|
}
|