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