PluginProbe
Page Builder: Pagelayer – Drag and Drop website builder / 2.2.2
Page Builder: Pagelayer – Drag and Drop website builder v2.2.2
2.2.2 2.2.1 2.2.0 2.1.9 2.1.8 2.1.7 2.1.6 2.1.5 2.1.4 2.1.3 trunk 0.9.0 0.9.1 0.9.2 0.9.3 0.9.4 0.9.5 0.9.6 0.9.7 0.9.8 0.9.9 1.0.0 1.0.2 1.0.3 1.0.4 All 130 releases
pagelayer / js / pagelayer-ai.js

pagelayer-ai.js in Page Builder: Pagelayer – Drag and Drop website builder 2.2.2, at js/pagelayer-ai.js

2,243 lines 93.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Pagelayer AI Layout Generator Frontend Controller
3 * Real-time Live Building & Direct Canvas Widget Assembly
4 * (c) Pagelayer Team
5 */
6
7 (function ($) {
8 'use strict';
9
10 var PagelayerAI = {
11 panel: null,
12 floatingBtn: null,
13 topbarBtn: null,
14 isGenerating: false,
15 settings: {
16 rest_url: (typeof pagelayer_ai_settings !== 'undefined' && pagelayer_ai_settings.rest_url) ? pagelayer_ai_settings.rest_url : '/wp-json/pagelayer/v1/ai/',
17 nonce: (typeof pagelayer_ai_settings !== 'undefined' && pagelayer_ai_settings.nonce) ? pagelayer_ai_settings.nonce : '',
18 ajax_url: (typeof pagelayer_ai_settings !== 'undefined' && pagelayer_ai_settings.ajax_url) ? pagelayer_ai_settings.ajax_url : (typeof pagelayer_ajax_url !== 'undefined' ? pagelayer_ajax_url : '/wp-admin/admin-ajax.php'),
19 ajax_nonce: (typeof pagelayer_ai_settings !== 'undefined' && pagelayer_ai_settings.ajax_nonce) ? pagelayer_ai_settings.ajax_nonce : (typeof pagelayer_ajax_nonce !== 'undefined' ? pagelayer_ajax_nonce : ''),
20 default_provider: 'gemini',
21 default_model: 'gemini-3.7-flash',
22 models: (typeof pagelayer_ai_settings !== 'undefined' && pagelayer_ai_settings.providers) ? pagelayer_ai_settings.providers : {}
23 },
24 models: {
25 gemini: 'gemini-3.7-flash',
26 deepseek: 'deepseek-chat',
27 openrouter: 'deepseek/deepseek-chat',
28 openai: 'gpt-4o-mini',
29 anthropic: 'claude-3-7-sonnet-20250219',
30 custom: 'llama3.3'
31 },
32 activeKeys: {
33 gemini: false,
34 deepseek: false,
35 openai: false,
36 anthropic: false,
37 openrouter: false,
38 custom: false
39 },
40 chatHistory: [],
41 _lastAiHighlight: null,
42
43 /**
44 * Initialize AI Assistant
45 */
46 init: function () {
47 var self = this;
48 var targetDoc = (window.parent && window.parent.document) ? window.parent.document : document;
49
50 // Build Panel UI
51 self.buildUI(targetDoc);
52
53 if (typeof pagelayer_ai_settings !== 'undefined' && pagelayer_ai_settings.user_settings) {
54 var boot = pagelayer_ai_settings.user_settings;
55 self.applyLoadedSettings({
56 has_deepseek_key: !!boot.deepseek_key,
57 has_gemini_key: !!boot.gemini_key,
58 has_openrouter_key: !!boot.openrouter_key,
59 has_openai_key: !!boot.openai_key,
60 has_anthropic_key: !!boot.anthropic_key,
61 has_custom_key: !!boot.custom_key,
62 deepseek_model: boot.deepseek_model || '',
63 gemini_model: boot.gemini_model || '',
64 openrouter_model: boot.openrouter_model || '',
65 openai_model: boot.openai_model || '',
66 anthropic_model: boot.anthropic_model || '',
67 custom_endpoint: boot.custom_endpoint || '',
68 custom_model: boot.custom_model || '',
69 default_provider: boot.default_provider || '',
70 default_model: boot.default_model || ''
71 });
72 }
73
74 // Load user AI settings
75 self.loadSettings();
76
77 // Bind event listeners
78 self.bindEvents(targetDoc);
79
80 // Inject Topbar Button
81 self.injectTopbarButton(targetDoc);
82
83 // Welcome message
84 self.addBotMessage(
85 "👋 Hello! I am your <strong>Pagelayer AI Architect</strong>. Describe any section, page hero, feature grid, or layout in natural language and watch it <strong>build live on your canvas step-by-step</strong> with fully editable widgets.<br><br>🎨 <em>Theme Mode: Set to keep your existing site colors & fonts by default. You can switch to generating new colors anytime in the Theme bar above.</em>",
86 null,
87 true
88 );
89 },
90
91 /**
92 * Build Panel and Floating Trigger DOM
93 */
94 buildUI: function (targetDoc) {
95 var self = this;
96 var $targetBody = $(targetDoc.body);
97 var $targetHead = $(targetDoc.head || targetDoc.getElementsByTagName('head')[0])
98 // Ensure AI CSS and critical topbar styles are loaded in the target document
99 if ($targetHead.length) {
100 if (!$targetHead.find('link[href*="pagelayer-ai.css"], link[href*="pagelayer-ai"]').length) {
101 var cssUrl = (typeof pagelayer_ai_settings !== 'undefined' && pagelayer_ai_settings.css_url) ? pagelayer_ai_settings.css_url : ((typeof pagelayer_url !== 'undefined' ? pagelayer_url : '') + '/css/pagelayer-ai.css');
102 if (cssUrl && cssUrl !== '/css/pagelayer-ai.css') {
103 $targetHead.append('<link rel="stylesheet" id="pagelayer-ai-css" href="' + cssUrl + '" type="text/css" media="all" />');
104 }
105 }
106 if (!$targetHead.find('#pagelayer-ai-critical-css').length) {
107 var styleEl = targetDoc.createElement('style');
108 styleEl.id = 'pagelayer-ai-critical-css';
109 styleEl.textContent = '.pagelayer-header-right .pagelayer-ai-topbar-btn { cursor:pointer!important; display:inline-flex!important; align-items:center!important; justify-content:center!important; width:34px!important; height:34px!important; border-radius:6px!important; color:#c084fc!important; background:rgba(168,85,247,0.15)!important; border:1px solid rgba(168,85,247,0.3)!important; transition:all .2s cubic-bezier(0.4,0,0.2,1)!important; position:relative!important; margin-right:4px!important; box-sizing:border-box!important; font-size:14px!important; line-height:1!important; } .pagelayer-header-right .pagelayer-ai-topbar-btn:hover, .pagelayer-header-right .pagelayer-ai-topbar-btn.active { background:linear-gradient(135deg,#7c3aed,#4f46e5)!important; color:#fff!important; border-color:transparent!important; box-shadow:0 0 12px rgba(124,58,237,0.5)!important; transform:translateY(-1px)!important; } .pagelayer-header-right .pagelayer-ai-topbar-btn i { font-size:14px!important; line-height:1!important; pointer-events:none!important; color:inherit!important; }';
110 $targetHead.append(styleEl);
111 }
112 }
113
114 // Remove any existing instance
115 $targetBody.find('.pagelayer-ai-chat-panel, .pagelayer-ai-floating-pill').remove();
116
117 // Main Floating Panel
118 var panelHtml = '<div class="pagelayer-ai-chat-panel pagelayer-ai-hidden" id="pagelayer-ai-panel">' +
119 '<!-- Header -->' +
120 '<div class="pagelayer-ai-header" id="pagelayer-ai-drag-handle">' +
121 '<div class="pagelayer-ai-header-top">' +
122 '<div class="pagelayer-ai-title-wrap">' +
123 '<div class="pagelayer-ai-logo-icon"><i class="fas fa-magic"></i></div>' +
124 '<div class="pagelayer-ai-title">Build with AI <span class="pagelayer-ai-badge">LIVE</span></div>' +
125 '</div>' +
126 '<div class="pagelayer-ai-header-actions">' +
127 '<button type="button" class="pagelayer-ai-api-btn" id="pagelayer-ai-settings-toggle" title="Configure AI API Keys"><i class="fas fa-key"></i> <span>API Keys</span></button>' +
128 '<button type="button" class="pagelayer-ai-icon-btn" id="pagelayer-ai-minimize-btn" title="Minimize"><i class="fas fa-minus"></i></button>' +
129 '<button type="button" class="pagelayer-ai-icon-btn" id="pagelayer-ai-close-btn" title="Close"><i class="fas fa-times"></i></button>' +
130 '</div>' +
131 '</div>' +
132 '<div class="pagelayer-ai-header-subbar">' +
133 '<div class="pagelayer-ai-provider-pill-wrap">' +
134 '<i class="fas fa-robot pagelayer-ai-subbar-icon"></i>' +
135 '<select class="pagelayer-ai-provider-select" id="pagelayer-ai-provider-picker" title="Active AI Provider">' +
136 '<option value="gemini">Google Gemini</option>' +
137 '<option value="deepseek">DeepSeek</option>' +
138 '<option value="openrouter">OpenRouter</option>' +
139 '<option value="openai">OpenAI</option>' +
140 '<option value="anthropic">Claude</option>' +
141 '<option value="custom">Custom / Local</option>' +
142 '</select>' +
143 '<span class="pagelayer-ai-active-model-tag" id="pagelayer-ai-active-model-tag" title="Click to edit model">gemini-3.7-flash</span>' +
144 '</div>' +
145 '</div>' +
146 '</div>' +
147
148 '<!-- Settings Drawer -->' +
149 '<div class="pagelayer-ai-settings-view" id="pagelayer-ai-settings-view">' +
150 '<div class="pagelayer-ai-settings-header">' +
151 '<div class="pagelayer-ai-settings-header-top">' +
152 '<div class="pagelayer-ai-settings-title">' +
153 '<i class="fas fa-key"></i>' +
154 '<span>AI Providers & Models</span>' +
155 '</div>' +
156 '<button type="button" class="pagelayer-ai-back-btn" id="pagelayer-ai-settings-close" title="Back to Chat">' +
157 '<i class="fas fa-arrow-left"></i> <span>Back</span>' +
158 '</button>' +
159 '</div>' +
160 '<p class="pagelayer-ai-settings-desc">Add your API keys and enter any custom model name you prefer.</p>' +
161 '</div>' +
162
163 '<div class="pagelayer-ai-settings-scroll-body">' +
164 '<div class="pagelayer-ai-settings-cards">' +
165
166 '<!-- Google Gemini Card -->' +
167 '<div class="pagelayer-ai-card" data-provider="gemini">' +
168 '<div class="pagelayer-ai-card-header">' +
169 '<div class="pagelayer-ai-card-title">' +
170 '<span class="pagelayer-ai-provider-dot" id="dot-gemini"></span>' +
171 '<strong>Google Gemini</strong>' +
172 '</div>' +
173 '<a href="https://aistudio.google.com/app/apikey" target="_blank" class="pagelayer-ai-key-link">Get Free Key <i class="fas fa-external-link-alt"></i></a>' +
174 '</div>' +
175 '<div class="pagelayer-ai-field">' +
176 '<label>API Key</label>' +
177 '<div class="pagelayer-ai-input-with-eye">' +
178 '<input type="password" class="pagelayer-ai-input" id="pagelayer-ai-gemini-key" placeholder="Enter Gemini API key (AIzaSy...)" />' +
179 '<button type="button" class="pagelayer-ai-eye-btn" title="Show/Hide Key"><i class="fas fa-eye"></i></button>' +
180 '</div>' +
181 '</div>' +
182 '<div class="pagelayer-ai-field">' +
183 '<label>Model Name <span class="pagelayer-ai-field-hint">e.g. gemini-3.7-flash, gemini-1.5-flash, gemini-1.5-pro</span></label>' +
184 '<input type="text" class="pagelayer-ai-input" id="pagelayer-ai-gemini-model" placeholder="gemini-3.7-flash" />' +
185 '</div>' +
186 '</div>' +
187
188 '<!-- DeepSeek Official Card -->' +
189 '<div class="pagelayer-ai-card" data-provider="deepseek">' +
190 '<div class="pagelayer-ai-card-header">' +
191 '<div class="pagelayer-ai-card-title">' +
192 '<span class="pagelayer-ai-provider-dot" id="dot-deepseek"></span>' +
193 '<strong>DeepSeek (Official)</strong>' +
194 '</div>' +
195 '<a href="https://platform.deepseek.com/api_keys" target="_blank" class="pagelayer-ai-key-link">Get Key <i class="fas fa-external-link-alt"></i></a>' +
196 '</div>' +
197 '<div class="pagelayer-ai-field">' +
198 '<label>API Key</label>' +
199 '<div class="pagelayer-ai-input-with-eye">' +
200 '<input type="password" class="pagelayer-ai-input" id="pagelayer-ai-deepseek-key" placeholder="Enter DeepSeek API key (sk-...)" />' +
201 '<button type="button" class="pagelayer-ai-eye-btn" title="Show/Hide Key"><i class="fas fa-eye"></i></button>' +
202 '</div>' +
203 '</div>' +
204 '<div class="pagelayer-ai-field">' +
205 '<label>Model Name <span class="pagelayer-ai-field-hint">e.g. deepseek-chat, deepseek-reasoner</span></label>' +
206 '<input type="text" class="pagelayer-ai-input" id="pagelayer-ai-deepseek-model" placeholder="deepseek-chat" />' +
207 '</div>' +
208 '</div>' +
209
210 '<!-- OpenRouter Card -->' +
211 '<div class="pagelayer-ai-card" data-provider="openrouter">' +
212 '<div class="pagelayer-ai-card-header">' +
213 '<div class="pagelayer-ai-card-title">' +
214 '<span class="pagelayer-ai-provider-dot" id="dot-openrouter"></span>' +
215 '<strong>OpenRouter (All Open Models)</strong>' +
216 '</div>' +
217 '<a href="https://openrouter.ai/keys" target="_blank" class="pagelayer-ai-key-link">Get Key <i class="fas fa-external-link-alt"></i></a>' +
218 '</div>' +
219 '<div class="pagelayer-ai-field">' +
220 '<label>API Key</label>' +
221 '<div class="pagelayer-ai-input-with-eye">' +
222 '<input type="password" class="pagelayer-ai-input" id="pagelayer-ai-openrouter-key" placeholder="Enter OpenRouter API key (sk-or-...)" />' +
223 '<button type="button" class="pagelayer-ai-eye-btn" title="Show/Hide Key"><i class="fas fa-eye"></i></button>' +
224 '</div>' +
225 '</div>' +
226 '<div class="pagelayer-ai-field">' +
227 '<label>Model Name <span class="pagelayer-ai-field-hint">e.g. deepseek/deepseek-chat, google/gemini-3.7-flash-001, meta-llama/llama-3.3-70b-instruct</span></label>' +
228 '<input type="text" class="pagelayer-ai-input" id="pagelayer-ai-openrouter-model" placeholder="deepseek/deepseek-chat" />' +
229 '</div>' +
230 '</div>' +
231
232 '<!-- OpenAI Card -->' +
233 '<div class="pagelayer-ai-card" data-provider="openai">' +
234 '<div class="pagelayer-ai-card-header">' +
235 '<div class="pagelayer-ai-card-title">' +
236 '<span class="pagelayer-ai-provider-dot" id="dot-openai"></span>' +
237 '<strong>OpenAI</strong>' +
238 '</div>' +
239 '<a href="https://platform.openai.com/api-keys" target="_blank" class="pagelayer-ai-key-link">Get Key <i class="fas fa-external-link-alt"></i></a>' +
240 '</div>' +
241 '<div class="pagelayer-ai-field">' +
242 '<label>API Key</label>' +
243 '<div class="pagelayer-ai-input-with-eye">' +
244 '<input type="password" class="pagelayer-ai-input" id="pagelayer-ai-openai-key" placeholder="Enter OpenAI API key (sk-...)" />' +
245 '<button type="button" class="pagelayer-ai-eye-btn" title="Show/Hide Key"><i class="fas fa-eye"></i></button>' +
246 '</div>' +
247 '</div>' +
248 '<div class="pagelayer-ai-field">' +
249 '<label>Model Name <span class="pagelayer-ai-field-hint">e.g. gpt-4o-mini, gpt-4o, o3-mini</span></label>' +
250 '<input type="text" class="pagelayer-ai-input" id="pagelayer-ai-openai-model" placeholder="gpt-4o-mini" />' +
251 '</div>' +
252 '</div>' +
253
254 '<!-- Anthropic Card -->' +
255 '<div class="pagelayer-ai-card" data-provider="anthropic">' +
256 '<div class="pagelayer-ai-card-header">' +
257 '<div class="pagelayer-ai-card-title">' +
258 '<span class="pagelayer-ai-provider-dot" id="dot-anthropic"></span>' +
259 '<strong>Anthropic Claude</strong>' +
260 '</div>' +
261 '<a href="https://console.anthropic.com/settings/keys" target="_blank" class="pagelayer-ai-key-link">Get Key <i class="fas fa-external-link-alt"></i></a>' +
262 '</div>' +
263 '<div class="pagelayer-ai-field">' +
264 '<label>API Key</label>' +
265 '<div class="pagelayer-ai-input-with-eye">' +
266 '<input type="password" class="pagelayer-ai-input" id="pagelayer-ai-anthropic-key" placeholder="Enter Claude API key (sk-ant-...)" />' +
267 '<button type="button" class="pagelayer-ai-eye-btn" title="Show/Hide Key"><i class="fas fa-eye"></i></button>' +
268 '</div>' +
269 '</div>' +
270 '<div class="pagelayer-ai-field">' +
271 '<label>Model Name <span class="pagelayer-ai-field-hint">e.g. claude-3-7-sonnet-20250219, claude-3-5-sonnet-20241022, claude-3-5-haiku-20241022</span></label>' +
272 '<input type="text" class="pagelayer-ai-input" id="pagelayer-ai-anthropic-model" placeholder="claude-3-7-sonnet-20250219" />' +
273 '</div>' +
274 '</div>' +
275
276 '<!-- Custom / Local Endpoint Card -->' +
277 '<div class="pagelayer-ai-card" data-provider="custom">' +
278 '<div class="pagelayer-ai-card-header">' +
279 '<div class="pagelayer-ai-card-title">' +
280 '<span class="pagelayer-ai-provider-dot" id="dot-custom"></span>' +
281 '<strong>Custom / Local (OpenAI-Compatible)</strong>' +
282 '</div>' +
283 '<span class="pagelayer-ai-badge-sm">Ollama / LM Studio / vLLM</span>' +
284 '</div>' +
285 '<div class="pagelayer-ai-field">' +
286 '<label>Endpoint URL</label>' +
287 '<input type="text" class="pagelayer-ai-input" id="pagelayer-ai-custom-endpoint" placeholder="e.g. http://localhost:11434/v1 or https://api.deepseek.com/v1" />' +
288 '</div>' +
289 '<div class="pagelayer-ai-field">' +
290 '<label>Custom Model Name <span class="pagelayer-ai-field-hint">e.g. llama3.3, qwen2.5-coder, deepseek-r1, mistral</span></label>' +
291 '<input type="text" class="pagelayer-ai-input" id="pagelayer-ai-custom-model" placeholder="llama3.3" />' +
292 '</div>' +
293 '<div class="pagelayer-ai-field">' +
294 '<label>API Key (Optional for local servers)</label>' +
295 '<div class="pagelayer-ai-input-with-eye">' +
296 '<input type="password" class="pagelayer-ai-input" id="pagelayer-ai-custom-key" placeholder="Enter API key if required" />' +
297 '<button type="button" class="pagelayer-ai-eye-btn" title="Show/Hide Key"><i class="fas fa-eye"></i></button>' +
298 '</div>' +
299 '</div>' +
300 '</div>' +
301
302 '</div>' +
303 '</div>' +
304
305 '<div class="pagelayer-ai-settings-footer-sticky">' +
306 '<button type="button" class="pagelayer-ai-save-settings-btn" id="pagelayer-ai-save-keys-btn">' +
307 '<i class="fas fa-save"></i> Save AI Settings' +
308 '</button>' +
309 '</div>' +
310 '</div>' +
311
312 '<!-- Chat Messages Container -->' +
313 '<div class="pagelayer-ai-messages" id="pagelayer-ai-messages-box"></div>' +
314
315 '<!-- Footer & Input -->' +
316 '<div class="pagelayer-ai-footer">' +
317 '<div class="pagelayer-ai-input-row">' +
318 '<textarea class="pagelayer-ai-textarea" id="pagelayer-ai-prompt-input" rows="2" placeholder="Describe section or layout (e.g. 3-column features with icons)..."></textarea>' +
319 '<button type="button" class="pagelayer-ai-submit-btn" id="pagelayer-ai-send-btn" title="Generate Layout (Enter)">' +
320 '<i class="fas fa-arrow-up"></i>' +
321 '</button>' +
322 '</div>' +
323 '<div class="pagelayer-ai-footer-options">' +
324 '<span>Insert at: ' +
325 '<select class="pagelayer-ai-insert-select" id="pagelayer-ai-target-position">' +
326 '<option value="end">Bottom of Canvas</option>' +
327 '<option value="start">Top of Canvas</option>' +
328 '<option value="selection">At Selected Element</option>' +
329 '</select>' +
330 '</span>' +
331 '<span>Shift+Enter for newline</span>' +
332 '</div>' +
333 '</div>' +
334 '</div>';
335
336 self.panel = $(panelHtml).appendTo($targetBody);
337 },
338
339 /**
340 * Inject "Build with AI" button in editor top bar
341 */
342 injectTopbarButton: function (targetDoc) {
343 var self = this;
344 var checkInterval = setInterval(function () {
345 var $headerRight = $(targetDoc).find('.pagelayer-top-header-bar .pagelayer-header-right');
346 if ($headerRight.length) {
347 // Always remove any previous button to ensure clean icon only (no text inside)
348 $headerRight.find('.pagelayer-ai-topbar-btn').remove();
349
350 // Strict icon-only button without ANY text node
351 var $btn = $('<span class="pagelayer-header-icon-wrap pagelayer-ai-topbar-btn" title="Build with AI" data-tlite="Build with AI"><i class="fas fa-magic"></i></span>');
352 $btn.on('click', function (e) {
353 e.preventDefault();
354 e.stopPropagation();
355 self.togglePanel();
356 });
357
358 var $divider = $headerRight.find('.pagelayer-header-divider');
359 if ($divider.length) {
360 $divider.before($btn);
361 } else {
362 $headerRight.prepend($btn);
363 }
364 self.topbarBtn = $btn;
365 clearInterval(checkInterval);
366 }
367 }, 300);
368 },
369
370 /**
371 * Bind all user interactions and drag behaviors
372 */
373 bindEvents: function (targetDoc) {
374 var self = this;
375 var $panel = self.panel;
376
377 // Close & Minimize buttons
378 $panel.find('#pagelayer-ai-close-btn').on('click', function () {
379 self.hidePanel();
380 });
381
382 $panel.find('#pagelayer-ai-minimize-btn').on('click', function () {
383 $panel.toggleClass('pagelayer-ai-minimized');
384 var isMin = $panel.hasClass('pagelayer-ai-minimized');
385 $(this).find('i').attr('class', isMin ? 'fas fa-chevron-up' : 'fas fa-minus');
386 });
387
388 // Settings Toggle (delegated for triggers like error bubble button)
389 $panel.on('click', '#pagelayer-ai-settings-toggle, .pagelayer-ai-open-settings-trigger, #pagelayer-ai-active-model-tag', function (e) {
390 e.preventDefault();
391 var $view = $panel.find('#pagelayer-ai-settings-view');
392 $view.addClass('active');
393 var curProvider = $panel.find('#pagelayer-ai-provider-picker').val() || 'deepseek';
394 var $card = $panel.find('.pagelayer-ai-card[data-provider="' + curProvider + '"]');
395 if ($card.length) {
396 var $body = $panel.find('#pagelayer-ai-settings-body');
397 if ($body.length) {
398 var cardPos = $card.position();
399 if (cardPos) {
400 $body.animate({ scrollTop: cardPos.top + $body.scrollTop() - 12 }, 200);
401 }
402 }
403 setTimeout(function () {
404 $card.find('#pagelayer-ai-' + curProvider + '-key').focus();
405 }, 220);
406 }
407 });
408
409 $panel.on('click', '#pagelayer-ai-settings-close', function (e) {
410 e.preventDefault();
411 $panel.find('#pagelayer-ai-settings-view').removeClass('active');
412 });
413
414 // Provider switch in header
415 $panel.find('#pagelayer-ai-provider-picker').on('change', function () {
416 self.updateActiveModelBadge();
417 });
418
419 // Eye password toggle
420 $panel.on('click', '.pagelayer-ai-eye-btn', function (e) {
421 e.preventDefault();
422 var $btn = $(this);
423 var $inp = $btn.siblings('input');
424 if ($inp.attr('type') === 'password') {
425 $inp.attr('type', 'text');
426 $btn.find('i').attr('class', 'fas fa-eye-slash');
427 } else {
428 $inp.attr('type', 'password');
429 $btn.find('i').attr('class', 'fas fa-eye');
430 }
431 });
432
433 // Live input updates for model tags
434 $panel.find('#pagelayer-ai-gemini-model, #pagelayer-ai-deepseek-model, #pagelayer-ai-openrouter-model, #pagelayer-ai-openai-model, #pagelayer-ai-anthropic-model, #pagelayer-ai-custom-model').on('input', function () {
435 self.updateActiveModelBadge();
436 });
437
438 // Save Settings
439 $panel.find('#pagelayer-ai-save-keys-btn').on('click', function () {
440 self.saveSettings();
441 });
442
443 // Autosize textarea & Submit on Enter (delegated for reliability)
444 $panel.on('input', '#pagelayer-ai-prompt-input', function () {
445 this.style.height = 'auto';
446 var h = Math.max(50, Math.min(this.scrollHeight, 120));
447 this.style.height = h + 'px';
448 this.style.overflowY = (this.scrollHeight > 120) ? 'auto' : 'hidden';
449 });
450
451 $panel.on('keydown', '#pagelayer-ai-prompt-input', function (e) {
452 if (e.which === 13 || e.keyCode === 13 || e.key === 'Enter') {
453 if (!e.shiftKey) {
454 e.preventDefault();
455 e.stopPropagation();
456 self.handlePromptSubmit();
457 }
458 }
459 });
460
461 $panel.on('click', '#pagelayer-ai-send-btn', function (e) {
462 e.preventDefault();
463 e.stopPropagation();
464 self.handlePromptSubmit();
465 });
466
467 // Keyboard shortcut: Ctrl+Shift+A / Cmd+Shift+A
468 $(window.parent.document).add(document).on('keydown', function (e) {
469 if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'A' || e.key === 'a')) {
470 e.preventDefault();
471 self.togglePanel();
472 }
473 });
474
475 // Canvas Add Section AI button trigger
476 $(targetDoc).add(document).on('click', '.pagelayer-add-circle-ai, .pagelayer-ai-canvas-btn, .pagelayer-ai-trigger-btn', function (e) {
477 e.preventDefault();
478 e.stopPropagation();
479 self.showPanel();
480 });
481
482 // Drag functionality
483 self.initDraggable(targetDoc);
484 },
485
486 /**
487 * Floating Draggable behavior
488 */
489 initDraggable: function (targetDoc) {
490 var self = this;
491 var $panel = self.panel;
492 var $handle = $panel.find('#pagelayer-ai-drag-handle');
493 var isDragging = false;
494 var startX, startY, initialLeft, initialTop;
495
496 $handle.on('mousedown', function (e) {
497 if ($(e.target).closest('button, select, input, .pagelayer-ai-provider-pill-wrap').length) return;
498 isDragging = true;
499 var offset = $panel.offset();
500 startX = e.clientX;
501 startY = e.clientY;
502 initialLeft = offset.left;
503 initialTop = offset.top;
504
505 $panel.css({
506 bottom: 'auto',
507 right: 'auto',
508 left: initialLeft + 'px',
509 top: initialTop + 'px'
510 });
511
512 $(targetDoc).on('mousemove.plAiDrag', function (ev) {
513 if (!isDragging) return;
514 var dx = ev.clientX - startX;
515 var dy = ev.clientY - startY;
516 var newLeft = Math.max(10, Math.min(targetDoc.documentElement.clientWidth - $panel.outerWidth() - 10, initialLeft + dx));
517 var newTop = Math.max(10, Math.min(targetDoc.documentElement.clientHeight - $panel.outerHeight() - 10, initialTop + dy));
518 $panel.css({ left: newLeft + 'px', top: newTop + 'px' });
519 });
520
521 $(targetDoc).on('mouseup.plAiDrag', function () {
522 isDragging = false;
523 $(targetDoc).off('.plAiDrag');
524 });
525 });
526 },
527
528 togglePanel: function () {
529 var self = this;
530 if (self.panel.hasClass('pagelayer-ai-hidden')) {
531 self.showPanel();
532 } else {
533 self.hidePanel();
534 }
535 },
536
537 showPanel: function () {
538 this.panel.removeClass('pagelayer-ai-hidden');
539 if (this.floatingBtn) this.floatingBtn.hide();
540 this.panel.find('#pagelayer-ai-prompt-input').focus();
541 },
542
543 hidePanel: function () {
544 this.panel.addClass('pagelayer-ai-hidden');
545 if (this.floatingBtn) this.floatingBtn.show();
546 },
547
548 /**
549 * Update status indicator dots on each provider card
550 */
551 updateStatusBadges: function () {
552 var self = this;
553 var setDot = function (id, isConnected) {
554 var $dot = $('#dot-' + id, self.panel);
555 if (isConnected) {
556 $dot.addClass('connected').attr('title', 'Connected');
557 } else {
558 $dot.removeClass('connected').attr('title', 'Not Configured');
559 }
560 };
561 setDot('deepseek', self.activeKeys.deepseek);
562 setDot('gemini', self.activeKeys.gemini);
563 setDot('openrouter', self.activeKeys.openrouter);
564 setDot('openai', self.activeKeys.openai);
565 setDot('anthropic', self.activeKeys.anthropic);
566 setDot('custom', self.activeKeys.custom);
567 },
568
569 /**
570 * Update the active model pill displayed in the header
571 */
572 updateActiveModelBadge: function () {
573 var self = this;
574 var provider = $('#pagelayer-ai-provider-picker', self.panel).val() || self.settings.default_provider || 'gemini';
575 var model = '';
576 if (provider === 'deepseek') {
577 model = $('#pagelayer-ai-deepseek-model', self.panel).val() || self.models.deepseek || 'deepseek-chat';
578 } else if (provider === 'gemini') {
579 model = $('#pagelayer-ai-gemini-model', self.panel).val() || self.models.gemini || 'gemini-3.7-flash';
580 } else if (provider === 'openrouter') {
581 model = $('#pagelayer-ai-openrouter-model', self.panel).val() || self.models.openrouter || 'deepseek/deepseek-chat';
582 } else if (provider === 'openai') {
583 model = $('#pagelayer-ai-openai-model', self.panel).val() || self.models.openai || 'gpt-4o-mini';
584 } else if (provider === 'anthropic') {
585 model = $('#pagelayer-ai-anthropic-model', self.panel).val() || self.models.anthropic || 'claude-3-7-sonnet-20250219';
586 } else if (provider === 'custom') {
587 model = $('#pagelayer-ai-custom-model', self.panel).val() || self.models.custom || 'llama3.3';
588 }
589 var shortModel = model;
590 if (shortModel.indexOf('/') !== -1) {
591 shortModel = shortModel.split('/').pop();
592 }
593 $('#pagelayer-ai-active-model-tag', self.panel).text(shortModel).attr('title', 'Active Model: ' + model + ' (Click to configure)');
594 },
595
596 /**
597 * Write Pagelayer global colors/fonts into live CSS variables so
598 * $primary / $text tokens in the new widgets resolve immediately.
599 */
600 applySiteGlobals: function (globals) {
601 if (!globals) return;
602 var colors = globals.colors || globals.global_colors;
603 var fonts = globals.fonts || globals.global_fonts;
604 var docs = [document];
605 try {
606 if (window.parent && window.parent.document && window.parent.document !== document) {
607 docs.push(window.parent.document);
608 }
609 } catch (e) { }
610 if (typeof pagelayer !== 'undefined') {
611 if (pagelayer.gDocument) docs.push(pagelayer.gDocument);
612 if (pagelayer.$ && pagelayer.$[0] && pagelayer.$[0].ownerDocument) {
613 docs.push(pagelayer.$[0].ownerDocument);
614 }
615 }
616
617 var seen = [];
618 docs.forEach(function (doc) {
619 if (!doc || !doc.documentElement || seen.indexOf(doc) !== -1) return;
620 seen.push(doc);
621 var root = doc.documentElement;
622 if (colors && typeof colors === 'object') {
623 Object.keys(colors).forEach(function (k) {
624 var entry = colors[k];
625 var v = (entry && typeof entry === 'object' && entry.value) ? entry.value : entry;
626 if (typeof v === 'string' && v) {
627 root.style.setProperty('--pagelayer-color-' + k, v);
628 }
629 });
630 }
631 if (fonts && typeof fonts === 'object') {
632 Object.keys(fonts).forEach(function (k) {
633 var val = fonts[k] && fonts[k].value ? fonts[k].value : null;
634 if (!val || typeof val !== 'object') return;
635 Object.keys(val).forEach(function (fk) {
636 var fv = val[fk];
637 if (!fv || typeof fv !== 'string') return;
638 var unit = (fk === 'font-size' || fk === 'letter-spacing' || fk === 'word-spacing') ? 'px' : '';
639 root.style.setProperty('--pagelayer-font-' + k + '-' + fk, fv + unit);
640 });
641 });
642 }
643 });
644
645 if (colors) {
646 if (typeof pagelayer_global_colors !== 'undefined') pagelayer_global_colors = colors;
647 try { if (window.parent && typeof window.parent.pagelayer_global_colors !== 'undefined') window.parent.pagelayer_global_colors = colors; } catch (e2) { }
648 }
649 if (fonts) {
650 if (typeof pagelayer_global_fonts !== 'undefined') pagelayer_global_fonts = fonts;
651 try { if (window.parent && typeof window.parent.pagelayer_global_fonts !== 'undefined') window.parent.pagelayer_global_fonts = fonts; } catch (e3) { }
652 }
653 },
654
655 /**
656 * Drop the live-build outline and Pagelayer "selected" state so the
657 * finished layout does not look like every widget is active.
658 */
659 ensureCanvasHighlightStyle: function ($canvas) {
660 var doc = document;
661 try {
662 if ($canvas && $canvas[0] && $canvas[0].ownerDocument) {
663 doc = $canvas[0].ownerDocument;
664 }
665 } catch (e) { }
666 if (!doc) {
667 return;
668 }
669 var uniformCss = '.pagelayer-ai-just-inserted{outline:3px solid #6366f1;outline-offset:2px;box-shadow:0 0 24px rgba(99,102,241,.3);}' +
670 '.pagelayer-service .pagelayer-service-image,.pagelayer-service-image{width:100%!important;overflow:hidden!important;margin:0 auto 16px auto!important;border-radius:8px!important;}' +
671 '.pagelayer-service .pagelayer-service-image img,.pagelayer-service img.pagelayer-img,.pagelayer-service-image img{width:100%!important;height:220px!important;max-height:220px!important;min-height:220px!important;object-fit:cover!important;object-position:center!important;border-radius:8px!important;display:block!important;margin:0 auto!important;}' +
672 '.pagelayer-service-container{display:flex!important;flex-direction:column!important;height:100%!important;}' +
673 '.pagelayer-service-details{display:flex!important;flex-direction:column!important;flex-grow:1!important;}' +
674 '.pagelayer-service-text{flex-grow:1!important;margin-bottom:16px!important;}' +
675 '.pagelayer-service-btn{margin-top:auto!important;align-self:center!important;}' +
676 '.pagelayer-iconbox .pagelayer-service-icon{display:inline-flex!important;align-items:center!important;justify-content:center!important;width:64px!important;height:64px!important;min-width:64px!important;min-height:64px!important;margin:0 auto 16px auto!important;line-height:1!important;}' +
677 '.pagelayer-image_slider,.pagelayer-image-slider-div{max-width:1000px!important;margin:0 auto!important;overflow:hidden!important;border-radius:12px!important;}' +
678 '.pagelayer-image-slider-ul{margin:0 auto!important;}' +
679 '.pagelayer-slider-item{overflow:hidden!important;border-radius:12px!important;}' +
680 '.pagelayer-image-slider-div img,.pagelayer-image_slider img,.pagelayer-slider-item img,.pagelayer-image-slider-ul img{width:100%!important;height:420px!important;max-height:440px!important;min-height:280px!important;object-fit:cover!important;object-position:center!important;border-radius:12px!important;display:block!important;margin:0 auto!important;}' +
681 '.pagelayer-row:not(.pagelayer-row-holder)>.pagelayer-row-holder{display:flex!important;flex-wrap:wrap!important;}' +
682 '.pagelayer-col{display:flex!important;flex-direction:column!important;}' +
683 '.pagelayer-col>.pagelayer-col-holder{display:flex!important;flex-direction:column!important;flex-grow:1!important;height:100%!important;}' +
684 '.pagelayer-col-holder>.pagelayer-ele-wrap{flex-grow:1!important;display:flex!important;flex-direction:column!important;}' +
685 '.pagelayer-col-holder>.pagelayer-ele-wrap>.pagelayer-ele{flex-grow:1!important;height:100%!important;}';
686 var existing = doc.getElementById('pagelayer-ai-canvas-style');
687 if (existing) {
688 if (existing.textContent.indexOf('.pagelayer-image_slider') === -1) {
689 existing.appendChild(doc.createTextNode(uniformCss));
690 }
691 return;
692 }
693 var style = doc.createElement('style');
694 style.id = 'pagelayer-ai-canvas-style';
695 style.appendChild(doc.createTextNode(uniformCss));
696 (doc.head || doc.documentElement).appendChild(style);
697 },
698
699 clearLiveBuildHighlight: function ($canvas, $fullNodes) {
700 var $scope = $fullNodes && $fullNodes.length ? $fullNodes : $canvas('body');
701 $scope.find('.pagelayer-ai-just-inserted').addBack('.pagelayer-ai-just-inserted').removeClass('pagelayer-ai-just-inserted');
702 $canvas('.pagelayer-ai-just-inserted').removeClass('pagelayer-ai-just-inserted');
703 this._lastAiHighlight = null;
704
705 $scope.find('[pagelayer-active]').addBack('[pagelayer-active]').removeAttr('pagelayer-active');
706 $scope.find('.pagelayer-active').addBack('.pagelayer-active').removeClass('pagelayer-active pagelayer-ele-hover pagelayer-drag-ele-hover');
707 $scope.find('.pagelayer-ele-overlay').removeClass('pagelayer-active pagelayer-ele-hover pagelayer-drag-ele-hover');
708 },
709
710 /**
711 * Load user API settings from REST / AJAX
712 */
713 loadSettings: function () {
714 var self = this;
715 var url = self.settings.rest_url + 'settings';
716
717 $.ajax({
718 url: url,
719 type: 'GET',
720 headers: { 'X-WP-Nonce': self.settings.nonce },
721 success: function (resp) {
722 if (resp && resp.success) {
723 self.applyLoadedSettings(resp);
724 }
725 },
726 error: function () {
727 // Fallback to AJAX
728 $.ajax({
729 url: self.settings.ajax_url + (self.settings.ajax_url.indexOf('?') === -1 ? '?' : '&') + 'action=pagelayer_ai_settings',
730 type: 'POST',
731 data: { pagelayer_nonce: self.settings.ajax_nonce },
732 success: function (r) {
733 if (r && !r.error && (r.success || r.default_provider || r.has_gemini_key !== undefined)) {
734 self.applyLoadedSettings(r.data || r);
735 }
736 }
737 });
738 }
739 });
740 },
741
742 applyLoadedSettings: function (resp) {
743 var self = this;
744 self.activeKeys.deepseek = !!resp.has_deepseek_key;
745 self.activeKeys.gemini = !!resp.has_gemini_key;
746 self.activeKeys.openrouter = !!resp.has_openrouter_key;
747 self.activeKeys.openai = !!resp.has_openai_key;
748 self.activeKeys.anthropic = !!resp.has_anthropic_key;
749 self.activeKeys.custom = !!(resp.custom_endpoint || resp.has_custom_key);
750
751 if (resp.deepseek_model) { self.models.deepseek = resp.deepseek_model; $('#pagelayer-ai-deepseek-model', self.panel).val(resp.deepseek_model); }
752 if (resp.gemini_model) { self.models.gemini = resp.gemini_model; $('#pagelayer-ai-gemini-model', self.panel).val(resp.gemini_model); }
753 if (resp.openrouter_model) { self.models.openrouter = resp.openrouter_model; $('#pagelayer-ai-openrouter-model', self.panel).val(resp.openrouter_model); }
754 if (resp.openai_model) { self.models.openai = resp.openai_model; $('#pagelayer-ai-openai-model', self.panel).val(resp.openai_model); }
755 if (resp.anthropic_model) { self.models.anthropic = resp.anthropic_model; $('#pagelayer-ai-anthropic-model', self.panel).val(resp.anthropic_model); }
756 if (resp.custom_model) { self.models.custom = resp.custom_model; $('#pagelayer-ai-custom-model', self.panel).val(resp.custom_model); }
757
758 if (resp.deepseek_masked) $('#pagelayer-ai-deepseek-key', self.panel).attr('placeholder', 'Saved: ' + resp.deepseek_masked);
759 if (resp.openrouter_masked) $('#pagelayer-ai-openrouter-key', self.panel).attr('placeholder', 'Saved: ' + resp.openrouter_masked);
760 if (resp.gemini_masked) $('#pagelayer-ai-gemini-key', self.panel).attr('placeholder', 'Saved: ' + resp.gemini_masked);
761 if (resp.openai_masked) $('#pagelayer-ai-openai-key', self.panel).attr('placeholder', 'Saved: ' + resp.openai_masked);
762 if (resp.anthropic_masked) $('#pagelayer-ai-anthropic-key', self.panel).attr('placeholder', 'Saved: ' + resp.anthropic_masked);
763 if (resp.custom_endpoint) $('#pagelayer-ai-custom-endpoint', self.panel).val(resp.custom_endpoint);
764 if (resp.custom_masked) $('#pagelayer-ai-custom-key', self.panel).attr('placeholder', 'Saved: ' + resp.custom_masked);
765
766 self.updateStatusBadges();
767
768 var provider = resp.default_provider || '';
769 var connected = {
770 deepseek: self.activeKeys.deepseek,
771 custom: self.activeKeys.custom,
772 openrouter: self.activeKeys.openrouter,
773 openai: self.activeKeys.openai,
774 anthropic: self.activeKeys.anthropic,
775 gemini: self.activeKeys.gemini
776 };
777 if (!provider || !connected[provider]) {
778 var order = ['deepseek', 'custom', 'openrouter', 'openai', 'anthropic', 'gemini'];
779 provider = '';
780 for (var i = 0; i < order.length; i++) {
781 if (connected[order[i]]) {
782 provider = order[i];
783 break;
784 }
785 }
786 }
787 if (!provider) provider = 'gemini';
788
789 var $picker = $('#pagelayer-ai-provider-picker', self.panel);
790 $picker.val(provider);
791 self.settings.default_provider = provider;
792
793 self.updateActiveModelBadge();
794 },
795
796 /**
797 * Save user API keys and models
798 */
799 saveSettings: function () {
800 var self = this;
801 var $panel = self.panel;
802 var deepseek = $('#pagelayer-ai-deepseek-key', $panel).val();
803 var deepseekModel = $('#pagelayer-ai-deepseek-model', $panel).val();
804 var openrouter = $('#pagelayer-ai-openrouter-key', $panel).val();
805 var openrouterModel = $('#pagelayer-ai-openrouter-model', $panel).val();
806 var gemini = $('#pagelayer-ai-gemini-key', $panel).val();
807 var geminiModel = $('#pagelayer-ai-gemini-model', $panel).val();
808 var openai = $('#pagelayer-ai-openai-key', $panel).val();
809 var openaiModel = $('#pagelayer-ai-openai-model', $panel).val();
810 var anthropic = $('#pagelayer-ai-anthropic-key', $panel).val();
811 var anthropicModel = $('#pagelayer-ai-anthropic-model', $panel).val();
812 var customEndpoint = $('#pagelayer-ai-custom-endpoint', $panel).val();
813 var customModel = $('#pagelayer-ai-custom-model', $panel).val();
814 var customKey = $('#pagelayer-ai-custom-key', $panel).val();
815 var activeProvider = $('#pagelayer-ai-provider-picker', $panel).val() || 'gemini';
816
817 var data = {
818 default_provider: activeProvider
819 };
820
821 if (deepseek) data.deepseek_key = deepseek;
822 if (deepseekModel !== undefined) data.deepseek_model = deepseekModel;
823 if (openrouter) data.openrouter_key = openrouter;
824 if (openrouterModel !== undefined) data.openrouter_model = openrouterModel;
825 if (gemini) data.gemini_key = gemini;
826 if (geminiModel !== undefined) data.gemini_model = geminiModel;
827 if (openai) data.openai_key = openai;
828 if (openaiModel !== undefined) data.openai_model = openaiModel;
829 if (anthropic) data.anthropic_key = anthropic;
830 if (anthropicModel !== undefined) data.anthropic_model = anthropicModel;
831 if (customEndpoint !== undefined) data.custom_endpoint = customEndpoint;
832 if (customModel !== undefined) data.custom_model = customModel;
833 if (customKey) data.custom_key = customKey;
834
835 // Determine default model based on active provider
836 if (activeProvider === 'deepseek') data.default_model = deepseekModel || self.models.deepseek;
837 else if (activeProvider === 'gemini') data.default_model = geminiModel || self.models.gemini;
838 else if (activeProvider === 'openrouter') data.default_model = openrouterModel || self.models.openrouter;
839 else if (activeProvider === 'openai') data.default_model = openaiModel || self.models.openai;
840 else if (activeProvider === 'anthropic') data.default_model = anthropicModel || self.models.anthropic;
841 else if (activeProvider === 'custom') data.default_model = customModel || self.models.custom;
842
843 // Custom/OpenCode is connected by endpoint; don't keep Gemini selected
844 // just because the dropdown still had it as the HTML default.
845 if (customEndpoint && !gemini && !self.activeKeys.gemini && !deepseek && !self.activeKeys.deepseek && data.default_provider === 'gemini') {
846 data.default_provider = 'custom';
847 data.default_model = customModel || 'llama3.3';
848 $('#pagelayer-ai-provider-picker', $panel).val('custom');
849 }
850
851 var $btn = $('#pagelayer-ai-save-keys-btn', $panel).html('<i class="fas fa-spinner fa-spin"></i> Saving...');
852
853 $.ajax({
854 url: self.settings.rest_url + 'settings',
855 type: 'POST',
856 headers: { 'X-WP-Nonce': self.settings.nonce },
857 contentType: 'application/json',
858 data: JSON.stringify(data),
859 success: function (resp) {
860 $btn.html('<i class="fas fa-check"></i> Settings Saved!');
861 setTimeout(function () {
862 $btn.html('<i class="fas fa-save"></i> Save AI Settings');
863 $panel.find('#pagelayer-ai-settings-view').removeClass('active');
864 }, 1200);
865 self.loadSettings();
866 },
867 error: function (xhr) {
868 var msg = (xhr.responseJSON && xhr.responseJSON.message) ? xhr.responseJSON.message : 'Error saving settings';
869 alert(msg);
870 $btn.html('<i class="fas fa-save"></i> Save AI Settings');
871 }
872 });
873 },
874
875 /**
876 * Handle Prompt Submission - Prompt user inside chat for style mode
877 */
878 handlePromptSubmit: function () {
879 var self = this;
880 if (self.isGenerating) return;
881
882 var $input = self.panel.find('#pagelayer-ai-prompt-input');
883 var prompt = $.trim($input.val());
884 if (!prompt) return;
885
886 var provider = self.panel.find('#pagelayer-ai-provider-picker').val() || self.settings.default_provider || 'deepseek';
887 var model = '';
888 if (provider === 'deepseek') {
889 model = $('#pagelayer-ai-deepseek-model', self.panel).val() || self.models.deepseek || 'deepseek-chat';
890 } else if (provider === 'gemini') {
891 model = $('#pagelayer-ai-gemini-model', self.panel).val() || self.models.gemini || 'gemini-3.7-flash';
892 } else if (provider === 'openrouter') {
893 model = $('#pagelayer-ai-openrouter-model', self.panel).val() || self.models.openrouter || 'deepseek/deepseek-chat';
894 } else if (provider === 'openai') {
895 model = $('#pagelayer-ai-openai-model', self.panel).val() || self.models.openai || 'gpt-4o-mini';
896 } else if (provider === 'anthropic') {
897 model = $('#pagelayer-ai-anthropic-model', self.panel).val() || self.models.anthropic || 'claude-3-7-sonnet-20250219';
898 } else if (provider === 'custom') {
899 model = $('#pagelayer-ai-custom-model', self.panel).val() || self.models.custom || 'llama3.3';
900 }
901
902 if (provider === 'gemini' && !self.activeKeys.gemini) {
903 if (self.activeKeys.deepseek) {
904 provider = 'deepseek';
905 model = $('#pagelayer-ai-deepseek-model', self.panel).val() || self.models.deepseek || 'deepseek-chat';
906 self.panel.find('#pagelayer-ai-provider-picker').val('deepseek');
907 } else if (self.activeKeys.openrouter) {
908 provider = 'openrouter';
909 model = $('#pagelayer-ai-openrouter-model', self.panel).val() || self.models.openrouter || 'deepseek/deepseek-chat';
910 self.panel.find('#pagelayer-ai-provider-picker').val('openrouter');
911 } else if (self.activeKeys.custom) {
912 provider = 'custom';
913 model = $('#pagelayer-ai-custom-model', self.panel).val() || self.models.custom || 'llama3.3';
914 self.panel.find('#pagelayer-ai-provider-picker').val('custom');
915 } else if (self.activeKeys.openai) {
916 provider = 'openai';
917 model = $('#pagelayer-ai-openai-model', self.panel).val() || self.models.openai || 'gpt-4o-mini';
918 self.panel.find('#pagelayer-ai-provider-picker').val('openai');
919 } else if (self.activeKeys.anthropic) {
920 provider = 'anthropic';
921 model = $('#pagelayer-ai-anthropic-model', self.panel).val() || self.models.anthropic || 'claude-3-7-sonnet-20250219';
922 self.panel.find('#pagelayer-ai-provider-picker').val('anthropic');
923 }
924 self.updateActiveModelBadge();
925 }
926
927 var targetPos = self.panel.find('#pagelayer-ai-target-position').val();
928 var postId = (typeof pagelayer_postID !== 'undefined') ? pagelayer_postID : ((typeof pagelayer_post !== 'undefined' && pagelayer_post.ID) ? pagelayer_post.ID : 0);
929
930 // Add User bubble
931 self.addUserMessage(prompt);
932 $input.val('').css('height', 'auto');
933
934 var postData = {
935 prompt: prompt,
936 provider: provider,
937 model: model,
938 post_id: postId,
939 target_position: targetPos,
940 context: {
941 page_title: (typeof pagelayer_post !== 'undefined' && pagelayer_post.post_title) ? pagelayer_post.post_title : '',
942 target_position: targetPos
943 }
944 };
945 if (provider === 'deepseek') {
946 var enteredKey = $('#pagelayer-ai-deepseek-key', self.panel).val();
947 if (enteredKey) postData.api_key = enteredKey;
948 }
949 if (provider === 'custom') {
950 postData.custom_endpoint = self.panel.find('#pagelayer-ai-custom-endpoint').val();
951 postData.custom_model = self.panel.find('#pagelayer-ai-custom-model').val() || model;
952 }
953
954 // Determine if this prompt is purely a color, background, or theme color tweak (skip choice modal)
955 // OR a build request (ask user Style & Theme Preference modal)
956 var isColorOrStyleOnly = self.isColorOrStyleChangeInstruction(prompt);
957
958 if (isColorOrStyleOnly) {
959 // Style/color tweaks, background changes, or global color updates execute directly without asking!
960 if (self.isGlobalColorInstruction(prompt)) {
961 postData.globals_mode = 'new';
962 postData.use_existing_globals = false;
963 } else if (self.isBackgroundColorInstruction(prompt)) {
964 postData.globals_mode = 'custom';
965 postData.custom_styling = true;
966 } else {
967 postData.globals_mode = 'existing';
968 postData.use_existing_globals = true;
969 }
970 self.executeGeneration(postData);
971 } else {
972 // Building/generating any layout, section, page, or "build other": ALWAYS ask Style & Theme Preference!
973 self.addStyleChoiceMessage(prompt, postData);
974 }
975 },
976
977 /**
978 * Detect if prompt is an explicit command to build/make a new section or page
979 */
980 isExplicitBuildRequest: function (prompt) {
981 return !this.isColorOrStyleChangeInstruction(prompt);
982 },
983
984 /**
985 * Detect if prompt is purely a direct color, theme color, or background change instruction (and NOT a build request)
986 */
987 isColorOrStyleChangeInstruction: function (prompt) {
988 var p = (prompt || '').trim().toLowerCase();
989 if (!p) {
990 return false;
991 }
992
993 // If it contains multiple lines, dashes, or bullets, it is a multi-section build request
994 if (p.indexOf('–') !== -1 || p.indexOf('—') !== -1 || /[-•]\s*\w+/i.test(p)) {
995 var lines = p.split(/[\r\n]+/);
996 if (lines.length >= 2) {
997 return false;
998 }
999 }
1000
1001 // Build commands are always build requests (e.g., "build other", "build this", "build pizza shop", "make a section")
1002 if (/^\s*(build|create|design|generate|add)\b/i.test(p)) {
1003 return false;
1004 }
1005 if (/^\s*make\s+(this|other|another|a\s+|an\s+|new\s+|page|section|site|layout|shop|store|landing|hero|card|table|widget)\b/i.test(p)) {
1006 return false;
1007 }
1008 if (/\b(build|create|design|generate)\s+(other|another|a\s+|an\s+|new\s+|this\s+|page|section|layout|site|shop|store|landing|hero)\b/i.test(p)) {
1009 return false;
1010 }
1011
1012 // 1. Explicit global color / theme color update
1013 // e.g., "change theme color global", "set this global color", "change global color"
1014 if (this.isGlobalColorInstruction(p)) {
1015 return true;
1016 }
1017
1018 // 2. Background color update
1019 // e.g., "change background color", "set background to black", "make all background dark"
1020 if (this.isBackgroundColorInstruction(p)) {
1021 return true;
1022 }
1023
1024 // 3. Direct color set/change instructions
1025 // e.g., "set this color", "give set this color", "change color to blue"
1026 if (/^\s*(give\s+)?(set|change|update|apply)\s+(this|the)?\s*colou?r\b/i.test(p) ||
1027 /^\s*(change|set|update)\s+(theme\s+colou?r|brand\s+colou?r|background|bg)\b/i.test(p)) {
1028 return true;
1029 }
1030
1031 return false;
1032 },
1033
1034 /**
1035 * Detect if prompt is asking to update/change global colors
1036 */
1037 isGlobalColorInstruction: function (prompt) {
1038 var p = (prompt || '').trim().toLowerCase();
1039 return /\b(global\s+colou?rs?|global\s+palette|brand\s+colou?rs?|site\s+colou?rs?|theme\s+colou?r\s+global)\b/i.test(p) ||
1040 /\b(change|set|update|modify|turn|make)\b.{0,30}\b(global\s+colou?r|global\s+palette|brand\s+colou?r|theme\s+colou?r\s+global)\b/i.test(p) ||
1041 /\b(theme\s+colou?r\s+global|change\s+theme\s+colou?r\s+global)\b/i.test(p) ||
1042 /\b(give\s+)?set\s+this\s+global\s+colou?r\b/i.test(p);
1043 },
1044
1045 /**
1046 * Detect if prompt is asking to change/set background color or canvas dark/light mode
1047 */
1048 isBackgroundColorInstruction: function (prompt) {
1049 var p = (prompt || '').trim().toLowerCase();
1050 return /\b(make|set|change|turn|switch)\b.{0,25}\b(all\s+)?(background|bg|theme)\b/i.test(p) ||
1051 /\b(change|set|update)\b.{0,20}\b(background\s+colou?r|bg\s+colou?r|canvas\s+colou?r)\b/i.test(p) ||
1052 /\b(make|set)\b.{0,20}\b(all\s+)?(background|bg)\b.{0,20}\b(dark|light|white|black|#[0-9a-f]{3,6}|[a-z]+)\b/i.test(p) ||
1053 (/\b(background|bg)\b.{0,20}\b(dark|light|white|black|#[0-9a-f]{3,6}|[a-z]+)\b/i.test(p) && !/\b(build|create)\s+(a|an|new)\s+(section|page|hero|pricing)\b/i.test(p)) ||
1054 (/\b(dark\s*mode|light\s*mode)\b/i.test(p) && !/\b(build|create)\s+(a|an|new)\s+(section|page|hero|pricing)\b/i.test(p)) ||
1055 /\b(give\s+)?set\s+this\s+colou?r\b/i.test(p);
1056 },
1057
1058 /**
1059 * Detect general style, color, font, or tweak instructions
1060 */
1061 isStyleOrColorInstruction: function (prompt) {
1062 return this.isColorOrStyleChangeInstruction(prompt);
1063 },
1064
1065 /**
1066 * Render interactive Style Choice Card in Chat
1067 */
1068 addStyleChoiceMessage: function (prompt, postData) {
1069 var self = this;
1070 var $box = self.panel.find('#pagelayer-ai-messages-box');
1071 var msgId = 'pl_choice_msg_' + Date.now();
1072
1073 // Detect if prompt contains explicit color, font, or theme styling requests
1074 var promptLower = prompt.toLowerCase();
1075 var hasCustomHints = /(#([0-9a-f]{3}|[0-9a-f]{6})\b|dark mode|light mode|theme|color|palette|font|roboto|poppins|inter|montserrat|open sans|lato|red|blue|green|purple|black|white|gold|orange|yellow|gradient|glassmorphism|cyberpunk|minimalist)/i.test(promptLower);
1076
1077 var html = '<div class="pagelayer-ai-msg assistant" id="' + msgId + '">' +
1078 '<div class="pagelayer-ai-avatar"><i class="fas fa-palette"></i></div>' +
1079 '<div class="pagelayer-ai-bubble" style="width:100%">' +
1080 '<div class="pagelayer-ai-choice-card">' +
1081 '<div class="pagelayer-ai-choice-title"><i class="fas fa-swatchbook"></i> Style & Theme Preference</div>' +
1082 '<p class="pagelayer-ai-choice-desc">How would you like to style this layout on your canvas?</p>' +
1083 '<div class="pagelayer-ai-choice-options">' +
1084
1085 '<button type="button" class="pagelayer-ai-choice-btn" data-mode="existing">' +
1086 '<div class="pagelayer-ai-choice-icon"><i class="fas fa-palette"></i></div>' +
1087 '<div class="pagelayer-ai-choice-text">' +
1088 '<strong>Use Existing Site Colors & Fonts</strong>' +
1089 '<span>Keep active site theme without overriding globals</span>' +
1090 '</div>' +
1091 '</button>' +
1092
1093 '<button type="button" class="pagelayer-ai-choice-btn" data-mode="new">' +
1094 '<div class="pagelayer-ai-choice-icon"><i class="fas fa-magic"></i></div>' +
1095 '<div class="pagelayer-ai-choice-text">' +
1096 '<strong>Generate New Brand Globals</strong>' +
1097 '<span>Create fresh AI colors & fonts and override site globals</span>' +
1098 '</div>' +
1099 '</button>' +
1100
1101 (hasCustomHints ?
1102 '<button type="button" class="pagelayer-ai-choice-btn" data-mode="custom">' +
1103 '<div class="pagelayer-ai-choice-icon"><i class="fas fa-sliders-h"></i></div>' +
1104 '<div class="pagelayer-ai-choice-text">' +
1105 '<strong>Use Custom Styling from Prompt</strong>' +
1106 '<span>Follow your prompt\'s specific colors & typography</span>' +
1107 '</div>' +
1108 '</button>' : '') +
1109
1110 '</div>' +
1111 '</div>' +
1112 '</div></div>';
1113
1114 var $msg = $(html).appendTo($box);
1115 self.scrollToBottom();
1116
1117 // Bind click handlers to choices
1118 $msg.find('.pagelayer-ai-choice-btn').on('click', function () {
1119 if (self.isGenerating) return;
1120 var $btn = $(this);
1121 var mode = $btn.attr('data-mode') || 'existing';
1122
1123 // Disable options and highlight chosen
1124 $msg.find('.pagelayer-ai-choice-btn').prop('disabled', true);
1125 $btn.addClass('active-selected');
1126
1127 postData.globals_mode = mode;
1128 postData.use_existing_globals = (mode === 'existing');
1129 if (mode === 'custom') {
1130 postData.custom_styling = true;
1131 }
1132
1133 // Execute generation
1134 self.executeGeneration(postData);
1135 });
1136 },
1137
1138 /**
1139 * Execute Generation with Chosen Style Mode
1140 */
1141 executeGeneration: function (postData) {
1142 var self = this;
1143 if (self.isGenerating) return;
1144
1145 var targetPos = postData.target_position || 'end';
1146
1147 // Add Loading / Real-time Planning bubble
1148 var loadingMsgId = self.addLoadingMessage(postData.prompt, postData);
1149 self.isGenerating = true;
1150 self.panel.find('#pagelayer-ai-send-btn').prop('disabled', true);
1151
1152 $.ajax({
1153 url: self.settings.rest_url + 'generate',
1154 type: 'POST',
1155 headers: { 'X-WP-Nonce': self.settings.nonce },
1156 contentType: 'application/json',
1157 data: JSON.stringify(postData),
1158 timeout: 180000,
1159 success: function (resp) {
1160 self.removeMessage(loadingMsgId);
1161
1162 if (resp && resp.success) {
1163 // 1. Direct Background Color Update
1164 if (resp.action === 'update_background') {
1165 self.applyCanvasBackgroundUpdate(resp);
1166 self.addBackgroundUpdatedMessage(resp);
1167 self.isGenerating = false;
1168 self.panel.find('#pagelayer-ai-send-btn').prop('disabled', false);
1169 return;
1170 }
1171
1172 // 2. Direct Site Globals Update
1173 if (resp.action === 'update_globals') {
1174 self.applyCanvasGlobalsUpdate(resp);
1175 self.addGlobalsUpdatedMessage(resp);
1176 self.isGenerating = false;
1177 self.panel.find('#pagelayer-ai-send-btn').prop('disabled', false);
1178 return;
1179 }
1180
1181 // 3. Layout generation
1182 if (resp.tree || resp.html || resp.theme_template) {
1183 if (resp.globals && resp.globals_updated) {
1184 self.applySiteGlobals(resp.globals);
1185 }
1186
1187 if (resp.kind) {
1188 try {
1189 var $canvasBody = (typeof pagelayer !== 'undefined' && pagelayer.$) ? pagelayer.$('body') : $('body');
1190 $canvasBody.removeClass(function (i, c) {
1191 return (c.match(/(^|\s)pagelayer-ai-kind-\S+/g) || []).join(' ');
1192 });
1193 $canvasBody.addClass('pagelayer-ai-kind-' + resp.kind);
1194 } catch (e) { }
1195 }
1196
1197 // Display Background AI Strategy & Plan Card in Chat
1198 if (resp.plan) {
1199 self.addPlanMessage(resp.plan, resp);
1200 }
1201
1202 var shouldInsert = (resp.insert_on_canvas !== false) && (resp.tree || resp.html);
1203 if (shouldInsert) {
1204 self.buildLiveStepByStep(resp, targetPos, function () {
1205 self.isGenerating = false;
1206 self.panel.find('#pagelayer-ai-send-btn').prop('disabled', false);
1207 self.addTaskDoneMessage(resp);
1208 });
1209 } else {
1210 self.isGenerating = false;
1211 self.panel.find('#pagelayer-ai-send-btn').prop('disabled', false);
1212 self.addTaskDoneMessage(resp);
1213 }
1214 return;
1215 }
1216 }
1217
1218 self.isGenerating = false;
1219 self.panel.find('#pagelayer-ai-send-btn').prop('disabled', false);
1220 self.addBotErrorMessage("Generated output was malformed. Please try again with a revised prompt.");
1221 },
1222 error: function (xhr) {
1223 self.removeMessage(loadingMsgId);
1224 self.isGenerating = false;
1225 self.panel.find('#pagelayer-ai-send-btn').prop('disabled', false);
1226
1227 var err = '';
1228 if (xhr.responseJSON) {
1229 if (xhr.responseJSON.message) {
1230 err = xhr.responseJSON.message;
1231 } else if (xhr.responseJSON.data && xhr.responseJSON.data.message) {
1232 err = xhr.responseJSON.data.message;
1233 } else if (typeof xhr.responseJSON === 'string') {
1234 err = xhr.responseJSON;
1235 }
1236 }
1237 if (!err && xhr.responseText) {
1238 try {
1239 var parsed = JSON.parse(xhr.responseText);
1240 err = parsed.message || (parsed.data && parsed.data.message) || parsed.error || '';
1241 } catch (e) { }
1242 }
1243 if (!err) {
1244 err = (xhr.statusText && xhr.statusText !== 'error') ? ('Request failed: ' + xhr.statusText) : 'Request failed. Please verify your API key and model settings.';
1245 }
1246
1247 var isApiKeyMissing = (xhr.responseJSON && (xhr.responseJSON.code === 'missing_api_key' || (xhr.responseJSON.data && xhr.responseJSON.data.provider))) ||
1248 (typeof err === 'string' && (/API key/i.test(err) || /No API key found/i.test(err)));
1249
1250 if (isApiKeyMissing) {
1251 self.addBotErrorMessage(err + '<br><button type="button" class="pagelayer-ai-btn-sm primary pagelayer-ai-open-settings-trigger" style="margin-top:8px;cursor:pointer"><i class="fas fa-key"></i> Open API Key Settings</button>');
1252 } else {
1253 self.addBotErrorMessage("<strong>Error:</strong> " + err);
1254 }
1255 }
1256 });
1257 },
1258
1259 /**
1260 * Apply Canvas Background & Dark Mode Update directly to editor DOM and rows
1261 */
1262 applyCanvasBackgroundUpdate: function (resp) {
1263 var self = this;
1264 var $canvas = (typeof pagelayer !== 'undefined' && pagelayer.$) ? pagelayer.$ : $;
1265 var $editable = $canvas(pagelayer_editable || '.pagelayer-editable-area');
1266 if (!$editable.length) $editable = $canvas('[pagelayer-data]').first();
1267 if (!$editable.length) $editable = $canvas('.entry-content, .post-content, main, body').first();
1268
1269 var bgColor = resp.bg_color || '#0f172a';
1270 var isDark = !!resp.is_dark;
1271
1272 // Update Canvas body background and dark classes
1273 var $body = $canvas('body');
1274 if (isDark) {
1275 $body.addClass('pagelayer-canvas-dark pagelayer-ai-dark-canvas');
1276 } else {
1277 $body.removeClass('pagelayer-canvas-dark pagelayer-ai-dark-canvas');
1278 }
1279 $body.css('background-color', bgColor);
1280
1281 // Update all section rows on canvas
1282 var $rows = $editable.find('.pagelayer-row:not(.pagelayer-row-holder)');
1283 if (!$rows.length) {
1284 $rows = $canvas('.pagelayer-row:not(.pagelayer-row-holder)');
1285 }
1286
1287 $rows.each(function () {
1288 var $row = $canvas(this);
1289 if ($row.closest('.pagelayer-add-widget-area').length) return;
1290
1291 var eleId = $row.attr('pagelayer-id');
1292 if (eleId && typeof pagelayer_set_atts === 'function') {
1293 var curClasses = (typeof pagelayer_get_att === 'function' ? pagelayer_get_att($row, 'ele_classes') : '') || '';
1294 curClasses = curClasses.replace(/\b(pagelayer-ai-on-dark|pagelayer-ai-on-light)\b/g, '').trim();
1295 var newClasses = curClasses + (isDark ? ' pagelayer-ai-on-dark' : '');
1296
1297 pagelayer_set_atts($row, 'ele_bg_type', 'color');
1298 pagelayer_set_atts($row, 'ele_bg_color', bgColor);
1299 pagelayer_set_atts($row, 'ele_classes', newClasses.trim());
1300 }
1301
1302 if (isDark) {
1303 $row.addClass('pagelayer-ai-on-dark').removeClass('pagelayer-ai-on-light');
1304 } else {
1305 $row.removeClass('pagelayer-ai-on-dark');
1306 }
1307
1308 if (typeof pagelayer_sc_render === 'function') {
1309 pagelayer_sc_render($row);
1310 }
1311 });
1312
1313 // Apply globals to CSS variables and pagelayer_global_colors
1314 if (resp.globals) {
1315 self.applySiteGlobals(resp.globals);
1316 }
1317
1318 // Mark canvas dirty for save
1319 if (typeof pagelayer_do_dirty === 'function') {
1320 pagelayer_do_dirty($editable);
1321 }
1322
1323 if (typeof pagelayer_history_action_push === 'function') {
1324 pagelayer_history_action_push({
1325 'title': 'AI Background Update',
1326 'action': 'Edited',
1327 'pl_id': 'canvas_bg',
1328 'color': bgColor
1329 });
1330 if (typeof pagelayer !== 'undefined') {
1331 pagelayer.history_action = true;
1332 pagelayer.global_render = true;
1333 }
1334 }
1335 },
1336
1337 /**
1338 * Apply Global Colors Update directly to editor CSS variables and re-render elements
1339 */
1340 applyCanvasGlobalsUpdate: function (resp) {
1341 var self = this;
1342 var $canvas = (typeof pagelayer !== 'undefined' && pagelayer.$) ? pagelayer.$ : $;
1343 var $editable = $canvas(pagelayer_editable || '.pagelayer-editable-area');
1344 if (!$editable.length) $editable = $canvas('[pagelayer-data]').first();
1345 if (!$editable.length) $editable = $canvas('.entry-content, .post-content, main, body').first();
1346
1347 // Apply site globals to CSS variables and pagelayer_global_colors
1348 if (resp.globals) {
1349 self.applySiteGlobals(resp.globals);
1350 }
1351
1352 // Re-render shortcodes for all elements
1353 if (typeof pagelayer_sc_render === 'function') {
1354 $canvas('.pagelayer-ele').each(function () {
1355 pagelayer_sc_render($canvas(this));
1356 });
1357 }
1358
1359 if (typeof pagelayer_do_dirty === 'function') {
1360 pagelayer_do_dirty($editable);
1361 }
1362
1363 if (typeof pagelayer !== 'undefined') {
1364 pagelayer.history_action = true;
1365 pagelayer.global_render = true;
1366 }
1367 },
1368
1369 /**
1370 * Add Background Updated Message Card in Chat
1371 */
1372 addBackgroundUpdatedMessage: function (resp) {
1373 var self = this;
1374 var $box = self.panel.find('#pagelayer-ai-messages-box');
1375 var msgId = 'pl_bg_msg_' + Date.now();
1376 var color = resp.bg_color || '#0f172a';
1377 var isDark = !!resp.is_dark;
1378 var title = isDark ? 'Dark Background Applied' : 'Background Color Updated';
1379 var desc = resp.message || (isDark ? 'Canvas & section backgrounds set to dark mode with high-contrast text.' : 'Background color set to ' + color + '.');
1380
1381 var html = '<div class="pagelayer-ai-msg assistant" id="' + msgId + '">' +
1382 '<div class="pagelayer-ai-avatar" style="background:#8b5cf6"><i class="fas fa-fill-drip"></i></div>' +
1383 '<div class="pagelayer-ai-bubble" style="width:100%">' +
1384 '<div class="pagelayer-ai-done-card">' +
1385 '<div class="pagelayer-ai-done-head">' +
1386 '<div class="pagelayer-ai-done-badge" style="background:rgba(139,92,246,0.2);color:#c4b5fd"><i class="fas fa-check-circle"></i> ' + escapeHtml(title) + '</div>' +
1387 '</div>' +
1388 '<div class="pagelayer-ai-done-body">' +
1389 '<p style="margin:0 0 8px 0">' + escapeHtml(desc) + '</p>' +
1390 '<div style="display:flex;align-items:center;gap:8px;font-size:12px;color:#94a3b8">' +
1391 '<span style="display:inline-block;width:20px;height:20px;border-radius:4px;border:1px solid rgba(255,255,255,0.2);background:' + escapeHtml(color) + '"></span>' +
1392 '<span>Color: <strong>' + escapeHtml(color) + '</strong> (' + (isDark ? 'Dark Mode' : 'Light Mode') + ')</span>' +
1393 '</div>' +
1394 '</div>' +
1395 '</div>' +
1396 '</div></div>';
1397
1398 $box.append(html);
1399 self.scrollToBottom();
1400 },
1401
1402 /**
1403 * Add Globals Updated Message Card in Chat
1404 */
1405 addGlobalsUpdatedMessage: function (resp) {
1406 var self = this;
1407 var $box = self.panel.find('#pagelayer-ai-messages-box');
1408 var msgId = 'pl_globals_msg_' + Date.now();
1409 var palette = resp.palette || (resp.globals && resp.globals.colors) || {};
1410 var swatches = [];
1411
1412 Object.keys(palette).forEach(function (k) {
1413 var val = palette[k];
1414 var hex = (val && typeof val === 'object' && val.value) ? val.value : val;
1415 if (typeof hex === 'string' && hex) {
1416 swatches.push('<span class="pagelayer-ai-plan-swatch" style="background:' + escapeHtml(hex) + '" title="' + escapeHtml(k + ': ' + hex) + '"></span>');
1417 }
1418 });
1419
1420 var html = '<div class="pagelayer-ai-msg assistant" id="' + msgId + '">' +
1421 '<div class="pagelayer-ai-avatar" style="background:#3b82f6"><i class="fas fa-palette"></i></div>' +
1422 '<div class="pagelayer-ai-bubble" style="width:100%">' +
1423 '<div class="pagelayer-ai-done-card">' +
1424 '<div class="pagelayer-ai-done-head">' +
1425 '<div class="pagelayer-ai-done-badge" style="background:rgba(59,130,246,0.2);color:#93c5fd"><i class="fas fa-check-circle"></i> Site Globals Updated</div>' +
1426 '</div>' +
1427 '<div class="pagelayer-ai-done-body">' +
1428 '<p style="margin:0 0 8px 0">' + escapeHtml(resp.message || 'Global site colors updated. All matching elements reflect changes.') + '</p>' +
1429 (swatches.length ? '<div class="pagelayer-ai-plan-swatches" style="margin-top:6px">' + swatches.join('') + '</div>' : '') +
1430 '</div>' +
1431 '</div>' +
1432 '</div></div>';
1433
1434 $box.append(html);
1435 self.scrollToBottom();
1436 },
1437
1438 /**
1439 * Live Step-by-Step Canvas Building Engine
1440 * Constructs rows, columns, and widgets sequentially in real-time with Pagelayer element setup
1441 */
1442 buildLiveStepByStep: function (response, position, onComplete) {
1443 var self = this;
1444 position = position || 'end';
1445 self.applySiteGlobals(response && response.globals);
1446 var $canvas = (typeof pagelayer !== 'undefined' && pagelayer.$) ? pagelayer.$ : $;
1447 self.ensureCanvasHighlightStyle($canvas);
1448
1449 // Locate active canvas editable container
1450 var $editable = $canvas(pagelayer_editable || '.pagelayer-editable-area');
1451 if (!$editable.length) $editable = $canvas('[pagelayer-data]').first();
1452 if (!$editable.length) $editable = $canvas('.entry-content, .post-content, main, body').first();
1453
1454 // Locate add-widget area ('Click here to add new row OR drag widgets')
1455 var $addSectionArea = $canvas('.pagelayer-add-widget-area').first();
1456 if (!$addSectionArea.length) {
1457 $addSectionArea = $canvas('.pagelayer-add-section-holder, .pagelayer-add-section').first();
1458 }
1459
1460 // Parse full HTML into DOM nodes
1461 var $fullNodes = $canvas(response.html);
1462 if (!$fullNodes.length) {
1463 if (typeof onComplete === 'function') onComplete();
1464 return;
1465 }
1466
1467 // Add a progress notification in chatbox
1468 var liveProgressMsgId = self.addLiveBuildingProgress();
1469
1470 // Check if canvas currently only has an empty placeholder row (e.g., blank page with 0 widgets)
1471 var $hasAnyWidgets = $editable.find('.pagelayer-ele:not(.pagelayer-row):not(.pagelayer-col)').length > 0;
1472 if (!$hasAnyWidgets) {
1473 $editable.find('.pagelayer-wrap-row, .pagelayer-row:not(.pagelayer-row-holder)').filter(function () {
1474 return !$canvas(this).closest('.pagelayer-add-widget-area').length;
1475 }).remove();
1476 }
1477
1478 // Insert outer container at target position (cleanly before add-widget area by default)
1479 if (position === 'start') {
1480 $editable.prepend($fullNodes);
1481 } else if (position === 'selection' && typeof pagelayer_active !== 'undefined' && pagelayer_active && pagelayer_active.id) {
1482 var $activeEle = $canvas('[pagelayer-id="' + pagelayer_active.id + '"]').first();
1483 if ($activeEle.length) {
1484 var $activeRow = $activeEle.closest('.pagelayer-wrap-row, .pagelayer-row:not(.pagelayer-row-holder)');
1485 var $activeRowWidgets = $activeRow.length ? $activeRow.find('.pagelayer-ele:not(.pagelayer-row):not(.pagelayer-col)') : [];
1486 if ($activeRow.length && $activeRowWidgets.length === 0) {
1487 $activeRow.replaceWith($fullNodes);
1488 } else {
1489 var $activeWrap = $activeEle.closest('.pagelayer-ele-wrap');
1490 if ($activeWrap.length) {
1491 $activeWrap.after($fullNodes);
1492 } else {
1493 $activeEle.after($fullNodes);
1494 }
1495 }
1496 } else if ($addSectionArea.length) {
1497 $addSectionArea.before($fullNodes);
1498 } else {
1499 $editable.append($fullNodes);
1500 }
1501 } else {
1502 if ($addSectionArea.length) {
1503 $addSectionArea.before($fullNodes);
1504 } else {
1505 $editable.append($fullNodes);
1506 }
1507 }
1508
1509 // Run Pagelayer element setup only on the newly inserted tree
1510 if (typeof pagelayer_element_setup === 'function') {
1511 $fullNodes.each(function () {
1512 var $root = $canvas(this);
1513 var $eles = $root.find('.pagelayer-ele').addBack('.pagelayer-ele');
1514 $eles.each(function () {
1515 var id = $canvas(this).attr('pagelayer-id');
1516 if (id) {
1517 pagelayer_element_setup('[pagelayer-id="' + id + '"]', true);
1518 }
1519 });
1520 });
1521 }
1522
1523 // Ensure no stray empty column dropzones remain inside populated columns
1524 $fullNodes.find('.pagelayer-col-holder').each(function () {
1525 var $holder = $canvas(this);
1526 if ($holder.find('.pagelayer-ele:not(.pagelayer-col)').length > 0) {
1527 $holder.children('.pagelayer-add-ele').remove();
1528 }
1529 });
1530
1531 // Prepare build sequence queue from the properly wrapped elements
1532 var queue = [];
1533 $fullNodes.find('.pagelayer-wrap-row, .pagelayer-wrap-col, .pagelayer-wrap-ele').addBack('.pagelayer-wrap-row, .pagelayer-wrap-col, .pagelayer-wrap-ele').each(function () {
1534 var $wrap = $canvas(this);
1535 var $ele = $wrap.children('.pagelayer-ele');
1536 if (!$ele.length) return;
1537
1538 var tag = $ele.attr('pagelayer-tag') || 'widget';
1539 var name = ucfirst(tag.replace('pl_', ''));
1540 if (tag === 'pl_row') name = 'Section Row';
1541 else if (tag === 'pl_col') name = 'Column';
1542 else if (tag === 'pl_heading') name = 'Heading';
1543 else if (tag === 'pl_text') name = 'Rich Text';
1544 else if (tag === 'pl_btn') name = 'Button';
1545 else if (tag === 'pl_image') name = 'Image';
1546 else if (tag === 'pl_service') name = 'Service Card';
1547 else if (tag === 'pl_icon') name = 'Icon';
1548 else if (tag === 'pl_testimonial') name = 'Testimonial';
1549
1550 queue.push({
1551 wrap: $wrap,
1552 ele: $ele,
1553 type: (tag === 'pl_row' ? 'row' : (tag === 'pl_col' ? 'col' : 'widget')),
1554 name: name
1555 });
1556 });
1557
1558 // Hide elements initially for sequential visual assembly
1559 queue.forEach(function (item) {
1560 if (item.type !== 'row') {
1561 item.wrap.css({ opacity: 0, transform: 'translateY(12px)', transition: 'all 0.25s cubic-bezier(0.16, 1, 0.3, 1)' });
1562 }
1563 });
1564
1565 // Execute sequential live building (snappy dynamic interval completing in ~1.5s)
1566 var stepIndex = 0;
1567 var stepInterval = Math.max(25, Math.min(45, Math.floor(1800 / Math.max(1, queue.length))));
1568
1569 function playNextStep() {
1570 if (stepIndex >= queue.length) {
1571 // Finalize complete setup for all elements
1572 $fullNodes.each(function () {
1573 var $node = $canvas(this);
1574 if (typeof pagelayer_sc_render === 'function') {
1575 pagelayer_sc_render($node);
1576 $node.find('.pagelayer-ele').each(function () {
1577 pagelayer_sc_render($canvas(this));
1578 });
1579 }
1580 });
1581
1582 // Push to Undo/Redo stack
1583 if (typeof pagelayer_history_action_push === 'function') {
1584 var firstId = $fullNodes.first().attr('pagelayer-id') || 'ai_' + Math.floor(Math.random() * 10000);
1585 pagelayer_history_action_push({
1586 'title': 'Build with AI Section',
1587 'action': 'Added',
1588 'pl_id': firstId,
1589 'html': $fullNodes[0] ? $fullNodes[0].outerHTML : '',
1590 'cEle': $fullNodes
1591 });
1592 if (typeof pagelayer !== 'undefined') {
1593 pagelayer.history_action = true;
1594 pagelayer.global_render = true;
1595 }
1596 }
1597
1598 // Mark Canvas Dirty for Save
1599 if (typeof pagelayer_do_dirty === 'function') {
1600 pagelayer_do_dirty($fullNodes);
1601 }
1602
1603 self.clearLiveBuildHighlight($canvas, $fullNodes);
1604
1605 // Remove progress indicator
1606 self.removeMessage(liveProgressMsgId);
1607
1608 if (typeof onComplete === 'function') onComplete();
1609 return;
1610 }
1611
1612 var cur = queue[stepIndex];
1613 stepIndex++;
1614
1615 // Reveal element — highlight only the one currently being placed
1616 if (self._lastAiHighlight && self._lastAiHighlight.length) {
1617 self._lastAiHighlight.removeClass('pagelayer-ai-just-inserted');
1618 }
1619 cur.wrap.css({ opacity: 1, transform: 'translateY(0)' });
1620 cur.wrap.addClass('pagelayer-ai-just-inserted');
1621 self._lastAiHighlight = cur.wrap;
1622
1623 // Render shortcode styles
1624 if (typeof pagelayer_sc_render === 'function') {
1625 pagelayer_sc_render(cur.ele);
1626 }
1627
1628 // Update live building chat status
1629 self.updateLiveBuildingStatus(liveProgressMsgId, cur.type, cur.name);
1630
1631 // Smooth scroll to active element being built
1632 if (cur.wrap[0] && cur.wrap[0].scrollIntoView) {
1633 cur.wrap[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1634 }
1635
1636 setTimeout(playNextStep, stepInterval);
1637 }
1638
1639 playNextStep();
1640 },
1641
1642 /**
1643 * Add Live Building Progress Indicator in Chat
1644 */
1645 addLiveBuildingProgress: function () {
1646 var self = this;
1647 var $box = self.panel.find('#pagelayer-ai-messages-box');
1648 var id = 'pl_live_build_' + Date.now();
1649 var html = '<div class="pagelayer-ai-msg assistant" id="' + id + '">' +
1650 '<div class="pagelayer-ai-avatar"><div class="pagelayer-ai-spinner"></div></div>' +
1651 '<div class="pagelayer-ai-bubble" style="min-width:200px">' +
1652 '<div style="font-size:12px;font-weight:600;color:#818cf8;margin-bottom:4px"><i class="fas fa-magic"></i> Live Building Canvas...</div>' +
1653 '<div class="pl-ai-step-status" style="font-size:11px;color:#d1d5db;">🏗️ Assembling section container...</div>' +
1654 '</div>' +
1655 '</div>';
1656 $box.append(html);
1657 self.scrollToBottom();
1658 return id;
1659 },
1660
1661 /**
1662 * Update Live Building Step Text
1663 */
1664 updateLiveBuildingStatus: function (msgId, type, name) {
1665 var self = this;
1666 var icon = '✨';
1667 if (type === 'row') icon = '🏗️';
1668 else if (type === 'col') icon = '📐';
1669 else if (name.toLowerCase().indexOf('heading') !== -1) icon = '✍️';
1670 else if (name.toLowerCase().indexOf('image') !== -1) icon = '🖼️';
1671 else if (name.toLowerCase().indexOf('btn') !== -1 || name.toLowerCase().indexOf('button') !== -1) icon = '🔘';
1672
1673 $('#' + msgId + ' .pl-ai-step-status', self.panel).html(icon + ' Placing & styling <strong>' + name + '</strong>...');
1674 },
1675
1676 /**
1677 * Append User Message
1678 */
1679 addUserMessage: function (text) {
1680 var self = this;
1681 var $box = self.panel.find('#pagelayer-ai-messages-box');
1682 var html = '<div class="pagelayer-ai-msg user">' +
1683 '<div class="pagelayer-ai-avatar"><i class="fas fa-user"></i></div>' +
1684 '<div class="pagelayer-ai-bubble">' + $('<div>').text(text).html() + '</div>' +
1685 '</div>';
1686 $box.append(html);
1687 self.scrollToBottom();
1688 },
1689
1690 /**
1691 * Add AI Architecture & Design Plan Card in Chat
1692 */
1693 addPlanMessage: function (plan, responseData) {
1694 var self = this;
1695 if (!plan) return null;
1696
1697 var $box = self.panel.find('#pagelayer-ai-messages-box');
1698 var msgId = 'pl_plan_msg_' + Date.now();
1699
1700 var moodHtml = plan.mood ? '<span class="pagelayer-ai-plan-mood"><i class="fas fa-bolt"></i> ' + escapeHtml(plan.mood) + '</span>' : '';
1701 var summaryHtml = plan.summary ? '<div class="pagelayer-ai-plan-summary">' + escapeHtml(plan.summary) + '</div>' : '';
1702
1703 // Sections badges
1704 var sectionsHtml = '';
1705 if (plan.sections && plan.sections.length) {
1706 sectionsHtml = '<div class="pagelayer-ai-plan-row">' +
1707 '<div class="pagelayer-ai-plan-label">Planned Structure:</div>' +
1708 '<div class="pagelayer-ai-plan-badges">' +
1709 plan.sections.map(function (s) {
1710 return '<span class="pagelayer-ai-plan-badge section"><i class="fas fa-layer-group"></i> ' + escapeHtml(s) + '</span>';
1711 }).join('') +
1712 '</div></div>';
1713 }
1714
1715 // Palette swatches
1716 var paletteHtml = '';
1717 if (plan.palette && typeof plan.palette === 'object') {
1718 var swatches = [];
1719 var colors = plan.palette;
1720 var isPreserved = responseData && (responseData.globals_mode === 'existing' || !responseData.globals_updated);
1721 var tagHtml = isPreserved ?
1722 '<span class="pagelayer-ai-globals-tag preserved"><i class="fas fa-check"></i> Preserving Site Theme</span>' :
1723 '<span class="pagelayer-ai-globals-tag override"><i class="fas fa-magic"></i> New Site Globals</span>';
1724
1725 Object.keys(colors).forEach(function (k) {
1726 var hex = (colors[k] && typeof colors[k] === 'object' && colors[k].value) ? colors[k].value : colors[k];
1727 if (typeof hex === 'string' && hex) {
1728 swatches.push('<span class="pagelayer-ai-plan-swatch" style="background:' + escapeHtml(hex) + '" title="' + escapeHtml(k + ': ' + hex) + '"></span>');
1729 }
1730 });
1731
1732 if (swatches.length) {
1733 paletteHtml = '<div class="pagelayer-ai-plan-row">' +
1734 '<div style="display:flex;align-items:center;justify-content:space-between">' +
1735 '<span class="pagelayer-ai-plan-label">Design Palette:</span>' +
1736 tagHtml +
1737 '</div>' +
1738 '<div class="pagelayer-ai-plan-swatches">' + swatches.join('') + '</div>' +
1739 '</div>';
1740 }
1741 }
1742
1743 // Fonts
1744 var fontsHtml = '';
1745 if (plan.fonts && typeof plan.fonts === 'object') {
1746 var fontBadges = [];
1747 Object.keys(plan.fonts).forEach(function (k) {
1748 var f = plan.fonts[k];
1749 var fam = (f && typeof f === 'object' && f.value && f.value['font-family']) ? f.value['font-family'] : ((typeof f === 'string') ? f : '');
1750 if (fam) {
1751 fontBadges.push('<span class="pagelayer-ai-plan-badge"><i class="fas fa-font"></i> ' + escapeHtml(ucfirst(k) + ': ' + fam) + '</span>');
1752 }
1753 });
1754 if (fontBadges.length) {
1755 fontsHtml = '<div class="pagelayer-ai-plan-row">' +
1756 '<div class="pagelayer-ai-plan-label">Typography:</div>' +
1757 '<div class="pagelayer-ai-plan-badges">' + fontBadges.join('') + '</div>' +
1758 '</div>';
1759 }
1760 }
1761
1762 // Widgets
1763 var widgetsHtml = '';
1764 if (plan.widgets && plan.widgets.length) {
1765 var wBadges = plan.widgets.slice(0, 8).map(function (w) {
1766 var label = ucfirst(w.replace('pl_', ''));
1767 return '<span class="pagelayer-ai-plan-badge widget"><i class="fas fa-cube"></i> ' + escapeHtml(label) + '</span>';
1768 });
1769 if (plan.widgets.length > 8) {
1770 wBadges.push('<span class="pagelayer-ai-plan-badge widget">+' + (plan.widgets.length - 8) + ' more</span>');
1771 }
1772 widgetsHtml = '<div class="pagelayer-ai-plan-row">' +
1773 '<div class="pagelayer-ai-plan-label">Widgets:</div>' +
1774 '<div class="pagelayer-ai-plan-badges">' + wBadges.join('') + '</div>' +
1775 '</div>';
1776 }
1777
1778 var html = '<div class="pagelayer-ai-msg assistant" id="' + msgId + '">' +
1779 '<div class="pagelayer-ai-avatar"><i class="fas fa-clipboard-list"></i></div>' +
1780 '<div class="pagelayer-ai-bubble" style="min-width:0">' +
1781 '<div class="pagelayer-ai-plan-card">' +
1782 '<div class="pagelayer-ai-plan-head">' +
1783 '<span class="pagelayer-ai-plan-title"><i class="fas fa-drafting-compass"></i> AI Design Plan</span>' +
1784 moodHtml +
1785 '</div>' +
1786 summaryHtml +
1787 sectionsHtml +
1788 paletteHtml +
1789 fontsHtml +
1790 widgetsHtml +
1791 '</div>' +
1792 '</div></div>';
1793
1794 var $msg = $(html).appendTo($box);
1795 self.scrollToBottom();
1796 return msgId;
1797 },
1798
1799 /**
1800 * Add Task Done Completion Summary Card in Chat
1801 */
1802 addTaskDoneMessage: function (responseData) {
1803 var self = this;
1804 var $box = self.panel.find('#pagelayer-ai-messages-box');
1805 var msgId = 'pl_done_msg_' + Date.now();
1806
1807 var isPreserved = responseData && (responseData.globals_mode === 'existing' || !responseData.globals_updated);
1808 var globalsText = isPreserved ? 'Site global colors & typography preserved' : 'New site global colors & typography applied';
1809 var globalsIcon = isPreserved ? 'fas fa-palette' : 'fas fa-magic';
1810
1811 var widgetCount = (responseData && responseData.widgets && responseData.widgets.length) ? responseData.widgets.length : 0;
1812 var shortcode = responseData && responseData.shortcode ? responseData.shortcode : '';
1813 var themeTpl = responseData && responseData.theme_template ? responseData.theme_template : null;
1814 var inserted = !(responseData && responseData.insert_on_canvas === false);
1815 var doneBody = inserted
1816 ? 'Layout successfully assembled live on your canvas. All widgets follow Pagelayer standards and are 100% editable.'
1817 : (responseData && responseData.message ? responseData.message : 'Theme template created. Open it in the live editor to review and publish.');
1818
1819 var html = '<div class="pagelayer-ai-msg assistant" id="' + msgId + '">' +
1820 '<div class="pagelayer-ai-avatar" style="background:#10b981"><i class="fas fa-check"></i></div>' +
1821 '<div class="pagelayer-ai-bubble" style="min-width:0">' +
1822 '<div class="pagelayer-ai-done-card">' +
1823 '<div class="pagelayer-ai-done-head">' +
1824 '<div class="pagelayer-ai-done-badge"><i class="fas fa-check-circle"></i> Task Completed!</div>' +
1825 '</div>' +
1826 '<div class="pagelayer-ai-done-body">' +
1827 '<p style="margin:0 0 6px 0">' + escapeHtml(doneBody) + '</p>' +
1828 (themeTpl && themeTpl.edit_url
1829 ? '<p style="margin:0 0 8px 0"><a href="' + escapeHtml(themeTpl.edit_url) + '" target="_blank" rel="noopener" class="pagelayer-ai-meta-item"><i class="fas fa-external-link-alt"></i> ' + escapeHtml(themeTpl.title || 'Open theme template') + '</a></p>'
1830 : '') +
1831 '<div class="pagelayer-ai-done-meta">' +
1832 (widgetCount ? '<span class="pagelayer-ai-meta-item"><i class="fas fa-cubes"></i> ' + widgetCount + ' Widget Types</span>' : '') +
1833 '<span class="pagelayer-ai-meta-item"><i class="' + globalsIcon + '"></i> ' + globalsText + '</span>' +
1834 '</div>' +
1835 '</div>';
1836
1837 if (inserted && responseData) {
1838 html += '<div class="pagelayer-ai-card-actions">' +
1839 '<button type="button" class="pagelayer-ai-btn-sm primary pl-ai-reinsert-btn"><i class="fas fa-redo"></i> Rebuild Live</button>' +
1840 '</div>';
1841 }
1842
1843 html += '</div></div></div>';
1844
1845 var $msg = $(html).appendTo($box);
1846
1847 // Rebuild button
1848 if (responseData) {
1849 $msg.find('.pl-ai-reinsert-btn').on('click', function () {
1850 self.buildLiveStepByStep(responseData, self.panel.find('#pagelayer-ai-target-position').val());
1851 });
1852 }
1853
1854 self.scrollToBottom();
1855 },
1856
1857 /**
1858 * Append Assistant Message
1859 */
1860 addBotMessage: function (text, shortcode, showChips, responseData) {
1861 var self = this;
1862 var $box = self.panel.find('#pagelayer-ai-messages-box');
1863 var msgId = 'pl_ai_msg_' + Date.now();
1864
1865 var html = '<div class="pagelayer-ai-msg assistant" id="' + msgId + '">' +
1866 '<div class="pagelayer-ai-avatar"><i class="fas fa-sparkles fa-magic"></i></div>' +
1867 '<div class="pagelayer-ai-bubble">' +
1868 '<div>' + text + '</div>';
1869
1870 if (showChips) {
1871 html += '<div class="pagelayer-ai-chips-wrap">' +
1872 '<span class="pagelayer-ai-chip" data-prompt="Modern SaaS hero section with headline, subtitle, primary CTA button, and responsive mockup image">🚀 Hero Section</span>' +
1873 '<span class="pagelayer-ai-chip" data-prompt="3-column responsive feature grid highlighting fast performance, bank-grade security, and 24/7 support with icons and descriptions">✨ 3-Column Features</span>' +
1874 '<span class="pagelayer-ai-chip" data-prompt="Customer testimonial section with client photo avatars, 5-star rating icons, quotes, and company names">💬 Testimonials Grid</span>' +
1875 '<span class="pagelayer-ai-chip" data-prompt="3-tier pricing table for Starter, Professional (recommended badge), and Enterprise plans with feature checklist and CTA buttons">📊 Pricing Table</span>' +
1876 '<span class="pagelayer-ai-chip" data-prompt="Modern 2-column Contact Us section with contact details, address, working hours, and email info box">📞 Contact Section</span>' +
1877 '</div>';
1878 }
1879
1880 if (responseData) {
1881 html += '<div class="pagelayer-ai-card-actions">' +
1882 '<button type="button" class="pagelayer-ai-btn-sm primary pl-ai-reinsert-btn"><i class="fas fa-redo"></i> Rebuild Live</button>' +
1883 '</div>';
1884 }
1885
1886 html += '</div></div>';
1887
1888 var $msg = $(html).appendTo($box);
1889
1890 // Chip click handling
1891 $msg.find('.pagelayer-ai-chip').on('click', function () {
1892 var prompt = $(this).attr('data-prompt');
1893 self.panel.find('#pagelayer-ai-prompt-input').val(prompt).trigger('input');
1894 self.handlePromptSubmit();
1895 });
1896
1897 // Rebuild button
1898 if (responseData) {
1899 $msg.find('.pl-ai-reinsert-btn').on('click', function () {
1900 self.buildLiveStepByStep(responseData, self.panel.find('#pagelayer-ai-target-position').val());
1901 });
1902 }
1903
1904 self.scrollToBottom();
1905 },
1906
1907 /**
1908 * Append Error Message
1909 */
1910 addBotErrorMessage: function (errMsg) {
1911 var self = this;
1912 var $box = self.panel.find('#pagelayer-ai-messages-box');
1913 var html = '<div class="pagelayer-ai-msg assistant">' +
1914 '<div class="pagelayer-ai-avatar" style="background:#ef4444"><i class="fas fa-exclamation-triangle"></i></div>' +
1915 '<div class="pagelayer-ai-bubble" style="border-color:rgba(239,68,68,0.3);background:rgba(239,68,68,0.1)">' + errMsg + '</div>' +
1916 '</div>';
1917 $box.append(html);
1918 self.scrollToBottom();
1919 },
1920
1921 /**
1922 * Infer Planning Metadata & Steps from prompt for real-time progress feedback
1923 */
1924 inferPlanningMeta: function (prompt, postData) {
1925 var p = (prompt || '').toLowerCase();
1926 var target = 'Custom Layout';
1927 var components = [];
1928 var thoughts = [];
1929
1930 // Detect target type
1931 if (/\b(header|navbar|nav bar)\b/.test(p) && /\b(template|theme|header)\b/.test(p) && p.indexOf('footer') === -1) {
1932 target = 'Header Theme Template';
1933 components = [
1934 { icon: 'fa-heading', label: 'Brand / Logo' },
1935 { icon: 'fa-bars', label: 'Primary Menu' },
1936 { icon: 'fa-mouse-pointer', label: 'Header CTA' }
1937 ];
1938 thoughts = [
1939 'Building a slim Theme Builder header bar (logo + menu)...',
1940 'Skipping homepage sections — header chrome only...',
1941 'Applying bar padding and contrast for site-wide use...'
1942 ];
1943 } else if (/\bfooter\b/.test(p)) {
1944 target = 'Footer Theme Template';
1945 components = [
1946 { icon: 'fa-columns', label: 'Footer Columns' },
1947 { icon: 'fa-link', label: 'Useful Links' },
1948 { icon: 'fa-share-alt', label: 'Social + Contact' },
1949 { icon: 'fa-copyright', label: 'Copyright Bar' }
1950 ];
1951 thoughts = [
1952 'Designing a real multi-column footer theme template...',
1953 'Excluding features, services, and other page sections...',
1954 'Styling brand, links, contact, and copyright rows...'
1955 ];
1956 } else if (p.indexOf('404') !== -1 || p.indexOf('not found') !== -1) {
1957 target = '404 Error Page';
1958 components = [
1959 { icon: 'fa-exclamation-circle', label: 'Error Code 404' },
1960 { icon: 'fa-heading', label: 'Lost Headline' },
1961 { icon: 'fa-align-left', label: 'Helpful Subtext' },
1962 { icon: 'fa-home', label: 'Back Home CTA' }
1963 ];
1964 thoughts = [
1965 'Designing clean, single-screen 404 layout without unrequested sections...',
1966 'Styling prominent error typography and navigation recovery button...',
1967 'Ensuring responsive viewport centering and theme contrast...'
1968 ];
1969 } else if (p.indexOf('hero') !== -1 || p.indexOf('banner') !== -1) {
1970 target = 'Hero Section';
1971 components = [
1972 { icon: 'fa-columns', label: 'Split Hero Row' },
1973 { icon: 'fa-heading', label: 'Primary Headline' },
1974 { icon: 'fa-paragraph', label: 'Subtitle Copy' },
1975 { icon: 'fa-mouse-pointer', label: 'Primary CTA Button' },
1976 { icon: 'fa-image', label: 'Mockup / Visual' }
1977 ];
1978 thoughts = [
1979 'Architecting high-converting visual hierarchy and spacing...',
1980 'Selecting responsive grid proportions for headline and media...',
1981 'Aligning typography scale with site styling tokens...'
1982 ];
1983 } else if (p.indexOf('price') !== -1 || p.indexOf('pricing') !== -1 || p.indexOf('plan') !== -1) {
1984 target = 'Pricing Table';
1985 components = [
1986 { icon: 'fa-table', label: 'Pricing Cards' },
1987 { icon: 'fa-dollar-sign', label: 'Tier Pricing & Badges' },
1988 { icon: 'fa-check', label: 'Feature Checklist' },
1989 { icon: 'fa-shopping-cart', label: 'Tier CTA Buttons' }
1990 ];
1991 thoughts = [
1992 'Structuring comparative multi-column pricing grid...',
1993 'Setting up highlight styling for recommended tier card...',
1994 'Standardizing feature list padding and checkmark icons...'
1995 ];
1996 } else if (p.indexOf('feature') !== -1 || p.indexOf('why choose') !== -1 || p.indexOf('service') !== -1) {
1997 target = 'Features & Services';
1998 components = [
1999 { icon: 'fa-heading', label: 'Section Header' },
2000 { icon: 'fa-th-large', label: 'Multi-Column Grid' },
2001 { icon: 'fa-cubes', label: 'Icon Boxes' },
2002 { icon: 'fa-arrow-right', label: 'Learn More Links' }
2003 ];
2004 thoughts = [
2005 'Calculating balanced column widths and card gutters...',
2006 'Selecting contextual icons and uniform content heights...',
2007 'Configuring hover elevations and subtle transitions...'
2008 ];
2009 } else if (p.indexOf('testimonial') !== -1 || p.indexOf('review') !== -1) {
2010 target = 'Testimonials & Social Proof';
2011 components = [
2012 { icon: 'fa-heading', label: 'Social Proof Title' },
2013 { icon: 'fa-quote-left', label: 'Review Cards' },
2014 { icon: 'fa-star', label: 'Star Rating Rows' },
2015 { icon: 'fa-user', label: 'Author Avatars' }
2016 ];
2017 thoughts = [
2018 'Laying out customer feedback cards and quote typography...',
2019 'Adding star rating elements and reviewer verification tags...',
2020 'Optimizing avatar photo dimensions and card borders...'
2021 ];
2022 } else if (p.indexOf('contact') !== -1 || p.indexOf('location') !== -1 || p.indexOf('hours') !== -1) {
2023 target = 'Contact & Location';
2024 components = [
2025 { icon: 'fa-info-circle', label: 'Contact Details' },
2026 { icon: 'fa-map-marker-alt', label: 'Location & Hours' },
2027 { icon: 'fa-envelope', label: 'Inquiry CTA' }
2028 ];
2029 thoughts = [
2030 'Organizing address, business hours, and contact channels...',
2031 'Configuring clean icon alignments for contact info items...',
2032 'Setting up call-to-action button styling...'
2033 ];
2034 } else if (p.indexOf('slider') !== -1 || p.indexOf('carousel') !== -1) {
2035 target = 'Interactive Slider';
2036 components = [
2037 { icon: 'fa-images', label: 'Slider Container' },
2038 { icon: 'fa-layer-group', label: 'Content Slides' },
2039 { icon: 'fa-chevron-left', label: 'Nav Arrows & Dots' }
2040 ];
2041 thoughts = [
2042 'Configuring slider viewport and slide animation timing...',
2043 'Restricting slide image dimensions to prevent oversized displays...',
2044 'Formatting slide headlines, subtitles, and button triggers...'
2045 ];
2046 } else {
2047 target = 'Section & Layout';
2048 components = [
2049 { icon: 'fa-layer-group', label: 'Pagelayer Row' },
2050 { icon: 'fa-heading', label: 'Headline & Content' },
2051 { icon: 'fa-shapes', label: 'Styled Widgets' }
2052 ];
2053 thoughts = [
2054 'Deconstructing layout requirements from your prompt...',
2055 'Architecting responsive rows, columns, and spacing...',
2056 'Selecting and styling suitable Pagelayer widgets...'
2057 ];
2058 }
2059
2060 // Add custom prompt components if detected
2061 if (p.indexOf('button') !== -1 && !components.some(function(c) { return c.label.indexOf('Button') !== -1; })) {
2062 components.push({ icon: 'fa-mouse-pointer', label: 'Action Button' });
2063 }
2064 if ((p.indexOf('image') !== -1 || p.indexOf('photo') !== -1) && !components.some(function(c) { return c.label.indexOf('Image') !== -1 || c.label.indexOf('Mockup') !== -1; })) {
2065 components.push({ icon: 'fa-image', label: 'Visual Media' });
2066 }
2067
2068 var steps = [
2069 { icon: 'fa-lightbulb', text: 'Deconstructing prompt requirements' },
2070 { icon: 'fa-sitemap', text: 'Architecting section layout & grid' },
2071 { icon: 'fa-cubes', text: 'Selecting Pagelayer widgets' },
2072 { icon: 'fa-palette', text: 'Applying typography & color hierarchy' },
2073 { icon: 'fa-code', text: 'Synthesizing layout JSON' }
2074 ];
2075
2076 return {
2077 target: target,
2078 components: components.slice(0, 5),
2079 thoughts: thoughts,
2080 steps: steps
2081 };
2082 },
2083
2084 /**
2085 * Add Loading & Real-Time Planning Architecture Bubble
2086 */
2087 addLoadingMessage: function (prompt, postData) {
2088 var self = this;
2089 var $box = self.panel.find('#pagelayer-ai-messages-box');
2090 var id = 'pl_loading_' + Date.now();
2091
2092 // Clean any existing interval
2093 if (self.loadingInterval) {
2094 clearInterval(self.loadingInterval);
2095 self.loadingInterval = null;
2096 }
2097
2098 var meta = self.inferPlanningMeta(prompt, postData);
2099
2100 var stepsHtml = '';
2101 meta.steps.forEach(function (st, idx) {
2102 var statusClass = (idx === 0) ? 'done' : (idx === 1 ? 'active' : 'pending');
2103 var iconClass = (idx === 0) ? 'fa-check-circle' : (idx === 1 ? 'fa-spinner' : 'fa-circle');
2104 stepsHtml += '<div class="pagelayer-ai-plan-step ' + statusClass + '" data-step="' + idx + '">' +
2105 '<span class="pl-step-icon"><i class="fas ' + iconClass + '"></i></span>' +
2106 '<span class="pl-step-text">' + escapeHtml(st.text) + '</span>' +
2107 '</div>';
2108 });
2109
2110 var badgesHtml = '';
2111 meta.components.forEach(function (cp) {
2112 badgesHtml += '<span class="pagelayer-ai-plan-chip"><i class="fas ' + cp.icon + '"></i> ' + escapeHtml(cp.label) + '</span>';
2113 });
2114
2115 var initialThought = meta.thoughts[0] || 'Analyzing requirements and planning visual architecture...';
2116
2117 var html = '<div class="pagelayer-ai-msg assistant" id="' + id + '">' +
2118 '<div class="pagelayer-ai-avatar"><div class="pagelayer-ai-spinner"></div></div>' +
2119 '<div class="pagelayer-ai-bubble pagelayer-ai-loading-bubble" style="min-width:260px">' +
2120 '<div class="pagelayer-ai-loading-plan-card">' +
2121 '<div class="pagelayer-ai-loading-plan-head">' +
2122 '<div class="pagelayer-ai-loading-plan-title">' +
2123 '<span class="pagelayer-ai-loading-pulse-dot"></span>' +
2124 '<span>AI Planning & Architecture</span>' +
2125 '</div>' +
2126 '<span class="pagelayer-ai-loading-plan-target" title="' + escapeHtml(meta.target) + '">' + escapeHtml(meta.target) + '</span>' +
2127 '</div>' +
2128
2129 '<div class="pagelayer-ai-planning-steps">' +
2130 stepsHtml +
2131 '</div>' +
2132
2133 (badgesHtml ?
2134 '<div class="pagelayer-ai-planning-components">' +
2135 '<div class="pagelayer-ai-planning-comp-label">Planned Elements</div>' +
2136 '<div class="pagelayer-ai-planning-badges-wrap">' + badgesHtml + '</div>' +
2137 '</div>' : '') +
2138
2139 '<div class="pagelayer-ai-live-thought">' +
2140 '<i class="fas fa-brain"></i>' +
2141 '<span class="pl-thought-text">' + escapeHtml(initialThought) + '</span>' +
2142 '</div>' +
2143
2144 '<div class="pagelayer-ai-loading-progress-wrap">' +
2145 '<div class="pagelayer-ai-progress-track">' +
2146 '<div class="pagelayer-ai-progress-bar" style="width: 25%"></div>' +
2147 '</div>' +
2148 '</div>' +
2149
2150 '</div>' + // .pagelayer-ai-loading-plan-card
2151 '</div>' + // .pagelayer-ai-bubble
2152 '</div>';
2153
2154 $box.append(html);
2155 self.scrollToBottom();
2156
2157 // Real-time animation ticker
2158 var currentStep = 1;
2159 var thoughtIdx = 0;
2160 var progressPct = 25;
2161 var $card = $('#' + id, self.panel);
2162
2163 self.loadingInterval = setInterval(function () {
2164 if (!$card.length || !$('#' + id, self.panel).length) {
2165 clearInterval(self.loadingInterval);
2166 self.loadingInterval = null;
2167 return;
2168 }
2169
2170 // Advance step progression
2171 if (currentStep < meta.steps.length - 1) {
2172 var $prev = $card.find('.pagelayer-ai-plan-step[data-step="' + currentStep + '"]');
2173 $prev.removeClass('active pending').addClass('done');
2174 $prev.find('.pl-step-icon').html('<i class="fas fa-check-circle"></i>');
2175
2176 currentStep++;
2177 var $next = $card.find('.pagelayer-ai-plan-step[data-step="' + currentStep + '"]');
2178 $next.removeClass('pending done').addClass('active');
2179 $next.find('.pl-step-icon').html('<i class="fas fa-spinner"></i>');
2180 }
2181
2182 // Update thought ticker
2183 thoughtIdx = (thoughtIdx + 1) % (meta.thoughts.length || 1);
2184 if (meta.thoughts[thoughtIdx]) {
2185 var $thought = $card.find('.pl-thought-text');
2186 $thought.fadeOut(200, function () {
2187 $thought.text(meta.thoughts[thoughtIdx]).fadeIn(200);
2188 });
2189 }
2190
2191 // Advance progress bar smoothly up to 92%
2192 if (progressPct < 92) {
2193 progressPct += Math.floor(Math.random() * 14) + 10;
2194 if (progressPct > 92) progressPct = 92;
2195 $card.find('.pagelayer-ai-progress-bar').css('width', progressPct + '%');
2196 }
2197 }, 2200);
2198
2199 return id;
2200 },
2201
2202 removeMessage: function (id) {
2203 if (this.loadingInterval) {
2204 clearInterval(this.loadingInterval);
2205 this.loadingInterval = null;
2206 }
2207 $('#' + id, this.panel).remove();
2208 },
2209
2210 scrollToBottom: function () {
2211 var $box = this.panel.find('#pagelayer-ai-messages-box');
2212 $box.stop().animate({ scrollTop: $box[0].scrollHeight }, 300);
2213 }
2214 };
2215
2216 function escapeHtml(str) {
2217 if (typeof str !== 'string') return '';
2218 return $('<div>').text(str).html();
2219 }
2220
2221 function ucfirst(str) {
2222 if (!str) return '';
2223 return str.charAt(0).toUpperCase() + str.slice(1);
2224 }
2225
2226 // Initialize when Document and Pagelayer Editor are ready
2227 $(document).ready(function () {
2228 if (typeof pagelayer !== 'undefined') {
2229 PagelayerAI.init();
2230 } else {
2231 $(window).on('load', function () {
2232 PagelayerAI.init();
2233 });
2234 }
2235 });
2236
2237 window.PagelayerAI = PagelayerAI;
2238 if (typeof window.parent !== 'undefined' && window.parent && window.parent !== window) {
2239 window.parent.PagelayerAI = PagelayerAI;
2240 }
2241
2242 })(jQuery);
2243