PluginProbe
Extendify / 3.1.1
Extendify v3.1.1
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.1.1, at src/Agent/workflows/block-selector/components/GenerateImageConfirm.jsx

284 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 { useQuickEditStore } from '@quick-edit/state/store';
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 = useQuickEditStore((s) => s.agentBlock);
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 source: 'agent',
113 });
114 updateImageCredits(imageCredits);
115 const url = images?.[0]?.url;
116 if (!url) throw new Error(__('No image returned', 'extendify-local'));
117 await preload(url);
118 setGeneratedImage(url);
119 } catch (e) {
120 setError(
121 e?.message || __('An unknown error occurred.', 'extendify-local'),
122 );
123 if (e?.imageCredits) updateImageCredits(e.imageCredits);
124 generatingImageRef.current = false;
125 } finally {
126 setGenerating(false);
127 }
128 };
129
130 generate();
131 }, [
132 inputs.prompt,
133 generatedImage,
134 noCredits,
135 subtractOneCredit,
136 updateImageCredits,
137 ]);
138
139 // Copied from Draft. Maybe not the best way to do this.
140 useEffect(() => {
141 const handle = () => {
142 setRefreshCheck((prev) => prev + 1);
143 if (!imageCredits.refresh) return;
144 if (new Date(Number(imageCredits.refresh)) > new Date()) return;
145 resetImageCredits();
146 };
147 if (refreshCheck === 0) handle(); // First run
148 const id = setTimeout(handle, 1000);
149 return () => clearTimeout(id);
150 }, [imageCredits, resetImageCredits, refreshCheck]);
151
152 useEffect(() => {
153 if (!generatedImage) return;
154 const originalImage = document.querySelector(
155 `[data-extendify-agent-block-id="${block?.id}"] img[src="${CSS.escape(
156 inputs.url.replaceAll(',', '%2C'),
157 )}"]`,
158 );
159 if (!originalImage) return;
160 originalImage.srcset = '';
161 // replace the original image source with the new image url
162 originalImage.src = generatedImage;
163 }, [generatedImage, inputs.url, block?.id]);
164
165 useEffect(() => {
166 if (!error) return;
167 const timer = setTimeout(() => onCancel(), 100);
168 const content = sprintf(
169 // translators: A chat message shown to the user
170 __('Error generating image: %s', 'extendify-local'),
171 error,
172 );
173 const last = messages.at(-1)?.details?.content;
174 if (content === last) return () => clearTimeout(timer);
175 addMessage('message', { role: 'assistant', content, error: true });
176 return () => clearTimeout(timer);
177 }, [error, onCancel, addMessage, messages]);
178
179 useEffect(() => {
180 if (!noCredits || generating || generatedImage) return;
181 const cancelTimer = setTimeout(() => onCancel(), 1500);
182 const content = sprintf(
183 // translator: %s is the time until credits reset.
184 __(
185 "It looks like you've run out of credits. They'll refresh in %s, so check back then.",
186 'extendify-local',
187 ),
188 humanTimeDiff(new Date(Number(imageCredits.refresh))),
189 );
190 const last = messages.at(-1)?.details?.content;
191 if (content === last) return () => clearTimeout(cancelTimer);
192 const messageTimer = setTimeout(() => {
193 addMessage('message', { role: 'assistant', content, error: true });
194 }, 1000);
195 return () => {
196 clearTimeout(cancelTimer);
197 clearTimeout(messageTimer);
198 };
199 }, [
200 noCredits,
201 generating,
202 generatedImage,
203 onCancel,
204 addMessage,
205 messages,
206 imageCredits.refresh,
207 ]);
208
209 if (error || (!generatedImage && noCredits && !generating)) return null;
210
211 if (generating) {
212 return (
213 <Wrapper>
214 <Content>
215 <p className="m-0 p-0 flex gap-0.5 items-center text-sm text-gray-900">
216 <Spinner className="my-0 h-4 w-4" />
217 <span>{__('Generating image...', 'extendify-local')}</span>
218 </p>
219 </Content>
220 </Wrapper>
221 );
222 }
223
224 return (
225 <Wrapper>
226 <Content>
227 <p className="m-0 p-0 text-sm text-gray-900">
228 {__('Image generated successfully!', 'extendify-local')}
229 </p>
230 </Content>
231 <div className="flex justify-start gap-2 p-3">
232 <button
233 type="button"
234 className="flex-1 rounded-sm border border-gray-500 bg-white p-2 text-sm text-gray-900"
235 onClick={onCancel}
236 >
237 {__('Cancel', 'extendify-local')}
238 </button>
239 <button
240 type="button"
241 className="flex-1 rounded-sm border border-gray-500 bg-white p-2 text-sm text-gray-900 disabled:opacity-50"
242 onClick={handleRetry}
243 disabled={importing || noCredits}
244 >
245 {__('Try Again', 'extendify-local')}
246 </button>
247 <button
248 type="button"
249 className="flex-1 rounded border border-design-main bg-design-main p-2 text-sm text-white disabled:opacity-50"
250 disabled={importing}
251 onClick={handleConfirm}
252 >
253 {importing
254 ? __('Saving...', 'extendify-local')
255 : __('Save', 'extendify-local')}
256 </button>
257 </div>
258 <div className="text-pretty px-4 pb-2 text-center text-xss leading-none text-gray-700">
259 {sprintf(
260 // translators: %1$s is the number of credits remaining, %2$s is the total credits
261 __(
262 'You have %1$s of %2$s daily image credits remaining.',
263 'extendify-local',
264 ),
265 imageCredits.remaining,
266 imageCredits.total,
267 )}
268 </div>
269 </Wrapper>
270 );
271 };
272
273 const Wrapper = ({ children }) => (
274 <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">
275 {children}
276 </div>
277 );
278
279 const Content = ({ children }) => (
280 <div className="rounded-lg border-b border-gray-300 bg-white">
281 <div className="p-3">{children}</div>
282 </div>
283 );
284