import { create } from 'zustand' import type { StoryFile, Market, Submission, ChatMessage, AnalysisMode, TextAnnotation, RevisionMeta } from '../types/borges' interface BorgesState { // Stories stories: StoryFile[] setStories: (stories: StoryFile[]) => void moveStory: (fromIdx: number, toIdx: number) => void // Active story activeStoryPath: string | null activeStoryId: string | null activeStoryContent: string isDirty: boolean setActiveStory: (path: string, id: string, content: string) => void setContent: (content: string) => void markSaved: () => void clearActiveStory: () => void // Markets & Submissions markets: Market[] setMarkets: (markets: Market[]) => void submissions: Submission[] setSubmissions: (submissions: Submission[]) => void selectedMarketId: string | null setSelectedMarketId: (id: string | null) => void // AI chat chatHistory: ChatMessage[] chatHistoryByStory: Record isAILoading: boolean aiError: string | null addUserMessage: (text: string) => void startAssistantMessage: () => void appendToLastMessage: (chunk: string) => void setAILoading: (v: boolean) => void setAIError: (e: string | null) => void newChat: () => void // Analysis analysisMode: AnalysisMode setAnalysisMode: (mode: AnalysisMode) => void annotations: TextAnnotation[] setAnnotations: (anns: TextAnnotation[]) => void removeAnnotation: (id: string) => void clearAnnotations: () => void useCollectionContext: boolean toggleCollectionContext: () => void useMarketBrief: boolean toggleMarketBrief: () => void // Revisions revisionPanelOpen: boolean toggleRevisionPanel: () => void revisions: RevisionMeta[] setRevisions: (revisions: RevisionMeta[]) => void // UI layout sidebarOpen: boolean setSidebarOpen: (v: boolean) => void submissionPanelOpen: boolean setSubmissionPanelOpen: (v: boolean) => void chatOpen: boolean setChatOpen: (v: boolean) => void focusMode: boolean toggleFocusMode: () => void submissionPanelTab: 'story' | 'markets' setSubmissionPanelTab: (tab: 'story' | 'markets') => void // Theme & font theme: 'dark' | 'light' toggleTheme: () => void fontSize: number setFontSize: (size: number) => void initPrefs: () => Promise // Session loadSession: () => Promise } let _sessionTimer: ReturnType | null = null function scheduleSave(getData: () => Record): void { if (_sessionTimer) clearTimeout(_sessionTimer) _sessionTimer = setTimeout(() => { const api = (window as Window).api api?.writeSession(getData()).catch(console.error) }, 1500) } let _autoSaveTimer: ReturnType | null = null function scheduleAutoSave(getData: () => { path: string; content: string } | null): void { if (_autoSaveTimer) clearTimeout(_autoSaveTimer) _autoSaveTimer = setTimeout(() => { const d = getData() if (!d) return const api = (window as Window).api api?.writeStory(d.path, d.content).catch(console.error) }, 10_000) } export const useBorgesStore = create((set, get) => ({ stories: [], setStories: (stories) => set({ stories }), moveStory: (fromIdx, toIdx) => { set((s) => { const stories = [...s.stories] const [moved] = stories.splice(fromIdx, 1) stories.splice(toIdx, 0, moved) const order = stories.map((s) => s.id) window.api.saveOrder(order).catch(console.error) return { stories } }) }, activeStoryPath: null, activeStoryId: null, activeStoryContent: '', isDirty: false, setActiveStory: (path, id, content) => { const s = get() const history = s.chatHistoryByStory[id] ?? [] set({ activeStoryPath: path, activeStoryId: id, activeStoryContent: content, isDirty: false, chatHistory: history, annotations: [], analysisMode: 'none' }) scheduleSave(() => ({ activeStoryPath: get().activeStoryPath, chatHistoryByStory: get().chatHistoryByStory })) }, setContent: (content) => { set({ activeStoryContent: content, isDirty: true }) scheduleAutoSave(() => { const { activeStoryPath, activeStoryContent } = get() return activeStoryPath ? { path: activeStoryPath, content: activeStoryContent } : null }) }, markSaved: () => set({ isDirty: false }), clearActiveStory: () => set({ activeStoryPath: null, activeStoryId: null, activeStoryContent: '', isDirty: false, chatHistory: [], annotations: [], analysisMode: 'none' }), markets: [], setMarkets: (markets) => set({ markets }), submissions: [], setSubmissions: (submissions) => set({ submissions }), selectedMarketId: null, setSelectedMarketId: (id) => set({ selectedMarketId: id }), chatHistory: [], chatHistoryByStory: {}, isAILoading: false, aiError: null, addUserMessage: (text) => { const msg: ChatMessage = { id: `user-${Date.now()}`, role: 'user', content: text } set((s) => { const history = [...s.chatHistory, msg] const chatHistoryByStory = s.activeStoryId ? { ...s.chatHistoryByStory, [s.activeStoryId]: history } : s.chatHistoryByStory return { chatHistory: history, chatHistoryByStory } }) scheduleSave(() => ({ activeStoryPath: get().activeStoryPath, chatHistoryByStory: get().chatHistoryByStory })) }, startAssistantMessage: () => { const msg: ChatMessage = { id: `asst-${Date.now()}`, role: 'assistant', content: '' } set((s) => ({ chatHistory: [...s.chatHistory, msg] })) }, appendToLastMessage: (chunk) => { set((s) => { const history = [...s.chatHistory] const last = history[history.length - 1] if (last?.role === 'assistant') history[history.length - 1] = { ...last, content: last.content + chunk } return { chatHistory: history } }) }, setAILoading: (isAILoading) => { if (!isAILoading) { const s = get() if (s.activeStoryId) { const chatHistoryByStory = { ...s.chatHistoryByStory, [s.activeStoryId]: s.chatHistory } set({ isAILoading, chatHistoryByStory }) scheduleSave(() => ({ activeStoryPath: get().activeStoryPath, chatHistoryByStory: get().chatHistoryByStory })) return } } set({ isAILoading }) }, setAIError: (aiError) => set({ aiError }), newChat: () => { set((s) => { const chatHistoryByStory = s.activeStoryId ? { ...s.chatHistoryByStory, [s.activeStoryId]: [] } : s.chatHistoryByStory return { chatHistory: [], chatHistoryByStory } }) }, analysisMode: 'none', setAnalysisMode: (analysisMode) => set({ analysisMode }), annotations: [], setAnnotations: (annotations) => set({ annotations }), removeAnnotation: (id) => set((s) => ({ annotations: s.annotations.filter((a) => a.id !== id) })), clearAnnotations: () => set({ annotations: [], analysisMode: 'none' }), useCollectionContext: false, toggleCollectionContext: () => set((s) => ({ useCollectionContext: !s.useCollectionContext })), useMarketBrief: false, toggleMarketBrief: () => set((s) => ({ useMarketBrief: !s.useMarketBrief })), revisionPanelOpen: false, toggleRevisionPanel: () => set((s) => ({ revisionPanelOpen: !s.revisionPanelOpen })), revisions: [], setRevisions: (revisions) => set({ revisions }), sidebarOpen: localStorage.getItem('sidebarOpen') !== 'false', setSidebarOpen: (v) => { localStorage.setItem('sidebarOpen', String(v)); set({ sidebarOpen: v }) }, submissionPanelOpen: localStorage.getItem('submissionPanelOpen') !== 'false', setSubmissionPanelOpen: (v) => { localStorage.setItem('submissionPanelOpen', String(v)); set({ submissionPanelOpen: v }) }, chatOpen: localStorage.getItem('chatOpen') !== 'false', setChatOpen: (v) => { localStorage.setItem('chatOpen', String(v)); set({ chatOpen: v }) }, focusMode: false, toggleFocusMode: () => set((s) => ({ focusMode: !s.focusMode })), submissionPanelTab: 'story', setSubmissionPanelTab: (tab) => set({ submissionPanelTab: tab }), theme: 'dark', toggleTheme: () => { set((s) => { const next = s.theme === 'dark' ? 'light' : 'dark' window.api.writeConfig({ theme: next }) document.documentElement.classList.toggle('light', next === 'light') return { theme: next } }) }, fontSize: 15, setFontSize: (size) => { const clamped = Math.max(11, Math.min(24, size)) window.api.writeConfig({ fontSize: clamped }) set({ fontSize: clamped }) }, initPrefs: async () => { const cfg = await window.api.readConfig() const fontSize = Math.max(11, Math.min(24, cfg.fontSize ?? 15)) const theme = cfg.theme ?? 'dark' document.documentElement.classList.toggle('light', theme === 'light') set({ fontSize, theme }) }, loadSession: async () => { try { const data = await window.api.readSession() const patch: Partial = {} if (data.chatHistoryByStory && typeof data.chatHistoryByStory === 'object') { patch.chatHistoryByStory = data.chatHistoryByStory as Record } set(patch) if (typeof data.activeStoryPath === 'string') { try { const content = await window.api.readStory(data.activeStoryPath) const id = data.activeStoryPath.split('/').pop()?.replace(/\.md$/, '') ?? '' get().setActiveStory(data.activeStoryPath, id, content) } catch { // file moved/deleted } } } catch { // no session } } }))