// 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
Need inspiration? Try a style
▴
Remove
×
Prompt sent to the AI when this style is selected:
`
);
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 = `
Analyzing your prompt…
`;
}
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 = `
`;
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