| 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 |