返回 presentation-ai
icon-list-item-static.tsx
根目录 / src / components / notebook / presentation / editor / custom-elements / static / icon-list-item-static.tsx
1 "use client";
2
3 import { ImageIcon } from "lucide-react";
4 import Image from "next/image";
5 import { NodeApi, PathApi } from "platejs";
6 import { SlateElement, type SlateElementProps } from "platejs/static";
7 import { useEffect } from "react";
8
9 import { Spinner } from "@/components/ui/spinner";
10 import {
11 getElementImageGenerationKey,
12 getElementImageGenerationTarget,
13 resolvePresentationImageGenerationSource,
14 } from "@/lib/presentation/image-generation";
15 import { cn } from "@/lib/utils";
16 import { usePresentationState } from "@/states/presentation-state";
17 import { type TIconListItemElement } from "../../plugins/icon-list-plugin";
18 import {
19 getAlignmentClasses,
20 getIconListMediaSize,
21 getIconListOrientation,
22 getIconListVariant,
23 } from "../../utils";
24 import { PresentationIcon } from "../presentation-icon";
25 import { getPresentationImageCropStyles } from "../presentation-image-layout";
26
27 export function IconListItemStatic(
28 props: SlateElementProps<TIconListItemElement>,
29 ) {
30 const slideId = String(props.editor.id ?? "");
31 const path = props.editor.api.findPath(props.element) ?? [-1];
32 const parentPath = PathApi.parent(path);
33 const parentElement = NodeApi.get(props.editor, parentPath) as {
34 alignment?: "left" | "center" | "right";
35 children?: unknown[];
36 mediaSize?: unknown;
37 orientation?: unknown;
38 variant?: unknown;
39 };
40 const alignment =
41 props.element.alignment ?? parentElement?.alignment ?? "left";
42 const { icon } = props.element;
43 const variant = getIconListVariant(parentElement?.variant);
44 const orientation = getIconListOrientation(
45 parentElement?.orientation,
46 parentElement?.children?.length ?? 0,
47 );
48 const mediaSize = getIconListMediaSize(parentElement?.mediaSize);
49 const iconSize = Math.round(mediaSize);
50 const mediaStyle = {
51 height: mediaSize,
52 width: mediaSize,
53 };
54 const prompt = props.element.prompt ?? props.element.query ?? "";
55 const imageSource = usePresentationState((s) => s.imageSource);
56 const imageModel = usePresentationState((s) => s.imageModel);
57 const stockImageProvider = usePresentationState((s) => s.stockImageProvider);
58 const startPresentationImageGeneration = usePresentationState(
59 (s) => s.startPresentationImageGeneration,
60 );
61 const rootImageGeneration = usePresentationState(
62 (s) => s.rootImageGeneration,
63 );
64 const elementId =
65 typeof props.element.id === "string" ? props.element.id : undefined;
66 const generationKey =
67 slideId && elementId
68 ? getElementImageGenerationKey(slideId, elementId)
69 : null;
70 const rawComputedGen = generationKey
71 ? rootImageGeneration[generationKey]
72 : undefined;
73 const computedGen =
74 rawComputedGen && rawComputedGen.query.trim() === prompt.trim()
75 ? rawComputedGen
76 : undefined;
77 const computedImageUrl =
78 computedGen?.status === "success" && computedGen.url
79 ? computedGen.url
80 : props.element.url;
81 const isGenerating =
82 computedGen?.status === "queued" || computedGen?.status === "generating";
83 const hasGenerationFailed =
84 computedGen?.status === "error" ||
85 props.element.imageGenerationStatus === "failed";
86
87 useEffect(() => {
88 const trimmedPrompt = prompt.trim();
89
90 if (
91 variant !== "image" ||
92 !slideId ||
93 !elementId ||
94 !trimmedPrompt ||
95 computedImageUrl ||
96 hasGenerationFailed
97 ) {
98 return;
99 }
100
101 if (computedGen?.query.trim() === trimmedPrompt) {
102 return;
103 }
104
105 const source = resolvePresentationImageGenerationSource({
106 globalImageSource: imageSource,
107 imageSource: props.element.imageSource,
108 });
109 const generationTarget = getElementImageGenerationTarget(
110 slideId,
111 elementId,
112 );
113
114 startPresentationImageGeneration(generationTarget, trimmedPrompt, {
115 imageModel,
116 source,
117 ...(source === "stock"
118 ? {
119 stockImageProvider:
120 props.element.stockImageProvider ?? stockImageProvider,
121 }
122 : {}),
123 });
124 }, [
125 computedGen?.query,
126 computedImageUrl,
127 elementId,
128 hasGenerationFailed,
129 imageModel,
130 imageSource,
131 prompt,
132 props.element.imageSource,
133 props.element.stockImageProvider,
134 slideId,
135 startPresentationImageGeneration,
136 stockImageProvider,
137 variant,
138 ]);
139
140 const mediaElement =
141 variant === "image" ? (
142 <div
143 className="flex shrink-0 items-center justify-center overflow-hidden rounded-md border bg-muted/30 shadow-xs"
144 data-decor="true"
145 style={mediaStyle}
146 >
147 {isGenerating && !computedImageUrl ? (
148 <Spinner className="size-5" />
149 ) : computedImageUrl ? (
150 <Image
151 unoptimized
152 width={400}
153 height={300}
154 alt={prompt}
155 className="size-full object-cover"
156 decoding="async"
157 loading="lazy"
158 src={computedImageUrl}
159 style={getPresentationImageCropStyles(props.element.cropSettings)}
160 />
161 ) : (
162 <ImageIcon
163 aria-hidden="true"
164 className={cn(
165 "text-muted-foreground",
166 hasGenerationFailed && "text-destructive",
167 )}
168 size={iconSize}
169 />
170 )}
171 </div>
172 ) : icon ? (
173 <div data-decor="true" style={mediaStyle}>
174 <PresentationIcon
175 icon={icon}
176 size={iconSize}
177 className="flex size-full shrink-0 items-center justify-center"
178 iconClassName="size-full"
179 />
180 </div>
181 ) : null;
182
183 return (
184 <SlateElement {...props}>
185 <div className={cn("group/icon-item relative w-full")}>
186 <div
187 className={cn(
188 "flex w-full gap-4",
189 orientation === "top" ? "flex-col items-start" : "items-start",
190 orientation === "side" &&
191 alignment === "right" &&
192 "flex-row-reverse",
193 alignment === "center" && "justify-center",
194 orientation === "top" && alignment === "center" && "items-center",
195 orientation === "top" && alignment === "right" && "items-end",
196 )}
197 >
198 {mediaElement}
199
200 <div className={cn("min-w-0 flex-1", getAlignmentClasses(alignment))}>
201 {props.children}
202 </div>
203 </div>
204 </div>
205 </SlateElement>
206 );
207 }
208
208 lines Plain Text