返回 oh-my-ppt
generateStore.ts
根目录 / src / renderer / src / store / generateStore.ts
1 import { create } from 'zustand'
2 import type { GenerateStartPayload, SessionPageEditPlan } from '@shared/generation'
3
4 export interface GenerateProgress {
5 stage: string
6 label: string
7 currentPage?: number
8 totalPages?: number
9 progress: number
10 }
11
12 export interface PageEditJob {
13 sessionId: string
14 pageId: string
15 pageNumber?: number
16 runId?: string
17 status: 'starting' | 'queued' | 'running' | 'cancelling'
18 label: string
19 progress: number
20 }
21
22 export interface DeckEditJob {
23 sessionId: string
24 runId?: string
25 status: 'starting' | 'queued' | 'running' | 'cancelling'
26 label: string
27 progress: number
28 totalPages: number
29 payload?: GenerateStartPayload
30 }
31
32 export interface DeckEditRetry {
33 sessionId: string
34 runId: string
35 failedPageCount: number
36 payload: GenerateStartPayload
37 }
38
39 export interface StyleSwitchJobPage {
40 pageId: string
41 pageNumber: number
42 title: string
43 status: 'pending' | 'running' | 'completed' | 'failed'
44 error: string | null
45 retryCount: number
46 }
47
48 export interface StyleSwitchJob {
49 sessionId: string
50 runId?: string
51 styleId: string
52 styleName?: string
53 status: 'starting' | 'running' | 'cancelling' | 'completed' | 'partial' | 'failed' | 'cancelled'
54 progress: number
55 totalPages: number
56 error: string | null
57 pages: StyleSwitchJobPage[]
58 }
59
60 export type StyleSwitchJobStateSnapshot = {
61 runId: string | null
62 status: 'idle' | StyleSwitchJob['status']
63 progress: number
64 totalPages: number
65 targetStyleId: string | null
66 targetStyleName: string | null
67 error: string | null
68 pages: StyleSwitchJobPage[]
69 }
70
71 export const hydrateStyleSwitchJob = (
72 sessionId: string,
73 snapshot: StyleSwitchJobStateSnapshot
74 ): void => {
75 const store = useGenerateStore.getState()
76 if (snapshot.status === 'idle') {
77 store.clearStyleSwitchJob(sessionId)
78 return
79 }
80 store.setStyleSwitchJob(sessionId, {
81 sessionId,
82 runId: snapshot.runId || undefined,
83 styleId: snapshot.targetStyleId || '',
84 styleName: snapshot.targetStyleName || undefined,
85 status: snapshot.status,
86 progress: snapshot.progress,
87 totalPages: snapshot.totalPages,
88 error: snapshot.error,
89 pages: snapshot.pages
90 })
91 }
92
93 export const isStyleSwitchJobActive = (job: StyleSwitchJob | null | undefined): boolean =>
94 Boolean(
95 job && (job.status === 'starting' || job.status === 'running' || job.status === 'cancelling')
96 )
97
98 export const isStyleSwitchPageLocked = (
99 job: StyleSwitchJob | null | undefined,
100 pageId: string | null | undefined
101 ): boolean => {
102 if (!job || !pageId) return false
103 const page = job.pages.find((item) => item.pageId === pageId)
104 return Boolean(page && page.status !== 'completed')
105 }
106
107 export interface PendingPageEditPlan {
108 sessionId: string
109 plan: SessionPageEditPlan
110 payload: GenerateStartPayload
111 targetPageId: string
112 targetPageNumber?: number
113 }
114
115 export interface PageEditPlanningState {
116 pageId: string
117 assessmentId?: string
118 isAssessing: boolean
119 pendingPlan: PendingPageEditPlan | null
120 }
121
122 type GenerateRunStatus = 'idle' | 'running' | 'completed' | 'cancelled' | 'failed'
123
124 interface GenerateStore {
125 status: GenerateRunStatus
126 isGenerating: boolean
127 progress: GenerateProgress | null
128 pageEditJobs: Record<string, PageEditJob | undefined>
129 deckEditJobs: Record<string, DeckEditJob | undefined>
130 deckEditRetries: Record<string, DeckEditRetry | undefined>
131 styleSwitchJobs: Record<string, StyleSwitchJob | undefined>
132 pageEditPlanning: Record<string, PageEditPlanningState | undefined>
133 currentPages: {
134 id: string
135 pageNumber: number
136 title: string
137 contentOutline?: string | null
138 html: string
139 htmlPath?: string
140 pageId?: string
141 sourceUrl?: string
142 status?: string
143 error?: string | null
144 }[]
145 error: string | null
146 sessionErrors: Record<string, string | undefined>
147 cancelReason: string | null
148
149 startGeneration: () => void
150 startPageEdit: (sessionId: string, job: Pick<PageEditJob, 'pageId' | 'pageNumber'>) => void
151 updatePageEdit: (
152 sessionId: string,
153 job: Partial<Omit<PageEditJob, 'sessionId' | 'pageId' | 'pageNumber'>>
154 ) => void
155 finishPageEdit: (sessionId: string) => void
156 startDeckEdit: (sessionId: string, job: Pick<DeckEditJob, 'totalPages' | 'payload'>) => void
157 updateDeckEdit: (sessionId: string, job: Partial<Omit<DeckEditJob, 'sessionId'>>) => void
158 finishDeckEdit: (sessionId: string, retry?: Omit<DeckEditRetry, 'sessionId'>) => void
159 clearDeckEditRetry: (sessionId: string) => void
160 startStyleSwitch: (
161 sessionId: string,
162 job: Pick<StyleSwitchJob, 'styleId' | 'styleName' | 'totalPages' | 'pages'>
163 ) => void
164 setStyleSwitchJob: (sessionId: string, job: StyleSwitchJob) => void
165 updateStyleSwitchJob: (
166 sessionId: string,
167 job: Partial<Omit<StyleSwitchJob, 'sessionId' | 'styleId' | 'pages'>>
168 ) => void
169 updateStyleSwitchPage: (
170 sessionId: string,
171 pageId: string,
172 patch: Partial<Omit<StyleSwitchJobPage, 'pageId'>>
173 ) => void
174 finishStyleSwitch: (sessionId: string, result: Pick<StyleSwitchJob, 'status' | 'error'>) => void
175 clearStyleSwitchJob: (sessionId: string) => void
176 startPageEditPlanning: (sessionId: string, pageId: string, assessmentId?: string) => void
177 setPendingPageEditPlan: (sessionId: string, plan: Omit<PendingPageEditPlan, 'sessionId'>) => void
178 finishPageEditPlanning: (sessionId: string, assessmentId?: string) => void
179 clearPendingPageEditPlan: (sessionId: string) => void
180 updateProgress: (progress: Partial<GenerateProgress>) => void
181 setPages: (
182 pages: {
183 id: string
184 pageNumber: number
185 title: string
186 contentOutline?: string | null
187 html: string
188 htmlPath?: string
189 pageId?: string
190 sourceUrl?: string
191 status?: string
192 error?: string | null
193 }[]
194 ) => void
195 addPage: (page: {
196 id: string
197 pageNumber: number
198 title: string
199 contentOutline?: string | null
200 html: string
201 htmlPath?: string
202 pageId?: string
203 sourceUrl?: string
204 status?: string
205 error?: string | null
206 }) => void
207 updatePage: (
208 pageId: string,
209 html: string,
210 patch?: Partial<{
211 pageNumber: number
212 title: string
213 htmlPath?: string
214 sourceUrl?: string
215 status?: string
216 error?: string | null
217 }>
218 ) => void
219 finishGeneration: () => void
220 cancelGeneration: (reason?: string) => void
221 setError: (error: string | null) => void
222 setSessionError: (sessionId: string, error: string | null) => void
223 clearSessionError: (sessionId: string) => void
224 reset: () => void
225 }
226
227 export const useGenerateStore = create<GenerateStore>((set) => ({
228 status: 'idle',
229 isGenerating: false,
230 progress: null,
231 pageEditJobs: {},
232 deckEditJobs: {},
233 deckEditRetries: {},
234 styleSwitchJobs: {},
235 pageEditPlanning: {},
236 currentPages: [],
237 error: null,
238 sessionErrors: {},
239 cancelReason: null,
240
241 startGeneration: () =>
242 set({
243 status: 'running',
244 isGenerating: true,
245 progress: null,
246 currentPages: [],
247 error: null,
248 cancelReason: null
249 }),
250
251 startPageEdit: (sessionId, { pageId, pageNumber }) =>
252 set((state) => {
253 const { [sessionId]: _cleared, ...sessionErrors } = state.sessionErrors
254 return {
255 pageEditJobs: {
256 ...state.pageEditJobs,
257 [sessionId]: {
258 sessionId,
259 pageId,
260 pageNumber,
261 status: 'starting',
262 label: '',
263 progress: 0
264 }
265 },
266 error: null,
267 sessionErrors
268 }
269 }),
270
271 updatePageEdit: (sessionId, job) =>
272 set((state) => ({
273 pageEditJobs: state.pageEditJobs[sessionId]
274 ? {
275 ...state.pageEditJobs,
276 [sessionId]: { ...state.pageEditJobs[sessionId], ...job }
277 }
278 : state.pageEditJobs
279 })),
280
281 finishPageEdit: (sessionId) =>
282 set((state) => {
283 const { [sessionId]: _finished, ...pageEditJobs } = state.pageEditJobs
284 return { pageEditJobs }
285 }),
286
287 startDeckEdit: (sessionId, { totalPages, payload }) =>
288 set((state) => {
289 const { [sessionId]: _cleared, ...sessionErrors } = state.sessionErrors
290 return {
291 deckEditJobs: {
292 ...state.deckEditJobs,
293 [sessionId]: {
294 sessionId,
295 status: 'starting',
296 label: '',
297 progress: 0,
298 totalPages: Math.max(1, totalPages),
299 payload
300 }
301 },
302 deckEditRetries: {
303 ...state.deckEditRetries,
304 [sessionId]: undefined
305 },
306 error: null,
307 sessionErrors,
308 cancelReason: null
309 }
310 }),
311
312 updateDeckEdit: (sessionId, job) =>
313 set((state) => ({
314 deckEditJobs: state.deckEditJobs[sessionId]
315 ? {
316 ...state.deckEditJobs,
317 [sessionId]: { ...state.deckEditJobs[sessionId], ...job }
318 }
319 : state.deckEditJobs
320 })),
321
322 finishDeckEdit: (sessionId, retry) =>
323 set((state) => {
324 const { [sessionId]: _finished, ...deckEditJobs } = state.deckEditJobs
325 return {
326 deckEditJobs,
327 deckEditRetries: {
328 ...state.deckEditRetries,
329 [sessionId]: retry ? { ...retry, sessionId } : undefined
330 },
331 cancelReason: null
332 }
333 }),
334
335 clearDeckEditRetry: (sessionId) =>
336 set((state) => {
337 const { [sessionId]: _cleared, ...deckEditRetries } = state.deckEditRetries
338 return { deckEditRetries }
339 }),
340
341 startStyleSwitch: (sessionId, { styleId, styleName, totalPages, pages }) =>
342 set((state) => {
343 const { [sessionId]: _cleared, ...sessionErrors } = state.sessionErrors
344 return {
345 styleSwitchJobs: {
346 ...state.styleSwitchJobs,
347 [sessionId]: {
348 sessionId,
349 styleId,
350 styleName,
351 status: 'starting',
352 progress: 0,
353 totalPages: Math.max(1, totalPages),
354 error: null,
355 pages
356 }
357 },
358 sessionErrors
359 }
360 }),
361
362 setStyleSwitchJob: (sessionId, job) =>
363 set((state) => ({
364 styleSwitchJobs: { ...state.styleSwitchJobs, [sessionId]: { ...job, sessionId } }
365 })),
366
367 updateStyleSwitchJob: (sessionId, job) =>
368 set((state) => ({
369 styleSwitchJobs: state.styleSwitchJobs[sessionId]
370 ? {
371 ...state.styleSwitchJobs,
372 [sessionId]: { ...state.styleSwitchJobs[sessionId], ...job }
373 }
374 : state.styleSwitchJobs
375 })),
376
377 updateStyleSwitchPage: (sessionId, pageId, patch) =>
378 set((state) => {
379 const job = state.styleSwitchJobs[sessionId]
380 if (!job) return { styleSwitchJobs: state.styleSwitchJobs }
381 return {
382 styleSwitchJobs: {
383 ...state.styleSwitchJobs,
384 [sessionId]: {
385 ...job,
386 pages: job.pages.map((page) => (page.pageId === pageId ? { ...page, ...patch } : page))
387 }
388 }
389 }
390 }),
391
392 finishStyleSwitch: (sessionId, result) =>
393 set((state) => ({
394 styleSwitchJobs: state.styleSwitchJobs[sessionId]
395 ? {
396 ...state.styleSwitchJobs,
397 [sessionId]: { ...state.styleSwitchJobs[sessionId], ...result }
398 }
399 : state.styleSwitchJobs
400 })),
401
402 clearStyleSwitchJob: (sessionId) =>
403 set((state) => {
404 const { [sessionId]: _cleared, ...styleSwitchJobs } = state.styleSwitchJobs
405 return { styleSwitchJobs }
406 }),
407
408 startPageEditPlanning: (sessionId, pageId, assessmentId) =>
409 set((state) => {
410 const { [sessionId]: _cleared, ...sessionErrors } = state.sessionErrors
411 return {
412 pageEditPlanning: {
413 ...state.pageEditPlanning,
414 [sessionId]: { pageId, assessmentId, isAssessing: true, pendingPlan: null }
415 },
416 error: null,
417 sessionErrors
418 }
419 }),
420
421 setPendingPageEditPlan: (sessionId, plan) =>
422 set((state) => ({
423 pageEditPlanning: {
424 ...state.pageEditPlanning,
425 [sessionId]: {
426 pageId: plan.targetPageId,
427 assessmentId: state.pageEditPlanning[sessionId]?.assessmentId,
428 isAssessing: false,
429 pendingPlan: { ...plan, sessionId }
430 }
431 }
432 })),
433
434 finishPageEditPlanning: (sessionId, assessmentId) =>
435 set((state) => {
436 const planning = state.pageEditPlanning[sessionId]
437 if (!planning) return { pageEditPlanning: state.pageEditPlanning }
438 if (assessmentId && planning.assessmentId !== assessmentId) {
439 return { pageEditPlanning: state.pageEditPlanning }
440 }
441 return {
442 pageEditPlanning: {
443 ...state.pageEditPlanning,
444 [sessionId]: { ...planning, isAssessing: false }
445 }
446 }
447 }),
448
449 clearPendingPageEditPlan: (sessionId) =>
450 set((state) => {
451 const planning = state.pageEditPlanning[sessionId]
452 if (!planning) return { pageEditPlanning: state.pageEditPlanning }
453 return {
454 pageEditPlanning: {
455 ...state.pageEditPlanning,
456 [sessionId]: { ...planning, pendingPlan: null }
457 }
458 }
459 }),
460
461 updateProgress: (progress) =>
462 set((state) => ({
463 progress: state.progress ? { ...state.progress, ...progress } : (progress as GenerateProgress)
464 })),
465
466 setPages: (pages) => set({ currentPages: pages }),
467
468 addPage: (page) =>
469 set((state) => {
470 const existingIndex = state.currentPages.findIndex(
471 (item) =>
472 page.id === item.id ||
473 (page.pageId && item.pageId
474 ? item.pageId === page.pageId
475 : item.pageNumber === page.pageNumber)
476 )
477 if (existingIndex < 0) {
478 return { currentPages: [...state.currentPages, page] }
479 }
480 return {
481 currentPages: state.currentPages.map((item, index) =>
482 index === existingIndex ? { ...item, ...page } : item
483 )
484 }
485 }),
486
487 updatePage: (pageId, html, patch) =>
488 set((state) => ({
489 currentPages: state.currentPages.map((page) =>
490 page.pageId === pageId ? { ...page, ...patch, html } : page
491 )
492 })),
493
494 finishGeneration: () =>
495 set({ status: 'completed', isGenerating: false, progress: null, cancelReason: null }),
496 cancelGeneration: (reason = 'User cancelled generation') =>
497 set({ status: 'cancelled', isGenerating: false, progress: null, cancelReason: reason }),
498 setError: (error) => set({ status: 'failed', error, isGenerating: false }),
499 setSessionError: (sessionId, error) =>
500 set((state) => {
501 const { [sessionId]: _cleared, ...sessionErrors } = state.sessionErrors
502 return {
503 sessionErrors: error ? { ...sessionErrors, [sessionId]: error } : sessionErrors
504 }
505 }),
506 clearSessionError: (sessionId) =>
507 set((state) => {
508 const { [sessionId]: _cleared, ...sessionErrors } = state.sessionErrors
509 return { sessionErrors }
510 }),
511 reset: () =>
512 set({
513 status: 'idle',
514 isGenerating: false,
515 progress: null,
516 pageEditJobs: {},
517 deckEditJobs: {},
518 deckEditRetries: {},
519 styleSwitchJobs: {},
520 pageEditPlanning: {},
521 currentPages: [],
522 error: null,
523 sessionErrors: {},
524 cancelReason: null
525 })
526 }))
527
527 lines TYPESCRIPT