返回 presentation-ai
presentation-state.ts
根目录 / src / states / presentation-state.ts
1 import { create } from "zustand";
2 import { createJSONStorage, persist } from "zustand/middleware";
3
4 import { type Image as GeneratedImage } from "@/app/_actions/apps/image-studio/fetch";
5 import { type PaletteDropTarget } from "@/components/notebook/presentation/editor/utils/paletteDrop";
6 import {
7 normalizePresentationSlides,
8 normalizePresentationValue,
9 } from "@/components/notebook/presentation/utils/normalizePresentationSlate";
10 import { type PlateSlide } from "@/components/notebook/presentation/utils/parser";
11 import { type ImageModelList } from "@/constants/image-models";
12 import { type NotebookAgentToolCall } from "@/lib/notebook/agent-activity";
13 import {
14 type NotebookAttachment,
15 type NotebookSelectedChunk,
16 } from "@/lib/notebook/attachments";
17 import {
18 DEFAULT_PRESENTATION_GENERATION_ASPECT_RATIO,
19 type PresentationGenerationAspectRatio,
20 } from "@/lib/presentation/aspect-ratio";
21 import {
22 getPresentationImageGenerationKey,
23 getRootImageGenerationTarget,
24 resolvePresentationImageGenerationSource,
25 type PresentationImageGenerationJob,
26 type PresentationImageGenerationSource,
27 type PresentationImageGenerationTarget,
28 } from "@/lib/presentation/image-generation";
29 import { type PresentationImageSearchResult } from "@/lib/presentation/image-search";
30 import { isBuiltInPresentationTheme } from "@/lib/presentation/theme-resolution";
31 import { type ThemeProperties, type Themes } from "@/lib/presentation/themes";
32 import { usePresentationHistoryState } from "./presentation-history-state";
33
34 export const MIN_PRESENTATION_ZOOM_LEVEL = 0.5;
35 export const MAX_PRESENTATION_ZOOM_LEVEL = 1.8;
36
37 const clampPresentationZoomLevel = (level: number): number =>
38 Math.min(
39 MAX_PRESENTATION_ZOOM_LEVEL,
40 Math.max(MIN_PRESENTATION_ZOOM_LEVEL, level),
41 );
42
43 function normalizeSlideUpdates(
44 updates: Partial<PlateSlide>,
45 ): Partial<PlateSlide> {
46 if (!("content" in updates)) {
47 return updates;
48 }
49
50 return {
51 ...updates,
52 content: normalizePresentationValue(
53 updates.content,
54 ) as PlateSlide["content"],
55 };
56 }
57
58 export type HistoryType = "history";
59
60 export type ImageEditorMode =
61 | "generate"
62 | "your-images"
63 | "generated-images"
64 | "embed"
65 | "search"
66 | "gif"
67 | "chart";
68
69 export type PresentationStockImageProvider = "unsplash" | "pixabay" | "google";
70
71 export type RightPanelType =
72 | "basicBlocks"
73 | "elements"
74 | "charts"
75 | "diagrams"
76 | "embed"
77 | "background"
78 | "theme"
79 | "agent"
80 | "globalSettings"
81 | "imageEditor"
82 | "chartEditor"
83 | "infographicEditor"
84 | "infographicGenerationEditor"
85 | "presentationImageEditor"
86 | "layoutEditor"
87 | "iconPicker"
88 | null;
89
90 export type LayoutEditorElementSnapshot = Record<string, unknown> & {
91 children?: unknown[];
92 id?: string;
93 type?: string;
94 };
95
96 export type LayoutEditorApplyLayout = (
97 type: string,
98 additionalData?: Record<string, unknown>,
99 ) => LayoutEditorElementSnapshot | null | void;
100
101 export type Chunk = NotebookSelectedChunk;
102
103 type PendingPresentationCreateRequest = {
104 attachments?: NotebookAttachment[];
105 language: string;
106 modelId: string;
107 modelProvider: "openai" | "ollama" | "lmstudio";
108 numSlides: number;
109 generationAspectRatio?: PresentationGenerationAspectRatio;
110 outputFormat?: "flow" | "html";
111 prompt: string;
112 webSearchEnabled: boolean;
113 autoThemeEnabled?: boolean;
114 };
115
116 interface PresentationState {
117 currentPresentationId: string | null;
118 currentPresentationTitle: string | null;
119 currentPresentationUpdatedAt: string | null;
120 currentPresentationOwnerId: string | null;
121 outputFormat: "flow" | "html";
122 contentVersion: number;
123 isGridView: boolean;
124 isSheetOpen: boolean;
125 numSlides: number;
126
127 theme: Themes | string;
128 customThemeData: ThemeProperties | null;
129 themeDataByTheme: Record<string, ThemeProperties | null | undefined>;
130 generatedThemeData: ThemeProperties | null;
131 language: string;
132 modelProvider: "openai" | "ollama" | "lmstudio";
133 modelId: string;
134 pageStyle: string;
135 presentationInput: string;
136 imageModel: ImageModelList;
137 imageSource: "automatic" | "ai" | "stock" | "gif";
138 stockImageProvider: PresentationStockImageProvider;
139 presentationStyle: string;
140 generationAspectRatio: PresentationGenerationAspectRatio;
141 // New customization options
142 textContent: "minimal" | "concise" | "detailed" | "extensive";
143 tone:
144 | "auto"
145 | "general"
146 | "persuasive"
147 | "inspiring"
148 | "instructive"
149 | "engaging";
150 audience:
151 | "auto"
152 | "general"
153 | "business"
154 | "investor"
155 | "teacher"
156 | "student";
157 scenario:
158 | "auto"
159 | "general"
160 | "analysis-report"
161 | "teaching-training"
162 | "promotional-materials"
163 | "public-speeches";
164 savingStatus: "idle" | "saving" | "saved";
165 isPresenting: boolean;
166 isPresentingLoading: boolean;
167 presentingScaleLocks: Record<string, boolean>;
168 currentSlideId: string | null;
169 isThemeCreatorOpen: boolean;
170
171 pageBackground: Record<string, unknown>;
172 setPageBackground: (pageBackground: Record<string, unknown>) => void;
173 // Generation states
174 shouldStartOutlineGeneration: boolean;
175 shouldStartPresentationGeneration: boolean;
176 shouldStartImageSlideGeneration: boolean;
177 isGeneratingOutline: boolean;
178 isGeneratingPresentation: boolean;
179 activeGenerationPresentationId: string | null;
180 completedGenerationPresentationId: string | null;
181 pendingCreateRequest: PendingPresentationCreateRequest | null;
182 outline: string[];
183 searchResults: Array<{ query: string; results: unknown[] }>; // Store search results for context
184 imageSearchResults: PresentationImageSearchResult[];
185 outlineToolCalls: NotebookAgentToolCall[];
186 webSearchEnabled: boolean; // Toggle for web search in outline generation
187 autoThemeEnabled: boolean; // Toggle for generated custom themes in outline generation
188 slides: PlateSlide[]; // This now holds the new object structure
189
190 // Presentation image generation tracking. Root image jobs use the slide id as
191 // their key for backward compatibility; nested image jobs use slideId:elementId.
192 // Each job also stores the owning presentation id so late async image results
193 // cannot be applied to a different deck that reuses the same generated slide id.
194 rootImageGeneration: Record<string, PresentationImageGenerationJob>;
195
196 isSidebarCollapsed: boolean;
197 setIsSidebarCollapsed: (update: boolean) => void;
198 isRightPanelCollapsed: boolean;
199 setIsRightPanelCollapsed: (update: boolean) => void;
200 setSlides: (
201 slides: PlateSlide[] | ((slides: PlateSlide[]) => PlateSlide[]),
202 type?: HistoryType,
203 ) => void;
204 updateSlide: (
205 slideId: string,
206 updates: Partial<PlateSlide>,
207 type?: HistoryType,
208 ) => void;
209 startPresentationImageGeneration: (
210 target: PresentationImageGenerationTarget,
211 query: string,
212 options?: {
213 imageModel?: ImageModelList;
214 presentationId?: string;
215 source?: PresentationImageGenerationSource;
216 stockImageProvider?: PresentationStockImageProvider;
217 },
218 ) => void;
219 completePresentationImageGeneration: (key: string, url: string) => void;
220 failPresentationImageGeneration: (key: string, error: string) => void;
221 clearPresentationImageGeneration: (
222 targetOrKey: PresentationImageGenerationTarget | string,
223 ) => void;
224 startRootImageGeneration: (
225 slideId: string,
226 query: string,
227 options?:
228 | ImageModelList
229 | {
230 imageModel?: ImageModelList;
231 source?: PresentationImageGenerationSource;
232 stockImageProvider?: PresentationStockImageProvider;
233 },
234 ) => void;
235 completeRootImageGeneration: (slideId: string, url: string) => void;
236 failRootImageGeneration: (slideId: string, error: string) => void;
237 clearRootImageGeneration: (slideId: string) => void;
238 setCurrentPresentation: (id: string | null, title: string | null) => void;
239 setCurrentPresentationOwnerId: (ownerId: string | null) => void;
240 setCurrentPresentationUpdatedAt: (updatedAt: Date | string | null) => void;
241 setOutputFormat: (outputFormat: "flow" | "html") => void;
242 setContentVersion: (version: number) => void;
243 setIsGridView: (isGrid: boolean) => void;
244 setIsSheetOpen: (isOpen: boolean) => void;
245 setNumSlides: (num: number) => void;
246 setTheme: (
247 theme: Themes | string,
248 customData?: ThemeProperties | null,
249 type?: HistoryType,
250 ) => void;
251 setThemeDataByTheme: (
252 themeDataByTheme: Record<string, ThemeProperties | null | undefined>,
253 ) => void;
254 setGeneratedThemeData: (data: ThemeProperties | null) => void;
255 shouldShowExitHeader: boolean;
256 setShouldShowExitHeader: (udpdate: boolean) => void;
257 thumbnailUrl?: string;
258 setThumbnailUrl: (url: string | undefined) => void;
259 setLanguage: (lang: string) => void;
260 setModelProvider: (provider: "openai" | "ollama" | "lmstudio") => void;
261 setModelId: (id: string) => void;
262 setPageStyle: (style: string) => void;
263 setPresentationInput: (input: string) => void;
264 setOutline: (topics: string[]) => void;
265 setSearchResults: (
266 results: Array<{ query: string; results: unknown[] }>,
267 ) => void;
268 setImageSearchResults: (results: PresentationImageSearchResult[]) => void;
269 setOutlineToolCalls: (toolCalls: NotebookAgentToolCall[]) => void;
270 setWebSearchEnabled: (enabled: boolean) => void;
271 setAutoThemeEnabled: (enabled: boolean) => void;
272 setImageModel: (model: ImageModelList) => void;
273 setImageSource: (source: "automatic" | "ai" | "stock" | "gif") => void;
274 setStockImageProvider: (provider: PresentationStockImageProvider) => void;
275 setPresentationStyle: (style: string) => void;
276 setGenerationAspectRatio: (
277 generationAspectRatio: PresentationGenerationAspectRatio,
278 ) => void;
279 setTextContent: (
280 content: "minimal" | "concise" | "detailed" | "extensive",
281 ) => void;
282 setTone: (
283 tone:
284 | "auto"
285 | "general"
286 | "persuasive"
287 | "inspiring"
288 | "instructive"
289 | "engaging",
290 ) => void;
291 setAudience: (
292 audience:
293 | "auto"
294 | "general"
295 | "business"
296 | "investor"
297 | "teacher"
298 | "student",
299 ) => void;
300 setScenario: (
301 scenario:
302 | "auto"
303 | "general"
304 | "analysis-report"
305 | "teaching-training"
306 | "promotional-materials"
307 | "public-speeches",
308 ) => void;
309 setSavingStatus: (status: "idle" | "saving" | "saved") => void;
310 setIsPresenting: (isPresenting: boolean) => void;
311 setIsPresentingLoading: (isLoading: boolean) => void;
312 setPresentingScaleLock: (slideId: string, locked: boolean) => void;
313 resetPresentingScaleLocks: () => void;
314 setCurrentSlideId: (id: string | null) => void;
315 nextSlide: () => void;
316 previousSlide: () => void;
317
318 setIsThemeCreatorOpen: (update: boolean) => void;
319 // Typography overrides
320 fontSize: "S" | "M" | "L"; // S=12px, M=16px, L=18px
321 setFontSize: (size: "S" | "M" | "L") => void;
322 fontFamily: { body: string; heading: string };
323 setFontFamily: (fonts: { body?: string; heading?: string }) => void;
324 // Generation actions
325 setShouldStartOutlineGeneration: (shouldStart: boolean) => void;
326 setShouldStartPresentationGeneration: (shouldStart: boolean) => void;
327 setShouldStartImageSlideGeneration: (shouldStart: boolean) => void;
328 setIsGeneratingOutline: (isGenerating: boolean) => void;
329 setIsGeneratingPresentation: (isGenerating: boolean) => void;
330 completePresentationGeneration: () => void;
331 dismissCompletedGeneration: (presentationId: string) => void;
332 setPendingCreateRequest: (
333 request: PendingPresentationCreateRequest | null,
334 ) => void;
335 consumePendingCreateRequest: () => PendingPresentationCreateRequest | null;
336 startOutlineGeneration: () => void;
337 startPresentationGeneration: () => void;
338 startImageSlideGeneration: () => void;
339 resetGeneration: () => void;
340 resetForNewGeneration: () => void;
341 resetPresentationState: () => void;
342
343 // Selection state
344 isSelecting: boolean;
345 selectedPresentations: string[];
346 toggleSelecting: () => void;
347 selectAllPresentations: (ids: string[]) => void;
348 deselectAllPresentations: () => void;
349 togglePresentationSelection: (id: string) => void;
350
351 // Unified right panel state (replaces isAgentOpen, isGlobalSettingsOpen)
352 activeRightPanel: RightPanelType;
353 setActiveRightPanel: (panel: RightPanelType) => void;
354 iconPickerCurrentIcon: string;
355 iconPickerSelectIcon: ((iconName: string) => void) | null;
356 iconPickerRemoveIcon: (() => void) | null;
357 openIconPicker: (
358 currentIcon: string,
359 onSelect: (iconName: string) => void,
360 onRemove?: () => void,
361 ) => void;
362 closeIconPicker: () => void;
363 layoutEditorElementId: string | null;
364 layoutEditorEditorId: string | null;
365 layoutEditorElement: LayoutEditorElementSnapshot | null;
366 layoutEditorApplyLayout: LayoutEditorApplyLayout | null;
367 paletteDropTarget: PaletteDropTarget | null;
368 setPaletteDropTarget: (target: PaletteDropTarget | null) => void;
369 openLayoutEditor: (
370 editorId: string | null,
371 elementId: string | null,
372 element: LayoutEditorElementSnapshot | null,
373 applyLayout?: LayoutEditorApplyLayout,
374 ) => void;
375
376 // Pending agent message (for slide-specific editing from Magic Menu)
377 pendingAgentMessage: {
378 message: string;
379 slideContext: string; // Serialized XML of the slide
380 } | null;
381 setPendingAgentMessage: (
382 pending: { message: string; slideContext: string } | null,
383 ) => void;
384
385 // Image editor state for root image editing
386 imageEditorInitialMode: ImageEditorMode | null;
387 openImageEditor: (mode?: ImageEditorMode) => void;
388 closeImageEditor: () => void;
389
390 // Chart editor state for inline chart element editing
391 chartEditorData: {
392 chartType: string;
393 chartData: unknown;
394 chartOptions: Record<string, unknown>;
395 } | null;
396 openChartEditor: (
397 chartData?: {
398 chartType: string;
399 chartData: unknown;
400 chartOptions: Record<string, unknown>;
401 },
402 updateElementFn?: (props: Record<string, unknown>) => void,
403 ) => void;
404 closeChartEditor: () => void;
405
406 // Infographic editor state for inline infographic element editing
407 openInfographicEditor: (
408 updateElementFn?: (props: Record<string, unknown>) => void,
409 ) => void;
410 closeInfographicEditor: () => void;
411 openInfographicGenerationEditor: (
412 updateElementFn?: (props: Record<string, unknown>) => void,
413 ) => void;
414 closeInfographicGenerationEditor: () => void;
415
416 // Presentation image editor state (for inline TImageElement editing)
417 presentationImageEditorInitialMode: ImageEditorMode | null;
418 presentationImageEditorElement: Record<string, unknown> | null;
419 presentationImageEditorFrame: {
420 height: number;
421 width: number;
422 } | null;
423 // Bound function to update the element from the panel
424 boundUpdateElement: ((props: Record<string, unknown>) => void) | null;
425 openPresentationImageEditor: (
426 mode?: ImageEditorMode,
427 updateElementFn?: (props: Record<string, unknown>) => void,
428 element?: Record<string, unknown>,
429 frame?: { height: number; width: number },
430 ) => void;
431 closePresentationImageEditor: () => void;
432
433 // Reordering state
434 isReorderingSlides: boolean;
435 setIsReorderingSlides: (isReordering: boolean) => void;
436
437 // Attached files (uploaded via UploadThing) for outline with docs
438 attachedFiles: NotebookAttachment[];
439 setAttachedFiles: (files: NotebookAttachment[]) => void;
440 isUploadingAttachment: boolean;
441 setIsUploadingAttachment: (uploading: boolean) => void;
442
443 // Generated image cache by prompt
444 generatedImageCache: Record<string, GeneratedImage[]>;
445 setGeneratedImageCache: (prompt: string, images: GeneratedImage[]) => void;
446
447 // Image search state
448 imageSearchState: {
449 mode: PresentationStockImageProvider;
450 unsplashQuery: string;
451 pixabayQuery: string;
452 googleQuery: string;
453 };
454 setImageSearchState: (
455 state: Partial<{
456 mode: PresentationStockImageProvider;
457 unsplashQuery: string;
458 pixabayQuery: string;
459 googleQuery: string;
460 }>,
461 ) => void;
462
463 // Slide template selection for outline
464 selectedSlideTemplates: string[]; // Array of template IDs from TEMPLATE_DEFINITIONS
465 setSelectedSlideTemplates: (templates: string[]) => void;
466 outlineItemIds: string[]; // Ordered outline item IDs used for per-slide layout mapping
467 setOutlineItemIds: (ids: string[]) => void;
468 outlineTemplateOverrides: Record<string, string | null>; // Map of outline ID -> template ID | null (null = auto)
469 setOutlineTemplateOverride: (
470 outlineId: string,
471 templateId: string | null,
472 ) => void;
473 clearOutlineTemplateOverrides: () => void;
474
475 // DB template selection for generation
476 selectedDbTemplate: {
477 id: string;
478 title: string;
479 slides: PlateSlide[];
480 } | null;
481 setSelectedDbTemplate: (
482 template: { id: string; title: string; slides: PlateSlide[] } | null,
483 ) => void;
484
485 // Manual extraction state for presentation generation
486 isManualExtractionEnabled: boolean;
487 setIsManualExtractionEnabled: (enabled: boolean) => void;
488 selectedChunks: Chunk[];
489 setSelectedChunks: (chunks: Chunk[]) => void;
490 addSelectedChunk: (chunk: Chunk) => void;
491 removeSelectedChunk: (chunkId: string, ragId: string) => void;
492 updateChunkSlideAssignment: (
493 chunkId: string,
494 ragId: string,
495 slideNumber: number | null,
496 ) => void;
497 clearSelectedChunks: () => void;
498 // Multi-file extraction support
499 extractorRagIds: string[];
500 addExtractorRagId: (id: string) => void;
501 removeExtractorRagId: (id: string) => void;
502 clearExtractorRagIds: () => void;
503 setExtractorRagIds: (ids: string[]) => void;
504 currentExtractorRagId: string | null;
505 setCurrentExtractorRagId: (id: string | null) => void;
506
507 // Zoom state for slide scaling in edit mode
508 zoomLevel: number; // Zoom multiplier (1 = 100%, 1.4 = 140%, etc.)
509 setZoomLevel: (level: number) => void;
510 isReadOnly: boolean;
511 setIsReadOnly: (isReadOnly: boolean) => void;
512 }
513
514 // Helper to handle history snapshots with circular dependency workaround
515 const pushHistorySnapshot = (
516 type: HistoryType | undefined,
517 slideId: string | undefined,
518 changeType: "slide" | "theme" | "full" = "full",
519 ) => {
520 if (type === "history") return;
521
522 // Dynamic import to avoid circular dependency
523
524 const { history, pushSnapshot } = usePresentationHistoryState.getState();
525 // Only push if history is initialized
526 if (history.present !== null) {
527 pushSnapshot(slideId, changeType);
528 }
529 };
530
531 export const usePresentationState = create<PresentationState>()(
532 persist(
533 (set, get) => ({
534 currentPresentationId: null,
535 currentPresentationTitle: null,
536 currentPresentationUpdatedAt: null,
537 currentPresentationOwnerId: null,
538 outputFormat: "flow",
539 contentVersion: 0,
540 isGridView: true,
541 isSheetOpen: false,
542 shouldShowExitHeader: false,
543 setShouldShowExitHeader: (update) =>
544 set({ shouldShowExitHeader: update }),
545 thumbnailUrl: undefined,
546 setThumbnailUrl: (url) => set({ thumbnailUrl: url }),
547 numSlides: 5,
548 language: "en-US",
549 modelProvider: "openai",
550 modelId: "gpt-4o-mini",
551 pageStyle: "default",
552 presentationInput: "",
553 outline: [],
554 searchResults: [],
555 imageSearchResults: [],
556 outlineToolCalls: [],
557 webSearchEnabled: true,
558 autoThemeEnabled: true,
559 theme: "mystique",
560 customThemeData: null,
561 themeDataByTheme: {},
562 generatedThemeData: null,
563 imageModel: "fal-ai/flux-2/flash",
564 imageSource: "automatic",
565 stockImageProvider: "unsplash",
566 presentationStyle: "professional",
567 generationAspectRatio: DEFAULT_PRESENTATION_GENERATION_ASPECT_RATIO,
568 textContent: "concise",
569 tone: "auto",
570 audience: "auto",
571 scenario: "auto",
572 slides: [], // Now holds the new slide object structure
573 rootImageGeneration: {},
574 savingStatus: "idle",
575 isPresenting: false,
576 isPresentingLoading: false,
577 presentingScaleLocks: {},
578 currentSlideId: null,
579 isThemeCreatorOpen: false,
580 pageBackground: {},
581 // Typography defaults
582 fontSize: "M",
583 setFontSize: (size) => set({ fontSize: size }),
584 fontFamily: { body: "", heading: "" },
585 setFontFamily: (fonts) =>
586 set((state) => ({
587 fontFamily: {
588 body: fonts.body ?? state.fontFamily.body,
589 heading: fonts.heading ?? state.fontFamily.heading,
590 },
591 })),
592 isReorderingSlides: false,
593 setIsReorderingSlides: (isReordering) =>
594 set({ isReorderingSlides: isReordering }),
595
596 // Attached files state
597 attachedFiles: [],
598 setAttachedFiles: (files) => set({ attachedFiles: files }),
599 isUploadingAttachment: false,
600 setIsUploadingAttachment: (uploading) =>
601 set({ isUploadingAttachment: uploading }),
602
603 // Generated image cache
604 generatedImageCache: {},
605 setGeneratedImageCache: (prompt, images) =>
606 set((state) => ({
607 generatedImageCache: {
608 ...state.generatedImageCache,
609 [prompt]: images,
610 },
611 })),
612
613 // Image search state
614 imageSearchState: {
615 mode: "unsplash",
616 unsplashQuery: "",
617 pixabayQuery: "",
618 googleQuery: "",
619 },
620 setImageSearchState: (newState) =>
621 set((state) => ({
622 imageSearchState: { ...state.imageSearchState, ...newState },
623 })),
624
625 // Slide template selection for outline
626 selectedSlideTemplates: [],
627 setSelectedSlideTemplates: (templates) =>
628 set({ selectedSlideTemplates: templates }),
629 outlineItemIds: [],
630 setOutlineItemIds: (ids) => set({ outlineItemIds: ids }),
631 outlineTemplateOverrides: {},
632 setOutlineTemplateOverride: (outlineId, templateId) =>
633 set((state) => {
634 if (templateId === null) {
635 const outlineTemplateOverrides = {
636 ...state.outlineTemplateOverrides,
637 };
638 delete outlineTemplateOverrides[outlineId];
639
640 return { outlineTemplateOverrides };
641 }
642
643 return {
644 outlineTemplateOverrides: {
645 ...state.outlineTemplateOverrides,
646 [outlineId]: templateId,
647 },
648 };
649 }),
650 clearOutlineTemplateOverrides: () =>
651 set({ outlineTemplateOverrides: {} }),
652
653 // DB template selection for generation
654 selectedDbTemplate: null,
655 setSelectedDbTemplate: (template) =>
656 set({ selectedDbTemplate: template }),
657
658 // Manual extraction state
659 isManualExtractionEnabled: false,
660 setIsManualExtractionEnabled: (enabled) =>
661 set({ isManualExtractionEnabled: enabled }),
662 selectedChunks: [],
663 setSelectedChunks: (selectedChunks) => set({ selectedChunks }),
664 addSelectedChunk: (chunk) =>
665 set((state) => ({
666 selectedChunks: state.selectedChunks.some(
667 (c) => c.chunkId === chunk.chunkId && c.ragId === chunk.ragId,
668 )
669 ? state.selectedChunks
670 : [...state.selectedChunks, chunk],
671 })),
672 removeSelectedChunk: (chunkId, ragId) =>
673 set((state) => ({
674 selectedChunks: state.selectedChunks.filter(
675 (c) => !(c.chunkId === chunkId && c.ragId === ragId),
676 ),
677 })),
678 updateChunkSlideAssignment: (chunkId, ragId, slideNumber) =>
679 set((state) => ({
680 selectedChunks: state.selectedChunks.map((c) =>
681 c.chunkId === chunkId && c.ragId === ragId
682 ? { ...c, slideNumber }
683 : c,
684 ),
685 })),
686 clearSelectedChunks: () => set({ selectedChunks: [] }),
687 // Multi-file extraction support
688 extractorRagIds: [],
689 addExtractorRagId: (id) =>
690 set((state) => ({
691 extractorRagIds: state.extractorRagIds.includes(id)
692 ? state.extractorRagIds
693 : [...state.extractorRagIds, id],
694 // Auto-set current if first file
695 currentExtractorRagId: state.currentExtractorRagId ?? id,
696 })),
697 removeExtractorRagId: (id) =>
698 set((state) => {
699 const newIds = state.extractorRagIds.filter((rid) => rid !== id);
700 return {
701 extractorRagIds: newIds,
702 // Reset current if removed
703 currentExtractorRagId:
704 state.currentExtractorRagId === id
705 ? (newIds[0] ?? null)
706 : state.currentExtractorRagId,
707 // Also remove chunks from this file
708 selectedChunks: state.selectedChunks.filter((c) => c.ragId !== id),
709 };
710 }),
711 clearExtractorRagIds: () =>
712 set({ extractorRagIds: [], currentExtractorRagId: null }),
713 setExtractorRagIds: (ids) => set({ extractorRagIds: ids }),
714 currentExtractorRagId: null,
715 setCurrentExtractorRagId: (id) => set({ currentExtractorRagId: id }),
716
717 // Zoom state for slide scaling in edit mode
718 zoomLevel: 1, // Default to 100% and clamp down on smaller layouts
719 setZoomLevel: (level) =>
720 set({ zoomLevel: clampPresentationZoomLevel(level) }),
721 isReadOnly: false,
722 setIsReadOnly: (isReadOnly) => set({ isReadOnly }),
723
724 // Sidebar states
725 isSidebarCollapsed: false,
726 setIsSidebarCollapsed: (update) => set({ isSidebarCollapsed: update }),
727 isRightPanelCollapsed: false,
728 setIsRightPanelCollapsed: (update) =>
729 set({ isRightPanelCollapsed: update }),
730
731 // Generation states
732 shouldStartOutlineGeneration: false,
733 shouldStartPresentationGeneration: false,
734 shouldStartImageSlideGeneration: false,
735 isGeneratingOutline: false,
736 isGeneratingPresentation: false,
737 activeGenerationPresentationId: null,
738 completedGenerationPresentationId: null,
739 pendingCreateRequest: null,
740
741 setSlides: (slides, type) => {
742 set((state) => ({
743 slides: normalizePresentationSlides(
744 typeof slides === "function" ? slides(state.slides) : slides,
745 ),
746 }));
747
748 pushHistorySnapshot(type, undefined, "full");
749 },
750 updateSlide: (slideId, updates, type) => {
751 const normalizedUpdates = normalizeSlideUpdates(updates);
752
753 set((state) => ({
754 slides: state.slides.map((slide) =>
755 slide.id === slideId ? { ...slide, ...normalizedUpdates } : slide,
756 ),
757 }));
758
759 pushHistorySnapshot(type, slideId, "slide");
760 },
761 setPageBackground: (pageBackground) => set({ pageBackground }),
762
763 // Unified right panel state
764 activeRightPanel: null,
765 setActiveRightPanel: (panel) =>
766 set((state) =>
767 state.activeRightPanel === panel ? state : { activeRightPanel: panel },
768 ),
769 iconPickerCurrentIcon: "",
770 iconPickerSelectIcon: null,
771 iconPickerRemoveIcon: null,
772 openIconPicker: (currentIcon, onSelect, onRemove) =>
773 set({
774 activeRightPanel: "iconPicker",
775 iconPickerCurrentIcon: currentIcon,
776 iconPickerSelectIcon: onSelect,
777 iconPickerRemoveIcon: onRemove ?? null,
778 }),
779 closeIconPicker: () =>
780 set((state) => ({
781 activeRightPanel:
782 state.activeRightPanel === "iconPicker"
783 ? null
784 : state.activeRightPanel,
785 iconPickerCurrentIcon: "",
786 iconPickerSelectIcon: null,
787 iconPickerRemoveIcon: null,
788 })),
789 layoutEditorElementId: null,
790 layoutEditorEditorId: null,
791 layoutEditorElement: null,
792 layoutEditorApplyLayout: null,
793 paletteDropTarget: null,
794 setPaletteDropTarget: (target) => set({ paletteDropTarget: target }),
795 openLayoutEditor: (editorId, elementId, element, applyLayout) =>
796 set({
797 activeRightPanel: "layoutEditor",
798 layoutEditorElementId: elementId,
799 layoutEditorEditorId: editorId,
800 layoutEditorElement: element,
801 layoutEditorApplyLayout: applyLayout ?? null,
802 }),
803
804 // Pending agent message
805 pendingAgentMessage: null,
806 setPendingAgentMessage: (pending) =>
807 set({ pendingAgentMessage: pending }),
808
809 // Image editor state
810 imageEditorInitialMode: null,
811 openImageEditor: (mode = "generate") =>
812 set({
813 imageEditorInitialMode: mode,
814 activeRightPanel: "imageEditor",
815 }),
816 closeImageEditor: () =>
817 set((state) => ({
818 imageEditorInitialMode: null,
819 activeRightPanel:
820 state.activeRightPanel === "imageEditor"
821 ? null
822 : state.activeRightPanel,
823 })),
824
825 // Chart editor state
826 chartEditorData: null,
827 openChartEditor: (chartData, updateElementFn) =>
828 set({
829 activeRightPanel: "chartEditor",
830 chartEditorData: chartData ?? null,
831 boundUpdateElement: updateElementFn ?? null,
832 }),
833 closeChartEditor: () =>
834 set((state) => ({
835 activeRightPanel:
836 state.activeRightPanel === "chartEditor"
837 ? null
838 : state.activeRightPanel,
839 chartEditorData: null,
840 boundUpdateElement:
841 state.activeRightPanel === "chartEditor"
842 ? null
843 : state.boundUpdateElement,
844 })),
845
846 // Infographic editor state
847 openInfographicEditor: (updateElementFn) =>
848 set({
849 activeRightPanel: "infographicEditor",
850 boundUpdateElement: updateElementFn ?? null,
851 }),
852 closeInfographicEditor: () =>
853 set((state) => ({
854 activeRightPanel:
855 state.activeRightPanel === "infographicEditor"
856 ? null
857 : state.activeRightPanel,
858 boundUpdateElement:
859 state.activeRightPanel === "infographicEditor"
860 ? null
861 : state.boundUpdateElement,
862 })),
863 openInfographicGenerationEditor: (updateElementFn) =>
864 set({
865 activeRightPanel: "infographicGenerationEditor",
866 boundUpdateElement: updateElementFn ?? null,
867 }),
868 closeInfographicGenerationEditor: () =>
869 set((state) => ({
870 activeRightPanel:
871 state.activeRightPanel === "infographicGenerationEditor"
872 ? null
873 : state.activeRightPanel,
874 boundUpdateElement:
875 state.activeRightPanel === "infographicGenerationEditor"
876 ? null
877 : state.boundUpdateElement,
878 })),
879
880 // Presentation image editor state (for inline TImageElement editing)
881 presentationImageElementId: null,
882 presentationImageEditorInitialMode: null,
883 presentationImageEditorElement: null,
884 presentationImageEditorFrame: null,
885 boundUpdateElement: null,
886 openPresentationImageEditor: (
887 mode = "generate",
888 updateElementFn,
889 element,
890 frame,
891 ) =>
892 set({
893 presentationImageEditorInitialMode: mode,
894 presentationImageEditorElement: element ?? null,
895 presentationImageEditorFrame: frame ?? null,
896 activeRightPanel: "presentationImageEditor",
897 boundUpdateElement: updateElementFn ?? null,
898 }),
899 closePresentationImageEditor: () =>
900 set((state) => ({
901 presentationImageEditorInitialMode: null,
902 presentationImageEditorElement: null,
903 presentationImageEditorFrame: null,
904 boundUpdateElement: null,
905 activeRightPanel:
906 state.activeRightPanel === "presentationImageEditor"
907 ? null
908 : state.activeRightPanel,
909 })),
910
911 startPresentationImageGeneration: (target, query, options) =>
912 set((state) => {
913 const key = getPresentationImageGenerationKey(target);
914 const existingGeneration = state.rootImageGeneration[key];
915 const presentationId =
916 options?.presentationId ??
917 state.activeGenerationPresentationId ??
918 state.currentPresentationId;
919
920 if (
921 existingGeneration &&
922 existingGeneration.presentationId === presentationId &&
923 existingGeneration.query.trim() === query.trim() &&
924 (existingGeneration.status === "queued" ||
925 existingGeneration.status === "generating")
926 ) {
927 return state;
928 }
929
930 return {
931 rootImageGeneration: {
932 ...state.rootImageGeneration,
933 [key]: {
934 query,
935 ...(presentationId ? { presentationId } : {}),
936 source:
937 options?.source ??
938 resolvePresentationImageGenerationSource({
939 globalImageSource: state.imageSource,
940 imageSource:
941 target.kind === "root"
942 ? state.slides.find(
943 (slide) => slide.id === target.slideId,
944 )?.rootImage?.imageSource
945 : undefined,
946 isImageSlide: state.slides.find(
947 (slide) => slide.id === target.slideId,
948 )?.isImageSlide,
949 }),
950 status: "queued",
951 target,
952 ...(options?.imageModel
953 ? { imageModel: options.imageModel }
954 : {}),
955 ...(options?.stockImageProvider
956 ? { stockImageProvider: options.stockImageProvider }
957 : {}),
958 },
959 },
960 };
961 }),
962 completePresentationImageGeneration: (key, url) =>
963 set((state) => {
964 const presentationId =
965 state.activeGenerationPresentationId ?? state.currentPresentationId;
966
967 return {
968 rootImageGeneration: {
969 ...state.rootImageGeneration,
970 [key]: {
971 ...(state.rootImageGeneration[key] ?? {
972 query: "",
973 ...(presentationId ? { presentationId } : {}),
974 source: "ai" as const,
975 status: "success" as const,
976 target: getRootImageGenerationTarget(key),
977 }),
978 status: "success",
979 url,
980 },
981 },
982 };
983 }),
984 failPresentationImageGeneration: (key, error) =>
985 set((state) => {
986 const presentationId =
987 state.activeGenerationPresentationId ?? state.currentPresentationId;
988
989 return {
990 rootImageGeneration: {
991 ...state.rootImageGeneration,
992 [key]: {
993 ...(state.rootImageGeneration[key] ?? {
994 query: "",
995 ...(presentationId ? { presentationId } : {}),
996 source: "ai" as const,
997 status: "error" as const,
998 target: getRootImageGenerationTarget(key),
999 }),
1000 status: "error",
1001 error,
1002 },
1003 },
1004 };
1005 }),
1006 clearPresentationImageGeneration: (targetOrKey) =>
1007 set((state) => {
1008 const key =
1009 typeof targetOrKey === "string"
1010 ? targetOrKey
1011 : getPresentationImageGenerationKey(targetOrKey);
1012 const { [key]: _removed, ...rest } = state.rootImageGeneration;
1013 return { rootImageGeneration: rest } as Partial<PresentationState>;
1014 }),
1015 startRootImageGeneration: (slideId, query, options) => {
1016 const normalizedOptions =
1017 typeof options === "string" ? { imageModel: options } : options;
1018
1019 get().startPresentationImageGeneration(
1020 getRootImageGenerationTarget(slideId),
1021 query,
1022 normalizedOptions,
1023 );
1024 },
1025 completeRootImageGeneration: (slideId, url) =>
1026 get().completePresentationImageGeneration(slideId, url),
1027 failRootImageGeneration: (slideId, error) =>
1028 get().failPresentationImageGeneration(slideId, error),
1029 clearRootImageGeneration: (slideId) =>
1030 get().clearPresentationImageGeneration(slideId),
1031 setCurrentPresentation: (id, title) =>
1032 set((state) =>
1033 state.currentPresentationId === id &&
1034 state.currentPresentationTitle === title
1035 ? state
1036 : {
1037 currentPresentationId: id,
1038 currentPresentationOwnerId:
1039 state.currentPresentationId === id
1040 ? state.currentPresentationOwnerId
1041 : null,
1042 currentPresentationTitle: title,
1043 },
1044 ),
1045 setCurrentPresentationOwnerId: (ownerId) =>
1046 set((state) =>
1047 state.currentPresentationOwnerId === ownerId
1048 ? state
1049 : { currentPresentationOwnerId: ownerId },
1050 ),
1051 setCurrentPresentationUpdatedAt: (updatedAt) => {
1052 const currentPresentationUpdatedAt =
1053 updatedAt instanceof Date ? updatedAt.toISOString() : updatedAt;
1054
1055 set((state) =>
1056 state.currentPresentationUpdatedAt === currentPresentationUpdatedAt
1057 ? state
1058 : { currentPresentationUpdatedAt },
1059 );
1060 },
1061 setOutputFormat: (outputFormat) =>
1062 set((state) =>
1063 state.outputFormat === outputFormat ? state : { outputFormat },
1064 ),
1065 setContentVersion: (contentVersion) => set({ contentVersion }),
1066 setIsGridView: (isGrid) => set({ isGridView: isGrid }),
1067 setIsSheetOpen: (isOpen) => set({ isSheetOpen: isOpen }),
1068 setNumSlides: (num) => set({ numSlides: num }),
1069 setLanguage: (lang) => set({ language: lang }),
1070 setModelProvider: (provider) => set({ modelProvider: provider }),
1071 setModelId: (id) => set({ modelId: id }),
1072 setTheme: (theme, customData, type) => {
1073 set((state) => {
1074 let nextCustomThemeData: ThemeProperties | null;
1075
1076 if (theme === "auto" && customData !== undefined) {
1077 // Auto theme with explicit data (e.g., customization save)
1078 nextCustomThemeData = customData;
1079 } else if (theme === "auto") {
1080 // Auto theme without explicit data: use generatedThemeData
1081 nextCustomThemeData = state.generatedThemeData;
1082 } else if (customData !== undefined) {
1083 // Explicit data passed (e.g., loading from DB or non-built-in theme)
1084 nextCustomThemeData = customData;
1085 } else if (isBuiltInPresentationTheme(theme)) {
1086 // Restore only the customization saved for this exact built-in theme.
1087 nextCustomThemeData = state.themeDataByTheme[theme] ?? null;
1088 } else {
1089 // Theme switch without explicit data: clear customization
1090 nextCustomThemeData = null;
1091 }
1092
1093 return {
1094 theme,
1095 customThemeData: nextCustomThemeData,
1096 };
1097 });
1098
1099 if (theme !== null) {
1100 pushHistorySnapshot(type, undefined, "theme");
1101 }
1102 },
1103 setThemeDataByTheme: (themeDataByTheme) => set({ themeDataByTheme }),
1104 setGeneratedThemeData: (data) => set({ generatedThemeData: data }),
1105 setPageStyle: (style) => set({ pageStyle: style }),
1106 setPresentationInput: (input) => set({ presentationInput: input }),
1107 setOutline: (topics) => set({ outline: topics }),
1108 setSearchResults: (results) => set({ searchResults: results }),
1109 setImageSearchResults: (results) => set({ imageSearchResults: results }),
1110 setOutlineToolCalls: (outlineToolCalls) => set({ outlineToolCalls }),
1111 setWebSearchEnabled: (enabled) => set({ webSearchEnabled: enabled }),
1112 setAutoThemeEnabled: (enabled) => set({ autoThemeEnabled: enabled }),
1113 setImageModel: (model) => set({ imageModel: model }),
1114 setImageSource: (source) => set({ imageSource: source }),
1115 setStockImageProvider: (provider) =>
1116 set({ stockImageProvider: provider }),
1117 setPresentationStyle: (style) => set({ presentationStyle: style }),
1118 setGenerationAspectRatio: (generationAspectRatio) =>
1119 set({ generationAspectRatio }),
1120 setTextContent: (content) => set({ textContent: content }),
1121 setTone: (tone) => set({ tone }),
1122 setAudience: (audience) => set({ audience }),
1123 setScenario: (scenario) => set({ scenario }),
1124 setSavingStatus: (status) => set({ savingStatus: status }),
1125 setIsPresenting: (isPresenting) =>
1126 set(() => ({
1127 isPresenting,
1128 ...(isPresenting
1129 ? {}
1130 : { isPresentingLoading: false, presentingScaleLocks: {} }),
1131 })),
1132 setIsPresentingLoading: (isLoading) =>
1133 set({ isPresentingLoading: isLoading }),
1134 setPresentingScaleLock: (slideId, locked) =>
1135 set((state) => ({
1136 presentingScaleLocks: {
1137 ...state.presentingScaleLocks,
1138 [slideId]: locked,
1139 },
1140 })),
1141 resetPresentingScaleLocks: () => set({ presentingScaleLocks: {} }),
1142 setCurrentSlideId: (id) => set({ currentSlideId: id }),
1143 nextSlide: () => {
1144 set((state) => {
1145 const currentIndex = state.slides.findIndex(
1146 (s) => s.id === state.currentSlideId,
1147 );
1148 const newIndex = Math.min(
1149 (currentIndex === -1 ? 0 : currentIndex) + 1,
1150 state.slides.length - 1,
1151 );
1152 const newSlideId = state.slides[newIndex]?.id ?? null;
1153 return { currentSlideId: newSlideId };
1154 });
1155 },
1156 previousSlide: () =>
1157 set((state) => {
1158 const currentIndex = state.slides.findIndex(
1159 (s) => s.id === state.currentSlideId,
1160 );
1161 const newIndex = Math.max(
1162 (currentIndex === -1 ? 0 : currentIndex) - 1,
1163 0,
1164 );
1165 const newSlideId = state.slides[newIndex]?.id ?? null;
1166 return {
1167 currentSlideId: newSlideId,
1168 };
1169 }),
1170
1171 // Generation actions
1172 setShouldStartOutlineGeneration: (shouldStart) =>
1173 set({ shouldStartOutlineGeneration: shouldStart }),
1174 setShouldStartPresentationGeneration: (shouldStart) =>
1175 set({ shouldStartPresentationGeneration: shouldStart }),
1176 setShouldStartImageSlideGeneration: (shouldStart) =>
1177 set({ shouldStartImageSlideGeneration: shouldStart }),
1178 setIsGeneratingOutline: (isGenerating) =>
1179 set({ isGeneratingOutline: isGenerating }),
1180 setIsGeneratingPresentation: (isGenerating) =>
1181 set({ isGeneratingPresentation: isGenerating }),
1182 completePresentationGeneration: () =>
1183 set((state) => ({
1184 isGeneratingPresentation: false,
1185 activeGenerationPresentationId: null,
1186 completedGenerationPresentationId:
1187 state.activeGenerationPresentationId ?? state.currentPresentationId,
1188 })),
1189 dismissCompletedGeneration: (presentationId) =>
1190 set((state) =>
1191 state.completedGenerationPresentationId === presentationId
1192 ? { completedGenerationPresentationId: null }
1193 : {},
1194 ),
1195 setPendingCreateRequest: (pendingCreateRequest) =>
1196 set({ pendingCreateRequest }),
1197 consumePendingCreateRequest: () => {
1198 const pendingCreateRequest = get().pendingCreateRequest;
1199 if (!pendingCreateRequest) {
1200 return null;
1201 }
1202
1203 set({ pendingCreateRequest: null });
1204 return pendingCreateRequest;
1205 },
1206 startOutlineGeneration: () =>
1207 set({
1208 shouldStartOutlineGeneration: true,
1209 isGeneratingOutline: true,
1210 shouldStartPresentationGeneration: false,
1211 isGeneratingPresentation: false,
1212 activeGenerationPresentationId: null,
1213 completedGenerationPresentationId: null,
1214 isPresenting: false,
1215 isPresentingLoading: false,
1216 presentingScaleLocks: {},
1217 outline: [],
1218 searchResults: [],
1219 imageSearchResults: [],
1220 outlineToolCalls: [],
1221 outlineItemIds: [],
1222 outlineTemplateOverrides: {},
1223 slides: [],
1224 rootImageGeneration: {},
1225 }),
1226 startPresentationGeneration: () =>
1227 set((state) =>
1228 state.outline.some((item) => item.trim().length > 0)
1229 ? {
1230 shouldStartPresentationGeneration: true,
1231 isGeneratingPresentation: true,
1232 activeGenerationPresentationId: state.currentPresentationId,
1233 completedGenerationPresentationId: null,
1234 isPresenting: false,
1235 isPresentingLoading: false,
1236 presentingScaleLocks: {},
1237 slides: [],
1238 rootImageGeneration: {},
1239 }
1240 : {
1241 shouldStartPresentationGeneration: false,
1242 isGeneratingPresentation: false,
1243 activeGenerationPresentationId: null,
1244 isPresenting: false,
1245 isPresentingLoading: false,
1246 presentingScaleLocks: {},
1247 },
1248 ),
1249 startImageSlideGeneration: () =>
1250 set((state) =>
1251 state.outline.some((item) => item.trim().length > 0)
1252 ? {
1253 shouldStartImageSlideGeneration: true,
1254 isGeneratingPresentation: true,
1255 activeGenerationPresentationId: state.currentPresentationId,
1256 completedGenerationPresentationId: null,
1257 isPresenting: false,
1258 isPresentingLoading: false,
1259 presentingScaleLocks: {},
1260 slides: [],
1261 rootImageGeneration: {},
1262 }
1263 : {
1264 shouldStartImageSlideGeneration: false,
1265 isGeneratingPresentation: false,
1266 activeGenerationPresentationId: null,
1267 isPresenting: false,
1268 isPresentingLoading: false,
1269 presentingScaleLocks: {},
1270 },
1271 ),
1272 resetGeneration: () =>
1273 set({
1274 shouldStartOutlineGeneration: false,
1275 shouldStartPresentationGeneration: false,
1276 shouldStartImageSlideGeneration: false,
1277 isGeneratingOutline: false,
1278 isGeneratingPresentation: false,
1279 activeGenerationPresentationId: null,
1280 completedGenerationPresentationId: null,
1281 searchResults: [],
1282 imageSearchResults: [],
1283 outlineToolCalls: [],
1284 }),
1285
1286 // Reset everything except ID and current input when starting new outline generation
1287 resetForNewGeneration: () =>
1288 set(() => ({
1289 thumbnailUrl: undefined,
1290 outline: [],
1291 searchResults: [],
1292 imageSearchResults: [],
1293 outlineToolCalls: [],
1294 slides: [],
1295 rootImageGeneration: {},
1296 pageBackground: {},
1297 selectedSlideTemplates: [],
1298 outlineItemIds: [],
1299 outlineTemplateOverrides: {},
1300 })),
1301
1302 // Comprehensive reset when navigating back to the presentation dashboard.
1303 resetPresentationState: () =>
1304 set(() => ({
1305 // Clear presentation-specific state
1306 currentPresentationId: null,
1307 currentPresentationTitle: null,
1308 currentPresentationUpdatedAt: null,
1309 currentPresentationOwnerId: null,
1310 contentVersion: 0,
1311 presentationInput: "",
1312 outline: [],
1313 slides: [],
1314 searchResults: [],
1315 imageSearchResults: [],
1316 outlineToolCalls: [],
1317 rootImageGeneration: {},
1318 attachedFiles: [],
1319 pageBackground: {},
1320 customThemeData: null,
1321 generatedThemeData: null,
1322 themeDataByTheme: {},
1323 thumbnailUrl: undefined,
1324 generationAspectRatio: DEFAULT_PRESENTATION_GENERATION_ASPECT_RATIO,
1325 isManualExtractionEnabled: false,
1326 selectedChunks: [],
1327 extractorRagIds: [],
1328 currentExtractorRagId: null,
1329
1330 // Reset generation flags
1331 shouldStartOutlineGeneration: false,
1332 shouldStartPresentationGeneration: false,
1333 isGeneratingOutline: false,
1334 isGeneratingPresentation: false,
1335 activeGenerationPresentationId: null,
1336 completedGenerationPresentationId: null,
1337 pendingCreateRequest: null,
1338 outputFormat: "flow",
1339
1340 // Reset UI state
1341 activeRightPanel: null,
1342 iconPickerCurrentIcon: "",
1343 iconPickerSelectIcon: null,
1344 iconPickerRemoveIcon: null,
1345 layoutEditorElementId: null,
1346 layoutEditorEditorId: null,
1347 layoutEditorElement: null,
1348 layoutEditorApplyLayout: null,
1349 paletteDropTarget: null,
1350 pendingAgentMessage: null,
1351 imageEditorInitialMode: null,
1352 presentationImageElementId: null,
1353 presentationImageEditorInitialMode: null,
1354 boundUpdateElement: null,
1355 isSidebarCollapsed: false,
1356 isRightPanelCollapsed: false,
1357 currentSlideId: null,
1358 savingStatus: "idle",
1359 isPresenting: false,
1360 isPresentingLoading: false,
1361 presentingScaleLocks: {},
1362 generatedImageCache: {},
1363 selectedSlideTemplates: [],
1364 outlineItemIds: [],
1365 outlineTemplateOverrides: {},
1366 isReadOnly: false,
1367 })),
1368
1369 setIsThemeCreatorOpen: (update) => set({ isThemeCreatorOpen: update }),
1370 // Selection state
1371 isSelecting: false,
1372 selectedPresentations: [],
1373 toggleSelecting: () =>
1374 set((state) => ({
1375 isSelecting: !state.isSelecting,
1376 selectedPresentations: [],
1377 })),
1378 selectAllPresentations: (ids) => set({ selectedPresentations: ids }),
1379 deselectAllPresentations: () => set({ selectedPresentations: [] }),
1380 togglePresentationSelection: (id) =>
1381 set((state) => ({
1382 selectedPresentations: state.selectedPresentations.includes(id)
1383 ? state.selectedPresentations.filter((p) => p !== id)
1384 : [...state.selectedPresentations, id],
1385 })),
1386 }),
1387 {
1388 name: "presentation-state",
1389 storage: createJSONStorage(() => localStorage),
1390 partialize: (state) => ({
1391 attachedFiles: state.attachedFiles,
1392 audience: state.audience,
1393 activeGenerationPresentationId: state.activeGenerationPresentationId,
1394 autoThemeEnabled: state.autoThemeEnabled,
1395 currentExtractorRagId: state.currentExtractorRagId,
1396 currentPresentationId: state.currentPresentationId,
1397 currentPresentationOwnerId: state.currentPresentationOwnerId,
1398 currentPresentationTitle: state.currentPresentationTitle,
1399 currentPresentationUpdatedAt: state.currentPresentationUpdatedAt,
1400 customThemeData: state.customThemeData,
1401 themeDataByTheme: state.themeDataByTheme,
1402 generatedThemeData: state.generatedThemeData,
1403 extractorRagIds: state.extractorRagIds,
1404 generationAspectRatio: state.generationAspectRatio,
1405 imageSearchResults: state.imageSearchResults,
1406 imageSource: state.imageSource,
1407 language: state.language,
1408 numSlides: state.numSlides,
1409 outline: state.outline,
1410 outlineItemIds: state.outlineItemIds,
1411 outlineTemplateOverrides: state.outlineTemplateOverrides,
1412 outlineToolCalls: state.outlineToolCalls,
1413 outputFormat: state.outputFormat,
1414 pageBackground: state.pageBackground,
1415 pageStyle: state.pageStyle,
1416 pendingCreateRequest: state.pendingCreateRequest,
1417 presentationInput: state.presentationInput,
1418 presentationStyle: state.presentationStyle,
1419 scenario: state.scenario,
1420 searchResults: state.searchResults,
1421 selectedChunks: state.selectedChunks,
1422 selectedSlideTemplates: state.selectedSlideTemplates,
1423 shouldStartImageSlideGeneration: state.shouldStartImageSlideGeneration,
1424 shouldStartOutlineGeneration: state.shouldStartOutlineGeneration,
1425 shouldStartPresentationGeneration:
1426 state.shouldStartPresentationGeneration,
1427 stockImageProvider: state.stockImageProvider,
1428 textContent: state.textContent,
1429 theme: state.theme,
1430 tone: state.tone,
1431 webSearchEnabled: state.webSearchEnabled,
1432 }),
1433 },
1434 ),
1435 );
1436
1436 lines TYPESCRIPT