返回 oh-my-ppt
sessionStore.ts
根目录 / src / renderer / src / store / sessionStore.ts
1 import { create } from 'zustand'
2 import { ipc } from '@renderer/lib/ipc'
3 import type { FontSelection, SourceDocumentPlan } from '@shared/generation'
4 import type { SlideSizePresetId } from '@shared/slide-size'
5
6 export interface Session {
7 id: string
8 title: string
9 topic: string | null
10 styleId: string | null
11 page_count: number | null
12 slideSizeId?: SlideSizePresetId
13 slideWidth?: number
14 slideHeight?: number
15 referenceDocumentPath?: string | null
16 reference_document_path?: string | null
17 status: string
18 provider: string
19 model: string
20 created_at: number
21 updated_at: number
22 metadata: string | null
23 designContract?: string | null
24 generation_duration_sec?: number | null
25 generated_count?: number | null
26 failed_count?: number | null
27 thumbnailPath?: string | null
28 }
29
30 export interface Message {
31 id: string
32 session_id: string
33 chat_scope: 'main' | 'page'
34 page_id: string | null
35 selector?: string | null
36 image_paths?: string[] | null
37 video_paths?: string[] | null
38 role: 'user' | 'assistant' | 'system' | 'tool'
39 content: string
40 type: string
41 tool_name: string | null
42 tool_call_id: string | null
43 token_count: number | null
44 run_model?: string | null
45 created_at: number
46 }
47
48 export interface GeneratedPage {
49 id: string
50 pageNumber: number
51 title: string
52 contentOutline?: string | null
53 html: string
54 htmlPath?: string
55 pageId?: string
56 sourceUrl?: string
57 status?: string
58 error?: string | null
59 }
60
61 interface SessionStore {
62 sessions: Session[]
63 currentSession: Session | null
64 currentMessages: Message[]
65 currentGeneratedPages: GeneratedPage[]
66 loading: boolean
67 error: string | null
68
69 fetchSessions: () => Promise<void>
70 createSession: (payload: {
71 topic: string
72 styleId: string
73 modelConfigId?: string
74 pageCount?: number
75 slideSizeId?: SlideSizePresetId
76 referenceDocumentPath?: string
77 fontSelection?: FontSelection
78 sourcePlan?: SourceDocumentPlan
79 }) => Promise<string>
80 loadSession: (sessionId: string, guard?: () => boolean) => Promise<void>
81 loadMessages: (payload: {
82 sessionId: string
83 chatType: 'main' | 'page'
84 pageId?: string
85 }) => Promise<void>
86 deleteSession: (sessionId: string) => Promise<void>
87 updateSessionTitle: (payload: { sessionId: string; title: string }) => Promise<void>
88 importSessionFile: () => Promise<{
89 cancelled?: boolean
90 sessionId?: string
91 title?: string
92 pageCount?: number
93 warnings?: string[]
94 }>
95 setCurrentSession: (session: Session | null) => void
96 setMessages: (messages: Message[]) => void
97 addMessage: (message: Message) => void
98 setLoading: (loading: boolean) => void
99 setError: (error: string | null) => void
100 resetRuntimeState: () => void
101 }
102
103 const isSameRecentMessage = (left: Message, right: Message): boolean =>
104 left.role === right.role &&
105 left.chat_scope === right.chat_scope &&
106 left.page_id === right.page_id &&
107 left.content === right.content &&
108 Math.abs(left.created_at - right.created_at) <= 1
109
110 const dedupeMessages = (messages: Message[]): Message[] => {
111 const result: Message[] = []
112 const seenIds = new Set<string>()
113 for (const message of messages) {
114 if (seenIds.has(message.id)) continue
115 if (result.some((item) => isSameRecentMessage(item, message))) continue
116 seenIds.add(message.id)
117 result.push(message)
118 }
119 return result
120 }
121
122 const sortMessages = (messages: Message[]): Message[] =>
123 [...messages].sort((left, right) => left.created_at - right.created_at)
124
125 const messageMatchesContext = (
126 message: Message,
127 sessionId: string,
128 chatType: 'main' | 'page',
129 pageId?: string
130 ): boolean =>
131 message.session_id === sessionId &&
132 message.chat_scope === chatType &&
133 (chatType === 'main' || message.page_id === pageId)
134
135 export const useSessionStore = create<SessionStore>((set, get) => ({
136 sessions: [],
137 currentSession: null,
138 currentMessages: [],
139 currentGeneratedPages: [],
140 loading: false,
141 error: null,
142
143 fetchSessions: async () => {
144 try {
145 const sessions = await ipc.listSessions()
146 set({ sessions: sessions as unknown as Session[] })
147 } catch {
148 set({ error: 'Failed to fetch sessions' })
149 }
150 },
151
152 createSession: async (payload) => {
153 const { sessionId } = await ipc.createSession(payload)
154 await get().fetchSessions()
155 return sessionId
156 },
157
158 loadSession: async (sessionId, guard) => {
159 set({ loading: true })
160 try {
161 const { session, generatedPages } = await ipc.getSession(sessionId)
162 if (guard && !guard()) {
163 return
164 }
165 set({
166 currentSession: (session as unknown as Session | null | undefined) ?? null,
167 // 消息由页面上下文独立管理。刷新会话/页面数据时不能清空正在显示的对话。
168 currentGeneratedPages: generatedPages,
169 loading: false
170 })
171 } catch {
172 if (guard && !guard()) return
173 set({ error: 'Failed to load session', loading: false })
174 }
175 },
176
177 loadMessages: async ({ sessionId, chatType, pageId }) => {
178 try {
179 const messages = await ipc.getSessionMessages({ sessionId, chatType, pageId })
180 const loadedMessages = (messages as unknown as Message[]).filter(
181 (message) => message.role === 'user' || message.role === 'assistant'
182 )
183 set((state) => {
184 const pendingMessages = state.currentMessages.filter((message) =>
185 messageMatchesContext(message, sessionId, chatType, pageId)
186 )
187 return {
188 currentMessages: sortMessages(dedupeMessages([...loadedMessages, ...pendingMessages]))
189 }
190 })
191 } catch {
192 set({ error: 'Failed to load messages' })
193 }
194 },
195
196 deleteSession: async (sessionId) => {
197 await ipc.deleteSession(sessionId)
198 await get().fetchSessions()
199 if (get().currentSession?.id === sessionId) {
200 set({ currentSession: null, currentMessages: [], currentGeneratedPages: [] })
201 }
202 },
203
204 updateSessionTitle: async ({ sessionId, title }) => {
205 await ipc.updateSessionTitle({ sessionId, title })
206 await get().fetchSessions()
207 const currentSession = get().currentSession
208 if (currentSession?.id === sessionId) {
209 set({ currentSession: { ...currentSession, title } })
210 }
211 },
212
213 importSessionFile: async () => {
214 const result = await ipc.importSessionFile()
215 if (!result.cancelled) {
216 await get().fetchSessions()
217 }
218 return result
219 },
220
221 setCurrentSession: (session) => set({ currentSession: session }),
222 setMessages: (messages) => set({ currentMessages: sortMessages(dedupeMessages(messages)) }),
223 addMessage: (message) =>
224 set((state) => {
225 const hasSameId = state.currentMessages.some((item) => item.id === message.id)
226 if (hasSameId) return state
227
228 const hasSameRecentMessage = state.currentMessages.some((item) =>
229 isSameRecentMessage(item, message)
230 )
231 if (hasSameRecentMessage) return state
232
233 return { currentMessages: sortMessages([...state.currentMessages, message]) }
234 }),
235 setLoading: (loading) => set({ loading }),
236 setError: (error) => set({ error }),
237 resetRuntimeState: () =>
238 set({
239 currentSession: null,
240 currentMessages: [],
241 currentGeneratedPages: [],
242 loading: false,
243 error: null
244 })
245 }))
246
246 lines TYPESCRIPT