PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / 2.1.2
AI Builder – Generate pages, blocks, images & translate with AI v2.1.2
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 2.3.10 All 122 releases
ai-builder / assets / js / chat-widget.js

chat-widget.js in AI Builder – Generate pages, blocks, images & translate with AI 2.1.2, at assets/js/chat-widget.js

1,207 lines 37.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // Charger la configuration globale de l'API
2 const script = document.createElement("script");
3 // script.src = "/wp-content/plugins/ai-builder/config.js";
4 document.head.appendChild(script);
5
6 // Initialiser Sentry pour le widget si disponible
7 (function initSentry() {
8 const SENTRY_SRC = "https://js-de.sentry-cdn.com/a9e1c731a49a9ab9736399318271e322.min.js";
9 if (window.__aiBuilderSentry) {
10 return;
11 }
12
13 try {
14 const sentryScript = document.createElement("script");
15 sentryScript.src = SENTRY_SRC;
16 sentryScript.crossOrigin = "anonymous";
17 sentryScript.onload = () => {
18 window.__aiBuilderSentry = true;
19 console.info("AI Builder: Sentry loaded");
20 };
21 sentryScript.onerror = () => {
22 console.warn("AI Builder: Impossible de charger Sentry");
23 };
24 document.head.appendChild(sentryScript);
25
26
27 } catch (error) {
28 console.warn("AI Builder: Erreur lors de l'initialisation de Sentry", error);
29 }
30 })();
31
32
33 // Récupération des variables injectées par wp_localize_script (admin)
34 if (typeof window.aiBuilderVars !== "undefined") {
35 // Forcer l'utilisation d'une URL relative pour éviter les erreurs CORS
36 window.ajaxurl = '/wp-admin/admin-ajax.php';
37 window.aiBuilderNonce = window.aiBuilderVars.nonce;
38 } else {
39 // Fallback pour éviter les erreurs CORS
40 window.ajaxurl = '/wp-admin/admin-ajax.php';
41 window.aiBuilderNonce = '';
42 }
43
44 document.addEventListener("DOMContentLoaded", () => {
45 // Charger le CSS des témoignages
46 const style = document.createElement("link");
47 style.rel = "stylesheet";
48 style.href = "/wp-content/plugins/ai-builder/assets/css/cards.css";
49 document.head.appendChild(style);
50
51 document.body.insertAdjacentHTML(
52 "beforeend",
53 `
54 <div id="chat-toggle">🤖 AI Builder</div>
55 <div id="chat-box">
56 <div id="chat-header">
57 <div id="chat-header-left">
58 <h3>AI Page Builder</h3>
59 <p class="warning-text">⚠️ The current page content will be replaced by AI-generated content</p>
60 </div>
61 <button id="css-edit-button" style="display: none;" title="Edit CSS">
62 <img src="/wp-content/plugins/ai-builder/assets/images/css-edit-icon.png" alt="Edit CSS" width="16" height="16">
63 </button>
64 </div>
65 <div id="chat-messages"></div>
66 <div id="chat-input">
67 <textarea placeholder="Create a modern pricing page with 3 plans and call-to-action buttons..." rows="2"></textarea>
68 <button>Generate</button>
69 <button id="chat-undo" style="display: none;">Undo</button>
70 </div>
71 </div>
72 <!-- Modale CSS -->
73 <div id="css-modal" style="display: none;">
74 <div id="css-modal-content">
75 <div id="css-modal-header">
76 <h3>Edit CSS</h3>
77 <button id="css-modal-close">&times;</button>
78 </div>
79 <div id="css-modal-tabs">
80 <button id="css-tab-page" class="css-tab active">Page CSS</button>
81 <button id="css-tab-blocks" class="css-tab">Blocks CSS</button>
82 </div>
83 <div id="css-modal-body">
84 <textarea id="css-editor" placeholder="Enter your custom CSS here..."></textarea>
85 </div>
86 <div id="css-modal-footer">
87 <button id="css-save">Save CSS</button>
88 <button id="css-cancel">Cancel</button>
89 </div>
90 </div>
91 </div>
92
93 `
94 );
95
96 const toggle = document.getElementById("chat-toggle");
97 const box = document.getElementById("chat-box");
98 const messages = document.getElementById("chat-messages");
99 const input = document.querySelector("#chat-input textarea");
100 const sendBtn = document.querySelector("#chat-input button");
101 const undoBtn = document.getElementById("chat-undo");
102 let chatHistory = [];
103
104 function getChatStorageKey() {
105 const prefix = "aiBuilderChatHistory";
106 const postId =
107 wp?.data?.select("core/editor")?.getCurrentPostId?.() ||
108 new URL(window.location.href, window.location.origin).searchParams.get("post") ||
109 new URL(window.location.href, window.location.origin).searchParams.get("postId") ||
110 new URL(window.location.href, window.location.origin).searchParams.get("p");
111 if (postId) {
112 return `${prefix}_post_${postId}`;
113 }
114 const patternName = getPatternName();
115 if (patternName) {
116 return `${prefix}_pattern_${patternName}`;
117 }
118 return `${prefix}_${window.location.pathname}`;
119 }
120
121 // Fonction pour migrer l'historique de post-new.php vers l'ID du post
122 function migrateChatHistoryFromNewPost() {
123 if (!window.localStorage) return;
124
125 const prefix = "aiBuilderChatHistory";
126 const newPostKey = `${prefix}_/wp-admin/post-new.php`;
127 const currentKey = getChatStorageKey();
128
129 // Si on est déjà sur la clé post-new.php, pas besoin de migrer
130 if (currentKey === newPostKey) return;
131
132 // Si la clé actuelle est basée sur un ID de post, vérifier s'il y a un historique à migrer
133 if (currentKey.startsWith(`${prefix}_post_`)) {
134 try {
135 const oldHistory = window.localStorage.getItem(newPostKey);
136 if (oldHistory) {
137 const parsedHistory = JSON.parse(oldHistory);
138 if (Array.isArray(parsedHistory) && parsedHistory.length > 0) {
139 // Fusionner avec l'historique existant (s'il y en a un)
140 const existingHistory = chatHistory.length > 0 ? chatHistory : [];
141 chatHistory = [...existingHistory, ...parsedHistory];
142
143 // Sauvegarder avec la nouvelle clé
144 window.localStorage.setItem(currentKey, JSON.stringify(chatHistory));
145
146 // Supprimer l'ancienne clé
147 window.localStorage.removeItem(newPostKey);
148
149 console.log("AI Builder: Chat history migrated from post-new.php to post ID");
150
151 // Re-rendre l'historique
152 renderChatHistory();
153 }
154 }
155 } catch (error) {
156 console.warn("AI Builder: Error migrating chat history", error);
157 }
158 }
159 }
160
161 // Éléments CSS
162 const cssEditButton = document.getElementById("css-edit-button");
163 const cssModal = document.getElementById("css-modal");
164 const cssEditor = document.getElementById("css-editor");
165 const cssSaveBtn = document.getElementById("css-save");
166 const cssCancelBtn = document.getElementById("css-cancel");
167 const cssModalClose = document.getElementById("css-modal-close");
168
169 // Éléments des onglets
170 const cssTabPage = document.getElementById("css-tab-page");
171 const cssTabBlocks = document.getElementById("css-tab-blocks");
172
173 toggle.onclick = () => {
174 const isOpening = box.style.display !== "flex";
175 box.style.display = box.style.display === "flex" ? "none" : "flex";
176 box.style.flexDirection = "column";
177
178 // Scroller vers le bas quand on ouvre le chat
179 if (isOpening && messages) {
180 // Petit délai pour s'assurer que le DOM est mis à jour
181 setTimeout(() => {
182 messages.scrollTop = messages.scrollHeight;
183 }, 100);
184 }
185 };
186
187 // Logique pour la modale CSS
188 let currentCSSContent = "";
189 let currentActiveTab = "page";
190
191 // Initialiser les variables CSS globales
192 window.aiBuilderPageCSS = window.aiBuilderPageCSS || "";
193 window.aiBuilderBlockCSS = window.aiBuilderBlockCSS || "";
194
195 function saveChatHistory() {
196 if (!window.localStorage) return;
197 try {
198 const key = getChatStorageKey();
199 window.localStorage.setItem(key, JSON.stringify(chatHistory));
200 } catch (error) {
201 console.warn("AI Builder: Unable to persist chat history", error);
202 }
203 }
204
205 function loadChatHistoryFromStorage() {
206 if (!window.localStorage) return;
207 try {
208 const key = getChatStorageKey();
209 console.log("Loading chat history from storage:", key);
210 const storedHistory = window.localStorage.getItem(key);
211 if (!storedHistory) return;
212 const parsedHistory = JSON.parse(storedHistory);
213 if (Array.isArray(parsedHistory)) {
214 chatHistory = parsedHistory;
215 }
216 } catch (error) {
217 console.warn("AI Builder: Unable to read chat history", error);
218 }
219 }
220
221 function renderChatHistory() {
222 if (!messages) return;
223 messages.innerHTML = "";
224 chatHistory.forEach(({ type, message }) => {
225 addMessage(message, type, { persist: false });
226 });
227 // Scroller vers le bas après avoir rendu l'historique
228 if (chatHistory.length > 0) {
229 setTimeout(() => {
230 messages.scrollTop = messages.scrollHeight;
231 }, 50);
232 }
233 }
234
235 function getConversationHistoryString(limit = 4) {
236 if (!chatHistory.length) return "";
237 const recentMessages = chatHistory.slice(-limit);
238 const segments = recentMessages.map(({ type, message }) => {
239 const label = type === "user" ? "User question" : "AI Response";
240 return `${label} : ${message}`;
241 });
242 return segments.join(". ") + (segments.length ? "." : "");
243 }
244
245 // Fonction pour ajouter des messages dans le chat
246 function addMessage(message, type = "assistant", options = {}) {
247 const { persist = true } = options;
248 if (!messages) return;
249
250 const messageDiv = document.createElement("div");
251 messageDiv.className = type === "assistant" ? "ai-message" : "user-message";
252
253 if (type === "assistant") {
254 messageDiv.innerHTML = `<strong>🤖</strong> ${message}`;
255 } else {
256 messageDiv.innerHTML = `<strong>👤</strong> ${message}`;
257 }
258
259 messages.appendChild(messageDiv);
260 messages.scrollTop = messages.scrollHeight;
261
262 if (persist) {
263 chatHistory.push({ type, message });
264 saveChatHistory();
265 }
266 }
267
268 loadChatHistoryFromStorage();
269 renderChatHistory();
270
271 // Migrer l'historique de post-new.php vers l'ID du post si nécessaire
272 migrateChatHistoryFromNewPost();
273
274 // Surveiller les changements d'ID de post (pour les drafts créés après le chargement)
275 let lastKnownPostId = null;
276 let lastKnownKey = getChatStorageKey();
277
278 function checkPostIdChange() {
279 try {
280 const currentPostId = wp?.data?.select("core/editor")?.getCurrentPostId?.();
281 const currentKey = getChatStorageKey();
282
283 if (currentPostId && currentPostId !== lastKnownPostId) {
284 // L'ID a changé, mettre à jour la clé de stockage
285 if (currentKey !== lastKnownKey && window.localStorage) {
286 // Migrer l'historique vers la nouvelle clé
287 const oldHistory = window.localStorage.getItem(lastKnownKey);
288 if (oldHistory) {
289 try {
290 const parsedHistory = JSON.parse(oldHistory);
291 if (Array.isArray(parsedHistory) && parsedHistory.length > 0) {
292 chatHistory = parsedHistory;
293 window.localStorage.setItem(currentKey, oldHistory);
294 // Ne pas supprimer l'ancienne clé immédiatement, au cas où
295 console.log("AI Builder: Chat history migrated to new post ID:", currentPostId);
296 renderChatHistory();
297 }
298 } catch (e) {
299 console.warn("AI Builder: Error migrating history on ID change", e);
300 }
301 }
302 }
303
304 lastKnownPostId = currentPostId;
305 lastKnownKey = currentKey;
306 } else if (currentKey !== lastKnownKey) {
307 // La clé a changé même si l'ID n'a pas changé (changement d'URL)
308 lastKnownKey = currentKey;
309 // Recharger l'historique avec la nouvelle clé
310 loadChatHistoryFromStorage();
311 renderChatHistory();
312 migrateChatHistoryFromNewPost();
313 }
314 } catch (e) {
315 // Ignorer les erreurs si wp.data n'est pas encore disponible
316 }
317 }
318
319 // Vérifier l'ID initial
320 setTimeout(() => {
321 try {
322 lastKnownPostId = wp?.data?.select("core/editor")?.getCurrentPostId?.();
323 } catch (e) { }
324 }, 1000);
325
326 // Surveiller les changements d'ID toutes les 2 secondes
327 setInterval(checkPostIdChange, 2000);
328
329 // Ouvrir la modale CSS
330 cssEditButton.onclick = () => {
331 cssModal.style.display = "flex";
332 // Charger le CSS de page par défaut
333 loadCSSForTab("page");
334 cssEditor.focus();
335 };
336
337 // Fermer la modale CSS
338 function closeCSSModal() {
339 cssModal.style.display = "none";
340 }
341
342 // Fonction pour charger le CSS selon l'onglet sélectionné
343 function loadCSSForTab(tabType) {
344 currentActiveTab = tabType;
345
346 // Mettre à jour les onglets actifs
347 document
348 .querySelectorAll(".css-tab")
349 .forEach((tab) => tab.classList.remove("active"));
350 document.getElementById(`css-tab-${tabType}`).classList.add("active");
351
352 // Charger le bon CSS
353 switch (tabType) {
354 case "page":
355 cssEditor.value = window.aiBuilderPageCSS || "";
356 console.log(
357 "Loading page CSS:",
358 (window.aiBuilderPageCSS || "").length,
359 "chars"
360 );
361 break;
362 case "blocks":
363 cssEditor.value = window.aiBuilderBlockCSS || "";
364 console.log(
365 "Loading blocks CSS:",
366 (window.aiBuilderBlockCSS || "").length,
367 "chars"
368 );
369 break;
370 }
371 }
372
373 // Gestion des onglets
374 cssTabPage.onclick = () => loadCSSForTab("page");
375 cssTabBlocks.onclick = () => loadCSSForTab("blocks");
376
377 cssModalClose.onclick = closeCSSModal;
378 cssCancelBtn.onclick = closeCSSModal;
379
380 // Sauvegarder le CSS
381 cssSaveBtn.onclick = async () => {
382 const newCSSContent = cssEditor.value;
383
384 // Déterminer le type de CSS à sauvegarder selon l'onglet actif
385 let cssType = "page";
386 if (currentActiveTab === "page") {
387 cssType = "page";
388 window.aiBuilderPageCSS = newCSSContent;
389 } else if (currentActiveTab === "blocks") {
390 cssType = "block";
391 window.aiBuilderBlockCSS = newCSSContent;
392 }
393
394 // Sauvegarder dans les meta du post
395 await saveCSSInPostMeta(newCSSContent, cssType);
396
397 // Recharger le CSS combiné depuis le serveur après sauvegarde
398 await loadCSSFromPostMeta();
399
400 closeCSSModal();
401
402 // Afficher un message de confirmation
403 addMessage("CSS saved successfully!", "assistant");
404 };
405
406 // Fermer la modale en cliquant à l'extérieur
407 cssModal.onclick = (e) => {
408 if (e.target === cssModal) {
409 closeCSSModal();
410 }
411 };
412
413
414
415 function buildBlock(block) {
416 const { blockName, attrs = {}, innerBlocks = [] } = block;
417 return wp.blocks.createBlock(
418 blockName,
419 attrs,
420 innerBlocks.map(buildBlock) // récursivité ici
421 );
422 }
423
424 // Utilitaire pour récupérer le token JWT via AJAX WordPress
425 async function getJwtToken() {
426 if (!window.ajaxurl || !window.aiBuilderNonce) {
427 console.warn("AI Builder: Missing AJAX URL or nonce");
428 showAIMissingAccountToast();
429 throw new Error("Missing AJAX URL or nonce");
430 }
431
432 try {
433 const res = await fetch(window.ajaxurl, {
434 method: "POST",
435 headers: { "Content-Type": "application/x-www-form-urlencoded" },
436 body: `action=aibui_get_token&nonce=${window.aiBuilderNonce}`,
437 });
438
439 if (!res.ok) {
440 throw new Error(`HTTP error! status: ${res.status}`);
441 }
442
443 const data = await res.json();
444 console.log("token res: ", data);
445 if (data.success && data.data.token) {
446 return data.data.token;
447 }
448 showAIMissingAccountToast();
449 throw new Error(
450 "You need to have an account and be logged in to use AI features."
451 );
452 } catch (error) {
453 console.error("Error fetching JWT token:", error);
454 showAIMissingAccountToast();
455 throw error;
456 }
457 }
458
459 // Fonction utilitaire pour afficher un toast UX si l'utilisateur n'a pas de compte/connexion
460 function showAIMissingAccountToast() {
461 if (document.getElementById("ai-missing-account-toast")) return; // Pas de doublon
462 const toast = document.createElement("div");
463 toast.id = "ai-missing-account-toast";
464 toast.innerHTML = `
465 <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;">
466 <span style='flex:1 1 200px;min-width:180px;'>You need to have an account and be logged in to use AI features.</span>
467 <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>
468 </div>
469 `;
470 document.body.appendChild(toast);
471 setTimeout(() => {
472 if (toast.parentNode) toast.parentNode.removeChild(toast);
473 }, 8000);
474 }
475
476 function showNotEnoughCreditsToast() {
477 if (document.getElementById("ai-missing-account-toast")) return; // Pas de doublon
478 const toast = document.createElement("div");
479 toast.id = "ai-missing-account-toast";
480 toast.innerHTML = `
481 <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;">
482 <span style='flex:1 1 200px;min-width:180px;'>You don't have enough credits.</span>
483 <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>
484 </div>
485 `;
486 document.body.appendChild(toast);
487 setTimeout(() => {
488 if (toast.parentNode) toast.parentNode.removeChild(toast);
489 }, 8000);
490 }
491
492 // Fonction pour afficher le nombre de crédits sous le titre ET sous le bouton
493 function updateCreditsDisplay(credits) {
494 // Sous le titre du chat
495 let creditsElem = document.getElementById("ai-credits-display");
496 if (!creditsElem) {
497 const header = document.querySelector("#chat-header-left");
498 if (header) {
499 creditsElem = document.createElement("div");
500 creditsElem.id = "ai-credits-display";
501 creditsElem.style.fontSize = "12px";
502 creditsElem.style.color = "#cccccc";
503 creditsElem.style.marginTop = "2px";
504 creditsElem.style.textAlign = "center";
505 header.appendChild(creditsElem);
506 }
507 }
508 if (creditsElem) {
509 creditsElem.textContent =
510 credits !== null ? `${credits} credits left` : "- credits left";
511 }
512
513 // À l'intérieur du bouton toggle
514 const toggle = document.getElementById("chat-toggle");
515 if (toggle) {
516 let span = toggle.querySelector(".ai-credits-inside");
517 if (!span) {
518 span = document.createElement("span");
519 span.className = "ai-credits-inside";
520 span.style.display = "block";
521 span.style.fontSize = "10px";
522 span.style.color = "#cccccc";
523 span.style.marginTop = "0px";
524 span.style.textAlign = "center";
525 toggle.appendChild(span);
526 }
527 span.textContent = credits !== null ? `${credits} credits` : "- credits";
528 }
529 }
530
531 window.updateAICreditsDisplay = updateCreditsDisplay;
532
533 // Fonction pour charger les crédits utilisateur
534 async function loadUserCredits() {
535 try {
536 const jwtToken = await getJwtToken();
537 const res = await fetch(window.config.apiUrl + "/user/profile", {
538 method: "GET",
539 headers: {
540 Authorization: `Bearer ${jwtToken}`,
541 "Content-Type": "application/json",
542 },
543 });
544 if (!res.ok) throw new Error("Failed to load profile");
545 const data = await res.json();
546 const aiCredits = data.user?.aiCredits || {};
547 const totalCredits =
548 (aiCredits.onAccountCreation || 0) +
549 (aiCredits.monthlySubscription || 0) +
550 (aiCredits.paid || 0);
551 console.log("Credits loaded:", totalCredits, "from:", aiCredits);
552 updateCreditsDisplay(totalCredits);
553 } catch (e) {
554 console.error("Error loading credits:", e);
555 updateCreditsDisplay(null);
556 }
557 }
558
559 // Fonction pour injecter le CSS dans l'éditeur WordPress
560 function injectCSSInEditor(cssContent) {
561 // Créer un style tag pour l'éditeur
562 const styleId = "ai-builder-editor-css";
563 let styleElement = document.getElementById(styleId);
564
565 if (!styleElement) {
566 styleElement = document.createElement("style");
567 styleElement.id = styleId;
568 styleElement.type = "text/css";
569 document.head.appendChild(styleElement);
570 }
571
572 styleElement.textContent = cssContent;
573 }
574
575 // Fonction pour injecter le CSS dans le frontend
576 function injectCSSInFrontend(cssContent) {
577 // Créer un style tag pour le frontend
578 const styleId = "ai-builder-frontend-css";
579 let styleElement = document.getElementById(styleId);
580
581 if (!styleElement) {
582 styleElement = document.createElement("style");
583 styleElement.id = styleId;
584 styleElement.type = "text/css";
585 document.head.appendChild(styleElement);
586 }
587
588 styleElement.textContent = cssContent;
589 }
590
591 // Fonction pour sauvegarder le CSS dans les meta du post via AJAX WordPress
592 async function saveCSSInPostMeta(cssContent, cssType = "page") {
593 try {
594 const postId = wp.data.select("core/editor").getCurrentPostId();
595
596 const formData = new FormData();
597 formData.append("action", "aibui_save_post_css");
598 formData.append("nonce", window.aiBuilderNonce);
599 formData.append("post_id", postId);
600 formData.append("css_content", cssContent);
601 formData.append("css_type", cssType);
602
603 await fetch(window.ajaxurl, {
604 method: "POST",
605 body: formData,
606 });
607 } catch (err) {
608 console.error("Error saving CSS to post meta:", err);
609 }
610 }
611
612
613
614 // Set the meta description field by id once (no retry)
615 async function setMetaDescriptionField(value) {
616 const el = document.getElementById('aibui_meta_description_field');
617 if (!el) return false;
618 el.value = value || '';
619 const evt = new Event('input', { bubbles: true });
620 el.dispatchEvent(evt);
621 return true;
622 }
623
624 // Update post title locally in the editor
625 function updatePostTitle(title) {
626 if (!title) return false;
627 try {
628 // Update the title in WordPress editor state
629 wp.data.dispatch('core/editor').editPost({ title: title });
630 return true;
631 } catch (err) {
632 console.error('Error updating post title:', err);
633 return false;
634 }
635 }
636
637 // Mark page as created via AI
638 async function markPageAsAICreated() {
639 try {
640 const postId =
641 wp?.data?.select("core/editor")?.getCurrentPostId?.() ||
642 new URL(window.location.href, window.location.origin).searchParams.get("post") ||
643 new URL(window.location.href, window.location.origin).searchParams.get("postId") ||
644 new URL(window.location.href, window.location.origin).searchParams.get("p");
645
646 if (!postId) {
647 console.log("No post ID found to mark as AI-created");
648 return;
649 }
650
651 const formData = new FormData();
652 formData.append("action", "aibui_mark_ai_created");
653 formData.append("post_id", postId);
654 formData.append("nonce", aiBuilderVars.nonce);
655
656 const response = await fetch(ajaxurl, {
657 method: "POST",
658 body: formData,
659 });
660
661 const result = await response.json();
662 if (result.success) {
663 console.log("Page marked as AI-created");
664 // Injecter le CSS admin immédiatement après marquage
665 injectAICreatedAdminCSS();
666 } else {
667 console.error("Failed to mark page as AI-created:", result);
668 }
669 } catch (error) {
670 console.error("Error marking page as AI-created:", error);
671 }
672 }
673
674 // Injecter le CSS admin pour masquer le titre
675 function injectAICreatedAdminCSS() {
676 // Vérifier si le style existe déjà
677 if (document.getElementById("aibui-hide-ai-title-admin")) {
678 return;
679 }
680
681 const style = document.createElement("style");
682 style.id = "aibui-hide-ai-title-admin";
683 style.type = "text/css";
684 style.textContent = `
685 /* Masquer le titre dans l'éditeur Gutenberg */
686 .editor-post-title,
687 .editor-post-title__input,
688 .edit-post-visual-editor__post-title-wrapper,
689 .editor-post-title__block,
690 .wp-block[data-type="core/post-title"],
691 .block-editor-block-list__block[data-type="core/post-title"],
692 .wp-block-post-title.editor-post-title__block {
693 display: none !important;
694 visibility: hidden !important;
695 height: 0 !important;
696 margin: 0 !important;
697 padding: 0 !important;
698 overflow: hidden !important;
699 opacity: 0 !important;
700 }
701
702 .edit-post-visual-editor__post-title-wrapper {
703 display: none !important;
704 visibility: hidden !important;
705 height: 0 !important;
706 margin: 0 !important;
707 padding: 0 !important;
708 overflow: hidden !important;
709 }
710 `;
711 document.head.appendChild(style);
712 }
713
714 // Save meta description in post meta via WordPress AJAX
715 async function saveMetaDescriptionInPostMeta(metaDesc) {
716 try {
717 const postId = wp.data.select("core/editor").getCurrentPostId();
718 const formData = new FormData();
719 formData.append("action", "aibui_save_meta_description");
720 formData.append("nonce", window.aiBuilderNonce);
721 formData.append("post_id", postId);
722 formData.append("meta_desc", metaDesc || "");
723 await fetch(window.ajaxurl, { method: "POST", body: formData });
724 } catch (err) {
725 console.error("Error saving meta description:", err);
726 }
727 }
728
729 // Attendre que l'ID du post soit disponible (l'éditeur met un peu de temps à charger)
730 async function waitForCurrentPostId(maxAttempts = 50, intervalMs = 100) {
731 for (let attemptIndex = 0; attemptIndex < maxAttempts; attemptIndex++) {
732 try {
733 const postId = wp?.data?.select("core/editor")?.getCurrentPostId?.();
734 if (postId) return postId;
735 } catch (e) { }
736 await new Promise((resolve) => setTimeout(resolve, intervalMs));
737 }
738 return null;
739 }
740
741 // Fonction pour charger le CSS depuis les meta du post via AJAX WordPress
742 async function loadCSSFromPostMeta() {
743 try {
744 const postId = wp.data.select("core/editor").getCurrentPostId();
745
746 const formData = new FormData();
747 formData.append("action", "aibui_get_post_css");
748 formData.append("nonce", window.aiBuilderNonce);
749 formData.append("post_id", postId);
750
751 const res = await fetch(window.ajaxurl, {
752 method: "POST",
753 body: formData,
754 });
755
756 if (res.ok) {
757 const data = await res.json();
758 if (data.success && data.data) {
759 // Stocker les CSS séparément
760 window.aiBuilderPageCSS = data.data.pageCss || "";
761 window.aiBuilderBlockCSS = data.data.blockCss || "";
762
763 // Utiliser le CSS combiné pour l'affichage
764 const combinedCSS = data.data.combinedCss || "";
765 currentCSSContent = combinedCSS;
766
767 // Injecter le CSS dans l'éditeur et le frontend
768 injectCSSInEditor(combinedCSS);
769 injectCSSInFrontend(combinedCSS);
770
771 // Afficher le bouton CSS s'il y a du CSS
772 if (combinedCSS.trim()) {
773 cssEditButton.style.display = "block";
774 }
775
776 console.log(
777 "CSS loaded - Page:",
778 window.aiBuilderPageCSS.length,
779 "chars, Blocks:",
780 window.aiBuilderBlockCSS.length,
781 "chars"
782 );
783 }
784 }
785 } catch (err) {
786 console.error("Error loading CSS from post meta:", err);
787 }
788 }
789
790
791
792 // Initialiser le chargement du CSS une fois que l'ID du post est prêt
793 (async function initEditorCssLoad() {
794 // Vérifier que les variables AJAX sont disponibles
795 if (!window.ajaxurl || !window.aiBuilderNonce) {
796 console.warn("AI Builder: AJAX variables not available, skipping CSS load");
797 return;
798 }
799
800 // Attendre un peu pour que WordPress soit complètement chargé
801 await new Promise(resolve => setTimeout(resolve, 1000));
802
803 const postId = await waitForCurrentPostId();
804 if (postId) {
805 loadCSSFromPostMeta();
806 } else {
807 console.warn(
808 "AI Builder: unable to resolve current post ID to load CSS."
809 );
810 }
811 })();
812
813 function getPatternName() {
814 try {
815 const isPatternEditor = (typeof aiBuilderVars !== 'undefined' && !!aiBuilderVars.isPatternEditor) || window.location.pathname.includes('site-editor.php');
816 if (!isPatternEditor) return '';
817 const url = new URL(window.location.href);
818 const p = url.searchParams.get('p') || url.searchParams.get('postId');
819 if (!p) return '';
820 const decoded = decodeURIComponent(p);
821 // Use correct WordPress terminology: template part (header/footer)
822 return decoded;
823 } catch (e) {
824 return '';
825 }
826 }
827
828 async function sendMessageAIV3() {
829 const messages = document.getElementById("chat-messages");
830 const input = document.querySelector("#chat-input textarea");
831 // const undoBtn = document.getElementById("chat-undo");
832 const sendBtn = document.querySelector("#chat-input button");
833 const toggleBtn = document.getElementById("chat-toggle");
834 const question = input.value.trim();
835
836 // Vérifier qu'un prompt est présent
837 let finalQuestion = question;
838 const patternName = getPatternName();
839 if (!finalQuestion) {
840 addMessage("Please enter a prompt first.", "assistant");
841 return;
842 }
843
844 const conversationHistory = getConversationHistoryString(6);
845
846 // 🔒 Désactiver le bouton + animation loading
847 sendBtn.disabled = true;
848 sendBtn.classList.add("loading");
849 sendBtn.textContent = "Generating...";
850
851 // 🎨 Activer l'effet visuel sur le toggle
852 if (toggleBtn) {
853 toggleBtn.classList.add("generating");
854 }
855
856 // Ajouter le message utilisateur à l'historique
857 addMessage(finalQuestion, "user");
858 input.value = "";
859 messages.scrollTop = messages.scrollHeight;
860
861 // Sauvegarder les blocs actuels
862 previousBlocks = wp.data.select("core/block-editor").getBlocks();
863
864 try {
865 // Récupérer le token JWT
866 const jwtToken = await getJwtToken();
867
868 console.log('window.config: ', window.config);
869
870 let res
871 if (patternName) {
872 const payload = {
873 userPrompt: finalQuestion,
874 // pageContent: pageContent,
875 patternName: patternName,
876 conversationHistory,
877 };
878 res = await fetch(
879 window.config.apiUrl + "/ai-transform-page/generate-pattern",
880 {
881 method: "POST",
882 headers: {
883 "Content-Type": "application/json",
884 Authorization: `Bearer ${jwtToken}`,
885 },
886 body: JSON.stringify(payload),
887 }
888 );
889 } else {
890 const payload = {
891 userPrompt: finalQuestion,
892 // pageContent: pageContent,
893 conversationHistory,
894 };
895 res = await fetch(
896 window.config.apiUrl + "/ai-transform-page/v2-page-generation",
897 {
898 method: "POST",
899 headers: {
900 "Content-Type": "application/json",
901 Authorization: `Bearer ${jwtToken}`,
902 },
903 body: JSON.stringify(payload),
904 }
905 );
906 }
907
908 const data = await res.json();
909 console.log("data: ", data);
910 console.log(data.pageContent);
911 if (data.error === "not-enough-credits") {
912 showNotEnoughCreditsToast();
913 return;
914 }
915
916 if (data.pageContent) {
917 if (data.pageContent !== "[-no-content-to-return-]") {
918 // Convertir chaque bloc JSON en bloc WordPress
919 const newBlocks = data.pageContent.map(buildBlock);
920
921 // 🔄 Supprimer tous les blocs existants
922 wp.data.dispatch("core/block-editor").resetBlocks([]);
923 // ➕ Insérer les nouveaux blocs
924 wp.data.dispatch("core/block-editor").insertBlocks(newBlocks);
925 }
926
927 // Handle CSS if present
928 if (data.cssContent && data.cssContent !== "[-no-content-to-return-]") {
929 console.log("Injecting CSS...");
930
931 // Sauvegarder le CSS dans les meta du post (type 'page')
932 await saveCSSInPostMeta(data.cssContent, "page");
933
934 // Recharger le CSS combiné depuis le serveur
935 await loadCSSFromPostMeta();
936
937 // Afficher le bouton CSS
938 cssEditButton.style.display = "block";
939 }
940
941 // Handle meta description if present
942 if (data.postMetaDesc && data.postMetaDesc !== "[-no-content-to-return-]") {
943 // await saveMetaDescriptionInPostMeta(data.postMetaDesc);
944 await setMetaDescriptionField(data.postMetaDesc);
945 }
946
947 // Handle title if present
948 if (data.postTitle && data.postTitle !== "[-no-content-to-return-]") {
949 updatePostTitle(data.postTitle);
950 }
951
952 let aiResponse =
953 "Page content has been updated with AI-generated content.";
954 if (data.aiResponse) {
955 aiResponse = data.aiResponse;
956 }
957
958 addMessage(aiResponse, "assistant");
959
960 // Marquer la page comme créée via IA
961 await markPageAsAICreated();
962
963 // undoBtn.style.display = "block"; // Affiche le bouton "Annuler"
964 // Mettre à jour les crédits si la réponse contient creditsLeft
965 if (typeof data.creditsLeft !== "undefined") {
966 updateCreditsDisplay(data.creditsLeft);
967 }
968 } else {
969 addMessage("Empty or invalid response.", "assistant");
970 }
971 } catch (err) {
972 addMessage("Internal server error", "assistant");
973 console.log("err : ", err);
974 } finally {
975 // 🔓 Réactiver le bouton + retirer animation
976 sendBtn.disabled = false;
977 sendBtn.classList.remove("loading");
978 sendBtn.textContent = "Generate";
979 messages.scrollTop = messages.scrollHeight;
980
981 // 🎨 Désactiver l'effet visuel sur le toggle
982 if (toggleBtn) {
983 toggleBtn.classList.remove("generating");
984 }
985 }
986 }
987
988 sendBtn.onclick = sendMessageAIV3;
989 input.addEventListener("keypress", (e) => {
990 if (e.key === "Enter") sendMessageAIV3();
991 });
992 loadUserCredits();
993 loadCSSFromPostMeta();
994 });
995
996 // Ajouter les styles CSS pour le bouton CSS et la modale
997 const cssStyles = document.createElement("style");
998 cssStyles.textContent = `
999 /* Header du chat avec bouton CSS intégré */
1000 #chat-header {
1001 display: flex;
1002 justify-content: space-between;
1003 align-items: flex-start;
1004 gap: 10px;
1005 }
1006
1007 #chat-header-left {
1008 flex: 1;
1009 }
1010
1011
1012
1013 #chat-header-left h3 {
1014 margin: 0 0 5px 0;
1015 }
1016
1017 #chat-header-left p {
1018 margin: 0;
1019 font-size: 12px;
1020 }
1021
1022 /* Bouton CSS intégré dans le header */
1023 #css-edit-button {
1024 width: 32px;
1025 height: 32px;
1026 background: #007cba;
1027 color: white;
1028 border: none;
1029 border-radius: 4px;
1030 cursor: pointer;
1031 display: flex;
1032 align-items: center;
1033 justify-content: center;
1034 flex-shrink: 0;
1035 transition: all 0.3s ease;
1036 padding: 0;
1037 }
1038
1039 #css-edit-button:hover {
1040 background: #005a87;
1041 transform: scale(1.05);
1042 }
1043
1044 #css-edit-button svg {
1045 width: 16px;
1046 height: 16px;
1047 }
1048
1049 #css-edit-button img {
1050 width: 16px;
1051 height: 16px;
1052 display: block;
1053 margin: 0 auto;
1054 }
1055
1056 /* Modale CSS */
1057 #css-modal {
1058 position: fixed;
1059 top: 0;
1060 left: 0;
1061 width: 100%;
1062 height: 100%;
1063 background: rgba(0, 0, 0, 0.7);
1064 display: flex;
1065 align-items: center;
1066 justify-content: center;
1067 z-index: 10000;
1068 }
1069
1070 #css-modal-content {
1071 background: white;
1072 border-radius: 8px;
1073 width: 80%;
1074 max-width: 800px;
1075 max-height: 80%;
1076 display: flex;
1077 flex-direction: column;
1078 box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
1079 }
1080
1081 #css-modal-header {
1082 padding: 20px;
1083 border-bottom: 1px solid #ddd;
1084 display: flex;
1085 justify-content: space-between;
1086 align-items: center;
1087 }
1088
1089 #css-modal-header h3 {
1090 margin: 0;
1091 color: #333;
1092 }
1093
1094 /* Onglets CSS */
1095 #css-modal-tabs {
1096 display: flex;
1097 border-bottom: 1px solid #ddd;
1098 background: #f8f9fa;
1099 }
1100
1101 .css-tab {
1102 flex: 1;
1103 padding: 12px 20px;
1104 border: none;
1105 background: transparent;
1106 cursor: pointer;
1107 font-size: 14px;
1108 font-weight: 500;
1109 color: #666;
1110 border-bottom: 2px solid transparent;
1111 transition: all 0.3s ease;
1112 }
1113
1114 .css-tab:hover {
1115 background: #e9ecef;
1116 color: #333;
1117 }
1118
1119 .css-tab.active {
1120 color: #007cba;
1121 border-bottom-color: #007cba;
1122 background: white;
1123 }
1124
1125 #css-modal-close {
1126 background: none;
1127 border: none;
1128 font-size: 24px;
1129 cursor: pointer;
1130 color: #666;
1131 padding: 0;
1132 width: 30px;
1133 height: 30px;
1134 display: flex;
1135 align-items: center;
1136 justify-content: center;
1137 }
1138
1139 #css-modal-close:hover {
1140 color: #000;
1141 }
1142
1143 #css-modal-body {
1144 flex: 1;
1145 padding: 20px;
1146 overflow: hidden;
1147 }
1148
1149 #css-editor {
1150 width: 100%;
1151 height: 400px;
1152 border: 1px solid #ddd;
1153 border-radius: 4px;
1154 padding: 15px;
1155 font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
1156 font-size: 14px;
1157 line-height: 1.5;
1158 resize: vertical;
1159 box-sizing: border-box;
1160 }
1161
1162 #css-editor:focus {
1163 outline: none;
1164 border-color: #007cba;
1165 box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2);
1166 }
1167
1168 #css-modal-footer {
1169 padding: 20px;
1170 border-top: 1px solid #ddd;
1171 display: flex;
1172 gap: 10px;
1173 justify-content: flex-end;
1174 }
1175
1176 #css-save, #css-cancel {
1177 padding: 10px 20px;
1178 border: none;
1179 border-radius: 4px;
1180 cursor: pointer;
1181 font-size: 14px;
1182 font-weight: 500;
1183 }
1184
1185 #css-save {
1186 background: #007cba;
1187 color: white;
1188 }
1189
1190 #css-save:hover {
1191 background: #005a87;
1192 }
1193
1194 #css-cancel {
1195 background: #f0f0f0;
1196 color: #333;
1197 }
1198
1199 #css-cancel:hover {
1200 background: #e0e0e0;
1201 }
1202
1203
1204 `;
1205
1206 document.head.appendChild(cssStyles);
1207