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