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

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

4,675 lines 157.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // Charger la configuration globale de l'API
2 const script = document.createElement("script");
3 // script.src = "/wp-content/plugins/ai-builder/config.js";
4 document.head.appendChild(script);
5
6 // 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);
13
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);
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);
28
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);
38 })();
39
40 // Récupération des variables injectées par wp_localize_script (admin)
41 if (typeof window.aiBuilderVars !== "undefined") {
42 // Forcer l'utilisation d'une URL relative pour éviter les erreurs CORS
43 window.ajaxurl = '/wp-admin/admin-ajax.php';
44 window.aiBuilderNonce = window.aiBuilderVars.nonce;
45 } else {
46 // Fallback pour éviter les erreurs CORS
47 window.ajaxurl = '/wp-admin/admin-ajax.php';
48 window.aiBuilderNonce = '';
49 }
50
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
72 // Charger le CSS des témoignages
73 const style = document.createElement("link");
74 style.rel = "stylesheet";
75 style.href = "/wp-content/plugins/ai-builder/assets/css/cards.css";
76 document.head.appendChild(style);
77
78 document.body.insertAdjacentHTML(
79 "beforeend",
80 `
81 <div id="chat-toggle">AI Builder</div>
82 <div id="chat-box">
83 <div id="chat-header">
84 <div id="chat-header-left">
85 <h3>AI Page Builder</h3>
86 </div>
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>
101 </div>
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>
115 <div id="chat-input">
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>
126 </div>
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>
154 <!-- Modale CSS -->
155 <div id="css-modal" style="display: none;">
156 <div id="css-modal-content">
157 <div id="css-modal-header">
158 <h3>Edit CSS</h3>
159 <button id="css-modal-close">&times;</button>
160 </div>
161 <div id="css-modal-body">
162 <textarea id="css-editor"></textarea>
163 </div>
164 <div id="css-modal-footer">
165 <button id="css-save">Save CSS</button>
166 <button id="css-cancel">Cancel</button>
167 </div>
168 </div>
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>
186
187 `
188 );
189
190 const toggle = document.getElementById("chat-toggle");
191 const box = document.getElementById("chat-box");
192 const messages = document.getElementById("chat-messages");
193 const input = document.querySelector("#chat-input textarea");
194 const pageGenerateBtn = document.getElementById("chat-generate-page");
195 const charCounter = document.getElementById("chat-char-counter");
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");
205 let chatHistory = [];
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
476 function getChatStorageKey() {
477 const prefix = "aiBuilderChatHistory";
478 const postId =
479 wp?.data?.select("core/editor")?.getCurrentPostId?.() ||
480 new URL(window.location.href, window.location.origin).searchParams.get("post") ||
481 new URL(window.location.href, window.location.origin).searchParams.get("postId") ||
482 new URL(window.location.href, window.location.origin).searchParams.get("p");
483 if (postId) {
484 return `${prefix}_post_${postId}`;
485 }
486 const patternName = getPatternName();
487 if (patternName) {
488 return `${prefix}_pattern_${patternName}`;
489 }
490 return `${prefix}_${window.location.pathname}`;
491 }
492
493 // Fonction pour migrer l'historique de post-new.php vers l'ID du post
494 function migrateChatHistoryFromNewPost() {
495 if (!window.localStorage) return;
496
497 const prefix = "aiBuilderChatHistory";
498 const newPostKey = `${prefix}_/wp-admin/post-new.php`;
499 const currentKey = getChatStorageKey();
500
501 // Si on est déjà sur la clé post-new.php, pas besoin de migrer
502 if (currentKey === newPostKey) return;
503
504 // Si la clé actuelle est basée sur un ID de post, vérifier s'il y a un historique à migrer
505 if (currentKey.startsWith(`${prefix}_post_`)) {
506 try {
507 const oldHistory = window.localStorage.getItem(newPostKey);
508 if (oldHistory) {
509 const parsedHistory = JSON.parse(oldHistory);
510 if (Array.isArray(parsedHistory) && parsedHistory.length > 0) {
511 // Fusionner avec l'historique existant (s'il y en a un)
512 const existingHistory = chatHistory.length > 0 ? chatHistory : [];
513 chatHistory = [...existingHistory, ...parsedHistory];
514
515 // Sauvegarder avec la nouvelle clé
516 window.localStorage.setItem(currentKey, JSON.stringify(chatHistory));
517
518 // Supprimer l'ancienne clé
519 window.localStorage.removeItem(newPostKey);
520
521 console.log("AI Builder: Chat history migrated from post-new.php to post ID");
522
523 // Re-rendre l'historique
524 renderChatHistory();
525 }
526 }
527 } catch (error) {
528 console.warn("AI Builder: Error migrating chat history", error);
529 }
530 }
531 }
532
533 // Éléments CSS
534 const cssEditButton = document.getElementById("css-edit-button");
535 const cssModal = document.getElementById("css-modal");
536 const cssEditor = document.getElementById("css-editor");
537 const cssSaveBtn = document.getElementById("css-save");
538 const cssCancelBtn = document.getElementById("css-cancel");
539 const cssModalClose = document.getElementById("css-modal-close");
540
541 // Éléments JS
542 const jsEditButton = document.getElementById("js-edit-button");
543 const jsModal = document.getElementById("js-modal");
544 const jsEditor = document.getElementById("js-editor");
545 const jsSaveBtn = document.getElementById("js-save");
546 const jsCancelBtn = document.getElementById("js-cancel");
547 const jsModalClose = document.getElementById("js-modal-close");
548
549 // Bouton Header/Footer
550 const headersFootersButton = document.getElementById("headers-footers-button");
551 if (headersFootersButton) {
552 headersFootersButton.addEventListener("click", () => {
553 const headersFootersUrl = "/wp-admin/admin.php?page=aibui-headers-footers";
554 window.open(headersFootersUrl, "_blank");
555 });
556 }
557
558 toggle.onclick = () => {
559 const isOpening = box.style.display !== "flex";
560 box.style.display = box.style.display === "flex" ? "none" : "flex";
561 box.style.flexDirection = "column";
562
563 // Scroller vers le bas quand on ouvre le chat
564 if (isOpening && messages) {
565 // Petit délai pour s'assurer que le DOM est mis à jour
566 setTimeout(() => {
567 messages.scrollTop = messages.scrollHeight;
568 }, 100);
569 }
570 };
571
572 // Logique pour la modale CSS
573 let currentCSSContent = "";
574 const AIBUI_COMBINED_PAGE_MARKER = "/* === AI Builder: Page CSS === */";
575 const AIBUI_COMBINED_BLOCKS_MARKER = "/* === AI Builder: Blocks CSS === */";
576
577 // Initialiser les variables CSS globales
578 window.aiBuilderPageCSS = window.aiBuilderPageCSS || "";
579 window.aiBuilderBlockCSS = window.aiBuilderBlockCSS || "";
580
581 function buildCombinedCSSValue(pageCss, blockCss) {
582 const p = (pageCss || "").trimEnd();
583 const b = (blockCss || "").trimEnd();
584 return (
585 AIBUI_COMBINED_PAGE_MARKER +
586 "\n" +
587 p +
588 "\n\n" +
589 AIBUI_COMBINED_BLOCKS_MARKER +
590 "\n" +
591 b +
592 "\n"
593 );
594 }
595
596 function splitCombinedCSSValue(combinedText) {
597 const raw = String(combinedText || "");
598 const iPage = raw.indexOf(AIBUI_COMBINED_PAGE_MARKER);
599 const iBlocks = raw.indexOf(AIBUI_COMBINED_BLOCKS_MARKER);
600
601 // If markers are missing, treat as page CSS and preserve existing blocks CSS
602 if (iPage === -1 || iBlocks === -1 || iBlocks < iPage) {
603 return {
604 pageCss: raw,
605 blockCss: window.aiBuilderBlockCSS || "",
606 };
607 }
608
609 const pageStart = iPage + AIBUI_COMBINED_PAGE_MARKER.length;
610 const blocksStart = iBlocks + AIBUI_COMBINED_BLOCKS_MARKER.length;
611 const pageCss = raw.slice(pageStart, iBlocks).replace(/^\s*\n/, "");
612 const blockCss = raw.slice(blocksStart).replace(/^\s*\n/, "");
613 return { pageCss, blockCss };
614 }
615
616 function saveChatHistory() {
617 if (!window.localStorage) return;
618 try {
619 const key = getChatStorageKey();
620 window.localStorage.setItem(key, JSON.stringify(chatHistory));
621 } catch (error) {
622 console.warn("AI Builder: Unable to persist chat history", error);
623 }
624 }
625
626 function loadChatHistoryFromStorage() {
627 if (!window.localStorage) return;
628 try {
629 const key = getChatStorageKey();
630 console.log("Loading chat history from storage:", key);
631 const storedHistory = window.localStorage.getItem(key);
632 if (!storedHistory) return;
633 const parsedHistory = JSON.parse(storedHistory);
634 if (Array.isArray(parsedHistory)) {
635 chatHistory = parsedHistory;
636 }
637 } catch (error) {
638 console.warn("AI Builder: Unable to read chat history", error);
639 }
640 }
641
642 function renderChatHistory() {
643 if (!messages) return;
644 messages.innerHTML = "";
645 chatHistory.forEach(({ type, message }) => {
646 addMessage(message, type, { persist: false });
647 });
648 // Scroller vers le bas après avoir rendu l'historique
649 if (chatHistory.length > 0) {
650 setTimeout(() => {
651 messages.scrollTop = messages.scrollHeight;
652 }, 50);
653 }
654 }
655
656 function getConversationHistoryString(limit = 4) {
657 if (!chatHistory.length) return "";
658 const recentMessages = chatHistory.slice(-limit);
659 const segments = recentMessages.map(({ type, message }) => {
660 const label = type === "user" ? "User question" : "AI Response";
661 return `${label} : ${message}`;
662 });
663 return segments.join(". ") + (segments.length ? "." : "");
664 }
665
666 // Fonction pour ajouter des messages dans le chat
667 function addMessage(message, type = "assistant", options = {}) {
668 const { persist = true } = options;
669 if (!messages) return;
670
671 // Si un indicateur de saisie est présent, le garder toujours en bas
672 const typingIndicator = document.getElementById("chat-typing-indicator");
673 if (typingIndicator && typingIndicator.parentNode === messages) {
674 messages.removeChild(typingIndicator);
675 }
676
677 const messageDiv = document.createElement("div");
678 messageDiv.className = type === "assistant" ? "ai-message" : "user-message";
679
680 if (type === "assistant") {
681 messageDiv.innerHTML = `${message}`;
682 } else {
683 messageDiv.innerHTML = `${message}`;
684 }
685
686 messages.appendChild(messageDiv);
687
688 // Ré‑ajouter l'indicateur de saisie en bas si nécessaire
689 if (typingIndicator) {
690 messages.appendChild(typingIndicator);
691 }
692 messages.scrollTop = messages.scrollHeight;
693
694 if (persist) {
695 chatHistory.push({ type, message });
696 saveChatHistory();
697 }
698 }
699
700 loadChatHistoryFromStorage();
701 renderChatHistory();
702
703 // Migrer l'historique de post-new.php vers l'ID du post si nécessaire
704 migrateChatHistoryFromNewPost();
705
706 // Surveiller les changements d'ID de post (pour les drafts créés après le chargement)
707 let lastKnownPostId = null;
708 let lastKnownKey = getChatStorageKey();
709
710 function checkPostIdChange() {
711 try {
712 const currentPostId = wp?.data?.select("core/editor")?.getCurrentPostId?.();
713 const currentKey = getChatStorageKey();
714
715 if (currentPostId && currentPostId !== lastKnownPostId) {
716 // L'ID a changé, mettre à jour la clé de stockage
717 if (currentKey !== lastKnownKey && window.localStorage) {
718 // Migrer l'historique vers la nouvelle clé
719 const oldHistory = window.localStorage.getItem(lastKnownKey);
720 if (oldHistory) {
721 try {
722 const parsedHistory = JSON.parse(oldHistory);
723 if (Array.isArray(parsedHistory) && parsedHistory.length > 0) {
724 chatHistory = parsedHistory;
725 window.localStorage.setItem(currentKey, oldHistory);
726 // Ne pas supprimer l'ancienne clé immédiatement, au cas où
727 console.log("AI Builder: Chat history migrated to new post ID:", currentPostId);
728 renderChatHistory();
729 }
730 } catch (e) {
731 console.warn("AI Builder: Error migrating history on ID change", e);
732 }
733 }
734 }
735
736 lastKnownPostId = currentPostId;
737 lastKnownKey = currentKey;
738 } else if (currentKey !== lastKnownKey) {
739 // La clé a changé même si l'ID n'a pas changé (changement d'URL)
740 lastKnownKey = currentKey;
741 // Recharger l'historique avec la nouvelle clé
742 loadChatHistoryFromStorage();
743 renderChatHistory();
744 migrateChatHistoryFromNewPost();
745 }
746 } catch (e) {
747 // Ignorer les erreurs si wp.data n'est pas encore disponible
748 }
749 }
750
751 // Vérifier l'ID initial
752 setTimeout(() => {
753 try {
754 lastKnownPostId = wp?.data?.select("core/editor")?.getCurrentPostId?.();
755 } catch (e) { }
756 }, 1000);
757
758 // Surveiller les changements d'ID toutes les 2 secondes
759 setInterval(checkPostIdChange, 2000);
760
761 // Fonction pour assurer un minimum de lignes dans CodeMirror
762 function ensureMinLines(cm, minLines) {
763 const lineCount = cm.lineCount();
764 if (lineCount < minLines) {
765 const linesToAdd = minLines - lineCount;
766 const padding = "\n".repeat(linesToAdd);
767 const currentValue = cm.getValue();
768 // Ne pas modifier si déjà suffisant
769 if (currentValue.split("\n").length < minLines) {
770 // On ne modifie pas le contenu, on utilise le CSS pour le padding
771 }
772 }
773 // Refresh pour s'assurer que l'affichage est correct
774 cm.refresh();
775 }
776
777 // Instance CodeMirror pour CSS
778 let cssCodeMirror = null;
779
780 // Initialiser CodeMirror pour CSS quand il est prêt
781 function initCSSCodeMirror() {
782 if (cssCodeMirror) return;
783 if (typeof CodeMirror === "undefined") {
784 setTimeout(initCSSCodeMirror, 100);
785 return;
786 }
787 cssCodeMirror = CodeMirror.fromTextArea(cssEditor, {
788 mode: "css",
789 theme: "material-darker",
790 lineNumbers: true,
791 lineWrapping: true,
792 tabSize: 2,
793 indentWithTabs: false,
794 autoCloseBrackets: true,
795 viewportMargin: Infinity,
796 });
797 cssCodeMirror.setSize("100%", "400px");
798 // Assurer un minimum de 10 lignes visibles
799 ensureMinLines(cssCodeMirror, 10);
800 }
801
802 // Ouvrir la modale CSS
803 cssEditButton.onclick = () => {
804 cssModal.style.display = "flex";
805 initCSSCodeMirror();
806 // Charger le CSS combiné (page + blocks)
807 const combined = buildCombinedCSSValue(window.aiBuilderPageCSS || "", window.aiBuilderBlockCSS || "");
808 if (cssCodeMirror) {
809 cssCodeMirror.setValue(combined);
810 setTimeout(() => cssCodeMirror.refresh(), 10);
811 } else {
812 cssEditor.value = combined;
813 }
814 setTimeout(() => {
815 if (cssCodeMirror) cssCodeMirror.focus();
816 }, 100);
817 };
818
819 // Fermer la modale CSS
820 function closeCSSModal() {
821 cssModal.style.display = "none";
822 }
823
824 cssModalClose.onclick = closeCSSModal;
825 cssCancelBtn.onclick = closeCSSModal;
826
827 // Sauvegarder le CSS
828 cssSaveBtn.onclick = async () => {
829 const newCSSContent = cssCodeMirror ? cssCodeMirror.getValue() : cssEditor.value;
830 const parts = splitCombinedCSSValue(newCSSContent);
831 window.aiBuilderPageCSS = parts.pageCss;
832 window.aiBuilderBlockCSS = parts.blockCss;
833
834 // Save both storages (replace = true since it's a manual edit)
835 await saveCSSInPostMeta(parts.pageCss, "page", true);
836 await saveCSSInPostMeta(parts.blockCss, "block", true);
837
838 // Recharger le CSS combiné depuis le serveur après sauvegarde
839 await loadCSSFromPostMeta();
840
841 closeCSSModal();
842 };
843
844 // Fermer la modale en cliquant à l'extérieur
845 cssModal.onclick = (e) => {
846 if (e.target === cssModal) {
847 closeCSSModal();
848 }
849 };
850
851 // Global: open CSS modal and scroll/highlight a specific class
852 window.openCSSModalForClass = function (classNames) {
853 if (!classNames) return;
854
855 const classes = classNames.split(/\s+/).filter(Boolean);
856 if (!classes.length) return;
857
858 cssModal.style.display = "flex";
859 initCSSCodeMirror();
860 const combined = buildCombinedCSSValue(window.aiBuilderPageCSS || "", window.aiBuilderBlockCSS || "");
861 if (cssCodeMirror) {
862 cssCodeMirror.setValue(combined);
863 setTimeout(() => cssCodeMirror.refresh(), 10);
864 } else {
865 cssEditor.value = combined;
866 }
867
868 // Helper: find and highlight all matching rules in the current CodeMirror content
869 function findAndHighlight() {
870 if (!cssCodeMirror) return;
871 cssCodeMirror.refresh();
872
873 // Clear previous highlights
874 if (window._cssClassMarks) {
875 window._cssClassMarks.forEach((m) => m.clear());
876 }
877 window._cssClassMarks = [];
878
879 const content = cssCodeMirror.getValue();
880 let firstMatchLine = null;
881
882 for (const cls of classes) {
883 const selector = "." + cls;
884 // Search line by line
885 for (let line = 0; line < cssCodeMirror.lineCount(); line++) {
886 const lineText = cssCodeMirror.getLine(line);
887 if (lineText.includes(selector)) {
888 if (firstMatchLine === null) firstMatchLine = line;
889
890 // Find the full CSS rule block: from this selector line to closing }
891 let ruleStart = line;
892 let ruleEnd = line;
893
894 for (let i = line; i < cssCodeMirror.lineCount(); i++) {
895 if (cssCodeMirror.getLine(i).includes("{")) {
896 let depth = 0;
897 for (let j = i; j < cssCodeMirror.lineCount(); j++) {
898 const lt = cssCodeMirror.getLine(j);
899 for (const ch of lt) {
900 if (ch === "{") depth++;
901 if (ch === "}") depth--;
902 }
903 if (depth <= 0) {
904 ruleEnd = j;
905 break;
906 }
907 }
908 break;
909 }
910 }
911
912 const mark = cssCodeMirror.markText(
913 { line: ruleStart, ch: 0 },
914 { line: ruleEnd, ch: cssCodeMirror.getLine(ruleEnd).length },
915 { className: "css-class-highlight" }
916 );
917 window._cssClassMarks.push(mark);
918 }
919 }
920 }
921
922 if (firstMatchLine !== null) {
923 cssCodeMirror.setCursor({ line: firstMatchLine, ch: 0 });
924 cssCodeMirror.scrollIntoView({ line: firstMatchLine, ch: 0 }, 120);
925 }
926 }
927
928 // Wait for CodeMirror to fully render before searching
929 setTimeout(findAndHighlight, 300);
930 };
931
932 // Logique pour la modale JS
933 let currentJSContent = "";
934 const AIBUI_COMBINED_PAGE_JS_MARKER = "/* === AI Builder: Page JS === */";
935 const AIBUI_COMBINED_BLOCKS_JS_MARKER = "/* === AI Builder: Blocks JS === */";
936
937 // Initialiser les variables JS globales
938 window.aiBuilderPageJS = window.aiBuilderPageJS || "";
939 window.aiBuilderBlockJS = window.aiBuilderBlockJS || "";
940
941 // Instance CodeMirror pour JS
942 let jsCodeMirror = null;
943
944 // Initialiser CodeMirror pour JS quand il est prêt
945 function initJSCodeMirror() {
946 if (jsCodeMirror) return;
947 if (typeof CodeMirror === "undefined") {
948 setTimeout(initJSCodeMirror, 100);
949 return;
950 }
951 jsCodeMirror = CodeMirror.fromTextArea(jsEditor, {
952 mode: "javascript",
953 theme: "material-darker",
954 lineNumbers: true,
955 lineWrapping: true,
956 tabSize: 2,
957 indentWithTabs: false,
958 autoCloseBrackets: true,
959 viewportMargin: Infinity,
960 });
961 jsCodeMirror.setSize("100%", "400px");
962 // Assurer un minimum de 10 lignes visibles
963 ensureMinLines(jsCodeMirror, 10);
964 }
965
966 // Ouvrir la modale JS
967 jsEditButton.onclick = () => {
968 jsModal.style.display = "flex";
969 initJSCodeMirror();
970 const combined = (
971 AIBUI_COMBINED_PAGE_JS_MARKER +
972 "\n" +
973 String(window.aiBuilderPageJS || "").trimEnd() +
974 "\n\n" +
975 AIBUI_COMBINED_BLOCKS_JS_MARKER +
976 "\n" +
977 String(window.aiBuilderBlockJS || "").trimEnd() +
978 "\n"
979 );
980 if (jsCodeMirror) {
981 jsCodeMirror.setValue(combined);
982 setTimeout(() => jsCodeMirror.refresh(), 10);
983 } else {
984 jsEditor.value = combined;
985 }
986 setTimeout(() => {
987 if (jsCodeMirror) jsCodeMirror.focus();
988 }, 100);
989 };
990
991 // Fermer la modale JS
992 function closeJSModal() {
993 jsModal.style.display = "none";
994 }
995
996 jsModalClose.onclick = closeJSModal;
997 jsCancelBtn.onclick = closeJSModal;
998
999 // Sauvegarder le JS
1000 jsSaveBtn.onclick = async () => {
1001 const newJSContent = jsCodeMirror ? jsCodeMirror.getValue() : jsEditor.value;
1002 const raw = String(newJSContent || "");
1003 const iPage = raw.indexOf(AIBUI_COMBINED_PAGE_JS_MARKER);
1004 const iBlocks = raw.indexOf(AIBUI_COMBINED_BLOCKS_JS_MARKER);
1005 let pageJs = raw;
1006 let blockJs = window.aiBuilderBlockJS || "";
1007 if (iPage !== -1 && iBlocks !== -1 && iBlocks > iPage) {
1008 const pageStart = iPage + AIBUI_COMBINED_PAGE_JS_MARKER.length;
1009 const blocksStart = iBlocks + AIBUI_COMBINED_BLOCKS_JS_MARKER.length;
1010 pageJs = raw.slice(pageStart, iBlocks).replace(/^\s*\n/, "");
1011 blockJs = raw.slice(blocksStart).replace(/^\s*\n/, "");
1012 }
1013 window.aiBuilderPageJS = pageJs;
1014 window.aiBuilderBlockJS = blockJs;
1015
1016 // Save both storages (replace = true since it's a manual edit)
1017 await saveJSInPostMeta(pageJs, "page", true);
1018 await saveJSInPostMeta(blockJs, "block", true);
1019
1020 // Recharger le JS combiné depuis le serveur après sauvegarde
1021 await loadJSFromPostMeta();
1022
1023 closeJSModal();
1024 };
1025
1026 // Fermer la modale JS en cliquant à l'extérieur
1027 jsModal.onclick = (e) => {
1028 if (e.target === jsModal) {
1029 closeJSModal();
1030 }
1031 };
1032
1033 function buildBlock(block) {
1034 if (!block) return null;
1035 const { blockName, attrs = {}, innerBlocks = [] } = block;
1036 const nextAttrs = { ...(attrs || {}) };
1037
1038 // API sometimes returns text payload as `content` (not in attrs).
1039 // Map it to the common Gutenberg attribute key so text doesn't disappear.
1040 if (typeof block.content === "string" && typeof nextAttrs.content !== "string") {
1041 nextAttrs.content = block.content;
1042 }
1043 // Common variants some generators use.
1044 if (typeof block.value === "string" && typeof nextAttrs.value !== "string") {
1045 nextAttrs.value = block.value;
1046 }
1047 if (typeof block.values === "string" && typeof nextAttrs.values !== "string") {
1048 nextAttrs.values = block.values;
1049 }
1050
1051 return wp.blocks.createBlock(
1052 blockName,
1053 nextAttrs,
1054 (innerBlocks || []).map(buildBlock) // récursivité ici
1055 );
1056 }
1057
1058 function getAibuiAccountAdminUrl() {
1059 const base =
1060 typeof aiBuilderVars !== "undefined" && aiBuilderVars.adminBaseUrl
1061 ? String(aiBuilderVars.adminBaseUrl).replace(/\/?$/, "/")
1062 : "/wp-admin/";
1063 return base + "admin.php?page=aibui-assistant";
1064 }
1065
1066 function getAibuiCreditsAdminUrl() {
1067 const base =
1068 typeof aiBuilderVars !== "undefined" && aiBuilderVars.adminBaseUrl
1069 ? String(aiBuilderVars.adminBaseUrl).replace(/\/?$/, "/")
1070 : "/wp-admin/";
1071 return base + "admin.php?page=aibui-credits";
1072 }
1073
1074 function aibuiAuthRequiredError(message) {
1075 const e = new Error(message);
1076 e.aibuiAuthRequired = true;
1077 return e;
1078 }
1079
1080 /** HTML for in-chat message when the user must sign in / create an AI Builder account */
1081 function getAccountRequiredChatMessageHtml() {
1082 const url = getAibuiAccountAdminUrl().replace(/&/g, "&amp;");
1083 return (
1084 '<div style="display:flex;flex-direction:column;align-items:center;gap:12px;">' +
1085 '<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>' +
1086 '<a href="' +
1087 url +
1088 '" 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>' +
1089 "</div>"
1090 );
1091 }
1092
1093 /** HTML for in-chat message when the user is out of credits */
1094 function getCreditsRequiredChatMessageHtml() {
1095 const url = getAibuiCreditsAdminUrl().replace(/&/g, "&amp;");
1096 return (
1097 '<div style="display:flex;flex-direction:column;align-items:center;gap:12px;">' +
1098 '<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>' +
1099 '<a href="' +
1100 url +
1101 '" 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>' +
1102 "</div>"
1103 );
1104 }
1105
1106 // Utilitaire pour récupérer le token JWT via AJAX WordPress
1107 async function getJwtToken() {
1108 if (!window.ajaxurl || !window.aiBuilderNonce) {
1109 console.warn("AI Builder: Missing AJAX URL or nonce");
1110 showAIMissingAccountToast();
1111 throw aibuiAuthRequiredError("Missing AJAX URL or nonce");
1112 }
1113
1114 let timeoutId;
1115 try {
1116 // Create AbortController for timeout
1117 const controller = new AbortController();
1118 timeoutId = setTimeout(() => controller.abort(), 20000); // 20 second timeout
1119
1120 const res = await fetch(window.ajaxurl, {
1121 method: "POST",
1122 headers: { "Content-Type": "application/x-www-form-urlencoded" },
1123 body: `action=aibui_get_token&nonce=${window.aiBuilderNonce}`,
1124 signal: controller.signal,
1125 });
1126
1127 clearTimeout(timeoutId);
1128
1129 if (!res.ok) {
1130 // Handle 500 errors specifically
1131 if (res.status === 500) {
1132 throw new Error("Server error: The request took too long or encountered an error. Please try again.");
1133 }
1134 console.log('res.status: ', res.status);
1135 if (res.status === 401 || res.status === 403) {
1136 showAIMissingAccountToast();
1137 throw aibuiAuthRequiredError(
1138 "You need to have an account and be logged in to use AI features."
1139 );
1140 }
1141 throw new Error(`HTTP error! status: ${res.status}`);
1142 }
1143
1144 const data = await res.json();
1145 console.log("token res: ", data);
1146 if (data.success && data.data.token) {
1147 return data.data.token;
1148 }
1149 showAIMissingAccountToast();
1150 throw aibuiAuthRequiredError(
1151 "You need to have an account and be logged in to use AI features."
1152 );
1153 } catch (error) {
1154 if (timeoutId) {
1155 clearTimeout(timeoutId);
1156 }
1157 console.error("Error fetching JWT token:", error);
1158
1159 // Handle timeout/abort errors
1160 if (error.name === 'AbortError' || error.message.includes('timeout')) {
1161 throw new Error("Request timeout: The server took too long to respond. Please check your connection and try again.");
1162 }
1163
1164 if (error.aibuiAuthRequired) {
1165 throw error;
1166 }
1167
1168 showAIMissingAccountToast();
1169 throw error;
1170 }
1171 }
1172
1173 // Fonction utilitaire pour afficher un toast UX si l'utilisateur n'a pas de compte/connexion
1174 function showAIMissingAccountToast() {
1175 if (document.getElementById("ai-missing-account-toast")) return; // Pas de doublon
1176 const accountUrl = getAibuiAccountAdminUrl();
1177 const toast = document.createElement("div");
1178 toast.id = "ai-missing-account-toast";
1179 toast.innerHTML = `
1180 <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;">
1181 <span style='flex:1 1 200px;min-width:180px;'>You need to have an account and be logged in to use AI features.</span>
1182 <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>
1183 </div>
1184 `;
1185 document.body.appendChild(toast);
1186 setTimeout(() => {
1187 if (toast.parentNode) toast.parentNode.removeChild(toast);
1188 }, 8000);
1189 }
1190
1191 function showNotEnoughCreditsToast() {
1192 if (document.getElementById("ai-missing-account-toast")) return; // Pas de doublon
1193 const toast = document.createElement("div");
1194 toast.id = "ai-missing-account-toast";
1195 toast.innerHTML = `
1196 <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;">
1197 <span style='flex:1 1 200px;min-width:180px;'>You don't have enough credits.</span>
1198 <a href='/wp-admin/admin.php?page=aibui-credits' style='background:#00b87c;color:#fff;padding:8px 18px;border-radius:8px;text-decoration:none;font-weight:600;transition:background 0.2s;' target='_blank'>Go to credits page</a>
1199 </div>
1200 `;
1201 document.body.appendChild(toast);
1202 setTimeout(() => {
1203 if (toast.parentNode) toast.parentNode.removeChild(toast);
1204 }, 8000);
1205 }
1206
1207 // Fonction pour afficher le nombre de crédits sous le titre ET sous le bouton
1208 function updateCreditsDisplay(credits) {
1209 // Sous le titre du chat
1210 let creditsElem = document.getElementById("ai-credits-display");
1211 if (!creditsElem) {
1212 const header = document.querySelector("#chat-header-left");
1213 if (header) {
1214 creditsElem = document.createElement("div");
1215 creditsElem.id = "ai-credits-display";
1216 creditsElem.style.fontSize = "12px";
1217 creditsElem.style.color = "#cccccc";
1218 creditsElem.style.marginTop = "2px";
1219 creditsElem.style.textAlign = "left";
1220 header.appendChild(creditsElem);
1221 }
1222 }
1223 if (creditsElem) {
1224 creditsElem.textContent =
1225 credits !== null ? `${credits} credits left` : "- credits left";
1226 }
1227
1228 // À l'intérieur du bouton toggle
1229 const toggle = document.getElementById("chat-toggle");
1230 if (toggle) {
1231 let span = toggle.querySelector(".ai-credits-inside");
1232 if (!span) {
1233 span = document.createElement("span");
1234 span.className = "ai-credits-inside";
1235 span.style.display = "block";
1236 span.style.fontSize = "10px";
1237 span.style.color = "#cccccc";
1238 span.style.marginTop = "0px";
1239 span.style.textAlign = "center";
1240 toggle.appendChild(span);
1241 }
1242 span.textContent = credits !== null ? `${credits} credits` : "- credits";
1243 }
1244 }
1245
1246 window.updateAICreditsDisplay = updateCreditsDisplay;
1247
1248 // Fonction pour charger les crédits utilisateur
1249 async function loadUserCredits() {
1250 let timeoutId;
1251 try {
1252 const jwtToken = await getJwtToken();
1253
1254 // Create AbortController for timeout
1255 const controller = new AbortController();
1256 timeoutId = setTimeout(() => controller.abort(), 20000); // 20 second timeout
1257
1258 const res = await fetch(window.config.apiUrl + "/user/profile", {
1259 method: "GET",
1260 headers: {
1261 Authorization: `Bearer ${jwtToken}`,
1262 "Content-Type": "application/json",
1263 },
1264 signal: controller.signal,
1265 });
1266
1267 clearTimeout(timeoutId);
1268
1269 if (!res.ok) {
1270 if (res.status === 500) {
1271 throw new Error("Server error: The request took too long or encountered an error.");
1272 }
1273 throw new Error("Failed to load profile");
1274 }
1275
1276 const data = await res.json();
1277 const aiCredits = data.user?.aiCredits || {};
1278 const totalCredits =
1279 (aiCredits.onAccountCreation || 0) +
1280 (aiCredits.monthlySubscription || 0) +
1281 (aiCredits.paid || 0);
1282 console.log("Credits loaded:", totalCredits, "from:", aiCredits);
1283 updateCreditsDisplay(totalCredits);
1284 } catch (e) {
1285 if (timeoutId) {
1286 clearTimeout(timeoutId);
1287 }
1288 console.error("Error loading credits:", e);
1289
1290 // Handle timeout/abort errors
1291 if (e.name === 'AbortError' || e.message.includes('timeout')) {
1292 console.warn("Credits loading timeout - will retry on next action");
1293 }
1294
1295 updateCreditsDisplay(null);
1296 }
1297 }
1298
1299 // Retourne la liste des documents cibles pour l'injection :
1300 // - le document principal de l'admin
1301 // - le(s) document(s) à l'intérieur des iframes du canvas d'édition
1302 // (Site Editor, Post Editor iso-mode, etc.).
1303 // Certains navigateurs ou modes peuvent retourner null pour contentDocument
1304 // (sécurité cross-origin) : on filtre silencieusement.
1305 function getEditorTargetDocuments() {
1306 const docs = [];
1307 try { if (document) docs.push(document); } catch (e) { /* ignore */ }
1308 try {
1309 const selectors = [
1310 'iframe[name="editor-canvas"]',
1311 'iframe.editor-canvas__iframe',
1312 '.block-editor-iframe__container iframe',
1313 '.edit-post-visual-editor iframe',
1314 '.edit-site-visual-editor iframe',
1315 '.interface-interface-skeleton__content iframe',
1316 ];
1317 const iframes = document.querySelectorAll(selectors.join(','));
1318 iframes.forEach((iframe) => {
1319 try {
1320 const doc = iframe.contentDocument;
1321 if (doc && doc.head) docs.push(doc);
1322 } catch (e) { /* ignore */ }
1323 });
1324 } catch (e) { /* ignore */ }
1325 return docs;
1326 }
1327
1328 // Signature légère d'un contenu pour détecter les changements sans recalcul.
1329 function computeContentSignature(content) {
1330 const s = String(content == null ? '' : content);
1331 return s.length + ':' + s.slice(0, 32);
1332 }
1333
1334 // Injecte ou met à jour un <style> donné dans tous les documents cibles.
1335 // Utilise data-aibui-sig pour éviter les write inutiles (qui ne changent
1336 // rien visuellement mais déclenchent des mutations du DOM).
1337 function upsertStyleInAllDocs(styleId, cssContent) {
1338 const sig = computeContentSignature(cssContent);
1339 getEditorTargetDocuments().forEach((doc) => {
1340 try {
1341 let el = doc.getElementById(styleId);
1342 if (el && el.getAttribute('data-aibui-sig') === sig) return;
1343 if (!el) {
1344 el = doc.createElement('style');
1345 el.id = styleId;
1346 el.type = 'text/css';
1347 (doc.head || doc.documentElement).appendChild(el);
1348 }
1349 el.textContent = cssContent || '';
1350 el.setAttribute('data-aibui-sig', sig);
1351 } catch (e) { /* ignore single doc failures */ }
1352 });
1353 }
1354
1355 // Injecte ou met à jour un <script> donné dans tous les documents cibles.
1356 // data-aibui-sig évite la ré-exécution du code lorsque rien n'a changé
1357 // (important : le MutationObserver observe le DOM et pourrait sinon
1358 // rejouer le script à chaque micro-changement de l'éditeur).
1359 function upsertScriptInAllDocs(scriptId, jsContent) {
1360 const sig = computeContentSignature(jsContent);
1361 getEditorTargetDocuments().forEach((doc) => {
1362 try {
1363 const previous = doc.getElementById(scriptId);
1364 if (previous && previous.getAttribute('data-aibui-sig') === sig) return;
1365 if (previous && previous.parentNode) {
1366 previous.parentNode.removeChild(previous);
1367 }
1368 if (!jsContent) return;
1369 const script = doc.createElement('script');
1370 script.id = scriptId;
1371 script.type = 'text/javascript';
1372 script.setAttribute('data-aibui-sig', sig);
1373 // IIFE + try/catch pour isoler les erreurs du code utilisateur.
1374 script.textContent = '(function(){ try { '
1375 + jsContent
1376 + ' } catch(e){ if (window.console) console.error("ai-builder editor JS error", e); } })();';
1377 (doc.head || doc.documentElement).appendChild(script);
1378 } catch (e) { /* ignore single doc failures */ }
1379 });
1380 }
1381
1382 // Mémoriser les derniers contenus pour permettre la ré-injection automatique
1383 // quand un nouvel iframe est créé (changement de template dans le Site Editor).
1384 let lastInjectedEditorCSS = '';
1385 let lastInjectedEditorJS = '';
1386
1387 function injectCSSInEditor(cssContent) {
1388 lastInjectedEditorCSS = cssContent || '';
1389 upsertStyleInAllDocs('ai-builder-editor-css', lastInjectedEditorCSS);
1390 }
1391
1392 // Fonction pour injecter le CSS dans le frontend
1393 function injectCSSInFrontend(cssContent) {
1394 // Créer un style tag pour le frontend
1395 const styleId = "ai-builder-frontend-css";
1396 let styleElement = document.getElementById(styleId);
1397
1398 if (!styleElement) {
1399 styleElement = document.createElement("style");
1400 styleElement.id = styleId;
1401 styleElement.type = "text/css";
1402 document.head.appendChild(styleElement);
1403 }
1404
1405 styleElement.textContent = cssContent;
1406 }
1407
1408 // Fonction pour sauvegarder le CSS dans les meta du post via AJAX WordPress
1409 async function saveCSSInPostMeta(cssContent, cssType = "page", replace = false) {
1410 try {
1411 const postId = (typeof getEditorTargetId === 'function')
1412 ? getEditorTargetId()
1413 : wp.data.select("core/editor").getCurrentPostId();
1414 if (!postId) return;
1415
1416 const formData = new FormData();
1417 formData.append("action", "aibui_save_post_css");
1418 formData.append("nonce", window.aiBuilderNonce);
1419 formData.append("post_id", postId);
1420 formData.append("css_content", cssContent);
1421 formData.append("css_type", cssType);
1422 formData.append("replace", replace ? "1" : "0");
1423
1424 await fetch(window.ajaxurl, {
1425 method: "POST",
1426 body: formData,
1427 });
1428 } catch (err) {
1429 console.error("Error saving CSS to post meta:", err);
1430 }
1431 }
1432
1433 function injectJSInEditor(jsContent) {
1434 lastInjectedEditorJS = jsContent || '';
1435 upsertScriptInAllDocs('ai-builder-editor-js', lastInjectedEditorJS);
1436 }
1437
1438 // Observe l'apparition / changement d'iframes dans l'admin pour ré-injecter
1439 // automatiquement le CSS / JS dès que l'éditeur (re)crée son canvas.
1440 (function setupEditorReinjectionObserver() {
1441 try {
1442 const observe = () => {
1443 const observer = new MutationObserver(() => {
1444 // On attend un micro-délai pour laisser l'iframe charger son document.
1445 clearTimeout(window.__aiBuilderReinjectTimer);
1446 window.__aiBuilderReinjectTimer = setTimeout(() => {
1447 try {
1448 if (lastInjectedEditorCSS) {
1449 upsertStyleInAllDocs('ai-builder-editor-css', lastInjectedEditorCSS);
1450 }
1451 if (lastInjectedEditorJS) {
1452 upsertScriptInAllDocs('ai-builder-editor-js', lastInjectedEditorJS);
1453 }
1454 } catch (err) { /* ignore */ }
1455 }, 250);
1456 });
1457 observer.observe(document.body, { childList: true, subtree: true });
1458 };
1459 if (document.body) {
1460 observe();
1461 } else {
1462 document.addEventListener('DOMContentLoaded', observe, { once: true });
1463 }
1464 } catch (e) { /* ignore */ }
1465 })();
1466
1467 // Fonction pour injecter le JS dans le frontend
1468 function injectJSInFrontend(jsContent) {
1469 // Créer un script tag pour le frontend
1470 const scriptId = "ai-builder-frontend-js";
1471 let scriptElement = document.getElementById(scriptId);
1472
1473 if (!scriptElement) {
1474 scriptElement = document.createElement("script");
1475 scriptElement.id = scriptId;
1476 scriptElement.type = "text/javascript";
1477 document.head.appendChild(scriptElement);
1478 }
1479
1480 scriptElement.textContent = jsContent;
1481 }
1482
1483 // Fonction pour sauvegarder le JS dans les meta du post via AJAX WordPress
1484 async function saveJSInPostMeta(jsContent, jsType = "page", replace = false) {
1485 try {
1486 const postId = (typeof getEditorTargetId === 'function')
1487 ? getEditorTargetId()
1488 : wp.data.select("core/editor").getCurrentPostId();
1489 if (!postId) return;
1490
1491 const formData = new FormData();
1492 formData.append("action", "aibui_save_post_js");
1493 formData.append("nonce", window.aiBuilderNonce);
1494 formData.append("post_id", postId);
1495 formData.append("js_content", jsContent);
1496 formData.append("js_type", jsType);
1497 formData.append("replace", replace ? "1" : "0");
1498
1499 await fetch(window.ajaxurl, {
1500 method: "POST",
1501 body: formData,
1502 });
1503 } catch (err) {
1504 console.error("Error saving JS to post meta:", err);
1505 }
1506 }
1507
1508 // Fonction pour charger le JS depuis les meta du post via AJAX WordPress
1509 async function loadJSFromPostMeta() {
1510 try {
1511 const postId = (typeof getEditorTargetId === 'function')
1512 ? getEditorTargetId()
1513 : wp.data.select("core/editor").getCurrentPostId();
1514 if (!postId) return;
1515
1516 const formData = new FormData();
1517 formData.append("action", "aibui_get_post_js");
1518 formData.append("nonce", window.aiBuilderNonce);
1519 formData.append("post_id", postId);
1520
1521 const res = await fetch(window.ajaxurl, {
1522 method: "POST",
1523 body: formData,
1524 });
1525
1526 if (res.ok) {
1527 const data = await res.json();
1528 if (data.success && data.data) {
1529 // Stocker les JS séparément
1530 window.aiBuilderPageJS = data.data.pageJS || "";
1531 window.aiBuilderBlockJS = data.data.blockJS || "";
1532
1533 // Utiliser le JS combiné pour l'affichage
1534 const combinedJS = data.data.combinedJS || "";
1535 currentJSContent = combinedJS;
1536
1537 // Injecter le JS dans l'éditeur et le frontend
1538 injectJSInEditor(combinedJS);
1539 injectJSInFrontend(combinedJS);
1540
1541 // Les boutons JS et CSS sont toujours visibles
1542
1543 console.log(
1544 "JS loaded - Page:",
1545 window.aiBuilderPageJS.length,
1546 "chars, Blocks:",
1547 window.aiBuilderBlockJS.length,
1548 "chars"
1549 );
1550 }
1551 }
1552 } catch (err) {
1553 console.error("Error loading JS from post meta:", err);
1554 }
1555 }
1556
1557
1558
1559 // Set the meta description field by id once (no retry)
1560 async function setMetaDescriptionField(value) {
1561 const el = document.getElementById('aibui_meta_description_field');
1562 if (!el) return false;
1563 el.value = value || '';
1564 const evt = new Event('input', { bubbles: true });
1565 el.dispatchEvent(evt);
1566 return true;
1567 }
1568
1569 // Update post title locally in the editor
1570 function updatePostTitle(title) {
1571 if (!title) return false;
1572 try {
1573 // Update the title in WordPress editor state
1574 wp.data.dispatch('core/editor').editPost({ title: title });
1575 return true;
1576 } catch (err) {
1577 console.error('Error updating post title:', err);
1578 return false;
1579 }
1580 }
1581
1582 // Mark page as created via AI
1583 async function markPageAsAICreated() {
1584 try {
1585 const postId =
1586 wp?.data?.select("core/editor")?.getCurrentPostId?.() ||
1587 new URL(window.location.href, window.location.origin).searchParams.get("post") ||
1588 new URL(window.location.href, window.location.origin).searchParams.get("postId") ||
1589 new URL(window.location.href, window.location.origin).searchParams.get("p");
1590
1591 if (!postId) {
1592 console.log("No post ID found to mark as AI-created");
1593 return;
1594 }
1595
1596 const formData = new FormData();
1597 formData.append("action", "aibui_mark_ai_created");
1598 formData.append("post_id", postId);
1599 formData.append("nonce", aiBuilderVars.nonce);
1600
1601 const response = await fetch(ajaxurl, {
1602 method: "POST",
1603 body: formData,
1604 });
1605
1606 const result = await response.json();
1607 if (result.success) {
1608 console.log("Page marked as AI-created");
1609 // Injecter le CSS admin immédiatement après marquage
1610 injectAICreatedAdminCSS();
1611 } else {
1612 console.error("Failed to mark page as AI-created:", result);
1613 }
1614 } catch (error) {
1615 console.error("Error marking page as AI-created:", error);
1616 }
1617 }
1618
1619 // Injecter le CSS admin pour masquer le titre
1620 function injectAICreatedAdminCSS() {
1621 // Vérifier si le style existe déjà
1622 if (document.getElementById("aibui-hide-ai-title-admin")) {
1623 return;
1624 }
1625
1626 const style = document.createElement("style");
1627 style.id = "aibui-hide-ai-title-admin";
1628 style.type = "text/css";
1629 style.textContent = `
1630 /* Masquer le titre dans l'éditeur Gutenberg */
1631 .editor-post-title,
1632 .editor-post-title__input,
1633 .edit-post-visual-editor__post-title-wrapper,
1634 .editor-post-title__block,
1635 .wp-block[data-type="core/post-title"],
1636 .block-editor-block-list__block[data-type="core/post-title"],
1637 .wp-block-post-title.editor-post-title__block {
1638 display: none !important;
1639 visibility: hidden !important;
1640 height: 0 !important;
1641 margin: 0 !important;
1642 padding: 0 !important;
1643 overflow: hidden !important;
1644 opacity: 0 !important;
1645 }
1646
1647 .edit-post-visual-editor__post-title-wrapper {
1648 display: none !important;
1649 visibility: hidden !important;
1650 height: 0 !important;
1651 margin: 0 !important;
1652 padding: 0 !important;
1653 overflow: hidden !important;
1654 }
1655 `;
1656 document.head.appendChild(style);
1657 }
1658
1659 // Save meta description in post meta via WordPress AJAX
1660 async function saveMetaDescriptionInPostMeta(metaDesc) {
1661 try {
1662 const postId = wp.data.select("core/editor").getCurrentPostId();
1663 const formData = new FormData();
1664 formData.append("action", "aibui_save_meta_description");
1665 formData.append("nonce", window.aiBuilderNonce);
1666 formData.append("post_id", postId);
1667 formData.append("meta_desc", metaDesc || "");
1668 await fetch(window.ajaxurl, { method: "POST", body: formData });
1669 } catch (err) {
1670 console.error("Error saving meta description:", err);
1671 }
1672 }
1673
1674 // Attendre que l'ID du post soit disponible (l'éditeur met un peu de temps à charger)
1675 async function waitForCurrentPostId(maxAttempts = 50, intervalMs = 100) {
1676 for (let attemptIndex = 0; attemptIndex < maxAttempts; attemptIndex++) {
1677 try {
1678 const postId = wp?.data?.select("core/editor")?.getCurrentPostId?.();
1679 if (postId) return postId;
1680 } catch (e) { }
1681 await new Promise((resolve) => setTimeout(resolve, intervalMs));
1682 }
1683 return null;
1684 }
1685
1686 // Récupérer l'identifiant cible pour la sauvegarde / lecture du CSS et du JS.
1687 // - Pour une page / article : ID numérique du post.
1688 // - Pour un template ou un template part (Site Editor) : chaîne composite
1689 // "theme//slug" (ex : "twentytwentyfive//404") qui est gérée côté serveur
1690 // par AIBUI_Template_Assets.
1691 // Cette fonction est robuste : tombe en sécurité sur null si aucune source
1692 // fiable n'est disponible, les consommateurs doivent vérifier la valeur.
1693 function getEditorTargetId() {
1694 const isSiteEditor = (typeof window !== 'undefined')
1695 && typeof window.location !== 'undefined'
1696 && typeof window.location.pathname === 'string'
1697 && window.location.pathname.indexOf('site-editor.php') !== -1;
1698
1699 // 1) Dans le Site Editor : essayer d'obtenir la clé "theme//slug".
1700 if (isSiteEditor) {
1701 try {
1702 const editSite = wp && wp.data && typeof wp.data.select === 'function'
1703 ? wp.data.select('core/edit-site')
1704 : null;
1705 if (editSite && typeof editSite.getEditedPostId === 'function') {
1706 const edited = editSite.getEditedPostId();
1707 if (typeof edited === 'string' && edited.indexOf('//') !== -1) {
1708 return edited;
1709 }
1710 }
1711 } catch (e) { /* ignore */ }
1712
1713 // Fallback : lire les paramètres URL. WordPress encode la barre en %2F ;
1714 // URLSearchParams décode automatiquement, donc on obtient le bon texte.
1715 try {
1716 const url = new URL(window.location.href);
1717 const rawP = url.searchParams.get('p') || url.searchParams.get('postId');
1718 if (typeof rawP === 'string' && rawP.indexOf('//') !== -1) {
1719 // Format possible : "/wp_template/theme//slug" → on retire le préfixe de type.
1720 const stripped = rawP.replace(/^\/(wp_template|wp_template_part|pattern)\//, '');
1721 if (stripped.indexOf('//') !== -1) return stripped;
1722 }
1723 } catch (e) { /* ignore */ }
1724 }
1725
1726 // 2) Cas standard : ID numérique fourni par core/editor.
1727 try {
1728 const id = wp && wp.data && typeof wp.data.select === 'function'
1729 ? wp.data.select('core/editor').getCurrentPostId()
1730 : null;
1731 if (id) return id;
1732 } catch (e) { /* ignore */ }
1733
1734 return null;
1735 }
1736 // Exposer le helper pour d'autres scripts du plugin si besoin.
1737 try { window.aiBuilderGetEditorTargetId = getEditorTargetId; } catch (e) { /* ignore */ }
1738
1739 // Fonction pour charger le CSS depuis les meta du post via AJAX WordPress
1740 async function loadCSSFromPostMeta() {
1741 try {
1742 const postId = getEditorTargetId();
1743 if (!postId) return;
1744
1745 const formData = new FormData();
1746 formData.append("action", "aibui_get_post_css");
1747 formData.append("nonce", window.aiBuilderNonce);
1748 formData.append("post_id", postId);
1749
1750 const res = await fetch(window.ajaxurl, {
1751 method: "POST",
1752 body: formData,
1753 });
1754
1755 if (res.ok) {
1756 const data = await res.json();
1757 if (data.success && data.data) {
1758 // Stocker les CSS séparément
1759 window.aiBuilderPageCSS = data.data.pageCss || "";
1760 window.aiBuilderBlockCSS = data.data.blockCss || "";
1761
1762 // Utiliser le CSS combiné pour l'affichage
1763 const combinedCSS = data.data.combinedCss || "";
1764 currentCSSContent = combinedCSS;
1765
1766 // Injecter le CSS dans l'éditeur et le frontend
1767 injectCSSInEditor(combinedCSS);
1768 injectCSSInFrontend(combinedCSS);
1769
1770 // Les boutons CSS et JS sont toujours visibles
1771
1772 console.log(
1773 "CSS loaded - Page:",
1774 window.aiBuilderPageCSS.length,
1775 "chars, Blocks:",
1776 window.aiBuilderBlockCSS.length,
1777 "chars"
1778 );
1779 }
1780 }
1781 } catch (err) {
1782 console.error("Error loading CSS from post meta:", err);
1783 }
1784 }
1785
1786
1787
1788 // Initialiser le chargement du CSS une fois que l'ID du post est prêt
1789 (async function initEditorCssLoad() {
1790 // Vérifier que les variables AJAX sont disponibles
1791 if (!window.ajaxurl || !window.aiBuilderNonce) {
1792 console.warn("AI Builder: AJAX variables not available, skipping CSS load");
1793 return;
1794 }
1795
1796 // Attendre un peu pour que WordPress soit complètement chargé
1797 await new Promise(resolve => setTimeout(resolve, 1000));
1798
1799 const postId = await waitForCurrentPostId();
1800 if (postId) {
1801 loadCSSFromPostMeta();
1802 loadJSFromPostMeta();
1803 } else {
1804 console.warn(
1805 "AI Builder: unable to resolve current post ID to load CSS/JS."
1806 );
1807 }
1808 })();
1809
1810 /**
1811 * Build a compact structural summary of the current page for AI context.
1812 *
1813 * The summary describes the existing blocks, custom CSS and JS so the AI
1814 * understands what is already on the page without receiving every raw byte.
1815 * The result is capped at PAGE_CONTEXT_MAX_CHARS characters so the token
1816 * overhead remains predictable.
1817 *
1818 * Format (plain text, easy to read by the LLM):
1819 *
1820 * Page title: <title>
1821 * Post type: page | post | ...
1822 * Blocks (N total):
1823 * [0] core/group (align:full) > core/heading(1): "Hero title" | core/paragraph: "Subtitle…"
1824 * [1] core/columns > col0: core/image | col1: core/paragraph: "Team member…"
1825 * ...
1826 * Custom CSS: yes (<N> chars) | none
1827 * Custom JS: yes (<N> chars) | none
1828 */
1829 const PAGE_CONTEXT_MAX_CHARS = 3000;
1830
1831 function buildPageContext() {
1832 try {
1833 if (
1834 !window.wp ||
1835 !wp.data ||
1836 !wp.data.select("core/block-editor") ||
1837 !wp.data.select("core/editor")
1838 ) {
1839 return "";
1840 }
1841
1842 const title =
1843 wp.data.select("core/editor").getEditedPostAttribute("title") || "";
1844 const postType =
1845 wp.data.select("core/editor").getCurrentPostType() || "";
1846 const blocks = wp.data.select("core/block-editor").getBlocks() || [];
1847
1848 // --- CSS hint (length only, not the full content) ---
1849 const cssLength =
1850 (window.aiBuilderPageCSS ? window.aiBuilderPageCSS.length : 0) +
1851 (window.aiBuilderBlockCSS ? window.aiBuilderBlockCSS.length : 0);
1852
1853 // --- JS hint: length + function names ---
1854 const jsStyleEl = document.getElementById("ai-builder-editor-js");
1855 const jsSource = jsStyleEl ? (jsStyleEl.textContent || "") : "";
1856 const jsLength = jsSource.length;
1857
1858 // Extract declared function names (named functions, arrow/const/let/var assignments)
1859 const jsFunctionNames = [];
1860 if (jsLength > 0) {
1861 const fnPatterns = [
1862 /\bfunction\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g,
1863 /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>|\w+\s*=>)/g,
1864 ];
1865 for (const re of fnPatterns) {
1866 let m;
1867 while ((m = re.exec(jsSource)) !== null) {
1868 if (!jsFunctionNames.includes(m[1])) {
1869 jsFunctionNames.push(m[1]);
1870 }
1871 }
1872 }
1873 }
1874
1875 // --- Block tree summariser ---
1876 function summariseBlock(block, depth) {
1877 if (!block || !block.name) return "";
1878
1879 const name = block.name; // e.g. "core/heading"
1880 const attrs = block.attributes || {};
1881
1882 // Extract the most useful attributes compactly
1883 const attrParts = [];
1884 if (attrs.level) attrParts.push("h" + attrs.level);
1885 if (attrs.align) attrParts.push("align:" + attrs.align);
1886 if (attrs.textAlign) attrParts.push("textAlign:" + attrs.textAlign);
1887 if (attrs.layout && attrs.layout.type)
1888 attrParts.push("layout:" + attrs.layout.type);
1889 if (attrs.className) attrParts.push("." + attrs.className);
1890 if (attrs.style && attrs.style.color && attrs.style.color.background)
1891 attrParts.push("bg:" + attrs.style.color.background);
1892
1893 // Extra detail for core/cover: overlay color and gradient
1894 if (name === "core/cover") {
1895 if (attrs.customGradient) {
1896 attrParts.push("gradient:" + attrs.customGradient);
1897 } else if (attrs.gradient) {
1898 attrParts.push("gradient-slug:" + attrs.gradient);
1899 }
1900 if (attrs.customOverlayColor) {
1901 attrParts.push("overlay:" + attrs.customOverlayColor);
1902 } else if (attrs.overlayColor) {
1903 attrParts.push("overlay-slug:" + attrs.overlayColor);
1904 }
1905 if (typeof attrs.dimRatio === "number") {
1906 attrParts.push("dim:" + attrs.dimRatio + "%");
1907 }
1908 }
1909
1910 const attrStr = attrParts.length ? "(" + attrParts.join(",") + ")" : "";
1911
1912 // Extract visible text content (first 80 chars)
1913 let textHint = "";
1914 if (attrs.content) {
1915 const raw = String(attrs.content).replace(/<[^>]*>/g, "");
1916 if (raw.trim()) textHint = ': "' + raw.trim().slice(0, 80) + '"';
1917 } else if (attrs.value) {
1918 const raw = String(attrs.value).replace(/<[^>]*>/g, "");
1919 if (raw.trim()) textHint = ': "' + raw.trim().slice(0, 80) + '"';
1920 } else if (attrs.text) {
1921 const raw = String(attrs.text).replace(/<[^>]*>/g, "");
1922 if (raw.trim()) textHint = ': "' + raw.trim().slice(0, 80) + '"';
1923 } else if (attrs.alt) {
1924 textHint = ': [img alt="' + String(attrs.alt).slice(0, 60) + '"]';
1925 } else if (attrs.url && name === "core/image") {
1926 textHint = ": [image]";
1927 }
1928
1929 const shortName = name.replace("core/", "").replace("ai-builder/", "aibui/");
1930 let line = shortName + attrStr + textHint;
1931
1932 // Recurse into inner blocks (max depth 3 to stay compact)
1933 if (
1934 Array.isArray(block.innerBlocks) &&
1935 block.innerBlocks.length > 0 &&
1936 depth < 3
1937 ) {
1938 const children = block.innerBlocks
1939 .map((b) => summariseBlock(b, depth + 1))
1940 .filter(Boolean)
1941 .join(" | ");
1942 if (children) line += " > " + children;
1943 }
1944
1945 return line;
1946 }
1947
1948 // Build block list lines
1949 const blockLines = blocks.map((block, idx) => {
1950 const summary = summariseBlock(block, 0);
1951 return "[" + idx + "] " + summary;
1952 });
1953
1954 const cssHint =
1955 cssLength > 0 ? "yes (" + cssLength + " chars)" : "none";
1956 let jsHint = "none";
1957 if (jsLength > 0) {
1958 jsHint = "yes (" + jsLength + " chars)";
1959 if (jsFunctionNames.length > 0) {
1960 jsHint += " — functions: " + jsFunctionNames.join(", ");
1961 }
1962 }
1963
1964 let context =
1965 "Page title: " +
1966 title +
1967 "\n" +
1968 "Post type: " +
1969 postType +
1970 "\n" +
1971 "Blocks (" +
1972 blocks.length +
1973 " total):\n" +
1974 blockLines.join("\n") +
1975 "\n" +
1976 "Custom CSS: " +
1977 cssHint +
1978 "\n" +
1979 "Custom JS: " +
1980 jsHint;
1981
1982 // Cap to PAGE_CONTEXT_MAX_CHARS
1983 if (context.length > PAGE_CONTEXT_MAX_CHARS) {
1984 context =
1985 context.slice(0, PAGE_CONTEXT_MAX_CHARS - 40) +
1986 "\n[... context truncated at " +
1987 PAGE_CONTEXT_MAX_CHARS +
1988 " chars]";
1989 }
1990
1991 console.log("context: ", context);
1992
1993 return context;
1994 } catch (e) {
1995 console.warn("AI Builder: could not build page context", e);
1996 return "";
1997 }
1998 }
1999
2000 function getPatternName() {
2001 try {
2002 const isPatternEditor = (typeof aiBuilderVars !== 'undefined' && !!aiBuilderVars.isPatternEditor) || window.location.pathname.includes('site-editor.php');
2003 if (!isPatternEditor) return '';
2004 const url = new URL(window.location.href);
2005 const p = url.searchParams.get('p') || url.searchParams.get('postId');
2006 if (!p) return '';
2007 const decoded = decodeURIComponent(p);
2008 // Use correct WordPress terminology: template part (header/footer)
2009 return decoded;
2010 } catch (e) {
2011 return '';
2012 }
2013 }
2014
2015 // --- Visual style presets for the chat widget ---
2016 // Style preview images live in WordPress Media Library (same folder / filenames).
2017 const CHAT_STYLES_CDN =
2018 "https://website-ai-builder.com/wp-content/uploads/2026/04/";
2019
2020 const CHAT_VISUAL_STYLES = [
2021 {
2022 id: "neobrutalism",
2023 label: "Neobrutalism",
2024 image: CHAT_STYLES_CDN + "neobrutalism.jpg",
2025 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.`,
2026 },
2027 {
2028 id: "premium-dark",
2029 label: "Premium Dark Mode",
2030 image: CHAT_STYLES_CDN + "premium-dark.jpg",
2031 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.`,
2032 },
2033 {
2034 id: "modern-minimalist",
2035 label: "Modern Minimalist",
2036 image: CHAT_STYLES_CDN + "modern-mini.jpg",
2037 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.`,
2038 },
2039 {
2040 id: "editorial-typography",
2041 label: "Editorial Typography",
2042 image: CHAT_STYLES_CDN + "edito.jpg",
2043 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.`,
2044 },
2045 {
2046 id: "organic-nature",
2047 label: "Organic & Nature",
2048 image: CHAT_STYLES_CDN + "orga-nat.jpg",
2049 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.`,
2050 },
2051 {
2052 id: "retrofuturism",
2053 label: "Retrofuturism",
2054 image: CHAT_STYLES_CDN + "retrofut.jpg",
2055 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.`,
2056 },
2057 {
2058 id: "saas-tech",
2059 label: "SaaS / Tech Startup",
2060 image: CHAT_STYLES_CDN + "saas-tech.jpg",
2061 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.`,
2062 },
2063 {
2064 id: "luxury",
2065 label: "Luxury & High-End",
2066 image: CHAT_STYLES_CDN + "luxury.jpg",
2067 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.`,
2068 },
2069 {
2070 id: "bento-grid",
2071 label: "Bento Grid",
2072 image: CHAT_STYLES_CDN + "bento.jpg",
2073 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.`,
2074 },
2075 {
2076 id: "y2k-revival",
2077 label: "Y2K Revival",
2078 image: CHAT_STYLES_CDN + "y2k.jpg",
2079 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.`,
2080 },
2081 {
2082 id: "swiss-typographic",
2083 label: "Swiss / Intl Style",
2084 image: CHAT_STYLES_CDN + "swiss.jpg",
2085 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.`,
2086 },
2087 {
2088 id: "claymorphism",
2089 label: "Claymorphism",
2090 image: CHAT_STYLES_CDN + "clay.jpg",
2091 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.`,
2092 },
2093 ];
2094
2095 function buildChatStyleStorageKey() {
2096 try {
2097 const id = wp?.data?.select("core/editor")?.getCurrentPostId?.();
2098 if (id && Number(id) > 0) {
2099 return "aibui_chat_visual_style_v1_post_" + id;
2100 }
2101 } catch (e) { }
2102 const raw = (window.location.pathname || "") + (window.location.search || "");
2103 try {
2104 return (
2105 "aibui_chat_visual_style_v1_ctx_" +
2106 btoa(unescape(encodeURIComponent(raw)))
2107 .replace(/\+/g, "-")
2108 .replace(/\//g, "_")
2109 .replace(/=+$/, "")
2110 );
2111 } catch (e2) {
2112 return "aibui_chat_visual_style_v1_fallback";
2113 }
2114 }
2115
2116 let chatSelectedVisualStyleId = null;
2117 let chatStylePanelOpen = false;
2118 let chatStyleStoragePollKey = "";
2119
2120 function loadChatVisualStyleFromStorage() {
2121 try {
2122 const raw = localStorage.getItem(buildChatStyleStorageKey());
2123 if (!raw) {
2124 chatSelectedVisualStyleId = null;
2125 return;
2126 }
2127 const data = JSON.parse(raw);
2128 const id = data && data.id ? String(data.id) : "";
2129 if (id && CHAT_VISUAL_STYLES.some((s) => s.id === id)) {
2130 chatSelectedVisualStyleId = id;
2131 } else {
2132 chatSelectedVisualStyleId = null;
2133 }
2134 } catch (e) {
2135 chatSelectedVisualStyleId = null;
2136 }
2137 }
2138
2139 function saveChatVisualStyleToStorage() {
2140 try {
2141 const key = buildChatStyleStorageKey();
2142 if (!chatSelectedVisualStyleId) {
2143 localStorage.removeItem(key);
2144 } else {
2145 localStorage.setItem(
2146 key,
2147 JSON.stringify({ id: chatSelectedVisualStyleId })
2148 );
2149 }
2150 } catch (e) { }
2151 }
2152
2153 function getVisualStylePromptForApi() {
2154 const s = CHAT_VISUAL_STYLES.find((x) => x.id === chatSelectedVisualStyleId);
2155 return s ? s.prompt : undefined;
2156 }
2157
2158 // --- AI Block modifier (Group block mini-panel) ---
2159 const AIBUI_BLOCK_CSS_START = "/* AIBUI_BLOCK_CSS_START";
2160 const AIBUI_BLOCK_CSS_END = "/* AIBUI_BLOCK_CSS_END";
2161 const AIBUI_BLOCK_ID_CLASS_PREFIX = "aibui-bid-";
2162 const AIBUI_BLOCK_MODIFY_ENDPOINT = "/ai-transform-page/v2-transform-single-block";
2163
2164 function aibuiSafeTruncate(str, maxChars) {
2165 const s = String(str || "");
2166 if (s.length <= maxChars) return s;
2167 return s.slice(0, maxChars - 40) + "\n/* …truncated… */\n";
2168 }
2169
2170 function aibuiShortIdFromClientId(clientId) {
2171 const raw = String(clientId || "").replace(/[^a-zA-Z0-9]/g, "");
2172 return raw.slice(0, 10) || "block";
2173 }
2174
2175 function aibuiEnsureBlockIdClass(block) {
2176 try {
2177 const className = (block && block.attributes && block.attributes.className) ? String(block.attributes.className) : "";
2178 const existing = className.split(/\s+/).find((c) => c.startsWith(AIBUI_BLOCK_ID_CLASS_PREFIX));
2179 if (existing) return existing;
2180 const bid = AIBUI_BLOCK_ID_CLASS_PREFIX + aibuiShortIdFromClientId(block.clientId);
2181 const next = (className ? className + " " : "") + bid;
2182 wp.data.dispatch("core/block-editor").updateBlockAttributes(block.clientId, { className: next });
2183 return bid;
2184 } catch (e) {
2185 return "";
2186 }
2187 }
2188
2189 function aibuiFindCssSegment(cssText, bidClass) {
2190 const css = String(cssText || "");
2191 if (!bidClass) return null;
2192 const startRe = new RegExp("/\\*\\s*AIBUI_BLOCK_CSS_START\\s+bid=" + bidClass.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&") + "\\s*\\*\\/");
2193 const endRe = new RegExp("/\\*\\s*AIBUI_BLOCK_CSS_END\\s+bid=" + bidClass.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&") + "\\s*\\*\\/");
2194 const mStart = startRe.exec(css);
2195 if (!mStart) return null;
2196 const startIdx = mStart.index + mStart[0].length;
2197 const mEnd = endRe.exec(css.slice(startIdx));
2198 if (!mEnd) return null;
2199 const endIdx = startIdx + mEnd.index;
2200 return { startIdx: mStart.index, endIdx: endIdx + mEnd[0].length, inner: css.slice(startIdx, endIdx).trim() };
2201 }
2202
2203 function aibuiExtractRelatedCssForApi(bidClass) {
2204 const pageCss = window.aiBuilderPageCSS || "";
2205 const blockCss = window.aiBuilderBlockCSS || "";
2206
2207 // Prefer explicit segments if present.
2208 const segPage = aibuiFindCssSegment(pageCss, bidClass);
2209 if (segPage) return { origin: "page", css: segPage.inner };
2210 const segBlock = aibuiFindCssSegment(blockCss, bidClass);
2211 if (segBlock) return { origin: "block", css: segBlock.inner };
2212
2213 // Heuristic fallback: collect small windows around occurrences of the class selector.
2214 const selector = "." + bidClass;
2215 const sources = [
2216 { origin: "page", text: pageCss },
2217 { origin: "block", text: blockCss },
2218 ];
2219 for (const src of sources) {
2220 if (!src.text || !src.text.includes(selector)) continue;
2221 const chunks = [];
2222 let idx = 0;
2223 while (idx < src.text.length) {
2224 const at = src.text.indexOf(selector, idx);
2225 if (at === -1) break;
2226 const from = Math.max(0, at - 600);
2227 const to = Math.min(src.text.length, at + 1400);
2228 chunks.push(src.text.slice(from, to));
2229 idx = at + selector.length;
2230 if (chunks.join("\n").length > 5200) break;
2231 }
2232 const combined = chunks.join("\n/* … */\n");
2233 return { origin: src.origin, css: combined };
2234 }
2235
2236 return { origin: "block", css: "" };
2237 }
2238
2239 async function aibuiUpsertBlockCssSegment(bidClass, newInnerCss, origin) {
2240 const cssKey = origin === "page" ? "aiBuilderPageCSS" : "aiBuilderBlockCSS";
2241 const current = window[cssKey] || "";
2242 const existing = aibuiFindCssSegment(current, bidClass);
2243 const startMarker = `/* AIBUI_BLOCK_CSS_START bid=${bidClass} */`;
2244 const endMarker = `/* AIBUI_BLOCK_CSS_END bid=${bidClass} */`;
2245 const nextSegment = `${startMarker}\n${String(newInnerCss || "").trim()}\n${endMarker}\n`;
2246
2247 let next;
2248 if (existing) {
2249 next = current.slice(0, existing.startIdx) + nextSegment + current.slice(existing.endIdx);
2250 } else {
2251 next = (String(current || "").trimEnd() + "\n\n" + nextSegment).trimEnd() + "\n";
2252 }
2253
2254 window[cssKey] = next;
2255 await saveCSSInPostMeta(next, origin === "page" ? "page" : "block", true);
2256 await loadCSSFromPostMeta();
2257 }
2258
2259 async function aibuiRemoveBlockCssSegmentMarkers(bidClass, origin) {
2260 const cssKey = origin === "page" ? "aiBuilderPageCSS" : "aiBuilderBlockCSS";
2261 const current = window[cssKey] || "";
2262 const existing = aibuiFindCssSegment(current, bidClass);
2263 if (!existing) return;
2264
2265 const cleanedSegment = String(existing.inner || "").trim();
2266 const before = current.slice(0, existing.startIdx).trimEnd();
2267 const after = current.slice(existing.endIdx).trimStart();
2268 const parts = [];
2269
2270 if (before) parts.push(before);
2271 if (cleanedSegment) parts.push(cleanedSegment);
2272 if (after) parts.push(after);
2273
2274 const next = parts.join("\n\n").trim();
2275 window[cssKey] = next ? next + "\n" : "";
2276 await saveCSSInPostMeta(window[cssKey], origin === "page" ? "page" : "block", true);
2277 await loadCSSFromPostMeta();
2278 }
2279
2280 const AIBUI_BM_PROMPT_MAX_CHARS = 1000;
2281
2282 function aibuiEnsureBlockModifierPanel() {
2283 let el = document.getElementById("aibui-block-modifier");
2284 if (el) return el;
2285 el = document.createElement("div");
2286 el.id = "aibui-block-modifier";
2287 el.setAttribute("hidden", "");
2288 el.innerHTML = `
2289 <div class="aibui-bm-head">
2290 <div class="aibui-bm-title">Modify</div>
2291 <button type="button" class="aibui-bm-close" aria-label="Close" data-aibui-bm-close>&times;</button>
2292 </div>
2293 <div class="aibui-bm-body">
2294 <label class="aibui-bm-label" for="aibui-bm-prompt">Instruction</label>
2295 <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>
2296 <div class="aibui-bm-meta" aria-live="polite">
2297 <span data-aibui-bm-count>0</span>/${AIBUI_BM_PROMPT_MAX_CHARS}
2298 </div>
2299
2300 <div class="aibui-bm-actions">
2301 <button type="button" class="aibui-bm-btn aibui-bm-btn--primary" data-aibui-bm-submit>Modify</button>
2302 </div>
2303 <div class="aibui-bm-status" role="status" aria-live="polite" data-aibui-bm-status>
2304 <span class="aibui-bm-status-dots" aria-hidden="true" hidden>
2305 <span class="typing-dot"></span>
2306 <span class="typing-dot"></span>
2307 <span class="typing-dot"></span>
2308 </span>
2309 <span class="aibui-bm-status-text" data-aibui-bm-status-text></span>
2310 </div>
2311 </div>
2312 `;
2313 document.body.appendChild(el);
2314
2315 const promptEl = el.querySelector("#aibui-bm-prompt");
2316 const countEl = el.querySelector("[data-aibui-bm-count]");
2317 const metaEl = el.querySelector(".aibui-bm-meta");
2318 const syncCount = () => {
2319 if (!promptEl || !countEl) return;
2320 if (promptEl.value.length > AIBUI_BM_PROMPT_MAX_CHARS) {
2321 promptEl.value = promptEl.value.slice(0, AIBUI_BM_PROMPT_MAX_CHARS);
2322 }
2323 countEl.textContent = String(promptEl.value.length);
2324 if (metaEl) {
2325 metaEl.classList.toggle("aibui-bm-meta--max", promptEl.value.length >= AIBUI_BM_PROMPT_MAX_CHARS);
2326 }
2327 };
2328 if (promptEl) {
2329 promptEl.addEventListener("input", syncCount);
2330 promptEl.addEventListener("paste", () => setTimeout(syncCount, 0));
2331 syncCount();
2332 }
2333
2334 const closeBtn = el.querySelector("[data-aibui-bm-close]");
2335 if (closeBtn) {
2336 closeBtn.addEventListener("click", () => {
2337 el.setAttribute("hidden", "");
2338 if (window._aibuiBlockModifierState) {
2339 window._aibuiBlockModifierState.panelOpen = false;
2340 }
2341 });
2342 }
2343
2344 return el;
2345 }
2346
2347 function aibuiEnsureBlockModifierButton() {
2348 let el = document.getElementById("aibui-block-modifier-btn");
2349 if (el) return el;
2350 el = document.createElement("button");
2351 el.type = "button";
2352 el.id = "aibui-block-modifier-btn";
2353 el.className = "aibui-bm-fab";
2354 el.setAttribute("hidden", "");
2355 el.textContent = "Modify with AI";
2356 document.body.appendChild(el);
2357 return el;
2358 }
2359
2360 function aibuiGetCanvasBlockEl(clientId) {
2361 const id = String(clientId || "");
2362 if (!id) return null;
2363
2364 function pickBestMatch(nodes) {
2365 if (!nodes || !nodes.length) return null;
2366 // Prefer elements in the writing flow / block list, and avoid list view tree.
2367 let best = null;
2368 for (const el of nodes) {
2369 if (!el || !el.getBoundingClientRect) continue;
2370 // Ignore hidden/non-rendered nodes.
2371 const rect = el.getBoundingClientRect();
2372 if (!rect || rect.width <= 0 || rect.height <= 0) continue;
2373 if (el.closest && el.closest(".block-editor-list-view-tree")) continue;
2374 if (el.closest && el.closest(".block-editor-list-view-leaf")) continue;
2375 const score =
2376 (el.closest && (el.closest(".block-editor-writing-flow") || el.closest(".block-editor-block-list__layout")) ? 100 : 0) +
2377 (el.classList && el.classList.contains("block-editor-block-list__block") ? 50 : 0) +
2378 Math.max(0, 20 - Math.min(20, rect.top / 50));
2379 if (!best || score > best.score) {
2380 best = { el, score };
2381 }
2382 }
2383 return best ? best.el : null;
2384 }
2385
2386 try {
2387 const nodes = Array.from(document.querySelectorAll(`[data-block="${id}"]`));
2388 const picked = pickBestMatch(nodes);
2389 if (picked) return { el: picked, iframe: null };
2390 } catch (e) { }
2391
2392 // If the editor canvas lives in an iframe, try to find the block there.
2393 try {
2394 const iframes = Array.from(document.querySelectorAll("iframe"));
2395 for (const frame of iframes) {
2396 try {
2397 const doc = frame.contentDocument;
2398 if (!doc) continue;
2399 const nodes = Array.from(doc.querySelectorAll(`[data-block="${id}"]`));
2400 const picked = pickBestMatch(nodes);
2401 if (picked) return { el: picked, iframe: frame };
2402 } catch (e2) {
2403 // cross-origin or unavailable iframe
2404 }
2405 }
2406 } catch (e3) { }
2407
2408 return null;
2409 }
2410
2411 function aibuiPositionPanelNextToBlock(panelEl, clientId) {
2412 try {
2413 const found = aibuiGetCanvasBlockEl(clientId);
2414 if (!found || !found.el) return;
2415 const blockEl = found.el;
2416 const rect = blockEl.getBoundingClientRect();
2417 const frameRect = found.iframe ? found.iframe.getBoundingClientRect() : null;
2418 const panelWidth = 320;
2419 const gap = 8;
2420 const top = Math.max(12, Math.min(window.innerHeight - 260, rect.top + 55));
2421 let left = rect.left + gap;
2422 if (left + panelWidth > window.innerWidth - 12) {
2423 left = Math.max(12, rect.right - panelWidth - gap);
2424 }
2425 const absTop = (frameRect ? frameRect.top : 0) + top;
2426 const absLeft = (frameRect ? frameRect.left : 0) + Math.max(12, left);
2427 panelEl.style.transform = `translate3d(${absLeft}px, ${absTop}px, 0)`;
2428 } catch (e) { }
2429 }
2430
2431 function aibuiPositionButtonNextToBlock(btnEl, clientId) {
2432 try {
2433 const found = aibuiGetCanvasBlockEl(clientId);
2434 if (!found || !found.el) return;
2435 const blockEl = found.el;
2436 const rect = blockEl.getBoundingClientRect();
2437 const frameRect = found.iframe ? found.iframe.getBoundingClientRect() : null;
2438 const gap = 8;
2439 const btnWidth = 120;
2440 const btnHeight = 32;
2441 // Emulate "position: sticky" but anchored to the selected block:
2442 // top = min( max(blockTop+offset, stickyTop), blockBottom - height - offset )
2443 const stickyTop = 12;
2444 const offset = 8;
2445 const topMin = rect.top + offset;
2446 const topMax = rect.bottom - btnHeight - offset;
2447 let top = Math.min(Math.max(topMin, stickyTop), topMax);
2448 top = Math.max(12, Math.min(window.innerHeight - btnHeight - 12, top));
2449
2450 // If the block is fully above the sticky line, fade out the button (still selected).
2451 if (rect.bottom < stickyTop + 2) {
2452 btnEl.style.opacity = "0";
2453 btnEl.style.pointerEvents = "none";
2454 } else {
2455 btnEl.style.opacity = "1";
2456 btnEl.style.pointerEvents = "";
2457 }
2458 let left = rect.left + 8;
2459 if (left + btnWidth > window.innerWidth - 12) left = Math.max(12, rect.right - btnWidth - gap);
2460 const absTop = (frameRect ? frameRect.top : 0) + top;
2461 const absLeft = (frameRect ? frameRect.left : 0) + Math.max(12, left);
2462 btnEl.style.transform = `translate3d(${absLeft}px, ${absTop}px, 0)`;
2463 } catch (e) { }
2464 }
2465
2466 function aibuiEnsureClassOnBlockObject(blockObj, bidClass) {
2467 try {
2468 if (!blockObj || !bidClass) return blockObj;
2469 const attrs = blockObj.attributes || {};
2470 const cur = attrs.className ? String(attrs.className) : "";
2471 if (cur.split(/\s+/).includes(bidClass)) return blockObj;
2472 blockObj.attributes = {
2473 ...attrs,
2474 className: (cur ? cur + " " : "") + bidClass,
2475 };
2476 return blockObj;
2477 } catch (e) {
2478 return blockObj;
2479 }
2480 }
2481
2482 async function aibuiModifySelectedBlock(clientId) {
2483 const panel = aibuiEnsureBlockModifierPanel();
2484 const promptEl = panel.querySelector("#aibui-bm-prompt");
2485 const statusEl = panel.querySelector("[data-aibui-bm-status]");
2486 const statusTextEl = panel.querySelector("[data-aibui-bm-status-text]");
2487 const statusDotsEl = panel.querySelector(".aibui-bm-status-dots");
2488 const submitBtn = panel.querySelector("[data-aibui-bm-submit]");
2489
2490 const userPrompt = (promptEl ? promptEl.value : "").trim();
2491 if (!userPrompt) {
2492 if (statusDotsEl) statusDotsEl.hidden = true;
2493 if (statusTextEl) statusTextEl.textContent = "Please enter an instruction.";
2494 else if (statusEl) statusEl.textContent = "Please enter an instruction.";
2495 return;
2496 }
2497 if (userPrompt.length > AIBUI_BM_PROMPT_MAX_CHARS) {
2498 if (statusDotsEl) statusDotsEl.hidden = true;
2499 const msg = `Please keep your instruction under ${AIBUI_BM_PROMPT_MAX_CHARS} characters.`;
2500 if (statusTextEl) statusTextEl.textContent = msg;
2501 else if (statusEl) statusEl.textContent = msg;
2502 return;
2503 }
2504
2505 try {
2506 if (submitBtn) submitBtn.disabled = true;
2507 if (statusDotsEl) statusDotsEl.hidden = false;
2508 if (statusTextEl) statusTextEl.textContent = "Modifying block…";
2509 else if (statusEl) statusEl.textContent = "Modifying block…";
2510
2511 let jwtToken = "";
2512 try {
2513 jwtToken = await getJwtToken();
2514 } catch (e) {
2515 if (statusDotsEl) statusDotsEl.hidden = true;
2516 const msg = "You need to connect your account. Go to your WordPress Dashboard → AI Builder → Account.";
2517 if (statusTextEl) statusTextEl.textContent = msg;
2518 else if (statusEl) statusEl.textContent = msg;
2519 return;
2520 }
2521 const block = wp.data.select("core/block-editor").getBlock(clientId);
2522 if (!block) throw new Error("Missing block");
2523 const bidClass = aibuiEnsureBlockIdClass(block);
2524 const cssInfo = aibuiExtractRelatedCssForApi(bidClass);
2525 const cssForApi = aibuiSafeTruncate(cssInfo.css, 5000);
2526
2527 const blockJson = (() => {
2528 try { return JSON.stringify(block); } catch (e) { return ""; }
2529 })();
2530 const blockSerialized = (() => {
2531 try { return wp.blocks.serialize([block]); } catch (e) { return ""; }
2532 })();
2533 const blockContentForApi = aibuiSafeTruncate(blockSerialized || blockJson, 14000);
2534
2535 const sitePlugins = (typeof aiBuilderVars !== 'undefined' && Array.isArray(aiBuilderVars.sitePlugins)) ? aiBuilderVars.sitePlugins.slice(0, 15) : [];
2536 const wordpressVersion = (typeof aiBuilderVars !== 'undefined' && aiBuilderVars.wordpressVersion) ? String(aiBuilderVars.wordpressVersion) : '';
2537 const activeThemeName = (typeof aiBuilderEditorVars !== 'undefined' && aiBuilderEditorVars.activeThemeName)
2538 ? aiBuilderEditorVars.activeThemeName
2539 : ((typeof aiBuilderVars !== 'undefined' && aiBuilderVars.activeThemeName) ? aiBuilderVars.activeThemeName : '');
2540 const conversationHistory = getConversationHistoryString(6);
2541
2542 const res = await fetch(
2543 window.config.apiUrl + AIBUI_BLOCK_MODIFY_ENDPOINT,
2544 {
2545 method: "POST",
2546 headers: {
2547 "Content-Type": "application/json",
2548 Authorization: `Bearer ${jwtToken}`,
2549 },
2550 body: JSON.stringify({
2551 userPrompt,
2552 activeThemeName,
2553 sitePlugins,
2554 wordpressVersion,
2555 conversationHistory,
2556 blockContent: blockContentForApi,
2557 blockCss: cssForApi,
2558 }),
2559 }
2560 );
2561
2562 const txt = await res.text();
2563 let data = {};
2564 try { data = txt ? JSON.parse(txt) : {}; } catch (e) { data = {}; }
2565
2566 if (!res.ok) {
2567 if (statusDotsEl) statusDotsEl.hidden = true;
2568 const rawMsg =
2569 (data && typeof data === "object" && (data.message || data.error || data.data)) ? String(data.message || data.error || data.data)
2570 : (txt ? String(txt) : "");
2571 console.log("rawMsg: ", rawMsg);
2572 const msgLower = rawMsg.toLowerCase();
2573
2574 if (res.status === 402 || (data && data.error === "not-enough-credits")) {
2575 // Credits exhausted
2576 try { showNotEnoughCreditsToast(); } catch (e) { }
2577 const msg = "You have no credits left. Please purchase more credits to continue.";
2578 if (statusTextEl) statusTextEl.textContent = msg;
2579 else if (statusEl) statusEl.textContent = msg;
2580 return;
2581 }
2582
2583 if (msgLower.includes("no token found")) {
2584 const msg = "You need to connect your account. Go to your WordPress Dashboard → AI Builder → Account.";
2585 if (statusTextEl) statusTextEl.textContent = msg;
2586 else if (statusEl) statusEl.textContent = msg;
2587 return;
2588 }
2589
2590 const msg = "Something went wrong. Please try again.";
2591 if (statusTextEl) statusTextEl.textContent = msg;
2592 else if (statusEl) statusEl.textContent = msg;
2593 return;
2594 }
2595
2596 if (data.error === "not-enough-credits") {
2597 showNotEnoughCreditsToast();
2598 if (statusDotsEl) statusDotsEl.hidden = true;
2599 if (statusTextEl) statusTextEl.textContent = "";
2600 else if (statusEl) statusEl.textContent = "";
2601 return;
2602 }
2603
2604 // Update block content if provided
2605 if (data.blockContent) {
2606 try {
2607 let newBlock = buildBlock(data.blockContent);
2608 // Preserve the bid class on the returned root block, so CSS scoping keeps working.
2609 newBlock = aibuiEnsureClassOnBlockObject(newBlock, bidClass);
2610 wp.data.dispatch("core/block-editor").replaceBlocks(clientId, [newBlock]);
2611 } catch (e) {
2612 // ignore if malformed; keep css update if any
2613 }
2614 }
2615
2616 // Update only the related CSS segment if provided
2617 if (data.cssContent) {
2618 const origin = cssInfo && cssInfo.origin ? cssInfo.origin : "block";
2619 await aibuiUpsertBlockCssSegment(bidClass, data.cssContent, origin);
2620 await aibuiRemoveBlockCssSegmentMarkers(bidClass, origin);
2621 }
2622
2623 if (typeof data.creditsLeft !== "undefined") {
2624 try { updateCreditsDisplay(data.creditsLeft); } catch (e) { }
2625 }
2626
2627 if (statusDotsEl) statusDotsEl.hidden = true;
2628 if (statusTextEl) statusTextEl.textContent = "Done.";
2629 else if (statusEl) statusEl.textContent = "Done.";
2630 } catch (e) {
2631 if (statusDotsEl) statusDotsEl.hidden = true;
2632 if (statusTextEl) statusTextEl.textContent = "Something went wrong. Please try again.";
2633 else if (statusEl) statusEl.textContent = "Something went wrong. Please try again.";
2634 } finally {
2635 if (submitBtn) submitBtn.disabled = false;
2636 if (statusDotsEl) statusDotsEl.hidden = true;
2637 }
2638 }
2639
2640 function initAibuiBlockModifier() {
2641 if (!wp || !wp.data || !wp.data.select) return;
2642 const panel = aibuiEnsureBlockModifierPanel();
2643 const btn = aibuiEnsureBlockModifierButton();
2644 let lastClientId = null;
2645 const state = (window._aibuiBlockModifierState = window._aibuiBlockModifierState || { panelOpen: false });
2646 let rafId = 0;
2647
2648 function stopTracking() {
2649 if (rafId) {
2650 try { cancelAnimationFrame(rafId); } catch (e) { }
2651 }
2652 rafId = 0;
2653 }
2654
2655 function track() {
2656 rafId = requestAnimationFrame(track);
2657 if (!lastClientId) return;
2658 if (!btn.hasAttribute("hidden")) aibuiPositionButtonNextToBlock(btn, lastClientId);
2659 if (!panel.hasAttribute("hidden")) aibuiPositionPanelNextToBlock(panel, lastClientId);
2660 }
2661
2662 function ensureTrackingRunning() {
2663 if (rafId) return;
2664 rafId = requestAnimationFrame(track);
2665 }
2666
2667 function isBlockTypeExcluded(blockName) {
2668 const name = String(blockName || "");
2669 if (!name) return true;
2670 // Exclusions: image blocks + text blocks (exception requested)
2671 const excluded = new Set([
2672 // Images / media-like
2673 "core/image",
2674 "core/gallery",
2675 "core/media-text",
2676 // Text
2677 "core/paragraph",
2678 "core/heading",
2679 "core/list",
2680 "core/quote",
2681 "core/pullquote",
2682 "core/preformatted",
2683 "core/code",
2684 "core/verse",
2685 "core/table",
2686 "core/button"
2687 ]);
2688 return excluded.has(name);
2689 }
2690
2691 function updatePanelForSelection() {
2692 const cid = wp.data.select("core/block-editor").getSelectedBlockClientId();
2693 if (!cid) {
2694 lastClientId = null;
2695 state.panelOpen = false;
2696 panel.setAttribute("hidden", "");
2697 btn.setAttribute("hidden", "");
2698 stopTracking();
2699 return;
2700 }
2701 if (cid === lastClientId) return;
2702 lastClientId = cid;
2703 state.panelOpen = false;
2704 panel.setAttribute("hidden", "");
2705
2706 const block = wp.data.select("core/block-editor").getBlock(cid);
2707 if (!block) {
2708 panel.setAttribute("hidden", "");
2709 btn.setAttribute("hidden", "");
2710 stopTracking();
2711 return;
2712 }
2713 if (isBlockTypeExcluded(block.name)) {
2714 panel.setAttribute("hidden", "");
2715 btn.setAttribute("hidden", "");
2716 stopTracking();
2717 return;
2718 }
2719
2720 btn.removeAttribute("hidden");
2721 aibuiPositionButtonNextToBlock(btn, cid);
2722 ensureTrackingRunning();
2723
2724 btn.onclick = () => {
2725 // Toggle panel on repeated clicks for the same selected block
2726 const isOpen = !panel.hasAttribute("hidden") && state.panelOpen === true;
2727 if (isOpen) {
2728 state.panelOpen = false;
2729 panel.setAttribute("hidden", "");
2730 return;
2731 }
2732
2733 state.panelOpen = true;
2734 panel.removeAttribute("hidden");
2735 aibuiPositionPanelNextToBlock(panel, cid);
2736 ensureTrackingRunning();
2737 const submitBtn = panel.querySelector("[data-aibui-bm-submit]");
2738 if (submitBtn) submitBtn.onclick = () => aibuiModifySelectedBlock(cid);
2739 };
2740 }
2741
2742 wp.data.subscribe(() => {
2743 try { updatePanelForSelection(); } catch (e) { }
2744 });
2745
2746 // Keep lightweight listeners as fallbacks (rAF handles smooth tracking)
2747 window.addEventListener("scroll", () => { ensureTrackingRunning(); }, { passive: true });
2748 window.addEventListener("resize", () => { ensureTrackingRunning(); });
2749 }
2750
2751 function syncChatStylePickerUI() {
2752 const trigger = document.getElementById("chat-style-trigger");
2753 const label = document.getElementById("chat-style-trigger-label");
2754 const chevron = document.getElementById("chat-style-trigger-chevron");
2755 const hint = document.getElementById("chat-style-swipe-hint");
2756 const removeBtn = document.getElementById("chat-style-remove");
2757 const outer = document.getElementById("chat-style-slider-outer");
2758 const slider = document.getElementById("chat-style-slider");
2759 if (!trigger || !label || !outer || !slider) return;
2760
2761 if (chatSelectedVisualStyleId) {
2762 const sel = CHAT_VISUAL_STYLES.find((s) => s.id === chatSelectedVisualStyleId);
2763 label.textContent = sel
2764 ? "Style: " + sel.label
2765 : "Style applied";
2766 } else {
2767 label.textContent = "Need inspiration? Try a style";
2768 }
2769
2770 if (removeBtn) {
2771 removeBtn.hidden = !chatSelectedVisualStyleId;
2772 }
2773
2774 const showSlider = chatStylePanelOpen;
2775 outer.hidden = !showSlider;
2776 if (hint) {
2777 hint.hidden = !showSlider;
2778 }
2779 trigger.setAttribute("aria-expanded", showSlider ? "true" : "false");
2780 if (chevron) {
2781 chevron.textContent = showSlider ? "" : "";
2782 }
2783
2784 slider.querySelectorAll(".chat-style-card").forEach((el) => {
2785 const sid = el.getAttribute("data-style-id");
2786 el.classList.toggle("is-selected", sid === chatSelectedVisualStyleId);
2787 el.setAttribute("aria-selected", sid === chatSelectedVisualStyleId ? "true" : "false");
2788 });
2789 }
2790
2791 function initChatVisualStylePicker() {
2792 const picker = document.getElementById("chat-style-picker");
2793 const trigger = document.getElementById("chat-style-trigger");
2794 const removeBtn = document.getElementById("chat-style-remove");
2795 const slider = document.getElementById("chat-style-slider");
2796 if (!picker || !trigger || !slider) return;
2797
2798 const eyeSvg =
2799 '<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>';
2800 slider.innerHTML = CHAT_VISUAL_STYLES.map((s) => {
2801 const safeLabel = String(s.label).replace(/"/g, "&quot;");
2802 return (
2803 `<div class="chat-style-card" data-style-id="${s.id}" role="option" tabindex="0" aria-selected="false" title="${safeLabel}">
2804 <div class="chat-style-card-inner">
2805 <img src="${s.image}" alt="" loading="lazy" width="100" height="100" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'"/>
2806 <span class="chat-style-card-fallback" style="display:none" aria-hidden="true"></span>
2807 <button type="button" class="chat-style-preview-btn" data-style-preview="${s.id}" aria-label="View full style and prompt" title="Preview">${eyeSvg}</button>
2808 </div>
2809 <span class="chat-style-card-label">${s.label}</span>
2810 </div>`
2811 );
2812 }).join("");
2813
2814 slider.querySelectorAll(".chat-style-card-fallback").forEach((fb, i) => {
2815 const preset = CHAT_VISUAL_STYLES[i];
2816 if (preset) {
2817 fb.classList.add("chat-style-fallback--" + preset.id);
2818 }
2819 });
2820
2821 function openChatStyleDetailModal(styleId) {
2822 const s = CHAT_VISUAL_STYLES.find((x) => x.id === styleId);
2823 if (!s) return;
2824 const modal = document.getElementById("chat-style-detail-modal");
2825 const imgEl = document.getElementById("chat-style-detail-img");
2826 const titleEl = document.getElementById("chat-style-detail-title");
2827 const promptEl = document.getElementById("chat-style-detail-prompt");
2828 if (!modal || !imgEl || !titleEl || !promptEl) return;
2829 titleEl.textContent = s.label;
2830 imgEl.src = s.image;
2831 imgEl.alt = s.label;
2832 promptEl.textContent = s.prompt;
2833 modal.removeAttribute("hidden");
2834 modal.setAttribute("aria-hidden", "false");
2835 const closeBtn = document.getElementById("chat-style-detail-close");
2836 if (closeBtn) closeBtn.focus();
2837 }
2838
2839 function closeChatStyleDetailModal() {
2840 const modal = document.getElementById("chat-style-detail-modal");
2841 if (!modal) return;
2842 modal.setAttribute("hidden", "");
2843 modal.setAttribute("aria-hidden", "true");
2844 }
2845
2846 const detailClose = document.getElementById("chat-style-detail-close");
2847 const detailBackdrop = document.getElementById("chat-style-detail-backdrop");
2848 if (detailClose) {
2849 detailClose.addEventListener("click", closeChatStyleDetailModal);
2850 }
2851 if (detailBackdrop) {
2852 detailBackdrop.addEventListener("click", closeChatStyleDetailModal);
2853 }
2854 document.addEventListener("keydown", function (ev) {
2855 if (ev.key !== "Escape") return;
2856 const modal = document.getElementById("chat-style-detail-modal");
2857 if (modal && !modal.hasAttribute("hidden")) {
2858 closeChatStyleDetailModal();
2859 }
2860 });
2861
2862 loadChatVisualStyleFromStorage();
2863 chatStyleStoragePollKey = buildChatStyleStorageKey();
2864 chatStylePanelOpen = !!chatSelectedVisualStyleId;
2865 syncChatStylePickerUI();
2866
2867 setInterval(function () {
2868 try {
2869 const k = buildChatStyleStorageKey();
2870 if (k !== chatStyleStoragePollKey) {
2871 chatStyleStoragePollKey = k;
2872 loadChatVisualStyleFromStorage();
2873 chatStylePanelOpen = !!chatSelectedVisualStyleId;
2874 syncChatStylePickerUI();
2875 }
2876 } catch (e) { }
2877 }, 1200);
2878
2879 trigger.addEventListener("click", function () {
2880 chatStylePanelOpen = !chatStylePanelOpen;
2881 syncChatStylePickerUI();
2882 });
2883
2884 if (removeBtn) {
2885 removeBtn.addEventListener("click", function (ev) {
2886 ev.stopPropagation();
2887 chatSelectedVisualStyleId = null;
2888 saveChatVisualStyleToStorage();
2889 chatStylePanelOpen = false;
2890 syncChatStylePickerUI();
2891 });
2892 }
2893
2894 slider.addEventListener("click", function (ev) {
2895 if (ev.target.closest(".chat-style-preview-btn")) {
2896 ev.preventDefault();
2897 ev.stopPropagation();
2898 const btn = ev.target.closest(".chat-style-preview-btn");
2899 const sid = btn && btn.getAttribute("data-style-preview");
2900 if (sid) openChatStyleDetailModal(sid);
2901 return;
2902 }
2903 const card = ev.target.closest(".chat-style-card");
2904 if (!card) return;
2905 const sid = card.getAttribute("data-style-id");
2906 if (!sid) return;
2907 if (chatSelectedVisualStyleId === sid) {
2908 chatSelectedVisualStyleId = null;
2909 } else {
2910 chatSelectedVisualStyleId = sid;
2911 chatStylePanelOpen = true;
2912 }
2913 saveChatVisualStyleToStorage();
2914 syncChatStylePickerUI();
2915 });
2916
2917 slider.addEventListener("keydown", function (ev) {
2918 const card = ev.target.closest(".chat-style-card");
2919 if (!card || document.activeElement !== card) return;
2920 if (ev.key !== "Enter" && ev.key !== " ") return;
2921 ev.preventDefault();
2922 const sid = card.getAttribute("data-style-id");
2923 if (!sid) return;
2924 if (chatSelectedVisualStyleId === sid) {
2925 chatSelectedVisualStyleId = null;
2926 } else {
2927 chatSelectedVisualStyleId = sid;
2928 chatStylePanelOpen = true;
2929 }
2930 saveChatVisualStyleToStorage();
2931 syncChatStylePickerUI();
2932 });
2933 }
2934
2935 const CHAT_GENERIC_FAILURE_MSG =
2936 "Something went wrong and we couldn't complete your request. Please try again.";
2937
2938 async function sendMessageAIV3() {
2939 const messages = document.getElementById("chat-messages");
2940 const input = document.querySelector("#chat-input textarea");
2941 // const undoBtn = document.getElementById("chat-undo");
2942 const sendBtn = document.getElementById("chat-generate-page");
2943 const blockBtn = document.getElementById("chat-generate-block");
2944 const toggleBtn = document.getElementById("chat-toggle");
2945 const question = input.value.trim();
2946
2947 // Vérifier qu'un prompt est présent
2948 let finalQuestion = question;
2949 const patternName = getPatternName();
2950 if (!finalQuestion) {
2951 addMessage("Please enter a prompt first.", "assistant");
2952 return;
2953 }
2954
2955 const conversationHistory = getConversationHistoryString(6);
2956
2957 // 🔒 Désactiver le bouton + animation loading
2958 sendBtn.disabled = true;
2959 if (blockBtn) blockBtn.disabled = true;
2960 // sendBtn.classList.add("loading");
2961 sendBtn.textContent = "Generating...";
2962
2963 // 🎨 Activer l'effet visuel sur le toggle
2964 if (toggleBtn) {
2965 toggleBtn.classList.add("generating");
2966 }
2967
2968 // 👀 Afficher l'indicateur de saisie de l'IA
2969 if (typeof showTypingIndicator === "function") {
2970 showTypingIndicator();
2971 }
2972
2973 // Ajouter le message utilisateur à l'historique
2974 addMessage(finalQuestion, "user");
2975 input.value = "";
2976 // Réinitialiser le compteur après envoi
2977 if (typeof updateChatCharCounter === "function") {
2978 updateChatCharCounter();
2979 }
2980 messages.scrollTop = messages.scrollHeight;
2981
2982 // Sauvegarder les blocs actuels
2983 previousBlocks = wp.data.select("core/block-editor").getBlocks();
2984
2985 try {
2986 const shouldProceed = await confirmReplacePageContentIfNeeded(previousBlocks);
2987 if (!shouldProceed) {
2988 return;
2989 }
2990
2991 // Récupérer le token JWT
2992 const jwtToken = await getJwtToken();
2993
2994 const wooCommerceInstalled = !!(typeof aiBuilderEditorVars !== 'undefined' && aiBuilderEditorVars.wooCommerceInstalled && aiBuilderEditorVars.wooCommerceInstalled !== '0');
2995 const activeThemeName = (typeof aiBuilderEditorVars !== 'undefined' && aiBuilderEditorVars.activeThemeName) ? aiBuilderEditorVars.activeThemeName : ((typeof aiBuilderVars !== 'undefined' && aiBuilderVars.activeThemeName) ? aiBuilderVars.activeThemeName : '');
2996 const sitePlugins = (typeof aiBuilderVars !== 'undefined' && Array.isArray(aiBuilderVars.sitePlugins)) ? aiBuilderVars.sitePlugins.slice(0, 15) : [];
2997 const wordpressVersion = (typeof aiBuilderVars !== 'undefined' && aiBuilderVars.wordpressVersion) ? String(aiBuilderVars.wordpressVersion) : '';
2998 const pageContext = buildPageContext();
2999 const visualStylePrompt = getVisualStylePromptForApi();
3000 // return;
3001
3002 let res
3003 if (patternName) {
3004 const payload = {
3005 userPrompt: finalQuestion,
3006 // pageContent: pageContent,
3007 patternName: patternName,
3008 conversationHistory,
3009 wooCommerceInstalled: wooCommerceInstalled,
3010 activeThemeName: activeThemeName,
3011 sitePlugins: sitePlugins,
3012 wordpressVersion: wordpressVersion,
3013 pageContext: pageContext || undefined,
3014 visualStylePrompt: visualStylePrompt || undefined,
3015 };
3016 res = await fetch(
3017 window.config.apiUrl + "/ai-transform-page/generate-pattern",
3018 {
3019 method: "POST",
3020 headers: {
3021 "Content-Type": "application/json",
3022 Authorization: `Bearer ${jwtToken}`,
3023 },
3024 body: JSON.stringify(payload),
3025 }
3026 );
3027 } else {
3028 const payload = {
3029 userPrompt: finalQuestion,
3030 // pageContent: pageContent,
3031 conversationHistory,
3032 wooCommerceInstalled: wooCommerceInstalled,
3033 activeThemeName: activeThemeName,
3034 sitePlugins: sitePlugins,
3035 wordpressVersion: wordpressVersion,
3036 pageContext: pageContext || undefined,
3037 visualStylePrompt: visualStylePrompt || undefined,
3038 };
3039 res = await fetch(
3040 window.config.apiUrl + "/ai-transform-page/v2-page-generation",
3041 {
3042 method: "POST",
3043 headers: {
3044 "Content-Type": "application/json",
3045 Authorization: `Bearer ${jwtToken}`,
3046 },
3047 body: JSON.stringify(payload),
3048 }
3049 );
3050 }
3051
3052 const responseText = await res.text();
3053 let data = {};
3054 try {
3055 data = responseText ? JSON.parse(responseText) : {};
3056 } catch (parseErr) {
3057 data = {};
3058 }
3059
3060 // API auth (passport isAuthenticated): not logged in / not verified → 401 + redirectTo
3061 if (res.status === 401) {
3062 addMessage(getAccountRequiredChatMessageHtml(), "assistant");
3063 return;
3064 }
3065
3066 // API billing: out of credits → 402
3067 if (res.status === 402) {
3068 addMessage(getCreditsRequiredChatMessageHtml(), "assistant");
3069 return;
3070 }
3071
3072 if (!res.ok) {
3073 addMessage(CHAT_GENERIC_FAILURE_MSG, "assistant");
3074 return;
3075 }
3076
3077 console.log("data: ", data);
3078 console.log(data.pageContent);
3079 if (data.error === "not-enough-credits") {
3080 showNotEnoughCreditsToast();
3081 return;
3082 }
3083
3084 if (data.pageContent) {
3085 if (data.pageContent !== "[-no-content-to-return-]") {
3086 // Convertir chaque bloc JSON en bloc WordPress
3087 const newBlocks = data.pageContent.map(buildBlock);
3088
3089 // 🔄 Supprimer tous les blocs existants
3090 wp.data.dispatch("core/block-editor").resetBlocks([]);
3091 // ➕ Insérer les nouveaux blocs
3092 wp.data.dispatch("core/block-editor").insertBlocks(newBlocks);
3093 }
3094
3095 // Handle CSS if present
3096 if (data.cssContent && data.cssContent !== "[-no-content-to-return-]") {
3097 console.log("Injecting CSS...");
3098
3099 // Sauvegarder le CSS dans les meta du post (type 'page')
3100 await saveCSSInPostMeta(data.cssContent, "page");
3101
3102 // Recharger le CSS combiné depuis le serveur
3103 await loadCSSFromPostMeta();
3104
3105 // Le bouton CSS est toujours visible
3106 }
3107
3108 // Handle JS if present
3109 if (data.jsContent && data.jsContent !== "[-no-content-to-return-]") {
3110 console.log("Injecting JS...");
3111
3112 // Sauvegarder le JS dans les meta du post (type 'page' pour le chat widget)
3113 await saveJSInPostMeta(data.jsContent, "page");
3114
3115 // Recharger le JS combiné depuis le serveur
3116 await loadJSFromPostMeta();
3117
3118 // Le bouton JS est toujours visible
3119 }
3120
3121 // Handle meta description if present
3122 if (data.postMetaDesc && data.postMetaDesc !== "[-no-content-to-return-]") {
3123 // await saveMetaDescriptionInPostMeta(data.postMetaDesc);
3124 await setMetaDescriptionField(data.postMetaDesc);
3125 }
3126
3127 // Handle title if present
3128 if (data.postTitle && data.postTitle !== "[-no-content-to-return-]") {
3129 updatePostTitle(data.postTitle);
3130 }
3131
3132 let aiResponse =
3133 "Page content has been updated with AI-generated content.";
3134 if (data.aiResponse) {
3135 aiResponse = data.aiResponse;
3136 }
3137
3138 addMessage(aiResponse, "assistant");
3139
3140 // Marquer la page comme créée via IA
3141 await markPageAsAICreated();
3142
3143 // undoBtn.style.display = "block"; // Affiche le bouton "Annuler"
3144 // Mettre à jour les crédits si la réponse contient creditsLeft
3145 if (typeof data.creditsLeft !== "undefined") {
3146 updateCreditsDisplay(data.creditsLeft);
3147 }
3148 } else {
3149 addMessage("Empty or invalid response.", "assistant");
3150 }
3151 } catch (err) {
3152 // WordPress has no stored API token / missing editor context (not linked to an AI Builder account)
3153 if (err && err.aibuiAuthRequired === true) {
3154 addMessage(getAccountRequiredChatMessageHtml(), "assistant");
3155 } else {
3156 addMessage(CHAT_GENERIC_FAILURE_MSG, "assistant");
3157 }
3158 console.log("err : ", err);
3159 } finally {
3160 // 🔓 Réactiver le bouton + retirer animation
3161 sendBtn.disabled = false;
3162 if (blockBtn) blockBtn.disabled = false;
3163 sendBtn.classList.remove("loading");
3164 sendBtn.textContent = "Generate page";
3165 messages.scrollTop = messages.scrollHeight;
3166
3167 // 🎨 Désactiver l'effet visuel sur le toggle
3168 if (toggleBtn) {
3169 toggleBtn.classList.remove("generating");
3170 }
3171
3172 // 👀 Masquer l'indicateur de saisie de l'IA
3173 if (typeof hideTypingIndicator === "function") {
3174 hideTypingIndicator();
3175 }
3176 }
3177 }
3178
3179 async function sendBlockAIV3() {
3180 const messages = document.getElementById("chat-messages");
3181 const input = document.querySelector("#chat-input textarea");
3182 const pageBtn = document.getElementById("chat-generate-page");
3183 const blockBtn = document.getElementById("chat-generate-block");
3184 const toggleBtn = document.getElementById("chat-toggle");
3185 const question = input.value.trim();
3186
3187 if (!question) {
3188 addMessage("Please enter a prompt first.", "assistant");
3189 return;
3190 }
3191
3192 // Disable both buttons while generating
3193 if (pageBtn) pageBtn.disabled = true;
3194 if (blockBtn) blockBtn.disabled = true;
3195 if (blockBtn) blockBtn.textContent = "Generating...";
3196
3197 if (toggleBtn) {
3198 toggleBtn.classList.add("generating");
3199 }
3200 if (typeof showTypingIndicator === "function") {
3201 showTypingIndicator();
3202 }
3203
3204 // Add user message and clear input
3205 addMessage(question, "user");
3206 input.value = "";
3207 if (typeof updateChatCharCounter === "function") {
3208 updateChatCharCounter();
3209 }
3210 if (messages) messages.scrollTop = messages.scrollHeight;
3211
3212 try {
3213 const jwtToken = await getJwtToken();
3214
3215 const wooCommerceInstalled = !!(typeof aiBuilderEditorVars !== 'undefined' && aiBuilderEditorVars.wooCommerceInstalled && aiBuilderEditorVars.wooCommerceInstalled !== '0');
3216 const activeThemeName = (typeof aiBuilderEditorVars !== 'undefined' && aiBuilderEditorVars.activeThemeName)
3217 ? aiBuilderEditorVars.activeThemeName
3218 : ((typeof aiBuilderVars !== 'undefined' && aiBuilderVars.activeThemeName) ? aiBuilderVars.activeThemeName : '');
3219 const sitePlugins = (typeof aiBuilderVars !== 'undefined' && Array.isArray(aiBuilderVars.sitePlugins)) ? aiBuilderVars.sitePlugins.slice(0, 15) : [];
3220 const wordpressVersion = (typeof aiBuilderVars !== 'undefined' && aiBuilderVars.wordpressVersion) ? String(aiBuilderVars.wordpressVersion) : '';
3221 const conversationHistory = getConversationHistoryString(6);
3222 const visualStylePrompt = getVisualStylePromptForApi();
3223
3224 const res = await fetch(
3225 window.config.apiUrl + "/ai-transform-page/v2-generate-single-block",
3226 {
3227 method: "POST",
3228 headers: {
3229 "Content-Type": "application/json",
3230 Authorization: `Bearer ${jwtToken}`,
3231 },
3232 body: JSON.stringify({
3233 prompt: question,
3234 wooCommerceInstalled: wooCommerceInstalled,
3235 activeThemeName: activeThemeName,
3236 sitePlugins: sitePlugins,
3237 wordpressVersion: wordpressVersion,
3238 visualStylePrompt: visualStylePrompt || undefined,
3239 }),
3240 }
3241 );
3242
3243 const responseText = await res.text();
3244 let data = {};
3245 try {
3246 data = responseText ? JSON.parse(responseText) : {};
3247 } catch (e) {
3248 data = {};
3249 }
3250
3251 if (!res.ok) {
3252 addMessage(CHAT_GENERIC_FAILURE_MSG, "assistant");
3253 return;
3254 }
3255
3256 if (data.error === "not-enough-credits") {
3257 showNotEnoughCreditsToast();
3258 return;
3259 }
3260
3261 if (!data.blockContent) {
3262 addMessage("Empty or invalid response.", "assistant");
3263 return;
3264 }
3265
3266 // Save and reload CSS/JS if provided (block-level)
3267 if (data.cssContent && data.cssContent !== "[-no-content-to-return-]") {
3268 await saveCSSInPostMeta(data.cssContent, "block");
3269 await loadCSSFromPostMeta();
3270 }
3271 if (data.jsContent && data.jsContent !== "[-no-content-to-return-]") {
3272 await saveJSInPostMeta(data.jsContent, "block");
3273 await loadJSFromPostMeta();
3274 }
3275
3276 // Insert generated block at the bottom without touching existing content
3277 const newBlock = buildBlock(data.blockContent);
3278 const existing = wp.data.select("core/block-editor").getBlocks();
3279 const insertIndex = Array.isArray(existing) ? existing.length : undefined;
3280 wp.data.dispatch("core/block-editor").insertBlocks([newBlock], insertIndex);
3281
3282 if (typeof data.creditsLeft !== "undefined") {
3283 try {
3284 updateCreditsDisplay(data.creditsLeft);
3285 } catch (e) { }
3286 }
3287
3288 const aiResponse = data.aiResponse || "Block added to the bottom of the page.";
3289 addMessage(aiResponse, "assistant");
3290 } catch (err) {
3291 if (err && err.aibuiAuthRequired === true) {
3292 addMessage(getAccountRequiredChatMessageHtml(), "assistant");
3293 } else {
3294 addMessage(CHAT_GENERIC_FAILURE_MSG, "assistant");
3295 }
3296 } finally {
3297 if (pageBtn) pageBtn.disabled = false;
3298 if (blockBtn) blockBtn.disabled = false;
3299 if (blockBtn) blockBtn.textContent = "Generate block";
3300 if (toggleBtn) toggleBtn.classList.remove("generating");
3301 if (typeof hideTypingIndicator === "function") {
3302 hideTypingIndicator();
3303 }
3304 if (messages) messages.scrollTop = messages.scrollHeight;
3305 }
3306 }
3307
3308 let cachedAICreatedStatus = null;
3309
3310 async function getCurrentPostAICreatedStatus() {
3311 if (cachedAICreatedStatus !== null) return cachedAICreatedStatus;
3312 try {
3313 const postId =
3314 wp?.data?.select("core/editor")?.getCurrentPostId?.() ||
3315 new URL(window.location.href, window.location.origin).searchParams.get("post") ||
3316 new URL(window.location.href, window.location.origin).searchParams.get("postId") ||
3317 new URL(window.location.href, window.location.origin).searchParams.get("p");
3318 if (!postId) return false;
3319
3320 const formData = new FormData();
3321 formData.append("action", "aibui_get_ai_created_status");
3322 formData.append("post_id", postId);
3323 formData.append("nonce", aiBuilderVars.nonce);
3324
3325 const response = await fetch(ajaxurl, { method: "POST", body: formData });
3326 const result = await response.json();
3327 cachedAICreatedStatus = !!(result && result.success && result.data && result.data.isAICreated);
3328 return cachedAICreatedStatus;
3329 } catch (e) {
3330 return false;
3331 }
3332 }
3333
3334 function doesPageHaveExistingContent(blocks) {
3335 try {
3336 if (!Array.isArray(blocks) || blocks.length === 0) return false;
3337 // Treat any non-empty block list as existing content.
3338 // (We can refine later to ignore purely empty placeholders.)
3339 return true;
3340 } catch (e) {
3341 return false;
3342 }
3343 }
3344
3345 function ensureReplaceContentModal() {
3346 let modal = document.getElementById("aibui-replace-content-modal");
3347 if (modal) return modal;
3348
3349 modal = document.createElement("div");
3350 modal.id = "aibui-replace-content-modal";
3351 modal.setAttribute("hidden", "");
3352 modal.innerHTML = `
3353 <div class="aibui-modal-backdrop" data-aibui-modal-close></div>
3354 <div class="aibui-modal-panel" role="dialog" aria-modal="true" aria-labelledby="aibui-replace-content-title">
3355 <h3 id="aibui-replace-content-title" class="aibui-modal-title">Warning</h3>
3356 <p class="aibui-modal-text">
3357 This page content will be replaced by AI-generated content.
3358 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.
3359 </p>
3360 <div class="aibui-modal-actions">
3361 <button type="button" class="aibui-btn aibui-btn--ghost" data-aibui-cancel>Cancel</button>
3362 <button type="button" class="aibui-btn aibui-btn--primary" data-aibui-confirm>Replace content</button>
3363 </div>
3364 </div>
3365 `;
3366 document.body.appendChild(modal);
3367 return modal;
3368 }
3369
3370 function openReplaceContentModal() {
3371 const modal = ensureReplaceContentModal();
3372 modal.removeAttribute("hidden");
3373 return modal;
3374 }
3375
3376 function closeReplaceContentModal() {
3377 const modal = document.getElementById("aibui-replace-content-modal");
3378 if (!modal) return;
3379 modal.setAttribute("hidden", "");
3380 }
3381
3382 async function confirmReplacePageContentIfNeeded(existingBlocks) {
3383 // Only warn if the editor already has content and it wasn't generated by AI Builder.
3384 if (!doesPageHaveExistingContent(existingBlocks)) return true;
3385 const isAICreated = await getCurrentPostAICreatedStatus();
3386 if (isAICreated) return true;
3387
3388 const modal = openReplaceContentModal();
3389
3390 return await new Promise((resolve) => {
3391 const confirmBtn = modal.querySelector("[data-aibui-confirm]");
3392 const cancelBtn = modal.querySelector("[data-aibui-cancel]");
3393 const backdrop = modal.querySelector("[data-aibui-modal-close]");
3394
3395 function cleanup() {
3396 if (confirmBtn) confirmBtn.removeEventListener("click", onConfirm);
3397 if (cancelBtn) cancelBtn.removeEventListener("click", onCancel);
3398 if (backdrop) backdrop.removeEventListener("click", onCancel);
3399 document.removeEventListener("keydown", onKeyDown);
3400 }
3401
3402 function onConfirm() {
3403 cleanup();
3404 closeReplaceContentModal();
3405 resolve(true);
3406 }
3407
3408 function onCancel() {
3409 cleanup();
3410 closeReplaceContentModal();
3411 resolve(false);
3412 }
3413
3414 function onKeyDown(ev) {
3415 if (ev.key === "Escape") onCancel();
3416 }
3417
3418 if (confirmBtn) confirmBtn.addEventListener("click", onConfirm);
3419 if (cancelBtn) cancelBtn.addEventListener("click", onCancel);
3420 if (backdrop) backdrop.addEventListener("click", onCancel);
3421 document.addEventListener("keydown", onKeyDown);
3422 });
3423 }
3424
3425 const pageBtn = document.getElementById("chat-generate-page");
3426 const generateBlockBtn = document.getElementById("chat-generate-block");
3427 if (pageBtn) pageBtn.onclick = sendMessageAIV3;
3428 if (generateBlockBtn) generateBlockBtn.onclick = sendBlockAIV3;
3429 input.addEventListener("keypress", (e) => {
3430 if (e.key === "Enter") sendMessageAIV3();
3431 });
3432 initChatVisualStylePicker();
3433 initAibuiBlockModifier();
3434 loadUserCredits();
3435 loadCSSFromPostMeta();
3436 loadJSFromPostMeta();
3437 });
3438
3439 // Ajouter les styles CSS pour le bouton CSS et la modale
3440 const cssStyles = document.createElement("style");
3441 cssStyles.textContent = `
3442 /* Zone de saisie du chat */
3443 #chat-input {
3444 display: flex;
3445 flex-direction: column;
3446 gap: 6px;
3447 }
3448
3449 #chat-input .chat-input-wrap{
3450 position: relative;
3451 width: 100%;
3452 }
3453
3454 #chat-input .chat-actions{
3455 display:flex;
3456 gap:8px;
3457 align-items:center;
3458 flex-wrap: wrap;
3459 }
3460
3461 #chat-input #chat-generate-page{
3462 flex: 1 1 auto;
3463 min-width: 160px;
3464 }
3465
3466 #chat-input #chat-generate-block{
3467 flex: 0 0 auto;
3468 }
3469
3470 #chat-input .chat-generate-block{
3471 background: rgba(255,255,255,0.9);
3472 border: 1px solid #e2e8f0;
3473 color: #0f172a;
3474 }
3475
3476 #chat-input .chat-generate-block:hover{
3477 background: #ffffff;
3478 }
3479
3480 #chat-input textarea {
3481 width: 100%;
3482 resize: vertical;
3483 }
3484
3485 #chat-expand-input.chat-expand-input{
3486 position: absolute;
3487 right: 8px;
3488 bottom: 8px;
3489 width: 26px;
3490 height: 26px;
3491 min-width: 26px;
3492 min-height: 26px;
3493 max-width: 26px;
3494 max-height: 26px;
3495 aspect-ratio: 1 / 1;
3496 box-sizing: border-box;
3497 border-radius: 8px;
3498 border: 1px solid rgba(148, 163, 184, 0.8);
3499 background: rgba(241, 245, 249, 0.95);
3500 color: #475569;
3501 cursor: pointer;
3502 padding: 0;
3503 display: inline-flex;
3504 align-items: center;
3505 justify-content: center;
3506 line-height: 0;
3507 box-shadow: 0 1px 2px rgba(0,0,0,0.06);
3508 }
3509 #chat-expand-input.chat-expand-input:hover{
3510 background: #ffffff;
3511 border-color: rgba(100, 116, 139, 0.6);
3512 }
3513 #chat-expand-input.chat-expand-input:active{
3514 transform: translateY(1px);
3515 }
3516 #chat-expand-input.chat-expand-input::before{
3517 content: "⤢";
3518 font-size: 14px;
3519 line-height: 1;
3520 display:block;
3521 transform: translateY(-1px);
3522 }
3523
3524 #chat-char-counter {
3525 align-self: flex-end;
3526 font-size: 11px;
3527 color: #888;
3528 }
3529
3530 /* Expanded prompt editor modal */
3531 #chat-expand-modal[hidden]{ display:none !important; }
3532 #chat-expand-modal.chat-expand-modal{
3533 position: fixed;
3534 inset: 0;
3535 z-index: 10020;
3536 }
3537 #chat-expand-modal .chat-expand-backdrop{
3538 position:absolute;
3539 inset:0;
3540 background: rgba(15, 23, 42, 0.55);
3541 border: none;
3542 padding: 0;
3543 cursor: default;
3544 }
3545 #chat-expand-modal .chat-expand-panel{
3546 position: absolute;
3547 left: 50%;
3548 top: 50%;
3549 transform: translate(-50%, -50%);
3550 width: min(900px, calc(100vw - 32px));
3551 height: min(70vh, 560px);
3552 background: #ffffff;
3553 border-radius: 14px;
3554 border: 1px solid rgba(226, 232, 240, 0.95);
3555 box-shadow: 0 18px 60px rgba(0,0,0,0.24);
3556 display:flex;
3557 flex-direction: column;
3558 overflow:hidden;
3559 }
3560 #chat-expand-modal .chat-expand-head{
3561 display:flex;
3562 align-items:center;
3563 justify-content: space-between;
3564 gap: 12px;
3565 padding: 12px 14px;
3566 border-bottom: 1px solid #eef2f7;
3567 background: #f8fafc;
3568 }
3569 #chat-expand-modal .chat-expand-title{
3570 margin: 0;
3571 font-size: 14px;
3572 font-weight: 800;
3573 color:#0f172a;
3574 }
3575 #chat-expand-modal .chat-expand-close{
3576 appearance:none;
3577 border:none;
3578 background:transparent;
3579 font-size: 22px;
3580 line-height: 1;
3581 cursor:pointer;
3582 color:#334155;
3583 width:32px;
3584 height:32px;
3585 display:flex;
3586 align-items:center;
3587 justify-content:center;
3588 border-radius: 10px;
3589 }
3590 #chat-expand-modal .chat-expand-close:hover{ background:#e2e8f0; }
3591 #chat-expand-modal .chat-expand-textarea{
3592 flex: 1 1 auto;
3593 width: 100%;
3594 box-sizing: border-box;
3595 border: none;
3596 outline: none;
3597 padding: 14px;
3598 resize: none;
3599 font-size: 14px;
3600 line-height: 1.5;
3601 }
3602 #chat-expand-modal .chat-expand-foot{
3603 display:flex;
3604 align-items:center;
3605 justify-content: space-between;
3606 gap: 10px;
3607 padding: 10px 14px;
3608 border-top: 1px solid #eef2f7;
3609 background: #ffffff;
3610 }
3611 #chat-expand-modal .chat-expand-hint{
3612 font-size: 12px;
3613 color:#64748b;
3614 }
3615 #chat-expand-modal .chat-expand-done{
3616 appearance:none;
3617 border: 1px solid #0ea5e9;
3618 background: #0ea5e9;
3619 color: #fff;
3620 border-radius: 10px;
3621 padding: 9px 12px;
3622 font-size: 13px;
3623 font-weight: 800;
3624 cursor: pointer;
3625 }
3626 #chat-expand-modal .chat-expand-done:hover{ filter: brightness(0.98); }
3627
3628 /* Indicateur de saisie de l'IA */
3629 #chat-typing-indicator {
3630 display: inline-flex;
3631 align-items: center;
3632 gap: 4px;
3633 padding: 6px 10px;
3634 border-radius: 12px;
3635 background-color: #f1f3f5;
3636 color: #555;
3637 font-size: 12px;
3638 max-width: 80%;
3639 margin-top: 4px;
3640 }
3641
3642 #chat-typing-indicator .chat-gen-status {
3643 margin-left: 8px;
3644 opacity: 0.75;
3645 transition: opacity 220ms ease;
3646 white-space: nowrap;
3647 overflow: hidden;
3648 text-overflow: ellipsis;
3649 max-width: 260px;
3650 }
3651
3652 #chat-typing-indicator .chat-gen-status.is-fading {
3653 opacity: 0;
3654 }
3655
3656 #chat-typing-indicator .typing-dot {
3657 width: 6px;
3658 height: 6px;
3659 border-radius: 50%;
3660 background-color: #888;
3661 display: inline-block;
3662 animation: ai-typing-bounce 1s infinite ease-in-out;
3663 }
3664
3665 #chat-typing-indicator .typing-dot:nth-child(2) {
3666 animation-delay: 0.15s;
3667 }
3668
3669 #chat-typing-indicator .typing-dot:nth-child(3) {
3670 animation-delay: 0.3s;
3671 }
3672
3673 @keyframes ai-typing-bounce {
3674 0%, 60%, 100% {
3675 transform: translateY(0);
3676 opacity: 0.4;
3677 }
3678 30% {
3679 transform: translateY(-4px);
3680 opacity: 1;
3681 }
3682 }
3683
3684 /* Confirm replace content modal (before generation) */
3685 #aibui-replace-content-modal[hidden] { display: none !important; }
3686 #aibui-replace-content-modal {
3687 position: fixed;
3688 inset: 0;
3689 z-index: 999999;
3690 }
3691 #aibui-replace-content-modal .aibui-modal-backdrop{
3692 position:absolute; inset:0;
3693 background: rgba(15, 23, 42, 0.52);
3694 backdrop-filter: blur(2px);
3695 }
3696 #aibui-replace-content-modal .aibui-modal-panel{
3697 position: relative;
3698 width: min(560px, calc(100vw - 32px));
3699 margin: 10vh auto 0;
3700 background: #fff;
3701 border-radius: 14px;
3702 box-shadow: 0 18px 60px rgba(0,0,0,0.22);
3703 padding: 16px 16px 14px;
3704 border: 1px solid rgba(226,232,240,0.9);
3705 }
3706 #aibui-replace-content-modal .aibui-modal-title{
3707 margin: 0 0 8px 0;
3708 font-size: 16px;
3709 line-height: 1.25;
3710 font-weight: 800;
3711 color: #0f172a;
3712 }
3713 #aibui-replace-content-modal .aibui-modal-text{
3714 margin: 0 0 14px 0;
3715 font-size: 13px;
3716 line-height: 1.6;
3717 color: #334155;
3718 }
3719 #aibui-replace-content-modal .aibui-modal-actions{
3720 display:flex;
3721 gap: 10px;
3722 align-items: center;
3723 justify-content: flex-start;
3724 flex-wrap: wrap;
3725 }
3726 #aibui-replace-content-modal .aibui-btn{
3727 appearance:none;
3728 border: 1px solid transparent;
3729 border-radius: 10px;
3730 padding: 8px 12px;
3731 font-size: 13px;
3732 font-weight: 700;
3733 cursor: pointer;
3734 }
3735 #aibui-replace-content-modal .aibui-btn--primary{
3736 background:#0ea5e9;
3737 color:#fff;
3738 }
3739 #aibui-replace-content-modal .aibui-btn--primary:hover{ filter: brightness(0.98); }
3740 #aibui-replace-content-modal .aibui-btn--secondary{
3741 background:#10b981;
3742 color:#fff;
3743 }
3744 #aibui-replace-content-modal .aibui-btn--secondary:hover{ filter: brightness(0.98); }
3745 #aibui-replace-content-modal .aibui-btn--ghost{
3746 background: transparent;
3747 border-color: #e2e8f0;
3748 color: #0f172a;
3749 }
3750 #aibui-replace-content-modal .aibui-btn--ghost:hover{ background: #f8fafc; }
3751
3752 /* AI Block modifier mini panel */
3753 #aibui-block-modifier[hidden]{ display:none !important; }
3754 #aibui-block-modifier{
3755 position: fixed;
3756 top: 0;
3757 left: 0;
3758 width:320px;
3759 z-index: 9001;
3760 background:#ffffff;
3761 border: 1px solid rgba(226,232,240,0.95);
3762 border-radius: 14px;
3763 box-shadow: 0 18px 60px rgba(0,0,0,0.14);
3764 overflow:hidden;
3765 will-change: transform;
3766 }
3767 #aibui-block-modifier .aibui-bm-head{
3768 display:flex;
3769 align-items:center;
3770 justify-content: space-between;
3771 gap:10px;
3772 padding:10px 12px;
3773 border-bottom:1px solid #eef2f7;
3774 background: #f8fafc;
3775 }
3776 #aibui-block-modifier .aibui-bm-title{
3777 font-size: 13px;
3778 font-weight: 800;
3779 color:#0f172a;
3780 }
3781 #aibui-block-modifier .aibui-bm-close{
3782 appearance:none;
3783 border:none;
3784 background:transparent;
3785 font-size: 18px;
3786 line-height: 1;
3787 cursor:pointer;
3788 color:#334155;
3789 width:28px;
3790 height:28px;
3791 display:flex;
3792 align-items:center;
3793 justify-content:center;
3794 border-radius: 8px;
3795 }
3796 #aibui-block-modifier .aibui-bm-close:hover{ background:#e2e8f0; }
3797 #aibui-block-modifier .aibui-bm-body{ padding: 12px; }
3798 #aibui-block-modifier .aibui-bm-label{
3799 display:block;
3800 font-size: 12px;
3801 font-weight: 700;
3802 color:#334155;
3803 margin: 0 0 6px;
3804 }
3805 #aibui-block-modifier .aibui-bm-prompt{
3806 width:100%;
3807 box-sizing:border-box;
3808 resize: vertical;
3809 border: 1px solid #e2e8f0;
3810 border-radius: 10px;
3811 padding: 10px 10px;
3812 font-size: 13px;
3813 line-height: 1.4;
3814 }
3815 #aibui-block-modifier .aibui-bm-meta{
3816 margin-top: 6px;
3817 display:flex;
3818 justify-content:flex-end;
3819 font-size: 11px;
3820 color:#64748b;
3821 user-select:none;
3822 }
3823 #aibui-block-modifier .aibui-bm-meta.aibui-bm-meta--max{
3824 color:#b45309;
3825 font-weight: 700;
3826 }
3827 #aibui-block-modifier .aibui-bm-actions{
3828 margin-top: 10px;
3829 display:flex;
3830 justify-content:flex-end;
3831 }
3832 #aibui-block-modifier .aibui-bm-btn{
3833 appearance:none;
3834 border:1px solid transparent;
3835 border-radius: 10px;
3836 padding: 9px 12px;
3837 font-size: 13px;
3838 font-weight: 800;
3839 cursor:pointer;
3840 }
3841 #aibui-block-modifier .aibui-bm-btn--primary{
3842 background:#0ea5e9;
3843 color:#fff;
3844 }
3845 #aibui-block-modifier .aibui-bm-btn--primary:hover{ filter: brightness(0.98); }
3846 #aibui-block-modifier .aibui-bm-status{
3847 margin-top: 8px;
3848 font-size: 12px;
3849 color:#64748b;
3850 min-height: 16px;
3851 }
3852 #aibui-block-modifier .aibui-bm-status-dots{
3853 display:inline-flex;
3854 align-items:center;
3855 gap:4px;
3856 margin-right: 8px;
3857 vertical-align: middle;
3858 }
3859 #aibui-block-modifier .aibui-bm-status-dots[hidden]{
3860 display:none !important;
3861 }
3862 #aibui-block-modifier .aibui-bm-status-dots .typing-dot{
3863 width: 6px;
3864 height: 6px;
3865 border-radius: 50%;
3866 background-color: #64748b;
3867 display: inline-block;
3868 animation: ai-typing-bounce 1s infinite ease-in-out;
3869 }
3870 #aibui-block-modifier .aibui-bm-status-dots .typing-dot:nth-child(2){ animation-delay: 0.15s; }
3871 #aibui-block-modifier .aibui-bm-status-dots .typing-dot:nth-child(3){ animation-delay: 0.3s; }
3872
3873 /* Small "Modify with AI" button (shown on group selection) */
3874 #aibui-block-modifier-btn[hidden]{ display:none !important; }
3875 #aibui-block-modifier-btn.aibui-bm-fab{
3876 position: fixed;
3877 top: 0;
3878 left: 0;
3879 z-index: 9000;
3880 height: 32px;
3881 padding: 0 10px;
3882 border-radius: 999px;
3883 background: rgba(15, 23, 42, 0.92);
3884 color: #fff;
3885 border: 1px solid rgba(148, 163, 184, 0.35);
3886 font-size: 12px;
3887 font-weight: 800;
3888 cursor: pointer;
3889 box-shadow: 0 10px 30px rgba(0,0,0,0.14);
3890 white-space: nowrap;
3891 will-change: transform;
3892 }
3893 #aibui-block-modifier-btn.aibui-bm-fab:hover{
3894 filter: brightness(1.03);
3895 }
3896
3897 /* Header du chat: layout propre et actions cohérentes */
3898 #chat-header {
3899 position: relative;
3900 display: flex;
3901 justify-content: space-between;
3902 align-items: flex-start;
3903 gap: 16px;
3904 text-align: left;
3905 padding-right: 42px;
3906 }
3907
3908 #chat-header .chat-close{
3909 position: absolute;
3910 top: 5px;
3911 right: 5px;
3912 appearance: none;
3913 border: 1px solid rgba(255, 255, 255, 0);
3914 background: rgba(255, 255, 255, 0);
3915 color: #fff;
3916 width: 20px;
3917 height: 20px;
3918 min-width: 20px;
3919 min-height: 20px;
3920 max-width: 20px;
3921 max-height: 20px;
3922 aspect-ratio: 1 / 1;
3923 box-sizing: border-box;
3924 border-radius: 10px;
3925 cursor: pointer;
3926 display: inline-flex;
3927 align-items: center;
3928 justify-content: center;
3929 font-size: 18px;
3930 line-height: 1;
3931 padding: 0;
3932 flex-shrink: 0;
3933 z-index: 1;
3934 }
3935 #chat-header .chat-close:hover{
3936 background: rgba(255, 255, 255, 0.1);
3937 border-color: rgba(255, 255, 255, 0);
3938 }
3939
3940 #chat-header-left {
3941 flex: 1;
3942 min-width: 0;
3943 }
3944
3945 #chat-header-left h3 {
3946 margin: 0 0 6px 0;
3947 font-size: 17px;
3948 line-height: 1.25;
3949 }
3950
3951 #chat-header-left p {
3952 margin: 0;
3953 font-size: 12px;
3954 line-height: 1.45;
3955 opacity: 0.92;
3956 }
3957
3958 #chat-header-actions {
3959 display: flex;
3960 flex-direction: column;
3961 align-items: flex-end;
3962 gap: 8px;
3963 flex-shrink: 0;
3964 }
3965
3966 #chat-header-tools {
3967 display: flex;
3968 align-items: center;
3969 justify-content: space-between;
3970 gap: 8px;
3971 width: 116px;
3972 box-sizing: border-box;
3973 }
3974
3975 /* Bouton Header/Footer */
3976 #headers-footers-button {
3977 background: rgba(255, 255, 255, 0.95);
3978 border: 1px solid rgba(191, 219, 254, 0.9);
3979 color: #1e40af;
3980 padding: 7px 11px;
3981 border-radius: 8px;
3982 cursor: pointer;
3983 font-size: 11px;
3984 font-weight: 600;
3985 letter-spacing: 0.2px;
3986 white-space: nowrap;
3987 transition: all 0.2s ease;
3988 width: 116px;
3989 box-sizing: border-box;
3990 text-align: center;
3991 }
3992
3993 #headers-footers-button:hover {
3994 background: #ffffff !important;
3995 border-color: #93c5fd !important;
3996 transform: translateY(-1px);
3997 box-shadow: 0 4px 10px rgba(30, 64, 175, 0.2);
3998 }
3999
4000 /* Boutons outils du header */
4001 #css-edit-button {
4002 width: 54px;
4003 height: 34px;
4004 background: rgba(255, 255, 255, 0.2);
4005 color: white;
4006 border: 1px solid rgba(255, 255, 255, 0.35);
4007 border-radius: 8px;
4008 cursor: pointer;
4009 display: flex;
4010 align-items: center;
4011 justify-content: center;
4012 flex-shrink: 0;
4013 transition: all 0.3s ease;
4014 padding: 0;
4015 box-sizing: border-box;
4016 }
4017
4018 #css-edit-button:hover {
4019 background: rgba(255, 255, 255, 0.3);
4020 border-color: rgba(255, 255, 255, 0.6);
4021 transform: translateY(-1px);
4022 }
4023
4024 #css-edit-button svg {
4025 width: 16px;
4026 height: 16px;
4027 }
4028
4029 #css-edit-button img {
4030 width: 16px;
4031 height: 16px;
4032 display: block;
4033 margin: 0 auto;
4034 }
4035
4036 /* Modale CSS */
4037 #css-modal {
4038 position: fixed;
4039 top: 0;
4040 left: 0;
4041 width: 100%;
4042 height: 100%;
4043 background: rgba(0, 0, 0, 0.7);
4044 display: flex;
4045 align-items: center;
4046 justify-content: center;
4047 z-index: 10000;
4048 }
4049
4050 #css-modal-content {
4051 background: white;
4052 border-radius: 8px;
4053 width: 80%;
4054 max-width: 800px;
4055 max-height: 80%;
4056 display: flex;
4057 flex-direction: column;
4058 box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
4059 }
4060
4061 #css-modal-header {
4062 padding: 20px;
4063 border-bottom: 1px solid #ddd;
4064 display: flex;
4065 justify-content: space-between;
4066 align-items: center;
4067 }
4068
4069 #css-modal-header h3 {
4070 margin: 0;
4071 color: #333;
4072 }
4073
4074 #css-modal-close {
4075 background: none;
4076 border: none;
4077 font-size: 24px;
4078 cursor: pointer;
4079 color: #666;
4080 padding: 0;
4081 width: 30px;
4082 height: 30px;
4083 display: flex;
4084 align-items: center;
4085 justify-content: center;
4086 }
4087
4088 #css-modal-close:hover {
4089 color: #000;
4090 }
4091
4092 #css-modal-body,
4093 #js-modal-body {
4094 flex: 1;
4095 padding: 20px;
4096 overflow: hidden;
4097 }
4098
4099 /* Styles pour CodeMirror */
4100 #css-modal-body .CodeMirror,
4101 #js-modal-body .CodeMirror {
4102 height: 400px;
4103 border-radius: 4px;
4104 font-size: 14px;
4105 line-height: 1.5;
4106 }
4107
4108 /* Assurer un minimum de 10 lignes visibles */
4109 #css-modal-body .CodeMirror-scroll,
4110 #js-modal-body .CodeMirror-scroll {
4111 min-height: 210px; /* ~10 lignes à 21px chacune */
4112 }
4113
4114 #css-modal-body .CodeMirror-sizer,
4115 #js-modal-body .CodeMirror-sizer {
4116 min-height: 210px !important;
4117 }
4118
4119 #css-modal-body .CodeMirror-lines,
4120 #js-modal-body .CodeMirror-lines {
4121 min-height: 210px;
4122 padding-bottom: 50px; /* Espace pour scroller au-delà du contenu */
4123 }
4124
4125 #css-editor, #js-editor {
4126 display: none; /* Caché car remplacé par CodeMirror */
4127 }
4128
4129 #css-modal-footer {
4130 padding: 20px;
4131 border-top: 1px solid #ddd;
4132 display: flex;
4133 gap: 10px;
4134 justify-content: flex-end;
4135 }
4136
4137 #css-save, #css-cancel {
4138 padding: 10px 20px;
4139 border: none;
4140 border-radius: 4px;
4141 cursor: pointer;
4142 font-size: 14px;
4143 font-weight: 500;
4144 }
4145
4146 #css-save {
4147 background: #007cba;
4148 color: white;
4149 }
4150
4151 #css-save:hover {
4152 background: #005a87;
4153 }
4154
4155 #css-cancel {
4156 background: #f0f0f0;
4157 color: #333;
4158 }
4159
4160 #css-cancel:hover {
4161 background: #e0e0e0;
4162 }
4163
4164 /* Bouton JS intégré dans le header */
4165 #js-edit-button {
4166 width: 54px;
4167 height: 34px;
4168 background: rgba(255, 255, 255, 0.2);
4169 color: white;
4170 border: 1px solid rgba(255, 255, 255, 0.35);
4171 border-radius: 8px;
4172 cursor: pointer;
4173 display: flex;
4174 align-items: center;
4175 justify-content: center;
4176 flex-shrink: 0;
4177 transition: all 0.3s ease;
4178 padding: 0;
4179 font-weight: 600;
4180 box-sizing: border-box;
4181 }
4182
4183 #js-edit-button:hover {
4184 background: rgba(255, 255, 255, 0.3);
4185 border-color: rgba(255, 255, 255, 0.6);
4186 transform: translateY(-1px);
4187 }
4188
4189 #js-edit-button span {
4190 font-size: 12px;
4191 font-weight: 700;
4192 letter-spacing: 0.3px;
4193 }
4194
4195 @media (max-width: 560px) {
4196 #chat-header {
4197 flex-direction: column;
4198 align-items: stretch;
4199 gap: 12px;
4200 }
4201
4202 #chat-header-actions {
4203 align-items: stretch;
4204 }
4205
4206 #chat-header-tools {
4207 justify-content: flex-end;
4208 }
4209
4210 #headers-footers-button {
4211 text-align: center;
4212 }
4213 }
4214
4215 /* Modale JS */
4216 #js-modal {
4217 position: fixed;
4218 top: 0;
4219 left: 0;
4220 width: 100%;
4221 height: 100%;
4222 background: rgba(0, 0, 0, 0.7);
4223 display: flex;
4224 align-items: center;
4225 justify-content: center;
4226 z-index: 10000;
4227 }
4228
4229 #js-modal-content {
4230 background: white;
4231 border-radius: 8px;
4232 width: 80%;
4233 max-width: 800px;
4234 max-height: 80%;
4235 display: flex;
4236 flex-direction: column;
4237 box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
4238 }
4239
4240 #js-modal-header {
4241 padding: 20px;
4242 border-bottom: 1px solid #ddd;
4243 display: flex;
4244 justify-content: space-between;
4245 align-items: center;
4246 }
4247
4248 #js-modal-header h3 {
4249 margin: 0;
4250 color: #333;
4251 }
4252
4253 #js-modal-close {
4254 background: none;
4255 border: none;
4256 font-size: 24px;
4257 cursor: pointer;
4258 color: #666;
4259 padding: 0;
4260 width: 30px;
4261 height: 30px;
4262 display: flex;
4263 align-items: center;
4264 justify-content: center;
4265 }
4266
4267 #js-modal-close:hover {
4268 color: #000;
4269 }
4270
4271 #js-modal-footer {
4272 padding: 20px;
4273 border-top: 1px solid #ddd;
4274 display: flex;
4275 gap: 10px;
4276 justify-content: flex-end;
4277 }
4278
4279 #js-save, #js-cancel {
4280 padding: 10px 20px;
4281 border: none;
4282 border-radius: 4px;
4283 cursor: pointer;
4284 font-size: 14px;
4285 font-weight: 500;
4286 }
4287
4288 #js-save {
4289 background: #007cba;
4290 color: white;
4291 }
4292
4293 #js-save:hover {
4294 background: #005a87;
4295 }
4296
4297 #js-cancel {
4298 background: #f0f0f0;
4299 color: #333;
4300 }
4301
4302 #js-cancel:hover {
4303 background: #e0e0e0;
4304 }
4305
4306 /* Highlight for CSS class search */
4307 .css-class-highlight {
4308 background: rgba(253, 237, 65, 0.17) !important;
4309 border-left: 3px solidrgba(251, 193, 45, 0.5) !important;
4310 }
4311
4312 /* Chat visual style picker (above input; panel background is light) */
4313 .chat-style-picker {
4314 padding: 10px 15px 8px;
4315 border-bottom: 1px solid #e8e8e8;
4316 background: #fff;
4317 flex-shrink: 0;
4318 }
4319
4320 .chat-style-picker-head {
4321 display: flex;
4322 align-items: center;
4323 justify-content: space-between;
4324 gap: 8px;
4325 flex-wrap: wrap;
4326 }
4327
4328 .chat-style-trigger {
4329 flex: 1;
4330 min-width: 0;
4331 text-align: left;
4332 display: flex;
4333 align-items: center;
4334 justify-content: space-between;
4335 gap: 10px;
4336 background: #f1f5f9;
4337 border: 1px solid #e2e8f0;
4338 border-radius: 10px;
4339 padding: 10px 12px;
4340 color: #64748b;
4341 font-size: 13px;
4342 font-weight: 500;
4343 cursor: pointer;
4344 transition: background 0.2s, border-color 0.2s;
4345 }
4346
4347 .chat-style-trigger-chevron {
4348 flex: 0 0 auto;
4349 font-size: 24px;
4350 line-height: 1;
4351 opacity: 0.9;
4352 }
4353
4354 .chat-style-trigger:hover {
4355 background: #e2e8f0;
4356 border-color: #cbd5e1;
4357 color: #475569;
4358 }
4359
4360 .chat-style-swipe-hint {
4361 font-size: 11px;
4362 color: #94a3b8;
4363 white-space: nowrap;
4364 flex-shrink: 0;
4365 }
4366
4367 .chat-style-remove {
4368 flex-shrink: 0;
4369 font-size: 11px;
4370 font-weight: 600;
4371 padding: 6px 10px;
4372 border-radius: 8px;
4373 border: 1px solid #e2e8f0;
4374 background: #fff;
4375 color: #64748b;
4376 cursor: pointer;
4377 }
4378
4379 .chat-style-remove:hover {
4380 background: #fef2f2;
4381 border-color: #fecaca;
4382 color: #b91c1c;
4383 }
4384
4385 .chat-style-slider-outer {
4386 margin-top: 10px;
4387 overflow: hidden;
4388 }
4389
4390 .chat-style-slider {
4391 display: flex;
4392 gap: 10px;
4393 overflow-x: auto;
4394 padding: 4px 2px 10px;
4395 scroll-snap-type: x mandatory;
4396 -webkit-overflow-scrolling: touch;
4397 }
4398
4399 .chat-style-slider::-webkit-scrollbar {
4400 height: 6px;
4401 }
4402
4403 .chat-style-slider::-webkit-scrollbar-thumb {
4404 background: #cbd5e1;
4405 border-radius: 3px;
4406 }
4407
4408 .chat-style-card {
4409 flex: 0 0 auto;
4410 scroll-snap-align: start;
4411 width: 100px;
4412 padding: 0;
4413 border: none;
4414 background: transparent;
4415 cursor: pointer;
4416 text-align: center;
4417 outline: none;
4418 }
4419
4420 .chat-style-card:focus-visible {
4421 box-shadow: 0 0 0 2px #fff, 0 0 0 4px #2563eb;
4422 border-radius: 14px;
4423 }
4424
4425 .chat-style-card-inner {
4426 position: relative;
4427 display: block;
4428 width: 100px;
4429 height: 100px;
4430 box-sizing: border-box;
4431 border-radius: 12px;
4432 overflow: hidden;
4433 border: 2px solid #e2e8f0;
4434 transition: border-color 0.2s, box-shadow 0.2s;
4435 background: #f1f5f9;
4436 }
4437
4438 .chat-style-preview-btn {
4439 position: absolute;
4440 top: 5px;
4441 right: 5px;
4442 width: 28px;
4443 height: 28px;
4444 border-radius: 8px;
4445 border: 1px solid rgba(15, 23, 42, 0.1);
4446 background: rgba(255, 255, 255, 0.94);
4447 box-shadow: 0 1px 4px rgba(15, 23, 42, 0.12);
4448 display: flex;
4449 align-items: center;
4450 justify-content: center;
4451 color: #475569;
4452 cursor: pointer;
4453 padding: 0;
4454 z-index: 3;
4455 transition: background 0.15s, color 0.15s, transform 0.15s;
4456 }
4457
4458 .chat-style-preview-btn:hover {
4459 background: #fff;
4460 color: #1d4ed8;
4461 transform: scale(1.05);
4462 }
4463
4464 .chat-style-preview-btn:focus-visible {
4465 outline: 2px solid #2563eb;
4466 outline-offset: 1px;
4467 }
4468
4469 .chat-style-preview-icon {
4470 display: block;
4471 flex-shrink: 0;
4472 }
4473
4474 .chat-style-card img {
4475 width: 100%;
4476 height: 100%;
4477 object-fit: cover;
4478 display: block;
4479 }
4480
4481 .chat-style-card-fallback {
4482 position: absolute;
4483 inset: 0;
4484 width: 100%;
4485 height: 100%;
4486 align-items: center;
4487 justify-content: center;
4488 }
4489
4490 .chat-style-fallback--neobrutalism {
4491 background: linear-gradient(135deg, #ffd700 40%, #ff6b9d, #4361ee);
4492 border: 3px solid #000;
4493 box-sizing: border-box;
4494 }
4495
4496 .chat-style-fallback--premium-dark {
4497 background: linear-gradient(160deg, #0a0a0b, #141416 60%, #8b5cf6, #ec4899);
4498 }
4499
4500 .chat-style-fallback--modern-minimalist {
4501 background: #ffffff;
4502 border: 1px solid #e5e5e5;
4503 box-sizing: border-box;
4504 }
4505
4506 .chat-style-fallback--editorial-typography {
4507 background: linear-gradient(180deg, #fbfaf7, #e8e4dc);
4508 }
4509
4510 .chat-style-fallback--organic-nature {
4511 background: linear-gradient(145deg, #87a878, #f5efe0 50%, #c97d5d);
4512 }
4513
4514 .chat-style-fallback--retrofuturism {
4515 background: linear-gradient(180deg, #0b0b2e 30%, #1a0b3e 70%, #ff006e);
4516 }
4517
4518 .chat-style-fallback--saas-tech {
4519 background: linear-gradient(135deg, #ffffff, #eef2ff 40%, #4f46e5 120%);
4520 }
4521
4522 .chat-style-fallback--luxury {
4523 background: linear-gradient(160deg, #0e0e0e, #1a1814 50%, #c5a572);
4524 }
4525
4526 .chat-style-fallback--bento-grid {
4527 background: #fafafa;
4528 background-image: linear-gradient(#e5e5e5 1px, transparent 1px), linear-gradient(90deg, #e5e5e5 1px, transparent 1px);
4529 background-size: 18px 18px;
4530 }
4531
4532 .chat-style-fallback--y2k-revival {
4533 background: linear-gradient(145deg, #a8dadc, #ffb6d9, #c8ff6b, #c8a4d4);
4534 }
4535
4536 .chat-style-fallback--swiss-typographic {
4537 background: linear-gradient(90deg, #ffffff 70%, #e63946 70%, #e63946 72%, #ffffff 72%);
4538 }
4539
4540 .chat-style-fallback--claymorphism {
4541 background: radial-gradient(circle at 30% 25%, #fff3b0, #e0d7ff 40%, #c7f0db 75%, #ffd4c4);
4542 }
4543
4544 .chat-style-card.is-selected .chat-style-card-inner {
4545 border-color: #38bdf8;
4546 box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.35);
4547 }
4548
4549 .chat-style-card-label {
4550 display: block;
4551 margin-top: 6px;
4552 font-size: 10px;
4553 line-height: 1.25;
4554 color: #64748b;
4555 font-weight: 500;
4556 max-width: 100px;
4557 margin-left: auto;
4558 margin-right: auto;
4559 }
4560
4561 /* Style preview modal (sibling of #chat-box, fixed to viewport) */
4562 .chat-style-detail-modal[hidden] {
4563 display: none !important;
4564 }
4565
4566 .chat-style-detail-modal:not([hidden]) {
4567 display: flex;
4568 align-items: center;
4569 justify-content: center;
4570 }
4571
4572 .chat-style-detail-modal {
4573 position: fixed;
4574 inset: 0;
4575 z-index: 1000000;
4576 padding: 16px;
4577 box-sizing: border-box;
4578 }
4579
4580 .chat-style-detail-backdrop {
4581 position: absolute;
4582 inset: 0;
4583 background: rgba(15, 23, 42, 0.55);
4584 border: none;
4585 cursor: pointer;
4586 padding: 0;
4587 margin: 0;
4588 }
4589
4590 .chat-style-detail-panel {
4591 position: relative;
4592 z-index: 1;
4593 background: #fff;
4594 border-radius: 16px;
4595 max-width: min(540px, 100%);
4596 width: 100%;
4597 max-height: min(90vh, 880px);
4598 overflow: auto;
4599 padding: 20px 20px 22px;
4600 box-shadow: 0 25px 50px rgba(0, 0, 0, 0.28);
4601 text-align: left;
4602 }
4603
4604 .chat-style-detail-close {
4605 position: absolute;
4606 top: 10px;
4607 right: 12px;
4608 width: 36px;
4609 height: 36px;
4610 border: none;
4611 background: #f1f5f9;
4612 color: #64748b;
4613 font-size: 22px;
4614 line-height: 1;
4615 border-radius: 10px;
4616 cursor: pointer;
4617 display: flex;
4618 align-items: center;
4619 justify-content: center;
4620 padding: 0;
4621 z-index: 2;
4622 }
4623
4624 .chat-style-detail-close:hover {
4625 background: #e2e8f0;
4626 color: #0f172a;
4627 }
4628
4629 .chat-style-detail-title {
4630 margin: 0 44px 14px 0;
4631 font-size: 18px;
4632 font-weight: 600;
4633 color: #0f172a;
4634 line-height: 1.3;
4635 }
4636
4637 .chat-style-detail-image-wrap {
4638 border-radius: 12px;
4639 overflow: hidden;
4640 background: #f8fafc;
4641 margin-bottom: 14px;
4642 }
4643
4644 .chat-style-detail-image-wrap img {
4645 display: block;
4646 width: 100%;
4647 max-height: min(42vh, 360px);
4648 height: auto;
4649 object-fit: contain;
4650 margin: 0 auto;
4651 }
4652
4653 .chat-style-detail-intro {
4654 margin: 0 0 8px 0;
4655 font-size: 12px;
4656 font-weight: 600;
4657 color: #64748b;
4658 text-transform: uppercase;
4659 letter-spacing: 0.04em;
4660 }
4661
4662 .chat-style-detail-prompt {
4663 margin: 0;
4664 font-size: 13px;
4665 line-height: 1.55;
4666 color: #334155;
4667 white-space: pre-wrap;
4668 word-break: break-word;
4669 }
4670
4671
4672 `;
4673
4674 document.head.appendChild(cssStyles);
4675