返回 oh-my-ppt
usePageSidebarController.ts
根目录 / src / renderer / src / components / session-detail / sidebar / usePageSidebarController.ts
1 import { useMemo } from 'react'
2 import { ipc } from '@renderer/lib/ipc'
3 import {
4 isStyleSwitchPageLocked,
5 hydrateStyleSwitchJob,
6 useGenerateStore,
7 useSessionDetailUiStore,
8 useSessionStore,
9 useToastStore
10 } from '@renderer/store'
11 import { useT } from '@renderer/i18n'
12 import { useModelAction } from '@renderer/hooks/useModelAction'
13 import { isPageGenerationLocked, normalizePagesForSelection } from '../shared/pageUtils'
14 import type { SessionPreviewPage } from '../shared/types'
15 import { useSessionPageActions } from '../hooks/useSessionPageActions'
16 import { useSessionReorderPages } from '../hooks/useSessionReorderPages'
17 import { isDefaultSlideSize, trySessionSlideSize } from '@shared/slide-size'
18
19 export function usePageSidebarController(sessionId: string) {
20 const t = useT()
21 const modelAction = useModelAction()
22 const { reorder: reorderSessionPages } = useSessionReorderPages(sessionId)
23 const currentPages = useGenerateStore((state) => state.currentPages)
24 const isGenerating = useGenerateStore((state) => state.isGenerating)
25 const pageEditJob = useGenerateStore((state) => state.pageEditJobs[sessionId] || null)
26 const deckEditJob = useGenerateStore((state) => state.deckEditJobs[sessionId] || null)
27 const styleSwitchJob = useGenerateStore((state) => state.styleSwitchJobs[sessionId] || null)
28 const selectedPageId = useSessionDetailUiStore((state) => state.selectedPageId)
29 const interactionMode = useSessionDetailUiStore((state) => state.interactionMode)
30 const isAddingPage = useSessionDetailUiStore((state) => state.isAddingPage)
31 const addingPageId = useSessionDetailUiStore((state) => state.addingPageId)
32 const isRetryingSinglePage = useSessionDetailUiStore((state) => state.isRetryingSinglePage)
33 const retryingSinglePageId = useSessionDetailUiStore((state) => state.retryingSinglePageId)
34 const isManagingPages = useSessionDetailUiStore((state) => state.isManagingPages)
35 const sidebarCollapsed = useSessionDetailUiStore((state) => state.sidebarCollapsed)
36 const toggleSidebarCollapsed = useSessionDetailUiStore((state) => state.toggleSidebarCollapsed)
37 const setAddPageDialogOpen = useSessionDetailUiStore((state) => state.setAddPageDialogOpen)
38 const setMergeSessionPagesDialogOpen = useSessionDetailUiStore(
39 (state) => state.setMergeSessionPagesDialogOpen
40 )
41 const setMergeTemplatePagesDialogOpen = useSessionDetailUiStore(
42 (state) => state.setMergeTemplatePagesDialogOpen
43 )
44 const openBlankPageDialog = useSessionDetailUiStore((state) => state.openBlankPageDialog)
45 const currentSession = useSessionStore((state) => state.currentSession)
46 const slideSize = trySessionSlideSize(currentSession)
47 const toastError = useToastStore((state) => state.error)
48 const pageActions = useSessionPageActions(sessionId)
49 const pages = useMemo(() => normalizePagesForSelection(currentPages), [currentPages])
50 const selectedPage = useMemo(
51 () => pages.find((page) => page.id === selectedPageId) ?? pages[0] ?? null,
52 [pages, selectedPageId]
53 )
54 const isStyleSwitchActive =
55 styleSwitchJob?.status === 'starting' ||
56 styleSwitchJob?.status === 'running' ||
57 styleSwitchJob?.status === 'cancelling'
58 const hasPageScopedGeneration =
59 isAddingPage ||
60 isRetryingSinglePage ||
61 Boolean(pageEditJob) ||
62 Boolean(styleSwitchJob)
63 const isSessionWideGenerating = isGenerating && !hasPageScopedGeneration
64 const isPageActionDisabled = (page: SessionPreviewPage): boolean =>
65 isSessionWideGenerating ||
66 isPageGenerationLocked(page.id, {
67 isAddingPage,
68 addingPageId,
69 isRetryingSinglePage,
70 retryingSinglePageId
71 }) ||
72 pageEditJob?.pageId === page.pageId ||
73 Boolean(deckEditJob) ||
74 isStyleSwitchPageLocked(styleSwitchJob, page.pageId) ||
75 isManagingPages
76
77 const handleRetryFailedPage = async (page: SessionPreviewPage): Promise<void> => {
78 if (!sessionId || !page.id) return
79 if (isStyleSwitchActive) return
80 const stylePageId = page.pageId || page.id
81 const styleSwitchFailedPage = styleSwitchJob?.pages.find(
82 (item) => item.pageId === stylePageId && item.status === 'failed'
83 )
84 if (styleSwitchJob && styleSwitchFailedPage) {
85 const modelConfigId = await modelAction.ensureModelActive()
86 if (!modelConfigId) return
87 useGenerateStore.getState().startStyleSwitch(sessionId, {
88 styleId: styleSwitchJob.styleId,
89 styleName: styleSwitchJob.styleName,
90 totalPages: 1,
91 pages: [{ ...styleSwitchFailedPage, status: 'pending', error: null }]
92 })
93 try {
94 const result = await ipc.retryStyleSwitchPage({
95 sessionId,
96 failedRunId: styleSwitchJob.runId,
97 pageId: styleSwitchFailedPage.pageId,
98 modelConfigId
99 })
100 if (result.alreadyRunning) {
101 hydrateStyleSwitchJob(sessionId, await ipc.getStyleSwitchState(sessionId))
102 return
103 }
104 if (result.runId) {
105 const currentJob = useGenerateStore.getState().styleSwitchJobs[sessionId]
106 if (currentJob) {
107 useGenerateStore.getState().updateStyleSwitchJob(sessionId, {
108 runId: result.runId,
109 status: currentJob.status === 'cancelling' ? 'cancelling' : 'running'
110 })
111 }
112 }
113 } catch (err) {
114 const message = err instanceof Error ? err.message : t('sessionDetail.retryPageFailed')
115 useGenerateStore
116 .getState()
117 .finishStyleSwitch(sessionId, { status: 'failed', error: message })
118 toastError(message)
119 }
120 return
121 }
122 useSessionDetailUiStore.getState().setIsRetryingSinglePage(true)
123 useSessionDetailUiStore.getState().setRetryingSinglePageId(page.id)
124 useGenerateStore.setState({ isGenerating: true, error: null, status: 'running' })
125 useGenerateStore.getState().addPage({ ...page, status: 'generating', error: null })
126 let handedToJob = false
127 try {
128 const modelConfigId = await modelAction.ensureModelActive()
129 if (!modelConfigId) return
130 const result = await ipc.retrySinglePage({ sessionId, pageId: page.id, modelConfigId })
131 if (result.alreadyRunning) return
132 handedToJob = true
133 void ipc
134 .clearSpeechScript(sessionId)
135 .catch((err) => console.warn('[speech] clearSpeechScript failed', err))
136 } catch (err) {
137 const message = err instanceof Error ? err.message : t('sessionDetail.retryPageFailed')
138 console.warn('[session-detail] retry single page failed', message)
139 } finally {
140 if (!handedToJob) {
141 useGenerateStore.getState().addPage(page)
142 useGenerateStore.getState().finishGeneration()
143 useSessionDetailUiStore.getState().setIsRetryingSinglePage(false)
144 }
145 }
146 }
147
148 const handleReorderPages = async (
149 orderedPageIds: string[],
150 selectedForKeep?: string
151 ): Promise<void> => {
152 await reorderSessionPages(orderedPageIds, selectedForKeep)
153 }
154
155 const handleUpdatePageOutline = async (
156 page: SessionPreviewPage,
157 contentOutline: string
158 ): Promise<void> => {
159 if (!sessionId) return
160 const normalizedOutline = contentOutline.replace(/\s+/g, ' ').trim()
161 if (normalizedOutline === (page.contentOutline || '').trim()) return
162 useSessionDetailUiStore.getState().setIsManagingPages(true)
163 try {
164 const result = await ipc.updateSessionPageOutline({
165 sessionId,
166 pageId: page.id,
167 contentOutline: normalizedOutline
168 })
169 useGenerateStore.getState().setPages(result.generatedPages)
170 useSessionDetailUiStore.getState().setSelectedPageId(result.selectedPageId || page.id)
171 void ipc
172 .clearSpeechScript(sessionId)
173 .catch((err) => console.warn('[speech] clearSpeechScript failed', err))
174 } catch (error) {
175 toastError(error instanceof Error ? error.message : t('pageManagement.updateOutlineFailed'))
176 throw error
177 } finally {
178 useSessionDetailUiStore.getState().setIsManagingPages(false)
179 }
180 }
181
182 return {
183 pages,
184 disabled: (interactionMode === 'ai-inspect' && isSessionWideGenerating) || Boolean(deckEditJob),
185 pageManagementDisabled:
186 isGenerating ||
187 Boolean(pageEditJob) ||
188 Boolean(deckEditJob) ||
189 isStyleSwitchActive ||
190 isAddingPage ||
191 isRetryingSinglePage ||
192 isManagingPages,
193 collapsed: sidebarCollapsed,
194 onAddBlankPage: () => openBlankPageDialog(selectedPage?.id || pages[0]?.id || ''),
195 onAddPage: () => setAddPageDialogOpen(true),
196 onMergeSessionPages: () => setMergeSessionPagesDialogOpen(true),
197 onMergeTemplatePages: () => setMergeTemplatePagesDialogOpen(true),
198 onRetryFailedPage: (page: SessionPreviewPage) => void handleRetryFailedPage(page),
199 onReorderPages: handleReorderPages,
200 onDeletePage: pageActions.deletePage,
201 onRenamePage: pageActions.renamePage,
202 onDuplicatePage: pageActions.duplicatePage,
203 onUpdatePageOutline: handleUpdatePageOutline,
204 onExportPagePptx: pageActions.exportPagePptx,
205 canExportPptx: slideSize ? isDefaultSlideSize(slideSize) : false,
206 onDownloadAllOutlines: pageActions.exportOutlinesMarkdown,
207 isPageActionDisabled,
208 onToggleCollapsed: toggleSidebarCollapsed
209 }
210 }
211
211 lines TYPESCRIPT