import { writable, derived, get } from 'svelte/store'; import { auth as authApi, setup as setupApi, features as featuresApi } from './api.js'; // ── Auth store ──────────────────────────────────────────────────────────────── function createAuthStore() { const { subscribe, set, update } = writable({ user: null, token: null, refreshToken: null, loading: true, initialized: false, }); return { subscribe, async init() { if (typeof localStorage === 'undefined') { update(s => ({ ...s, loading: false, initialized: true })); return; } const token = localStorage.getItem('mac_token'); const refreshToken = localStorage.getItem('mac_refresh'); if (!token) { update(s => ({ ...s, loading: false, initialized: true })); return; } try { const user = await authApi.me(); update(s => ({ ...s, user, token, refreshToken, loading: false, initialized: true })); } catch { localStorage.removeItem('mac_token'); localStorage.removeItem('mac_refresh'); update(s => ({ ...s, loading: false, initialized: true })); } }, async login(identifier, password) { const data = await authApi.login(identifier, password); localStorage.setItem('mac_token', data.access_token); if (data.refresh_token) localStorage.setItem('mac_refresh', data.refresh_token); update(s => ({ ...s, user: data.user, token: data.access_token, refreshToken: data.refresh_token })); return data; }, async logout() { const rt = localStorage.getItem('mac_refresh'); try { if (rt) await authApi.logout(); } catch {} localStorage.removeItem('mac_token'); localStorage.removeItem('mac_refresh'); set({ user: null, token: null, refreshToken: null, loading: false, initialized: true }); }, setUser(user) { update(s => ({ ...s, user })); }, }; } export const authStore = createAuthStore(); export const user = derived(authStore, $a => $a.user); export const isAdmin = derived(authStore, $a => $a.user?.role === 'admin'); export const isFacultyOrAdmin = derived(authStore, $a => ['faculty', 'admin'].includes($a.user?.role)); export const isLoggedIn = derived(authStore, $a => !!$a.user); // ── Setup store ─────────────────────────────────────────────────────────────── export const setupStore = writable({ is_first_run: null, checked: false }); export async function checkSetup() { try { const data = await setupApi.status(); setupStore.set({ ...data, checked: true }); return data; } catch { setupStore.set({ is_first_run: false, checked: true }); } } // ── Feature flags store ─────────────────────────────────────────────────────── export const featureStore = writable({ flags: {}, roles: {}, loaded: false }); export async function loadFeatures() { try { const data = await featuresApi.status(); featureStore.set({ ...data, loaded: true }); } catch { featureStore.set({ flags: {}, roles: {}, loaded: true }); } } export function hasFeature(key) { return derived(featureStore, $f => !!$f.flags[key]); } // ── UI state ────────────────────────────────────────────────────────────────── export const sidebarOpen = writable(true); export const theme = writable('light'); // ── Loading state ───────────────────────────────────────────────────────────── export const globalLoading = writable(false); export const loadingMessage = writable(''); export const locale = writable('en'); export const toast = writable(null); let toastTimer = null; export function showToast(message, type = 'info', duration = 4000) { if (toastTimer) clearTimeout(toastTimer); toast.set({ message, type, id: Date.now() }); toastTimer = setTimeout(() => toast.set(null), duration); } // ── Chat store ──────────────────────────────────────────────────────────────── export const chatStore = writable({ conversations: [], // [{ id, title, messages: [{role, content, model, ts}] }] activeId: null, streaming: false, }); export function newConversation() { const id = crypto.randomUUID(); chatStore.update(s => ({ ...s, conversations: [{ id, title: 'New Chat', messages: [] }, ...s.conversations], activeId: id, })); return id; } export function appendMessage(convId, message) { chatStore.update(s => ({ ...s, conversations: s.conversations.map(c => c.id === convId ? { ...c, messages: [...c.messages, message], title: c.title === 'New Chat' && message.role === 'user' ? message.content.slice(0, 40) + (message.content.length > 40 ? '…' : '') : c.title } : c ), })); } export function updateLastMessage(convId, patch) { chatStore.update(s => ({ ...s, conversations: s.conversations.map(c => c.id === convId ? { ...c, messages: c.messages.map((m, i) => i === c.messages.length - 1 ? { ...m, ...patch } : m) } : c ), })); }