PluginProbe
Extendify / 3.0.6
Extendify v3.0.6
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / Agent / workflows / block-selector / components / GenerateImageConfirm.jsx

GenerateImageConfirm.jsx in Extendify 3.0.6, at src/Agent/workflows/block-selector/components/GenerateImageConfirm.jsx

283 lines 8.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { walkAndUpdateImageDetails } from '@agent/lib/blocks';
2 import { useChatStore } from '@agent/state/chat';
3 import { useWorkflowStore } from '@agent/state/workflows';
4 import { generateImage } from '@shared/api/DataApi';
5 import { downloadImage } from '@shared/api/wp';
6 import { useImageGenerationStore } from '@shared/state/generate-images';
7 import { registerCoreBlocks } from '@wordpress/block-library';
8 import { getBlockTypes } from '@wordpress/blocks';
9 import { Spinner } from '@wordpress/components';
10 import { humanTimeDiff } from '@wordpress/date';
11 import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
12 import { __, sprintf } from '@wordpress/i18n';
13
14 const preload = (src) =>
15 new Promise((resolve, reject) => {
16 const img = new Image();
17 img.onload = () => resolve(src);
18 img.onerror = reject;
19 img.src = src;
20 });
21
22 export const GenerateImageConfirm = ({
23 inputs,
24 onConfirm,
25 onCancel,
26 onRetry,
27 }) => {
28 const [generatedImage, setGeneratedImage] = useState(null);
29 const [generating, setGenerating] = useState(false);
30 const [error, setError] = useState(null);
31 const [refreshCheck, setRefreshCheck] = useState(0);
32 const [importing, setImporting] = useState(false);
33 const {
34 imageCredits,
35 updateImageCredits,
36 subtractOneCredit,
37 resetImageCredits,
38 } = useImageGenerationStore();
39 const { addMessage, messages } = useChatStore();
40 const { block } = useWorkflowStore();
41 const noCredits = Number(imageCredits.remaining) === 0;
42 const generatingImageRef = useRef(false);
43 const confirmed = useRef(false);
44
45 const handleConfirm = async () => {
46 if (importing) return;
47 confirmed.current = true;
48 setImporting(true);
49 try {
50 const importedImage = await downloadImage(
51 null,
52 generatedImage,
53 'ai-generated',
54 );
55 await onConfirm({
56 data: {
57 previousContent: inputs.previousContent,
58 newContent: walkAndUpdateImageDetails(inputs, importedImage),
59 },
60 shouldRefreshPage: true,
61 });
62 } catch (err) {
63 confirmed.current = false;
64 setImporting(false);
65 setError(err?.message || __('Failed to save image', 'extendify-local'));
66 }
67 };
68
69 const resetImagePreview = useCallback(() => {
70 if (!generatedImage) return false;
71 // The CSS.escape() method can also be used for escaping strings
72 // https://developer.mozilla.org/en-US/docs/Web/API/CSS/escape_static
73 const imageElement = document.querySelector(
74 `[data-extendify-agent-block-id="${block?.id}"] img[src="${CSS.escape(
75 generatedImage,
76 )}"]`,
77 );
78 if (imageElement) {
79 imageElement.src = inputs.url.replaceAll(',', '%2C');
80 return true;
81 }
82 return false;
83 }, [block?.id, generatedImage, inputs.url]);
84
85 useEffect(() => {
86 if (!generatedImage) return;
87 return () => {
88 if (!confirmed.current) resetImagePreview();
89 };
90 }, [generatedImage]);
91
92 const handleRetry = useCallback(() => {
93 resetImagePreview();
94 onRetry();
95 }, [onRetry, resetImagePreview]);
96
97 useEffect(() => {
98 if (getBlockTypes().length !== 0) return;
99 registerCoreBlocks();
100 }, []);
101
102 useEffect(() => {
103 if (generatedImage || noCredits || generatingImageRef.current) return;
104
105 generatingImageRef.current = true;
106 setGenerating(true);
107 subtractOneCredit();
108 const generate = async () => {
109 try {
110 const { imageCredits, images } = await generateImage({
111 prompt: inputs.prompt,
112 });
113 updateImageCredits(imageCredits);
114 const url = images?.[0]?.url;
115 if (!url) throw new Error(__('No image returned', 'extendify-local'));
116 await preload(url);
117 setGeneratedImage(url);
118 } catch (e) {
119 setError(
120 e?.message || __('An unknown error occurred.', 'extendify-local'),
121 );
122 if (e?.imageCredits) updateImageCredits(e.imageCredits);
123 generatingImageRef.current = false;
124 } finally {
125 setGenerating(false);
126 }
127 };
128
129 generate();
130 }, [
131 inputs.prompt,
132 generatedImage,
133 noCredits,
134 subtractOneCredit,
135 updateImageCredits,
136 ]);
137
138 // Copied from Draft. Maybe not the best way to do this.
139 useEffect(() => {
140 const handle = () => {
141 setRefreshCheck((prev) => prev + 1);
142 if (!imageCredits.refresh) return;
143 if (new Date(Number(imageCredits.refresh)) > new Date()) return;
144 resetImageCredits();
145 };
146 if (refreshCheck === 0) handle(); // First run
147 const id = setTimeout(handle, 1000);
148 return () => clearTimeout(id);
149 }, [imageCredits, resetImageCredits, refreshCheck]);
150
151 useEffect(() => {
152 if (!generatedImage) return;
153 const originalImage = document.querySelector(
154 `[data-extendify-agent-block-id="${block?.id}"] img[src="${CSS.escape(
155 inputs.url.replaceAll(',', '%2C'),
156 )}"]`,
157 );
158 if (!originalImage) return;
159 originalImage.srcset = '';
160 // replace the original image source with the new image url
161 originalImage.src = generatedImage;
162 }, [generatedImage, inputs.url, block?.id]);
163
164 useEffect(() => {
165 if (!error) return;
166 const timer = setTimeout(() => onCancel(), 100);
167 const content = sprintf(
168 // translators: A chat message shown to the user
169 __('Error generating image: %s', 'extendify-local'),
170 error,
171 );
172 const last = messages.at(-1)?.details?.content;
173 if (content === last) return () => clearTimeout(timer);
174 addMessage('message', { role: 'assistant', content, error: true });
175 return () => clearTimeout(timer);
176 }, [error, onCancel, addMessage, messages]);
177
178 useEffect(() => {
179 if (!noCredits || generating || generatedImage) return;
180 const cancelTimer = setTimeout(() => onCancel(), 1500);
181 const content = sprintf(
182 // translator: %s is the time until credits reset.
183 __(
184 "It looks like you've run out of credits. They'll refresh in %s, so check back then.",
185 'extendify-local',
186 ),
187 humanTimeDiff(new Date(Number(imageCredits.refresh))),
188 );
189 const last = messages.at(-1)?.details?.content;
190 if (content === last) return () => clearTimeout(cancelTimer);
191 const messageTimer = setTimeout(() => {
192 addMessage('message', { role: 'assistant', content, error: true });
193 }, 1000);
194 return () => {
195 clearTimeout(cancelTimer);
196 clearTimeout(messageTimer);
197 };
198 }, [
199 noCredits,
200 generating,
201 generatedImage,
202 onCancel,
203 addMessage,
204 messages,
205 imageCredits.refresh,
206 ]);
207
208 if (error || (!generatedImage && noCredits && !generating)) return null;
209
210 if (generating) {
211 return (
212 <Wrapper>
213 <Content>
214 <p className="m-0 p-0 flex gap-0.5 items-center text-sm text-gray-900">
215 <Spinner className="my-0 h-4 w-4" />
216 <span>{__('Generating image...', 'extendify-local')}</span>
217 </p>
218 </Content>
219 </Wrapper>
220 );
221 }
222
223 return (
224 <Wrapper>
225 <Content>
226 <p className="m-0 p-0 text-sm text-gray-900">
227 {__('Image generated successfully!', 'extendify-local')}
228 </p>
229 </Content>
230 <div className="flex justify-start gap-2 p-3">
231 <button
232 type="button"
233 className="flex-1 rounded-sm border border-gray-500 bg-white p-2 text-sm text-gray-900"
234 onClick={onCancel}
235 >
236 {__('Cancel', 'extendify-local')}
237 </button>
238 <button
239 type="button"
240 className="flex-1 rounded-sm border border-gray-500 bg-white p-2 text-sm text-gray-900 disabled:opacity-50"
241 onClick={handleRetry}
242 disabled={importing || noCredits}
243 >
244 {__('Try Again', 'extendify-local')}
245 </button>
246 <button
247 type="button"
248 className="flex-1 rounded border border-design-main bg-design-main p-2 text-sm text-white disabled:opacity-50"
249 disabled={importing}
250 onClick={handleConfirm}
251 >
252 {importing
253 ? __('Saving...', 'extendify-local')
254 : __('Save', 'extendify-local')}
255 </button>
256 </div>
257 <div className="text-pretty px-4 pb-2 text-center text-xss leading-none text-gray-700">
258 {sprintf(
259 // translators: %1$s is the number of credits remaining, %2$s is the total credits
260 __(
261 'You have %1$s of %2$s daily image credits remaining.',
262 'extendify-local',
263 ),
264 imageCredits.remaining,
265 imageCredits.total,
266 )}
267 </div>
268 </Wrapper>
269 );
270 };
271
272 const Wrapper = ({ children }) => (
273 <div className="mb-4 ml-10 mr-2 flex flex-col rounded-lg border border-gray-300 bg-gray-50 rtl:ml-2 rtl:mr-10">
274 {children}
275 </div>
276 );
277
278 const Content = ({ children }) => (
279 <div className="rounded-lg border-b border-gray-300 bg-white">
280 <div className="p-3">{children}</div>
281 </div>
282 );
283