// Charger la configuration globale de l'API const script = document.createElement("script"); // script.src = "/wp-content/plugins/ai-builder/config.js"; document.head.appendChild(script); // Charger CodeMirror pour l'édition de code avec coloration syntaxique (function loadCodeMirror() { // CSS de CodeMirror const cmCSS = document.createElement("link"); cmCSS.rel = "stylesheet"; cmCSS.href = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css"; document.head.appendChild(cmCSS); // Thème Material Darker const cmTheme = document.createElement("link"); cmTheme.rel = "stylesheet"; cmTheme.href = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/material-darker.min.css"; document.head.appendChild(cmTheme); // Script CodeMirror core const cmScript = document.createElement("script"); cmScript.src = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"; cmScript.onload = () => { // Charger le mode CSS const cssMode = document.createElement("script"); cssMode.src = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/css/css.min.js"; document.head.appendChild(cssMode); // Charger le mode JavaScript const jsMode = document.createElement("script"); jsMode.src = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/javascript/javascript.min.js"; document.head.appendChild(jsMode); console.info("AI Builder: CodeMirror loaded"); window.codeMirrorReady = true; }; document.head.appendChild(cmScript); })(); // Récupération des variables injectées par wp_localize_script (admin) if (typeof window.aiBuilderVars !== "undefined") { // Forcer l'utilisation d'une URL relative pour éviter les erreurs CORS window.ajaxurl = '/wp-admin/admin-ajax.php'; window.aiBuilderNonce = window.aiBuilderVars.nonce; } else { // Fallback pour éviter les erreurs CORS window.ajaxurl = '/wp-admin/admin-ajax.php'; window.aiBuilderNonce = ''; } document.addEventListener("DOMContentLoaded", () => { // Fonction pour vérifier si le widget doit être affiché dans le site editor function shouldShowWidgetInSiteEditor() { const isSiteEditor = window.location.pathname.includes('site-editor.php'); if (!isSiteEditor) { return true; // Toujours afficher en dehors du site editor } const urlParams = new URLSearchParams(window.location.search); const pParam = urlParams.get('p'); // URLSearchParams.get() décode automatiquement %2F en / // Ne pas afficher le widget si : // 1. Aucun paramètre p n'est présent // 2. p est exactement '/pattern' (page de sélection des patterns) if (!pParam || pParam === '/pattern') { return false; // Ne pas afficher le chat widget } // Sinon, si p contient un template/pattern spécifique (ex: /wp_template_part/...), afficher le widget return true; } // Charger le CSS des témoignages const style = document.createElement("link"); style.rel = "stylesheet"; style.href = "/wp-content/plugins/ai-builder/assets/css/cards.css"; document.head.appendChild(style); document.body.insertAdjacentHTML( "beforeend", `
AI Builder

AI Page Builder

