返回 presentation-ai
presentationActions.ts
根目录 / src / app / _actions / notebook / presentation / presentationActions.ts
1 "use server";
2
3 import { type PlateSlide } from "@/components/notebook/presentation/utils/parser";
4 import { type NotebookAgentToolCall } from "@/lib/notebook/agent-activity";
5 import { type NotebookSelectedChunk } from "@/lib/notebook/attachments";
6 import { type PresentationCustomization } from "@/lib/presentation/customization";
7 import { getPresentationThumbnailUrl } from "@/lib/presentation/thumbnail";
8 import { isPresentationAutoTheme } from "@/lib/presentation/theme-resolution";
9 import { auth } from "@/server/auth";
10 import { db } from "@/server/db";
11 import { canEditDocument, canReadDocument } from "@/server/share/authorization";
12 import { normalizeShareEmail } from "@/server/share/utils";
13 import { type InputJsonValue } from "@prisma/client/runtime/client";
14 import { notFound } from "next/navigation";
15
16 export type PresentationOwnerProfile = {
17 id: string;
18 image: string | null;
19 name: string | null;
20 };
21
22 export async function createPresentation({
23 content,
24 title,
25 theme = "mystique",
26 outline,
27 imageSource,
28 presentationStyle,
29 customization,
30 language,
31 }: {
32 content: {
33 slides: PlateSlide[];
34 };
35 title: string;
36 theme?: string;
37 outline?: string[];
38 imageSource?: string;
39 presentationStyle?: string;
40 customization?: PresentationCustomization;
41 language?: string;
42 }) {
43 const session = await auth();
44 if (!session?.user) {
45 throw new Error("Unauthorized");
46 }
47
48 try {
49 const presentation = await db.baseDocument.create({
50 data: {
51 type: "PRESENTATION",
52 documentType: "presentation",
53 title: title || "Untitled Presentation",
54 userId: session.user.id,
55 thumbnailUrl: getPresentationThumbnailUrl(content.slides) ?? undefined,
56 presentation: {
57 create: {
58 content: content as unknown as InputJsonValue,
59 ...(!isPresentationAutoTheme(theme) ? { theme } : {}),
60 imageSource,
61 presentationStyle,
62 customization: customization as InputJsonValue | undefined,
63 language,
64 outline,
65 },
66 },
67 },
68 include: {
69 presentation: true,
70 },
71 });
72
73 return {
74 success: true,
75 message: "Presentation created successfully",
76 presentation,
77 };
78 } catch (error) {
79 console.error(error);
80 return {
81 success: false,
82 message: "Failed to create presentation",
83 };
84 }
85 }
86
87 export async function createEmptyPresentation({
88 title,
89 theme = "mystique",
90 language = "en-US",
91 customization,
92 }: {
93 title: string;
94 theme?: string;
95 language?: string;
96 customization?: PresentationCustomization;
97 }) {
98 return createPresentation({
99 content: { slides: [] },
100 title,
101 theme,
102 language,
103 customization,
104 });
105 }
106
107 export async function createBlankPresentation(
108 title: string,
109 theme = "mystique",
110 language = "en-US",
111 ) {
112 const blankSlide: PlateSlide = {
113 content: [
114 {
115 type: "h1",
116 children: [{ text: "" }],
117 },
118 ],
119 id: crypto.randomUUID(),
120 alignment: "center",
121 };
122
123 return createPresentation({
124 content: { slides: [blankSlide] },
125 title,
126 theme,
127 language,
128 });
129 }
130
131 export async function updatePresentation({
132 id,
133 content,
134 prompt,
135 title,
136 theme,
137 outline,
138 searchResults,
139 toolCalls,
140 selectedChunks,
141 imageSource,
142 presentationStyle,
143 customization,
144 language,
145 thumbnailUrl,
146 }: {
147 id: string;
148 content?: {
149 slides: PlateSlide[];
150 config?: Record<string, unknown>;
151 };
152 title?: string;
153 theme?: string;
154 prompt?: string;
155 outline?: string[];
156 searchResults?: Array<{ query: string; results: unknown[] }>;
157 toolCalls?: NotebookAgentToolCall[];
158 selectedChunks?: NotebookSelectedChunk[];
159 imageSource?: string;
160 presentationStyle?: string;
161 customization?: PresentationCustomization;
162 language?: string;
163 thumbnailUrl?: string | null;
164 }) {
165 const session = await auth();
166 if (!session?.user) {
167 throw new Error("Unauthorized");
168 }
169
170 const canEdit = await canEditDocument(id, {
171 userId: session.user.id,
172 userEmail: normalizeShareEmail(session.user.email),
173 });
174 if (!canEdit) {
175 return {
176 success: false,
177 message: "You do not have permission to edit this presentation",
178 };
179 }
180
181 try {
182 const shouldPersistTheme =
183 theme !== undefined && !isPresentationAutoTheme(theme);
184
185 const presentation = await db.baseDocument.update({
186 where: { id },
187 data: {
188 title,
189 thumbnailUrl:
190 content !== undefined
191 ? getPresentationThumbnailUrl(content.slides)
192 : thumbnailUrl,
193 presentation: {
194 update: {
195 prompt,
196 content: content as unknown as InputJsonValue,
197 ...(shouldPersistTheme ? { theme } : {}),
198 imageSource,
199 presentationStyle,
200 customization: customization as InputJsonValue | undefined,
201 language,
202 outline,
203 searchResults: searchResults as unknown as InputJsonValue,
204 toolCalls: toolCalls as unknown as InputJsonValue,
205 selectedChunks: selectedChunks as unknown as InputJsonValue,
206 },
207 },
208 },
209 include: {
210 presentation: true,
211 },
212 });
213
214 return {
215 success: true,
216 message: "Presentation updated successfully",
217 presentation,
218 };
219 } catch (error) {
220 console.error(error);
221 return {
222 success: false,
223 message: "Failed to update presentation",
224 };
225 }
226 }
227
228 export async function getPresentationOwner(id: string): Promise<
229 | {
230 success: true;
231 owner: PresentationOwnerProfile;
232 }
233 | {
234 success: false;
235 message: string;
236 }
237 > {
238 const session = await auth();
239 const canRead = await canReadDocument(id, {
240 userId: session?.user.id ?? null,
241 userEmail: session?.user.email
242 ? normalizeShareEmail(session.user.email)
243 : null,
244 });
245
246 if (!canRead) {
247 return {
248 success: false,
249 message: "Unauthorized access",
250 };
251 }
252
253 const presentation = await db.baseDocument.findUnique({
254 where: { id },
255 select: {
256 user: {
257 select: {
258 id: true,
259 image: true,
260 name: true,
261 },
262 },
263 },
264 });
265
266 if (!presentation) {
267 return {
268 success: false,
269 message: "Presentation not found",
270 };
271 }
272
273 return {
274 success: true,
275 owner: presentation.user,
276 };
277 }
278
279 export async function updatePresentationTitle(id: string, title: string) {
280 const session = await auth();
281 if (!session?.user) {
282 throw new Error("Unauthorized");
283 }
284
285 const canEdit = await canEditDocument(id, {
286 userId: session.user.id,
287 userEmail: normalizeShareEmail(session.user.email),
288 });
289 if (!canEdit) {
290 return {
291 success: false,
292 message: "You do not have permission to edit this presentation",
293 };
294 }
295
296 try {
297 const presentation = await db.baseDocument.update({
298 where: { id },
299 data: { title },
300 include: {
301 presentation: true,
302 },
303 });
304
305 return {
306 success: true,
307 message: "Presentation title updated successfully",
308 presentation,
309 };
310 } catch (error) {
311 console.error(error);
312 return {
313 success: false,
314 message: "Failed to update presentation title",
315 };
316 }
317 }
318
319 export async function deletePresentation(id: string) {
320 return deletePresentations([id]);
321 }
322
323 export async function deletePresentations(ids: string[]) {
324 const session = await auth();
325 if (!session?.user) {
326 throw new Error("Unauthorized");
327 }
328
329 try {
330 const result = await db.baseDocument.deleteMany({
331 where: {
332 id: { in: ids },
333 userId: session.user.id,
334 },
335 });
336
337 return {
338 success: result.count > 0,
339 message:
340 ids.length === 1
341 ? "Presentation deleted successfully"
342 : `${result.count} presentations deleted successfully`,
343 };
344 } catch (error) {
345 console.error("Failed to delete presentations:", error);
346 return {
347 success: false,
348 message: "Failed to delete presentations",
349 };
350 }
351 }
352
353 export async function getPresentation(id: string) {
354 const session = await auth();
355 const canRead = await canReadDocument(id, {
356 userId: session?.user.id ?? null,
357 userEmail: normalizeShareEmail(session?.user.email),
358 });
359 const canEdit = await canEditDocument(id, {
360 userId: session?.user.id ?? null,
361 userEmail: normalizeShareEmail(session?.user.email),
362 });
363
364 try {
365 const presentation = await db.baseDocument.findUnique({
366 where: { id },
367 include: {
368 presentation: true,
369 favorites: session?.user.id
370 ? {
371 where: { userId: session.user.id },
372 select: { id: true },
373 }
374 : false,
375 },
376 });
377
378 if (!presentation) {
379 notFound();
380 }
381
382 if (!canRead) {
383 notFound();
384 }
385
386 return {
387 success: true,
388 presentation,
389 canEdit,
390 };
391 } catch (error) {
392 console.error(error);
393 return {
394 success: false,
395 message: "Failed to fetch presentation",
396 };
397 }
398 }
399
400 export async function getPresentationContent(id: string) {
401 const session = await auth();
402 const canRead = await canReadDocument(id, {
403 userId: session?.user.id ?? null,
404 userEmail: normalizeShareEmail(session?.user.email),
405 });
406
407 try {
408 const presentation = await db.baseDocument.findUnique({
409 where: { id },
410 include: {
411 presentation: {
412 select: {
413 id: true,
414 content: true,
415 theme: true,
416 outline: true,
417 customization: true,
418 },
419 },
420 },
421 });
422
423 if (!presentation) {
424 return {
425 success: false,
426 message: "Presentation not found",
427 };
428 }
429
430 if (!canRead) {
431 return {
432 success: false,
433 message: "Unauthorized access",
434 };
435 }
436
437 return {
438 success: true,
439 presentation: presentation.presentation,
440 };
441 } catch (error) {
442 console.error(error);
443 return {
444 success: false,
445 message: "Failed to fetch presentation",
446 };
447 }
448 }
449
450 export async function updatePresentationTheme(id: string, theme: string) {
451 return updatePresentation({ id, theme });
452 }
453
454 export async function duplicatePresentation(id: string, newTitle?: string) {
455 const session = await auth();
456 if (!session?.user) {
457 throw new Error("Unauthorized");
458 }
459
460 const canRead = await canReadDocument(id, {
461 userId: session.user.id,
462 userEmail: normalizeShareEmail(session.user.email),
463 });
464 if (!canRead) {
465 return {
466 success: false,
467 message: "You do not have permission to view this presentation",
468 };
469 }
470
471 try {
472 const original = await db.baseDocument.findUnique({
473 where: { id },
474 include: {
475 presentation: true,
476 },
477 });
478
479 if (!original?.presentation) {
480 return {
481 success: false,
482 message: "Original presentation not found",
483 };
484 }
485
486 const duplicated = await db.baseDocument.create({
487 data: {
488 type: "PRESENTATION",
489 documentType: "presentation",
490 title: newTitle ?? `(Copy) ${original.title}`,
491 userId: session.user.id,
492 thumbnailUrl: original.thumbnailUrl,
493 presentation: {
494 create: {
495 content: original.presentation.content as unknown as InputJsonValue,
496 theme: original.presentation.theme,
497 customization:
498 (original.presentation.customization as InputJsonValue) ??
499 undefined,
500 searchResults:
501 (original.presentation.searchResults as InputJsonValue) ??
502 undefined,
503 toolCalls:
504 (original.presentation.toolCalls as InputJsonValue) ?? undefined,
505 selectedChunks:
506 (original.presentation.selectedChunks as InputJsonValue) ??
507 undefined,
508 },
509 },
510 },
511 include: {
512 presentation: true,
513 },
514 });
515
516 return {
517 success: true,
518 message: "Presentation duplicated successfully",
519 presentation: duplicated,
520 };
521 } catch (error) {
522 console.error(error);
523 return {
524 success: false,
525 message: "Failed to duplicate presentation",
526 };
527 }
528 }
529
529 lines TYPESCRIPT