| 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 }), |
| })) |
|
|