0/4000
` ); const toggle = document.getElementById("chat-toggle"); const box = document.getElementById("chat-box"); const messages = document.getElementById("chat-messages"); const input = document.querySelector("#chat-input textarea"); const pageGenerateBtn = document.getElementById("chat-generate-page"); const charCounter = document.getElementById("chat-char-counter"); const undoBtn = document.getElementById("chat-undo"); const closeChatBtn = document.getElementById("chat-close"); const expandBtn = document.getElementById("chat-expand-input"); const expandModal = document.getElementById("chat-expand-modal"); const expandBackdrop = document.getElementById("chat-expand-backdrop"); const expandClose = document.getElementById("chat-expand-close"); const expandDone = document.getElementById("chat-expand-done"); const expandTextarea = document.getElementById("chat-expand-textarea"); const expandCounter = document.getElementById("chat-expand-counter"); let chatHistory = []; // Fonction pour afficher/masquer le widget selon l'URL dans le site editor function updateWidgetVisibility() { const shouldShow = shouldShowWidgetInSiteEditor(); if (toggle && box) { if (shouldShow) { toggle.style.display = 'inline-block'; // Si le widget était ouvert, le garder ouvert } else { toggle.style.display = 'none'; box.style.display = 'none'; // Fermer aussi la boîte si elle était ouverte } } } // Mettre à jour la visibilité au chargement initial updateWidgetVisibility(); // Close button (keeps toggle visible) if (closeChatBtn && box) { closeChatBtn.addEventListener("click", () => { box.style.display = "none"; try { closeExpandModal(); } catch (e) { } }); } // Écouter les changements d'URL dans le site editor (navigation JavaScript) if (window.location.pathname.includes('site-editor.php')) { // Observer les changements d'URL via popstate (navigation navigateur) window.addEventListener('popstate', updateWidgetVisibility); // Observer les changements d'URL via l'API History (pushState/replaceState) const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function () { originalPushState.apply(history, arguments); setTimeout(updateWidgetVisibility, 100); // Petit délai pour laisser WordPress mettre à jour l'URL }; history.replaceState = function () { originalReplaceState.apply(history, arguments); setTimeout(updateWidgetVisibility, 100); }; // Observer les changements dans le DOM (WordPress peut changer l'URL sans utiliser l'API History) const urlObserver = new MutationObserver(() => { updateWidgetVisibility(); }); // Observer les changements dans l'URL de la page let lastUrl = location.href; setInterval(() => { const currentUrl = location.href; if (currentUrl !== lastUrl) { lastUrl = currentUrl; updateWidgetVisibility(); } }, 500); // Vérifier toutes les 500ms } const MAX_CHAT_CHARS = 4000; let isSyncingExpandedPrompt = false; // Met à jour le compteur de caractères du chat function updateChatCharCounter() { if (!input || !charCounter) return; const currentLength = input.value.length; // Sécurité : tronquer si, pour une raison quelconque, on dépasse la limite if (currentLength > MAX_CHAT_CHARS) { input.value = input.value.slice(0, MAX_CHAT_CHARS); } charCounter.textContent = `${input.value.length}/${MAX_CHAT_CHARS}`; // If the expanded editor is open, keep it in sync even for programmatic changes. if (isExpandModalOpen && typeof isExpandModalOpen === "function" && isExpandModalOpen() && expandTextarea && !isSyncingExpandedPrompt) { isSyncingExpandedPrompt = true; expandTextarea.value = input.value.slice(0, MAX_CHAT_CHARS); updateExpandedCounter(); isSyncingExpandedPrompt = false; } } function updateExpandedCounter() { if (!expandTextarea || !expandCounter) return; expandCounter.textContent = `${expandTextarea.value.length}/${MAX_CHAT_CHARS}`; } function isExpandModalOpen() { return !!expandModal && !expandModal.hasAttribute("hidden"); } function openExpandModal() { if (!expandModal || !expandTextarea) return; expandModal.removeAttribute("hidden"); expandModal.setAttribute("aria-hidden", "false"); expandTextarea.value = (input ? input.value : "").slice(0, MAX_CHAT_CHARS); updateExpandedCounter(); // Focus after paint to ensure cursor placement window.setTimeout(() => { try { expandTextarea.focus(); expandTextarea.selectionStart = expandTextarea.value.length; expandTextarea.selectionEnd = expandTextarea.value.length; } catch (e) { } }, 0); } function closeExpandModal() { if (!expandModal) return; expandModal.setAttribute("hidden", ""); expandModal.setAttribute("aria-hidden", "true"); // keep focus in the main input for quick send try { if (input) input.focus(); } catch (e) { } } if (input) { input.addEventListener("input", updateChatCharCounter); input.addEventListener("input", () => { if (!isExpandModalOpen() || !expandTextarea) return; if (isSyncingExpandedPrompt) return; isSyncingExpandedPrompt = true; expandTextarea.value = input.value.slice(0, MAX_CHAT_CHARS); updateExpandedCounter(); isSyncingExpandedPrompt = false; }); // Initialiser l'affichage du compteur updateChatCharCounter(); } if (expandTextarea) { expandTextarea.addEventListener("input", () => { if (!input) return; if (isSyncingExpandedPrompt) return; isSyncingExpandedPrompt = true; input.value = expandTextarea.value.slice(0, MAX_CHAT_CHARS); updateChatCharCounter(); updateExpandedCounter(); isSyncingExpandedPrompt = false; }); expandTextarea.addEventListener("paste", () => setTimeout(() => { updateExpandedCounter(); }, 0)); updateExpandedCounter(); } if (expandBtn) { expandBtn.addEventListener("click", () => { openExpandModal(); }); } if (expandBackdrop) expandBackdrop.addEventListener("click", closeExpandModal); if (expandClose) expandClose.addEventListener("click", closeExpandModal); if (expandDone) expandDone.addEventListener("click", closeExpandModal); document.addEventListener("keydown", (e) => { if (e.key !== "Escape") return; if (!isExpandModalOpen()) return; e.preventDefault(); closeExpandModal(); }); // Indicateur de saisie de l'IA (3 points animés) let stopChatGenerationStatusMessages = null; function startChatGenerationStatusMessages(indicatorEl) { if (!indicatorEl) return function () { }; const statusEl = indicatorEl.querySelector(".chat-gen-status"); if (!statusEl) return function () { }; const steps = [ { text: "Analyzing your prompt…", ms: 5000 }, { text: "Designing your page structure…", ms: 7000 }, { text: "Selecting the best blocks for your content…", ms: 7000 }, { text: "Generating block content…", ms: 15000 }, { text: "Crafting your custom CSS styles…", ms: 10000 }, { text: "Optimizing for responsive layouts…", ms: 5000 }, { text: "Fine-tuning typography and spacing…", ms: 5000 }, { text: "Harmonizing your color palette…", ms: 5000 }, { text: "Adding accessibility attributes…", ms: 4000 }, { text: "Polishing visual details…", ms: 5000 }, { text: "Running final quality checks…", ms: 4000 }, { text: "Almost there, preparing your page…", ms: Infinity }, ]; let i = 0; const timeouts = []; let stopped = false; function setTextWithFade(nextText) { if (stopped) return; statusEl.classList.add("is-fading"); window.setTimeout(function () { if (stopped) return; statusEl.textContent = nextText; statusEl.classList.remove("is-fading"); }, 180); } // initialize immediately statusEl.textContent = steps[0].text; statusEl.classList.remove("is-fading"); function scheduleNext() { if (stopped) return; const step = steps[i]; if (!step || !Number.isFinite(step.ms)) { return; } const tid = window.setTimeout(function () { if (stopped) return; i = Math.min(i + 1, steps.length - 1); setTextWithFade(steps[i].text); scheduleNext(); }, step.ms); timeouts.push(tid); } scheduleNext(); return function stop() { stopped = true; while (timeouts.length) { try { window.clearTimeout(timeouts.pop()); } catch (e) { } } }; } function createTypingIndicator() { if (!messages) return null; let indicator = document.getElementById("chat-typing-indicator"); if (!indicator) { indicator = document.createElement("div"); indicator.id = "chat-typing-indicator"; indicator.innerHTML = ` `; } return indicator; } function showTypingIndicator() { if (!messages) return; const indicator = createTypingIndicator(); if (!indicator) return; if (!messages.contains(indicator)) { messages.appendChild(indicator); } if (typeof stopChatGenerationStatusMessages === "function") { stopChatGenerationStatusMessages(); } stopChatGenerationStatusMessages = startChatGenerationStatusMessages(indicator); } function hideTypingIndicator() { if (typeof stopChatGenerationStatusMessages === "function") { stopChatGenerationStatusMessages(); } stopChatGenerationStatusMessages = null; const indicator = document.getElementById("chat-typing-indicator"); if (indicator && indicator.parentNode) { indicator.parentNode.removeChild(indicator); } } function getChatStorageKey() { const prefix = "aiBuilderChatHistory"; const postId = wp?.data?.select("core/editor")?.getCurrentPostId?.() || new URL(window.location.href, window.location.origin).searchParams.get("post") || new URL(window.location.href, window.location.origin).searchParams.get("postId") || new URL(window.location.href, window.location.origin).searchParams.get("p"); if (postId) { return `${prefix}_post_${postId}`; } const patternName = getPatternName(); if (patternName) { return `${prefix}_pattern_${patternName}`; } return `${prefix}_${window.location.pathname}`; } // Fonction pour migrer l'historique de post-new.php vers l'ID du post function migrateChatHistoryFromNewPost() { if (!window.localStorage) return; const prefix = "aiBuilderChatHistory"; const newPostKey = `${prefix}_/wp-admin/post-new.php`; const currentKey = getChatStorageKey(); // Si on est déjà sur la clé post-new.php, pas besoin de migrer if (currentKey === newPostKey) return; // Si la clé actuelle est basée sur un ID de post, vérifier s'il y a un historique à migrer if (currentKey.startsWith(`${prefix}_post_`)) { try { const oldHistory = window.localStorage.getItem(newPostKey); if (oldHistory) { const parsedHistory = JSON.parse(oldHistory); if (Array.isArray(parsedHistory) && parsedHistory.length > 0) { // Fusionner avec l'historique existant (s'il y en a un) const existingHistory = chatHistory.length > 0 ? chatHistory : []; chatHistory = [...existingHistory, ...parsedHistory]; // Sauvegarder avec la nouvelle clé window.localStorage.setItem(currentKey, JSON.stringify(chatHistory)); // Supprimer l'ancienne clé window.localStorage.removeItem(newPostKey); console.log("AI Builder: Chat history migrated from post-new.php to post ID"); // Re-rendre l'historique renderChatHistory(); } } } catch (error) { console.warn("AI Builder: Error migrating chat history", error); } } } // Éléments CSS const cssEditButton = document.getElementById("css-edit-button"); const cssModal = document.getElementById("css-modal"); const cssEditor = document.getElementById("css-editor"); const cssSaveBtn = document.getElementById("css-save"); const cssCancelBtn = document.getElementById("css-cancel"); const cssModalClose = document.getElementById("css-modal-close"); // Éléments JS const jsEditButton = document.getElementById("js-edit-button"); const jsModal = document.getElementById("js-modal"); const jsEditor = document.getElementById("js-editor"); const jsSaveBtn = document.getElementById("js-save"); const jsCancelBtn = document.getElementById("js-cancel"); const jsModalClose = document.getElementById("js-modal-close"); // Bouton Header/Footer const headersFootersButton = document.getElementById("headers-footers-button"); if (headersFootersButton) { headersFootersButton.addEventListener("click", () => { const headersFootersUrl = "/wp-admin/admin.php?page=aibui-headers-footers"; window.open(headersFootersUrl, "_blank"); }); } toggle.onclick = () => { const isOpening = box.style.display !== "flex"; box.style.display = box.style.display === "flex" ? "none" : "flex"; box.style.flexDirection = "column"; // Scroller vers le bas quand on ouvre le chat if (isOpening && messages) { // Petit délai pour s'assurer que le DOM est mis à jour setTimeout(() => { messages.scrollTop = messages.scrollHeight; }, 100); } }; // Logique pour la modale CSS let currentCSSContent = ""; const AIBUI_COMBINED_PAGE_MARKER = "/* === AI Builder: Page CSS === */"; const AIBUI_COMBINED_BLOCKS_MARKER = "/* === AI Builder: Blocks CSS === */"; // Initialiser les variables CSS globales window.aiBuilderPageCSS = window.aiBuilderPageCSS || ""; window.aiBuilderBlockCSS = window.aiBuilderBlockCSS || ""; function buildCombinedCSSValue(pageCss, blockCss) { const p = (pageCss || "").trimEnd(); const b = (blockCss || "").trimEnd(); return ( AIBUI_COMBINED_PAGE_MARKER + "\n" + p + "\n\n" + AIBUI_COMBINED_BLOCKS_MARKER + "\n" + b + "\n" ); } function splitCombinedCSSValue(combinedText) { const raw = String(combinedText || ""); const iPage = raw.indexOf(AIBUI_COMBINED_PAGE_MARKER); const iBlocks = raw.indexOf(AIBUI_COMBINED_BLOCKS_MARKER); // If markers are missing, treat as page CSS and preserve existing blocks CSS if (iPage === -1 || iBlocks === -1 || iBlocks < iPage) { return { pageCss: raw, blockCss: window.aiBuilderBlockCSS || "", }; } const pageStart = iPage + AIBUI_COMBINED_PAGE_MARKER.length; const blocksStart = iBlocks + AIBUI_COMBINED_BLOCKS_MARKER.length; const pageCss = raw.slice(pageStart, iBlocks).replace(/^\s*\n/, ""); const blockCss = raw.slice(blocksStart).replace(/^\s*\n/, ""); return { pageCss, blockCss }; } function saveChatHistory() { if (!window.localStorage) return; try { const key = getChatStorageKey(); window.localStorage.setItem(key, JSON.stringify(chatHistory)); } catch (error) { console.warn("AI Builder: Unable to persist chat history", error); } } function loadChatHistoryFromStorage() { if (!window.localStorage) return; try { const key = getChatStorageKey(); console.log("Loading chat history from storage:", key); const storedHistory = window.localStorage.getItem(key); if (!storedHistory) return; const parsedHistory = JSON.parse(storedHistory); if (Array.isArray(parsedHistory)) { chatHistory = parsedHistory; } } catch (error) { console.warn("AI Builder: Unable to read chat history", error); } } function renderChatHistory() { if (!messages) return; messages.innerHTML = ""; chatHistory.forEach(({ type, message }) => { addMessage(message, type, { persist: false }); }); // Scroller vers le bas après avoir rendu l'historique if (chatHistory.length > 0) { setTimeout(() => { messages.scrollTop = messages.scrollHeight; }, 50); } } function getConversationHistoryString(limit = 4) { if (!chatHistory.length) return ""; const recentMessages = chatHistory.slice(-limit); const segments = recentMessages.map(({ type, message }) => { const label = type === "user" ? "User question" : "AI Response"; return `${label} : ${message}`; }); return segments.join(". ") + (segments.length ? "." : ""); } // Fonction pour ajouter des messages dans le chat function addMessage(message, type = "assistant", options = {}) { const { persist = true } = options; if (!messages) return; // Si un indicateur de saisie est présent, le garder toujours en bas const typingIndicator = document.getElementById("chat-typing-indicator"); if (typingIndicator && typingIndicator.parentNode === messages) { messages.removeChild(typingIndicator); } const messageDiv = document.createElement("div"); messageDiv.className = type === "assistant" ? "ai-message" : "user-message"; if (type === "assistant") { messageDiv.innerHTML = `${message}`; } else { messageDiv.innerHTML = `${message}`; } messages.appendChild(messageDiv); // Ré‑ajouter l'indicateur de saisie en bas si nécessaire if (typingIndicator) { messages.appendChild(typingIndicator); } messages.scrollTop = messages.scrollHeight; if (persist) { chatHistory.push({ type, message }); saveChatHistory(); } } loadChatHistoryFromStorage(); renderChatHistory(); // Migrer l'historique de post-new.php vers l'ID du post si nécessaire migrateChatHistoryFromNewPost(); // Surveiller les changements d'ID de post (pour les drafts créés après le chargement) let lastKnownPostId = null; let lastKnownKey = getChatStorageKey(); function checkPostIdChange() { try { const currentPostId = wp?.data?.select("core/editor")?.getCurrentPostId?.(); const currentKey = getChatStorageKey(); if (currentPostId && currentPostId !== lastKnownPostId) { // L'ID a changé, mettre à jour la clé de stockage if (currentKey !== lastKnownKey && window.localStorage) { // Migrer l'historique vers la nouvelle clé const oldHistory = window.localStorage.getItem(lastKnownKey); if (oldHistory) { try { const parsedHistory = JSON.parse(oldHistory); if (Array.isArray(parsedHistory) && parsedHistory.length > 0) { chatHistory = parsedHistory; window.localStorage.setItem(currentKey, oldHistory); // Ne pas supprimer l'ancienne clé immédiatement, au cas où console.log("AI Builder: Chat history migrated to new post ID:", currentPostId); renderChatHistory(); } } catch (e) { console.warn("AI Builder: Error migrating history on ID change", e); } } } lastKnownPostId = currentPostId; lastKnownKey = currentKey; } else if (currentKey !== lastKnownKey) { // La clé a changé même si l'ID n'a pas changé (changement d'URL) lastKnownKey = currentKey; // Recharger l'historique avec la nouvelle clé loadChatHistoryFromStorage(); renderChatHistory(); migrateChatHistoryFromNewPost(); } } catch (e) { // Ignorer les erreurs si wp.data n'est pas encore disponible } } // Vérifier l'ID initial setTimeout(() => { try { lastKnownPostId = wp?.data?.select("core/editor")?.getCurrentPostId?.(); } catch (e) { } }, 1000); // Surveiller les changements d'ID toutes les 2 secondes setInterval(checkPostIdChange, 2000); // Fonction pour assurer un minimum de lignes dans CodeMirror function ensureMinLines(cm, minLines) { const lineCount = cm.lineCount(); if (lineCount < minLines) { const linesToAdd = minLines - lineCount; const padding = "\n".repeat(linesToAdd); const currentValue = cm.getValue(); // Ne pas modifier si déjà suffisant if (currentValue.split("\n").length < minLines) { // On ne modifie pas le contenu, on utilise le CSS pour le padding } } // Refresh pour s'assurer que l'affichage est correct cm.refresh(); } // Instance CodeMirror pour CSS let cssCodeMirror = null; // Initialiser CodeMirror pour CSS quand il est prêt function initCSSCodeMirror() { if (cssCodeMirror) return; if (typeof CodeMirror === "undefined") { setTimeout(initCSSCodeMirror, 100); return; } cssCodeMirror = CodeMirror.fromTextArea(cssEditor, { mode: "css", theme: "material-darker", lineNumbers: true, lineWrapping: true, tabSize: 2, indentWithTabs: false, autoCloseBrackets: true, viewportMargin: Infinity, }); cssCodeMirror.setSize("100%", "400px"); // Assurer un minimum de 10 lignes visibles ensureMinLines(cssCodeMirror, 10); } // Ouvrir la modale CSS cssEditButton.onclick = () => { cssModal.style.display = "flex"; initCSSCodeMirror(); // Charger le CSS combiné (page + blocks) const combined = buildCombinedCSSValue(window.aiBuilderPageCSS || "", window.aiBuilderBlockCSS || ""); if (cssCodeMirror) { cssCodeMirror.setValue(combined); setTimeout(() => cssCodeMirror.refresh(), 10); } else { cssEditor.value = combined; } setTimeout(() => { if (cssCodeMirror) cssCodeMirror.focus(); }, 100); }; // Fermer la modale CSS function closeCSSModal() { cssModal.style.display = "none"; } cssModalClose.onclick = closeCSSModal; cssCancelBtn.onclick = closeCSSModal; // Sauvegarder le CSS cssSaveBtn.onclick = async () => { const newCSSContent = cssCodeMirror ? cssCodeMirror.getValue() : cssEditor.value; const parts = splitCombinedCSSValue(newCSSContent); window.aiBuilderPageCSS = parts.pageCss; window.aiBuilderBlockCSS = parts.blockCss; // Save both storages (replace = true since it's a manual edit) await saveCSSInPostMeta(parts.pageCss, "page", true); await saveCSSInPostMeta(parts.blockCss, "block", true); // Recharger le CSS combiné depuis le serveur après sauvegarde await loadCSSFromPostMeta(); closeCSSModal(); }; // Fermer la modale en cliquant à l'extérieur cssModal.onclick = (e) => { if (e.target === cssModal) { closeCSSModal(); } }; // Global: open CSS modal and scroll/highlight a specific class window.openCSSModalForClass = function (classNames) { if (!classNames) return; const classes = classNames.split(/\s+/).filter(Boolean); if (!classes.length) return; cssModal.style.display = "flex"; initCSSCodeMirror(); const combined = buildCombinedCSSValue(window.aiBuilderPageCSS || "", window.aiBuilderBlockCSS || ""); if (cssCodeMirror) { cssCodeMirror.setValue(combined); setTimeout(() => cssCodeMirror.refresh(), 10); } else { cssEditor.value = combined; } // Helper: find and highlight all matching rules in the current CodeMirror content function findAndHighlight() { if (!cssCodeMirror) return; cssCodeMirror.refresh(); // Clear previous highlights if (window._cssClassMarks) { window._cssClassMarks.forEach((m) => m.clear()); } window._cssClassMarks = []; const content = cssCodeMirror.getValue(); let firstMatchLine = null; for (const cls of classes) { const selector = "." + cls; // Search line by line for (let line = 0; line < cssCodeMirror.lineCount(); line++) { const lineText = cssCodeMirror.getLine(line); if (lineText.includes(selector)) { if (firstMatchLine === null) firstMatchLine = line; // Find the full CSS rule block: from this selector line to closing } let ruleStart = line; let ruleEnd = line; for (let i = line; i < cssCodeMirror.lineCount(); i++) { if (cssCodeMirror.getLine(i).includes("{")) { let depth = 0; for (let j = i; j < cssCodeMirror.lineCount(); j++) { const lt = cssCodeMirror.getLine(j); for (const ch of lt) { if (ch === "{") depth++; if (ch === "}") depth--; } if (depth <= 0) { ruleEnd = j; break; } } break; } } const mark = cssCodeMirror.markText( { line: ruleStart, ch: 0 }, { line: ruleEnd, ch: cssCodeMirror.getLine(ruleEnd).length }, { className: "css-class-highlight" } ); window._cssClassMarks.push(mark); } } } if (firstMatchLine !== null) { cssCodeMirror.setCursor({ line: firstMatchLine, ch: 0 }); cssCodeMirror.scrollIntoView({ line: firstMatchLine, ch: 0 }, 120); } } // Wait for CodeMirror to fully render before searching setTimeout(findAndHighlight, 300); }; // Logique pour la modale JS let currentJSContent = ""; const AIBUI_COMBINED_PAGE_JS_MARKER = "/* === AI Builder: Page JS === */"; const AIBUI_COMBINED_BLOCKS_JS_MARKER = "/* === AI Builder: Blocks JS === */"; // Initialiser les variables JS globales window.aiBuilderPageJS = window.aiBuilderPageJS || ""; window.aiBuilderBlockJS = window.aiBuilderBlockJS || ""; // Instance CodeMirror pour JS let jsCodeMirror = null; // Initialiser CodeMirror pour JS quand il est prêt function initJSCodeMirror() { if (jsCodeMirror) return; if (typeof CodeMirror === "undefined") { setTimeout(initJSCodeMirror, 100); return; } jsCodeMirror = CodeMirror.fromTextArea(jsEditor, { mode: "javascript", theme: "material-darker", lineNumbers: true, lineWrapping: true, tabSize: 2, indentWithTabs: false, autoCloseBrackets: true, viewportMargin: Infinity, }); jsCodeMirror.setSize("100%", "400px"); // Assurer un minimum de 10 lignes visibles ensureMinLines(jsCodeMirror, 10); } // Ouvrir la modale JS jsEditButton.onclick = () => { jsModal.style.display = "flex"; initJSCodeMirror(); const combined = ( AIBUI_COMBINED_PAGE_JS_MARKER + "\n" + String(window.aiBuilderPageJS || "").trimEnd() + "\n\n" + AIBUI_COMBINED_BLOCKS_JS_MARKER + "\n" + String(window.aiBuilderBlockJS || "").trimEnd() + "\n" ); if (jsCodeMirror) { jsCodeMirror.setValue(combined); setTimeout(() => jsCodeMirror.refresh(), 10); } else { jsEditor.value = combined; } setTimeout(() => { if (jsCodeMirror) jsCodeMirror.focus(); }, 100); }; // Fermer la modale JS function closeJSModal() { jsModal.style.display = "none"; } jsModalClose.onclick = closeJSModal; jsCancelBtn.onclick = closeJSModal; // Sauvegarder le JS jsSaveBtn.onclick = async () => { const newJSContent = jsCodeMirror ? jsCodeMirror.getValue() : jsEditor.value; const raw = String(newJSContent || ""); const iPage = raw.indexOf(AIBUI_COMBINED_PAGE_JS_MARKER); const iBlocks = raw.indexOf(AIBUI_COMBINED_BLOCKS_JS_MARKER); let pageJs = raw; let blockJs = window.aiBuilderBlockJS || ""; if (iPage !== -1 && iBlocks !== -1 && iBlocks > iPage) { const pageStart = iPage + AIBUI_COMBINED_PAGE_JS_MARKER.length; const blocksStart = iBlocks + AIBUI_COMBINED_BLOCKS_JS_MARKER.length; pageJs = raw.slice(pageStart, iBlocks).replace(/^\s*\n/, ""); blockJs = raw.slice(blocksStart).replace(/^\s*\n/, ""); } window.aiBuilderPageJS = pageJs; window.aiBuilderBlockJS = blockJs; // Save both storages (replace = true since it's a manual edit) await saveJSInPostMeta(pageJs, "page", true); await saveJSInPostMeta(blockJs, "block", true); // Recharger le JS combiné depuis le serveur après sauvegarde await loadJSFromPostMeta(); closeJSModal(); }; // Fermer la modale JS en cliquant à l'extérieur jsModal.onclick = (e) => { if (e.target === jsModal) { closeJSModal(); } }; function buildBlock(block) { if (!block) return null; const { blockName, attrs = {}, innerBlocks = [] } = block; const nextAttrs = { ...(attrs || {}) }; // API sometimes returns text payload as `content` (not in attrs). // Map it to the common Gutenberg attribute key so text doesn't disappear. if (typeof block.content === "string" && typeof nextAttrs.content !== "string") { nextAttrs.content = block.content; } // Common variants some generators use. if (typeof block.value === "string" && typeof nextAttrs.value !== "string") { nextAttrs.value = block.value; } if (typeof block.values === "string" && typeof nextAttrs.values !== "string") { nextAttrs.values = block.values; } return wp.blocks.createBlock( blockName, nextAttrs, (innerBlocks || []).map(buildBlock) // récursivité ici ); } function getAibuiAccountAdminUrl() { const base = typeof aiBuilderVars !== "undefined" && aiBuilderVars.adminBaseUrl ? String(aiBuilderVars.adminBaseUrl).replace(/\/?$/, "/") : "/wp-admin/"; return base + "admin.php?page=aibui-assistant"; } function getAibuiCreditsAdminUrl() { const base = typeof aiBuilderVars !== "undefined" && aiBuilderVars.adminBaseUrl ? String(aiBuilderVars.adminBaseUrl).replace(/\/?$/, "/") : "/wp-admin/"; return base + "admin.php?page=aibui-credits"; } function aibuiAuthRequiredError(message) { const e = new Error(message); e.aibuiAuthRequired = true; return e; } /** HTML for in-chat message when the user must sign in / create an AI Builder account */ function getAccountRequiredChatMessageHtml() { const url = getAibuiAccountAdminUrl().replace(/&/g, "&"); return ( '
' + '
You are not signed in to an AI Builder account. Open the account page to sign in or create an account first, then you can use the AI Page Builder.
' + 'Go to Account' + "
" ); } /** HTML for in-chat message when the user is out of credits */ function getCreditsRequiredChatMessageHtml() { const url = getAibuiCreditsAdminUrl().replace(/&/g, "&"); return ( '
' + '
You don\'t have enough credits to generate more content. Open the credits page to purchase or manage your credits.
' + 'Go to Credits' + "
" ); } // Utilitaire pour récupérer le token JWT via AJAX WordPress async function getJwtToken() { if (!window.ajaxurl || !window.aiBuilderNonce) { console.warn("AI Builder: Missing AJAX URL or nonce"); showAIMissingAccountToast(); throw aibuiAuthRequiredError("Missing AJAX URL or nonce"); } let timeoutId; try { // Create AbortController for timeout const controller = new AbortController(); timeoutId = setTimeout(() => controller.abort(), 20000); // 20 second timeout const res = await fetch(window.ajaxurl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `action=aibui_get_token&nonce=${window.aiBuilderNonce}`, signal: controller.signal, }); clearTimeout(timeoutId); if (!res.ok) { // Handle 500 errors specifically if (res.status === 500) { throw new Error("Server error: The request took too long or encountered an error. Please try again."); } console.log('res.status: ', res.status); if (res.status === 401 || res.status === 403) { showAIMissingAccountToast(); throw aibuiAuthRequiredError( "You need to have an account and be logged in to use AI features." ); } throw new Error(`HTTP error! status: ${res.status}`); } const data = await res.json(); console.log("token res: ", data); if (data.success && data.data.token) { return data.data.token; } showAIMissingAccountToast(); throw aibuiAuthRequiredError( "You need to have an account and be logged in to use AI features." ); } catch (error) { if (timeoutId) { clearTimeout(timeoutId); } console.error("Error fetching JWT token:", error); // Handle timeout/abort errors if (error.name === 'AbortError' || error.message.includes('timeout')) { throw new Error("Request timeout: The server took too long to respond. Please check your connection and try again."); } if (error.aibuiAuthRequired) { throw error; } showAIMissingAccountToast(); throw error; } } // Fonction utilitaire pour afficher un toast UX si l'utilisateur n'a pas de compte/connexion function showAIMissingAccountToast() { if (document.getElementById("ai-missing-account-toast")) return; // Pas de doublon const accountUrl = getAibuiAccountAdminUrl(); const toast = document.createElement("div"); toast.id = "ai-missing-account-toast"; toast.innerHTML = `
You need to have an account and be logged in to use AI features. Go to Account
`; document.body.appendChild(toast); setTimeout(() => { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 8000); } function showNotEnoughCreditsToast() { if (document.getElementById("ai-missing-account-toast")) return; // Pas de doublon const toast = document.createElement("div"); toast.id = "ai-missing-account-toast"; toast.innerHTML = `
You don't have enough credits. Go to credits page
`; document.body.appendChild(toast); setTimeout(() => { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 8000); } // Fonction pour afficher le nombre de crédits sous le titre ET sous le bouton function updateCreditsDisplay(credits) { // Sous le titre du chat let creditsElem = document.getElementById("ai-credits-display"); if (!creditsElem) { const header = document.querySelector("#chat-header-left"); if (header) { creditsElem = document.createElement("div"); creditsElem.id = "ai-credits-display"; creditsElem.style.fontSize = "12px"; creditsElem.style.color = "#cccccc"; creditsElem.style.marginTop = "2px"; creditsElem.style.textAlign = "left"; header.appendChild(creditsElem); } } if (creditsElem) { creditsElem.textContent = credits !== null ? `${credits} credits left` : "- credits left"; } // À l'intérieur du bouton toggle const toggle = document.getElementById("chat-toggle"); if (toggle) { let span = toggle.querySelector(".ai-credits-inside"); if (!span) { span = document.createElement("span"); span.className = "ai-credits-inside"; span.style.display = "block"; span.style.fontSize = "10px"; span.style.color = "#cccccc"; span.style.marginTop = "0px"; span.style.textAlign = "center"; toggle.appendChild(span); } span.textContent = credits !== null ? `${credits} credits` : "- credits"; } } window.updateAICreditsDisplay = updateCreditsDisplay; // Fonction pour charger les crédits utilisateur async function loadUserCredits() { let timeoutId; try { const jwtToken = await getJwtToken(); // Create AbortController for timeout const controller = new AbortController(); timeoutId = setTimeout(() => controller.abort(), 20000); // 20 second timeout const res = await fetch(window.config.apiUrl + "/user/profile", { method: "GET", headers: { Authorization: `Bearer ${jwtToken}`, "Content-Type": "application/json", }, signal: controller.signal, }); clearTimeout(timeoutId); if (!res.ok) { if (res.status === 500) { throw new Error("Server error: The request took too long or encountered an error."); } throw new Error("Failed to load profile"); } const data = await res.json(); const aiCredits = data.user?.aiCredits || {}; const totalCredits = (aiCredits.onAccountCreation || 0) + (aiCredits.monthlySubscription || 0) + (aiCredits.paid || 0); console.log("Credits loaded:", totalCredits, "from:", aiCredits); updateCreditsDisplay(totalCredits); } catch (e) { if (timeoutId) { clearTimeout(timeoutId); } console.error("Error loading credits:", e); // Handle timeout/abort errors if (e.name === 'AbortError' || e.message.includes('timeout')) { console.warn("Credits loading timeout - will retry on next action"); } updateCreditsDisplay(null); } } // Retourne la liste des documents cibles pour l'injection : // - le document principal de l'admin // - le(s) document(s) à l'intérieur des iframes du canvas d'édition // (Site Editor, Post Editor iso-mode, etc.). // Certains navigateurs ou modes peuvent retourner null pour contentDocument // (sécurité cross-origin) : on filtre silencieusement. function getEditorTargetDocuments() { const docs = []; try { if (document) docs.push(document); } catch (e) { /* ignore */ } try { const selectors = [ 'iframe[name="editor-canvas"]', 'iframe.editor-canvas__iframe', '.block-editor-iframe__container iframe', '.edit-post-visual-editor iframe', '.edit-site-visual-editor iframe', '.interface-interface-skeleton__content iframe', ]; const iframes = document.querySelectorAll(selectors.join(',')); iframes.forEach((iframe) => { try { const doc = iframe.contentDocument; if (doc && doc.head) docs.push(doc); } catch (e) { /* ignore */ } }); } catch (e) { /* ignore */ } return docs; } // Signature légère d'un contenu pour détecter les changements sans recalcul. function computeContentSignature(content) { const s = String(content == null ? '' : content); return s.length + ':' + s.slice(0, 32); } // Injecte ou met à jour un