PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / 2.2.1
AI Builder – Generate pages, blocks, images & translate with AI v2.2.1
2.8.0 2.7.10 2.7.9 2.7.8 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 All 123 releases
ai-builder / assets / js / src / editor-blocks / ai-block / ai-block.js

ai-block.js in AI Builder – Generate pages, blocks, images & translate with AI 2.2.1, at assets/js/src/editor-blocks/ai-block/ai-block.js

409 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { registerBlockType } from "@wordpress/blocks";
2 import {
3 useBlockProps,
4 InspectorControls,
5 useInnerBlocksProps,
6 store as blockEditorStore,
7 } from "@wordpress/block-editor";
8 import {
9 PanelBody,
10 TextareaControl,
11 Button,
12 Spinner,
13 Placeholder,
14 } from "@wordpress/components";
15 import { useState } from "@wordpress/element";
16 import { __ } from "@wordpress/i18n";
17 import { useDispatch } from "@wordpress/data";
18 import "./ai-block.css";
19
20 registerBlockType("ai-builder/ai-block", {
21 title: __("AI Block", "ai-builder"),
22 description: __(
23 "Generate and insert Gutenberg blocks using AI",
24 "ai-builder"
25 ),
26 icon: "admin-generic",
27 category: "common",
28 keywords: [
29 __("ai", "ai-builder"),
30 __("block", "ai-builder"),
31 __("generate", "ai-builder"),
32 ],
33 supports: {
34 align: true,
35 alignWide: true,
36 html: false,
37 spacing: {
38 margin: true,
39 padding: true,
40 },
41 },
42 attributes: {
43 prompt: {
44 type: "string",
45 default: "",
46 },
47 generatedContent: {
48 type: "string",
49 default: "",
50 },
51 isGenerated: {
52 type: "boolean",
53 default: false,
54 },
55 },
56
57 edit: ({ attributes, setAttributes, clientId }) => {
58 const blockProps = useBlockProps();
59 const { prompt, isGenerated } = attributes;
60 const [loading, setLoading] = useState(false);
61 const [error, setError] = useState("");
62 const [localPrompt, setLocalPrompt] = useState(prompt);
63
64 const { replaceBlock, insertBlock } = useDispatch(blockEditorStore);
65
66 // Fonction utilitaire pour afficher un toast UX si l'utilisateur n'a pas de compte/connexion
67 function showAIMissingAccountToast() {
68 if (document.getElementById("ai-missing-account-toast")) return; // Pas de doublon
69 const toast = document.createElement("div");
70 toast.id = "ai-missing-account-toast";
71 toast.innerHTML = `
72 <div style="display:flex;align-items:center;justify-content:space-between;gap:16px;max-width:420px;width:90vw;background:#23272f;color:#fff;padding:18px 24px;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,0.18);font-size:16px;position:fixed;left:50%;bottom:32px;transform:translateX(-50%);z-index:99999;flex-wrap:wrap;">
73 <span style='flex:1 1 200px;min-width:180px;'>You need to have an account and be logged in to use AI features.</span>
74 <a href='/wp-admin/admin.php?page=aibui-assistant' style='background:#00b87c;color:#fff;padding:8px 18px;border-radius:8px;text-decoration:none;font-weight:600;transition:background 0.2s;' target='_blank'>Go to Account</a>
75 </div>
76 `;
77 document.body.appendChild(toast);
78 setTimeout(() => {
79 if (toast.parentNode) toast.parentNode.removeChild(toast);
80 }, 8000);
81 }
82
83 function showNotEnoughCreditsToast() {
84 if (document.getElementById("ai-missing-account-toast")) return; // Pas de doublon
85 const toast = document.createElement("div");
86 toast.id = "ai-missing-account-toast";
87 toast.innerHTML = `
88 <div style="display:flex;align-items:center;justify-content:space-between;gap:16px;max-width:420px;width:90vw;background:#23272f;color:#fff;padding:18px 24px;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,0.18);font-size:16px;position:fixed;left:50%;bottom:32px;transform:translateX(-50%);z-index:99999;flex-wrap:wrap;">
89 <span style='flex:1 1 200px;min-width:180px;'>You don't have enough credits.</span>
90 <a href='/wp-admin/admin.php?page=aibui-credits' style='background:#00b87c;color:#fff;padding:8px 18px;border-radius:8px;text-decoration:none;font-weight:600;transition:background 0.2s;' target='_blank'>Go to credits page</a>
91 </div>
92 `;
93 document.body.appendChild(toast);
94 setTimeout(() => {
95 if (toast.parentNode) toast.parentNode.removeChild(toast);
96 }, 8000);
97 }
98
99 // Utilitaire pour récupérer le token JWT via AJAX WordPress
100 async function getJwtToken() {
101 if (!window.ajaxurl || !window.aiBuilderNonce) {
102 showAIMissingAccountToast();
103 throw new Error("Missing AJAX URL or nonce");
104 }
105 const res = await fetch(window.ajaxurl, {
106 method: "POST",
107 headers: { "Content-Type": "application/x-www-form-urlencoded" },
108 body: `action=aibui_get_token&nonce=${window.aiBuilderNonce}`,
109 });
110 const data = await res.json();
111 if (data.success && data.data.token) {
112 return data.data.token;
113 }
114 showAIMissingAccountToast();
115 throw new Error(
116 "You need to have an account and be logged in to use AI features."
117 );
118 }
119
120 // Helpers to handle CSS like sendMessageAIV2
121 function injectCSSInEditor(cssContent) {
122 const styleId = "ai-builder-editor-css";
123 let styleElement = document.getElementById(styleId);
124 if (!styleElement) {
125 styleElement = document.createElement("style");
126 styleElement.id = styleId;
127 styleElement.type = "text/css";
128 document.head.appendChild(styleElement);
129 }
130 styleElement.textContent = cssContent;
131 }
132 function injectCSSInFrontend(cssContent) {
133 const styleId = "ai-builder-frontend-css";
134 let styleElement = document.getElementById(styleId);
135 if (!styleElement) {
136 styleElement = document.createElement("style");
137 styleElement.id = styleId;
138 styleElement.type = "text/css";
139 document.head.appendChild(styleElement);
140 }
141 styleElement.textContent = cssContent;
142 }
143 async function saveCSSInPostMeta(cssContent, cssType = "block") {
144 try {
145 const postId = wp.data.select("core/editor").getCurrentPostId();
146 const formData = new FormData();
147 formData.append("action", "aibui_save_post_css");
148 formData.append(
149 "nonce",
150 window.aiBuilderNonce ||
151 (typeof aiBuilderVars !== "undefined" ? aiBuilderVars.nonce : "")
152 );
153 formData.append("post_id", postId);
154 formData.append("css_content", cssContent);
155 formData.append("css_type", cssType);
156 await fetch(
157 window.ajaxurl ||
158 (typeof aiBuilderVars !== "undefined" ? aiBuilderVars.ajaxurl : ""),
159 { method: "POST", body: formData }
160 );
161 } catch (e) {
162 /* silent */
163 }
164 }
165
166 async function reloadCombinedCSS() {
167 try {
168 const postId = wp.data.select("core/editor").getCurrentPostId();
169 const formData = new FormData();
170 formData.append("action", "aibui_get_post_css");
171 formData.append(
172 "nonce",
173 window.aiBuilderNonce ||
174 (typeof aiBuilderVars !== "undefined" ? aiBuilderVars.nonce : "")
175 );
176 formData.append("post_id", postId);
177
178 const res = await fetch(
179 window.ajaxurl ||
180 (typeof aiBuilderVars !== "undefined" ? aiBuilderVars.ajaxurl : ""),
181 { method: "POST", body: formData }
182 );
183
184 if (res.ok) {
185 const data = await res.json();
186 if (data.success && data.data) {
187 // Mettre à jour les variables globales
188 window.aiBuilderPageCSS = data.data.pageCss || "";
189 window.aiBuilderBlockCSS = data.data.blockCss || "";
190
191 // Injecter le CSS combiné
192 const combinedCSS = data.data.combinedCss || "";
193 injectCSSInEditor(combinedCSS);
194 injectCSSInFrontend(combinedCSS);
195 }
196 }
197 } catch (e) {
198 console.error("Error reloading combined CSS:", e);
199 }
200 }
201
202 const generateBlock = async () => {
203 if (!localPrompt.trim()) {
204 setError("Please enter a prompt");
205 return;
206 }
207
208 try {
209 setLoading(true);
210 setError("");
211
212 // Récupérer le token JWT
213 const jwtToken = await getJwtToken();
214
215 console.log("jwtToken: ", jwtToken);
216
217 // Requête vers l'API IA V2 pour générer un bloc
218 const res = await fetch(
219 window.config.apiUrl + "/ai-transform-page/v2-generate-single-block",
220 {
221 method: "POST",
222 headers: {
223 "Content-Type": "application/json",
224 Authorization: `Bearer ${jwtToken}`,
225 },
226 body: JSON.stringify({ prompt: localPrompt }),
227 }
228 );
229
230 const data = await res.json();
231 if (data.error === "not-enough-credits") {
232 showNotEnoughCreditsToast();
233 throw new Error("Not enough credits.");
234 }
235 if (!data.blockContent) {
236 throw new Error("Error during block generation");
237 }
238
239 // Handle cssContent like sendMessageAIV2
240 if (data.cssContent) {
241 // Sauvegarder d'abord le CSS du bloc
242 await saveCSSInPostMeta(data.cssContent, "block");
243
244 // Puis recharger le CSS combiné depuis le serveur
245 await reloadCombinedCSS();
246 }
247
248 // Mettre à jour les crédits dans le chat-widget si possible
249 if (
250 typeof data.creditsLeft !== "undefined" &&
251 window.updateAICreditsDisplay
252 ) {
253 window.updateAICreditsDisplay(data.creditsLeft);
254 }
255
256 // Sauvegarder le prompt utilisé
257 setAttributes({
258 prompt: localPrompt,
259 generatedContent: JSON.stringify(data.blockContent),
260 isGenerated: true,
261 });
262
263 // Créer le bloc à partir du JSON avec la structure correcte
264 try {
265 console.log("blockData: ", data);
266 const blockData = data.blockContent;
267
268 // Fonction récursive pour créer les blocs (comme dans chat-widget.js)
269 function buildBlock(block) {
270 const { blockName, attrs = {}, innerBlocks = [] } = block;
271 return wp.blocks.createBlock(
272 blockName,
273 attrs,
274 innerBlocks.map(buildBlock) // récursivité ici
275 );
276 }
277
278 // Créer le bloc principal
279 const block = buildBlock(blockData);
280
281 // Remplacer le bloc AI Block par le bloc généré
282 replaceBlock(clientId, block);
283 } catch (blockError) {
284 console.error("Error creating block from JSON:", blockError);
285 setError("Error creating block from generated JSON");
286 }
287 } catch (err) {
288 setError(err.message);
289 console.error("AI Block generation error:", err);
290 } finally {
291 setLoading(false);
292 }
293 };
294
295 // Si le bloc a déjà été généré, afficher un placeholder
296 if (isGenerated) {
297 return (
298 <div {...blockProps}>
299 <Placeholder
300 icon="admin-generic"
301 label={__("AI Block - Generated", "ai-builder")}
302 instructions={__(
303 "This AI Block has been replaced with generated content.",
304 "ai-builder"
305 )}
306 >
307 <Button
308 onClick={() =>
309 setAttributes({
310 isGenerated: false,
311 prompt: "",
312 generatedContent: "",
313 })
314 }
315 variant="secondary"
316 >
317 {__("Generate New Block", "ai-builder")}
318 </Button>
319 </Placeholder>
320 </div>
321 );
322 }
323
324 return (
325 <div {...blockProps}>
326 <InspectorControls>
327 <PanelBody
328 title={__("AI Block Generation", "ai-builder")}
329 initialOpen={true}
330 className="ai-block-controls"
331 >
332 <TextareaControl
333 label={__(
334 "Describe the block you want to generate",
335 "ai-builder"
336 )}
337 value={localPrompt}
338 onChange={setLocalPrompt}
339 placeholder="e.g. Create a testimonials section with 3 customer reviews, each with a star rating and quote"
340 rows={4}
341 help={__(
342 "Be specific about the type of block, content, and layout you want.",
343 "ai-builder"
344 )}
345 />
346
347 <Button
348 onClick={generateBlock}
349 variant="primary"
350 isBusy={loading}
351 disabled={loading || !localPrompt.trim()}
352 style={{ width: "100%", marginTop: "10px" }}
353 >
354 {loading ? (
355 <>
356 <Spinner />
357 {__("Generating Block...", "ai-builder")}
358 </>
359 ) : (
360 __("Generate AI Block", "ai-builder")
361 )}
362 </Button>
363
364 {error && (
365 <div
366 style={{
367 color: "#dc3545",
368 marginTop: "10px",
369 fontSize: "12px",
370 }}
371 >
372 {error}
373 </div>
374 )}
375 </PanelBody>
376 </InspectorControls>
377
378 <Placeholder
379 icon="admin-generic"
380 label={__("AI Block", "ai-builder")}
381 instructions={__(
382 "Use the sidebar to generate a Gutenberg block with AI.",
383 "ai-builder"
384 )}
385 className="ai-block-placeholder"
386 >
387 <div style={{ textAlign: "center", padding: "20px" }}>
388 <p style={{ marginBottom: "15px", color: "#666" }}>
389 {__(
390 "This block will be replaced with AI-generated content.",
391 "ai-builder"
392 )}
393 </p>
394 <p style={{ fontSize: "12px", color: "#999" }}>
395 {__(
396 "Examples: testimonials, pricing tables, feature lists, etc.",
397 "ai-builder"
398 )}
399 </p>
400 </div>
401 </Placeholder>
402 </div>
403 );
404 },
405
406 // Pas de fonction save - le bloc sera remplacé par le contenu généré
407 save: () => null,
408 });
409