import { create } from 'zustand' import { getToken, saveToken } from '../utils/api' const THEME_KEY = 'owngpt_theme' const PROVIDER_KEY = 'owngpt_provider' const MODEL_KEY = 'owngpt_model' const MODE_KEY = 'owngpt_mode' const SIDEBAR_KEY = 'owngpt_sidebar_open' const VALID_THEME_PREFERENCES = new Set(['system', 'light', 'dark']) const THEME_COLORS = { dark: '#212121', light: '#ffffff', } const MODE_CONFIG = { chat: { id: 'chat', label: 'Chat', description: 'Balanced conversation for general help.', }, research: { id: 'research', label: 'Research', description: 'Biases the assistant toward search-heavy responses.', }, coding: { id: 'coding', label: 'Coding', description: 'Focuses on implementation, debugging, and code generation.', }, document: { id: 'document', label: 'Document Q&A', description: 'Optimized for file-grounded answers and summaries.', }, } export const CHAT_MODES = Object.values(MODE_CONFIG) function createId() { if (typeof crypto !== 'undefined' && crypto.randomUUID) { return crypto.randomUUID() } return `id-${Math.random().toString(36).slice(2, 11)}` } function readStorage(key, fallback) { if (typeof window === 'undefined') return fallback const value = window.localStorage.getItem(key) return value ?? fallback } function getSystemTheme() { if ( typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(prefers-color-scheme: light)').matches ) { return 'light' } return 'dark' } function readTheme() { const storedTheme = readStorage(THEME_KEY, 'system') if (VALID_THEME_PREFERENCES.has(storedTheme)) return storedTheme return 'system' } function resolveTheme(themePreference) { return themePreference === 'system' ? getSystemTheme() : themePreference } function readBoolean(key, fallback) { const value = readStorage(key, String(fallback)) return value === 'true' } function writeStorage(key, value) { if (typeof window === 'undefined') return window.localStorage.setItem(key, String(value)) } function applyTheme(themePreference) { if (typeof document === 'undefined') return const resolvedTheme = resolveTheme(themePreference) document.documentElement.setAttribute('data-theme', resolvedTheme) document.documentElement.setAttribute('data-theme-preference', themePreference) document.documentElement.style.colorScheme = resolvedTheme const themeColor = document.querySelector('meta[name="theme-color"]') if (themeColor) { themeColor.setAttribute('content', THEME_COLORS[resolvedTheme] || THEME_COLORS.dark) } return resolvedTheme } const initialThemePreference = readTheme() const initialTheme = applyTheme(initialThemePreference) export function encodeSharePayload(payload) { if (typeof window === 'undefined') return '' return window.btoa(unescape(encodeURIComponent(JSON.stringify(payload)))) } export function decodeSharePayload(hash) { if (typeof window === 'undefined' || !hash.startsWith('#share=')) return null try { const raw = hash.replace('#share=', '') return JSON.parse(decodeURIComponent(escape(window.atob(raw)))) } catch { return null } } export const useAppStore = create((set, get) => ({ token: getToken(), theme: initialTheme, themePreference: initialThemePreference, sidebarOpen: readBoolean(SIDEBAR_KEY, true), currentSessionId: null, currentSessionTitle: 'New Chat', provider: readStorage(PROVIDER_KEY, 'groq'), model: readStorage(MODEL_KEY, 'llama-3.3-70b-versatile'), mode: readStorage(MODE_KEY, 'chat'), composerText: '', attachments: [], authMode: null, activeModal: null, modalPayload: null, sessionSearch: '', sidePanel: null, selectedFile: null, sharedConversation: null, toasts: [], speakingMessageId: null, setToken: (token) => { saveToken(token) set({ token }) }, hydrateToken: () => { set({ token: getToken() }) }, setThemePreference: (themePreference) => { const nextPreference = VALID_THEME_PREFERENCES.has(themePreference) ? themePreference : 'system' writeStorage(THEME_KEY, nextPreference) const theme = applyTheme(nextPreference) set({ themePreference: nextPreference, theme }) }, setTheme: (themePreference) => get().setThemePreference(themePreference), syncSystemTheme: () => { if (get().themePreference !== 'system') return const theme = applyTheme('system') set({ theme }) }, setSidebarOpen: (sidebarOpen) => { writeStorage(SIDEBAR_KEY, sidebarOpen) set({ sidebarOpen }) }, toggleSidebar: () => { const next = !get().sidebarOpen writeStorage(SIDEBAR_KEY, next) set({ sidebarOpen: next }) }, setCurrentSession: (currentSessionId, currentSessionTitle = 'New Chat') => set({ currentSessionId, currentSessionTitle, selectedFile: null, sidePanel: null, sharedConversation: null, }), renameCurrentSession: (currentSessionTitle) => set({ currentSessionTitle }), startNewChat: () => set({ currentSessionId: null, currentSessionTitle: 'New Chat', composerText: '', attachments: [], selectedFile: null, sidePanel: null, sharedConversation: null, }), setComposerText: (composerText) => set({ composerText }), addAttachment: (attachment) => set((state) => ({ attachments: [ ...state.attachments, { ...attachment, localId: attachment.localId || createId(), }, ], })), removeAttachment: (localId) => set((state) => ({ attachments: state.attachments.filter((attachment) => attachment.localId !== localId), })), clearAttachments: () => set({ attachments: [] }), setMode: (mode) => { writeStorage(MODE_KEY, mode) set({ mode }) }, setProviderModel: (provider, model) => { writeStorage(PROVIDER_KEY, provider) writeStorage(MODEL_KEY, model) set({ provider, model }) }, openAuth: (authMode = 'login') => set({ activeModal: 'auth', authMode }), closeAuth: () => set({ activeModal: null, authMode: null }), openModal: (activeModal, modalPayload = null) => set({ activeModal, modalPayload }), closeModal: () => set({ activeModal: null, modalPayload: null }), setSessionSearch: (sessionSearch) => set({ sessionSearch }), setSidePanel: (sidePanel) => set({ sidePanel }), previewFile: (selectedFile) => set({ selectedFile, sidePanel: 'file' }), clearSelectedFile: () => set({ selectedFile: null, sidePanel: null }), setSharedConversation: (sharedConversation) => set({ sharedConversation, currentSessionId: null, currentSessionTitle: sharedConversation?.title || 'Shared Conversation', }), clearSharedConversation: () => set({ sharedConversation: null }), addToast: ({ title, description = '', variant = 'info', duration = 4200 }) => { const id = createId() set((state) => ({ toasts: [...state.toasts, { id, title, description, variant, duration }], })) if (typeof window !== 'undefined') { window.setTimeout(() => { get().removeToast(id) }, duration) } return id }, removeToast: (id) => set((state) => ({ toasts: state.toasts.filter((toast) => toast.id !== id), })), setSpeakingMessageId: (speakingMessageId) => set({ speakingMessageId }), }))