🎉 initial commit
This commit is contained in:
262
src/renderer/store/borgesStore.ts
Normal file
262
src/renderer/store/borgesStore.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
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<string, ChatMessage[]>
|
||||
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<void>
|
||||
|
||||
// Session
|
||||
loadSession: () => Promise<void>
|
||||
}
|
||||
|
||||
let _sessionTimer: ReturnType<typeof setTimeout> | null = null
|
||||
function scheduleSave(getData: () => Record<string, unknown>): void {
|
||||
if (_sessionTimer) clearTimeout(_sessionTimer)
|
||||
_sessionTimer = setTimeout(() => {
|
||||
const api = (window as Window).api
|
||||
api?.writeSession(getData()).catch(console.error)
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
let _autoSaveTimer: ReturnType<typeof setTimeout> | 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<BorgesState>((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<BorgesState> = {}
|
||||
if (data.chatHistoryByStory && typeof data.chatHistoryByStory === 'object') {
|
||||
patch.chatHistoryByStory = data.chatHistoryByStory as Record<string, ChatMessage[]>
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}))
|
||||
Reference in New Issue
Block a user