PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / 2.7.10
AI Builder – Generate pages, blocks, images & translate with AI v2.7.10
2.8.0 2.7.10 2.7.9 2.7.8 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 All 123 releases
← All changes | assets/js/chat-widget.js +4452 -265 2.1.5 → 2.7.10 View file →
@@ -2,35 +2,42 @@
2 2 const script = document.createElement("script");
3 3 // script.src = "/wp-content/plugins/ai-builder/config.js";
4 4 document.head.appendChild(script);
5 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 - }
6 +// Charger CodeMirror pour l'édition de code avec coloration syntaxique
7 +(function loadCodeMirror() {
8 + // CSS de CodeMirror
9 + const cmCSS = document.createElement("link");
10 + cmCSS.rel = "stylesheet";
11 + cmCSS.href = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css";
12 + document.head.appendChild(cmCSS);
12 13
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);
14 + // Thème Material Darker
15 + const cmTheme = document.createElement("link");
16 + cmTheme.rel = "stylesheet";
17 + cmTheme.href = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/material-darker.min.css";
18 + document.head.appendChild(cmTheme);
25 19
20 + // Script CodeMirror core
21 + const cmScript = document.createElement("script");
22 + cmScript.src = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js";
23 + cmScript.onload = () => {
24 + // Charger le mode CSS
25 + const cssMode = document.createElement("script");
26 + cssMode.src = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/css/css.min.js";
27 + document.head.appendChild(cssMode);
26 28
27 - } catch (error) {
28 - console.warn("AI Builder: Erreur lors de l'initialisation de Sentry", error);
29 - }
29 + // Charger le mode JavaScript
30 + const jsMode = document.createElement("script");
31 + jsMode.src = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/javascript/javascript.min.js";
32 + document.head.appendChild(jsMode);
33 +
34 + console.info("AI Builder: CodeMirror loaded");
35 + window.codeMirrorReady = true;
36 + };
37 + document.head.appendChild(cmScript);
30 38 })();
31 39
32 -
33 40 // Récupération des variables injectées par wp_localize_script (admin)
34 41 if (typeof window.aiBuilderVars !== "undefined") {
35 42 // Forcer l'utilisation d'une URL relative pour éviter les erreurs CORS
36 43 window.ajaxurl = '/wp-admin/admin-ajax.php';
@@ -41,8 +48,28 @@
41 48 window.aiBuilderNonce = '';
42 49 }
43 50
44 51 document.addEventListener("DOMContentLoaded", () => {
52 + // Fonction pour vérifier si le widget doit être affiché dans le site editor
53 + function shouldShowWidgetInSiteEditor() {
54 + const isSiteEditor = window.location.pathname.includes('site-editor.php');
55 + if (!isSiteEditor) {
56 + return true; // Toujours afficher en dehors du site editor
57 + }
58 +
59 + const urlParams = new URLSearchParams(window.location.search);
60 + const pParam = urlParams.get('p'); // URLSearchParams.get() décode automatiquement %2F en /
61 +
62 + // Ne pas afficher le widget si :
63 + // 1. Aucun paramètre p n'est présent
64 + // 2. p est exactement '/pattern' (page de sélection des patterns)
65 + if (!pParam || pParam === '/pattern') {
66 + return false; // Ne pas afficher le chat widget
67 + }
68 + // Sinon, si p contient un template/pattern spécifique (ex: /wp_template_part/...), afficher le widget
69 + return true;
70 + }
71 +
45 72 // Charger le CSS des témoignages
46 73 const style = document.createElement("link");
47 74 style.rel = "stylesheet";
48 75 style.href = "/wp-content/plugins/ai-builder/assets/css/cards.css";
@@ -50,26 +77,81 @@
50 77
51 78 document.body.insertAdjacentHTML(
52 79 "beforeend",
53 80 `
54 - <div id="chat-toggle">🤖 AI Builder</div>
81 + <div id="chat-toggle">AI Builder</div>
55 82 <div id="chat-box">
56 83 <div id="chat-header">
57 84 <div id="chat-header-left">
58 85 <h3>AI Page Builder</h3>
59 - <p class="warning-text">⚠️ The current page content will be replaced by AI-generated content</p>
60 86 </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>
87 + <div id="chat-header-actions">
88 + <div id="chat-header-tools">
89 + <button id="js-edit-button" title="Edit JS">
90 + <span>JS</span>
91 + </button>
92 + <button id="css-edit-button" title="Edit CSS">
93 + <img src="/wp-content/plugins/ai-builder/assets/images/css-edit-icon.png" alt="Edit CSS" width="16" height="16">
94 + </button>
95 + </div>
96 + <button id="headers-footers-button" title="Headers & Footers">
97 + Header/Footer
98 + </button>
99 + </div>
100 + <button type="button" id="chat-close" class="chat-close" aria-label="Close chat">&times;</button>
64 101 </div>
65 102 <div id="chat-messages"></div>
103 + <div id="chat-style-picker" class="chat-style-picker">
104 + <div class="chat-style-picker-head">
105 + <button type="button" id="chat-style-trigger" class="chat-style-trigger" aria-expanded="false">
106 + <span id="chat-style-trigger-label">Need inspiration? Try a style</span>
107 + <span id="chat-style-trigger-chevron" class="chat-style-trigger-chevron" aria-hidden="true">▴</span>
108 + </button>
109 + <button type="button" id="chat-style-remove" class="chat-style-remove" hidden aria-label="Remove style">Remove</button>
110 + </div>
111 + <div id="chat-style-slider-outer" class="chat-style-slider-outer" hidden>
112 + <div id="chat-style-slider" class="chat-style-slider" role="listbox" aria-label="Visual styles"></div>
113 + </div>
114 + </div>
66 115 <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>
116 + <div class="chat-input-wrap">
117 + <textarea placeholder="Create a modern pricing page with 3 plans and call-to-action buttons..." rows="2" maxlength="4000"></textarea>
118 + <button type="button" id="chat-expand-input" class="chat-expand-input" aria-label="Expand prompt editor" title="Expand"></button>
119 + </div>
120 + <div id="chat-char-counter">0/4000</div>
121 + <div class="chat-actions">
122 + <button type="button" id="chat-generate-page">Generate page</button>
123 + <button type="button" id="chat-generate-block" class="chat-generate-block">Generate block</button>
124 + <button id="chat-undo" style="display: none;">Undo</button>
125 + </div>
70 126 </div>
71 127 </div>
128 + <div id="chat-expand-modal" class="chat-expand-modal" hidden aria-hidden="true">
129 + <button type="button" class="chat-expand-backdrop" id="chat-expand-backdrop" aria-label="Close editor"></button>
130 + <div class="chat-expand-panel" role="dialog" aria-modal="true" aria-labelledby="chat-expand-title">
131 + <div class="chat-expand-head">
132 + <h3 id="chat-expand-title" class="chat-expand-title">Edit prompt</h3>
133 + <button type="button" class="chat-expand-close" id="chat-expand-close" aria-label="Close">&times;</button>
134 + </div>
135 + <textarea id="chat-expand-textarea" class="chat-expand-textarea" maxlength="4000"></textarea>
136 + <div class="chat-expand-foot">
137 + <div class="chat-expand-hint"><span id="chat-expand-counter">0/4000</span></div>
138 + <button type="button" class="chat-expand-done" id="chat-expand-done">Done</button>
139 + </div>
140 + </div>
141 + </div>
142 + <div id="chat-style-detail-modal" class="chat-style-detail-modal" hidden aria-hidden="true">
143 + <button type="button" class="chat-style-detail-backdrop" id="chat-style-detail-backdrop" aria-label="Close preview"></button>
144 + <div class="chat-style-detail-panel" role="dialog" aria-modal="true" aria-labelledby="chat-style-detail-title">
145 + <button type="button" class="chat-style-detail-close" id="chat-style-detail-close" aria-label="Close">&times;</button>
146 + <h3 id="chat-style-detail-title" class="chat-style-detail-title"></h3>
147 + <div class="chat-style-detail-image-wrap">
148 + <img id="chat-style-detail-img" alt="" />
149 + </div>
150 + <p class="chat-style-detail-intro">Prompt sent to the AI when this style is selected:</p>
151 + <div id="chat-style-detail-prompt" class="chat-style-detail-prompt"></div>
152 + </div>
153 + </div>
72 154 <!-- Modale CSS -->
73 155 <div id="css-modal" style="display: none;">
74 156 <div id="css-modal-content">
75 157 <div id="css-modal-header">
@@ -75,14 +157,10 @@
75 157 <div id="css-modal-header">
76 158 <h3>Edit CSS</h3>
77 159 <button id="css-modal-close">&times;</button>
78 160 </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 161 <div id="css-modal-body">
84 - <textarea id="css-editor" placeholder="Enter your custom CSS here..."></textarea>
162 + <textarea id="css-editor"></textarea>
85 163 </div>
86 164 <div id="css-modal-footer">
87 165 <button id="css-save">Save CSS</button>
88 166 <button id="css-cancel">Cancel</button>
@@ -88,8 +166,24 @@
88 166 <button id="css-cancel">Cancel</button>
89 167 </div>
90 168 </div>
91 169 </div>
170 + <!-- Modale JS -->
171 + <div id="js-modal" style="display: none;">
172 + <div id="js-modal-content">
173 + <div id="js-modal-header">
174 + <h3>Edit JS</h3>
175 + <button id="js-modal-close">&times;</button>
176 + </div>
177 + <div id="js-modal-body">
178 + <textarea id="js-editor"></textarea>
179 + </div>
180 + <div id="js-modal-footer">
181 + <button id="js-save">Save JS</button>
182 + <button id="js-cancel">Cancel</button>
183 + </div>
184 + </div>
185 + </div>
92 186
93 187 `
94 188 );
95 189
@@ -96,22 +190,340 @@
96 190 const toggle = document.getElementById("chat-toggle");
97 191 const box = document.getElementById("chat-box");
98 192 const messages = document.getElementById("chat-messages");
99 193 const input = document.querySelector("#chat-input textarea");
100 - const sendBtn = document.querySelector("#chat-input button");
194 + const pageGenerateBtn = document.getElementById("chat-generate-page");
195 + const charCounter = document.getElementById("chat-char-counter");
101 196 const undoBtn = document.getElementById("chat-undo");
197 + const closeChatBtn = document.getElementById("chat-close");
198 + const expandBtn = document.getElementById("chat-expand-input");
199 + const expandModal = document.getElementById("chat-expand-modal");
200 + const expandBackdrop = document.getElementById("chat-expand-backdrop");
201 + const expandClose = document.getElementById("chat-expand-close");
202 + const expandDone = document.getElementById("chat-expand-done");
203 + const expandTextarea = document.getElementById("chat-expand-textarea");
204 + const expandCounter = document.getElementById("chat-expand-counter");
102 205 let chatHistory = [];
103 206
207 + // Fonction pour afficher/masquer le widget selon l'URL dans le site editor
208 + function updateWidgetVisibility() {
209 + const shouldShow = shouldShowWidgetInSiteEditor();
210 + if (toggle && box) {
211 + if (shouldShow) {
212 + toggle.style.display = 'inline-block';
213 + // Si le widget était ouvert, le garder ouvert
214 + } else {
215 + toggle.style.display = 'none';
216 + box.style.display = 'none'; // Fermer aussi la boîte si elle était ouverte
217 + }
218 + }
219 + }
220 +
221 + // Mettre à jour la visibilité au chargement initial
222 + updateWidgetVisibility();
223 +
224 + // Close button (keeps toggle visible)
225 + if (closeChatBtn && box) {
226 + closeChatBtn.addEventListener("click", () => {
227 + box.style.display = "none";
228 + try { closeExpandModal(); } catch (e) { }
229 + });
230 + }
231 +
232 + // Écouter les changements d'URL dans le site editor (navigation JavaScript)
233 + if (window.location.pathname.includes('site-editor.php')) {
234 + // Observer les changements d'URL via popstate (navigation navigateur)
235 + window.addEventListener('popstate', updateWidgetVisibility);
236 +
237 + // Observer les changements d'URL via l'API History (pushState/replaceState)
238 + const originalPushState = history.pushState;
239 + const originalReplaceState = history.replaceState;
240 +
241 + history.pushState = function () {
242 + originalPushState.apply(history, arguments);
243 + setTimeout(updateWidgetVisibility, 100); // Petit délai pour laisser WordPress mettre à jour l'URL
244 + };
245 +
246 + history.replaceState = function () {
247 + originalReplaceState.apply(history, arguments);
248 + setTimeout(updateWidgetVisibility, 100);
249 + };
250 +
251 + // Observer les changements dans le DOM (WordPress peut changer l'URL sans utiliser l'API History)
252 + const urlObserver = new MutationObserver(() => {
253 + updateWidgetVisibility();
254 + });
255 +
256 + // Observer les changements dans l'URL de la page
257 + let lastUrl = location.href;
258 + setInterval(() => {
259 + const currentUrl = location.href;
260 + if (currentUrl !== lastUrl) {
261 + lastUrl = currentUrl;
262 + updateWidgetVisibility();
263 + }
264 + }, 500); // Vérifier toutes les 500ms
265 + }
266 +
267 + const MAX_CHAT_CHARS = 4000;
268 + let isSyncingExpandedPrompt = false;
269 +
270 + // Met à jour le compteur de caractères du chat
271 + function updateChatCharCounter() {
272 + if (!input || !charCounter) return;
273 + const currentLength = input.value.length;
274 + // Sécurité : tronquer si, pour une raison quelconque, on dépasse la limite
275 + if (currentLength > MAX_CHAT_CHARS) {
276 + input.value = input.value.slice(0, MAX_CHAT_CHARS);
277 + }
278 + charCounter.textContent = `${input.value.length}/${MAX_CHAT_CHARS}`;
279 + // If the expanded editor is open, keep it in sync even for programmatic changes.
280 + if (isExpandModalOpen && typeof isExpandModalOpen === "function" && isExpandModalOpen() && expandTextarea && !isSyncingExpandedPrompt) {
281 + isSyncingExpandedPrompt = true;
282 + expandTextarea.value = input.value.slice(0, MAX_CHAT_CHARS);
283 + updateExpandedCounter();
284 + isSyncingExpandedPrompt = false;
285 + }
286 + }
287 +
288 + function updateExpandedCounter() {
289 + if (!expandTextarea || !expandCounter) return;
290 + expandCounter.textContent = `${expandTextarea.value.length}/${MAX_CHAT_CHARS}`;
291 + }
292 +
293 + function isExpandModalOpen() {
294 + return !!expandModal && !expandModal.hasAttribute("hidden");
295 + }
296 +
297 + function openExpandModal() {
298 + if (!expandModal || !expandTextarea) return;
299 + expandModal.removeAttribute("hidden");
300 + expandModal.setAttribute("aria-hidden", "false");
301 + expandTextarea.value = (input ? input.value : "").slice(0, MAX_CHAT_CHARS);
302 + updateExpandedCounter();
303 + // Focus after paint to ensure cursor placement
304 + window.setTimeout(() => {
305 + try {
306 + expandTextarea.focus();
307 + expandTextarea.selectionStart = expandTextarea.value.length;
308 + expandTextarea.selectionEnd = expandTextarea.value.length;
309 + } catch (e) { }
310 + }, 0);
311 + }
312 +
313 + function closeExpandModal() {
314 + if (!expandModal) return;
315 + expandModal.setAttribute("hidden", "");
316 + expandModal.setAttribute("aria-hidden", "true");
317 + // keep focus in the main input for quick send
318 + try { if (input) input.focus(); } catch (e) { }
319 + }
320 +
321 + if (input) {
322 + input.addEventListener("input", updateChatCharCounter);
323 + input.addEventListener("input", () => {
324 + if (!isExpandModalOpen() || !expandTextarea) return;
325 + if (isSyncingExpandedPrompt) return;
326 + isSyncingExpandedPrompt = true;
327 + expandTextarea.value = input.value.slice(0, MAX_CHAT_CHARS);
328 + updateExpandedCounter();
329 + isSyncingExpandedPrompt = false;
330 + });
331 + // Initialiser l'affichage du compteur
332 + updateChatCharCounter();
333 + }
334 +
335 + if (expandTextarea) {
336 + expandTextarea.addEventListener("input", () => {
337 + if (!input) return;
338 + if (isSyncingExpandedPrompt) return;
339 + isSyncingExpandedPrompt = true;
340 + input.value = expandTextarea.value.slice(0, MAX_CHAT_CHARS);
341 + updateChatCharCounter();
342 + updateExpandedCounter();
343 + isSyncingExpandedPrompt = false;
344 + });
345 + expandTextarea.addEventListener("paste", () => setTimeout(() => {
346 + updateExpandedCounter();
347 + }, 0));
348 + updateExpandedCounter();
349 + }
350 +
351 + if (expandBtn) {
352 + expandBtn.addEventListener("click", () => {
353 + openExpandModal();
354 + });
355 + }
356 + if (expandBackdrop) expandBackdrop.addEventListener("click", closeExpandModal);
357 + if (expandClose) expandClose.addEventListener("click", closeExpandModal);
358 + if (expandDone) expandDone.addEventListener("click", closeExpandModal);
359 + document.addEventListener("keydown", (e) => {
360 + if (e.key !== "Escape") return;
361 + if (!isExpandModalOpen()) return;
362 + e.preventDefault();
363 + closeExpandModal();
364 + });
365 +
366 + // Indicateur de saisie de l'IA (3 points animés)
367 + let stopChatGenerationStatusMessages = null;
368 +
369 + function startChatGenerationStatusMessages(indicatorEl) {
370 + if (!indicatorEl) return function () { };
371 + const statusEl = indicatorEl.querySelector(".chat-gen-status");
372 + if (!statusEl) return function () { };
373 +
374 + const steps = [
375 + { text: "Analyzing your prompt…", ms: 5000 },
376 + { text: "Designing your page structure…", ms: 7000 },
377 + { text: "Selecting the best blocks for your content…", ms: 7000 },
378 + { text: "Generating block content…", ms: 15000 },
379 + { text: "Crafting your custom CSS styles…", ms: 10000 },
380 + { text: "Optimizing for responsive layouts…", ms: 5000 },
381 + { text: "Fine-tuning typography and spacing…", ms: 5000 },
382 + { text: "Harmonizing your color palette…", ms: 5000 },
383 + { text: "Adding accessibility attributes…", ms: 4000 },
384 + { text: "Polishing visual details…", ms: 5000 },
385 + { text: "Running final quality checks…", ms: 4000 },
386 + { text: "Almost there, preparing your page…", ms: Infinity },
387 + ];
388 +
389 + let i = 0;
390 + const timeouts = [];
391 + let stopped = false;
392 +
393 + function setTextWithFade(nextText) {
394 + if (stopped) return;
395 + statusEl.classList.add("is-fading");
396 + window.setTimeout(function () {
397 + if (stopped) return;
398 + statusEl.textContent = nextText;
399 + statusEl.classList.remove("is-fading");
400 + }, 180);
401 + }
402 +
403 + // initialize immediately
404 + statusEl.textContent = steps[0].text;
405 + statusEl.classList.remove("is-fading");
406 +
407 + function scheduleNext() {
408 + if (stopped) return;
409 + const step = steps[i];
410 + if (!step || !Number.isFinite(step.ms)) {
411 + return;
412 + }
413 + const tid = window.setTimeout(function () {
414 + if (stopped) return;
415 + i = Math.min(i + 1, steps.length - 1);
416 + setTextWithFade(steps[i].text);
417 + scheduleNext();
418 + }, step.ms);
419 + timeouts.push(tid);
420 + }
421 +
422 + scheduleNext();
423 +
424 + return function stop() {
425 + stopped = true;
426 + while (timeouts.length) {
427 + try {
428 + window.clearTimeout(timeouts.pop());
429 + } catch (e) { }
430 + }
431 + };
432 + }
433 +
434 + function createTypingIndicator() {
435 + if (!messages) return null;
436 + let indicator = document.getElementById("chat-typing-indicator");
437 + if (!indicator) {
438 + indicator = document.createElement("div");
439 + indicator.id = "chat-typing-indicator";
440 + indicator.innerHTML = `
441 + <span class="typing-dots" aria-hidden="true">
442 + <span class="typing-dot"></span>
443 + <span class="typing-dot"></span>
444 + <span class="typing-dot"></span>
445 + </span>
446 + <span class="chat-gen-status chat-gen-status--init" aria-hidden="true">Analyzing your prompt…</span>
447 + `;
448 + }
449 + return indicator;
450 + }
451 +
452 + function showTypingIndicator() {
453 + if (!messages) return;
454 + const indicator = createTypingIndicator();
455 + if (!indicator) return;
456 + if (!messages.contains(indicator)) {
457 + messages.appendChild(indicator);
458 + }
459 + if (typeof stopChatGenerationStatusMessages === "function") {
460 + stopChatGenerationStatusMessages();
461 + }
462 + stopChatGenerationStatusMessages = startChatGenerationStatusMessages(indicator);
463 + }
464 +
465 + function hideTypingIndicator() {
466 + if (typeof stopChatGenerationStatusMessages === "function") {
467 + stopChatGenerationStatusMessages();
468 + }
469 + stopChatGenerationStatusMessages = null;
470 + const indicator = document.getElementById("chat-typing-indicator");
471 + if (indicator && indicator.parentNode) {
472 + indicator.parentNode.removeChild(indicator);
473 + }
474 + }
475 +
104 476 function getChatStorageKey() {
105 477 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}`;
478 +
479 + // 1) Contexte Gutenberg (Site Editor inclus).
480 + // - template / template-part → clé stable par "theme//slug"
481 + // - post / page classique → clé par post_id numérique
482 + try {
483 + if (typeof getEditorTargetContext === "function") {
484 + const ctx = getEditorTargetContext();
485 + if (ctx && ctx.templateId) {
486 + const type = ctx.templateType || "template";
487 + return `${prefix}_${type}_${ctx.templateId}`;
488 + }
489 + if (ctx && ctx.postId) {
490 + return `${prefix}_post_${ctx.postId}`;
491 + }
492 + }
493 + } catch (e) {
494 + /* ignore, on retombe sur l'URL */
113 495 }
496 +
497 + // 2) Fallback URL (utile tant que wp.data n'est pas prêt dans le Site
498 + // Editor). La route du Site Editor encode le template dans ?p=
499 + // sous la forme "/wp_template_part/theme//slug".
500 + try {
501 + const url = new URL(window.location.href, window.location.origin);
502 + const pParam = url.searchParams.get("p");
503 + if (pParam && pParam.indexOf("//") !== -1) {
504 + const pieces = pParam.replace(/^\/+/, "").split("/");
505 + if (
506 + pieces.length >= 3 &&
507 + (pieces[0] === "wp_template" || pieces[0] === "wp_template_part")
508 + ) {
509 + const tType = pieces[0];
510 + const tId = pieces.slice(1).join("/"); // "theme//slug"
511 + return `${prefix}_${tType}_${tId}`;
512 + }
513 + }
514 + const legacyPostId =
515 + url.searchParams.get("post") ||
516 + url.searchParams.get("postId") ||
517 + pParam;
518 + if (legacyPostId) {
519 + return `${prefix}_post_${legacyPostId}`;
520 + }
521 + } catch (e) {
522 + /* ignore */
523 + }
524 +
525 + // 3) Fallbacks historiques.
114 526 const patternName = getPatternName();
115 527 if (patternName) {
116 528 return `${prefix}_pattern_${patternName}`;
117 529 }
@@ -165,12 +577,25 @@
165 577 const cssSaveBtn = document.getElementById("css-save");
166 578 const cssCancelBtn = document.getElementById("css-cancel");
167 579 const cssModalClose = document.getElementById("css-modal-close");
168 580
169 - // Éléments des onglets
170 - const cssTabPage = document.getElementById("css-tab-page");
171 - const cssTabBlocks = document.getElementById("css-tab-blocks");
581 + // Éléments JS
582 + const jsEditButton = document.getElementById("js-edit-button");
583 + const jsModal = document.getElementById("js-modal");
584 + const jsEditor = document.getElementById("js-editor");
585 + const jsSaveBtn = document.getElementById("js-save");
586 + const jsCancelBtn = document.getElementById("js-cancel");
587 + const jsModalClose = document.getElementById("js-modal-close");
172 588
589 + // Bouton Header/Footer
590 + const headersFootersButton = document.getElementById("headers-footers-button");
591 + if (headersFootersButton) {
592 + headersFootersButton.addEventListener("click", () => {
593 + const headersFootersUrl = "/wp-admin/admin.php?page=aibui-headers-footers";
594 + window.open(headersFootersUrl, "_blank");
595 + });
596 + }
597 +
173 598 toggle.onclick = () => {
174 599 const isOpening = box.style.display !== "flex";
175 600 box.style.display = box.style.display === "flex" ? "none" : "flex";
176 601 box.style.flexDirection = "column";
@@ -185,14 +610,50 @@
185 610 };
186 611
187 612 // Logique pour la modale CSS
188 613 let currentCSSContent = "";
189 - let currentActiveTab = "page";
614 + const AIBUI_COMBINED_PAGE_MARKER = "/* === AI Builder: Page CSS === */";
615 + const AIBUI_COMBINED_BLOCKS_MARKER = "/* === AI Builder: Blocks CSS === */";
190 616
191 617 // Initialiser les variables CSS globales
192 618 window.aiBuilderPageCSS = window.aiBuilderPageCSS || "";
193 619 window.aiBuilderBlockCSS = window.aiBuilderBlockCSS || "";
194 620
621 + function buildCombinedCSSValue(pageCss, blockCss) {
622 + const p = (pageCss || "").trimEnd();
623 + const b = (blockCss || "").trimEnd();
624 + return (
625 + AIBUI_COMBINED_PAGE_MARKER +
626 + "\n" +
627 + p +
628 + "\n\n" +
629 + AIBUI_COMBINED_BLOCKS_MARKER +
630 + "\n" +
631 + b +
632 + "\n"
633 + );
634 + }
635 +
636 + function splitCombinedCSSValue(combinedText) {
637 + const raw = String(combinedText || "");
638 + const iPage = raw.indexOf(AIBUI_COMBINED_PAGE_MARKER);
639 + const iBlocks = raw.indexOf(AIBUI_COMBINED_BLOCKS_MARKER);
640 +
641 + // If markers are missing, treat as page CSS and preserve existing blocks CSS
642 + if (iPage === -1 || iBlocks === -1 || iBlocks < iPage) {
643 + return {
644 + pageCss: raw,
645 + blockCss: window.aiBuilderBlockCSS || "",
646 + };
647 + }
648 +
649 + const pageStart = iPage + AIBUI_COMBINED_PAGE_MARKER.length;
650 + const blocksStart = iBlocks + AIBUI_COMBINED_BLOCKS_MARKER.length;
651 + const pageCss = raw.slice(pageStart, iBlocks).replace(/^\s*\n/, "");
652 + const blockCss = raw.slice(blocksStart).replace(/^\s*\n/, "");
653 + return { pageCss, blockCss };
654 + }
655 +
195 656 function saveChatHistory() {
196 657 if (!window.localStorage) return;
197 658 try {
198 659 const key = getChatStorageKey();
@@ -202,19 +663,25 @@
202 663 }
203 664 }
204 665
205 666 function loadChatHistoryFromStorage() {
206 - if (!window.localStorage) return;
667 + if (!window.localStorage) {
668 + chatHistory = [];
669 + return;
670 + }
207 671 try {
208 672 const key = getChatStorageKey();
209 - console.log("Loading chat history from storage:", key);
210 673 const storedHistory = window.localStorage.getItem(key);
211 - if (!storedHistory) return;
674 + if (!storedHistory) {
675 + // Pas d'historique pour cette clé → on repart proprement,
676 + // sinon l'UI garderait la conversation du template précédent.
677 + chatHistory = [];
678 + return;
679 + }
212 680 const parsedHistory = JSON.parse(storedHistory);
213 - if (Array.isArray(parsedHistory)) {
214 - chatHistory = parsedHistory;
215 - }
681 + chatHistory = Array.isArray(parsedHistory) ? parsedHistory : [];
216 682 } catch (error) {
683 + chatHistory = [];
217 684 console.warn("AI Builder: Unable to read chat history", error);
218 685 }
219 686 }
220 687
@@ -220,10 +687,13 @@
220 687
221 688 function renderChatHistory() {
222 689 if (!messages) return;
223 690 messages.innerHTML = "";
224 - chatHistory.forEach(({ type, message }) => {
225 - addMessage(message, type, { persist: false });
691 + chatHistory.forEach((entry) => {
692 + addMessage(entry.message, entry.type, {
693 + persist: false,
694 + richHtml: !!entry.richHtml,
695 + });
226 696 });
227 697 // Scroller vers le bas après avoir rendu l'historique
228 698 if (chatHistory.length > 0) {
229 699 setTimeout(() => {
@@ -232,36 +702,80 @@
232 702 }
233 703 }
234 704
235 705 function getConversationHistoryString(limit = 4) {
236 - if (!chatHistory.length) return "";
237 - const recentMessages = chatHistory.slice(-limit);
238 - const segments = recentMessages.map(({ type, message }) => {
706 + // Failure notices are UI-only: replaying them would pollute the AI context
707 + // (and would send raw HTML to the API).
708 + const usable = chatHistory.filter((m) => m && !m.excludeFromContext);
709 + if (!usable.length) return "";
710 + const recentMessages = usable.slice(-limit);
711 + const segments = recentMessages.map(({ type, message, plain }) => {
239 712 const label = type === "user" ? "User question" : "AI Response";
240 - return `${label} : ${message}`;
713 + return `${label} : ${plain || message}`;
241 714 });
242 715 return segments.join(". ") + (segments.length ? "." : "");
243 716 }
244 717
718 + /**
719 + * Réponses IA : échappe le HTML, convertit **gras** en <strong>, sauts de ligne en <br>.
720 + * L'historique localStorage reste en texte brut (pas de HTML dans les saves).
721 + */
722 + function formatAssistantMessageForChat(raw) {
723 + const s = String(raw)
724 + .replace(/&/g, "&amp;")
725 + .replace(/</g, "&lt;")
726 + .replace(/>/g, "&gt;")
727 + .replace(/"/g, "&quot;");
728 + const withBold = s.replace(
729 + /\*\*((?:[^*]|\*(?!\*))+?)\*\*/g,
730 + "<strong>$1</strong>"
731 + );
732 + const withBreaks = withBold.replace(/\r\n|\r|\n/g, "<br>");
733 + return '<div class="ai-message-body">' + withBreaks + "</div>";
734 + }
735 +
245 736 // Fonction pour ajouter des messages dans le chat
246 737 function addMessage(message, type = "assistant", options = {}) {
247 - const { persist = true } = options;
738 + const {
739 + persist = true,
740 + richHtml = false,
741 + // Plain-text twin of a richHtml message, used for the AI conversation
742 + // history and for anything that cannot render markup.
743 + plain = "",
744 + // Keep this message out of the history sent to the API (error notices).
745 + excludeFromContext = false,
746 + } = options;
248 747 if (!messages) return;
249 748
749 + // Si un indicateur de saisie est présent, le garder toujours en bas
750 + const typingIndicator = document.getElementById("chat-typing-indicator");
751 + if (typingIndicator && typingIndicator.parentNode === messages) {
752 + messages.removeChild(typingIndicator);
753 + }
754 +
250 755 const messageDiv = document.createElement("div");
251 756 messageDiv.className = type === "assistant" ? "ai-message" : "user-message";
252 757
253 758 if (type === "assistant") {
254 - messageDiv.innerHTML = `<strong>🤖</strong> ${message}`;
759 + messageDiv.innerHTML = richHtml ? String(message) : formatAssistantMessageForChat(message);
255 760 } else {
256 - messageDiv.innerHTML = `<strong>👤</strong> ${message}`;
761 + messageDiv.innerHTML = `${message}`;
257 762 }
258 763
259 764 messages.appendChild(messageDiv);
765 +
766 + // Ré‑ajouter l'indicateur de saisie en bas si nécessaire
767 + if (typingIndicator) {
768 + messages.appendChild(typingIndicator);
769 + }
260 770 messages.scrollTop = messages.scrollHeight;
261 771
262 772 if (persist) {
263 - chatHistory.push({ type, message });
773 + const entry = { type, message };
774 + if (richHtml) entry.richHtml = true;
775 + if (plain) entry.plain = plain;
776 + if (excludeFromContext) entry.excludeFromContext = true;
777 + chatHistory.push(entry);
264 778 saveChatHistory();
265 779 }
266 780 }
267 781
@@ -270,69 +784,127 @@
270 784
271 785 // Migrer l'historique de post-new.php vers l'ID du post si nécessaire
272 786 migrateChatHistoryFromNewPost();
273 787
274 - // Surveiller les changements d'ID de post (pour les drafts créés après le chargement)
275 - let lastKnownPostId = null;
788 + // Surveiller les changements de clé de stockage (changement de post, de
789 + // template, de template-part dans le Site Editor, création d'un draft, etc.).
790 + // On ne migre plus automatiquement un historique vers une autre clé :
791 + // chaque cible (post, template ou template-part) a sa propre conversation.
792 + // Exception conservée : la migration explicite post-new.php → post_ID.
276 793 let lastKnownKey = getChatStorageKey();
277 794
278 795 function checkPostIdChange() {
279 796 try {
280 - const currentPostId = wp?.data?.select("core/editor")?.getCurrentPostId?.();
281 797 const currentKey = getChatStorageKey();
798 + if (currentKey === lastKnownKey) {
799 + return;
800 + }
282 801
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 - }
802 + lastKnownKey = currentKey;
303 803
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 - }
804 + // 1) Conversation : recharger l'historique propre à cette cible
805 + // (ou vider l'affichage si la cible n'a jamais discuté).
806 + loadChatHistoryFromStorage();
807 + renderChatHistory();
808 + migrateChatHistoryFromNewPost();
809 +
810 + // 2) CSS / JS custom : recharger depuis la meta du bon post.
811 + // Indispensable : sans ça, les globals window.aiBuilderPageJS /
812 + // BlockJS et le <script id="ai-builder-editor-js"> gardent le
813 + // contenu du template précédent. buildPageContext() extrait les
814 + // noms de fonctions de ce <script> et les passe à l'IA, qui finit
815 + // par réimplémenter ailleurs le code saisi sur un autre template.
816 + //
817 + // On commence par purger l'état client SYNCHRONE avant le fetch,
818 + // pour qu'aucune requête AI envoyée pendant la transition ne
819 + // récupère par erreur le contenu de la cible précédente.
820 + try {
821 + window.aiBuilderPageCSS = "";
822 + window.aiBuilderBlockCSS = "";
823 + window.aiBuilderPageJS = "";
824 + window.aiBuilderBlockJS = "";
825 + if (typeof injectCSSInEditor === "function") injectCSSInEditor("");
826 + if (typeof injectJSInEditor === "function") injectJSInEditor("");
827 + } catch (e) { /* ignore */ }
828 +
829 + try {
830 + if (typeof loadCSSFromPostMeta === "function") loadCSSFromPostMeta();
831 + } catch (e) { /* ignore */ }
832 + try {
833 + if (typeof loadJSFromPostMeta === "function") loadJSFromPostMeta();
834 + } catch (e) { /* ignore */ }
314 835 } catch (e) {
315 836 // Ignorer les erreurs si wp.data n'est pas encore disponible
316 837 }
317 838 }
318 839
319 - // Vérifier l'ID initial
840 + // Relecture initiale après montée en charge de wp.data (le Site Editor
841 + // peut ne pas encore avoir renseigné le contexte au tout premier tick).
320 842 setTimeout(() => {
321 843 try {
322 - lastKnownPostId = wp?.data?.select("core/editor")?.getCurrentPostId?.();
844 + checkPostIdChange();
323 845 } catch (e) { }
324 846 }, 1000);
325 847
326 - // Surveiller les changements d'ID toutes les 2 secondes
848 + // Surveiller les changements de clé toutes les 2 secondes
327 849 setInterval(checkPostIdChange, 2000);
328 850
851 + // Fonction pour assurer un minimum de lignes dans CodeMirror
852 + function ensureMinLines(cm, minLines) {
853 + const lineCount = cm.lineCount();
854 + if (lineCount < minLines) {
855 + const linesToAdd = minLines - lineCount;
856 + const padding = "\n".repeat(linesToAdd);
857 + const currentValue = cm.getValue();
858 + // Ne pas modifier si déjà suffisant
859 + if (currentValue.split("\n").length < minLines) {
860 + // On ne modifie pas le contenu, on utilise le CSS pour le padding
861 + }
862 + }
863 + // Refresh pour s'assurer que l'affichage est correct
864 + cm.refresh();
865 + }
866 +
867 + // Instance CodeMirror pour CSS
868 + let cssCodeMirror = null;
869 +
870 + // Initialiser CodeMirror pour CSS quand il est prêt
871 + function initCSSCodeMirror() {
872 + if (cssCodeMirror) return;
873 + if (typeof CodeMirror === "undefined") {
874 + setTimeout(initCSSCodeMirror, 100);
875 + return;
876 + }
877 + cssCodeMirror = CodeMirror.fromTextArea(cssEditor, {
878 + mode: "css",
879 + theme: "material-darker",
880 + lineNumbers: true,
881 + lineWrapping: true,
882 + tabSize: 2,
883 + indentWithTabs: false,
884 + autoCloseBrackets: true,
885 + viewportMargin: Infinity,
886 + });
887 + cssCodeMirror.setSize("100%", "400px");
888 + // Assurer un minimum de 10 lignes visibles
889 + ensureMinLines(cssCodeMirror, 10);
890 + }
891 +
329 892 // Ouvrir la modale CSS
330 893 cssEditButton.onclick = () => {
331 894 cssModal.style.display = "flex";
332 - // Charger le CSS de page par défaut
333 - loadCSSForTab("page");
334 - cssEditor.focus();
895 + initCSSCodeMirror();
896 + // Charger le CSS combiné (page + blocks)
897 + const combined = buildCombinedCSSValue(window.aiBuilderPageCSS || "", window.aiBuilderBlockCSS || "");
898 + if (cssCodeMirror) {
899 + cssCodeMirror.setValue(combined);
900 + setTimeout(() => cssCodeMirror.refresh(), 10);
901 + } else {
902 + cssEditor.value = combined;
903 + }
904 + setTimeout(() => {
905 + if (cssCodeMirror) cssCodeMirror.focus();
906 + }, 100);
335 907 };
336 908
337 909 // Fermer la modale CSS
338 910 function closeCSSModal() {
@@ -338,70 +910,26 @@
338 910 function closeCSSModal() {
339 911 cssModal.style.display = "none";
340 912 }
341 913
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 914 cssModalClose.onclick = closeCSSModal;
378 915 cssCancelBtn.onclick = closeCSSModal;
379 916
380 917 // Sauvegarder le CSS
381 918 cssSaveBtn.onclick = async () => {
382 - const newCSSContent = cssEditor.value;
919 + const newCSSContent = cssCodeMirror ? cssCodeMirror.getValue() : cssEditor.value;
920 + const parts = splitCombinedCSSValue(newCSSContent);
921 + window.aiBuilderPageCSS = parts.pageCss;
922 + window.aiBuilderBlockCSS = parts.blockCss;
383 923
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 - }
924 + // Save both storages (replace = true since it's a manual edit)
925 + await saveCSSInPostMeta(parts.pageCss, "page", true);
926 + await saveCSSInPostMeta(parts.blockCss, "block", true);
393 927
394 - // Sauvegarder dans les meta du post
395 - await saveCSSInPostMeta(newCSSContent, cssType);
396 -
397 928 // Recharger le CSS combiné depuis le serveur après sauvegarde
398 929 await loadCSSFromPostMeta();
399 930
400 931 closeCSSModal();
401 -
402 - // Afficher un message de confirmation
403 - addMessage("CSS saved successfully!", "assistant");
404 932 };
405 933
406 934 // Fermer la modale en cliquant à l'extérieur
407 935 cssModal.onclick = (e) => {
@@ -409,49 +937,389 @@
409 937 closeCSSModal();
410 938 }
411 939 };
412 940
941 + // Global: open CSS modal and scroll/highlight a specific class
942 + window.openCSSModalForClass = function (classNames) {
943 + if (!classNames) return;
413 944
945 + const classes = classNames.split(/\s+/).filter(Boolean);
946 + if (!classes.length) return;
414 947
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
948 + cssModal.style.display = "flex";
949 + initCSSCodeMirror();
950 + const combined = buildCombinedCSSValue(window.aiBuilderPageCSS || "", window.aiBuilderBlockCSS || "");
951 + if (cssCodeMirror) {
952 + cssCodeMirror.setValue(combined);
953 + setTimeout(() => cssCodeMirror.refresh(), 10);
954 + } else {
955 + cssEditor.value = combined;
956 + }
957 +
958 + // Helper: find and highlight all matching rules in the current CodeMirror content
959 + function findAndHighlight() {
960 + if (!cssCodeMirror) return;
961 + cssCodeMirror.refresh();
962 +
963 + // Clear previous highlights
964 + if (window._cssClassMarks) {
965 + window._cssClassMarks.forEach((m) => m.clear());
966 + }
967 + window._cssClassMarks = [];
968 +
969 + const content = cssCodeMirror.getValue();
970 + let firstMatchLine = null;
971 +
972 + for (const cls of classes) {
973 + const selector = "." + cls;
974 + // Search line by line
975 + for (let line = 0; line < cssCodeMirror.lineCount(); line++) {
976 + const lineText = cssCodeMirror.getLine(line);
977 + if (lineText.includes(selector)) {
978 + if (firstMatchLine === null) firstMatchLine = line;
979 +
980 + // Find the full CSS rule block: from this selector line to closing }
981 + let ruleStart = line;
982 + let ruleEnd = line;
983 +
984 + for (let i = line; i < cssCodeMirror.lineCount(); i++) {
985 + if (cssCodeMirror.getLine(i).includes("{")) {
986 + let depth = 0;
987 + for (let j = i; j < cssCodeMirror.lineCount(); j++) {
988 + const lt = cssCodeMirror.getLine(j);
989 + for (const ch of lt) {
990 + if (ch === "{") depth++;
991 + if (ch === "}") depth--;
992 + }
993 + if (depth <= 0) {
994 + ruleEnd = j;
995 + break;
996 + }
997 + }
998 + break;
999 + }
1000 + }
1001 +
1002 + const mark = cssCodeMirror.markText(
1003 + { line: ruleStart, ch: 0 },
1004 + { line: ruleEnd, ch: cssCodeMirror.getLine(ruleEnd).length },
1005 + { className: "css-class-highlight" }
1006 + );
1007 + window._cssClassMarks.push(mark);
1008 + }
1009 + }
1010 + }
1011 +
1012 + if (firstMatchLine !== null) {
1013 + cssCodeMirror.setCursor({ line: firstMatchLine, ch: 0 });
1014 + cssCodeMirror.scrollIntoView({ line: firstMatchLine, ch: 0 }, 120);
1015 + }
1016 + }
1017 +
1018 + // Wait for CodeMirror to fully render before searching
1019 + setTimeout(findAndHighlight, 300);
1020 + };
1021 +
1022 + // Logique pour la modale JS
1023 + let currentJSContent = "";
1024 + const AIBUI_COMBINED_PAGE_JS_MARKER = "/* === AI Builder: Page JS === */";
1025 + const AIBUI_COMBINED_BLOCKS_JS_MARKER = "/* === AI Builder: Blocks JS === */";
1026 +
1027 + // Initialiser les variables JS globales
1028 + window.aiBuilderPageJS = window.aiBuilderPageJS || "";
1029 + window.aiBuilderBlockJS = window.aiBuilderBlockJS || "";
1030 +
1031 + // Instance CodeMirror pour JS
1032 + let jsCodeMirror = null;
1033 +
1034 + // Initialiser CodeMirror pour JS quand il est prêt
1035 + function initJSCodeMirror() {
1036 + if (jsCodeMirror) return;
1037 + if (typeof CodeMirror === "undefined") {
1038 + setTimeout(initJSCodeMirror, 100);
1039 + return;
1040 + }
1041 + jsCodeMirror = CodeMirror.fromTextArea(jsEditor, {
1042 + mode: "javascript",
1043 + theme: "material-darker",
1044 + lineNumbers: true,
1045 + lineWrapping: true,
1046 + tabSize: 2,
1047 + indentWithTabs: false,
1048 + autoCloseBrackets: true,
1049 + viewportMargin: Infinity,
1050 + });
1051 + jsCodeMirror.setSize("100%", "400px");
1052 + // Assurer un minimum de 10 lignes visibles
1053 + ensureMinLines(jsCodeMirror, 10);
1054 + }
1055 +
1056 + // Ouvrir la modale JS
1057 + jsEditButton.onclick = () => {
1058 + jsModal.style.display = "flex";
1059 + initJSCodeMirror();
1060 + const combined = (
1061 + AIBUI_COMBINED_PAGE_JS_MARKER +
1062 + "\n" +
1063 + String(window.aiBuilderPageJS || "").trimEnd() +
1064 + "\n\n" +
1065 + AIBUI_COMBINED_BLOCKS_JS_MARKER +
1066 + "\n" +
1067 + String(window.aiBuilderBlockJS || "").trimEnd() +
1068 + "\n"
421 1069 );
1070 + if (jsCodeMirror) {
1071 + jsCodeMirror.setValue(combined);
1072 + setTimeout(() => jsCodeMirror.refresh(), 10);
1073 + } else {
1074 + jsEditor.value = combined;
1075 + }
1076 + setTimeout(() => {
1077 + if (jsCodeMirror) jsCodeMirror.focus();
1078 + }, 100);
1079 + };
1080 +
1081 + // Fermer la modale JS
1082 + function closeJSModal() {
1083 + jsModal.style.display = "none";
422 1084 }
423 1085
1086 + jsModalClose.onclick = closeJSModal;
1087 + jsCancelBtn.onclick = closeJSModal;
1088 +
1089 + // Sauvegarder le JS
1090 + jsSaveBtn.onclick = async () => {
1091 + const newJSContent = jsCodeMirror ? jsCodeMirror.getValue() : jsEditor.value;
1092 + const raw = String(newJSContent || "");
1093 + const iPage = raw.indexOf(AIBUI_COMBINED_PAGE_JS_MARKER);
1094 + const iBlocks = raw.indexOf(AIBUI_COMBINED_BLOCKS_JS_MARKER);
1095 + let pageJs = raw;
1096 + let blockJs = window.aiBuilderBlockJS || "";
1097 + if (iPage !== -1 && iBlocks !== -1 && iBlocks > iPage) {
1098 + const pageStart = iPage + AIBUI_COMBINED_PAGE_JS_MARKER.length;
1099 + const blocksStart = iBlocks + AIBUI_COMBINED_BLOCKS_JS_MARKER.length;
1100 + pageJs = raw.slice(pageStart, iBlocks).replace(/^\s*\n/, "");
1101 + blockJs = raw.slice(blocksStart).replace(/^\s*\n/, "");
1102 + }
1103 + window.aiBuilderPageJS = pageJs;
1104 + window.aiBuilderBlockJS = blockJs;
1105 +
1106 + // Save both storages (replace = true since it's a manual edit)
1107 + await saveJSInPostMeta(pageJs, "page", true);
1108 + await saveJSInPostMeta(blockJs, "block", true);
1109 +
1110 + // Recharger le JS combiné depuis le serveur après sauvegarde
1111 + await loadJSFromPostMeta();
1112 +
1113 + closeJSModal();
1114 + };
1115 +
1116 + // Fermer la modale JS en cliquant à l'extérieur
1117 + jsModal.onclick = (e) => {
1118 + if (e.target === jsModal) {
1119 + closeJSModal();
1120 + }
1121 + };
1122 +
1123 + /**
1124 + * The API sometimes returns the text payload outside of `attrs`. Move the
1125 + * known variants back onto the attributes so text doesn't disappear.
1126 + */
1127 + function normaliseBlockAttrs(block) {
1128 + const nextAttrs = { ...(block.attrs || {}) };
1129 +
1130 + if (typeof block.content === "string" && typeof nextAttrs.content !== "string") {
1131 + nextAttrs.content = block.content;
1132 + }
1133 + // Common variants some generators use.
1134 + if (typeof block.value === "string" && typeof nextAttrs.value !== "string") {
1135 + nextAttrs.value = block.value;
1136 + }
1137 + if (typeof block.values === "string" && typeof nextAttrs.values !== "string") {
1138 + nextAttrs.values = block.values;
1139 + }
1140 +
1141 + return nextAttrs;
1142 + }
1143 +
1144 + /**
1145 + * Turn an API block description into a Gutenberg block, without ever throwing.
1146 + *
1147 + * `wp.blocks.createBlock` raises on a block type that isn't registered on this
1148 + * install — a theme or plugin filtering `allowed_block_types_all`, or a block
1149 + * name the API returned that simply doesn't exist here. That single throw used
1150 + * to discard an entire (already billed) generation. Offending blocks are now
1151 + * skipped and collected in `failures` so we can name them to the user.
1152 + *
1153 + * @param {Object} block Raw block description from the API.
1154 + * @param {string[]} failures Mutated: names of the blocks we had to skip.
1155 + * @returns {Object|null} A Gutenberg block, or null if it could not be built.
1156 + */
1157 + function buildBlockSafe(block, failures) {
1158 + if (!block) return null;
1159 + const blockName = block.blockName || "(unnamed)";
1160 +
1161 + try {
1162 + if (!wp.blocks.getBlockType(blockName)) {
1163 + failures.push(blockName);
1164 + return null;
1165 + }
1166 + } catch (e) {
1167 + // getBlockType unavailable: fall through and let createBlock decide.
1168 + }
1169 +
1170 + const innerBlocks = (block.innerBlocks || [])
1171 + .map((child) => buildBlockSafe(child, failures))
1172 + .filter(Boolean);
1173 +
1174 + try {
1175 + return wp.blocks.createBlock(blockName, normaliseBlockAttrs(block), innerBlocks);
1176 + } catch (e) {
1177 + failures.push(blockName);
1178 + return null;
1179 + }
1180 + }
1181 +
1182 + function getAibuiAccountAdminUrl() {
1183 + const base =
1184 + typeof aiBuilderVars !== "undefined" && aiBuilderVars.adminBaseUrl
1185 + ? String(aiBuilderVars.adminBaseUrl).replace(/\/?$/, "/")
1186 + : "/wp-admin/";
1187 + return base + "admin.php?page=aibui-assistant";
1188 + }
1189 +
1190 + function getAibuiCreditsAdminUrl() {
1191 + const base =
1192 + typeof aiBuilderVars !== "undefined" && aiBuilderVars.adminBaseUrl
1193 + ? String(aiBuilderVars.adminBaseUrl).replace(/\/?$/, "/")
1194 + : "/wp-admin/";
1195 + return base + "admin.php?page=aibui-credits";
1196 + }
1197 +
1198 + function aibuiAuthRequiredError(message) {
1199 + const e = new Error(message);
1200 + e.aibuiAuthRequired = true;
1201 + return e;
1202 + }
1203 +
1204 + /**
1205 + * The WordPress side rejected us, not the AI service: an expired nonce or a
1206 + * cache/security layer swallowing admin-ajax. The cure is "reload the page",
1207 + * not "create an account", so it gets its own flag.
1208 + */
1209 + function aibuiSessionExpiredError(message) {
1210 + const e = new Error(message);
1211 + e.aibuiSessionExpired = true;
1212 + return e;
1213 + }
1214 +
1215 + /** HTML for in-chat message when the user must sign in / create an AI Builder account */
1216 + function getAccountRequiredChatMessageHtml() {
1217 + const url = getAibuiAccountAdminUrl().replace(/&/g, "&amp;");
1218 + return (
1219 + '<div style="display:flex;flex-direction:column;align-items:center;gap:12px;">' +
1220 + '<div style="line-height:1.55;color:inherit;">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.</div>' +
1221 + '<a href="' +
1222 + url +
1223 + '" target="_blank" rel="noopener noreferrer" style="display:inline-block;background:#00b87c;color:#fff;padding:8px 18px;border-radius:8px;text-decoration:none;font-weight:600;transition:background 0.2s;">Go to Account</a>' +
1224 + "</div>"
1225 + );
1226 + }
1227 +
1228 + /** HTML for in-chat message when the user is out of credits */
1229 + function getCreditsRequiredChatMessageHtml() {
1230 + const url = getAibuiCreditsAdminUrl().replace(/&/g, "&amp;");
1231 + return (
1232 + '<div style="display:flex;flex-direction:column;align-items:center;gap:12px;">' +
1233 + '<div style="line-height:1.55;color:inherit;">You don\'t have enough credits to generate more content. Open the credits page to purchase or manage your credits.</div>' +
1234 + '<a href="' +
1235 + url +
1236 + '" target="_blank" rel="noopener noreferrer" style="display:inline-block;background:#00b87c;color:#fff;padding:8px 18px;border-radius:8px;text-decoration:none;font-weight:600;transition:background 0.2s;">Go to Credits</a>' +
1237 + "</div>"
1238 + );
1239 + }
1240 +
424 1241 // Utilitaire pour récupérer le token JWT via AJAX WordPress
425 1242 async function getJwtToken() {
426 1243 if (!window.ajaxurl || !window.aiBuilderNonce) {
427 1244 console.warn("AI Builder: Missing AJAX URL or nonce");
428 1245 showAIMissingAccountToast();
429 - throw new Error("Missing AJAX URL or nonce");
1246 + throw aibuiAuthRequiredError("Missing AJAX URL or nonce");
430 1247 }
431 1248
1249 + let timeoutId;
432 1250 try {
1251 + // Create AbortController for timeout
1252 + const controller = new AbortController();
1253 + timeoutId = setTimeout(() => controller.abort(), 20000); // 20 second timeout
1254 +
433 1255 const res = await fetch(window.ajaxurl, {
434 1256 method: "POST",
435 1257 headers: { "Content-Type": "application/x-www-form-urlencoded" },
436 1258 body: `action=aibui_get_token&nonce=${window.aiBuilderNonce}`,
1259 + signal: controller.signal,
437 1260 });
438 1261
1262 + clearTimeout(timeoutId);
1263 +
439 1264 if (!res.ok) {
1265 + const bodyText = await res.text().catch(() => "");
1266 +
1267 + // A rejected nonce is not the same problem as "no account": say so, so
1268 + // the user reloads the page instead of hunting for a login they have.
1269 + if (res.status === 403 && /nonce|security check/i.test(bodyText)) {
1270 + throw aibuiSessionExpiredError("WordPress security nonce rejected");
1271 + }
1272 + // Handle 500 errors specifically
1273 + if (res.status === 500) {
1274 + throw new Error("Server error: The request took too long or encountered an error. Please try again.");
1275 + }
1276 + if (res.status === 401 || res.status === 403) {
1277 + showAIMissingAccountToast();
1278 + throw aibuiAuthRequiredError(
1279 + "You need to have an account and be logged in to use AI features."
1280 + );
1281 + }
440 1282 throw new Error(`HTTP error! status: ${res.status}`);
441 1283 }
442 1284
443 - const data = await res.json();
444 - console.log("token res: ", data);
1285 + let data;
1286 + try {
1287 + data = await res.json();
1288 + } catch (parseErr) {
1289 + // admin-ajax answered with something that isn't JSON: historically a
1290 + // wp_die() HTML page on an expired nonce, or a caching / security layer
1291 + // intercepting the call.
1292 + throw aibuiSessionExpiredError("admin-ajax returned a non-JSON response");
1293 + }
445 1294 if (data.success && data.data.token) {
446 1295 return data.data.token;
447 1296 }
448 1297 showAIMissingAccountToast();
449 - throw new Error(
1298 + throw aibuiAuthRequiredError(
450 1299 "You need to have an account and be logged in to use AI features."
451 1300 );
452 1301 } catch (error) {
1302 + if (timeoutId) {
1303 + clearTimeout(timeoutId);
1304 + }
453 1305 console.error("Error fetching JWT token:", error);
1306 +
1307 + // Handle timeout/abort errors
1308 + if (error.name === 'AbortError' || error.message.includes('timeout')) {
1309 + throw new Error("Request timeout: The server took too long to respond. Please check your connection and try again.");
1310 + }
1311 +
1312 + if (error.aibuiAuthRequired) {
1313 + throw error;
1314 + }
1315 +
1316 + // Expired WordPress session: the caller renders its own explanation, and
1317 + // the "create an account" toast would be actively misleading here.
1318 + if (error.aibuiSessionExpired) {
1319 + throw error;
1320 + }
1321 +
454 1322 showAIMissingAccountToast();
455 1323 throw error;
456 1324 }
457 1325 }
@@ -458,14 +1326,15 @@
458 1326
459 1327 // Fonction utilitaire pour afficher un toast UX si l'utilisateur n'a pas de compte/connexion
460 1328 function showAIMissingAccountToast() {
461 1329 if (document.getElementById("ai-missing-account-toast")) return; // Pas de doublon
1330 + const accountUrl = getAibuiAccountAdminUrl();
462 1331 const toast = document.createElement("div");
463 1332 toast.id = "ai-missing-account-toast";
464 1333 toast.innerHTML = `
465 1334 <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 1335 <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>
1336 + <a href="${accountUrl}" style='background:#00b87c;color:#fff;padding:8px 18px;border-radius:8px;text-decoration:none;font-weight:600;transition:background 0.2s;' target='_blank' rel="noopener noreferrer">Go to Account</a>
468 1337 </div>
469 1338 `;
470 1339 document.body.appendChild(toast);
471 1340 setTimeout(() => {
@@ -500,9 +1369,9 @@
500 1369 creditsElem.id = "ai-credits-display";
501 1370 creditsElem.style.fontSize = "12px";
502 1371 creditsElem.style.color = "#cccccc";
503 1372 creditsElem.style.marginTop = "2px";
504 - creditsElem.style.textAlign = "center";
1373 + creditsElem.style.textAlign = "left";
505 1374 header.appendChild(creditsElem);
506 1375 }
507 1376 }
508 1377 if (creditsElem) {
@@ -531,10 +1400,16 @@
531 1400 window.updateAICreditsDisplay = updateCreditsDisplay;
532 1401
533 1402 // Fonction pour charger les crédits utilisateur
534 1403 async function loadUserCredits() {
1404 + let timeoutId;
535 1405 try {
536 1406 const jwtToken = await getJwtToken();
1407 +
1408 + // Create AbortController for timeout
1409 + const controller = new AbortController();
1410 + timeoutId = setTimeout(() => controller.abort(), 20000); // 20 second timeout
1411 +
537 1412 const res = await fetch(window.config.apiUrl + "/user/profile", {
538 1413 method: "GET",
539 1414 headers: {
540 1415 Authorization: `Bearer ${jwtToken}`,
@@ -539,10 +1414,20 @@
539 1414 headers: {
540 1415 Authorization: `Bearer ${jwtToken}`,
541 1416 "Content-Type": "application/json",
542 1417 },
1418 + signal: controller.signal,
543 1419 });
544 - if (!res.ok) throw new Error("Failed to load profile");
1420 +
1421 + clearTimeout(timeoutId);
1422 +
1423 + if (!res.ok) {
1424 + if (res.status === 500) {
1425 + throw new Error("Server error: The request took too long or encountered an error.");
1426 + }
1427 + throw new Error("Failed to load profile");
1428 + }
1429 +
545 1430 const data = await res.json();
546 1431 const aiCredits = data.user?.aiCredits || {};
547 1432 const totalCredits =
548 1433 (aiCredits.onAccountCreation || 0) +
@@ -550,27 +1435,145 @@
550 1435 (aiCredits.paid || 0);
551 1436 console.log("Credits loaded:", totalCredits, "from:", aiCredits);
552 1437 updateCreditsDisplay(totalCredits);
553 1438 } catch (e) {
1439 + if (timeoutId) {
1440 + clearTimeout(timeoutId);
1441 + }
554 1442 console.error("Error loading credits:", e);
1443 +
1444 + // Handle timeout/abort errors
1445 + if (e.name === 'AbortError' || e.message.includes('timeout')) {
1446 + console.warn("Credits loading timeout - will retry on next action");
1447 + }
1448 +
555 1449 updateCreditsDisplay(null);
556 1450 }
557 1451 }
558 1452
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);
1453 + // Récupère les documents cibles où injecter un style/script côté éditeur.
1454 + // - document principal (éditeur classique post.php/post-new.php)
1455 + // - contentDocument de l'iframe du canvas (Site Editor, FSE : template/template parts)
1456 + // Retourne un tableau de Document. Silencieux en cas d'erreur cross-origin.
1457 + function getEditorInjectionDocuments() {
1458 + const docs = [document];
1459 + try {
1460 + // Site Editor : l'iframe a name="editor-canvas" en WP 6.x.
1461 + // Fallbacks : classes WP connues puis toutes les iframes visibles.
1462 + const selectors = [
1463 + 'iframe[name="editor-canvas"]',
1464 + '.edit-site-visual-editor iframe',
1465 + '.block-editor-iframe__iframe',
1466 + '.editor-canvas__iframe',
1467 + ];
1468 + const seen = new Set();
1469 + for (const sel of selectors) {
1470 + const nodes = document.querySelectorAll(sel);
1471 + nodes.forEach((frame) => {
1472 + try {
1473 + const d = frame.contentDocument;
1474 + if (d && d.head && !seen.has(d)) {
1475 + seen.add(d);
1476 + docs.push(d);
1477 + }
1478 + } catch (e) {
1479 + // cross-origin / not ready : on ignore silencieusement
1480 + }
1481 + });
1482 + }
1483 + } catch (e) {
1484 + // on garde au moins le document principal
1485 + }
1486 + return docs;
1487 + }
564 1488
565 - if (!styleElement) {
566 - styleElement = document.createElement("style");
567 - styleElement.id = styleId;
568 - styleElement.type = "text/css";
569 - document.head.appendChild(styleElement);
1489 + // Crée/mets à jour un <style id> ou <script id> dans chaque document cible.
1490 + function upsertTagInDocs(tagName, tagId, content) {
1491 + const docs = getEditorInjectionDocuments();
1492 + docs.forEach((doc) => {
1493 + try {
1494 + if (!doc || !doc.head) return;
1495 + let el = doc.getElementById(tagId);
1496 + if (!el) {
1497 + el = doc.createElement(tagName);
1498 + el.id = tagId;
1499 + if (tagName === "style") {
1500 + el.type = "text/css";
1501 + } else {
1502 + el.type = "text/javascript";
1503 + }
1504 + doc.head.appendChild(el);
1505 + }
1506 + el.textContent = content || "";
1507 + } catch (e) {
1508 + // injection best-effort : on ignore
1509 + }
1510 + });
1511 + }
1512 +
1513 + // Cache du dernier CSS/JS injecté pour pouvoir réinjecter automatiquement
1514 + // quand l'iframe du Site Editor est recréée (changement de template, etc.).
1515 + let lastInjectedEditorCSS = "";
1516 + let lastInjectedEditorJS = "";
1517 +
1518 + // Détecte la "cible" de sauvegarde (page, article, template, template part).
1519 + // Retourne toujours un objet utilisable :
1520 + // { postId, templateId, templateType }
1521 + // - postId : numérique si dispo (pages/articles, templates déjà en base)
1522 + // ou la valeur brute retournée par core/editor sinon ("")
1523 + // - templateId : "theme//slug" quand on édite un wp_template ou wp_template_part
1524 + // - templateType: "wp_template" | "wp_template_part" | ""
1525 + //
1526 + // Backend : si templateId+templateType sont fournis et que postId n'est pas
1527 + // numérique, l'ajax handler résout/crée le post correspondant.
1528 + function getEditorTargetContext() {
1529 + const ctx = { postId: "", templateId: "", templateType: "" };
1530 + try {
1531 + const sel = wp && wp.data && wp.data.select ? wp.data.select("core/editor") : null;
1532 + if (!sel) return ctx;
1533 +
1534 + const rawId = sel.getCurrentPostId ? sel.getCurrentPostId() : null;
1535 + const postType = sel.getCurrentPostType ? sel.getCurrentPostType() : null;
1536 +
1537 + // Type template ?
1538 + if (postType === "wp_template" || postType === "wp_template_part") {
1539 + ctx.templateType = postType;
1540 + // Dans le Site Editor, rawId peut être :
1541 + // - numérique (template déjà matérialisé en base)
1542 + // - string "theme//slug" (template file-based pas encore sauvé)
1543 + if (typeof rawId === "string" && rawId.indexOf("//") !== -1) {
1544 + ctx.templateId = rawId;
1545 + ctx.postId = ""; // pas de postId numérique utile
1546 + } else if (typeof rawId === "number" || (typeof rawId === "string" && /^\d+$/.test(rawId))) {
1547 + ctx.postId = rawId;
1548 + // On renseigne quand même templateId/type pour que le backend puisse
1549 + // retomber dessus si le post a été supprimé entre-temps.
1550 + try {
1551 + const entity = wp.data.select("core").getEditedEntityRecord("postType", postType, rawId);
1552 + if (entity && typeof entity.id === "string" && entity.id.indexOf("//") !== -1) {
1553 + ctx.templateId = entity.id;
1554 + }
1555 + } catch (e) {
1556 + /* ignore */
1557 + }
1558 + }
1559 + return ctx;
1560 + }
1561 +
1562 + // Cas standard (page, article, etc.)
1563 + if (typeof rawId === "number" || (typeof rawId === "string" && /^\d+$/.test(rawId))) {
1564 + ctx.postId = rawId;
1565 + }
1566 + } catch (e) {
1567 + /* ignore */
570 1568 }
1569 + return ctx;
1570 + }
571 1571
572 - styleElement.textContent = cssContent;
1572 + // Fonction pour injecter le CSS dans l'éditeur WordPress
1573 + function injectCSSInEditor(cssContent) {
1574 + lastInjectedEditorCSS = cssContent || "";
1575 + upsertTagInDocs("style", "ai-builder-editor-css", lastInjectedEditorCSS);
573 1576 }
574 1577
575 1578 // Fonction pour injecter le CSS dans le frontend
576 1579 function injectCSSInFrontend(cssContent) {
@@ -588,30 +1591,213 @@
588 1591 styleElement.textContent = cssContent;
589 1592 }
590 1593
591 1594 // Fonction pour sauvegarder le CSS dans les meta du post via AJAX WordPress
592 - async function saveCSSInPostMeta(cssContent, cssType = "page") {
1595 + async function saveCSSInPostMeta(cssContent, cssType = "page", replace = false) {
593 1596 try {
594 - const postId = wp.data.select("core/editor").getCurrentPostId();
1597 + const ctx = getEditorTargetContext();
595 1598
596 1599 const formData = new FormData();
597 1600 formData.append("action", "aibui_save_post_css");
598 1601 formData.append("nonce", window.aiBuilderNonce);
599 - formData.append("post_id", postId);
1602 + formData.append("post_id", ctx.postId || "");
1603 + if (ctx.templateId) formData.append("template_id", ctx.templateId);
1604 + if (ctx.templateType) formData.append("template_type", ctx.templateType);
600 1605 formData.append("css_content", cssContent);
601 1606 formData.append("css_type", cssType);
1607 + formData.append("replace", replace ? "1" : "0");
602 1608
603 - await fetch(window.ajaxurl, {
1609 + const res = await fetch(window.ajaxurl, {
604 1610 method: "POST",
605 1611 body: formData,
606 1612 });
1613 + if (res.ok) {
1614 + try {
1615 + const data = await res.json();
1616 + if (data && data.success === false && data.data) {
1617 + console.warn("AI Builder: CSS save error:", data.data);
1618 + }
1619 + } catch (e) {
1620 + /* ignore parse */
1621 + }
1622 + }
607 1623 } catch (err) {
608 1624 console.error("Error saving CSS to post meta:", err);
609 1625 }
610 1626 }
611 1627
1628 + // Fonction pour injecter le JS dans l'éditeur WordPress
1629 + function injectJSInEditor(jsContent) {
1630 + lastInjectedEditorJS = jsContent || "";
1631 + // On wrappe dans un IIFE protégé pour qu'un script cassé n'arrête pas
1632 + // l'éditeur. Identique à l'isolation appliquée côté frontend PHP.
1633 + const wrapped = lastInjectedEditorJS
1634 + ? "(function(){ try{ " +
1635 + lastInjectedEditorJS +
1636 + " } catch(e){ if (window.console) console.error('ai-builder editor JS', e); } })();"
1637 + : "";
1638 + upsertTagInDocs("script", "ai-builder-editor-js", wrapped);
1639 + }
612 1640
1641 + // Réinjection automatique quand une iframe du canvas apparaît ou est
1642 + // remplacée (changement de template dans le Site Editor). On utilise une
1643 + // seule MutationObserver globale, en DOM principal.
1644 + (function setupCanvasIframeRebindObserver() {
1645 + try {
1646 + if (typeof MutationObserver === "undefined") return;
1647 + let scheduled = false;
1648 + const reinject = () => {
1649 + scheduled = false;
1650 + try {
1651 + if (lastInjectedEditorCSS) {
1652 + upsertTagInDocs(
1653 + "style",
1654 + "ai-builder-editor-css",
1655 + lastInjectedEditorCSS
1656 + );
1657 + }
1658 + if (lastInjectedEditorJS) {
1659 + const wrapped =
1660 + "(function(){ try{ " +
1661 + lastInjectedEditorJS +
1662 + " } catch(e){ if (window.console) console.error('ai-builder editor JS', e); } })();";
1663 + upsertTagInDocs("script", "ai-builder-editor-js", wrapped);
1664 + }
1665 + } catch (e) {
1666 + // silencieux
1667 + }
1668 + };
1669 + const schedule = () => {
1670 + if (scheduled) return;
1671 + scheduled = true;
1672 + // rAF + timeout pour laisser l'iframe finir son load
1673 + requestAnimationFrame(() => setTimeout(reinject, 50));
1674 + };
1675 + const observer = new MutationObserver((mutations) => {
1676 + for (const m of mutations) {
1677 + if (!m.addedNodes || m.addedNodes.length === 0) continue;
1678 + for (const n of m.addedNodes) {
1679 + if (n && n.nodeType === 1) {
1680 + if (
1681 + n.tagName === "IFRAME" ||
1682 + (n.querySelector && n.querySelector("iframe"))
1683 + ) {
1684 + schedule();
1685 + return;
1686 + }
1687 + }
1688 + }
1689 + }
1690 + });
1691 + observer.observe(document.documentElement || document.body, {
1692 + childList: true,
1693 + subtree: true,
1694 + });
1695 + } catch (e) {
1696 + // silencieux : injection restera limitée au document principal
1697 + }
1698 + })();
613 1699
1700 + // Fonction pour injecter le JS dans le frontend
1701 + function injectJSInFrontend(jsContent) {
1702 + // Créer un script tag pour le frontend
1703 + const scriptId = "ai-builder-frontend-js";
1704 + let scriptElement = document.getElementById(scriptId);
1705 +
1706 + if (!scriptElement) {
1707 + scriptElement = document.createElement("script");
1708 + scriptElement.id = scriptId;
1709 + scriptElement.type = "text/javascript";
1710 + document.head.appendChild(scriptElement);
1711 + }
1712 +
1713 + scriptElement.textContent = jsContent;
1714 + }
1715 +
1716 + // Fonction pour sauvegarder le JS dans les meta du post via AJAX WordPress
1717 + async function saveJSInPostMeta(jsContent, jsType = "page", replace = false) {
1718 + try {
1719 + const ctx = getEditorTargetContext();
1720 +
1721 + const formData = new FormData();
1722 + formData.append("action", "aibui_save_post_js");
1723 + formData.append("nonce", window.aiBuilderNonce);
1724 + formData.append("post_id", ctx.postId || "");
1725 + if (ctx.templateId) formData.append("template_id", ctx.templateId);
1726 + if (ctx.templateType) formData.append("template_type", ctx.templateType);
1727 + formData.append("js_content", jsContent);
1728 + formData.append("js_type", jsType);
1729 + formData.append("replace", replace ? "1" : "0");
1730 +
1731 + const res = await fetch(window.ajaxurl, {
1732 + method: "POST",
1733 + body: formData,
1734 + });
1735 + if (res.ok) {
1736 + try {
1737 + const data = await res.json();
1738 + if (data && data.success === false && data.data) {
1739 + console.warn("AI Builder: JS save error:", data.data);
1740 + }
1741 + } catch (e) {
1742 + /* ignore parse */
1743 + }
1744 + }
1745 + } catch (err) {
1746 + console.error("Error saving JS to post meta:", err);
1747 + }
1748 + }
1749 +
1750 + // Fonction pour charger le JS depuis les meta du post via AJAX WordPress
1751 + async function loadJSFromPostMeta() {
1752 + try {
1753 + const ctx = getEditorTargetContext();
1754 +
1755 + const formData = new FormData();
1756 + formData.append("action", "aibui_get_post_js");
1757 + formData.append("nonce", window.aiBuilderNonce);
1758 + formData.append("post_id", ctx.postId || "");
1759 + if (ctx.templateId) formData.append("template_id", ctx.templateId);
1760 + if (ctx.templateType) formData.append("template_type", ctx.templateType);
1761 +
1762 + const res = await fetch(window.ajaxurl, {
1763 + method: "POST",
1764 + body: formData,
1765 + });
1766 +
1767 + if (res.ok) {
1768 + const data = await res.json();
1769 + if (data.success && data.data) {
1770 + // Stocker les JS séparément
1771 + window.aiBuilderPageJS = data.data.pageJS || "";
1772 + window.aiBuilderBlockJS = data.data.blockJS || "";
1773 +
1774 + // Utiliser le JS combiné pour l'affichage
1775 + const combinedJS = data.data.combinedJS || "";
1776 + currentJSContent = combinedJS;
1777 +
1778 + // Injecter le JS dans l'éditeur et le frontend
1779 + injectJSInEditor(combinedJS);
1780 + injectJSInFrontend(combinedJS);
1781 +
1782 + // Les boutons JS et CSS sont toujours visibles
1783 +
1784 + console.log(
1785 + "JS loaded - Page:",
1786 + window.aiBuilderPageJS.length,
1787 + "chars, Blocks:",
1788 + window.aiBuilderBlockJS.length,
1789 + "chars"
1790 + );
1791 + }
1792 + }
1793 + } catch (err) {
1794 + console.error("Error loading JS from post meta:", err);
1795 + }
1796 + }
1797 +
1798 +
1799 +
614 1800 // Set the meta description field by id once (no retry)
615 1801 async function setMetaDescriptionField(value) {
616 1802 const el = document.getElementById('aibui_meta_description_field');
617 1803 if (!el) return false;
@@ -740,14 +1926,16 @@
740 1926
741 1927 // Fonction pour charger le CSS depuis les meta du post via AJAX WordPress
742 1928 async function loadCSSFromPostMeta() {
743 1929 try {
744 - const postId = wp.data.select("core/editor").getCurrentPostId();
1930 + const ctx = getEditorTargetContext();
745 1931
746 1932 const formData = new FormData();
747 1933 formData.append("action", "aibui_get_post_css");
748 1934 formData.append("nonce", window.aiBuilderNonce);
749 - formData.append("post_id", postId);
1935 + formData.append("post_id", ctx.postId || "");
1936 + if (ctx.templateId) formData.append("template_id", ctx.templateId);
1937 + if (ctx.templateType) formData.append("template_type", ctx.templateType);
750 1938
751 1939 const res = await fetch(window.ajaxurl, {
752 1940 method: "POST",
753 1941 body: formData,
@@ -767,12 +1955,9 @@
767 1955 // Injecter le CSS dans l'éditeur et le frontend
768 1956 injectCSSInEditor(combinedCSS);
769 1957 injectCSSInFrontend(combinedCSS);
770 1958
771 - // Afficher le bouton CSS s'il y a du CSS
772 - if (combinedCSS.trim()) {
773 - cssEditButton.style.display = "block";
774 - }
1959 + // Les boutons CSS et JS sont toujours visibles
775 1960
776 1961 console.log(
777 1962 "CSS loaded - Page:",
778 1963 window.aiBuilderPageCSS.length,
@@ -802,15 +1987,206 @@
802 1987
803 1988 const postId = await waitForCurrentPostId();
804 1989 if (postId) {
805 1990 loadCSSFromPostMeta();
1991 + loadJSFromPostMeta();
806 1992 } else {
807 1993 console.warn(
808 - "AI Builder: unable to resolve current post ID to load CSS."
1994 + "AI Builder: unable to resolve current post ID to load CSS/JS."
809 1995 );
810 1996 }
811 1997 })();
812 1998
1999 + /**
2000 + * Build a compact structural summary of the current page for AI context.
2001 + *
2002 + * The summary describes the existing blocks, custom CSS and JS so the AI
2003 + * understands what is already on the page without receiving every raw byte.
2004 + * The result is capped at PAGE_CONTEXT_MAX_CHARS characters so the token
2005 + * overhead remains predictable.
2006 + *
2007 + * Format (plain text, easy to read by the LLM):
2008 + *
2009 + * Page title: <title>
2010 + * Post type: page | post | ...
2011 + * Blocks (N total):
2012 + * [0] core/group (align:full) > core/heading(1): "Hero title" | core/paragraph: "Subtitle…"
2013 + * [1] core/columns > col0: core/image | col1: core/paragraph: "Team member…"
2014 + * ...
2015 + * Custom CSS: yes (<N> chars) | none
2016 + * Custom JS: yes (<N> chars) | none
2017 + */
2018 + const PAGE_CONTEXT_MAX_CHARS = 3000;
2019 +
2020 + function buildPageContext() {
2021 + try {
2022 + if (
2023 + !window.wp ||
2024 + !wp.data ||
2025 + !wp.data.select("core/block-editor") ||
2026 + !wp.data.select("core/editor")
2027 + ) {
2028 + return "";
2029 + }
2030 +
2031 + const title =
2032 + wp.data.select("core/editor").getEditedPostAttribute("title") || "";
2033 + const postType =
2034 + wp.data.select("core/editor").getCurrentPostType() || "";
2035 + const blocks = wp.data.select("core/block-editor").getBlocks() || [];
2036 +
2037 + // --- CSS hint (length only, not the full content) ---
2038 + const cssLength =
2039 + (window.aiBuilderPageCSS ? window.aiBuilderPageCSS.length : 0) +
2040 + (window.aiBuilderBlockCSS ? window.aiBuilderBlockCSS.length : 0);
2041 +
2042 + // --- JS hint: length + function names ---
2043 + const jsStyleEl = document.getElementById("ai-builder-editor-js");
2044 + const jsSource = jsStyleEl ? (jsStyleEl.textContent || "") : "";
2045 + const jsLength = jsSource.length;
2046 +
2047 + // Extract declared function names (named functions, arrow/const/let/var assignments)
2048 + const jsFunctionNames = [];
2049 + if (jsLength > 0) {
2050 + const fnPatterns = [
2051 + /\bfunction\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g,
2052 + /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>|\w+\s*=>)/g,
2053 + ];
2054 + for (const re of fnPatterns) {
2055 + let m;
2056 + while ((m = re.exec(jsSource)) !== null) {
2057 + if (!jsFunctionNames.includes(m[1])) {
2058 + jsFunctionNames.push(m[1]);
2059 + }
2060 + }
2061 + }
2062 + }
2063 +
2064 + // --- Block tree summariser ---
2065 + function summariseBlock(block, depth) {
2066 + if (!block || !block.name) return "";
2067 +
2068 + const name = block.name; // e.g. "core/heading"
2069 + const attrs = block.attributes || {};
2070 +
2071 + // Extract the most useful attributes compactly
2072 + const attrParts = [];
2073 + if (attrs.level) attrParts.push("h" + attrs.level);
2074 + if (attrs.align) attrParts.push("align:" + attrs.align);
2075 + if (attrs.textAlign) attrParts.push("textAlign:" + attrs.textAlign);
2076 + if (attrs.layout && attrs.layout.type)
2077 + attrParts.push("layout:" + attrs.layout.type);
2078 + if (attrs.className) attrParts.push("." + attrs.className);
2079 + if (attrs.style && attrs.style.color && attrs.style.color.background)
2080 + attrParts.push("bg:" + attrs.style.color.background);
2081 +
2082 + // Extra detail for core/cover: overlay color and gradient
2083 + if (name === "core/cover") {
2084 + if (attrs.customGradient) {
2085 + attrParts.push("gradient:" + attrs.customGradient);
2086 + } else if (attrs.gradient) {
2087 + attrParts.push("gradient-slug:" + attrs.gradient);
2088 + }
2089 + if (attrs.customOverlayColor) {
2090 + attrParts.push("overlay:" + attrs.customOverlayColor);
2091 + } else if (attrs.overlayColor) {
2092 + attrParts.push("overlay-slug:" + attrs.overlayColor);
2093 + }
2094 + if (typeof attrs.dimRatio === "number") {
2095 + attrParts.push("dim:" + attrs.dimRatio + "%");
2096 + }
2097 + }
2098 +
2099 + const attrStr = attrParts.length ? "(" + attrParts.join(",") + ")" : "";
2100 +
2101 + // Extract visible text content (first 80 chars)
2102 + let textHint = "";
2103 + if (attrs.content) {
2104 + const raw = String(attrs.content).replace(/<[^>]*>/g, "");
2105 + if (raw.trim()) textHint = ': "' + raw.trim().slice(0, 80) + '"';
2106 + } else if (attrs.value) {
2107 + const raw = String(attrs.value).replace(/<[^>]*>/g, "");
2108 + if (raw.trim()) textHint = ': "' + raw.trim().slice(0, 80) + '"';
2109 + } else if (attrs.text) {
2110 + const raw = String(attrs.text).replace(/<[^>]*>/g, "");
2111 + if (raw.trim()) textHint = ': "' + raw.trim().slice(0, 80) + '"';
2112 + } else if (attrs.alt) {
2113 + textHint = ': [img alt="' + String(attrs.alt).slice(0, 60) + '"]';
2114 + } else if (attrs.url && name === "core/image") {
2115 + textHint = ": [image]";
2116 + }
2117 +
2118 + const shortName = name.replace("core/", "").replace("ai-builder/", "aibui/");
2119 + let line = shortName + attrStr + textHint;
2120 +
2121 + // Recurse into inner blocks (max depth 3 to stay compact)
2122 + if (
2123 + Array.isArray(block.innerBlocks) &&
2124 + block.innerBlocks.length > 0 &&
2125 + depth < 3
2126 + ) {
2127 + const children = block.innerBlocks
2128 + .map((b) => summariseBlock(b, depth + 1))
2129 + .filter(Boolean)
2130 + .join(" | ");
2131 + if (children) line += " > " + children;
2132 + }
2133 +
2134 + return line;
2135 + }
2136 +
2137 + // Build block list lines
2138 + const blockLines = blocks.map((block, idx) => {
2139 + const summary = summariseBlock(block, 0);
2140 + return "[" + idx + "] " + summary;
2141 + });
2142 +
2143 + const cssHint =
2144 + cssLength > 0 ? "yes (" + cssLength + " chars)" : "none";
2145 + let jsHint = "none";
2146 + if (jsLength > 0) {
2147 + jsHint = "yes (" + jsLength + " chars)";
2148 + if (jsFunctionNames.length > 0) {
2149 + jsHint += " — functions: " + jsFunctionNames.join(", ");
2150 + }
2151 + }
2152 +
2153 + let context =
2154 + "Page title: " +
2155 + title +
2156 + "\n" +
2157 + "Post type: " +
2158 + postType +
2159 + "\n" +
2160 + "Blocks (" +
2161 + blocks.length +
2162 + " total):\n" +
2163 + blockLines.join("\n") +
2164 + "\n" +
2165 + "Custom CSS: " +
2166 + cssHint +
2167 + "\n" +
2168 + "Custom JS: " +
2169 + jsHint;
2170 +
2171 + // Cap to PAGE_CONTEXT_MAX_CHARS
2172 + if (context.length > PAGE_CONTEXT_MAX_CHARS) {
2173 + context =
2174 + context.slice(0, PAGE_CONTEXT_MAX_CHARS - 40) +
2175 + "\n[... context truncated at " +
2176 + PAGE_CONTEXT_MAX_CHARS +
2177 + " chars]";
2178 + }
2179 +
2180 + console.log("context: ", context);
2181 +
2182 + return context;
2183 + } catch (e) {
2184 + console.warn("AI Builder: could not build page context", e);
2185 + return "";
2186 + }
2187 + }
2188 +
813 2189 function getPatternName() {
814 2190 try {
815 2191 const isPatternEditor = (typeof aiBuilderVars !== 'undefined' && !!aiBuilderVars.isPatternEditor) || window.location.pathname.includes('site-editor.php');
816 2192 if (!isPatternEditor) return '';
@@ -824,13 +2200,1276 @@
824 2200 return '';
825 2201 }
826 2202 }
827 2203
2204 + // --- Visual style presets for the chat widget ---
2205 + // Style preview images live in WordPress Media Library (same folder / filenames).
2206 + const CHAT_STYLES_CDN =
2207 + "https://website-ai-builder.com/wp-content/uploads/2026/04/";
2208 +
2209 + const CHAT_VISUAL_STYLES = [
2210 + {
2211 + id: "neobrutalism",
2212 + label: "Neobrutalism",
2213 + image: CHAT_STYLES_CDN + "neobrutalism.jpg",
2214 + prompt: `Build a SaaS landing page in strict neobrutalism. Every card, button, and image has a 3px solid black border plus a hard offset shadow (6px 6px, no blur, solid black). Flat fills only, no gradients. Palette: bright yellow #FFD700, hot pink #FF6B9D, electric blue #4361EE on cream #FAF9F6. Massive bold sans-serif headlines in Archivo Black or Space Grotesk, oversized. Asymmetric layout with generous 32px padding. Sections: chunky hero with huge heading, 3 feature cards in a row. Corners square or 8px max.`,
2215 + },
2216 + {
2217 + id: "premium-dark",
2218 + label: "Premium Dark Mode",
2219 + image: CHAT_STYLES_CDN + "premium-dark.jpg",
2220 + prompt: `Design a premium SaaS landing page in dark mode. Background near-black #0A0A0B with subtle section variation #141416. Single accent: vibrant gradient violet #8B5CF6 to pink #EC4899, used only on primary CTAs and headline highlights. Soft glow under buttons. Text white #F5F5F7 primary, muted #94949C secondary. Inter or Geist font, tight letter-spacing -0.02em. Hero with floating dashboard mockup, 3 minimal feature cards with 1px border #27272A and dark fill #111114, logo cloud, testimonial, pricing table with middle plan glow-highlighted.`,
2221 + },
2222 + {
2223 + id: "modern-minimalist",
2224 + label: "Modern Minimalist",
2225 + image: CHAT_STYLES_CDN + "modern-mini.jpg",
2226 + prompt: `Create a minimalist landing page with extreme whitespace. Pure white background #FFFFFF, single accent deep navy #0F172A. Inter or Helvetica Neue: body 16px regular, headlines 64-80px in light weight 300. No borders, no shadows, no gradients, no illustrations. Hero with one huge headline, one-line subtitle, one black pill button. 3 feature blocks separated by thin 1px hairlines #E5E5E5, no cards. Typographic testimonial as a single quote. Pricing as 3 plain columns separated by hairlines. Vertical spacing 120px+ between every section.`,
2227 + },
2228 + {
2229 + id: "editorial-typography",
2230 + label: "Editorial Typography",
2231 + image: CHAT_STYLES_CDN + "edito.jpg",
2232 + prompt: `Design a magazine-style editorial landing page. Serif display headlines in Playfair Display or Fraunces, 80-120px, mixing weights and italic. Body in Inter or Source Serif. Asymmetric grid with large drop caps, oversized italic pull quotes, occasional 2-column text. Off-white background #FBFAF7, ink #1A1A1A, single ochre accent #B8860B. Hero with massive headline broken across 3 lines plus a byline. Sections framed like articles with column rules, captions in small caps, classic page-number style markers, generous outer margins.`,
2233 + },
2234 + {
2235 + id: "organic-nature",
2236 + label: "Organic & Nature",
2237 + image: CHAT_STYLES_CDN + "orga-nat.jpg",
2238 + prompt: `Build an organic, nature-inspired landing page. Earthy palette: sage green #87A878, warm cream #F5EFE0, terracotta #C97D5D, deep forest #2F4F2F. Soft rounded corners 16-24px everywhere, wavy SVG dividers between sections. Cormorant or Lora for headlines, Nunito for body. Botanical leaf SVG accents in corners. Hero with curved background shape and a nature photo right. Feature cards with rounded blob backgrounds and gentle shadows. Testimonial section with circular avatars. Pricing in soft cream cards with terracotta CTAs. No sharp edges anywhere.`,
2239 + },
2240 + {
2241 + id: "retrofuturism",
2242 + label: "Retrofuturism",
2243 + image: CHAT_STYLES_CDN + "retrofut.jpg",
2244 + prompt: `Create an 80s retrofuturism synthwave landing page. Dark navy #0B0B2E with a perspective neon grid floor in magenta #FF006E and cyan #00F5FF. Headlines in chrome or magenta-to-purple gradient with soft pink glow. Display font Monoton, Audiowide or Orbitron. Sun/horizon imagery, scanline overlay, geometric triangles, palm tree silhouettes. Hero with massive glowing headline over the grid. Feature cards with neon 2px borders and dark fills #1A0B3E. CTAs with chrome gradient and pink outer glow. Pricing table with neon-outlined plans, VHS-style testimonial section.`,
2245 + },
2246 + {
2247 + id: "saas-tech",
2248 + label: "SaaS / Tech Startup",
2249 + image: CHAT_STYLES_CDN + "saas-tech.jpg",
2250 + prompt: `Design a clean modern SaaS landing page. White background #FFFFFF, primary indigo #4F46E5, secondary mint #10B981, neutral gray scale. Inter font throughout, headlines 48-64px weight 600. Hero: large headline left, animated product screenshot right, indigo CTA button with subtle shadow. Logo cloud bar below. 3-column features with small colored icon, bold title, gray description. Centered testimonial carousel with circular avatars. Pricing table 3 plans, middle highlighted with indigo border and "Most popular" badge. FAQ accordion. Rounded 12px corners, soft shadows, subtle gradient feature backgrounds.`,
2251 + },
2252 + {
2253 + id: "luxury",
2254 + label: "Luxury & High-End",
2255 + image: CHAT_STYLES_CDN + "luxury.jpg",
2256 + prompt: `Build a luxury brand landing page. Palette: matte black #0E0E0E, ivory #F5F1E8, brushed gold #C5A572. Generous whitespace, no clutter. Headlines in Didot, Bodoni or Italiana at 96px+ in weight 300 with wide letter-spacing 0.05em. Body in Cormorant Garamond or Montserrat weight 200. Hero: full-bleed editorial image with minimal headline overlay and a thin-outline gold CTA button. Sections divided by thin 1px gold hairlines. Editorial image grids. Testimonial as a single italic serif quote centered. No icons, no shadows, no gradients.`,
2257 + },
2258 + {
2259 + id: "bento-grid",
2260 + label: "Bento Grid",
2261 + image: CHAT_STYLES_CDN + "bento.jpg",
2262 + prompt: `Create a modern landing page using a bento grid layout. Hero is one large rounded card with the headline. Below: a 4-column bento grid with cards of varying spans — some 2x2, some 1x1, some 2x1 — each rounded 20px with consistent 16px gaps. Background #FAFAFA, white cards #FFFFFF with 0.5px border #E5E5E5 and very subtle shadow. Each card showcases one feature: large icon or screenshot, short bold headline, one-line description. Mix in 2-3 dark cards (#0F172A with white text) for rhythm. Inter font, asymmetric balance, glanceable.`,
2263 + },
2264 + {
2265 + id: "y2k-revival",
2266 + label: "Y2K Revival",
2267 + image: CHAT_STYLES_CDN + "y2k.jpg",
2268 + prompt: `Design a Y2K revival landing page. Glossy chrome and bubble aesthetic. Palette: chrome silver gradients, baby blue #A8DADC, bubblegum pink #FFB6D9, lime green #C8FF6B, lilac #C8A4D4. Bubbly rounded buttons with chrome gradient and inner highlight. Display font with bubble feel (Modak, Bungee or bold Quicksand). Hero with reflective metallic headline, sticker-style icons, star and sparkle SVG accents floating around. Glossy 3D blob shapes scattered as background. Feature cards as iridescent rounded rectangles. Testimonials in chat-bubble style. Pricing as glossy chrome-bordered plan cards.`,
2269 + },
2270 + {
2271 + id: "swiss-typographic",
2272 + label: "Swiss / Intl Style",
2273 + image: CHAT_STYLES_CDN + "swiss.jpg",
2274 + prompt: `Build a landing page in pure Swiss International style. Strict 12-column grid, asymmetric balance. Palette: white #FFFFFF, pure black #000000, single accent red #E63946. Helvetica or Neue Haas Grotesk throughout. Headlines flush-left, 72-96px weight 700, generous leading. Body 16px weight 400. Zero ornaments — no shadows, gradients, illustrations, icons or rounded corners. Strong 1px black horizontal rules dividing sections. Hero with massive left-aligned headline and a 2-column supporting paragraph. Features as a numbered list 01 / 02 / 03.`,
2275 + },
2276 + {
2277 + id: "claymorphism",
2278 + label: "Claymorphism",
2279 + image: CHAT_STYLES_CDN + "clay.jpg",
2280 + prompt: `Design a soft claymorphism landing page. Pastel palette: lavender #E0D7FF, peach #FFD4C4, mint #C7F0DB, butter #FFF3B0, on background #F8F4FF. All elements have a soft 3D clay look: rounded 24-32px corners, subtle inner highlights, outer shadows in matching pastel tones (never black). No flat surfaces — everything looks puffy and squishable. Rounded sans-serif DM Sans or Quicksand weight 600. Hero with a 3D clay illustration. Feature cards as plump pastel pills. CTA buttons look pressable with soft inner glow. Friendly, playful, cozy mood throughout.`,
2281 + },
2282 + ];
2283 +
2284 + function buildChatStyleStorageKey() {
2285 + try {
2286 + const id = wp?.data?.select("core/editor")?.getCurrentPostId?.();
2287 + if (id && Number(id) > 0) {
2288 + return "aibui_chat_visual_style_v1_post_" + id;
2289 + }
2290 + } catch (e) { }
2291 + const raw = (window.location.pathname || "") + (window.location.search || "");
2292 + try {
2293 + return (
2294 + "aibui_chat_visual_style_v1_ctx_" +
2295 + btoa(unescape(encodeURIComponent(raw)))
2296 + .replace(/\+/g, "-")
2297 + .replace(/\//g, "_")
2298 + .replace(/=+$/, "")
2299 + );
2300 + } catch (e2) {
2301 + return "aibui_chat_visual_style_v1_fallback";
2302 + }
2303 + }
2304 +
2305 + let chatSelectedVisualStyleId = null;
2306 + let chatStylePanelOpen = false;
2307 + let chatStyleStoragePollKey = "";
2308 +
2309 + function loadChatVisualStyleFromStorage() {
2310 + try {
2311 + const raw = localStorage.getItem(buildChatStyleStorageKey());
2312 + if (!raw) {
2313 + chatSelectedVisualStyleId = null;
2314 + return;
2315 + }
2316 + const data = JSON.parse(raw);
2317 + const id = data && data.id ? String(data.id) : "";
2318 + if (id && CHAT_VISUAL_STYLES.some((s) => s.id === id)) {
2319 + chatSelectedVisualStyleId = id;
2320 + } else {
2321 + chatSelectedVisualStyleId = null;
2322 + }
2323 + } catch (e) {
2324 + chatSelectedVisualStyleId = null;
2325 + }
2326 + }
2327 +
2328 + function saveChatVisualStyleToStorage() {
2329 + try {
2330 + const key = buildChatStyleStorageKey();
2331 + if (!chatSelectedVisualStyleId) {
2332 + localStorage.removeItem(key);
2333 + } else {
2334 + localStorage.setItem(
2335 + key,
2336 + JSON.stringify({ id: chatSelectedVisualStyleId })
2337 + );
2338 + }
2339 + } catch (e) { }
2340 + }
2341 +
2342 + function getVisualStylePromptForApi() {
2343 + const s = CHAT_VISUAL_STYLES.find((x) => x.id === chatSelectedVisualStyleId);
2344 + return s ? s.prompt : undefined;
2345 + }
2346 +
2347 + // --- AI Block modifier (Group block mini-panel) ---
2348 + const AIBUI_BLOCK_CSS_START = "/* AIBUI_BLOCK_CSS_START";
2349 + const AIBUI_BLOCK_CSS_END = "/* AIBUI_BLOCK_CSS_END";
2350 + const AIBUI_BLOCK_ID_CLASS_PREFIX = "aibui-bid-";
2351 + const AIBUI_BLOCK_MODIFY_ENDPOINT = "/ai-transform-page/v2-transform-single-block";
2352 +
2353 + function aibuiSafeTruncate(str, maxChars) {
2354 + const s = String(str || "");
2355 + if (s.length <= maxChars) return s;
2356 + return s.slice(0, maxChars - 40) + "\n/* …truncated… */\n";
2357 + }
2358 +
2359 + function aibuiShortIdFromClientId(clientId) {
2360 + const raw = String(clientId || "").replace(/[^a-zA-Z0-9]/g, "");
2361 + return raw.slice(0, 10) || "block";
2362 + }
2363 +
2364 + function aibuiEnsureBlockIdClass(block) {
2365 + try {
2366 + const className = (block && block.attributes && block.attributes.className) ? String(block.attributes.className) : "";
2367 + const existing = className.split(/\s+/).find((c) => c.startsWith(AIBUI_BLOCK_ID_CLASS_PREFIX));
2368 + if (existing) return existing;
2369 + const bid = AIBUI_BLOCK_ID_CLASS_PREFIX + aibuiShortIdFromClientId(block.clientId);
2370 + const next = (className ? className + " " : "") + bid;
2371 + wp.data.dispatch("core/block-editor").updateBlockAttributes(block.clientId, { className: next });
2372 + return bid;
2373 + } catch (e) {
2374 + return "";
2375 + }
2376 + }
2377 +
2378 + function aibuiFindCssSegment(cssText, bidClass) {
2379 + const css = String(cssText || "");
2380 + if (!bidClass) return null;
2381 + const startRe = new RegExp("/\\*\\s*AIBUI_BLOCK_CSS_START\\s+bid=" + bidClass.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&") + "\\s*\\*\\/");
2382 + const endRe = new RegExp("/\\*\\s*AIBUI_BLOCK_CSS_END\\s+bid=" + bidClass.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&") + "\\s*\\*\\/");
2383 + const mStart = startRe.exec(css);
2384 + if (!mStart) return null;
2385 + const startIdx = mStart.index + mStart[0].length;
2386 + const mEnd = endRe.exec(css.slice(startIdx));
2387 + if (!mEnd) return null;
2388 + const endIdx = startIdx + mEnd.index;
2389 + return { startIdx: mStart.index, endIdx: endIdx + mEnd[0].length, inner: css.slice(startIdx, endIdx).trim() };
2390 + }
2391 +
2392 + function aibuiExtractRelatedCssForApi(bidClass) {
2393 + const pageCss = window.aiBuilderPageCSS || "";
2394 + const blockCss = window.aiBuilderBlockCSS || "";
2395 +
2396 + // Prefer explicit segments if present.
2397 + const segPage = aibuiFindCssSegment(pageCss, bidClass);
2398 + if (segPage) return { origin: "page", css: segPage.inner };
2399 + const segBlock = aibuiFindCssSegment(blockCss, bidClass);
2400 + if (segBlock) return { origin: "block", css: segBlock.inner };
2401 +
2402 + // Heuristic fallback: collect small windows around occurrences of the class selector.
2403 + const selector = "." + bidClass;
2404 + const sources = [
2405 + { origin: "page", text: pageCss },
2406 + { origin: "block", text: blockCss },
2407 + ];
2408 + for (const src of sources) {
2409 + if (!src.text || !src.text.includes(selector)) continue;
2410 + const chunks = [];
2411 + let idx = 0;
2412 + while (idx < src.text.length) {
2413 + const at = src.text.indexOf(selector, idx);
2414 + if (at === -1) break;
2415 + const from = Math.max(0, at - 600);
2416 + const to = Math.min(src.text.length, at + 1400);
2417 + chunks.push(src.text.slice(from, to));
2418 + idx = at + selector.length;
2419 + if (chunks.join("\n").length > 5200) break;
2420 + }
2421 + const combined = chunks.join("\n/* … */\n");
2422 + return { origin: src.origin, css: combined };
2423 + }
2424 +
2425 + return { origin: "block", css: "" };
2426 + }
2427 +
2428 + async function aibuiUpsertBlockCssSegment(bidClass, newInnerCss, origin) {
2429 + const cssKey = origin === "page" ? "aiBuilderPageCSS" : "aiBuilderBlockCSS";
2430 + const current = window[cssKey] || "";
2431 + const existing = aibuiFindCssSegment(current, bidClass);
2432 + const startMarker = `/* AIBUI_BLOCK_CSS_START bid=${bidClass} */`;
2433 + const endMarker = `/* AIBUI_BLOCK_CSS_END bid=${bidClass} */`;
2434 + const nextSegment = `${startMarker}\n${String(newInnerCss || "").trim()}\n${endMarker}\n`;
2435 +
2436 + let next;
2437 + if (existing) {
2438 + next = current.slice(0, existing.startIdx) + nextSegment + current.slice(existing.endIdx);
2439 + } else {
2440 + next = (String(current || "").trimEnd() + "\n\n" + nextSegment).trimEnd() + "\n";
2441 + }
2442 +
2443 + window[cssKey] = next;
2444 + await saveCSSInPostMeta(next, origin === "page" ? "page" : "block", true);
2445 + await loadCSSFromPostMeta();
2446 + }
2447 +
2448 + async function aibuiRemoveBlockCssSegmentMarkers(bidClass, origin) {
2449 + const cssKey = origin === "page" ? "aiBuilderPageCSS" : "aiBuilderBlockCSS";
2450 + const current = window[cssKey] || "";
2451 + const existing = aibuiFindCssSegment(current, bidClass);
2452 + if (!existing) return;
2453 +
2454 + const cleanedSegment = String(existing.inner || "").trim();
2455 + const before = current.slice(0, existing.startIdx).trimEnd();
2456 + const after = current.slice(existing.endIdx).trimStart();
2457 + const parts = [];
2458 +
2459 + if (before) parts.push(before);
2460 + if (cleanedSegment) parts.push(cleanedSegment);
2461 + if (after) parts.push(after);
2462 +
2463 + const next = parts.join("\n\n").trim();
2464 + window[cssKey] = next ? next + "\n" : "";
2465 + await saveCSSInPostMeta(window[cssKey], origin === "page" ? "page" : "block", true);
2466 + await loadCSSFromPostMeta();
2467 + }
2468 +
2469 + const AIBUI_BM_PROMPT_MAX_CHARS = 1000;
2470 +
2471 + function aibuiEnsureBlockModifierPanel() {
2472 + let el = document.getElementById("aibui-block-modifier");
2473 + if (el) return el;
2474 + el = document.createElement("div");
2475 + el.id = "aibui-block-modifier";
2476 + el.setAttribute("hidden", "");
2477 + el.innerHTML = `
2478 + <div class="aibui-bm-head">
2479 + <div class="aibui-bm-title">Modify</div>
2480 + <button type="button" class="aibui-bm-close" aria-label="Close" data-aibui-bm-close>&times;</button>
2481 + </div>
2482 + <div class="aibui-bm-body">
2483 + <label class="aibui-bm-label" for="aibui-bm-prompt">Instruction</label>
2484 + <textarea id="aibui-bm-prompt" class="aibui-bm-prompt" rows="3" maxlength="${AIBUI_BM_PROMPT_MAX_CHARS}" placeholder='Add the text "hello world" and change the block color to red'></textarea>
2485 + <div class="aibui-bm-meta" aria-live="polite">
2486 + <span data-aibui-bm-count>0</span>/${AIBUI_BM_PROMPT_MAX_CHARS}
2487 + </div>
2488 +
2489 + <div class="aibui-bm-actions">
2490 + <button type="button" class="aibui-bm-btn aibui-bm-btn--primary" data-aibui-bm-submit>Modify</button>
2491 + </div>
2492 + <div class="aibui-bm-status" role="status" aria-live="polite" data-aibui-bm-status>
2493 + <span class="aibui-bm-status-dots" aria-hidden="true" hidden>
2494 + <span class="typing-dot"></span>
2495 + <span class="typing-dot"></span>
2496 + <span class="typing-dot"></span>
2497 + </span>
2498 + <span class="aibui-bm-status-text" data-aibui-bm-status-text></span>
2499 + </div>
2500 + </div>
2501 + `;
2502 + document.body.appendChild(el);
2503 +
2504 + const promptEl = el.querySelector("#aibui-bm-prompt");
2505 + const countEl = el.querySelector("[data-aibui-bm-count]");
2506 + const metaEl = el.querySelector(".aibui-bm-meta");
2507 + const syncCount = () => {
2508 + if (!promptEl || !countEl) return;
2509 + if (promptEl.value.length > AIBUI_BM_PROMPT_MAX_CHARS) {
2510 + promptEl.value = promptEl.value.slice(0, AIBUI_BM_PROMPT_MAX_CHARS);
2511 + }
2512 + countEl.textContent = String(promptEl.value.length);
2513 + if (metaEl) {
2514 + metaEl.classList.toggle("aibui-bm-meta--max", promptEl.value.length >= AIBUI_BM_PROMPT_MAX_CHARS);
2515 + }
2516 + };
2517 + if (promptEl) {
2518 + promptEl.addEventListener("input", syncCount);
2519 + promptEl.addEventListener("paste", () => setTimeout(syncCount, 0));
2520 + syncCount();
2521 + }
2522 +
2523 + const closeBtn = el.querySelector("[data-aibui-bm-close]");
2524 + if (closeBtn) {
2525 + closeBtn.addEventListener("click", () => {
2526 + el.setAttribute("hidden", "");
2527 + if (window._aibuiBlockModifierState) {
2528 + window._aibuiBlockModifierState.panelOpen = false;
2529 + }
2530 + });
2531 + }
2532 +
2533 + return el;
2534 + }
2535 +
2536 + function aibuiEnsureBlockModifierButton() {
2537 + let el = document.getElementById("aibui-block-modifier-btn");
2538 + if (el) return el;
2539 + el = document.createElement("button");
2540 + el.type = "button";
2541 + el.id = "aibui-block-modifier-btn";
2542 + el.className = "aibui-bm-fab";
2543 + el.setAttribute("hidden", "");
2544 + el.textContent = "Modify with AI";
2545 + document.body.appendChild(el);
2546 + return el;
2547 + }
2548 +
2549 + function aibuiGetCanvasBlockEl(clientId) {
2550 + const id = String(clientId || "");
2551 + if (!id) return null;
2552 +
2553 + function pickBestMatch(nodes) {
2554 + if (!nodes || !nodes.length) return null;
2555 + // Prefer elements in the writing flow / block list, and avoid list view tree.
2556 + let best = null;
2557 + for (const el of nodes) {
2558 + if (!el || !el.getBoundingClientRect) continue;
2559 + // Ignore hidden/non-rendered nodes.
2560 + const rect = el.getBoundingClientRect();
2561 + if (!rect || rect.width <= 0 || rect.height <= 0) continue;
2562 + if (el.closest && el.closest(".block-editor-list-view-tree")) continue;
2563 + if (el.closest && el.closest(".block-editor-list-view-leaf")) continue;
2564 + const score =
2565 + (el.closest && (el.closest(".block-editor-writing-flow") || el.closest(".block-editor-block-list__layout")) ? 100 : 0) +
2566 + (el.classList && el.classList.contains("block-editor-block-list__block") ? 50 : 0) +
2567 + Math.max(0, 20 - Math.min(20, rect.top / 50));
2568 + if (!best || score > best.score) {
2569 + best = { el, score };
2570 + }
2571 + }
2572 + return best ? best.el : null;
2573 + }
2574 +
2575 + try {
2576 + const nodes = Array.from(document.querySelectorAll(`[data-block="${id}"]`));
2577 + const picked = pickBestMatch(nodes);
2578 + if (picked) return { el: picked, iframe: null };
2579 + } catch (e) { }
2580 +
2581 + // If the editor canvas lives in an iframe, try to find the block there.
2582 + try {
2583 + const iframes = Array.from(document.querySelectorAll("iframe"));
2584 + for (const frame of iframes) {
2585 + try {
2586 + const doc = frame.contentDocument;
2587 + if (!doc) continue;
2588 + const nodes = Array.from(doc.querySelectorAll(`[data-block="${id}"]`));
2589 + const picked = pickBestMatch(nodes);
2590 + if (picked) return { el: picked, iframe: frame };
2591 + } catch (e2) {
2592 + // cross-origin or unavailable iframe
2593 + }
2594 + }
2595 + } catch (e3) { }
2596 +
2597 + return null;
2598 + }
2599 +
2600 + function aibuiPositionPanelNextToBlock(panelEl, clientId) {
2601 + try {
2602 + const found = aibuiGetCanvasBlockEl(clientId);
2603 + if (!found || !found.el) return;
2604 + const blockEl = found.el;
2605 + const rect = blockEl.getBoundingClientRect();
2606 + const frameRect = found.iframe ? found.iframe.getBoundingClientRect() : null;
2607 + const panelWidth = 320;
2608 + const gap = 8;
2609 + const top = Math.max(12, Math.min(window.innerHeight - 260, rect.top + 55));
2610 + let left = rect.left + gap;
2611 + if (left + panelWidth > window.innerWidth - 12) {
2612 + left = Math.max(12, rect.right - panelWidth - gap);
2613 + }
2614 + const absTop = (frameRect ? frameRect.top : 0) + top;
2615 + const absLeft = (frameRect ? frameRect.left : 0) + Math.max(12, left);
2616 + panelEl.style.transform = `translate3d(${absLeft}px, ${absTop}px, 0)`;
2617 + } catch (e) { }
2618 + }
2619 +
2620 + function aibuiPositionButtonNextToBlock(btnEl, clientId) {
2621 + try {
2622 + const found = aibuiGetCanvasBlockEl(clientId);
2623 + if (!found || !found.el) return;
2624 + const blockEl = found.el;
2625 + const rect = blockEl.getBoundingClientRect();
2626 + const frameRect = found.iframe ? found.iframe.getBoundingClientRect() : null;
2627 + const gap = 8;
2628 + const btnWidth = 120;
2629 + const btnHeight = 32;
2630 + // Emulate "position: sticky" but anchored to the selected block:
2631 + // top = min( max(blockTop+offset, stickyTop), blockBottom - height - offset )
2632 + const stickyTop = 12;
2633 + const offset = 8;
2634 + const topMin = rect.top + offset;
2635 + const topMax = rect.bottom - btnHeight - offset;
2636 + let top = Math.min(Math.max(topMin, stickyTop), topMax);
2637 + top = Math.max(12, Math.min(window.innerHeight - btnHeight - 12, top));
2638 +
2639 + // If the block is fully above the sticky line, fade out the button (still selected).
2640 + if (rect.bottom < stickyTop + 2) {
2641 + btnEl.style.opacity = "0";
2642 + btnEl.style.pointerEvents = "none";
2643 + } else {
2644 + btnEl.style.opacity = "1";
2645 + btnEl.style.pointerEvents = "";
2646 + }
2647 + let left = rect.left + 8;
2648 + if (left + btnWidth > window.innerWidth - 12) left = Math.max(12, rect.right - btnWidth - gap);
2649 + const absTop = (frameRect ? frameRect.top : 0) + top;
2650 + const absLeft = (frameRect ? frameRect.left : 0) + Math.max(12, left);
2651 + btnEl.style.transform = `translate3d(${absLeft}px, ${absTop}px, 0)`;
2652 + } catch (e) { }
2653 + }
2654 +
2655 + function aibuiEnsureClassOnBlockObject(blockObj, bidClass) {
2656 + try {
2657 + if (!blockObj || !bidClass) return blockObj;
2658 + const attrs = blockObj.attributes || {};
2659 + const cur = attrs.className ? String(attrs.className) : "";
2660 + if (cur.split(/\s+/).includes(bidClass)) return blockObj;
2661 + blockObj.attributes = {
2662 + ...attrs,
2663 + className: (cur ? cur + " " : "") + bidClass,
2664 + };
2665 + return blockObj;
2666 + } catch (e) {
2667 + return blockObj;
2668 + }
2669 + }
2670 +
2671 + async function aibuiModifySelectedBlock(clientId) {
2672 + const panel = aibuiEnsureBlockModifierPanel();
2673 + const promptEl = panel.querySelector("#aibui-bm-prompt");
2674 + const statusEl = panel.querySelector("[data-aibui-bm-status]");
2675 + const statusTextEl = panel.querySelector("[data-aibui-bm-status-text]");
2676 + const statusDotsEl = panel.querySelector(".aibui-bm-status-dots");
2677 + const submitBtn = panel.querySelector("[data-aibui-bm-submit]");
2678 +
2679 + const userPrompt = (promptEl ? promptEl.value : "").trim();
2680 + if (!userPrompt) {
2681 + if (statusDotsEl) statusDotsEl.hidden = true;
2682 + if (statusTextEl) statusTextEl.textContent = "Please enter an instruction.";
2683 + else if (statusEl) statusEl.textContent = "Please enter an instruction.";
2684 + return;
2685 + }
2686 + if (userPrompt.length > AIBUI_BM_PROMPT_MAX_CHARS) {
2687 + if (statusDotsEl) statusDotsEl.hidden = true;
2688 + const msg = `Please keep your instruction under ${AIBUI_BM_PROMPT_MAX_CHARS} characters.`;
2689 + if (statusTextEl) statusTextEl.textContent = msg;
2690 + else if (statusEl) statusEl.textContent = msg;
2691 + return;
2692 + }
2693 +
2694 + // Diagnostics context, see sendMessageAIV3.
2695 + const requestContext = { action: "block modification", startedAt: Date.now() };
2696 +
2697 + try {
2698 + if (submitBtn) submitBtn.disabled = true;
2699 + if (statusDotsEl) statusDotsEl.hidden = false;
2700 + if (statusTextEl) statusTextEl.textContent = "Modifying block…";
2701 + else if (statusEl) statusEl.textContent = "Modifying block…";
2702 +
2703 + let jwtToken = "";
2704 + try {
2705 + jwtToken = await getJwtToken();
2706 + } catch (e) {
2707 + if (statusDotsEl) statusDotsEl.hidden = true;
2708 + if (e && e.aibuiSessionExpired === true) {
2709 + aibuiSetBlockModifierFailure(
2710 + statusTextEl,
2711 + statusEl,
2712 + aibuiDescribeThrownFailure(e, requestContext)
2713 + );
2714 + return;
2715 + }
2716 + const msg = "You need to connect your account. Go to your WordPress Dashboard → AI Builder → Account.";
2717 + if (statusTextEl) statusTextEl.textContent = msg;
2718 + else if (statusEl) statusEl.textContent = msg;
2719 + return;
2720 + }
2721 + const block = wp.data.select("core/block-editor").getBlock(clientId);
2722 + if (!block) throw new Error("Missing block");
2723 + const bidClass = aibuiEnsureBlockIdClass(block);
2724 + const cssInfo = aibuiExtractRelatedCssForApi(bidClass);
2725 + const cssForApi = aibuiSafeTruncate(cssInfo.css, 5000);
2726 +
2727 + const blockJson = (() => {
2728 + try { return JSON.stringify(block); } catch (e) { return ""; }
2729 + })();
2730 + const blockSerialized = (() => {
2731 + try { return wp.blocks.serialize([block]); } catch (e) { return ""; }
2732 + })();
2733 + const blockContentForApi = aibuiSafeTruncate(blockSerialized || blockJson, 14000);
2734 +
2735 + const sitePlugins = (typeof aiBuilderVars !== 'undefined' && Array.isArray(aiBuilderVars.sitePlugins)) ? aiBuilderVars.sitePlugins.slice(0, 15) : [];
2736 + const wordpressVersion = (typeof aiBuilderVars !== 'undefined' && aiBuilderVars.wordpressVersion) ? String(aiBuilderVars.wordpressVersion) : '';
2737 + const activeThemeName = (typeof aiBuilderEditorVars !== 'undefined' && aiBuilderEditorVars.activeThemeName)
2738 + ? aiBuilderEditorVars.activeThemeName
2739 + : ((typeof aiBuilderVars !== 'undefined' && aiBuilderVars.activeThemeName) ? aiBuilderVars.activeThemeName : '');
2740 + const conversationHistory = getConversationHistoryString(6);
2741 +
2742 + const unavailable = aibuiApiUnavailableFailure(requestContext);
2743 + if (unavailable) {
2744 + if (statusDotsEl) statusDotsEl.hidden = true;
2745 + aibuiSetBlockModifierFailure(statusTextEl, statusEl, unavailable);
2746 + return;
2747 + }
2748 +
2749 + requestContext.endpoint = window.config.apiUrl + AIBUI_BLOCK_MODIFY_ENDPOINT;
2750 + requestContext.startedAt = Date.now();
2751 + const res = await fetch(
2752 + requestContext.endpoint,
2753 + {
2754 + method: "POST",
2755 + headers: {
2756 + "Content-Type": "application/json",
2757 + Authorization: `Bearer ${jwtToken}`,
2758 + },
2759 + body: JSON.stringify({
2760 + userPrompt,
2761 + activeThemeName,
2762 + sitePlugins,
2763 + wordpressVersion,
2764 + conversationHistory,
2765 + blockContent: blockContentForApi,
2766 + blockCss: cssForApi,
2767 + }),
2768 + }
2769 + );
2770 +
2771 + const txt = await res.text();
2772 + let data = {};
2773 + try { data = txt ? JSON.parse(txt) : {}; } catch (e) { data = {}; }
2774 +
2775 + if (!res.ok) {
2776 + if (statusDotsEl) statusDotsEl.hidden = true;
2777 + const rawMsg =
2778 + (data && typeof data === "object" && (data.message || data.error || data.data)) ? String(data.message || data.error || data.data)
2779 + : (txt ? String(txt) : "");
2780 + console.log("rawMsg: ", rawMsg);
2781 + const msgLower = rawMsg.toLowerCase();
2782 +
2783 + if (res.status === 402 || (data && data.error === "not-enough-credits")) {
2784 + // Credits exhausted
2785 + try { showNotEnoughCreditsToast(); } catch (e) { }
2786 + const msg = "You have no credits left. Please purchase more credits to continue.";
2787 + if (statusTextEl) statusTextEl.textContent = msg;
2788 + else if (statusEl) statusEl.textContent = msg;
2789 + return;
2790 + }
2791 +
2792 + if (msgLower.includes("no token found")) {
2793 + const msg = "You need to connect your account. Go to your WordPress Dashboard → AI Builder → Account.";
2794 + if (statusTextEl) statusTextEl.textContent = msg;
2795 + else if (statusEl) statusEl.textContent = msg;
2796 + return;
2797 + }
2798 +
2799 + aibuiSetBlockModifierFailure(
2800 + statusTextEl,
2801 + statusEl,
2802 + aibuiDescribeHttpFailure(res, data, requestContext)
2803 + );
2804 + return;
2805 + }
2806 +
2807 + if (data.error === "not-enough-credits") {
2808 + showNotEnoughCreditsToast();
2809 + if (statusDotsEl) statusDotsEl.hidden = true;
2810 + if (statusTextEl) statusTextEl.textContent = "";
2811 + else if (statusEl) statusEl.textContent = "";
2812 + return;
2813 + }
2814 +
2815 + // Update block content if provided
2816 + if (data.blockContent) {
2817 + const skippedBlocks = [];
2818 + let newBlock = buildBlockSafe(data.blockContent, skippedBlocks);
2819 +
2820 + if (!newBlock) {
2821 + if (statusDotsEl) statusDotsEl.hidden = true;
2822 + aibuiSetBlockModifierFailure(statusTextEl, statusEl, {
2823 + code: "AIB-BLOCK-01",
2824 + message:
2825 + "The AI returned a block your site cannot display: it is not " +
2826 + "available in this WordPress install. Your block was left " +
2827 + "unchanged. " +
2828 + AIBUI_CREDIT_REFUND_NOTE,
2829 + context: requestContext,
2830 + detail: "unavailable blocks: " + skippedBlocks.join(", "),
2831 + });
2832 + return;
2833 + }
2834 +
2835 + try {
2836 + // Preserve the bid class on the returned root block, so CSS scoping keeps working.
2837 + newBlock = aibuiEnsureClassOnBlockObject(newBlock, bidClass);
2838 + wp.data.dispatch("core/block-editor").replaceBlocks(clientId, [newBlock]);
2839 + } catch (e) {
2840 + if (statusDotsEl) statusDotsEl.hidden = true;
2841 + aibuiSetBlockModifierFailure(statusTextEl, statusEl, {
2842 + code: "AIB-EDITOR-01",
2843 + message:
2844 + "The modified block was generated but the editor refused to " +
2845 + "replace it. Save your work, reload the editor, and try again. " +
2846 + AIBUI_CREDIT_REFUND_NOTE,
2847 + context: requestContext,
2848 + detail: aibuiTruncate(e && e.message, 160),
2849 + cause: e,
2850 + });
2851 + return;
2852 + }
2853 + }
2854 +
2855 + // Update only the related CSS segment if provided
2856 + if (data.cssContent) {
2857 + const origin = cssInfo && cssInfo.origin ? cssInfo.origin : "block";
2858 + await aibuiUpsertBlockCssSegment(bidClass, data.cssContent, origin);
2859 + await aibuiRemoveBlockCssSegmentMarkers(bidClass, origin);
2860 + }
2861 +
2862 + if (typeof data.creditsLeft !== "undefined") {
2863 + try { updateCreditsDisplay(data.creditsLeft); } catch (e) { }
2864 + }
2865 +
2866 + if (statusDotsEl) statusDotsEl.hidden = true;
2867 + if (statusTextEl) statusTextEl.textContent = "Done.";
2868 + else if (statusEl) statusEl.textContent = "Done.";
2869 + } catch (e) {
2870 + if (statusDotsEl) statusDotsEl.hidden = true;
2871 + aibuiSetBlockModifierFailure(
2872 + statusTextEl,
2873 + statusEl,
2874 + aibuiDescribeThrownFailure(e, requestContext)
2875 + );
2876 + } finally {
2877 + if (submitBtn) submitBtn.disabled = false;
2878 + if (statusDotsEl) statusDotsEl.hidden = true;
2879 + }
2880 + }
2881 +
2882 + function initAibuiBlockModifier() {
2883 + if (!wp || !wp.data || !wp.data.select) return;
2884 + const panel = aibuiEnsureBlockModifierPanel();
2885 + const btn = aibuiEnsureBlockModifierButton();
2886 + let lastClientId = null;
2887 + const state = (window._aibuiBlockModifierState = window._aibuiBlockModifierState || { panelOpen: false });
2888 + let rafId = 0;
2889 +
2890 + function stopTracking() {
2891 + if (rafId) {
2892 + try { cancelAnimationFrame(rafId); } catch (e) { }
2893 + }
2894 + rafId = 0;
2895 + }
2896 +
2897 + function track() {
2898 + rafId = requestAnimationFrame(track);
2899 + if (!lastClientId) return;
2900 + if (!btn.hasAttribute("hidden")) aibuiPositionButtonNextToBlock(btn, lastClientId);
2901 + if (!panel.hasAttribute("hidden")) aibuiPositionPanelNextToBlock(panel, lastClientId);
2902 + }
2903 +
2904 + function ensureTrackingRunning() {
2905 + if (rafId) return;
2906 + rafId = requestAnimationFrame(track);
2907 + }
2908 +
2909 + function isBlockTypeExcluded(blockName) {
2910 + const name = String(blockName || "");
2911 + if (!name) return true;
2912 + // Exclusions: image blocks + text blocks (exception requested)
2913 + const excluded = new Set([
2914 + // Images / media-like
2915 + "core/image",
2916 + "core/gallery",
2917 + "core/media-text",
2918 + // Text
2919 + "core/paragraph",
2920 + "core/heading",
2921 + "core/list",
2922 + "core/quote",
2923 + "core/pullquote",
2924 + "core/preformatted",
2925 + "core/code",
2926 + "core/verse",
2927 + "core/table",
2928 + "core/button"
2929 + ]);
2930 + return excluded.has(name);
2931 + }
2932 +
2933 + function updatePanelForSelection() {
2934 + const cid = wp.data.select("core/block-editor").getSelectedBlockClientId();
2935 + if (!cid) {
2936 + lastClientId = null;
2937 + state.panelOpen = false;
2938 + panel.setAttribute("hidden", "");
2939 + btn.setAttribute("hidden", "");
2940 + stopTracking();
2941 + return;
2942 + }
2943 + if (cid === lastClientId) return;
2944 + lastClientId = cid;
2945 + state.panelOpen = false;
2946 + panel.setAttribute("hidden", "");
2947 +
2948 + const block = wp.data.select("core/block-editor").getBlock(cid);
2949 + if (!block) {
2950 + panel.setAttribute("hidden", "");
2951 + btn.setAttribute("hidden", "");
2952 + stopTracking();
2953 + return;
2954 + }
2955 + if (isBlockTypeExcluded(block.name)) {
2956 + panel.setAttribute("hidden", "");
2957 + btn.setAttribute("hidden", "");
2958 + stopTracking();
2959 + return;
2960 + }
2961 +
2962 + btn.removeAttribute("hidden");
2963 + aibuiPositionButtonNextToBlock(btn, cid);
2964 + ensureTrackingRunning();
2965 +
2966 + btn.onclick = () => {
2967 + // Toggle panel on repeated clicks for the same selected block
2968 + const isOpen = !panel.hasAttribute("hidden") && state.panelOpen === true;
2969 + if (isOpen) {
2970 + state.panelOpen = false;
2971 + panel.setAttribute("hidden", "");
2972 + return;
2973 + }
2974 +
2975 + state.panelOpen = true;
2976 + panel.removeAttribute("hidden");
2977 + aibuiPositionPanelNextToBlock(panel, cid);
2978 + ensureTrackingRunning();
2979 + const submitBtn = panel.querySelector("[data-aibui-bm-submit]");
2980 + if (submitBtn) submitBtn.onclick = () => aibuiModifySelectedBlock(cid);
2981 + };
2982 + }
2983 +
2984 + wp.data.subscribe(() => {
2985 + try { updatePanelForSelection(); } catch (e) { }
2986 + });
2987 +
2988 + // Keep lightweight listeners as fallbacks (rAF handles smooth tracking)
2989 + window.addEventListener("scroll", () => { ensureTrackingRunning(); }, { passive: true });
2990 + window.addEventListener("resize", () => { ensureTrackingRunning(); });
2991 + }
2992 +
2993 + function syncChatStylePickerUI() {
2994 + const trigger = document.getElementById("chat-style-trigger");
2995 + const label = document.getElementById("chat-style-trigger-label");
2996 + const chevron = document.getElementById("chat-style-trigger-chevron");
2997 + const hint = document.getElementById("chat-style-swipe-hint");
2998 + const removeBtn = document.getElementById("chat-style-remove");
2999 + const outer = document.getElementById("chat-style-slider-outer");
3000 + const slider = document.getElementById("chat-style-slider");
3001 + if (!trigger || !label || !outer || !slider) return;
3002 +
3003 + if (chatSelectedVisualStyleId) {
3004 + const sel = CHAT_VISUAL_STYLES.find((s) => s.id === chatSelectedVisualStyleId);
3005 + label.textContent = sel
3006 + ? "Style: " + sel.label
3007 + : "Style applied";
3008 + } else {
3009 + label.textContent = "Need inspiration? Try a style";
3010 + }
3011 +
3012 + if (removeBtn) {
3013 + removeBtn.hidden = !chatSelectedVisualStyleId;
3014 + }
3015 +
3016 + const showSlider = chatStylePanelOpen;
3017 + outer.hidden = !showSlider;
3018 + if (hint) {
3019 + hint.hidden = !showSlider;
3020 + }
3021 + trigger.setAttribute("aria-expanded", showSlider ? "true" : "false");
3022 + if (chevron) {
3023 + chevron.textContent = showSlider ? "▾" : "▴";
3024 + }
3025 +
3026 + slider.querySelectorAll(".chat-style-card").forEach((el) => {
3027 + const sid = el.getAttribute("data-style-id");
3028 + el.classList.toggle("is-selected", sid === chatSelectedVisualStyleId);
3029 + el.setAttribute("aria-selected", sid === chatSelectedVisualStyleId ? "true" : "false");
3030 + });
3031 + }
3032 +
3033 + function initChatVisualStylePicker() {
3034 + const picker = document.getElementById("chat-style-picker");
3035 + const trigger = document.getElementById("chat-style-trigger");
3036 + const removeBtn = document.getElementById("chat-style-remove");
3037 + const slider = document.getElementById("chat-style-slider");
3038 + if (!picker || !trigger || !slider) return;
3039 +
3040 + const eyeSvg =
3041 + '<svg class="chat-style-preview-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/></svg>';
3042 + slider.innerHTML = CHAT_VISUAL_STYLES.map((s) => {
3043 + const safeLabel = String(s.label).replace(/"/g, "&quot;");
3044 + return (
3045 + `<div class="chat-style-card" data-style-id="${s.id}" role="option" tabindex="0" aria-selected="false" title="${safeLabel}">
3046 + <div class="chat-style-card-inner">
3047 + <img src="${s.image}" alt="" loading="lazy" width="100" height="100" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'"/>
3048 + <span class="chat-style-card-fallback" style="display:none" aria-hidden="true"></span>
3049 + <button type="button" class="chat-style-preview-btn" data-style-preview="${s.id}" aria-label="View full style and prompt" title="Preview">${eyeSvg}</button>
3050 + </div>
3051 + <span class="chat-style-card-label">${s.label}</span>
3052 + </div>`
3053 + );
3054 + }).join("");
3055 +
3056 + slider.querySelectorAll(".chat-style-card-fallback").forEach((fb, i) => {
3057 + const preset = CHAT_VISUAL_STYLES[i];
3058 + if (preset) {
3059 + fb.classList.add("chat-style-fallback--" + preset.id);
3060 + }
3061 + });
3062 +
3063 + function openChatStyleDetailModal(styleId) {
3064 + const s = CHAT_VISUAL_STYLES.find((x) => x.id === styleId);
3065 + if (!s) return;
3066 + const modal = document.getElementById("chat-style-detail-modal");
3067 + const imgEl = document.getElementById("chat-style-detail-img");
3068 + const titleEl = document.getElementById("chat-style-detail-title");
3069 + const promptEl = document.getElementById("chat-style-detail-prompt");
3070 + if (!modal || !imgEl || !titleEl || !promptEl) return;
3071 + titleEl.textContent = s.label;
3072 + imgEl.src = s.image;
3073 + imgEl.alt = s.label;
3074 + promptEl.textContent = s.prompt;
3075 + modal.removeAttribute("hidden");
3076 + modal.setAttribute("aria-hidden", "false");
3077 + const closeBtn = document.getElementById("chat-style-detail-close");
3078 + if (closeBtn) closeBtn.focus();
3079 + }
3080 +
3081 + function closeChatStyleDetailModal() {
3082 + const modal = document.getElementById("chat-style-detail-modal");
3083 + if (!modal) return;
3084 + modal.setAttribute("hidden", "");
3085 + modal.setAttribute("aria-hidden", "true");
3086 + }
3087 +
3088 + const detailClose = document.getElementById("chat-style-detail-close");
3089 + const detailBackdrop = document.getElementById("chat-style-detail-backdrop");
3090 + if (detailClose) {
3091 + detailClose.addEventListener("click", closeChatStyleDetailModal);
3092 + }
3093 + if (detailBackdrop) {
3094 + detailBackdrop.addEventListener("click", closeChatStyleDetailModal);
3095 + }
3096 + document.addEventListener("keydown", function (ev) {
3097 + if (ev.key !== "Escape") return;
3098 + const modal = document.getElementById("chat-style-detail-modal");
3099 + if (modal && !modal.hasAttribute("hidden")) {
3100 + closeChatStyleDetailModal();
3101 + }
3102 + });
3103 +
3104 + loadChatVisualStyleFromStorage();
3105 + chatStyleStoragePollKey = buildChatStyleStorageKey();
3106 + chatStylePanelOpen = !!chatSelectedVisualStyleId;
3107 + syncChatStylePickerUI();
3108 +
3109 + setInterval(function () {
3110 + try {
3111 + const k = buildChatStyleStorageKey();
3112 + if (k !== chatStyleStoragePollKey) {
3113 + chatStyleStoragePollKey = k;
3114 + loadChatVisualStyleFromStorage();
3115 + chatStylePanelOpen = !!chatSelectedVisualStyleId;
3116 + syncChatStylePickerUI();
3117 + }
3118 + } catch (e) { }
3119 + }, 1200);
3120 +
3121 + trigger.addEventListener("click", function () {
3122 + chatStylePanelOpen = !chatStylePanelOpen;
3123 + syncChatStylePickerUI();
3124 + });
3125 +
3126 + if (removeBtn) {
3127 + removeBtn.addEventListener("click", function (ev) {
3128 + ev.stopPropagation();
3129 + chatSelectedVisualStyleId = null;
3130 + saveChatVisualStyleToStorage();
3131 + chatStylePanelOpen = false;
3132 + syncChatStylePickerUI();
3133 + });
3134 + }
3135 +
3136 + slider.addEventListener("click", function (ev) {
3137 + if (ev.target.closest(".chat-style-preview-btn")) {
3138 + ev.preventDefault();
3139 + ev.stopPropagation();
3140 + const btn = ev.target.closest(".chat-style-preview-btn");
3141 + const sid = btn && btn.getAttribute("data-style-preview");
3142 + if (sid) openChatStyleDetailModal(sid);
3143 + return;
3144 + }
3145 + const card = ev.target.closest(".chat-style-card");
3146 + if (!card) return;
3147 + const sid = card.getAttribute("data-style-id");
3148 + if (!sid) return;
3149 + if (chatSelectedVisualStyleId === sid) {
3150 + chatSelectedVisualStyleId = null;
3151 + } else {
3152 + chatSelectedVisualStyleId = sid;
3153 + chatStylePanelOpen = true;
3154 + }
3155 + saveChatVisualStyleToStorage();
3156 + syncChatStylePickerUI();
3157 + });
3158 +
3159 + slider.addEventListener("keydown", function (ev) {
3160 + const card = ev.target.closest(".chat-style-card");
3161 + if (!card || document.activeElement !== card) return;
3162 + if (ev.key !== "Enter" && ev.key !== " ") return;
3163 + ev.preventDefault();
3164 + const sid = card.getAttribute("data-style-id");
3165 + if (!sid) return;
3166 + if (chatSelectedVisualStyleId === sid) {
3167 + chatSelectedVisualStyleId = null;
3168 + } else {
3169 + chatSelectedVisualStyleId = sid;
3170 + chatStylePanelOpen = true;
3171 + }
3172 + saveChatVisualStyleToStorage();
3173 + syncChatStylePickerUI();
3174 + });
3175 + }
3176 +
3177 + // ---------------------------------------------------------------------------
3178 + // Failure reporting
3179 + //
3180 + // Every chat failure used to collapse into a single opaque sentence, which
3181 + // told the user nothing and told support even less. A failure now renders as
3182 + // a plain-English explanation of what happened and what to do about it,
3183 + // followed by a small, dimmed technical line (error code + context) the user
3184 + // can read back to support.
3185 + // ---------------------------------------------------------------------------
3186 +
3187 + // Appended whenever the request reached the API, so billing may already have
3188 + // happened even though the user got nothing.
3189 + const AIBUI_CREDIT_REFUND_NOTE =
3190 + "Nothing was added to your page. If credits were deducted, contact support " +
3191 + "with the code below and we will refund them.";
3192 +
3193 + function aibuiEscapeHtml(value) {
3194 + return String(value == null ? "" : value)
3195 + .replace(/&/g, "&amp;")
3196 + .replace(/</g, "&lt;")
3197 + .replace(/>/g, "&gt;")
3198 + .replace(/"/g, "&quot;")
3199 + .replace(/'/g, "&#39;");
3200 + }
3201 +
3202 + function aibuiTruncate(value, max) {
3203 + const str = String(value == null ? "" : value);
3204 + return str.length > max ? str.slice(0, max) + "…" : str;
3205 + }
3206 +
3207 + function aibuiPluginVersion() {
3208 + try {
3209 + if (typeof aiBuilderVars !== "undefined" && aiBuilderVars.pluginVersion) {
3210 + return "AI Builder v" + String(aiBuilderVars.pluginVersion);
3211 + }
3212 + } catch (e) {
3213 + /* ignore */
3214 + }
3215 + return "";
3216 + }
3217 +
3218 + /** Last path segment of an endpoint URL, e.g. "v2-page-generation". */
3219 + function aibuiShortEndpoint(url) {
3220 + const str = String(url || "");
3221 + const i = str.lastIndexOf("/");
3222 + return i >= 0 ? str.slice(i + 1) : str;
3223 + }
3224 +
3225 + /**
3226 + * Build the small grey line under a failure message. Missing pieces are left
3227 + * out rather than printed as "undefined".
3228 + */
3229 + function aibuiFormatDiagnostics(info) {
3230 + const ctx = info.context || {};
3231 + const parts = [info.code];
3232 +
3233 + if (ctx.action) parts.push(ctx.action);
3234 + if (typeof info.status === "number" && info.status > 0) {
3235 + parts.push("HTTP " + info.status);
3236 + }
3237 + if (ctx.endpoint) parts.push(aibuiShortEndpoint(ctx.endpoint));
3238 + if (typeof ctx.startedAt === "number") {
3239 + parts.push(Math.round((Date.now() - ctx.startedAt) / 1000) + "s");
3240 + }
3241 + if (info.requestId) parts.push("req " + info.requestId);
3242 + if (info.detail) parts.push(aibuiTruncate(info.detail, 160));
3243 +
3244 + const version = aibuiPluginVersion();
3245 + if (version) parts.push(version);
3246 +
3247 + return parts.filter(Boolean).join(" · ");
3248 + }
3249 +
3250 + function aibuiFailureHtml(info) {
3251 + return (
3252 + '<div style="line-height:1.55;">' +
3253 + aibuiEscapeHtml(info.message) +
3254 + "</div>" +
3255 + '<div style="margin-top:8px;font-size:11px;line-height:1.45;opacity:0.55;' +
3256 + "font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;" +
3257 + 'word-break:break-word;">' +
3258 + aibuiEscapeHtml(aibuiFormatDiagnostics(info)) +
3259 + "</div>"
3260 + );
3261 + }
3262 +
3263 + /** Post a failure into the chat: human sentence + small diagnostics line. */
3264 + function addFailureMessage(info) {
3265 + const diagnostics = aibuiFormatDiagnostics(info);
3266 + addMessage(aibuiFailureHtml(info), "assistant", {
3267 + richHtml: true,
3268 + plain: info.message + " (" + diagnostics + ")",
3269 + excludeFromContext: true,
3270 + });
3271 + console.error("AI Builder failure — " + diagnostics, info.cause || "");
3272 + }
3273 +
3274 + /**
3275 + * Same report, rendered into the one-line status area of the block modifier
3276 + * panel: the sentence on the first line, the diagnostics underneath in small
3277 + * type. Falls back to plain text if the dedicated text node is missing.
3278 + */
3279 + function aibuiSetBlockModifierFailure(statusTextEl, statusEl, info) {
3280 + const diagnostics = aibuiFormatDiagnostics(info);
3281 + if (statusTextEl) {
3282 + statusTextEl.innerHTML =
3283 + '<span style="display:block;">' +
3284 + aibuiEscapeHtml(info.message) +
3285 + "</span>" +
3286 + '<span style="display:block;margin-top:4px;font-size:10px;line-height:1.4;' +
3287 + "opacity:0.55;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas," +
3288 + 'monospace;word-break:break-word;">' +
3289 + aibuiEscapeHtml(diagnostics) +
3290 + "</span>";
3291 + } else if (statusEl) {
3292 + statusEl.textContent = info.message + " (" + diagnostics + ")";
3293 + }
3294 + console.error("AI Builder failure — " + diagnostics, info.cause || "");
3295 + }
3296 +
3297 + /**
3298 + * Explain a non-OK API response in terms a non-technical user can act on.
3299 + * 401 (no account) and 402 (no credits) have their own dedicated messages and
3300 + * never reach this function.
3301 + *
3302 + * @returns {{message: string, code: string, status: number, requestId: string,
3303 + * detail: string, context: Object}}
3304 + */
3305 + function aibuiDescribeHttpFailure(res, data, context) {
3306 + const status = res && typeof res.status === "number" ? res.status : 0;
3307 +
3308 + let requestId = "";
3309 + try {
3310 + requestId = (res && res.headers && res.headers.get("x-request-id")) || "";
3311 + } catch (e) {
3312 + /* header not exposed by CORS */
3313 + }
3314 + if (!requestId && data && typeof data === "object") {
3315 + requestId = data.requestId || data.request_id || "";
3316 + }
3317 +
3318 + // Whatever the API itself said, for support only.
3319 + let detail = "";
3320 + if (data && typeof data === "object" && (data.message || data.error)) {
3321 + detail = aibuiTruncate(String(data.message || data.error), 160);
3322 + }
3323 +
3324 + let code;
3325 + let message;
3326 +
3327 + if (status === 401) {
3328 + // Normally intercepted by the dedicated "sign in" message; this is the
3329 + // safety net for any path that doesn't check the status itself.
3330 + code = "AIB-AUTH-401";
3331 + message =
3332 + "You are not signed in to an AI Builder account. Open AI Builder → " +
3333 + "Account in your WordPress dashboard to sign in, then try again.";
3334 + } else if (status === 402) {
3335 + code = "AIB-CREDITS-402";
3336 + message =
3337 + "You don't have enough credits left to generate this. Open AI Builder → " +
3338 + "Credits in your WordPress dashboard to top up.";
3339 + } else if (status === 400 || status === 422) {
3340 + code = "AIB-REQ-" + status;
3341 + message =
3342 + "The AI service could not process this request. Try rewording your " +
3343 + "prompt, or making it more specific. " +
3344 + AIBUI_CREDIT_REFUND_NOTE;
3345 + } else if (status === 403) {
3346 + code = "AIB-REQ-403";
3347 + message =
3348 + "The AI service refused this request. A security plugin, a firewall or " +
3349 + "a browser extension on your side may be blocking it. " +
3350 + AIBUI_CREDIT_REFUND_NOTE;
3351 + } else if (status === 404) {
3352 + code = "AIB-REQ-404";
3353 + message =
3354 + "This AI Builder feature is no longer available at that address. Please " +
3355 + "update the AI Builder plugin to the latest version, then try again.";
3356 + } else if (status === 408 || status === 504 || status === 524) {
3357 + code = "AIB-TIMEOUT-" + status;
3358 + message =
3359 + "The generation took too long and the connection closed before the " +
3360 + "result came back. Try again with a shorter prompt, or on an empty " +
3361 + "page. " +
3362 + AIBUI_CREDIT_REFUND_NOTE;
3363 + } else if (status === 413) {
3364 + code = "AIB-SIZE-413";
3365 + message =
3366 + "The page you are working on is too large to send to the AI. Try " +
3367 + "generating on a new empty page, or remove some content first.";
3368 + } else if (status === 429) {
3369 + code = "AIB-RATE-429";
3370 + message =
3371 + "Too many requests were sent in a short time. Please wait a minute and " +
3372 + "try again.";
3373 + } else if (status >= 500) {
3374 + code = "AIB-SERVER-" + status;
3375 + message =
3376 + "The AI service hit an error on our side — this is not caused by your " +
3377 + "website. Please try again in a few minutes. " +
3378 + AIBUI_CREDIT_REFUND_NOTE;
3379 + } else {
3380 + code = "AIB-HTTP-" + (status || "0");
3381 + message =
3382 + "The AI service returned an unexpected answer. Please try again. " +
3383 + AIBUI_CREDIT_REFUND_NOTE;
3384 + }
3385 +
3386 + return { message, code, status, requestId, detail, context };
3387 + }
3388 +
3389 + /**
3390 + * Explain an exception thrown anywhere in a generation flow.
3391 + * Auth errors (`aibuiAuthRequired`) are handled by their own message and never
3392 + * reach this function.
3393 + */
3394 + function aibuiDescribeThrownFailure(err, context) {
3395 + const name = (err && err.name) || "Error";
3396 + const raw = (err && err.message) || String(err || "");
3397 +
3398 + if (err && err.aibuiSessionExpired === true) {
3399 + return {
3400 + code: "AIB-SESSION-01",
3401 + message:
3402 + "Your WordPress session has expired, so this request could not be " +
3403 + "signed. Save your work, reload this page, and try again.",
3404 + context,
3405 + detail: aibuiTruncate(raw, 160),
3406 + cause: err,
3407 + };
3408 + }
3409 +
3410 + if (name === "AbortError") {
3411 + return {
3412 + code: "AIB-TIMEOUT-CLIENT",
3413 + message:
3414 + "The request was interrupted before it finished. Check your internet " +
3415 + "connection and try again. " +
3416 + AIBUI_CREDIT_REFUND_NOTE,
3417 + context,
3418 + detail: aibuiTruncate(raw, 160),
3419 + cause: err,
3420 + };
3421 + }
3422 +
3423 + if (name === "TypeError" && /fetch|network|load failed/i.test(raw)) {
3424 + return {
3425 + code: "AIB-NET-01",
3426 + message:
3427 + "Your browser could not reach the AI Builder service. Check your " +
3428 + "internet connection, then any firewall, ad blocker or security " +
3429 + "plugin that could be blocking api.wordpress-ai-builder.com. " +
3430 + AIBUI_CREDIT_REFUND_NOTE,
3431 + context,
3432 + detail: aibuiTruncate(raw, 160),
3433 + cause: err,
3434 + };
3435 + }
3436 +
3437 + return {
3438 + code: "AIB-JS-01",
3439 + message:
3440 + "Something went wrong inside the editor while handling the AI answer, " +
3441 + "so your page was left untouched. Please try again. " +
3442 + AIBUI_CREDIT_REFUND_NOTE,
3443 + context,
3444 + detail: aibuiTruncate(name + ": " + raw, 160),
3445 + cause: err,
3446 + };
3447 + }
3448 +
3449 + /**
3450 + * Guard run before every API call: without config.js there is no endpoint to
3451 + * call at all, which is almost always a caching / JS-optimisation plugin.
3452 + */
3453 + function aibuiApiUnavailableFailure(context) {
3454 + if (window.config && window.config.apiUrl) return null;
3455 + return {
3456 + code: "AIB-CONF-01",
3457 + message:
3458 + "AI Builder could not load its configuration, so it does not know which " +
3459 + "service to contact. This is usually caused by a caching or " +
3460 + "JavaScript-optimisation plugin: clear your cache, or exclude AI " +
3461 + "Builder from JavaScript minification, then reload this page.",
3462 + context,
3463 + };
3464 + }
3465 +
828 3466 async function sendMessageAIV3() {
829 3467 const messages = document.getElementById("chat-messages");
830 3468 const input = document.querySelector("#chat-input textarea");
831 3469 // const undoBtn = document.getElementById("chat-undo");
832 - const sendBtn = document.querySelector("#chat-input button");
3470 + const sendBtn = document.getElementById("chat-generate-page");
3471 + const blockBtn = document.getElementById("chat-generate-block");
833 3472 const toggleBtn = document.getElementById("chat-toggle");
834 3473 const question = input.value.trim();
835 3474
836 3475 // Vérifier qu'un prompt est présent
@@ -844,9 +3483,10 @@
844 3483 const conversationHistory = getConversationHistoryString(6);
845 3484
846 3485 // 🔒 Désactiver le bouton + animation loading
847 3486 sendBtn.disabled = true;
848 - sendBtn.classList.add("loading");
3487 + if (blockBtn) blockBtn.disabled = true;
3488 + // sendBtn.classList.add("loading");
849 3489 sendBtn.textContent = "Generating...";
850 3490
851 3491 // 🎨 Activer l'effet visuel sur le toggle
852 3492 if (toggleBtn) {
@@ -852,22 +3492,52 @@
852 3492 if (toggleBtn) {
853 3493 toggleBtn.classList.add("generating");
854 3494 }
855 3495
3496 + // 👀 Afficher l'indicateur de saisie de l'IA
3497 + if (typeof showTypingIndicator === "function") {
3498 + showTypingIndicator();
3499 + }
3500 +
856 3501 // Ajouter le message utilisateur à l'historique
857 3502 addMessage(finalQuestion, "user");
858 3503 input.value = "";
3504 + // Réinitialiser le compteur après envoi
3505 + if (typeof updateChatCharCounter === "function") {
3506 + updateChatCharCounter();
3507 + }
859 3508 messages.scrollTop = messages.scrollHeight;
860 3509
861 3510 // Sauvegarder les blocs actuels
862 3511 previousBlocks = wp.data.select("core/block-editor").getBlocks();
863 3512
3513 + // Diagnostics context, filled in as the request progresses so a failure can
3514 + // name the endpoint it was talking to and how long it waited.
3515 + const requestContext = { action: "page generation", startedAt: Date.now() };
3516 +
864 3517 try {
3518 + const shouldProceed = await confirmReplacePageContentIfNeeded(previousBlocks);
3519 + if (!shouldProceed) {
3520 + return;
3521 + }
3522 +
865 3523 // Récupérer le token JWT
866 3524 const jwtToken = await getJwtToken();
867 3525
868 - console.log('window.config: ', window.config);
3526 + const wooCommerceInstalled = !!(typeof aiBuilderEditorVars !== 'undefined' && aiBuilderEditorVars.wooCommerceInstalled && aiBuilderEditorVars.wooCommerceInstalled !== '0');
3527 + const activeThemeName = (typeof aiBuilderEditorVars !== 'undefined' && aiBuilderEditorVars.activeThemeName) ? aiBuilderEditorVars.activeThemeName : ((typeof aiBuilderVars !== 'undefined' && aiBuilderVars.activeThemeName) ? aiBuilderVars.activeThemeName : '');
3528 + const sitePlugins = (typeof aiBuilderVars !== 'undefined' && Array.isArray(aiBuilderVars.sitePlugins)) ? aiBuilderVars.sitePlugins.slice(0, 15) : [];
3529 + const wordpressVersion = (typeof aiBuilderVars !== 'undefined' && aiBuilderVars.wordpressVersion) ? String(aiBuilderVars.wordpressVersion) : '';
3530 + const pageContext = buildPageContext();
3531 + const visualStylePrompt = getVisualStylePromptForApi();
3532 + // return;
869 3533
3534 + const unavailable = aibuiApiUnavailableFailure(requestContext);
3535 + if (unavailable) {
3536 + addFailureMessage(unavailable);
3537 + return;
3538 + }
3539 +
870 3540 let res
871 3541 if (patternName) {
872 3542 const payload = {
873 3543 userPrompt: finalQuestion,
@@ -873,11 +3543,20 @@
873 3543 userPrompt: finalQuestion,
874 3544 // pageContent: pageContent,
875 3545 patternName: patternName,
876 3546 conversationHistory,
3547 + wooCommerceInstalled: wooCommerceInstalled,
3548 + activeThemeName: activeThemeName,
3549 + sitePlugins: sitePlugins,
3550 + wordpressVersion: wordpressVersion,
3551 + pageContext: pageContext || undefined,
3552 + visualStylePrompt: visualStylePrompt || undefined,
877 3553 };
3554 + requestContext.endpoint =
3555 + window.config.apiUrl + "/ai-transform-page/generate-pattern";
3556 + requestContext.startedAt = Date.now();
878 3557 res = await fetch(
879 - window.config.apiUrl + "/ai-transform-page/generate-pattern",
3558 + requestContext.endpoint,
880 3559 {
881 3560 method: "POST",
882 3561 headers: {
883 3562 "Content-Type": "application/json",
@@ -890,11 +3569,20 @@
890 3569 const payload = {
891 3570 userPrompt: finalQuestion,
892 3571 // pageContent: pageContent,
893 3572 conversationHistory,
3573 + wooCommerceInstalled: wooCommerceInstalled,
3574 + activeThemeName: activeThemeName,
3575 + sitePlugins: sitePlugins,
3576 + wordpressVersion: wordpressVersion,
3577 + pageContext: pageContext || undefined,
3578 + visualStylePrompt: visualStylePrompt || undefined,
894 3579 };
3580 + requestContext.endpoint =
3581 + window.config.apiUrl + "/ai-transform-page/v2-page-generation";
3582 + requestContext.startedAt = Date.now();
895 3583 res = await fetch(
896 - window.config.apiUrl + "/ai-transform-page/v2-page-generation",
3584 + requestContext.endpoint,
897 3585 {
898 3586 method: "POST",
899 3587 headers: {
900 3588 "Content-Type": "application/json",
@@ -904,11 +3592,50 @@
904 3592 }
905 3593 );
906 3594 }
907 3595
908 - const data = await res.json();
909 - console.log("data: ", data);
910 - console.log(data.pageContent);
3596 + const responseText = await res.text();
3597 + let data = {};
3598 + let responseUnreadable = false;
3599 + try {
3600 + data = responseText ? JSON.parse(responseText) : {};
3601 + if (!responseText) responseUnreadable = true;
3602 + } catch (parseErr) {
3603 + data = {};
3604 + responseUnreadable = true;
3605 + }
3606 +
3607 + // API auth (passport isAuthenticated): not logged in / not verified → 401 + redirectTo
3608 + if (res.status === 401) {
3609 + addMessage(getAccountRequiredChatMessageHtml(), "assistant", { richHtml: true });
3610 + return;
3611 + }
3612 +
3613 + // API billing: out of credits → 402
3614 + if (res.status === 402) {
3615 + addMessage(getCreditsRequiredChatMessageHtml(), "assistant", { richHtml: true });
3616 + return;
3617 + }
3618 +
3619 + if (!res.ok) {
3620 + addFailureMessage(aibuiDescribeHttpFailure(res, data, requestContext));
3621 + return;
3622 + }
3623 +
3624 + if (responseUnreadable) {
3625 + addFailureMessage({
3626 + code: "AIB-RESP-01",
3627 + message:
3628 + "The AI service answered, but the answer was empty or unreadable. " +
3629 + "Please try again. " +
3630 + AIBUI_CREDIT_REFUND_NOTE,
3631 + context: requestContext,
3632 + status: res.status,
3633 + detail: aibuiTruncate(responseText, 120),
3634 + });
3635 + return;
3636 + }
3637 +
911 3638 if (data.error === "not-enough-credits") {
912 3639 showNotEnoughCreditsToast();
913 3640 return;
914 3641 }
@@ -914,15 +3641,76 @@
914 3641 }
915 3642
916 3643 if (data.pageContent) {
917 3644 if (data.pageContent !== "[-no-content-to-return-]") {
918 - // Convertir chaque bloc JSON en bloc WordPress
919 - const newBlocks = data.pageContent.map(buildBlock);
3645 + if (!Array.isArray(data.pageContent)) {
3646 + addFailureMessage({
3647 + code: "AIB-RESP-03",
3648 + message:
3649 + "The AI returned page content in a format this version of the " +
3650 + "plugin cannot read. Please update AI Builder to the latest " +
3651 + "version; if the problem remains, contact support with the code " +
3652 + "below. " +
3653 + AIBUI_CREDIT_REFUND_NOTE,
3654 + context: requestContext,
3655 + detail: "pageContent is " + typeof data.pageContent,
3656 + });
3657 + return;
3658 + }
920 3659
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);
3660 + // Convertir chaque bloc JSON en bloc WordPress. Un bloc absent de cette
3661 + // installation est ignoré au lieu de faire échouer toute la génération.
3662 + const skippedBlocks = [];
3663 + const newBlocks = data.pageContent
3664 + .map((rawBlock) => buildBlockSafe(rawBlock, skippedBlocks))
3665 + .filter(Boolean);
3666 +
3667 + if (!newBlocks.length) {
3668 + addFailureMessage({
3669 + code: "AIB-BLOCK-01",
3670 + message:
3671 + "The AI returned content your site cannot display: none of the " +
3672 + "generated blocks are available in this WordPress install. A " +
3673 + "theme or a plugin is probably restricting which blocks can be " +
3674 + "used. " +
3675 + AIBUI_CREDIT_REFUND_NOTE,
3676 + context: requestContext,
3677 + detail: "unavailable blocks: " + skippedBlocks.join(", "),
3678 + });
3679 + return;
3680 + }
3681 +
3682 + try {
3683 + // 🔄 Supprimer tous les blocs existants
3684 + wp.data.dispatch("core/block-editor").resetBlocks([]);
3685 + // ➕ Insérer les nouveaux blocs
3686 + wp.data.dispatch("core/block-editor").insertBlocks(newBlocks);
3687 + } catch (insertErr) {
3688 + addFailureMessage({
3689 + code: "AIB-EDITOR-01",
3690 + message:
3691 + "The content was generated but the editor refused to insert it. " +
3692 + "Save your work, reload the editor, and try again. " +
3693 + AIBUI_CREDIT_REFUND_NOTE,
3694 + context: requestContext,
3695 + detail: aibuiTruncate(insertErr && insertErr.message, 160),
3696 + cause: insertErr,
3697 + });
3698 + return;
3699 + }
3700 +
3701 + if (skippedBlocks.length) {
3702 + addFailureMessage({
3703 + code: "AIB-BLOCK-02",
3704 + message:
3705 + "Your page was generated, but " +
3706 + skippedBlocks.length +
3707 + " block(s) could not be added because they are not available on " +
3708 + "this site. The rest of the page is intact.",
3709 + context: requestContext,
3710 + detail: "skipped: " + skippedBlocks.join(", "),
3711 + });
3712 + }
925 3713 }
926 3714
927 3715 // Handle CSS if present
928 3716 if (data.cssContent && data.cssContent !== "[-no-content-to-return-]") {
@@ -933,12 +3721,24 @@
933 3721
934 3722 // Recharger le CSS combiné depuis le serveur
935 3723 await loadCSSFromPostMeta();
936 3724
937 - // Afficher le bouton CSS
938 - cssEditButton.style.display = "block";
3725 + // Le bouton CSS est toujours visible
939 3726 }
940 3727
3728 + // Handle JS if present
3729 + if (data.jsContent && data.jsContent !== "[-no-content-to-return-]") {
3730 + console.log("Injecting JS...");
3731 +
3732 + // Sauvegarder le JS dans les meta du post (type 'page' pour le chat widget)
3733 + await saveJSInPostMeta(data.jsContent, "page");
3734 +
3735 + // Recharger le JS combiné depuis le serveur
3736 + await loadJSFromPostMeta();
3737 +
3738 + // Le bouton JS est toujours visible
3739 + }
3740 +
941 3741 // Handle meta description if present
942 3742 if (data.postMetaDesc && data.postMetaDesc !== "[-no-content-to-return-]") {
943 3743 // await saveMetaDescriptionInPostMeta(data.postMetaDesc);
944 3744 await setMetaDescriptionField(data.postMetaDesc);
@@ -965,18 +3765,31 @@
965 3765 if (typeof data.creditsLeft !== "undefined") {
966 3766 updateCreditsDisplay(data.creditsLeft);
967 3767 }
968 3768 } else {
969 - addMessage("Empty or invalid response.", "assistant");
3769 + addFailureMessage({
3770 + code: "AIB-RESP-02",
3771 + message:
3772 + "The AI service replied without any page content. Please try again, " +
3773 + "and if it keeps happening try rewording your prompt. " +
3774 + AIBUI_CREDIT_REFUND_NOTE,
3775 + context: requestContext,
3776 + status: res.status,
3777 + });
970 3778 }
971 3779 } catch (err) {
972 - addMessage("Internal server error", "assistant");
973 - console.log("err : ", err);
3780 + // WordPress has no stored API token / missing editor context (not linked to an AI Builder account)
3781 + if (err && err.aibuiAuthRequired === true) {
3782 + addMessage(getAccountRequiredChatMessageHtml(), "assistant", { richHtml: true });
3783 + } else {
3784 + addFailureMessage(aibuiDescribeThrownFailure(err, requestContext));
3785 + }
974 3786 } finally {
975 3787 // 🔓 Réactiver le bouton + retirer animation
976 3788 sendBtn.disabled = false;
3789 + if (blockBtn) blockBtn.disabled = false;
977 3790 sendBtn.classList.remove("loading");
978 - sendBtn.textContent = "Generate";
3791 + sendBtn.textContent = "Generate page";
979 3792 messages.scrollTop = messages.scrollHeight;
980 3793
981 3794 // 🎨 Désactiver l'effet visuel sur le toggle
982 3795 if (toggleBtn) {
@@ -981,53 +3794,937 @@
981 3794 // 🎨 Désactiver l'effet visuel sur le toggle
982 3795 if (toggleBtn) {
983 3796 toggleBtn.classList.remove("generating");
984 3797 }
3798 +
3799 + // 👀 Masquer l'indicateur de saisie de l'IA
3800 + if (typeof hideTypingIndicator === "function") {
3801 + hideTypingIndicator();
3802 + }
985 3803 }
986 3804 }
987 3805
988 - sendBtn.onclick = sendMessageAIV3;
3806 + async function sendBlockAIV3() {
3807 + const messages = document.getElementById("chat-messages");
3808 + const input = document.querySelector("#chat-input textarea");
3809 + const pageBtn = document.getElementById("chat-generate-page");
3810 + const blockBtn = document.getElementById("chat-generate-block");
3811 + const toggleBtn = document.getElementById("chat-toggle");
3812 + const question = input.value.trim();
3813 +
3814 + if (!question) {
3815 + addMessage("Please enter a prompt first.", "assistant");
3816 + return;
3817 + }
3818 +
3819 + // Disable both buttons while generating
3820 + if (pageBtn) pageBtn.disabled = true;
3821 + if (blockBtn) blockBtn.disabled = true;
3822 + if (blockBtn) blockBtn.textContent = "Generating...";
3823 +
3824 + if (toggleBtn) {
3825 + toggleBtn.classList.add("generating");
3826 + }
3827 + if (typeof showTypingIndicator === "function") {
3828 + showTypingIndicator();
3829 + }
3830 +
3831 + // Add user message and clear input
3832 + addMessage(question, "user");
3833 + input.value = "";
3834 + if (typeof updateChatCharCounter === "function") {
3835 + updateChatCharCounter();
3836 + }
3837 + if (messages) messages.scrollTop = messages.scrollHeight;
3838 +
3839 + // Diagnostics context, see sendMessageAIV3.
3840 + const requestContext = { action: "block generation", startedAt: Date.now() };
3841 +
3842 + try {
3843 + const jwtToken = await getJwtToken();
3844 +
3845 + const wooCommerceInstalled = !!(typeof aiBuilderEditorVars !== 'undefined' && aiBuilderEditorVars.wooCommerceInstalled && aiBuilderEditorVars.wooCommerceInstalled !== '0');
3846 + const activeThemeName = (typeof aiBuilderEditorVars !== 'undefined' && aiBuilderEditorVars.activeThemeName)
3847 + ? aiBuilderEditorVars.activeThemeName
3848 + : ((typeof aiBuilderVars !== 'undefined' && aiBuilderVars.activeThemeName) ? aiBuilderVars.activeThemeName : '');
3849 + const sitePlugins = (typeof aiBuilderVars !== 'undefined' && Array.isArray(aiBuilderVars.sitePlugins)) ? aiBuilderVars.sitePlugins.slice(0, 15) : [];
3850 + const wordpressVersion = (typeof aiBuilderVars !== 'undefined' && aiBuilderVars.wordpressVersion) ? String(aiBuilderVars.wordpressVersion) : '';
3851 + const conversationHistory = getConversationHistoryString(6);
3852 + const visualStylePrompt = getVisualStylePromptForApi();
3853 +
3854 + const unavailable = aibuiApiUnavailableFailure(requestContext);
3855 + if (unavailable) {
3856 + addFailureMessage(unavailable);
3857 + return;
3858 + }
3859 +
3860 + requestContext.endpoint =
3861 + window.config.apiUrl + "/ai-transform-page/v2-generate-single-block";
3862 + requestContext.startedAt = Date.now();
3863 + const res = await fetch(
3864 + requestContext.endpoint,
3865 + {
3866 + method: "POST",
3867 + headers: {
3868 + "Content-Type": "application/json",
3869 + Authorization: `Bearer ${jwtToken}`,
3870 + },
3871 + body: JSON.stringify({
3872 + prompt: question,
3873 + wooCommerceInstalled: wooCommerceInstalled,
3874 + activeThemeName: activeThemeName,
3875 + sitePlugins: sitePlugins,
3876 + wordpressVersion: wordpressVersion,
3877 + visualStylePrompt: visualStylePrompt || undefined,
3878 + }),
3879 + }
3880 + );
3881 +
3882 + const responseText = await res.text();
3883 + let data = {};
3884 + let responseUnreadable = false;
3885 + try {
3886 + data = responseText ? JSON.parse(responseText) : {};
3887 + if (!responseText) responseUnreadable = true;
3888 + } catch (e) {
3889 + data = {};
3890 + responseUnreadable = true;
3891 + }
3892 +
3893 + // API auth (passport isAuthenticated): not logged in / not verified → 401
3894 + if (res.status === 401) {
3895 + addMessage(getAccountRequiredChatMessageHtml(), "assistant", { richHtml: true });
3896 + return;
3897 + }
3898 +
3899 + // API billing: out of credits → 402
3900 + if (res.status === 402) {
3901 + addMessage(getCreditsRequiredChatMessageHtml(), "assistant", { richHtml: true });
3902 + return;
3903 + }
3904 +
3905 + if (!res.ok) {
3906 + addFailureMessage(aibuiDescribeHttpFailure(res, data, requestContext));
3907 + return;
3908 + }
3909 +
3910 + if (responseUnreadable) {
3911 + addFailureMessage({
3912 + code: "AIB-RESP-01",
3913 + message:
3914 + "The AI service answered, but the answer was empty or unreadable. " +
3915 + "Please try again. " +
3916 + AIBUI_CREDIT_REFUND_NOTE,
3917 + context: requestContext,
3918 + status: res.status,
3919 + detail: aibuiTruncate(responseText, 120),
3920 + });
3921 + return;
3922 + }
3923 +
3924 + if (data.error === "not-enough-credits") {
3925 + showNotEnoughCreditsToast();
3926 + return;
3927 + }
3928 +
3929 + if (!data.blockContent) {
3930 + addFailureMessage({
3931 + code: "AIB-RESP-02",
3932 + message:
3933 + "The AI service replied without any block content. Please try again, " +
3934 + "and if it keeps happening try rewording your instruction. " +
3935 + AIBUI_CREDIT_REFUND_NOTE,
3936 + context: requestContext,
3937 + status: res.status,
3938 + });
3939 + return;
3940 + }
3941 +
3942 + // Save and reload CSS/JS if provided (block-level)
3943 + if (data.cssContent && data.cssContent !== "[-no-content-to-return-]") {
3944 + await saveCSSInPostMeta(data.cssContent, "block");
3945 + await loadCSSFromPostMeta();
3946 + }
3947 + if (data.jsContent && data.jsContent !== "[-no-content-to-return-]") {
3948 + await saveJSInPostMeta(data.jsContent, "block");
3949 + await loadJSFromPostMeta();
3950 + }
3951 +
3952 + // Insert generated block at the bottom without touching existing content
3953 + const skippedBlocks = [];
3954 + const newBlock = buildBlockSafe(data.blockContent, skippedBlocks);
3955 +
3956 + if (!newBlock) {
3957 + addFailureMessage({
3958 + code: "AIB-BLOCK-01",
3959 + message:
3960 + "The AI returned a block your site cannot display: it is not " +
3961 + "available in this WordPress install. A theme or a plugin is " +
3962 + "probably restricting which blocks can be used. " +
3963 + AIBUI_CREDIT_REFUND_NOTE,
3964 + context: requestContext,
3965 + detail: "unavailable blocks: " + skippedBlocks.join(", "),
3966 + });
3967 + return;
3968 + }
3969 +
3970 + try {
3971 + const existing = wp.data.select("core/block-editor").getBlocks();
3972 + const insertIndex = Array.isArray(existing) ? existing.length : undefined;
3973 + wp.data.dispatch("core/block-editor").insertBlocks([newBlock], insertIndex);
3974 + } catch (insertErr) {
3975 + addFailureMessage({
3976 + code: "AIB-EDITOR-01",
3977 + message:
3978 + "The block was generated but the editor refused to insert it. Save " +
3979 + "your work, reload the editor, and try again. " +
3980 + AIBUI_CREDIT_REFUND_NOTE,
3981 + context: requestContext,
3982 + detail: aibuiTruncate(insertErr && insertErr.message, 160),
3983 + cause: insertErr,
3984 + });
3985 + return;
3986 + }
3987 +
3988 + if (skippedBlocks.length) {
3989 + addFailureMessage({
3990 + code: "AIB-BLOCK-02",
3991 + message:
3992 + "The block was added, but " +
3993 + skippedBlocks.length +
3994 + " nested element(s) had to be left out because they are not " +
3995 + "available on this site.",
3996 + context: requestContext,
3997 + detail: "skipped: " + skippedBlocks.join(", "),
3998 + });
3999 + }
4000 +
4001 + if (typeof data.creditsLeft !== "undefined") {
4002 + try {
4003 + updateCreditsDisplay(data.creditsLeft);
4004 + } catch (e) { }
4005 + }
4006 +
4007 + const aiResponse = data.aiResponse || "Block added to the bottom of the page.";
4008 + addMessage(aiResponse, "assistant");
4009 + } catch (err) {
4010 + if (err && err.aibuiAuthRequired === true) {
4011 + addMessage(getAccountRequiredChatMessageHtml(), "assistant", { richHtml: true });
4012 + } else {
4013 + addFailureMessage(aibuiDescribeThrownFailure(err, requestContext));
4014 + }
4015 + } finally {
4016 + if (pageBtn) pageBtn.disabled = false;
4017 + if (blockBtn) blockBtn.disabled = false;
4018 + if (blockBtn) blockBtn.textContent = "Generate block";
4019 + if (toggleBtn) toggleBtn.classList.remove("generating");
4020 + if (typeof hideTypingIndicator === "function") {
4021 + hideTypingIndicator();
4022 + }
4023 + if (messages) messages.scrollTop = messages.scrollHeight;
4024 + }
4025 + }
4026 +
4027 + let cachedAICreatedStatus = null;
4028 +
4029 + async function getCurrentPostAICreatedStatus() {
4030 + if (cachedAICreatedStatus !== null) return cachedAICreatedStatus;
4031 + try {
4032 + const postId =
4033 + wp?.data?.select("core/editor")?.getCurrentPostId?.() ||
4034 + new URL(window.location.href, window.location.origin).searchParams.get("post") ||
4035 + new URL(window.location.href, window.location.origin).searchParams.get("postId") ||
4036 + new URL(window.location.href, window.location.origin).searchParams.get("p");
4037 + if (!postId) return false;
4038 +
4039 + const formData = new FormData();
4040 + formData.append("action", "aibui_get_ai_created_status");
4041 + formData.append("post_id", postId);
4042 + formData.append("nonce", aiBuilderVars.nonce);
4043 +
4044 + const response = await fetch(ajaxurl, { method: "POST", body: formData });
4045 + const result = await response.json();
4046 + cachedAICreatedStatus = !!(result && result.success && result.data && result.data.isAICreated);
4047 + return cachedAICreatedStatus;
4048 + } catch (e) {
4049 + return false;
4050 + }
4051 + }
4052 +
4053 + function doesPageHaveExistingContent(blocks) {
4054 + try {
4055 + if (!Array.isArray(blocks) || blocks.length === 0) return false;
4056 + // Treat any non-empty block list as existing content.
4057 + // (We can refine later to ignore purely empty placeholders.)
4058 + return true;
4059 + } catch (e) {
4060 + return false;
4061 + }
4062 + }
4063 +
4064 + function ensureReplaceContentModal() {
4065 + let modal = document.getElementById("aibui-replace-content-modal");
4066 + if (modal) return modal;
4067 +
4068 + modal = document.createElement("div");
4069 + modal.id = "aibui-replace-content-modal";
4070 + modal.setAttribute("hidden", "");
4071 + modal.innerHTML = `
4072 + <div class="aibui-modal-backdrop" data-aibui-modal-close></div>
4073 + <div class="aibui-modal-panel" role="dialog" aria-modal="true" aria-labelledby="aibui-replace-content-title">
4074 + <h3 id="aibui-replace-content-title" class="aibui-modal-title">Warning</h3>
4075 + <p class="aibui-modal-text">
4076 + This page content will be replaced by AI-generated content.
4077 + If you don’t want to replace it and would rather add new content, use <strong>Generate block</strong> to append content without overwriting what’s already on the page.
4078 + </p>
4079 + <div class="aibui-modal-actions">
4080 + <button type="button" class="aibui-btn aibui-btn--ghost" data-aibui-cancel>Cancel</button>
4081 + <button type="button" class="aibui-btn aibui-btn--primary" data-aibui-confirm>Replace content</button>
4082 + </div>
4083 + </div>
4084 + `;
4085 + document.body.appendChild(modal);
4086 + return modal;
4087 + }
4088 +
4089 + function openReplaceContentModal() {
4090 + const modal = ensureReplaceContentModal();
4091 + modal.removeAttribute("hidden");
4092 + return modal;
4093 + }
4094 +
4095 + function closeReplaceContentModal() {
4096 + const modal = document.getElementById("aibui-replace-content-modal");
4097 + if (!modal) return;
4098 + modal.setAttribute("hidden", "");
4099 + }
4100 +
4101 + async function confirmReplacePageContentIfNeeded(existingBlocks) {
4102 + // Only warn if the editor already has content and it wasn't generated by AI Builder.
4103 + if (!doesPageHaveExistingContent(existingBlocks)) return true;
4104 + const isAICreated = await getCurrentPostAICreatedStatus();
4105 + if (isAICreated) return true;
4106 +
4107 + const modal = openReplaceContentModal();
4108 +
4109 + return await new Promise((resolve) => {
4110 + const confirmBtn = modal.querySelector("[data-aibui-confirm]");
4111 + const cancelBtn = modal.querySelector("[data-aibui-cancel]");
4112 + const backdrop = modal.querySelector("[data-aibui-modal-close]");
4113 +
4114 + function cleanup() {
4115 + if (confirmBtn) confirmBtn.removeEventListener("click", onConfirm);
4116 + if (cancelBtn) cancelBtn.removeEventListener("click", onCancel);
4117 + if (backdrop) backdrop.removeEventListener("click", onCancel);
4118 + document.removeEventListener("keydown", onKeyDown);
4119 + }
4120 +
4121 + function onConfirm() {
4122 + cleanup();
4123 + closeReplaceContentModal();
4124 + resolve(true);
4125 + }
4126 +
4127 + function onCancel() {
4128 + cleanup();
4129 + closeReplaceContentModal();
4130 + resolve(false);
4131 + }
4132 +
4133 + function onKeyDown(ev) {
4134 + if (ev.key === "Escape") onCancel();
4135 + }
4136 +
4137 + if (confirmBtn) confirmBtn.addEventListener("click", onConfirm);
4138 + if (cancelBtn) cancelBtn.addEventListener("click", onCancel);
4139 + if (backdrop) backdrop.addEventListener("click", onCancel);
4140 + document.addEventListener("keydown", onKeyDown);
4141 + });
4142 + }
4143 +
4144 + const pageBtn = document.getElementById("chat-generate-page");
4145 + const generateBlockBtn = document.getElementById("chat-generate-block");
4146 + if (pageBtn) pageBtn.onclick = sendMessageAIV3;
4147 + if (generateBlockBtn) generateBlockBtn.onclick = sendBlockAIV3;
989 4148 input.addEventListener("keypress", (e) => {
990 4149 if (e.key === "Enter") sendMessageAIV3();
991 4150 });
4151 + initChatVisualStylePicker();
4152 + initAibuiBlockModifier();
992 4153 loadUserCredits();
993 4154 loadCSSFromPostMeta();
4155 + loadJSFromPostMeta();
994 4156 });
995 4157
996 4158 // Ajouter les styles CSS pour le bouton CSS et la modale
997 4159 const cssStyles = document.createElement("style");
998 4160 cssStyles.textContent = `
999 - /* Header du chat avec bouton CSS intégré */
4161 + /* Zone de saisie du chat */
4162 + #chat-input {
4163 + display: flex;
4164 + flex-direction: column;
4165 + gap: 6px;
4166 + }
4167 +
4168 + #chat-input .chat-input-wrap{
4169 + position: relative;
4170 + width: 100%;
4171 + }
4172 +
4173 + #chat-input .chat-actions{
4174 + display:flex;
4175 + gap:8px;
4176 + align-items:center;
4177 + flex-wrap: wrap;
4178 + }
4179 +
4180 + #chat-input #chat-generate-page{
4181 + flex: 1 1 auto;
4182 + min-width: 160px;
4183 + }
4184 +
4185 + #chat-input #chat-generate-block{
4186 + flex: 0 0 auto;
4187 + }
4188 +
4189 + #chat-input .chat-generate-block{
4190 + background: rgba(255,255,255,0.9);
4191 + border: 1px solid #e2e8f0;
4192 + color: #0f172a;
4193 + }
4194 +
4195 + #chat-input .chat-generate-block:hover{
4196 + background: #ffffff;
4197 + }
4198 +
4199 + #chat-input textarea {
4200 + width: 100%;
4201 + resize: vertical;
4202 + }
4203 +
4204 + #chat-expand-input.chat-expand-input{
4205 + position: absolute;
4206 + right: 8px;
4207 + bottom: 8px;
4208 + width: 26px;
4209 + height: 26px;
4210 + min-width: 26px;
4211 + min-height: 26px;
4212 + max-width: 26px;
4213 + max-height: 26px;
4214 + aspect-ratio: 1 / 1;
4215 + box-sizing: border-box;
4216 + border-radius: 8px;
4217 + border: 1px solid rgba(148, 163, 184, 0.8);
4218 + background: rgba(241, 245, 249, 0.95);
4219 + color: #475569;
4220 + cursor: pointer;
4221 + padding: 0;
4222 + display: inline-flex;
4223 + align-items: center;
4224 + justify-content: center;
4225 + line-height: 0;
4226 + box-shadow: 0 1px 2px rgba(0,0,0,0.06);
4227 + }
4228 + #chat-expand-input.chat-expand-input:hover{
4229 + background: #ffffff;
4230 + border-color: rgba(100, 116, 139, 0.6);
4231 + }
4232 + #chat-expand-input.chat-expand-input:active{
4233 + transform: translateY(1px);
4234 + }
4235 + #chat-expand-input.chat-expand-input::before{
4236 + content: "⤢";
4237 + font-size: 14px;
4238 + line-height: 1;
4239 + display:block;
4240 + transform: translateY(-1px);
4241 + }
4242 +
4243 + #chat-char-counter {
4244 + align-self: flex-end;
4245 + font-size: 11px;
4246 + color: #888;
4247 + }
4248 +
4249 + /* Expanded prompt editor modal */
4250 + #chat-expand-modal[hidden]{ display:none !important; }
4251 + #chat-expand-modal.chat-expand-modal{
4252 + position: fixed;
4253 + inset: 0;
4254 + z-index: 10020;
4255 + }
4256 + #chat-expand-modal .chat-expand-backdrop{
4257 + position:absolute;
4258 + inset:0;
4259 + background: rgba(15, 23, 42, 0.55);
4260 + border: none;
4261 + padding: 0;
4262 + cursor: default;
4263 + }
4264 + #chat-expand-modal .chat-expand-panel{
4265 + position: absolute;
4266 + left: 50%;
4267 + top: 50%;
4268 + transform: translate(-50%, -50%);
4269 + width: min(900px, calc(100vw - 32px));
4270 + height: min(70vh, 560px);
4271 + background: #ffffff;
4272 + border-radius: 14px;
4273 + border: 1px solid rgba(226, 232, 240, 0.95);
4274 + box-shadow: 0 18px 60px rgba(0,0,0,0.24);
4275 + display:flex;
4276 + flex-direction: column;
4277 + overflow:hidden;
4278 + }
4279 + #chat-expand-modal .chat-expand-head{
4280 + display:flex;
4281 + align-items:center;
4282 + justify-content: space-between;
4283 + gap: 12px;
4284 + padding: 12px 14px;
4285 + border-bottom: 1px solid #eef2f7;
4286 + background: #f8fafc;
4287 + }
4288 + #chat-expand-modal .chat-expand-title{
4289 + margin: 0;
4290 + font-size: 14px;
4291 + font-weight: 800;
4292 + color:#0f172a;
4293 + }
4294 + #chat-expand-modal .chat-expand-close{
4295 + appearance:none;
4296 + border:none;
4297 + background:transparent;
4298 + font-size: 22px;
4299 + line-height: 1;
4300 + cursor:pointer;
4301 + color:#334155;
4302 + width:32px;
4303 + height:32px;
4304 + display:flex;
4305 + align-items:center;
4306 + justify-content:center;
4307 + border-radius: 10px;
4308 + }
4309 + #chat-expand-modal .chat-expand-close:hover{ background:#e2e8f0; }
4310 + #chat-expand-modal .chat-expand-textarea{
4311 + flex: 1 1 auto;
4312 + width: 100%;
4313 + box-sizing: border-box;
4314 + border: none;
4315 + outline: none;
4316 + padding: 14px;
4317 + resize: none;
4318 + font-size: 14px;
4319 + line-height: 1.5;
4320 + }
4321 + #chat-expand-modal .chat-expand-foot{
4322 + display:flex;
4323 + align-items:center;
4324 + justify-content: space-between;
4325 + gap: 10px;
4326 + padding: 10px 14px;
4327 + border-top: 1px solid #eef2f7;
4328 + background: #ffffff;
4329 + }
4330 + #chat-expand-modal .chat-expand-hint{
4331 + font-size: 12px;
4332 + color:#64748b;
4333 + }
4334 + #chat-expand-modal .chat-expand-done{
4335 + appearance:none;
4336 + border: 1px solid #0ea5e9;
4337 + background: #0ea5e9;
4338 + color: #fff;
4339 + border-radius: 10px;
4340 + padding: 9px 12px;
4341 + font-size: 13px;
4342 + font-weight: 800;
4343 + cursor: pointer;
4344 + }
4345 + #chat-expand-modal .chat-expand-done:hover{ filter: brightness(0.98); }
4346 +
4347 + /* Indicateur de saisie de l'IA */
4348 + #chat-typing-indicator {
4349 + display: inline-flex;
4350 + align-items: center;
4351 + gap: 4px;
4352 + padding: 6px 10px;
4353 + border-radius: 12px;
4354 + background-color: #f1f3f5;
4355 + color: #555;
4356 + font-size: 12px;
4357 + max-width: 80%;
4358 + margin-top: 4px;
4359 + }
4360 +
4361 + #chat-typing-indicator .chat-gen-status {
4362 + margin-left: 8px;
4363 + opacity: 0.75;
4364 + transition: opacity 220ms ease;
4365 + white-space: nowrap;
4366 + overflow: hidden;
4367 + text-overflow: ellipsis;
4368 + max-width: 260px;
4369 + }
4370 +
4371 + #chat-typing-indicator .chat-gen-status.is-fading {
4372 + opacity: 0;
4373 + }
4374 +
4375 + #chat-typing-indicator .typing-dot {
4376 + width: 6px;
4377 + height: 6px;
4378 + border-radius: 50%;
4379 + background-color: #888;
4380 + display: inline-block;
4381 + animation: ai-typing-bounce 1s infinite ease-in-out;
4382 + }
4383 +
4384 + #chat-typing-indicator .typing-dot:nth-child(2) {
4385 + animation-delay: 0.15s;
4386 + }
4387 +
4388 + #chat-typing-indicator .typing-dot:nth-child(3) {
4389 + animation-delay: 0.3s;
4390 + }
4391 +
4392 + @keyframes ai-typing-bounce {
4393 + 0%, 60%, 100% {
4394 + transform: translateY(0);
4395 + opacity: 0.4;
4396 + }
4397 + 30% {
4398 + transform: translateY(-4px);
4399 + opacity: 1;
4400 + }
4401 + }
4402 +
4403 + /* Confirm replace content modal (before generation) */
4404 + #aibui-replace-content-modal[hidden] { display: none !important; }
4405 + #aibui-replace-content-modal {
4406 + position: fixed;
4407 + inset: 0;
4408 + z-index: 999999;
4409 + }
4410 + #aibui-replace-content-modal .aibui-modal-backdrop{
4411 + position:absolute; inset:0;
4412 + background: rgba(15, 23, 42, 0.52);
4413 + backdrop-filter: blur(2px);
4414 + }
4415 + #aibui-replace-content-modal .aibui-modal-panel{
4416 + position: relative;
4417 + width: min(560px, calc(100vw - 32px));
4418 + margin: 10vh auto 0;
4419 + background: #fff;
4420 + border-radius: 14px;
4421 + box-shadow: 0 18px 60px rgba(0,0,0,0.22);
4422 + padding: 16px 16px 14px;
4423 + border: 1px solid rgba(226,232,240,0.9);
4424 + }
4425 + #aibui-replace-content-modal .aibui-modal-title{
4426 + margin: 0 0 8px 0;
4427 + font-size: 16px;
4428 + line-height: 1.25;
4429 + font-weight: 800;
4430 + color: #0f172a;
4431 + }
4432 + #aibui-replace-content-modal .aibui-modal-text{
4433 + margin: 0 0 14px 0;
4434 + font-size: 13px;
4435 + line-height: 1.6;
4436 + color: #334155;
4437 + }
4438 + #aibui-replace-content-modal .aibui-modal-actions{
4439 + display:flex;
4440 + gap: 10px;
4441 + align-items: center;
4442 + justify-content: flex-start;
4443 + flex-wrap: wrap;
4444 + }
4445 + #aibui-replace-content-modal .aibui-btn{
4446 + appearance:none;
4447 + border: 1px solid transparent;
4448 + border-radius: 10px;
4449 + padding: 8px 12px;
4450 + font-size: 13px;
4451 + font-weight: 700;
4452 + cursor: pointer;
4453 + }
4454 + #aibui-replace-content-modal .aibui-btn--primary{
4455 + background:#0ea5e9;
4456 + color:#fff;
4457 + }
4458 + #aibui-replace-content-modal .aibui-btn--primary:hover{ filter: brightness(0.98); }
4459 + #aibui-replace-content-modal .aibui-btn--secondary{
4460 + background:#10b981;
4461 + color:#fff;
4462 + }
4463 + #aibui-replace-content-modal .aibui-btn--secondary:hover{ filter: brightness(0.98); }
4464 + #aibui-replace-content-modal .aibui-btn--ghost{
4465 + background: transparent;
4466 + border-color: #e2e8f0;
4467 + color: #0f172a;
4468 + }
4469 + #aibui-replace-content-modal .aibui-btn--ghost:hover{ background: #f8fafc; }
4470 +
4471 + /* AI Block modifier mini panel */
4472 + #aibui-block-modifier[hidden]{ display:none !important; }
4473 + #aibui-block-modifier{
4474 + position: fixed;
4475 + top: 0;
4476 + left: 0;
4477 + width:320px;
4478 + z-index: 9001;
4479 + background:#ffffff;
4480 + border: 1px solid rgba(226,232,240,0.95);
4481 + border-radius: 14px;
4482 + box-shadow: 0 18px 60px rgba(0,0,0,0.14);
4483 + overflow:hidden;
4484 + will-change: transform;
4485 + }
4486 + #aibui-block-modifier .aibui-bm-head{
4487 + display:flex;
4488 + align-items:center;
4489 + justify-content: space-between;
4490 + gap:10px;
4491 + padding:10px 12px;
4492 + border-bottom:1px solid #eef2f7;
4493 + background: #f8fafc;
4494 + }
4495 + #aibui-block-modifier .aibui-bm-title{
4496 + font-size: 13px;
4497 + font-weight: 800;
4498 + color:#0f172a;
4499 + }
4500 + #aibui-block-modifier .aibui-bm-close{
4501 + appearance:none;
4502 + border:none;
4503 + background:transparent;
4504 + font-size: 18px;
4505 + line-height: 1;
4506 + cursor:pointer;
4507 + color:#334155;
4508 + width:28px;
4509 + height:28px;
4510 + display:flex;
4511 + align-items:center;
4512 + justify-content:center;
4513 + border-radius: 8px;
4514 + }
4515 + #aibui-block-modifier .aibui-bm-close:hover{ background:#e2e8f0; }
4516 + #aibui-block-modifier .aibui-bm-body{ padding: 12px; }
4517 + #aibui-block-modifier .aibui-bm-label{
4518 + display:block;
4519 + font-size: 12px;
4520 + font-weight: 700;
4521 + color:#334155;
4522 + margin: 0 0 6px;
4523 + }
4524 + #aibui-block-modifier .aibui-bm-prompt{
4525 + width:100%;
4526 + box-sizing:border-box;
4527 + resize: vertical;
4528 + border: 1px solid #e2e8f0;
4529 + border-radius: 10px;
4530 + padding: 10px 10px;
4531 + font-size: 13px;
4532 + line-height: 1.4;
4533 + }
4534 + #aibui-block-modifier .aibui-bm-meta{
4535 + margin-top: 6px;
4536 + display:flex;
4537 + justify-content:flex-end;
4538 + font-size: 11px;
4539 + color:#64748b;
4540 + user-select:none;
4541 + }
4542 + #aibui-block-modifier .aibui-bm-meta.aibui-bm-meta--max{
4543 + color:#b45309;
4544 + font-weight: 700;
4545 + }
4546 + #aibui-block-modifier .aibui-bm-actions{
4547 + margin-top: 10px;
4548 + display:flex;
4549 + justify-content:flex-end;
4550 + }
4551 + #aibui-block-modifier .aibui-bm-btn{
4552 + appearance:none;
4553 + border:1px solid transparent;
4554 + border-radius: 10px;
4555 + padding: 9px 12px;
4556 + font-size: 13px;
4557 + font-weight: 800;
4558 + cursor:pointer;
4559 + }
4560 + #aibui-block-modifier .aibui-bm-btn--primary{
4561 + background:#0ea5e9;
4562 + color:#fff;
4563 + }
4564 + #aibui-block-modifier .aibui-bm-btn--primary:hover{ filter: brightness(0.98); }
4565 + #aibui-block-modifier .aibui-bm-status{
4566 + margin-top: 8px;
4567 + font-size: 12px;
4568 + color:#64748b;
4569 + min-height: 16px;
4570 + }
4571 + #aibui-block-modifier .aibui-bm-status-dots{
4572 + display:inline-flex;
4573 + align-items:center;
4574 + gap:4px;
4575 + margin-right: 8px;
4576 + vertical-align: middle;
4577 + }
4578 + #aibui-block-modifier .aibui-bm-status-dots[hidden]{
4579 + display:none !important;
4580 + }
4581 + #aibui-block-modifier .aibui-bm-status-dots .typing-dot{
4582 + width: 6px;
4583 + height: 6px;
4584 + border-radius: 50%;
4585 + background-color: #64748b;
4586 + display: inline-block;
4587 + animation: ai-typing-bounce 1s infinite ease-in-out;
4588 + }
4589 + #aibui-block-modifier .aibui-bm-status-dots .typing-dot:nth-child(2){ animation-delay: 0.15s; }
4590 + #aibui-block-modifier .aibui-bm-status-dots .typing-dot:nth-child(3){ animation-delay: 0.3s; }
4591 +
4592 + /* Small "Modify with AI" button (shown on group selection) */
4593 + #aibui-block-modifier-btn[hidden]{ display:none !important; }
4594 + #aibui-block-modifier-btn.aibui-bm-fab{
4595 + position: fixed;
4596 + top: 0;
4597 + left: 0;
4598 + z-index: 9000;
4599 + height: 32px;
4600 + padding: 0 10px;
4601 + border-radius: 999px;
4602 + background: rgba(15, 23, 42, 0.92);
4603 + color: #fff;
4604 + border: 1px solid rgba(148, 163, 184, 0.35);
4605 + font-size: 12px;
4606 + font-weight: 800;
4607 + cursor: pointer;
4608 + box-shadow: 0 10px 30px rgba(0,0,0,0.14);
4609 + white-space: nowrap;
4610 + will-change: transform;
4611 + }
4612 + #aibui-block-modifier-btn.aibui-bm-fab:hover{
4613 + filter: brightness(1.03);
4614 + }
4615 +
4616 + /* Header du chat: layout propre et actions cohérentes */
1000 4617 #chat-header {
4618 + position: relative;
1001 4619 display: flex;
1002 4620 justify-content: space-between;
1003 4621 align-items: flex-start;
1004 - gap: 10px;
4622 + gap: 16px;
4623 + text-align: left;
4624 + padding-right: 42px;
1005 4625 }
1006 4626
4627 + #chat-header .chat-close{
4628 + position: absolute;
4629 + top: 5px;
4630 + right: 5px;
4631 + appearance: none;
4632 + border: 1px solid rgba(255, 255, 255, 0);
4633 + background: rgba(255, 255, 255, 0);
4634 + color: #fff;
4635 + width: 20px;
4636 + height: 20px;
4637 + min-width: 20px;
4638 + min-height: 20px;
4639 + max-width: 20px;
4640 + max-height: 20px;
4641 + aspect-ratio: 1 / 1;
4642 + box-sizing: border-box;
4643 + border-radius: 10px;
4644 + cursor: pointer;
4645 + display: inline-flex;
4646 + align-items: center;
4647 + justify-content: center;
4648 + font-size: 18px;
4649 + line-height: 1;
4650 + padding: 0;
4651 + flex-shrink: 0;
4652 + z-index: 1;
4653 + }
4654 + #chat-header .chat-close:hover{
4655 + background: rgba(255, 255, 255, 0.1);
4656 + border-color: rgba(255, 255, 255, 0);
4657 + }
4658 +
1007 4659 #chat-header-left {
1008 4660 flex: 1;
4661 + min-width: 0;
1009 4662 }
1010 4663
1011 -
1012 -
1013 4664 #chat-header-left h3 {
1014 - margin: 0 0 5px 0;
4665 + margin: 0 0 6px 0;
4666 + font-size: 17px;
4667 + line-height: 1.25;
1015 4668 }
1016 4669
1017 4670 #chat-header-left p {
1018 4671 margin: 0;
1019 4672 font-size: 12px;
4673 + line-height: 1.45;
4674 + opacity: 0.92;
1020 4675 }
1021 4676
1022 - /* Bouton CSS intégré dans le header */
4677 + #chat-header-actions {
4678 + display: flex;
4679 + flex-direction: column;
4680 + align-items: flex-end;
4681 + gap: 8px;
4682 + flex-shrink: 0;
4683 + }
4684 +
4685 + #chat-header-tools {
4686 + display: flex;
4687 + align-items: center;
4688 + justify-content: space-between;
4689 + gap: 8px;
4690 + width: 116px;
4691 + box-sizing: border-box;
4692 + }
4693 +
4694 + /* Bouton Header/Footer */
4695 + #headers-footers-button {
4696 + background: rgba(255, 255, 255, 0.95);
4697 + border: 1px solid rgba(191, 219, 254, 0.9);
4698 + color: #1e40af;
4699 + padding: 7px 11px;
4700 + border-radius: 8px;
4701 + cursor: pointer;
4702 + font-size: 11px;
4703 + font-weight: 600;
4704 + letter-spacing: 0.2px;
4705 + white-space: nowrap;
4706 + transition: all 0.2s ease;
4707 + width: 116px;
4708 + box-sizing: border-box;
4709 + text-align: center;
4710 + }
4711 +
4712 + #headers-footers-button:hover {
4713 + background: #ffffff !important;
4714 + border-color: #93c5fd !important;
4715 + transform: translateY(-1px);
4716 + box-shadow: 0 4px 10px rgba(30, 64, 175, 0.2);
4717 + }
4718 +
4719 + /* Boutons outils du header */
1023 4720 #css-edit-button {
1024 - width: 32px;
1025 - height: 32px;
1026 - background: #007cba;
4721 + width: 54px;
4722 + height: 34px;
4723 + background: rgba(255, 255, 255, 0.2);
1027 4724 color: white;
1028 - border: none;
1029 - border-radius: 4px;
4725 + border: 1px solid rgba(255, 255, 255, 0.35);
4726 + border-radius: 8px;
1030 4727 cursor: pointer;
1031 4728 display: flex;
1032 4729 align-items: center;
1033 4730 justify-content: center;
@@ -1033,13 +4730,15 @@
1033 4730 justify-content: center;
1034 4731 flex-shrink: 0;
1035 4732 transition: all 0.3s ease;
1036 4733 padding: 0;
4734 + box-sizing: border-box;
1037 4735 }
1038 4736
1039 4737 #css-edit-button:hover {
1040 - background: #005a87;
1041 - transform: scale(1.05);
4738 + background: rgba(255, 255, 255, 0.3);
4739 + border-color: rgba(255, 255, 255, 0.6);
4740 + transform: translateY(-1px);
1042 4741 }
1043 4742
1044 4743 #css-edit-button svg {
1045 4744 width: 16px;
@@ -1090,39 +4789,8 @@
1090 4789 margin: 0;
1091 4790 color: #333;
1092 4791 }
1093 4792
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 4793 #css-modal-close {
1126 4794 background: none;
1127 4795 border: none;
1128 4796 font-size: 24px;
@@ -1139,33 +4807,45 @@
1139 4807 #css-modal-close:hover {
1140 4808 color: #000;
1141 4809 }
1142 4810
1143 - #css-modal-body {
4811 + #css-modal-body,
4812 + #js-modal-body {
1144 4813 flex: 1;
1145 4814 padding: 20px;
1146 4815 overflow: hidden;
1147 4816 }
1148 4817
1149 - #css-editor {
1150 - width: 100%;
4818 + /* Styles pour CodeMirror */
4819 + #css-modal-body .CodeMirror,
4820 + #js-modal-body .CodeMirror {
1151 4821 height: 400px;
1152 - border: 1px solid #ddd;
1153 4822 border-radius: 4px;
1154 - padding: 15px;
1155 - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
1156 4823 font-size: 14px;
1157 4824 line-height: 1.5;
1158 - resize: vertical;
1159 - box-sizing: border-box;
1160 4825 }
1161 4826
1162 - #css-editor:focus {
1163 - outline: none;
1164 - border-color: #007cba;
1165 - box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2);
4827 + /* Assurer un minimum de 10 lignes visibles */
4828 + #css-modal-body .CodeMirror-scroll,
4829 + #js-modal-body .CodeMirror-scroll {
4830 + min-height: 210px; /* ~10 lignes à 21px chacune */
1166 4831 }
1167 4832
4833 + #css-modal-body .CodeMirror-sizer,
4834 + #js-modal-body .CodeMirror-sizer {
4835 + min-height: 210px !important;
4836 + }
4837 +
4838 + #css-modal-body .CodeMirror-lines,
4839 + #js-modal-body .CodeMirror-lines {
4840 + min-height: 210px;
4841 + padding-bottom: 50px; /* Espace pour scroller au-delà du contenu */
4842 + }
4843 +
4844 + #css-editor, #js-editor {
4845 + display: none; /* Caché car remplacé par CodeMirror */
4846 + }
4847 +
1168 4848 #css-modal-footer {
1169 4849 padding: 20px;
1170 4850 border-top: 1px solid #ddd;
1171 4851 display: flex;
@@ -1197,8 +4877,515 @@
1197 4877 }
1198 4878
1199 4879 #css-cancel:hover {
1200 4880 background: #e0e0e0;
4881 + }
4882 +
4883 + /* Bouton JS intégré dans le header */
4884 + #js-edit-button {
4885 + width: 54px;
4886 + height: 34px;
4887 + background: rgba(255, 255, 255, 0.2);
4888 + color: white;
4889 + border: 1px solid rgba(255, 255, 255, 0.35);
4890 + border-radius: 8px;
4891 + cursor: pointer;
4892 + display: flex;
4893 + align-items: center;
4894 + justify-content: center;
4895 + flex-shrink: 0;
4896 + transition: all 0.3s ease;
4897 + padding: 0;
4898 + font-weight: 600;
4899 + box-sizing: border-box;
4900 + }
4901 +
4902 + #js-edit-button:hover {
4903 + background: rgba(255, 255, 255, 0.3);
4904 + border-color: rgba(255, 255, 255, 0.6);
4905 + transform: translateY(-1px);
4906 + }
4907 +
4908 + #js-edit-button span {
4909 + font-size: 12px;
4910 + font-weight: 700;
4911 + letter-spacing: 0.3px;
4912 + }
4913 +
4914 + @media (max-width: 560px) {
4915 + #chat-header {
4916 + flex-direction: column;
4917 + align-items: stretch;
4918 + gap: 12px;
4919 + }
4920 +
4921 + #chat-header-actions {
4922 + align-items: stretch;
4923 + }
4924 +
4925 + #chat-header-tools {
4926 + justify-content: flex-end;
4927 + }
4928 +
4929 + #headers-footers-button {
4930 + text-align: center;
4931 + }
4932 + }
4933 +
4934 + /* Modale JS */
4935 + #js-modal {
4936 + position: fixed;
4937 + top: 0;
4938 + left: 0;
4939 + width: 100%;
4940 + height: 100%;
4941 + background: rgba(0, 0, 0, 0.7);
4942 + display: flex;
4943 + align-items: center;
4944 + justify-content: center;
4945 + z-index: 10000;
4946 + }
4947 +
4948 + #js-modal-content {
4949 + background: white;
4950 + border-radius: 8px;
4951 + width: 80%;
4952 + max-width: 800px;
4953 + max-height: 80%;
4954 + display: flex;
4955 + flex-direction: column;
4956 + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
4957 + }
4958 +
4959 + #js-modal-header {
4960 + padding: 20px;
4961 + border-bottom: 1px solid #ddd;
4962 + display: flex;
4963 + justify-content: space-between;
4964 + align-items: center;
4965 + }
4966 +
4967 + #js-modal-header h3 {
4968 + margin: 0;
4969 + color: #333;
4970 + }
4971 +
4972 + #js-modal-close {
4973 + background: none;
4974 + border: none;
4975 + font-size: 24px;
4976 + cursor: pointer;
4977 + color: #666;
4978 + padding: 0;
4979 + width: 30px;
4980 + height: 30px;
4981 + display: flex;
4982 + align-items: center;
4983 + justify-content: center;
4984 + }
4985 +
4986 + #js-modal-close:hover {
4987 + color: #000;
4988 + }
4989 +
4990 + #js-modal-footer {
4991 + padding: 20px;
4992 + border-top: 1px solid #ddd;
4993 + display: flex;
4994 + gap: 10px;
4995 + justify-content: flex-end;
4996 + }
4997 +
4998 + #js-save, #js-cancel {
4999 + padding: 10px 20px;
5000 + border: none;
5001 + border-radius: 4px;
5002 + cursor: pointer;
5003 + font-size: 14px;
5004 + font-weight: 500;
5005 + }
5006 +
5007 + #js-save {
5008 + background: #007cba;
5009 + color: white;
5010 + }
5011 +
5012 + #js-save:hover {
5013 + background: #005a87;
5014 + }
5015 +
5016 + #js-cancel {
5017 + background: #f0f0f0;
5018 + color: #333;
5019 + }
5020 +
5021 + #js-cancel:hover {
5022 + background: #e0e0e0;
5023 + }
5024 +
5025 + /* Highlight for CSS class search */
5026 + .css-class-highlight {
5027 + background: rgba(253, 237, 65, 0.17) !important;
5028 + border-left: 3px solidrgba(251, 193, 45, 0.5) !important;
5029 + }
5030 +
5031 + /* Chat visual style picker (above input; panel background is light) */
5032 + .chat-style-picker {
5033 + padding: 10px 15px 8px;
5034 + border-bottom: 1px solid #e8e8e8;
5035 + background: #fff;
5036 + flex-shrink: 0;
5037 + }
5038 +
5039 + .chat-style-picker-head {
5040 + display: flex;
5041 + align-items: center;
5042 + justify-content: space-between;
5043 + gap: 8px;
5044 + flex-wrap: wrap;
5045 + }
5046 +
5047 + .chat-style-trigger {
5048 + flex: 1;
5049 + min-width: 0;
5050 + text-align: left;
5051 + display: flex;
5052 + align-items: center;
5053 + justify-content: space-between;
5054 + gap: 10px;
5055 + background: #f1f5f9;
5056 + border: 1px solid #e2e8f0;
5057 + border-radius: 10px;
5058 + padding: 10px 12px;
5059 + color: #64748b;
5060 + font-size: 13px;
5061 + font-weight: 500;
5062 + cursor: pointer;
5063 + transition: background 0.2s, border-color 0.2s;
5064 + }
5065 +
5066 + .chat-style-trigger-chevron {
5067 + flex: 0 0 auto;
5068 + font-size: 24px;
5069 + line-height: 1;
5070 + opacity: 0.9;
5071 + }
5072 +
5073 + .chat-style-trigger:hover {
5074 + background: #e2e8f0;
5075 + border-color: #cbd5e1;
5076 + color: #475569;
5077 + }
5078 +
5079 + .chat-style-swipe-hint {
5080 + font-size: 11px;
5081 + color: #94a3b8;
5082 + white-space: nowrap;
5083 + flex-shrink: 0;
5084 + }
5085 +
5086 + .chat-style-remove {
5087 + flex-shrink: 0;
5088 + font-size: 11px;
5089 + font-weight: 600;
5090 + padding: 6px 10px;
5091 + border-radius: 8px;
5092 + border: 1px solid #e2e8f0;
5093 + background: #fff;
5094 + color: #64748b;
5095 + cursor: pointer;
5096 + }
5097 +
5098 + .chat-style-remove:hover {
5099 + background: #fef2f2;
5100 + border-color: #fecaca;
5101 + color: #b91c1c;
5102 + }
5103 +
5104 + .chat-style-slider-outer {
5105 + margin-top: 10px;
5106 + overflow: hidden;
5107 + }
5108 +
5109 + .chat-style-slider {
5110 + display: flex;
5111 + gap: 10px;
5112 + overflow-x: auto;
5113 + padding: 4px 2px 10px;
5114 + scroll-snap-type: x mandatory;
5115 + -webkit-overflow-scrolling: touch;
5116 + }
5117 +
5118 + .chat-style-slider::-webkit-scrollbar {
5119 + height: 6px;
5120 + }
5121 +
5122 + .chat-style-slider::-webkit-scrollbar-thumb {
5123 + background: #cbd5e1;
5124 + border-radius: 3px;
5125 + }
5126 +
5127 + .chat-style-card {
5128 + flex: 0 0 auto;
5129 + scroll-snap-align: start;
5130 + width: 100px;
5131 + padding: 0;
5132 + border: none;
5133 + background: transparent;
5134 + cursor: pointer;
5135 + text-align: center;
5136 + outline: none;
5137 + }
5138 +
5139 + .chat-style-card:focus-visible {
5140 + box-shadow: 0 0 0 2px #fff, 0 0 0 4px #2563eb;
5141 + border-radius: 14px;
5142 + }
5143 +
5144 + .chat-style-card-inner {
5145 + position: relative;
5146 + display: block;
5147 + width: 100px;
5148 + height: 100px;
5149 + box-sizing: border-box;
5150 + border-radius: 12px;
5151 + overflow: hidden;
5152 + border: 2px solid #e2e8f0;
5153 + transition: border-color 0.2s, box-shadow 0.2s;
5154 + background: #f1f5f9;
5155 + }
5156 +
5157 + .chat-style-preview-btn {
5158 + position: absolute;
5159 + top: 5px;
5160 + right: 5px;
5161 + width: 28px;
5162 + height: 28px;
5163 + border-radius: 8px;
5164 + border: 1px solid rgba(15, 23, 42, 0.1);
5165 + background: rgba(255, 255, 255, 0.94);
5166 + box-shadow: 0 1px 4px rgba(15, 23, 42, 0.12);
5167 + display: flex;
5168 + align-items: center;
5169 + justify-content: center;
5170 + color: #475569;
5171 + cursor: pointer;
5172 + padding: 0;
5173 + z-index: 3;
5174 + transition: background 0.15s, color 0.15s, transform 0.15s;
5175 + }
5176 +
5177 + .chat-style-preview-btn:hover {
5178 + background: #fff;
5179 + color: #1d4ed8;
5180 + transform: scale(1.05);
5181 + }
5182 +
5183 + .chat-style-preview-btn:focus-visible {
5184 + outline: 2px solid #2563eb;
5185 + outline-offset: 1px;
5186 + }
5187 +
5188 + .chat-style-preview-icon {
5189 + display: block;
5190 + flex-shrink: 0;
5191 + }
5192 +
5193 + .chat-style-card img {
5194 + width: 100%;
5195 + height: 100%;
5196 + object-fit: cover;
5197 + display: block;
5198 + }
5199 +
5200 + .chat-style-card-fallback {
5201 + position: absolute;
5202 + inset: 0;
5203 + width: 100%;
5204 + height: 100%;
5205 + align-items: center;
5206 + justify-content: center;
5207 + }
5208 +
5209 + .chat-style-fallback--neobrutalism {
5210 + background: linear-gradient(135deg, #ffd700 40%, #ff6b9d, #4361ee);
5211 + border: 3px solid #000;
5212 + box-sizing: border-box;
5213 + }
5214 +
5215 + .chat-style-fallback--premium-dark {
5216 + background: linear-gradient(160deg, #0a0a0b, #141416 60%, #8b5cf6, #ec4899);
5217 + }
5218 +
5219 + .chat-style-fallback--modern-minimalist {
5220 + background: #ffffff;
5221 + border: 1px solid #e5e5e5;
5222 + box-sizing: border-box;
5223 + }
5224 +
5225 + .chat-style-fallback--editorial-typography {
5226 + background: linear-gradient(180deg, #fbfaf7, #e8e4dc);
5227 + }
5228 +
5229 + .chat-style-fallback--organic-nature {
5230 + background: linear-gradient(145deg, #87a878, #f5efe0 50%, #c97d5d);
5231 + }
5232 +
5233 + .chat-style-fallback--retrofuturism {
5234 + background: linear-gradient(180deg, #0b0b2e 30%, #1a0b3e 70%, #ff006e);
5235 + }
5236 +
5237 + .chat-style-fallback--saas-tech {
5238 + background: linear-gradient(135deg, #ffffff, #eef2ff 40%, #4f46e5 120%);
5239 + }
5240 +
5241 + .chat-style-fallback--luxury {
5242 + background: linear-gradient(160deg, #0e0e0e, #1a1814 50%, #c5a572);
5243 + }
5244 +
5245 + .chat-style-fallback--bento-grid {
5246 + background: #fafafa;
5247 + background-image: linear-gradient(#e5e5e5 1px, transparent 1px), linear-gradient(90deg, #e5e5e5 1px, transparent 1px);
5248 + background-size: 18px 18px;
5249 + }
5250 +
5251 + .chat-style-fallback--y2k-revival {
5252 + background: linear-gradient(145deg, #a8dadc, #ffb6d9, #c8ff6b, #c8a4d4);
5253 + }
5254 +
5255 + .chat-style-fallback--swiss-typographic {
5256 + background: linear-gradient(90deg, #ffffff 70%, #e63946 70%, #e63946 72%, #ffffff 72%);
5257 + }
5258 +
5259 + .chat-style-fallback--claymorphism {
5260 + background: radial-gradient(circle at 30% 25%, #fff3b0, #e0d7ff 40%, #c7f0db 75%, #ffd4c4);
5261 + }
5262 +
5263 + .chat-style-card.is-selected .chat-style-card-inner {
5264 + border-color: #38bdf8;
5265 + box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.35);
5266 + }
5267 +
5268 + .chat-style-card-label {
5269 + display: block;
5270 + margin-top: 6px;
5271 + font-size: 10px;
5272 + line-height: 1.25;
5273 + color: #64748b;
5274 + font-weight: 500;
5275 + max-width: 100px;
5276 + margin-left: auto;
5277 + margin-right: auto;
5278 + }
5279 +
5280 + /* Style preview modal (sibling of #chat-box, fixed to viewport) */
5281 + .chat-style-detail-modal[hidden] {
5282 + display: none !important;
5283 + }
5284 +
5285 + .chat-style-detail-modal:not([hidden]) {
5286 + display: flex;
5287 + align-items: center;
5288 + justify-content: center;
5289 + }
5290 +
5291 + .chat-style-detail-modal {
5292 + position: fixed;
5293 + inset: 0;
5294 + z-index: 1000000;
5295 + padding: 16px;
5296 + box-sizing: border-box;
5297 + }
5298 +
5299 + .chat-style-detail-backdrop {
5300 + position: absolute;
5301 + inset: 0;
5302 + background: rgba(15, 23, 42, 0.55);
5303 + border: none;
5304 + cursor: pointer;
5305 + padding: 0;
5306 + margin: 0;
5307 + }
5308 +
5309 + .chat-style-detail-panel {
5310 + position: relative;
5311 + z-index: 1;
5312 + background: #fff;
5313 + border-radius: 16px;
5314 + max-width: min(540px, 100%);
5315 + width: 100%;
5316 + max-height: min(90vh, 880px);
5317 + overflow: auto;
5318 + padding: 20px 20px 22px;
5319 + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.28);
5320 + text-align: left;
5321 + }
5322 +
5323 + .chat-style-detail-close {
5324 + position: absolute;
5325 + top: 10px;
5326 + right: 12px;
5327 + width: 36px;
5328 + height: 36px;
5329 + border: none;
5330 + background: #f1f5f9;
5331 + color: #64748b;
5332 + font-size: 22px;
5333 + line-height: 1;
5334 + border-radius: 10px;
5335 + cursor: pointer;
5336 + display: flex;
5337 + align-items: center;
5338 + justify-content: center;
5339 + padding: 0;
5340 + z-index: 2;
5341 + }
5342 +
5343 + .chat-style-detail-close:hover {
5344 + background: #e2e8f0;
5345 + color: #0f172a;
5346 + }
5347 +
5348 + .chat-style-detail-title {
5349 + margin: 0 44px 14px 0;
5350 + font-size: 18px;
5351 + font-weight: 600;
5352 + color: #0f172a;
5353 + line-height: 1.3;
5354 + }
5355 +
5356 + .chat-style-detail-image-wrap {
5357 + border-radius: 12px;
5358 + overflow: hidden;
5359 + background: #f8fafc;
5360 + margin-bottom: 14px;
5361 + }
5362 +
5363 + .chat-style-detail-image-wrap img {
5364 + display: block;
5365 + width: 100%;
5366 + max-height: min(42vh, 360px);
5367 + height: auto;
5368 + object-fit: contain;
5369 + margin: 0 auto;
5370 + }
5371 +
5372 + .chat-style-detail-intro {
5373 + margin: 0 0 8px 0;
5374 + font-size: 12px;
5375 + font-weight: 600;
5376 + color: #64748b;
5377 + text-transform: uppercase;
5378 + letter-spacing: 0.04em;
5379 + }
5380 +
5381 + .chat-style-detail-prompt {
5382 + margin: 0;
5383 + font-size: 13px;
5384 + line-height: 1.55;
5385 + color: #334155;
5386 + white-space: pre-wrap;
5387 + word-break: break-word;
1201 5388 }
1202 5389
1203 5390
1204 5391 `;