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