feat(frontend): add React SPA with chat, auth, and admin pages

- Vite 6 + React 19 + TypeScript + Tailwind 4 + Zustand 5
- Login page with JWT auth
- Chat interface: MessageList, ChatInput, ChatContainer
- WebSocket hook with auto-reconnect and exponential backoff
- Sidebar with new chat button + session navigation
- Admin dashboard placeholder
- 57 modules, 242KB JS, TypeScript zero errors
This commit is contained in:
Aska committed 2026-08-12 11:47:22 +08:00
1 parent 192144563b
commit 4a71965686
35 files changed
+3146

No files matched your search

View File
Whitespace-only changes.
+44
View File
@@ -0,0 +1,44 @@
// API client — simple fetch wrapper with JWT support
const BASE = '/api/v1'
let token: string | null = localStorage.getItem('cyrene_token')
export function setToken(t: string | null) {
token = t
if (t) localStorage.setItem('cyrene_token', t)
else localStorage.removeItem('cyrene_token')
}
export function getToken() { return token }
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(options.headers as Record<string, string> || {}),
}
if (token) headers['Authorization'] = `Bearer ${token}`
const res = await fetch(`${BASE}${path}`, { ...options, headers })
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }))
throw new Error(err.error || err.message || `HTTP ${res.status}`)
}
return res.json()
}
export const api = {
login: (u: string, p: string) =>
request<{ token: string }>('/auth/login', { method: 'POST', body: JSON.stringify({ username: u, password: p }) }),
health: () => request<{ status: string }>('/health'),
sessions: {
list: () => request<{ sessions: { id: string; title: string }[] }>('/sessions'),
create: () => request<{ session_id: string }>('/sessions', { method: 'POST' }),
delete: (id: string) => request<void>(`/sessions/${id}`, { method: 'DELETE' }),
},
admin: {
dashboard: () => request<{ dashboard: Record<string, unknown> }>('/admin/dashboard'),
},
}