返回 presentation-ai
SlideEditorContext.tsx
根目录 / src / components / presentation / slides / slide-editor / SlideEditorContext.tsx
1 "use client";
2
3 import type React from "react";
4 import { createContext, useCallback, useContext, useState } from "react";
5
6 import { type LayoutType } from "@/components/notebook/presentation/utils/parser";
7 import { serializeSlidesToXml } from "@/components/notebook/presentation/utils/slide-serializer";
8 import { usePresentationState } from "@/states/presentation-state";
9 import {
10 layoutMap,
11 type ContentAlignment,
12 type SlideUpdatePayload,
13 } from "./types";
14
15 const getPresentationState = usePresentationState.getState;
16
17 interface SlideEditorContextValue {
18 // State
19 slideId: string;
20 isMenuOpen: boolean;
21 setIsMenuOpen: (open: boolean) => void;
22 showDeleteConfirm: boolean;
23 setShowDeleteConfirm: (show: boolean) => void;
24 aiInput: string;
25 setAiInput: (input: string) => void;
26 selectedLayout: number;
27 setSelectedLayout: (layout: number) => void;
28
29 // Callbacks
30 onDuplicate?: () => void;
31 onDelete?: () => void;
32 dragListeners?: React.HTMLAttributes<HTMLElement>;
33
34 // Derived state
35 currentSlide:
36 | ReturnType<typeof getPresentationState>["slides"][0]
37 | undefined;
38 currentTheme: string;
39 currentThemeData: ReturnType<typeof getPresentationState>["customThemeData"];
40 currentLayout: LayoutType;
41 currentWidth: "S" | "M" | "L";
42 currentAlignment: ContentAlignment;
43 hasRootImage: boolean;
44
45 // Actions
46 updateSlide: (updates: SlideUpdatePayload) => void;
47 handleImageEdit: () => void;
48 handleLayoutChange: (idx: number) => void;
49 handleMagicPrompt: (prompt: string) => void;
50 handleAiSubmit: () => void;
51 handleAiKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
52 }
53
54 const SlideEditorContext = createContext<SlideEditorContextValue | null>(null);
55
56 export function useSlideEditorContext() {
57 const context = useContext(SlideEditorContext);
58 if (!context) {
59 throw new Error(
60 "useSlideEditorContext must be used within SlideEditorProvider",
61 );
62 }
63 return context;
64 }
65
66 interface SlideEditorProviderProps {
67 slideId: string;
68 dragListeners?: React.HTMLAttributes<HTMLElement>;
69 onDuplicate?: () => void;
70 onDelete?: () => void;
71 children: React.ReactNode;
72 }
73
74 export function SlideEditorProvider({
75 slideId,
76 dragListeners,
77 onDuplicate,
78 onDelete,
79 children,
80 }: SlideEditorProviderProps) {
81 const openImageEditor = usePresentationState((s) => s.openImageEditor);
82 const setCurrentSlideId = usePresentationState((s) => s.setCurrentSlideId);
83 const setPendingAgentMessage = usePresentationState(
84 (s) => s.setPendingAgentMessage,
85 );
86 const setActiveRightPanel = usePresentationState(
87 (s) => s.setActiveRightPanel,
88 );
89
90 // Local state
91 const [isMenuOpen, setIsMenuOpen] = useState(false);
92 const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
93 const [aiInput, setAiInput] = useState("");
94
95 // Helper to get current slide data on-demand
96 const getCurrentSlideData = useCallback(() => {
97 const { slides } = usePresentationState.getState();
98 const currentSlide = slides.find((s) => s.id === slideId);
99 return {
100 currentSlide,
101 currentLayout: currentSlide?.layoutType ?? "none",
102 currentWidth: currentSlide?.width ?? "M",
103 currentAlignment: currentSlide?.alignment ?? "start",
104 hasRootImage: !!currentSlide?.rootImage?.url,
105 };
106 }, [slideId]);
107
108 // Derived state from Zustand (non-reactive for theme)
109 const currentTheme = usePresentationState.getState().theme;
110 const currentThemeData = usePresentationState.getState().customThemeData;
111
112 // Get initial slide data for selected layout initialization
113 const initialSlideData = getCurrentSlideData();
114
115 // Selected layout state
116 const [selectedLayout, setSelectedLayout] = useState(
117 initialSlideData.currentLayout === "vertical"
118 ? 0
119 : initialSlideData.currentLayout === "left"
120 ? 2
121 : initialSlideData.currentLayout === "right"
122 ? 3
123 : initialSlideData.currentLayout === "background"
124 ? 4
125 : 1,
126 );
127
128 // Actions
129 const updateSlide = useCallback(
130 (updates: SlideUpdatePayload) => {
131 const { slides, setSlides } = usePresentationState.getState();
132 const updatedSlides = slides.map((slide) => {
133 if (slide.id === slideId) {
134 return {
135 ...slide,
136 ...updates,
137 };
138 }
139 return slide;
140 });
141 setSlides(updatedSlides);
142 },
143 [slideId],
144 );
145
146 const handleImageEdit = useCallback(() => {
147 setCurrentSlideId(slideId);
148 openImageEditor("generate");
149 }, [openImageEditor, setCurrentSlideId, slideId]);
150
151 const handleLayoutChange = useCallback(
152 (idx: number) => {
153 setSelectedLayout(idx);
154 updateSlide({ layoutType: layoutMap[idx] });
155 },
156 [updateSlide],
157 );
158
159 const handleMagicPrompt = useCallback(
160 (prompt: string) => {
161 const trimmedPrompt = prompt.trim();
162 if (!trimmedPrompt) return;
163
164 const { slides } = usePresentationState.getState();
165 const slideToEdit = slides.find((s) => s.id === slideId);
166 if (!slideToEdit) return;
167
168 const slideXml = serializeSlidesToXml([slideToEdit], false);
169 setCurrentSlideId(slideId);
170 setPendingAgentMessage({
171 message: trimmedPrompt,
172 slideContext: slideXml,
173 });
174 setActiveRightPanel("agent");
175 },
176 [setActiveRightPanel, setCurrentSlideId, setPendingAgentMessage, slideId],
177 );
178
179 const handleAiSubmit = useCallback(() => {
180 const trimmedInput = aiInput.trim();
181 if (!trimmedInput) return;
182
183 handleMagicPrompt(trimmedInput);
184 setAiInput("");
185 }, [aiInput, handleMagicPrompt]);
186
187 const handleAiKeyDown = useCallback(
188 (e: React.KeyboardEvent<HTMLInputElement>) => {
189 if (e.key === "Enter" && !e.shiftKey) {
190 e.preventDefault();
191 handleAiSubmit();
192 }
193 },
194 [handleAiSubmit],
195 );
196
197 const value: SlideEditorContextValue = {
198 slideId,
199 isMenuOpen,
200 setIsMenuOpen,
201 showDeleteConfirm,
202 setShowDeleteConfirm,
203 aiInput,
204 setAiInput,
205 selectedLayout,
206 setSelectedLayout,
207 onDuplicate,
208 onDelete,
209 dragListeners,
210 currentSlide: initialSlideData.currentSlide,
211 currentTheme,
212 currentThemeData,
213 currentLayout: initialSlideData.currentLayout,
214 currentWidth: initialSlideData.currentWidth,
215 currentAlignment: initialSlideData.currentAlignment,
216 hasRootImage: initialSlideData.hasRootImage,
217 updateSlide,
218 handleImageEdit,
219 handleLayoutChange,
220 handleMagicPrompt,
221 handleAiSubmit,
222 handleAiKeyDown,
223 };
224
225 return (
226 <SlideEditorContext.Provider value={value}>
227 {children}
228 </SlideEditorContext.Provider>
229 );
230 }
231
231 lines Plain Text