返回 oh-my-ppt
htmlEditorUiStore.ts
根目录 / src / renderer / src / store / htmlEditorUiStore.ts
1 import { create } from 'zustand'
2 import type { UploadedAsset } from '@shared/generation.js'
3 import type { SpeechConfig } from '@shared/speech'
4 import type {
5 ImageGenerationMessage,
6 InteractionMode,
7 SessionDetailAiPanelMode,
8 SessionDetailChatType,
9 SessionWorkspaceTab
10 } from '@renderer/types/session-detail'
11
12 export type {
13 ImageGenerationMessage,
14 InteractionMode,
15 SessionDetailAiPanelMode,
16 SessionDetailChatType,
17 SessionWorkspaceTab
18 } from '@renderer/types/session-detail'
19
20 export interface EditorGuideState {
21 vertical: number[]
22 horizontal: number[]
23 }
24
25 interface HtmlEditorUiStore {
26 input: string
27 aiPanelMode: SessionDetailAiPanelMode
28 chatType: SessionDetailChatType
29 imagePrompt: string
30 imageMessages: ImageGenerationMessage[]
31 imageMessageCache: Record<string, ImageGenerationMessage[]>
32 loadedImageMessageKeys: Record<string, boolean>
33 selectedImageModelConfigId: string
34 imageSize: string
35 imageCount: number
36 isGeneratingImage: boolean
37 imageProgress: { label?: string; progress: number } | null
38 selectedPageId: string | null
39 consoleOpen: boolean
40 previewKey: number
41 isExportingPdf: boolean
42 isExportingPng: boolean
43 isExportingLongImage: boolean
44 isExportingPptx: boolean
45 isExportingVideo: boolean
46 isExportingSlidePack: boolean
47 isExportingSessionZip: boolean
48 interactionMode: InteractionMode
49 workspaceTab: SessionWorkspaceTab
50 editorSnapEnabled: boolean
51 editorGridVisible: boolean
52 editorGridSize: number
53 editorGuidesByPage: Record<string, EditorGuideState>
54 thumbnailVersions: Record<string, number>
55 selectedSelector: string | null
56 editSelectedSelector: string | null
57 selectorLabel: string
58 elementTag: string
59 elementText: string
60 pendingAssets: UploadedAsset[]
61 assetDragActive: boolean
62 isUploadingAssets: boolean
63 addPageDialogOpen: boolean
64 blankPageDialogOpen: boolean
65 mergeSessionPagesDialogOpen: boolean
66 mergeTemplatePagesDialogOpen: boolean
67 blankPageSourceId: string
68 historyDialogOpen: boolean
69 pageTitleEditPageId: string | null
70 pageTitleEditDraft: string
71 deleteConfirmPageId: string | null
72 isAddingPage: boolean
73 isRetryingSinglePage: boolean
74 isManagingPages: boolean
75 sidebarCollapsed: boolean
76 assetPickerOpen: boolean
77 assetPickerType: 'image' | 'video'
78 isGeneratingSpeechScript: boolean
79 speechProgress: { current: number; total: number } | null
80 speechScriptDialogOpen: boolean
81 speechConfig: SpeechConfig
82
83 setInput: (input: string) => void
84 setAiPanelMode: (mode: SessionDetailAiPanelMode) => void
85 setChatType: (chatType: SessionDetailChatType) => void
86 setImagePrompt: (input: string) => void
87 setImageMessages: (messages: ImageGenerationMessage[]) => void
88 cacheImageMessages: (key: string, messages: ImageGenerationMessage[]) => void
89 setLoadedImageMessages: (key: string, messages: ImageGenerationMessage[]) => void
90 addImageMessage: (message: ImageGenerationMessage) => void
91 addCachedImageMessage: (key: string, message: ImageGenerationMessage) => void
92 setSelectedImageModelConfigId: (id: string) => void
93 setImageSize: (size: string) => void
94 setImageCount: (count: number) => void
95 setIsGeneratingImage: (generating: boolean) => void
96 setImageProgress: (progress: { label?: string; progress: number } | null) => void
97 setSelectedPageId: (pageId: string | null) => void
98 setConsoleOpen: (open: boolean | ((open: boolean) => boolean)) => void
99 bumpPreviewKey: () => void
100 setIsExportingPdf: (isExporting: boolean) => void
101 setIsExportingPng: (isExporting: boolean) => void
102 setIsExportingLongImage: (isExporting: boolean) => void
103 setIsExportingPptx: (isExporting: boolean) => void
104 setIsExportingVideo: (isExporting: boolean) => void
105 setIsExportingSlidePack: (isExporting: boolean) => void
106 setIsExportingSessionZip: (isExporting: boolean) => void
107 setInteractionMode: (mode: InteractionMode) => void
108 setWorkspaceTab: (tab: SessionWorkspaceTab) => void
109 setEditorSnapEnabled: (enabled: boolean) => void
110 setEditorGridVisible: (visible: boolean) => void
111 setEditorGridSize: (size: number) => void
112 addEditorGuide: (pageId: string, axis: keyof EditorGuideState, position: number) => void
113 moveEditorGuide: (
114 pageId: string,
115 axis: keyof EditorGuideState,
116 index: number,
117 position: number
118 ) => void
119 removeEditorGuide: (pageId: string, axis: keyof EditorGuideState, index: number) => void
120 setSelectedElement: (
121 selector: string,
122 label: string,
123 elementTag?: string,
124 elementText?: string
125 ) => void
126 setEditSelectedElement: (selector: string | null) => void
127 clearEditSelectedElement: () => void
128 clearSelectedElement: () => void
129 addPendingAssets: (assets: UploadedAsset[]) => void
130 removePendingAsset: (assetId: string) => void
131 clearPendingAssets: () => void
132 setAssetDragActive: (active: boolean) => void
133 setIsUploadingAssets: (isUploading: boolean) => void
134 bumpThumbnailVersion: (pageId: string) => void
135 setAddPageDialogOpen: (open: boolean) => void
136 setBlankPageDialogOpen: (open: boolean) => void
137 setMergeSessionPagesDialogOpen: (open: boolean) => void
138 setMergeTemplatePagesDialogOpen: (open: boolean) => void
139 setBlankPageSourceId: (pageId: string) => void
140 openBlankPageDialog: (sourcePageId: string) => void
141 setHistoryDialogOpen: (open: boolean) => void
142 openPageTitleEdit: (pageId: string, title: string) => void
143 setPageTitleEditDraft: (title: string) => void
144 closePageTitleEdit: () => void
145 setDeleteConfirmPageId: (pageId: string | null) => void
146 setIsAddingPage: (adding: boolean) => void
147 setIsRetryingSinglePage: (retrying: boolean) => void
148 setIsManagingPages: (managing: boolean) => void
149 toggleSidebarCollapsed: () => void
150 setAssetPickerOpen: (open: boolean, type?: 'image' | 'video') => void
151 setIsGeneratingSpeechScript: (v: boolean) => void
152 setSpeechProgress: (progress: { current: number; total: number } | null) => void
153 setSpeechScriptDialogOpen: (v: boolean) => void
154 setSpeechConfig: (config: SpeechConfig) => void
155 finishAddPage: (selectedPageId?: string | null) => void
156 resetForPageChange: () => void
157 resetEditingPageState: () => void
158 resetForSessionChange: () => void
159 }
160
161 export const useHtmlEditorUiStore = create<HtmlEditorUiStore>((set) => ({
162 input: '',
163 aiPanelMode: 'chat',
164 chatType: 'page',
165 imagePrompt: '',
166 imageMessages: [],
167 imageMessageCache: {},
168 loadedImageMessageKeys: {},
169 selectedImageModelConfigId: '',
170 imageSize: '16:9',
171 imageCount: 1,
172 isGeneratingImage: false,
173 imageProgress: null,
174 selectedPageId: null,
175 consoleOpen: true,
176 previewKey: 0,
177 isExportingPdf: false,
178 isExportingPng: false,
179 isExportingLongImage: false,
180 isExportingPptx: false,
181 isExportingVideo: false,
182 isExportingSlidePack: false,
183 isExportingSessionZip: false,
184 interactionMode: 'preview' as InteractionMode,
185 workspaceTab: 'preview' as SessionWorkspaceTab,
186 editorSnapEnabled: true,
187 editorGridVisible: false,
188 editorGridSize: 20,
189 editorGuidesByPage: {},
190 thumbnailVersions: {},
191 selectedSelector: null,
192 editSelectedSelector: null,
193 selectorLabel: '',
194 elementTag: '',
195 elementText: '',
196 pendingAssets: [],
197 assetDragActive: false,
198 isUploadingAssets: false,
199 addPageDialogOpen: false,
200 blankPageDialogOpen: false,
201 mergeSessionPagesDialogOpen: false,
202 mergeTemplatePagesDialogOpen: false,
203 blankPageSourceId: '',
204 historyDialogOpen: false,
205 pageTitleEditPageId: null,
206 pageTitleEditDraft: '',
207 deleteConfirmPageId: null,
208 isAddingPage: false,
209 isRetryingSinglePage: false,
210 isManagingPages: false,
211 sidebarCollapsed: false,
212 assetPickerOpen: false,
213 assetPickerType: 'image' as const,
214 isGeneratingSpeechScript: false,
215 speechProgress: null,
216 speechScriptDialogOpen: false,
217 speechConfig: {
218 scope: 'all' as const,
219 length: 'medium' as const,
220 style: 'conversational' as const
221 },
222
223 setInput: (input) => set({ input }),
224 setAiPanelMode: (aiPanelMode) => set({ aiPanelMode }),
225 setChatType: (chatType) => set({ chatType }),
226 setImagePrompt: (imagePrompt) => set({ imagePrompt }),
227 setImageMessages: (imageMessages) => set({ imageMessages }),
228 cacheImageMessages: (key, messages) =>
229 set((state) => ({
230 imageMessageCache: {
231 ...state.imageMessageCache,
232 [key]: messages
233 }
234 })),
235 setLoadedImageMessages: (key, messages) =>
236 set((state) => ({
237 imageMessageCache: {
238 ...state.imageMessageCache,
239 [key]: messages
240 },
241 loadedImageMessageKeys: {
242 ...state.loadedImageMessageKeys,
243 [key]: true
244 }
245 })),
246 addImageMessage: (message) =>
247 set((state) => ({
248 imageMessages: [...state.imageMessages, message].slice(-48)
249 })),
250 addCachedImageMessage: (key, message) =>
251 set((state) => {
252 const cached = state.imageMessageCache[key] || []
253 return {
254 imageMessageCache: {
255 ...state.imageMessageCache,
256 [key]: [...cached, message].slice(-48)
257 }
258 }
259 }),
260 setSelectedImageModelConfigId: (selectedImageModelConfigId) =>
261 set({ selectedImageModelConfigId }),
262 setImageSize: (imageSize) => set({ imageSize }),
263 setImageCount: (imageCount) => set({ imageCount: Math.max(1, Math.min(4, imageCount)) }),
264 setIsGeneratingImage: (isGeneratingImage) => set({ isGeneratingImage }),
265 setImageProgress: (imageProgress) => set({ imageProgress }),
266 setSelectedPageId: (selectedPageId) => set({ selectedPageId }),
267 setConsoleOpen: (open) =>
268 set((state) => ({
269 consoleOpen: typeof open === 'function' ? open(state.consoleOpen) : open
270 })),
271 bumpPreviewKey: () => set((state) => ({ previewKey: state.previewKey + 1 })),
272 setIsExportingPdf: (isExportingPdf) => set({ isExportingPdf }),
273 setIsExportingPng: (isExportingPng) => set({ isExportingPng }),
274 setIsExportingLongImage: (isExportingLongImage) => set({ isExportingLongImage }),
275 setIsExportingPptx: (isExportingPptx) => set({ isExportingPptx }),
276 setIsExportingVideo: (isExportingVideo) => set({ isExportingVideo }),
277 setIsExportingSlidePack: (isExportingSlidePack) => set({ isExportingSlidePack }),
278 setIsExportingSessionZip: (isExportingSessionZip) => set({ isExportingSessionZip }),
279 setInteractionMode: (interactionMode) => set({ interactionMode }),
280 setWorkspaceTab: (workspaceTab) => set({ workspaceTab }),
281 setEditorSnapEnabled: (editorSnapEnabled) => set({ editorSnapEnabled }),
282 setEditorGridVisible: (editorGridVisible) => set({ editorGridVisible }),
283 setEditorGridSize: (editorGridSize) =>
284 set({ editorGridSize: Math.max(4, Math.min(200, Math.round(editorGridSize))) }),
285 addEditorGuide: (pageId, axis, position) =>
286 set((state) => {
287 const current = state.editorGuidesByPage[pageId] || { vertical: [], horizontal: [] }
288 return {
289 editorGuidesByPage: {
290 ...state.editorGuidesByPage,
291 [pageId]: {
292 ...current,
293 [axis]: [...current[axis], Number(position.toFixed(1))]
294 }
295 }
296 }
297 }),
298 moveEditorGuide: (pageId, axis, index, position) =>
299 set((state) => {
300 const current = state.editorGuidesByPage[pageId]
301 if (!current || index < 0 || index >= current[axis].length) return state
302 const nextAxis = [...current[axis]]
303 nextAxis[index] = Number(position.toFixed(1))
304 return {
305 editorGuidesByPage: {
306 ...state.editorGuidesByPage,
307 [pageId]: { ...current, [axis]: nextAxis }
308 }
309 }
310 }),
311 removeEditorGuide: (pageId, axis, index) =>
312 set((state) => {
313 const current = state.editorGuidesByPage[pageId]
314 if (!current || index < 0 || index >= current[axis].length) return state
315 return {
316 editorGuidesByPage: {
317 ...state.editorGuidesByPage,
318 [pageId]: {
319 ...current,
320 [axis]: current[axis].filter((_, guideIndex) => guideIndex !== index)
321 }
322 }
323 }
324 }),
325 // Fix: only reset to preview when currently in preview mode.
326 // In edit/ai-inspect mode, selecting an element should NOT change the mode.
327 setSelectedElement: (selectedSelector, selectorLabel, elementTag = '', elementText = '') =>
328 set((state) => ({
329 selectedSelector,
330 selectorLabel,
331 elementTag,
332 elementText,
333 interactionMode:
334 state.interactionMode === 'preview' ? ('preview' as InteractionMode) : state.interactionMode
335 })),
336 setEditSelectedElement: (editSelectedSelector) => set({ editSelectedSelector }),
337 clearEditSelectedElement: () => set({ editSelectedSelector: null }),
338 clearSelectedElement: () =>
339 set({
340 selectedSelector: null,
341 selectorLabel: '',
342 elementTag: '',
343 elementText: ''
344 }),
345 addPendingAssets: (assets) =>
346 set((state) => ({
347 pendingAssets: [...state.pendingAssets, ...assets]
348 })),
349 removePendingAsset: (assetId) =>
350 set((state) => ({
351 pendingAssets: state.pendingAssets.filter((asset) => asset.id !== assetId)
352 })),
353 clearPendingAssets: () => set({ pendingAssets: [] }),
354 setAssetDragActive: (assetDragActive) => set({ assetDragActive }),
355 setIsUploadingAssets: (isUploadingAssets) => set({ isUploadingAssets }),
356 bumpThumbnailVersion: (pageId) =>
357 set((state) => ({
358 thumbnailVersions: {
359 ...state.thumbnailVersions,
360 [pageId]: (state.thumbnailVersions[pageId] || 0) + 1
361 }
362 })),
363 setAddPageDialogOpen: (addPageDialogOpen) => set({ addPageDialogOpen }),
364 setBlankPageDialogOpen: (blankPageDialogOpen) => set({ blankPageDialogOpen }),
365 setMergeSessionPagesDialogOpen: (mergeSessionPagesDialogOpen) =>
366 set({ mergeSessionPagesDialogOpen }),
367 setMergeTemplatePagesDialogOpen: (mergeTemplatePagesDialogOpen) =>
368 set({ mergeTemplatePagesDialogOpen }),
369 setBlankPageSourceId: (blankPageSourceId) => set({ blankPageSourceId }),
370 openBlankPageDialog: (blankPageSourceId) =>
371 set({
372 blankPageSourceId,
373 blankPageDialogOpen: true
374 }),
375 setHistoryDialogOpen: (historyDialogOpen) => set({ historyDialogOpen }),
376 openPageTitleEdit: (pageTitleEditPageId, pageTitleEditDraft) =>
377 set({
378 pageTitleEditPageId,
379 pageTitleEditDraft
380 }),
381 setPageTitleEditDraft: (pageTitleEditDraft) => set({ pageTitleEditDraft }),
382 closePageTitleEdit: () => set({ pageTitleEditPageId: null, pageTitleEditDraft: '' }),
383 setDeleteConfirmPageId: (deleteConfirmPageId) => set({ deleteConfirmPageId }),
384 setIsAddingPage: (isAddingPage) => set({ isAddingPage }),
385 setIsRetryingSinglePage: (isRetryingSinglePage) => set({ isRetryingSinglePage }),
386 setIsManagingPages: (isManagingPages) => set({ isManagingPages }),
387 toggleSidebarCollapsed: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
388 setAssetPickerOpen: (open, type) =>
389 set((state) => ({
390 assetPickerOpen: open,
391 ...(type ? { assetPickerType: type } : { assetPickerType: state.assetPickerType })
392 })),
393 setIsGeneratingSpeechScript: (isGeneratingSpeechScript) => set({ isGeneratingSpeechScript }),
394 setSpeechProgress: (speechProgress) => set({ speechProgress }),
395 setSpeechScriptDialogOpen: (speechScriptDialogOpen) => set({ speechScriptDialogOpen }),
396 setSpeechConfig: (speechConfig) => set({ speechConfig }),
397 finishAddPage: (selectedPageId) =>
398 set((state) => ({
399 isAddingPage: false,
400 selectedPageId: typeof selectedPageId === 'undefined' ? state.selectedPageId : selectedPageId
401 })),
402 resetForPageChange: () =>
403 set({
404 interactionMode: 'preview' as InteractionMode,
405 selectedSelector: null,
406 editSelectedSelector: null,
407 selectorLabel: '',
408 elementTag: '',
409 elementText: ''
410 }),
411 resetEditingPageState: () =>
412 set({
413 interactionMode: 'preview' as InteractionMode,
414 workspaceTab: 'preview' as SessionWorkspaceTab,
415 editorSnapEnabled: true,
416 editorGridVisible: false,
417 editorGridSize: 20,
418 editorGuidesByPage: {},
419 selectedSelector: null,
420 editSelectedSelector: null,
421 selectorLabel: '',
422 elementTag: '',
423 elementText: '',
424 pendingAssets: [],
425 assetDragActive: false,
426 assetPickerOpen: false,
427 isGeneratingSpeechScript: false,
428 speechProgress: null,
429 speechScriptDialogOpen: false
430 }),
431 resetForSessionChange: () =>
432 set({
433 input: '',
434 aiPanelMode: 'chat',
435 chatType: 'page',
436 imagePrompt: '',
437 imageMessages: [],
438 imageMessageCache: {},
439 loadedImageMessageKeys: {},
440 selectedImageModelConfigId: '',
441 imageSize: '16:9',
442 imageCount: 1,
443 isGeneratingImage: false,
444 imageProgress: null,
445 selectedPageId: null,
446 interactionMode: 'preview' as InteractionMode,
447 workspaceTab: 'preview' as SessionWorkspaceTab,
448 editorSnapEnabled: true,
449 editorGridVisible: false,
450 editorGridSize: 20,
451 editorGuidesByPage: {},
452 selectedSelector: null,
453 editSelectedSelector: null,
454 selectorLabel: '',
455 elementTag: '',
456 elementText: '',
457 pendingAssets: [],
458 assetDragActive: false,
459 isUploadingAssets: false,
460 thumbnailVersions: {},
461 addPageDialogOpen: false,
462 blankPageDialogOpen: false,
463 mergeSessionPagesDialogOpen: false,
464 mergeTemplatePagesDialogOpen: false,
465 blankPageSourceId: '',
466 historyDialogOpen: false,
467 pageTitleEditPageId: null,
468 pageTitleEditDraft: '',
469 deleteConfirmPageId: null,
470 isAddingPage: false,
471 isRetryingSinglePage: false,
472 isManagingPages: false,
473 sidebarCollapsed: false,
474 assetPickerOpen: false,
475 isGeneratingSpeechScript: false,
476 speechProgress: null,
477 speechScriptDialogOpen: false,
478 speechConfig: {
479 scope: 'all' as const,
480 length: 'medium' as const,
481 style: 'conversational' as const
482 }
483 })
484 }))
485
485 lines TYPESCRIPT