| 1 |
<?php |
| 2 |
|
| 3 |
////////////////////////////////////////////////////////////// |
| 4 |
//=========================================================== |
| 5 |
// ai-controller.php |
| 6 |
//=========================================================== |
| 7 |
// PAGELAYER |
| 8 |
// AI Layout Engine & Generator Controller |
| 9 |
//=========================================================== |
| 10 |
////////////////////////////////////////////////////////////// |
| 11 |
|
| 12 |
if (!defined('ABSPATH')) exit; |
| 13 |
|
| 14 |
if (!class_exists('Pagelayer_AI_Layout_Engine')) { |
| 15 |
include_once(dirname(__FILE__) . '/ai-layout-engine.php'); |
| 16 |
} |
| 17 |
|
| 18 |
class Pagelayer_AI_Controller { |
| 19 |
|
| 20 |
private static $instance = null; |
| 21 |
const REST_NAMESPACE = 'pagelayer/v1'; |
| 22 |
const META_KEY = 'pagelayer_ai_keys'; |
| 23 |
|
| 24 |
public static function get_instance() { |
| 25 |
if (self::$instance === null) { |
| 26 |
self::$instance = new self(); |
| 27 |
} |
| 28 |
return self::$instance; |
| 29 |
} |
| 30 |
|
| 31 |
public function __construct() { |
| 32 |
add_action('rest_api_init', array($this, 'register_rest_routes')); |
| 33 |
add_filter('theme_page_templates', array($this, 'register_canvas_page_template')); |
| 34 |
add_filter('template_include', array($this, 'include_canvas_page_template'), 1100); |
| 35 |
} |
| 36 |
|
| 37 |
/** |
| 38 |
* Blank full-width template so AI landing pages render edge-to-edge. |
| 39 |
*/ |
| 40 |
public function register_canvas_page_template($templates) { |
| 41 |
if (!is_array($templates)) { |
| 42 |
$templates = array(); |
| 43 |
} |
| 44 |
$templates['pagelayer-canvas'] = __('Pagelayer Canvas', 'pagelayer'); |
| 45 |
return $templates; |
| 46 |
} |
| 47 |
|
| 48 |
public function include_canvas_page_template($template) { |
| 49 |
if (!is_singular()) { |
| 50 |
return $template; |
| 51 |
} |
| 52 |
$post_id = get_queried_object_id(); |
| 53 |
if (!$post_id) { |
| 54 |
return $template; |
| 55 |
} |
| 56 |
$slug = get_page_template_slug($post_id); |
| 57 |
if ($slug !== 'pagelayer-canvas') { |
| 58 |
return $template; |
| 59 |
} |
| 60 |
$file = (defined('PAGELAYER_DIR') ? PAGELAYER_DIR : dirname(__FILE__) . '/..') . '/templates/pagelayer-canvas.php'; |
| 61 |
return file_exists($file) ? $file : $template; |
| 62 |
} |
| 63 |
|
| 64 |
public function assign_canvas_template($post_id) { |
| 65 |
$post_id = (int) $post_id; |
| 66 |
if ($post_id <= 0 || get_post_type($post_id) !== 'page') { |
| 67 |
return false; |
| 68 |
} |
| 69 |
update_post_meta($post_id, '_wp_page_template', 'pagelayer-canvas'); |
| 70 |
return true; |
| 71 |
} |
| 72 |
|
| 73 |
/** |
| 74 |
* Register REST API routes |
| 75 |
*/ |
| 76 |
public function register_rest_routes() { |
| 77 |
// POST /pagelayer/v1/ai/settings |
| 78 |
register_rest_route(self::REST_NAMESPACE, '/ai/settings', array( |
| 79 |
array( |
| 80 |
'methods' => WP_REST_Server::CREATABLE, |
| 81 |
'callback' => array($this, 'rest_save_settings'), |
| 82 |
'permission_callback' => array($this, 'check_permissions'), |
| 83 |
), |
| 84 |
array( |
| 85 |
'methods' => WP_REST_Server::READABLE, |
| 86 |
'callback' => array($this, 'rest_get_settings'), |
| 87 |
'permission_callback' => array($this, 'check_permissions'), |
| 88 |
), |
| 89 |
)); |
| 90 |
|
| 91 |
// POST /pagelayer/v1/ai/generate |
| 92 |
register_rest_route(self::REST_NAMESPACE, '/ai/generate', array( |
| 93 |
'methods' => WP_REST_Server::CREATABLE, |
| 94 |
'callback' => array($this, 'rest_generate'), |
| 95 |
'permission_callback' => array($this, 'check_permissions'), |
| 96 |
)); |
| 97 |
} |
| 98 |
|
| 99 |
/** |
| 100 |
* Verify user permissions |
| 101 |
*/ |
| 102 |
public function check_permissions() { |
| 103 |
return current_user_can('edit_posts'); |
| 104 |
} |
| 105 |
|
| 106 |
/** |
| 107 |
* Available models and providers definition |
| 108 |
*/ |
| 109 |
public function get_available_models() { |
| 110 |
return array( |
| 111 |
'gemini' => array( |
| 112 |
'name' => 'Google Gemini', |
| 113 |
'default_model' => 'gemini-3.7-flash', |
| 114 |
'placeholder' => 'e.g. gemini-3.7-flash, gemini-1.5-flash, gemini-1.5-pro', |
| 115 |
'docs_url' => 'https://aistudio.google.com/app/apikey', |
| 116 |
), |
| 117 |
'deepseek' => array( |
| 118 |
'name' => 'DeepSeek (Official API)', |
| 119 |
'default_model' => 'deepseek-chat', |
| 120 |
'placeholder' => 'e.g. deepseek-chat, deepseek-reasoner', |
| 121 |
'docs_url' => 'https://platform.deepseek.com/api_keys', |
| 122 |
), |
| 123 |
'openrouter' => array( |
| 124 |
'name' => 'OpenRouter (All Open Models)', |
| 125 |
'default_model' => 'deepseek/deepseek-chat', |
| 126 |
'placeholder' => 'e.g. deepseek/deepseek-chat, google/gemini-3.7-flash-001, meta-llama/llama-3.3-70b-instruct', |
| 127 |
'docs_url' => 'https://openrouter.ai/keys', |
| 128 |
), |
| 129 |
'openai' => array( |
| 130 |
'name' => 'OpenAI', |
| 131 |
'default_model' => 'gpt-4o-mini', |
| 132 |
'placeholder' => 'e.g. gpt-4o-mini, gpt-4o, o3-mini', |
| 133 |
'docs_url' => 'https://platform.openai.com/api-keys', |
| 134 |
), |
| 135 |
'anthropic' => array( |
| 136 |
'name' => 'Anthropic Claude', |
| 137 |
'default_model' => 'claude-3-7-sonnet-20250219', |
| 138 |
'placeholder' => 'e.g. claude-3-7-sonnet-20250219, claude-3-5-sonnet-20241022, claude-3-5-haiku-20241022', |
| 139 |
'docs_url' => 'https://console.anthropic.com/settings/keys', |
| 140 |
), |
| 141 |
'custom' => array( |
| 142 |
'name' => 'Custom / Local (OpenAI-Compatible)', |
| 143 |
'default_model' => 'llama3.3', |
| 144 |
'placeholder' => 'e.g. llama3.3, qwen2.5-coder, deepseek-r1, mistral', |
| 145 |
'docs_url' => '', |
| 146 |
), |
| 147 |
); |
| 148 |
} |
| 149 |
|
| 150 |
/** |
| 151 |
* Encrypt sensitive string |
| 152 |
*/ |
| 153 |
private function encrypt($plain_text) { |
| 154 |
if (empty($plain_text)) return ''; |
| 155 |
$key = hash('sha256', wp_salt('auth'), true); |
| 156 |
$iv = openssl_random_pseudo_bytes(16); |
| 157 |
$cipher = openssl_encrypt($plain_text, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv); |
| 158 |
if ($cipher === false) return base64_encode($plain_text); |
| 159 |
return base64_encode($iv . $cipher); |
| 160 |
} |
| 161 |
|
| 162 |
/** |
| 163 |
* Decrypt sensitive string |
| 164 |
*/ |
| 165 |
private function decrypt($encrypted_text) { |
| 166 |
if (empty($encrypted_text)) return ''; |
| 167 |
$data = base64_decode($encrypted_text); |
| 168 |
if ($data === false || strlen($data) < 17) { |
| 169 |
return $data !== false ? $data : ''; |
| 170 |
} |
| 171 |
$key = hash('sha256', wp_salt('auth'), true); |
| 172 |
$iv = substr($data, 0, 16); |
| 173 |
$cipher = substr($data, 16); |
| 174 |
$plain = openssl_decrypt($cipher, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv); |
| 175 |
if ($plain === false) { |
| 176 |
return $data; |
| 177 |
} |
| 178 |
return $plain; |
| 179 |
} |
| 180 |
|
| 181 |
/** |
| 182 |
* Mask an API key for safe client display |
| 183 |
*/ |
| 184 |
private function mask_key($key) { |
| 185 |
if (empty($key)) return ''; |
| 186 |
$len = strlen($key); |
| 187 |
if ($len <= 8) return '••••••••'; |
| 188 |
return substr($key, 0, 4) . '••••••••' . substr($key, -4); |
| 189 |
} |
| 190 |
|
| 191 |
/** |
| 192 |
* Get saved API keys and settings for current user |
| 193 |
*/ |
| 194 |
public function get_user_ai_settings($user_id = 0) { |
| 195 |
if (!$user_id) { |
| 196 |
$user_id = get_current_user_id(); |
| 197 |
} |
| 198 |
$data = get_user_meta($user_id, self::META_KEY, true); |
| 199 |
if (!is_array($data)) { |
| 200 |
$data = array(); |
| 201 |
} |
| 202 |
|
| 203 |
$deepseek_key = !empty($data['deepseek_key']) ? $this->decrypt($data['deepseek_key']) : ''; |
| 204 |
$deepseek_model = !empty($data['deepseek_model']) ? sanitize_text_field($data['deepseek_model']) : (!empty($data['default_model']) && (!empty($data['default_provider']) && $data['default_provider'] === 'deepseek') ? sanitize_text_field($data['default_model']) : 'deepseek-chat'); |
| 205 |
$gemini_key = !empty($data['gemini_key']) ? $this->decrypt($data['gemini_key']) : ''; |
| 206 |
$gemini_model = !empty($data['gemini_model']) ? sanitize_text_field($data['gemini_model']) : (!empty($data['default_model']) && (!empty($data['default_provider']) && $data['default_provider'] === 'gemini') ? sanitize_text_field($data['default_model']) : 'gemini-3.7-flash'); |
| 207 |
if ($gemini_model === 'gemini-2.5-flash' || $gemini_model === 'gemini-2.5-pro' || strpos($gemini_model, 'gemini-2.5') !== false) { |
| 208 |
$gemini_model = 'gemini-3.7-flash'; |
| 209 |
} |
| 210 |
$openrouter_key = !empty($data['openrouter_key']) ? $this->decrypt($data['openrouter_key']) : ''; |
| 211 |
$openrouter_model = !empty($data['openrouter_model']) ? sanitize_text_field($data['openrouter_model']) : (!empty($data['default_model']) && (!empty($data['default_provider']) && $data['default_provider'] === 'openrouter') ? sanitize_text_field($data['default_model']) : 'deepseek/deepseek-chat'); |
| 212 |
$openai_key = !empty($data['openai_key']) ? $this->decrypt($data['openai_key']) : ''; |
| 213 |
$openai_model = !empty($data['openai_model']) ? sanitize_text_field($data['openai_model']) : (!empty($data['default_model']) && (!empty($data['default_provider']) && $data['default_provider'] === 'openai') ? sanitize_text_field($data['default_model']) : 'gpt-4o-mini'); |
| 214 |
$anthropic_key = !empty($data['anthropic_key']) ? $this->decrypt($data['anthropic_key']) : ''; |
| 215 |
$anthropic_model = !empty($data['anthropic_model']) ? sanitize_text_field($data['anthropic_model']) : (!empty($data['default_model']) && (!empty($data['default_provider']) && $data['default_provider'] === 'anthropic') ? sanitize_text_field($data['default_model']) : 'claude-3-7-sonnet-20250219'); |
| 216 |
$custom_key = !empty($data['custom_key']) ? $this->decrypt($data['custom_key']) : ''; |
| 217 |
$custom_endpoint = !empty($data['custom_endpoint']) ? sanitize_text_field($data['custom_endpoint']) : ''; |
| 218 |
$custom_model = !empty($data['custom_model']) ? sanitize_text_field($data['custom_model']) : ''; |
| 219 |
|
| 220 |
$resolved = $this->resolve_default_provider($data, array( |
| 221 |
'deepseek_key' => $deepseek_key, |
| 222 |
'deepseek_model' => $deepseek_model, |
| 223 |
'gemini_key' => $gemini_key, |
| 224 |
'gemini_model' => $gemini_model, |
| 225 |
'openrouter_key' => $openrouter_key, |
| 226 |
'openrouter_model' => $openrouter_model, |
| 227 |
'openai_key' => $openai_key, |
| 228 |
'openai_model' => $openai_model, |
| 229 |
'anthropic_key' => $anthropic_key, |
| 230 |
'anthropic_model' => $anthropic_model, |
| 231 |
'custom_key' => $custom_key, |
| 232 |
'custom_endpoint' => $custom_endpoint, |
| 233 |
'custom_model' => $custom_model, |
| 234 |
)); |
| 235 |
|
| 236 |
return array( |
| 237 |
'default_provider' => $resolved['provider'], |
| 238 |
'default_model' => $resolved['model'], |
| 239 |
'deepseek_key' => $deepseek_key, |
| 240 |
'deepseek_model' => $deepseek_model, |
| 241 |
'gemini_key' => $gemini_key, |
| 242 |
'gemini_model' => $gemini_model, |
| 243 |
'openrouter_key' => $openrouter_key, |
| 244 |
'openrouter_model' => $openrouter_model, |
| 245 |
'openai_key' => $openai_key, |
| 246 |
'openai_model' => $openai_model, |
| 247 |
'anthropic_key' => $anthropic_key, |
| 248 |
'anthropic_model' => $anthropic_model, |
| 249 |
'custom_key' => $custom_key, |
| 250 |
'custom_endpoint' => $custom_endpoint, |
| 251 |
'custom_model' => $custom_model, |
| 252 |
); |
| 253 |
} |
| 254 |
|
| 255 |
/** |
| 256 |
* Prefer a provider the user actually connected. Custom/OpenCode is |
| 257 |
* connected when an endpoint is set (API key is optional for local servers). |
| 258 |
* Never keep Gemini as default when it has no key. |
| 259 |
*/ |
| 260 |
private function resolve_default_provider($data, $keys) { |
| 261 |
$saved_provider = !empty($data['default_provider']) ? $data['default_provider'] : ''; |
| 262 |
$saved_model = !empty($data['default_model']) ? $data['default_model'] : ''; |
| 263 |
|
| 264 |
$connected = array(); |
| 265 |
if (!empty($keys['custom_endpoint'])) { |
| 266 |
$connected['custom'] = !empty($keys['custom_model']) ? $keys['custom_model'] : 'llama3.3'; |
| 267 |
} |
| 268 |
if (!empty($keys['deepseek_key'])) { |
| 269 |
$connected['deepseek'] = !empty($keys['deepseek_model']) ? $keys['deepseek_model'] : 'deepseek-chat'; |
| 270 |
} |
| 271 |
if (!empty($keys['openrouter_key'])) { |
| 272 |
$connected['openrouter'] = !empty($keys['openrouter_model']) ? $keys['openrouter_model'] : 'deepseek/deepseek-chat'; |
| 273 |
} |
| 274 |
if (!empty($keys['openai_key'])) { |
| 275 |
$connected['openai'] = !empty($keys['openai_model']) ? $keys['openai_model'] : 'gpt-4o-mini'; |
| 276 |
} |
| 277 |
if (!empty($keys['anthropic_key'])) { |
| 278 |
$connected['anthropic'] = !empty($keys['anthropic_model']) ? $keys['anthropic_model'] : 'claude-3-7-sonnet-20250219'; |
| 279 |
} |
| 280 |
if (!empty($keys['gemini_key'])) { |
| 281 |
$connected['gemini'] = !empty($keys['gemini_model']) ? $keys['gemini_model'] : 'gemini-3.7-flash'; |
| 282 |
} |
| 283 |
|
| 284 |
if ($saved_provider !== '' && isset($connected[$saved_provider])) { |
| 285 |
$model = $saved_model; |
| 286 |
if ($model === '') { |
| 287 |
$model = $connected[$saved_provider]; |
| 288 |
} |
| 289 |
return array('provider' => $saved_provider, 'model' => $model); |
| 290 |
} |
| 291 |
|
| 292 |
foreach ($connected as $provider => $model) { |
| 293 |
return array('provider' => $provider, 'model' => $model); |
| 294 |
} |
| 295 |
|
| 296 |
return array('provider' => 'gemini', 'model' => !empty($keys['gemini_model']) ? $keys['gemini_model'] : 'gemini-3.7-flash'); |
| 297 |
} |
| 298 |
|
| 299 |
/** |
| 300 |
* REST: GET /pagelayer/v1/ai/settings |
| 301 |
*/ |
| 302 |
public function rest_get_settings($request) { |
| 303 |
$settings = $this->get_user_ai_settings(); |
| 304 |
|
| 305 |
return rest_ensure_response(array( |
| 306 |
'success' => true, |
| 307 |
'default_provider' => $settings['default_provider'], |
| 308 |
'default_model' => $settings['default_model'], |
| 309 |
'has_deepseek_key' => !empty($settings['deepseek_key']), |
| 310 |
'has_gemini_key' => !empty($settings['gemini_key']), |
| 311 |
'has_openrouter_key' => !empty($settings['openrouter_key']), |
| 312 |
'has_openai_key' => !empty($settings['openai_key']), |
| 313 |
'has_anthropic_key' => !empty($settings['anthropic_key']), |
| 314 |
'has_custom_key' => !empty($settings['custom_key']), |
| 315 |
'deepseek_model' => $settings['deepseek_model'], |
| 316 |
'gemini_model' => $settings['gemini_model'], |
| 317 |
'openrouter_model' => $settings['openrouter_model'], |
| 318 |
'openai_model' => $settings['openai_model'], |
| 319 |
'anthropic_model' => $settings['anthropic_model'], |
| 320 |
'custom_endpoint' => $settings['custom_endpoint'], |
| 321 |
'custom_model' => $settings['custom_model'], |
| 322 |
'deepseek_masked' => $this->mask_key($settings['deepseek_key']), |
| 323 |
'gemini_masked' => $this->mask_key($settings['gemini_key']), |
| 324 |
'openrouter_masked' => $this->mask_key($settings['openrouter_key']), |
| 325 |
'openai_masked' => $this->mask_key($settings['openai_key']), |
| 326 |
'anthropic_masked' => $this->mask_key($settings['anthropic_key']), |
| 327 |
'custom_masked' => $this->mask_key($settings['custom_key']), |
| 328 |
'available_models' => $this->get_available_models(), |
| 329 |
)); |
| 330 |
} |
| 331 |
|
| 332 |
/** |
| 333 |
* REST: POST /pagelayer/v1/ai/settings |
| 334 |
*/ |
| 335 |
public function rest_save_settings($request) { |
| 336 |
$user_id = get_current_user_id(); |
| 337 |
$params = $request->get_json_params(); |
| 338 |
if (empty($params)) { |
| 339 |
$params = $request->get_params(); |
| 340 |
} |
| 341 |
|
| 342 |
$existing = get_user_meta($user_id, self::META_KEY, true); |
| 343 |
if (!is_array($existing)) { |
| 344 |
$existing = array(); |
| 345 |
} |
| 346 |
|
| 347 |
// Handle DeepSeek key & model |
| 348 |
if (isset($params['deepseek_key'])) { |
| 349 |
$raw_key = trim($params['deepseek_key']); |
| 350 |
if ($raw_key === '') { |
| 351 |
unset($existing['deepseek_key']); |
| 352 |
} elseif (strpos($raw_key, '••••') === false) { |
| 353 |
$existing['deepseek_key'] = $this->encrypt(sanitize_text_field($raw_key)); |
| 354 |
} |
| 355 |
} |
| 356 |
if (isset($params['deepseek_model'])) { |
| 357 |
$existing['deepseek_model'] = sanitize_text_field(trim($params['deepseek_model'])); |
| 358 |
} |
| 359 |
|
| 360 |
// Handle Gemini key & model |
| 361 |
if (isset($params['gemini_key'])) { |
| 362 |
$raw_key = trim($params['gemini_key']); |
| 363 |
if ($raw_key === '') { |
| 364 |
unset($existing['gemini_key']); |
| 365 |
} elseif (strpos($raw_key, '••••') === false) { |
| 366 |
$existing['gemini_key'] = $this->encrypt(sanitize_text_field($raw_key)); |
| 367 |
} |
| 368 |
} |
| 369 |
if (isset($params['gemini_model'])) { |
| 370 |
$existing['gemini_model'] = sanitize_text_field(trim($params['gemini_model'])); |
| 371 |
} |
| 372 |
|
| 373 |
// Handle OpenRouter key & model |
| 374 |
if (isset($params['openrouter_key'])) { |
| 375 |
$raw_key = trim($params['openrouter_key']); |
| 376 |
if ($raw_key === '') { |
| 377 |
unset($existing['openrouter_key']); |
| 378 |
} elseif (strpos($raw_key, '••••') === false) { |
| 379 |
$existing['openrouter_key'] = $this->encrypt(sanitize_text_field($raw_key)); |
| 380 |
} |
| 381 |
} |
| 382 |
if (isset($params['openrouter_model'])) { |
| 383 |
$existing['openrouter_model'] = sanitize_text_field(trim($params['openrouter_model'])); |
| 384 |
} |
| 385 |
|
| 386 |
// Handle OpenAI key & model |
| 387 |
if (isset($params['openai_key'])) { |
| 388 |
$raw_key = trim($params['openai_key']); |
| 389 |
if ($raw_key === '') { |
| 390 |
unset($existing['openai_key']); |
| 391 |
} elseif (strpos($raw_key, '••••') === false) { |
| 392 |
$existing['openai_key'] = $this->encrypt(sanitize_text_field($raw_key)); |
| 393 |
} |
| 394 |
} |
| 395 |
if (isset($params['openai_model'])) { |
| 396 |
$existing['openai_model'] = sanitize_text_field(trim($params['openai_model'])); |
| 397 |
} |
| 398 |
|
| 399 |
// Handle Anthropic key & model |
| 400 |
if (isset($params['anthropic_key'])) { |
| 401 |
$raw_key = trim($params['anthropic_key']); |
| 402 |
if ($raw_key === '') { |
| 403 |
unset($existing['anthropic_key']); |
| 404 |
} elseif (strpos($raw_key, '••••') === false) { |
| 405 |
$existing['anthropic_key'] = $this->encrypt(sanitize_text_field($raw_key)); |
| 406 |
} |
| 407 |
} |
| 408 |
if (isset($params['anthropic_model'])) { |
| 409 |
$existing['anthropic_model'] = sanitize_text_field(trim($params['anthropic_model'])); |
| 410 |
} |
| 411 |
|
| 412 |
// Handle Custom endpoint, key, and model |
| 413 |
if (isset($params['custom_endpoint'])) { |
| 414 |
$existing['custom_endpoint'] = sanitize_text_field(trim($params['custom_endpoint'])); |
| 415 |
} |
| 416 |
if (isset($params['custom_model'])) { |
| 417 |
$existing['custom_model'] = sanitize_text_field(trim($params['custom_model'])); |
| 418 |
} |
| 419 |
if (isset($params['custom_key'])) { |
| 420 |
$raw_key = trim($params['custom_key']); |
| 421 |
if ($raw_key === '') { |
| 422 |
unset($existing['custom_key']); |
| 423 |
} elseif (strpos($raw_key, '••••') === false) { |
| 424 |
$existing['custom_key'] = $this->encrypt(sanitize_text_field($raw_key)); |
| 425 |
} |
| 426 |
} |
| 427 |
|
| 428 |
// Default provider and model |
| 429 |
if (!empty($params['default_provider'])) { |
| 430 |
$existing['default_provider'] = sanitize_text_field($params['default_provider']); |
| 431 |
} |
| 432 |
if (!empty($params['default_model'])) { |
| 433 |
$existing['default_model'] = sanitize_text_field($params['default_model']); |
| 434 |
} |
| 435 |
|
| 436 |
// Saving a custom/OpenCode endpoint with no Gemini key should not leave |
| 437 |
// Gemini as the default just because the picker still had it selected. |
| 438 |
$has_custom = !empty($existing['custom_endpoint']); |
| 439 |
$has_gemini = !empty($existing['gemini_key']); |
| 440 |
if ($has_custom && !$has_gemini && (empty($existing['default_provider']) || $existing['default_provider'] === 'gemini')) { |
| 441 |
$existing['default_provider'] = 'custom'; |
| 442 |
$existing['default_model'] = !empty($existing['custom_model']) ? $existing['custom_model'] : 'custom-model'; |
| 443 |
} |
| 444 |
|
| 445 |
update_user_meta($user_id, self::META_KEY, $existing); |
| 446 |
|
| 447 |
$settings = $this->get_user_ai_settings($user_id); |
| 448 |
|
| 449 |
return rest_ensure_response(array( |
| 450 |
'success' => true, |
| 451 |
'message' => __('AI settings saved successfully.', 'pagelayer'), |
| 452 |
'default_provider' => $settings['default_provider'], |
| 453 |
'default_model' => $settings['default_model'], |
| 454 |
'has_deepseek_key' => !empty($settings['deepseek_key']), |
| 455 |
'has_gemini_key' => !empty($settings['gemini_key']), |
| 456 |
'has_openrouter_key' => !empty($settings['openrouter_key']), |
| 457 |
'has_openai_key' => !empty($settings['openai_key']), |
| 458 |
'has_anthropic_key' => !empty($settings['anthropic_key']), |
| 459 |
'has_custom_key' => !empty($settings['custom_key']), |
| 460 |
'deepseek_model' => $settings['deepseek_model'], |
| 461 |
'gemini_model' => $settings['gemini_model'], |
| 462 |
'openrouter_model' => $settings['openrouter_model'], |
| 463 |
'openai_model' => $settings['openai_model'], |
| 464 |
'anthropic_model' => $settings['anthropic_model'], |
| 465 |
'custom_endpoint' => $settings['custom_endpoint'], |
| 466 |
'custom_model' => $settings['custom_model'], |
| 467 |
'deepseek_masked' => $this->mask_key($settings['deepseek_key']), |
| 468 |
'gemini_masked' => $this->mask_key($settings['gemini_key']), |
| 469 |
'openrouter_masked' => $this->mask_key($settings['openrouter_key']), |
| 470 |
'openai_masked' => $this->mask_key($settings['openai_key']), |
| 471 |
'anthropic_masked' => $this->mask_key($settings['anthropic_key']), |
| 472 |
'custom_masked' => $this->mask_key($settings['custom_key']), |
| 473 |
)); |
| 474 |
} |
| 475 |
|
| 476 |
/** |
| 477 |
* Build system instructions from the live widget registry. |
| 478 |
*/ |
| 479 |
public function get_system_prompt($widget_tags = array()) { |
| 480 |
$engine = Pagelayer_AI_Layout_Engine::instance(); |
| 481 |
return $engine->get_build_prompt($widget_tags); |
| 482 |
} |
| 483 |
|
| 484 |
public function rest_generate($request) { |
| 485 |
$params = $request->get_json_params(); |
| 486 |
if (empty($params)) { |
| 487 |
$params = $request->get_params(); |
| 488 |
} |
| 489 |
|
| 490 |
$prompt = !empty($params['prompt']) ? trim(sanitize_textarea_field($params['prompt'])) : ''; |
| 491 |
if (empty($prompt)) { |
| 492 |
return new WP_Error('missing_prompt', __('Please provide a prompt describing the layout you want to build.', 'pagelayer'), array('status' => 400)); |
| 493 |
} |
| 494 |
|
| 495 |
$user_id = get_current_user_id(); |
| 496 |
$settings = $this->get_user_ai_settings($user_id); |
| 497 |
|
| 498 |
$provider = !empty($params['provider']) ? sanitize_text_field($params['provider']) : $settings['default_provider']; |
| 499 |
$model = !empty($params['model']) ? sanitize_text_field($params['model']) : $settings['default_model']; |
| 500 |
|
| 501 |
// Don't try Gemini when the user never connected it. |
| 502 |
if ($provider === 'gemini' && empty($settings['gemini_key'])) { |
| 503 |
$provider = $settings['default_provider']; |
| 504 |
$model = $settings['default_model']; |
| 505 |
} |
| 506 |
$context = !empty($params['context']) ? (is_array($params['context']) ? $params['context'] : array('page_context' => sanitize_text_field($params['context']))) : array(); |
| 507 |
$post_id = !empty($params['post_id']) ? intval($params['post_id']) : (!empty($_REQUEST['postID']) ? intval($_REQUEST['postID']) : 0); |
| 508 |
|
| 509 |
$api_key = ''; |
| 510 |
if (!empty($params['api_key'])) { |
| 511 |
$api_key = trim(sanitize_text_field($params['api_key'])); |
| 512 |
} else { |
| 513 |
if ($provider === 'deepseek') { |
| 514 |
$api_key = $settings['deepseek_key']; |
| 515 |
} elseif ($provider === 'gemini') { |
| 516 |
$api_key = $settings['gemini_key']; |
| 517 |
} elseif ($provider === 'openrouter') { |
| 518 |
$api_key = $settings['openrouter_key']; |
| 519 |
} elseif ($provider === 'openai') { |
| 520 |
$api_key = $settings['openai_key']; |
| 521 |
} elseif ($provider === 'anthropic') { |
| 522 |
$api_key = $settings['anthropic_key']; |
| 523 |
} elseif ($provider === 'custom') { |
| 524 |
$api_key = $settings['custom_key']; |
| 525 |
} |
| 526 |
} |
| 527 |
|
| 528 |
$engine = Pagelayer_AI_Layout_Engine::instance(); |
| 529 |
|
| 530 |
// 1. Direct Global Color update requests ("change global color to red", "set this global color #3b82f6") |
| 531 |
if ($engine->is_global_color_request($prompt)) { |
| 532 |
$res = $engine->handle_global_color_instruction($prompt); |
| 533 |
return rest_ensure_response($res); |
| 534 |
} |
| 535 |
|
| 536 |
// 2. Direct Background Color / Dark mode requests ("make all background dark", "change background color to ...", "set this color") |
| 537 |
if ($engine->is_background_color_request($prompt)) { |
| 538 |
$res = $engine->handle_background_color_instruction($prompt); |
| 539 |
return rest_ensure_response($res); |
| 540 |
} |
| 541 |
|
| 542 |
if (empty($api_key) && $provider !== 'custom') { |
| 543 |
return new WP_Error( |
| 544 |
'missing_api_key', |
| 545 |
sprintf(__('No API key found for %s. Please enter your API key in the AI Settings.', 'pagelayer'), strtoupper($provider)), |
| 546 |
array('status' => 400, 'provider' => $provider) |
| 547 |
); |
| 548 |
} |
| 549 |
|
| 550 |
$globals_mode = !empty($params['globals_mode']) ? sanitize_text_field($params['globals_mode']) : (!empty($params['use_existing_globals']) ? 'existing' : 'new'); |
| 551 |
$use_existing_globals = ($globals_mode === 'existing'); |
| 552 |
$custom_styling = ($globals_mode === 'custom') || !empty($params['custom_styling']); |
| 553 |
|
| 554 |
$engine = Pagelayer_AI_Layout_Engine::instance(); |
| 555 |
$call = array( |
| 556 |
'provider' => $provider, |
| 557 |
'api_key' => $api_key, |
| 558 |
'model' => $model, |
| 559 |
'settings' => $settings, |
| 560 |
'params' => $params, |
| 561 |
); |
| 562 |
|
| 563 |
$plan = array( |
| 564 |
'widgets' => $engine->widgets_for_request($prompt), |
| 565 |
'sections' => array(), |
| 566 |
'palette' => array(), |
| 567 |
'fonts' => array(), |
| 568 |
'summary' => '', |
| 569 |
'mood' => '', |
| 570 |
'layout' => array(), |
| 571 |
'use_existing_globals' => $use_existing_globals, |
| 572 |
'custom_styling' => $custom_styling, |
| 573 |
); |
| 574 |
|
| 575 |
if ($use_existing_globals) { |
| 576 |
$existing_tokens = $engine->get_existing_tokens(); |
| 577 |
$plan['palette'] = $existing_tokens['palette']; |
| 578 |
$plan['fonts'] = $existing_tokens['fonts']; |
| 579 |
$context['use_existing_globals'] = true; |
| 580 |
$context['existing_palette'] = $existing_tokens['palette']; |
| 581 |
$context['existing_fonts'] = $existing_tokens['fonts']; |
| 582 |
} elseif ($custom_styling) { |
| 583 |
$context['custom_styling'] = true; |
| 584 |
} |
| 585 |
|
| 586 |
$kind = $engine->request_kind($prompt); |
| 587 |
|
| 588 |
$requested_sections = $engine->is_scoped_kind($kind) ? array() : $engine->extract_requested_sections($prompt); |
| 589 |
if (!empty($requested_sections)) { |
| 590 |
$context['requested_sections'] = $requested_sections; |
| 591 |
$plan['requested_sections'] = $requested_sections; |
| 592 |
} |
| 593 |
|
| 594 |
$full_page = ($kind === 'home' || (!$engine->is_scoped_kind($kind) && count($requested_sections) >= 2)); |
| 595 |
|
| 596 |
// Page-level requests plan identity first so the builder designs a theme. |
| 597 |
if ($full_page || $engine->should_plan($kind)) { |
| 598 |
$plan_raw = $this->call_provider( |
| 599 |
$call, |
| 600 |
$engine->get_plan_prompt(), |
| 601 |
$engine->get_plan_user_message($prompt, $context), |
| 602 |
array('temperature' => 0.7, 'max_tokens' => 2048, 'json' => true) |
| 603 |
); |
| 604 |
if (!is_wp_error($plan_raw)) { |
| 605 |
$parsed = $engine->parse_plan($plan_raw); |
| 606 |
if (!empty($parsed['widgets'])) { |
| 607 |
$merged = array_values(array_unique(array_merge($parsed['widgets'], $plan['widgets']))); |
| 608 |
$plan['widgets'] = $engine->expand_related_widgets($merged); |
| 609 |
$plan['sections'] = $parsed['sections']; |
| 610 |
if (!$use_existing_globals && !empty($parsed['palette'])) { |
| 611 |
$plan['palette'] = $parsed['palette']; |
| 612 |
} |
| 613 |
if (!$use_existing_globals && !empty($parsed['fonts'])) { |
| 614 |
$plan['fonts'] = $parsed['fonts']; |
| 615 |
} |
| 616 |
$plan['summary'] = $parsed['summary']; |
| 617 |
$plan['mood'] = !empty($parsed['mood']) ? $parsed['mood'] : ''; |
| 618 |
$plan['layout'] = !empty($parsed['layout']) ? $parsed['layout'] : array(); |
| 619 |
} else { |
| 620 |
if (!$use_existing_globals && !empty($parsed['palette'])) { |
| 621 |
$plan['palette'] = $parsed['palette']; |
| 622 |
} |
| 623 |
if (!$use_existing_globals && !empty($parsed['fonts'])) { |
| 624 |
$plan['fonts'] = $parsed['fonts']; |
| 625 |
} |
| 626 |
if (!empty($parsed['mood'])) { |
| 627 |
$plan['mood'] = $parsed['mood']; |
| 628 |
} |
| 629 |
if (!empty($parsed['sections'])) { |
| 630 |
$plan['sections'] = $parsed['sections']; |
| 631 |
} |
| 632 |
if (!empty($parsed['summary'])) { |
| 633 |
$plan['summary'] = $parsed['summary']; |
| 634 |
} |
| 635 |
if (!empty($parsed['layout'])) { |
| 636 |
$plan['layout'] = $parsed['layout']; |
| 637 |
} |
| 638 |
} |
| 639 |
} |
| 640 |
} |
| 641 |
|
| 642 |
$plan['widgets'] = $engine->expand_related_widgets($plan['widgets']); |
| 643 |
$kind_pack = $engine->widgets_for_kind($kind); |
| 644 |
if (!empty($kind_pack)) { |
| 645 |
$plan['widgets'] = $engine->expand_related_widgets($kind_pack); |
| 646 |
if (!empty($plan['layout']) && is_array($plan['layout'])) { |
| 647 |
$plan['layout'] = $this->filter_plan_layout_widgets($plan['layout'], $plan['widgets']); |
| 648 |
$plan['layout'] = $this->filter_plan_layout_for_kind($plan['layout'], $kind); |
| 649 |
} |
| 650 |
} |
| 651 |
|
| 652 |
if (!$use_existing_globals) { |
| 653 |
$engine->apply_ai_globals( |
| 654 |
!empty($plan['palette']) ? $plan['palette'] : array(), |
| 655 |
!empty($plan['fonts']) ? $plan['fonts'] : array(), |
| 656 |
true |
| 657 |
); |
| 658 |
} |
| 659 |
|
| 660 |
$system_prompt = $engine->get_build_prompt($plan['widgets'], array( |
| 661 |
'full_page' => $full_page, |
| 662 |
'kind' => $kind, |
| 663 |
'prompt' => $prompt, |
| 664 |
'structure_brief' => $engine->structure_brief($kind, $prompt), |
| 665 |
)); |
| 666 |
$user_message = $engine->get_build_user_message($prompt, $context, $plan); |
| 667 |
|
| 668 |
$result = $this->call_provider( |
| 669 |
$call, |
| 670 |
$system_prompt, |
| 671 |
$user_message, |
| 672 |
array( |
| 673 |
'temperature' => $full_page ? 0.7 : ($engine->should_plan($kind) ? 0.55 : 0.45), |
| 674 |
'max_tokens' => $full_page ? 16384 : 8192, |
| 675 |
'json' => true, |
| 676 |
) |
| 677 |
); |
| 678 |
|
| 679 |
if (is_wp_error($result)) { |
| 680 |
return $result; |
| 681 |
} |
| 682 |
|
| 683 |
$shortcode = ''; |
| 684 |
$layout_globals = $engine->extract_globals_from_json($result); |
| 685 |
if (!$use_existing_globals) { |
| 686 |
$engine->apply_ai_globals( |
| 687 |
!empty($layout_globals['palette']) ? $layout_globals['palette'] : (!empty($plan['palette']) ? $plan['palette'] : array()), |
| 688 |
!empty($layout_globals['fonts']) ? $layout_globals['fonts'] : (!empty($plan['fonts']) ? $plan['fonts'] : array()), |
| 689 |
true |
| 690 |
); |
| 691 |
} |
| 692 |
|
| 693 |
$nodes = $engine->parse_layout($result); |
| 694 |
|
| 695 |
// If user requested specific sections, verify that all sections were created |
| 696 |
if (!empty($requested_sections) && is_array($nodes)) { |
| 697 |
// If the last row is an incomplete stub (e.g. heading only for reviews, but no cards) |
| 698 |
if (count($nodes) > 1) { |
| 699 |
$last_row = end($nodes); |
| 700 |
$last_stats = $engine->layout_stats(array($last_row)); |
| 701 |
if ($last_stats['leaves'] <= 1) { |
| 702 |
$last_summary = $engine->get_row_summary_text($last_row); |
| 703 |
if (preg_match('/(reviews?|testimonials?|why\s*choose|hours?|location|features?|pricing|faq)/i', $last_summary['text'])) { |
| 704 |
array_pop($nodes); |
| 705 |
} |
| 706 |
} |
| 707 |
} |
| 708 |
|
| 709 |
$missing = $engine->find_missing_sections($nodes, $requested_sections); |
| 710 |
if (!empty($missing)) { |
| 711 |
$missing_list = array(); |
| 712 |
foreach ($missing as $m_i => $m_sec) { |
| 713 |
$num = $m_i + 1; |
| 714 |
$desc = !empty($m_sec['desc']) ? ' — ' . $m_sec['desc'] : ''; |
| 715 |
$missing_list[] = "{$num}. [pl_row] \"{$m_sec['name']}\"{$desc}"; |
| 716 |
} |
| 717 |
|
| 718 |
$continuation_prompt = "The initial layout generated earlier sections successfully, but the following user-requested sections are still MISSING:\n\n" |
| 719 |
. implode("\n", $missing_list) . "\n\n" |
| 720 |
. "Generate ONLY the missing sections above as an array of top-level [pl_row] nodes in JSON: {\"nodes\":[...]}.\n" |
| 721 |
. "- Every missing section MUST be its own complete [pl_row] with rich, realistic dummy content and full styling.\n" |
| 722 |
. "- Match the same brand palette and typography.\n" |
| 723 |
. "- For Customer Reviews: use pl_testimonial or pl_review cards with names, quotes, and star ratings.\n" |
| 724 |
. "- For Location & Hours: include address, opening hours, and location/map CTA.\n" |
| 725 |
. "- For Final CTA: include energetic heading, description, and Order Now button.\n" |
| 726 |
. "Return JSON {\"nodes\":[...]} only. Do not repeat sections already created."; |
| 727 |
|
| 728 |
$add_result = $this->call_provider( |
| 729 |
$call, |
| 730 |
$system_prompt, |
| 731 |
$continuation_prompt, |
| 732 |
array( |
| 733 |
'temperature' => 0.5, |
| 734 |
'max_tokens' => 8192, |
| 735 |
'json' => true, |
| 736 |
) |
| 737 |
); |
| 738 |
|
| 739 |
if (!is_wp_error($add_result)) { |
| 740 |
$additional_nodes = $engine->parse_layout($add_result); |
| 741 |
if (!empty($additional_nodes)) { |
| 742 |
$nodes = array_merge((array) $nodes, (array) $additional_nodes); |
| 743 |
} |
| 744 |
} |
| 745 |
} |
| 746 |
} |
| 747 |
|
| 748 |
// If a page came back as a fragment or unstyled sketch, ask the model |
| 749 |
// once more — never inject a canned theme. |
| 750 |
if (($engine->should_plan($kind) || !empty($requested_sections)) && $engine->is_thin_layout($nodes, $kind, $requested_sections)) { |
| 751 |
$retry_msg = $user_message . "\nYour previous JSON was too thin or unstyled for this request. "; |
| 752 |
if (!empty($requested_sections)) { |
| 753 |
$retry_msg .= "You MUST generate ALL " . count($requested_sections) . " requested sections. "; |
| 754 |
} |
| 755 |
$retry_msg .= "Design a complete, visually finished Pagelayer theme for this brand. "; |
| 756 |
$retry_msg .= "Invent the structure from the product. Set real widget settings from the schemas (colors, typography, backgrounds with ele_bg_type, padding, radius, shadow, icons, images). JSON only."; |
| 757 |
$retry = $this->call_provider( |
| 758 |
$call, |
| 759 |
$system_prompt, |
| 760 |
$retry_msg, |
| 761 |
array( |
| 762 |
'temperature' => 0.75, |
| 763 |
'max_tokens' => $full_page ? 16384 : 8192, |
| 764 |
'json' => true, |
| 765 |
) |
| 766 |
); |
| 767 |
if (!is_wp_error($retry)) { |
| 768 |
$retry_globals = $engine->extract_globals_from_json($retry); |
| 769 |
if (!$use_existing_globals) { |
| 770 |
$engine->apply_ai_globals( |
| 771 |
!empty($retry_globals['palette']) ? $retry_globals['palette'] : array(), |
| 772 |
!empty($retry_globals['fonts']) ? $retry_globals['fonts'] : array(), |
| 773 |
true |
| 774 |
); |
| 775 |
if (!empty($retry_globals['palette']) || !empty($retry_globals['fonts'])) { |
| 776 |
$layout_globals = $retry_globals; |
| 777 |
} |
| 778 |
} |
| 779 |
$retry_nodes = $engine->parse_layout($retry); |
| 780 |
if (!$engine->is_thin_layout($retry_nodes, $kind, $requested_sections)) { |
| 781 |
$nodes = $retry_nodes; |
| 782 |
$result = $retry; |
| 783 |
} elseif (!empty($retry_nodes)) { |
| 784 |
$nodes = $retry_nodes; |
| 785 |
} |
| 786 |
} |
| 787 |
} |
| 788 |
|
| 789 |
if (!empty($nodes)) { |
| 790 |
$nodes = $engine->finalize_nodes($nodes, $prompt); |
| 791 |
$shortcode = $engine->nodes_to_shortcode($nodes); |
| 792 |
} |
| 793 |
|
| 794 |
if (empty($shortcode)) { |
| 795 |
$shortcode = $this->sanitize_ai_shortcode($result); |
| 796 |
} |
| 797 |
|
| 798 |
$shortcode = $this->ensure_dark_band_contrast($shortcode); |
| 799 |
if (!in_array($kind, array('header', 'footer', '404'), true)) { |
| 800 |
$shortcode = $this->ensure_row_section_padding($shortcode); |
| 801 |
} |
| 802 |
$shortcode = $this->ensure_iconbox_center($shortcode); |
| 803 |
$shortcode = $this->ensure_slider_image_size($shortcode); |
| 804 |
$shortcode = $this->ensure_social_contrast($shortcode); |
| 805 |
$shortcode = $this->ensure_accordion_contrast($shortcode); |
| 806 |
$shortcode = $this->ensure_counter_start($shortcode); |
| 807 |
|
| 808 |
if (empty($shortcode)) { |
| 809 |
return new WP_Error('empty_generation', __('AI generated an empty response. Please try with a more detailed prompt.', 'pagelayer'), array('status' => 500)); |
| 810 |
} |
| 811 |
|
| 812 |
$theme_template = null; |
| 813 |
if ($engine->is_theme_template_kind($kind) && !empty($nodes)) { |
| 814 |
$theme_template = $this->save_theme_template($kind, $prompt, $nodes, $post_id); |
| 815 |
} |
| 816 |
|
| 817 |
$editing_matching_template = false; |
| 818 |
if ($post_id && get_post_type($post_id) === 'pagelayer-template') { |
| 819 |
$current_type = (string) get_post_meta($post_id, 'pagelayer_template_type', true); |
| 820 |
$spec = $engine->theme_template_spec($kind, $prompt); |
| 821 |
$editing_matching_template = ($spec && $current_type === $spec['type']); |
| 822 |
} |
| 823 |
|
| 824 |
// Header/footer are Theme Builder chrome — do not dump them as extra |
| 825 |
// sections onto a regular page. 404 and inner templates still preview |
| 826 |
// on the current canvas so the user can see the design. |
| 827 |
$insert_on_canvas = true; |
| 828 |
if (in_array($kind, array('header', 'footer'), true) && !$editing_matching_template) { |
| 829 |
$insert_on_canvas = false; |
| 830 |
} |
| 831 |
|
| 832 |
if ($post_id && $insert_on_canvas && ($full_page || $kind === 'home')) { |
| 833 |
$this->assign_canvas_template($post_id); |
| 834 |
} |
| 835 |
|
| 836 |
if ($post_id && $kind && $insert_on_canvas) { |
| 837 |
update_post_meta($post_id, 'pagelayer_ai_layout_kind', $kind); |
| 838 |
} |
| 839 |
if (is_array($theme_template) && !empty($theme_template['template_id']) && $kind) { |
| 840 |
update_post_meta((int) $theme_template['template_id'], 'pagelayer_ai_layout_kind', $kind); |
| 841 |
} |
| 842 |
|
| 843 |
$render_id = ($theme_template && !empty($theme_template['template_id'])) ? (int) $theme_template['template_id'] : $post_id; |
| 844 |
$rendered_html = $this->render_shortcodes_for_canvas($shortcode, $render_id ? $render_id : $post_id); |
| 845 |
$tree = $this->parse_shortcodes_to_tree($shortcode); |
| 846 |
$globals = $engine->get_globals_snapshot(); |
| 847 |
|
| 848 |
$message = __('Layout generated successfully.', 'pagelayer'); |
| 849 |
if (is_array($theme_template) && !empty($theme_template['template_id'])) { |
| 850 |
$labels = array( |
| 851 |
'header' => __('Header theme template created.', 'pagelayer'), |
| 852 |
'footer' => __('Footer theme template created.', 'pagelayer'), |
| 853 |
'404' => __('404 theme template created.', 'pagelayer'), |
| 854 |
'blog' => __('Blog archive theme template created.', 'pagelayer'), |
| 855 |
'single' => __('Single post theme template created.', 'pagelayer'), |
| 856 |
'search' => __('Search results theme template created.', 'pagelayer'), |
| 857 |
); |
| 858 |
$message = isset($labels[$kind]) ? $labels[$kind] : $message; |
| 859 |
} |
| 860 |
|
| 861 |
return rest_ensure_response(array( |
| 862 |
'success' => true, |
| 863 |
'shortcode' => $shortcode, |
| 864 |
'html' => $rendered_html, |
| 865 |
'tree' => $tree, |
| 866 |
'provider' => $provider, |
| 867 |
'model' => $model, |
| 868 |
'widgets' => $plan['widgets'], |
| 869 |
'plan' => $plan, |
| 870 |
'globals' => $globals, |
| 871 |
'globals_updated' => ($globals_mode === 'new'), |
| 872 |
'globals_mode' => $globals_mode, |
| 873 |
'page_template' => 'pagelayer-canvas', |
| 874 |
'kind' => $kind, |
| 875 |
'theme_template' => $theme_template, |
| 876 |
'insert_on_canvas' => $insert_on_canvas, |
| 877 |
'message' => $message, |
| 878 |
)); |
| 879 |
} |
| 880 |
|
| 881 |
/** |
| 882 |
* Persist a generated layout as a Pagelayer Theme Builder template. |
| 883 |
* |
| 884 |
* @return array|null |
| 885 |
*/ |
| 886 |
private function save_theme_template($kind, $prompt, $nodes, $post_id = 0) { |
| 887 |
$engine = Pagelayer_AI_Layout_Engine::instance(); |
| 888 |
$spec = $engine->theme_template_spec($kind, $prompt); |
| 889 |
if (!$spec || empty($nodes) || !is_array($nodes)) { |
| 890 |
return null; |
| 891 |
} |
| 892 |
|
| 893 |
$existing_id = $this->find_theme_template_id($spec, $post_id); |
| 894 |
|
| 895 |
if (class_exists('Pagelayer_Abilities_Register')) { |
| 896 |
$input = array( |
| 897 |
'title' => $spec['title'], |
| 898 |
'type' => $spec['type'], |
| 899 |
'pagelayer_data' => $nodes, |
| 900 |
'conditions' => $spec['conditions'], |
| 901 |
'skip_validation' => true, |
| 902 |
); |
| 903 |
if ($kind === 'header') { |
| 904 |
$input['single_page_site'] = $this->header_nodes_need_menu_opt_out($nodes); |
| 905 |
} |
| 906 |
|
| 907 |
if ($existing_id) { |
| 908 |
$input['template_id'] = $existing_id; |
| 909 |
$res = Pagelayer_Abilities_Register::execute_update_template($input); |
| 910 |
} else { |
| 911 |
$res = Pagelayer_Abilities_Register::execute_create_template($input); |
| 912 |
} |
| 913 |
|
| 914 |
if (!is_wp_error($res) && !empty($res['template_id'])) { |
| 915 |
$tid = (int) $res['template_id']; |
| 916 |
return array( |
| 917 |
'template_id' => $tid, |
| 918 |
'type' => $spec['type'], |
| 919 |
'kind' => $kind, |
| 920 |
'title' => $spec['title'], |
| 921 |
'edit_url' => $this->theme_template_edit_url($tid), |
| 922 |
'updated' => (bool) $existing_id, |
| 923 |
); |
| 924 |
} |
| 925 |
} |
| 926 |
|
| 927 |
$tid = $this->save_theme_template_direct($spec, $nodes, $existing_id); |
| 928 |
if (!$tid) { |
| 929 |
return null; |
| 930 |
} |
| 931 |
|
| 932 |
return array( |
| 933 |
'template_id' => $tid, |
| 934 |
'type' => $spec['type'], |
| 935 |
'kind' => $kind, |
| 936 |
'title' => $spec['title'], |
| 937 |
'edit_url' => $this->theme_template_edit_url($tid), |
| 938 |
'updated' => (bool) $existing_id, |
| 939 |
); |
| 940 |
} |
| 941 |
|
| 942 |
private function header_nodes_need_menu_opt_out($nodes) { |
| 943 |
$has_bound_menu = false; |
| 944 |
$walk = function ($list) use (&$walk, &$has_bound_menu) { |
| 945 |
foreach ((array) $list as $node) { |
| 946 |
if (!is_array($node) || empty($node['tag'])) { |
| 947 |
continue; |
| 948 |
} |
| 949 |
$tag = str_replace('pagelayer_', 'pl_', (string) $node['tag']); |
| 950 |
if ($tag === 'pl_wp_menu') { |
| 951 |
$nav = isset($node['attrs']['nav_list']) ? trim((string) $node['attrs']['nav_list']) : ''; |
| 952 |
if ($nav !== '' && $nav !== '0') { |
| 953 |
$has_bound_menu = true; |
| 954 |
return; |
| 955 |
} |
| 956 |
} |
| 957 |
if (isset($node['content']) && is_array($node['content'])) { |
| 958 |
$walk($node['content']); |
| 959 |
} |
| 960 |
} |
| 961 |
}; |
| 962 |
$walk($nodes); |
| 963 |
return !$has_bound_menu; |
| 964 |
} |
| 965 |
|
| 966 |
private function find_theme_template_id($spec, $post_id = 0) { |
| 967 |
if ($post_id && get_post_type($post_id) === 'pagelayer-template') { |
| 968 |
$current = (string) get_post_meta($post_id, 'pagelayer_template_type', true); |
| 969 |
if ($current === $spec['type']) { |
| 970 |
return (int) $post_id; |
| 971 |
} |
| 972 |
} |
| 973 |
|
| 974 |
$posts = get_posts(array( |
| 975 |
'post_type' => 'pagelayer-template', |
| 976 |
'post_status' => array('publish', 'draft'), |
| 977 |
'posts_per_page' => 20, |
| 978 |
'meta_key' => 'pagelayer_template_type', |
| 979 |
'meta_value' => $spec['type'], |
| 980 |
'fields' => 'ids', |
| 981 |
)); |
| 982 |
if (empty($posts)) { |
| 983 |
return 0; |
| 984 |
} |
| 985 |
|
| 986 |
$want_sub = ''; |
| 987 |
if (!empty($spec['conditions'][0]['sub_template'])) { |
| 988 |
$want_sub = (string) $spec['conditions'][0]['sub_template']; |
| 989 |
} |
| 990 |
|
| 991 |
if ($want_sub === '') { |
| 992 |
return (int) $posts[0]; |
| 993 |
} |
| 994 |
|
| 995 |
foreach ($posts as $id) { |
| 996 |
$conds = get_post_meta($id, 'pagelayer_template_conditions', true); |
| 997 |
if (!is_array($conds)) { |
| 998 |
continue; |
| 999 |
} |
| 1000 |
foreach ($conds as $c) { |
| 1001 |
if (!empty($c['sub_template']) && (string) $c['sub_template'] === $want_sub) { |
| 1002 |
return (int) $id; |
| 1003 |
} |
| 1004 |
} |
| 1005 |
} |
| 1006 |
|
| 1007 |
return 0; |
| 1008 |
} |
| 1009 |
|
| 1010 |
private function save_theme_template_direct($spec, $nodes, $existing_id = 0) { |
| 1011 |
$template_id = (int) $existing_id; |
| 1012 |
if ($template_id <= 0) { |
| 1013 |
$template_id = wp_insert_post(array( |
| 1014 |
'post_title' => $spec['title'], |
| 1015 |
'post_type' => 'pagelayer-template', |
| 1016 |
'post_status' => 'publish', |
| 1017 |
), true); |
| 1018 |
if (is_wp_error($template_id) || !$template_id) { |
| 1019 |
return 0; |
| 1020 |
} |
| 1021 |
} else { |
| 1022 |
wp_update_post(array( |
| 1023 |
'ID' => $template_id, |
| 1024 |
'post_title' => $spec['title'], |
| 1025 |
)); |
| 1026 |
} |
| 1027 |
|
| 1028 |
$normalized = $nodes; |
| 1029 |
if (class_exists('Pagelayer_Abilities_Register') && method_exists('Pagelayer_Abilities_Register', 'normalize_layout_data')) { |
| 1030 |
$normalized = Pagelayer_Abilities_Register::normalize_layout_data($nodes); |
| 1031 |
} |
| 1032 |
|
| 1033 |
update_post_meta($template_id, 'pagelayer-data', $normalized); |
| 1034 |
update_post_meta($template_id, 'pagelayer_template_type', $spec['type']); |
| 1035 |
update_post_meta($template_id, 'pagelayer_template_conditions', $spec['conditions']); |
| 1036 |
|
| 1037 |
if (class_exists('Pagelayer_Abilities_Register') && method_exists('Pagelayer_Abilities_Register', 'serialize_layout_to_blocks')) { |
| 1038 |
$blocks = Pagelayer_Abilities_Register::serialize_layout_to_blocks($normalized); |
| 1039 |
wp_update_post(array('ID' => $template_id, 'post_content' => $blocks)); |
| 1040 |
} |
| 1041 |
|
| 1042 |
return (int) $template_id; |
| 1043 |
} |
| 1044 |
|
| 1045 |
private function theme_template_edit_url($template_id) { |
| 1046 |
$template_id = (int) $template_id; |
| 1047 |
if ($template_id <= 0) { |
| 1048 |
return ''; |
| 1049 |
} |
| 1050 |
if (function_exists('pagelayer_livelink')) { |
| 1051 |
return pagelayer_livelink($template_id); |
| 1052 |
} |
| 1053 |
return admin_url('post.php?post=' . $template_id . '&action=edit'); |
| 1054 |
} |
| 1055 |
|
| 1056 |
private function filter_plan_layout_for_kind($layout, $kind) { |
| 1057 |
if (!is_array($layout) || $layout === array()) { |
| 1058 |
return $layout; |
| 1059 |
} |
| 1060 |
$drop = '/(feature|service|testimonial|review|pricing|hero|cta|faq|counter|stats|team|gallery)/i'; |
| 1061 |
if ($kind === 'footer') { |
| 1062 |
$keep = array(); |
| 1063 |
foreach ($layout as $block) { |
| 1064 |
$name = is_array($block) && !empty($block['name']) ? (string) $block['name'] : ''; |
| 1065 |
if ($name !== '' && preg_match($drop, $name) && !preg_match('/(footer|copyright|link|contact|social|brand|about)/i', $name)) { |
| 1066 |
continue; |
| 1067 |
} |
| 1068 |
$keep[] = $block; |
| 1069 |
} |
| 1070 |
return $keep; |
| 1071 |
} |
| 1072 |
if ($kind === 'header') { |
| 1073 |
return array_slice($layout, 0, 1); |
| 1074 |
} |
| 1075 |
if ($kind === '404') { |
| 1076 |
$keep = array(); |
| 1077 |
foreach ($layout as $block) { |
| 1078 |
$name = is_array($block) && !empty($block['name']) ? (string) $block['name'] : ''; |
| 1079 |
if ($name !== '' && preg_match($drop, $name) && !preg_match('/(404|error|not\s*found|search)/i', $name)) { |
| 1080 |
continue; |
| 1081 |
} |
| 1082 |
$keep[] = $block; |
| 1083 |
} |
| 1084 |
return $keep; |
| 1085 |
} |
| 1086 |
return $layout; |
| 1087 |
} |
| 1088 |
|
| 1089 |
private function filter_plan_layout_widgets($layout, $allowed) { |
| 1090 |
if (!is_array($layout) || empty($allowed) || !is_array($allowed)) { |
| 1091 |
return $layout; |
| 1092 |
} |
| 1093 |
$allow = array_flip($allowed); |
| 1094 |
foreach ($layout as &$block) { |
| 1095 |
if (!is_array($block) || empty($block['widgets']) || !is_array($block['widgets'])) { |
| 1096 |
continue; |
| 1097 |
} |
| 1098 |
$keep = array(); |
| 1099 |
foreach ($block['widgets'] as $tag) { |
| 1100 |
$tag = is_string($tag) ? $tag : ''; |
| 1101 |
if ($tag !== '' && isset($allow[$tag])) { |
| 1102 |
$keep[] = $tag; |
| 1103 |
} |
| 1104 |
} |
| 1105 |
$block['widgets'] = $keep; |
| 1106 |
} |
| 1107 |
unset($block); |
| 1108 |
return $layout; |
| 1109 |
} |
| 1110 |
|
| 1111 |
private function call_provider($call, $system_prompt, $user_message, $opts = array()) { |
| 1112 |
$provider = $call['provider']; |
| 1113 |
$api_key = $call['api_key']; |
| 1114 |
$model = $call['model']; |
| 1115 |
$settings = $call['settings']; |
| 1116 |
$params = $call['params']; |
| 1117 |
|
| 1118 |
if ($provider === 'deepseek') { |
| 1119 |
return $this->call_deepseek($api_key, $model, $system_prompt, $user_message, $opts); |
| 1120 |
} |
| 1121 |
if ($provider === 'gemini') { |
| 1122 |
return $this->call_gemini($api_key, $model, $system_prompt, $user_message, $opts); |
| 1123 |
} |
| 1124 |
if ($provider === 'openrouter') { |
| 1125 |
return $this->call_openrouter($api_key, $model, $system_prompt, $user_message, $opts); |
| 1126 |
} |
| 1127 |
if ($provider === 'openai') { |
| 1128 |
return $this->call_openai($api_key, $model, $system_prompt, $user_message, $opts); |
| 1129 |
} |
| 1130 |
if ($provider === 'anthropic') { |
| 1131 |
return $this->call_anthropic($api_key, $model, $system_prompt, $user_message, $opts); |
| 1132 |
} |
| 1133 |
if ($provider === 'custom') { |
| 1134 |
$endpoint = !empty($params['custom_endpoint']) ? $params['custom_endpoint'] : $settings['custom_endpoint']; |
| 1135 |
$custom_m = !empty($params['custom_model']) ? $params['custom_model'] : (!empty($settings['custom_model']) ? $settings['custom_model'] : $model); |
| 1136 |
return $this->call_custom_endpoint($endpoint, $api_key, $custom_m, $system_prompt, $user_message, $opts); |
| 1137 |
} |
| 1138 |
|
| 1139 |
return new WP_Error('invalid_provider', __('Unsupported AI provider selected.', 'pagelayer'), array('status' => 400)); |
| 1140 |
} |
| 1141 |
|
| 1142 |
/** |
| 1143 |
* Parse shortcode structure into component tree for fast live step-by-step building |
| 1144 |
*/ |
| 1145 |
public function parse_shortcodes_to_tree($shortcode) { |
| 1146 |
$tree = array(); |
| 1147 |
$shortcode = $this->sanitize_ai_shortcode($shortcode); |
| 1148 |
$catalog = Pagelayer_AI_Layout_Engine::instance()->get_widget_catalog(); |
| 1149 |
|
| 1150 |
// Match rows |
| 1151 |
if (preg_match_all('/\[pl_row([^\]]*)\](.*?)\[\/pl_row\]/is', $shortcode, $row_matches, PREG_SET_ORDER)) { |
| 1152 |
foreach ($row_matches as $r_idx => $r_match) { |
| 1153 |
$row_atts_raw = trim($r_match[1]); |
| 1154 |
$row_atts = shortcode_parse_atts($row_atts_raw); |
| 1155 |
if (!is_array($row_atts)) $row_atts = array(); |
| 1156 |
|
| 1157 |
$row_content = $r_match[2]; |
| 1158 |
$row_node = array( |
| 1159 |
'id' => 'row_' . $r_idx, |
| 1160 |
'type' => 'pl_row', |
| 1161 |
'tag' => 'pl_row', |
| 1162 |
'name' => 'Section Row', |
| 1163 |
'atts' => $row_atts, |
| 1164 |
'cols' => array(), |
| 1165 |
); |
| 1166 |
|
| 1167 |
// Match columns inside row |
| 1168 |
if (preg_match_all('/\[pl_col([^\]]*)\](.*?)\[\/pl_col\]/is', $row_content, $col_matches, PREG_SET_ORDER)) { |
| 1169 |
foreach ($col_matches as $c_idx => $c_match) { |
| 1170 |
$col_atts_raw = trim($c_match[1]); |
| 1171 |
$col_atts = shortcode_parse_atts($col_atts_raw); |
| 1172 |
if (!is_array($col_atts)) $col_atts = array(); |
| 1173 |
|
| 1174 |
$col_content = $c_match[2]; |
| 1175 |
$col_width_label = !empty($col_atts['col']) ? $col_atts['col'] . '/12' : 'Full'; |
| 1176 |
$col_node = array( |
| 1177 |
'id' => 'col_' . $r_idx . '_' . $c_idx, |
| 1178 |
'type' => 'pl_col', |
| 1179 |
'tag' => 'pl_col', |
| 1180 |
'name' => 'Column (' . $col_width_label . ')', |
| 1181 |
'atts' => $col_atts, |
| 1182 |
'widgets' => array(), |
| 1183 |
); |
| 1184 |
|
| 1185 |
// Match widgets inside column |
| 1186 |
$widget_pattern = '/\[(pl_[a-zA-Z0-9_-]+)([^\]]*)(?:\](.*?)\[\/\1\]|\s*\/\]|\])/is'; |
| 1187 |
if (preg_match_all($widget_pattern, $col_content, $widget_matches, PREG_SET_ORDER)) { |
| 1188 |
foreach ($widget_matches as $w_idx => $w_match) { |
| 1189 |
$w_tag = $w_match[1]; |
| 1190 |
$w_atts_raw = trim($w_match[2]); |
| 1191 |
$w_atts = shortcode_parse_atts($w_atts_raw); |
| 1192 |
if (!is_array($w_atts)) $w_atts = array(); |
| 1193 |
$w_inner = isset($w_match[3]) ? trim($w_match[3]) : ''; |
| 1194 |
|
| 1195 |
// Individual widget shortcode and HTML |
| 1196 |
$single_sc = '[' . $w_tag . ($w_atts_raw ? ' ' . $w_atts_raw : '') . ']' . ($w_inner ? $w_inner . '[/' . $w_tag . ']' : ''); |
| 1197 |
$single_html = $this->render_shortcodes_for_canvas($single_sc); |
| 1198 |
|
| 1199 |
$w_name = ucfirst(str_replace('_', ' ', str_replace('pl_', '', $w_tag))); |
| 1200 |
if (isset($catalog[$w_tag])) { |
| 1201 |
$w_name = explode('|', $catalog[$w_tag])[0]; |
| 1202 |
} |
| 1203 |
|
| 1204 |
$col_node['widgets'][] = array( |
| 1205 |
'id' => 'w_' . $r_idx . '_' . $c_idx . '_' . $w_idx, |
| 1206 |
'type' => $w_tag, |
| 1207 |
'tag' => $w_tag, |
| 1208 |
'name' => $w_name, |
| 1209 |
'atts' => $w_atts, |
| 1210 |
'inner' => $w_inner, |
| 1211 |
'shortcode' => $single_sc, |
| 1212 |
'html' => $single_html, |
| 1213 |
); |
| 1214 |
} |
| 1215 |
} |
| 1216 |
|
| 1217 |
$row_node['cols'][] = $col_node; |
| 1218 |
} |
| 1219 |
} |
| 1220 |
|
| 1221 |
$tree[] = $row_node; |
| 1222 |
} |
| 1223 |
} |
| 1224 |
|
| 1225 |
return $tree; |
| 1226 |
} |
| 1227 |
|
| 1228 |
/** |
| 1229 |
* Call Google Gemini API |
| 1230 |
*/ |
| 1231 |
private function call_gemini($api_key, $model, $system_prompt, $user_prompt, $opts = array()) { |
| 1232 |
if (empty($model) || $model === 'gemini-2.5-flash' || $model === 'gemini-2.5-pro' || strpos($model, 'gemini-2.5') !== false) { |
| 1233 |
$model = 'gemini-3.7-flash'; |
| 1234 |
} |
| 1235 |
$url = 'https://generativelanguage.googleapis.com/v1beta/models/' . urlencode($model) . ':generateContent?key=' . urlencode($api_key); |
| 1236 |
|
| 1237 |
$payload = array( |
| 1238 |
'systemInstruction' => array( |
| 1239 |
'parts' => array( |
| 1240 |
array('text' => $system_prompt) |
| 1241 |
) |
| 1242 |
), |
| 1243 |
'contents' => array( |
| 1244 |
array( |
| 1245 |
'role' => 'user', |
| 1246 |
'parts' => array( |
| 1247 |
array('text' => $user_prompt) |
| 1248 |
) |
| 1249 |
) |
| 1250 |
), |
| 1251 |
'generationConfig' => array( |
| 1252 |
'temperature' => isset($opts['temperature']) ? floatval($opts['temperature']) : 0.4, |
| 1253 |
'maxOutputTokens' => !empty($opts['max_tokens']) ? intval($opts['max_tokens']) : 8192, |
| 1254 |
) |
| 1255 |
); |
| 1256 |
if (!empty($opts['json'])) { |
| 1257 |
$payload['generationConfig']['responseMimeType'] = 'application/json'; |
| 1258 |
} |
| 1259 |
|
| 1260 |
$response = wp_remote_post($url, array( |
| 1261 |
'timeout' => 120, |
| 1262 |
'headers' => array( |
| 1263 |
'Content-Type' => 'application/json', |
| 1264 |
), |
| 1265 |
'body' => wp_json_encode($payload), |
| 1266 |
)); |
| 1267 |
|
| 1268 |
if (is_wp_error($response)) { |
| 1269 |
return new WP_Error('gemini_request_failed', $response->get_error_message(), array('status' => 500)); |
| 1270 |
} |
| 1271 |
|
| 1272 |
$code = wp_remote_retrieve_response_code($response); |
| 1273 |
$body = wp_remote_retrieve_body($response); |
| 1274 |
$data = json_decode($body, true); |
| 1275 |
|
| 1276 |
if ($code !== 200) { |
| 1277 |
$err_msg = !empty($data['error']['message']) ? $data['error']['message'] : sprintf(__('Gemini API returned error code %d', 'pagelayer'), $code); |
| 1278 |
return new WP_Error('gemini_api_error', $err_msg, array('status' => $code)); |
| 1279 |
} |
| 1280 |
|
| 1281 |
if (!empty($data['candidates'][0]['content']['parts']) && is_array($data['candidates'][0]['content']['parts'])) { |
| 1282 |
$text = ''; |
| 1283 |
foreach ($data['candidates'][0]['content']['parts'] as $part) { |
| 1284 |
if (!empty($part['text'])) { |
| 1285 |
$text .= $part['text']; |
| 1286 |
} |
| 1287 |
} |
| 1288 |
if ($text !== '') { |
| 1289 |
return $text; |
| 1290 |
} |
| 1291 |
} |
| 1292 |
|
| 1293 |
return new WP_Error('gemini_empty_response', __('Gemini returned an empty candidate list.', 'pagelayer'), array('status' => 500)); |
| 1294 |
} |
| 1295 |
|
| 1296 |
/** |
| 1297 |
* Call OpenAI API |
| 1298 |
*/ |
| 1299 |
private function call_openai($api_key, $model, $system_prompt, $user_prompt, $opts = array()) { |
| 1300 |
if (empty($model)) $model = 'gpt-4o-mini'; |
| 1301 |
$url = 'https://api.openai.com/v1/chat/completions'; |
| 1302 |
|
| 1303 |
$payload = array( |
| 1304 |
'model' => $model, |
| 1305 |
'messages' => array( |
| 1306 |
array('role' => 'system', 'content' => $system_prompt), |
| 1307 |
array('role' => 'user', 'content' => $user_prompt), |
| 1308 |
), |
| 1309 |
'temperature' => isset($opts['temperature']) ? floatval($opts['temperature']) : 0.4, |
| 1310 |
'max_tokens' => !empty($opts['max_tokens']) ? intval($opts['max_tokens']) : 16384, |
| 1311 |
); |
| 1312 |
if (preg_match('/^(o1|o3|gpt-4o|chatgpt)/i', $model)) { |
| 1313 |
$payload['max_completion_tokens'] = !empty($opts['max_tokens']) ? intval($opts['max_tokens']) : 16384; |
| 1314 |
if (strpos($model, 'o1') === 0 || strpos($model, 'o3') === 0) { |
| 1315 |
unset($payload['max_tokens']); |
| 1316 |
unset($payload['temperature']); |
| 1317 |
} |
| 1318 |
} |
| 1319 |
if (!empty($opts['json'])) { |
| 1320 |
$payload['response_format'] = array('type' => 'json_object'); |
| 1321 |
} |
| 1322 |
|
| 1323 |
$response = wp_remote_post($url, array( |
| 1324 |
'timeout' => 120, |
| 1325 |
'headers' => array( |
| 1326 |
'Authorization' => 'Bearer ' . $api_key, |
| 1327 |
'Content-Type' => 'application/json', |
| 1328 |
), |
| 1329 |
'body' => wp_json_encode($payload), |
| 1330 |
)); |
| 1331 |
|
| 1332 |
if (is_wp_error($response)) { |
| 1333 |
return new WP_Error('openai_request_failed', $response->get_error_message(), array('status' => 500)); |
| 1334 |
} |
| 1335 |
|
| 1336 |
$code = wp_remote_retrieve_response_code($response); |
| 1337 |
$body = wp_remote_retrieve_body($response); |
| 1338 |
$data = json_decode($body, true); |
| 1339 |
|
| 1340 |
if ($code !== 200) { |
| 1341 |
$err_msg = !empty($data['error']['message']) ? $data['error']['message'] : sprintf(__('OpenAI API returned error code %d', 'pagelayer'), $code); |
| 1342 |
return new WP_Error('openai_api_error', $err_msg, array('status' => $code)); |
| 1343 |
} |
| 1344 |
|
| 1345 |
if (!empty($data['choices'][0]['message']['content'])) { |
| 1346 |
return $data['choices'][0]['message']['content']; |
| 1347 |
} |
| 1348 |
|
| 1349 |
return new WP_Error('openai_empty_response', __('OpenAI returned an empty response.', 'pagelayer'), array('status' => 500)); |
| 1350 |
} |
| 1351 |
|
| 1352 |
/** |
| 1353 |
* Call Anthropic Claude API |
| 1354 |
*/ |
| 1355 |
private function call_anthropic($api_key, $model, $system_prompt, $user_prompt, $opts = array()) { |
| 1356 |
if (empty($model)) $model = 'claude-3-7-sonnet-20250219'; |
| 1357 |
$url = 'https://api.anthropic.com/v1/messages'; |
| 1358 |
|
| 1359 |
$payload = array( |
| 1360 |
'model' => $model, |
| 1361 |
'system' => $system_prompt, |
| 1362 |
'messages' => array( |
| 1363 |
array('role' => 'user', 'content' => $user_prompt) |
| 1364 |
), |
| 1365 |
'max_tokens' => !empty($opts['max_tokens']) ? intval($opts['max_tokens']) : 8192, |
| 1366 |
'temperature'=> isset($opts['temperature']) ? floatval($opts['temperature']) : 0.4, |
| 1367 |
); |
| 1368 |
|
| 1369 |
$response = wp_remote_post($url, array( |
| 1370 |
'timeout' => 120, |
| 1371 |
'connect_timeout' => 30, |
| 1372 |
'headers' => array( |
| 1373 |
'x-api-key' => $api_key, |
| 1374 |
'anthropic-version' => '2023-06-01', |
| 1375 |
'Content-Type' => 'application/json', |
| 1376 |
), |
| 1377 |
'body' => wp_json_encode($payload), |
| 1378 |
)); |
| 1379 |
|
| 1380 |
if (is_wp_error($response)) { |
| 1381 |
return new WP_Error('anthropic_request_failed', $response->get_error_message(), array('status' => 500)); |
| 1382 |
} |
| 1383 |
|
| 1384 |
$code = wp_remote_retrieve_response_code($response); |
| 1385 |
$body = wp_remote_retrieve_body($response); |
| 1386 |
$data = json_decode($body, true); |
| 1387 |
|
| 1388 |
if ($code !== 200) { |
| 1389 |
$err_msg = !empty($data['error']['message']) ? $data['error']['message'] : sprintf(__('Anthropic API returned error code %d', 'pagelayer'), $code); |
| 1390 |
return new WP_Error('anthropic_api_error', $err_msg, array('status' => $code)); |
| 1391 |
} |
| 1392 |
|
| 1393 |
if (!empty($data['content'][0]['text'])) { |
| 1394 |
return $data['content'][0]['text']; |
| 1395 |
} |
| 1396 |
|
| 1397 |
return new WP_Error('anthropic_empty_response', __('Anthropic returned an empty response.', 'pagelayer'), array('status' => 500)); |
| 1398 |
} |
| 1399 |
|
| 1400 |
/** |
| 1401 |
* Call DeepSeek Official API (deepseek-chat / deepseek-reasoner) |
| 1402 |
*/ |
| 1403 |
private function call_deepseek($api_key, $model, $system_prompt, $user_prompt, $opts = array()) { |
| 1404 |
$model = trim($model); |
| 1405 |
if (strpos($model, '/') !== false) { |
| 1406 |
$model = basename($model); |
| 1407 |
} |
| 1408 |
if (empty($model) || ($model !== 'deepseek-reasoner' && $model !== 'deepseek-chat')) { |
| 1409 |
if (stripos($model, 'reason') !== false || stripos($model, 'r1') !== false) { |
| 1410 |
$model = 'deepseek-reasoner'; |
| 1411 |
} else { |
| 1412 |
$model = 'deepseek-chat'; |
| 1413 |
} |
| 1414 |
} |
| 1415 |
|
| 1416 |
$url = 'https://api.deepseek.com/chat/completions'; |
| 1417 |
$api_key = trim($api_key); |
| 1418 |
|
| 1419 |
$payload = array( |
| 1420 |
'model' => $model, |
| 1421 |
'messages' => array( |
| 1422 |
array('role' => 'system', 'content' => $system_prompt), |
| 1423 |
array('role' => 'user', 'content' => $user_prompt), |
| 1424 |
), |
| 1425 |
'temperature' => isset($opts['temperature']) ? floatval($opts['temperature']) : 0.4, |
| 1426 |
'max_tokens' => !empty($opts['max_tokens']) ? intval($opts['max_tokens']) : 8192, |
| 1427 |
); |
| 1428 |
|
| 1429 |
$response = wp_remote_post($url, array( |
| 1430 |
'timeout' => 120, |
| 1431 |
'sslverify' => false, |
| 1432 |
'headers' => array( |
| 1433 |
'Authorization' => 'Bearer ' . $api_key, |
| 1434 |
'Content-Type' => 'application/json', |
| 1435 |
'Accept' => 'application/json', |
| 1436 |
), |
| 1437 |
'body' => wp_json_encode($payload), |
| 1438 |
)); |
| 1439 |
|
| 1440 |
if (is_wp_error($response)) { |
| 1441 |
return new WP_Error('deepseek_request_failed', $response->get_error_message(), array('status' => 500)); |
| 1442 |
} |
| 1443 |
|
| 1444 |
$code = wp_remote_retrieve_response_code($response); |
| 1445 |
$body = wp_remote_retrieve_body($response); |
| 1446 |
$data = json_decode($body, true); |
| 1447 |
|
| 1448 |
if ($code !== 200) { |
| 1449 |
$err_msg = !empty($data['error']['message']) ? $data['error']['message'] : sprintf(__('DeepSeek API returned error code %d', 'pagelayer'), $code); |
| 1450 |
if ($code === 402 || stripos($err_msg, 'balance') !== false) { |
| 1451 |
$err_msg = __('DeepSeek API: Insufficient account balance. Please check/top-up your balance at platform.deepseek.com', 'pagelayer'); |
| 1452 |
} elseif ($code === 401 || stripos($err_msg, 'Authentication') !== false || stripos($err_msg, 'invalid key') !== false) { |
| 1453 |
$err_msg = __('DeepSeek API: Authentication failed. Please verify your DeepSeek API key in AI Settings.', 'pagelayer'); |
| 1454 |
} |
| 1455 |
return new WP_Error('deepseek_api_error', $err_msg, array('status' => $code)); |
| 1456 |
} |
| 1457 |
|
| 1458 |
if (!empty($data['choices'][0]['message']['content'])) { |
| 1459 |
return $data['choices'][0]['message']['content']; |
| 1460 |
} |
| 1461 |
|
| 1462 |
return new WP_Error('deepseek_empty_response', __('DeepSeek returned an empty response.', 'pagelayer'), array('status' => 500)); |
| 1463 |
} |
| 1464 |
|
| 1465 |
/** |
| 1466 |
* Call OpenRouter API (Access to ALL models: DeepSeek, Llama, Qwen, Mistral, etc.) |
| 1467 |
*/ |
| 1468 |
private function call_openrouter($api_key, $model, $system_prompt, $user_prompt, $opts = array()) { |
| 1469 |
if (empty($model)) $model = 'deepseek/deepseek-chat'; |
| 1470 |
$url = 'https://openrouter.ai/api/v1/chat/completions'; |
| 1471 |
$api_key = trim($api_key); |
| 1472 |
|
| 1473 |
$payload = array( |
| 1474 |
'model' => $model, |
| 1475 |
'messages' => array( |
| 1476 |
array('role' => 'system', 'content' => $system_prompt), |
| 1477 |
array('role' => 'user', 'content' => $user_prompt), |
| 1478 |
), |
| 1479 |
'temperature' => isset($opts['temperature']) ? floatval($opts['temperature']) : 0.4, |
| 1480 |
'max_tokens' => !empty($opts['max_tokens']) ? intval($opts['max_tokens']) : 8192, |
| 1481 |
); |
| 1482 |
|
| 1483 |
$response = wp_remote_post($url, array( |
| 1484 |
'timeout' => 120, |
| 1485 |
'connect_timeout' => 30, |
| 1486 |
'sslverify' => false, |
| 1487 |
'headers' => array( |
| 1488 |
'Authorization' => 'Bearer ' . $api_key, |
| 1489 |
'Content-Type' => 'application/json', |
| 1490 |
'Accept' => 'application/json', |
| 1491 |
'HTTP-Referer' => site_url(), |
| 1492 |
'X-Title' => 'Pagelayer AI', |
| 1493 |
), |
| 1494 |
'body' => wp_json_encode($payload), |
| 1495 |
)); |
| 1496 |
|
| 1497 |
if (is_wp_error($response)) { |
| 1498 |
return new WP_Error('openrouter_request_failed', $response->get_error_message(), array('status' => 500)); |
| 1499 |
} |
| 1500 |
|
| 1501 |
$code = wp_remote_retrieve_response_code($response); |
| 1502 |
$body = wp_remote_retrieve_body($response); |
| 1503 |
$data = json_decode($body, true); |
| 1504 |
|
| 1505 |
if ($code !== 200) { |
| 1506 |
$err_msg = !empty($data['error']['message']) ? $data['error']['message'] : sprintf(__('OpenRouter API returned error code %d', 'pagelayer'), $code); |
| 1507 |
return new WP_Error('openrouter_api_error', $err_msg, array('status' => $code)); |
| 1508 |
} |
| 1509 |
|
| 1510 |
if (!empty($data['choices'][0]['message']['content'])) { |
| 1511 |
return $data['choices'][0]['message']['content']; |
| 1512 |
} |
| 1513 |
|
| 1514 |
return new WP_Error('openrouter_empty_response', __('OpenRouter returned an empty response.', 'pagelayer'), array('status' => 500)); |
| 1515 |
} |
| 1516 |
|
| 1517 |
/** |
| 1518 |
* Call Custom / OpenAI-Compatible Endpoint (OpenCode, Ollama, LM Studio, Groq, DeepSeek, Together, etc.) |
| 1519 |
*/ |
| 1520 |
private function call_custom_endpoint($endpoint, $api_key, $model, $system_prompt, $user_prompt, $opts = array()) { |
| 1521 |
$endpoint = trim($endpoint); |
| 1522 |
if (empty($endpoint)) { |
| 1523 |
$endpoint = 'https://openrouter.ai/api/v1/chat/completions'; |
| 1524 |
} |
| 1525 |
if (strpos($endpoint, '/chat/completions') === false) { |
| 1526 |
$endpoint = rtrim($endpoint, '/') . '/chat/completions'; |
| 1527 |
} |
| 1528 |
|
| 1529 |
if (empty($model)) $model = 'custom-model'; |
| 1530 |
|
| 1531 |
$payload = array( |
| 1532 |
'model' => $model, |
| 1533 |
'messages' => array( |
| 1534 |
array('role' => 'system', 'content' => $system_prompt), |
| 1535 |
array('role' => 'user', 'content' => $user_prompt), |
| 1536 |
), |
| 1537 |
'temperature' => isset($opts['temperature']) ? floatval($opts['temperature']) : 0.4, |
| 1538 |
'max_tokens' => !empty($opts['max_tokens']) ? intval($opts['max_tokens']) : 8192, |
| 1539 |
); |
| 1540 |
|
| 1541 |
$headers = array( |
| 1542 |
'Content-Type' => 'application/json', |
| 1543 |
); |
| 1544 |
if (!empty($api_key)) { |
| 1545 |
$headers['Authorization'] = 'Bearer ' . trim($api_key); |
| 1546 |
} |
| 1547 |
|
| 1548 |
$response = wp_remote_post($endpoint, array( |
| 1549 |
'timeout' => 120, |
| 1550 |
'sslverify' => false, |
| 1551 |
'headers' => $headers, |
| 1552 |
'body' => wp_json_encode($payload), |
| 1553 |
)); |
| 1554 |
|
| 1555 |
if (is_wp_error($response)) { |
| 1556 |
return new WP_Error('custom_endpoint_failed', $response->get_error_message(), array('status' => 500)); |
| 1557 |
} |
| 1558 |
|
| 1559 |
$code = wp_remote_retrieve_response_code($response); |
| 1560 |
$body = wp_remote_retrieve_body($response); |
| 1561 |
$data = json_decode($body, true); |
| 1562 |
|
| 1563 |
if ($code !== 200) { |
| 1564 |
$err_msg = !empty($data['error']['message']) ? $data['error']['message'] : sprintf(__('Custom API returned error code %d', 'pagelayer'), $code); |
| 1565 |
return new WP_Error('custom_api_error', $err_msg, array('status' => $code)); |
| 1566 |
} |
| 1567 |
|
| 1568 |
if (!empty($data['choices'][0]['message']['content'])) { |
| 1569 |
return $data['choices'][0]['message']['content']; |
| 1570 |
} |
| 1571 |
|
| 1572 |
return new WP_Error('custom_empty_response', __('Custom API returned an empty response.', 'pagelayer'), array('status' => 500)); |
| 1573 |
} |
| 1574 |
|
| 1575 |
/** |
| 1576 |
* Clean, sanitize and normalize shortcodes from LLM response |
| 1577 |
*/ |
| 1578 |
public function sanitize_ai_shortcode($raw_text) { |
| 1579 |
$text = trim($raw_text); |
| 1580 |
|
| 1581 |
// Remove Markdown code block fences |
| 1582 |
$text = preg_replace('/^```[a-zA-Z0-9_-]*\s*/i', '', $text); |
| 1583 |
$text = preg_replace('/\s*```$/i', '', $text); |
| 1584 |
$text = trim($text); |
| 1585 |
|
| 1586 |
// Normalize [pagelayer_*] into [pl_*] to match Pagelayer shortcode registry |
| 1587 |
$text = preg_replace('/\[(\/)?pagelayer_([a-zA-Z0-9_-]+)/', '[$1pl_$2', $text); |
| 1588 |
|
| 1589 |
// If shortcode lacks an outer row, wrap it in a default row & column |
| 1590 |
if (strpos($text, '[pl_row') === false && strpos($text, '[pagelayer_row') === false) { |
| 1591 |
$text = '[pl_row col_gap="20" ele_padding="50px,20px,50px,20px"][pl_col col="12"]' . $text . '[/pl_col][/pl_row]'; |
| 1592 |
} |
| 1593 |
|
| 1594 |
// Remove empty spacer columns. Card grids of 4+ items become sibling |
| 1595 |
// rows of 3 columns (col=4) instead of being squashed into one row. |
| 1596 |
$text = preg_replace_callback('/\[pl_row([^\]]*)\](.*?)\[\/pl_row\]/is', function($row_match) { |
| 1597 |
$row_atts = $row_match[1]; |
| 1598 |
$row_content = $row_match[2]; |
| 1599 |
|
| 1600 |
if (strpos($row_atts, 'col_gap=') === false) { |
| 1601 |
$row_atts .= ' col_gap="20"'; |
| 1602 |
} |
| 1603 |
|
| 1604 |
$card_tags = 'pl_iconbox|pl_service|pl_testimonial|pl_counter|pl_pricing|pl_flipbox|pl_review|pl_author_box'; |
| 1605 |
|
| 1606 |
$emit_row = function ($atts, $cols) { |
| 1607 |
$content = ''; |
| 1608 |
foreach ($cols as $c) { |
| 1609 |
$content .= '[pl_col' . $c['atts'] . ']' . $c['inner'] . '[/pl_col]'; |
| 1610 |
} |
| 1611 |
return '[pl_row' . $atts . ']' . $content . '[/pl_row]'; |
| 1612 |
}; |
| 1613 |
|
| 1614 |
$set_col_width = function ($col, $width) { |
| 1615 |
$atts = preg_replace('/col="[^"]*"/', 'col="' . $width . '"', $col['atts']); |
| 1616 |
if (strpos($atts, 'col="') === false) { |
| 1617 |
$atts = ' col="' . $width . '"' . $atts; |
| 1618 |
} |
| 1619 |
$col['atts'] = $atts; |
| 1620 |
return $col; |
| 1621 |
}; |
| 1622 |
|
| 1623 |
if (preg_match_all('/\[pl_col([^\]]*)\](.*?)\[\/pl_col\]/is', $row_content, $col_matches, PREG_SET_ORDER)) { |
| 1624 |
$non_empty_cols = array(); |
| 1625 |
foreach ($col_matches as $col) { |
| 1626 |
$col_atts = $col[1]; |
| 1627 |
$col_inner = trim($col[2]); |
| 1628 |
if (!empty($col_inner) && preg_match('/\[pl_[a-zA-Z0-9_-]+|<[a-z]+|[a-zA-Z0-9]/', $col_inner)) { |
| 1629 |
$non_empty_cols[] = array( |
| 1630 |
'raw' => $col[0], |
| 1631 |
'atts' => $col_atts, |
| 1632 |
'inner' => $col_inner, |
| 1633 |
'is_card' => (bool) preg_match('/\[(' . $card_tags . ')\b/i', $col_inner) |
| 1634 |
&& !preg_match('/\[pl_(heading|text|btn|image|row|inner_row)\b/i', $col_inner), |
| 1635 |
); |
| 1636 |
} |
| 1637 |
} |
| 1638 |
|
| 1639 |
if (!empty($non_empty_cols)) { |
| 1640 |
if (count($non_empty_cols) === 1) { |
| 1641 |
$only = $set_col_width($non_empty_cols[0], 12); |
| 1642 |
return $emit_row($row_atts, array($only)); |
| 1643 |
} |
| 1644 |
|
| 1645 |
// Split leading full-width header columns from a trailing card grid. |
| 1646 |
$header = array(); |
| 1647 |
$grid = array(); |
| 1648 |
$in_grid = false; |
| 1649 |
foreach ($non_empty_cols as $c) { |
| 1650 |
$w = 0; |
| 1651 |
if (preg_match('/col="(\d+)"/', $c['atts'], $cm)) { |
| 1652 |
$w = intval($cm[1]); |
| 1653 |
} |
| 1654 |
if (!$in_grid && ($c['is_card'] || ($w > 0 && $w <= 4))) { |
| 1655 |
$in_grid = true; |
| 1656 |
} |
| 1657 |
if ($in_grid) { |
| 1658 |
$grid[] = $c; |
| 1659 |
} else { |
| 1660 |
$header[] = $c; |
| 1661 |
} |
| 1662 |
} |
| 1663 |
|
| 1664 |
// 4+ card/grid columns: new sibling rows of 3, never squash to one line. |
| 1665 |
if (count($grid) >= 2) { |
| 1666 |
$out = ''; |
| 1667 |
if (!empty($header)) { |
| 1668 |
foreach ($header as $i => $h) { |
| 1669 |
$header[$i] = $set_col_width($h, 12); |
| 1670 |
} |
| 1671 |
$head_atts = $row_atts; |
| 1672 |
$head_atts = preg_replace('/ele_padding="[^"]*"/', 'ele_padding="50px,20px,50px,20px"', $head_atts); |
| 1673 |
if (strpos($head_atts, 'ele_padding=') === false) { |
| 1674 |
$head_atts .= ' ele_padding="50px,20px,50px,20px"'; |
| 1675 |
} |
| 1676 |
$out .= $emit_row($head_atts, $header); |
| 1677 |
} |
| 1678 |
$chunks = array_chunk($grid, 3); |
| 1679 |
foreach ($chunks as $ci => $chunk) { |
| 1680 |
$n = count($chunk); |
| 1681 |
$width = ($n >= 3) ? 4 : (($n === 2) ? 6 : 4); |
| 1682 |
foreach ($chunk as $i => $c) { |
| 1683 |
$chunk[$i] = $set_col_width($c, $width); |
| 1684 |
} |
| 1685 |
$chunk_atts = $row_atts; |
| 1686 |
$pad = '50px,20px,50px,20px'; |
| 1687 |
if (preg_match('/ele_padding="/', $chunk_atts)) { |
| 1688 |
$chunk_atts = preg_replace('/ele_padding="[^"]*"/', 'ele_padding="' . $pad . '"', $chunk_atts); |
| 1689 |
} else { |
| 1690 |
$chunk_atts .= ' ele_padding="' . $pad . '"'; |
| 1691 |
} |
| 1692 |
$out .= $emit_row($chunk_atts, $chunk); |
| 1693 |
} |
| 1694 |
return $out; |
| 1695 |
} |
| 1696 |
|
| 1697 |
$total_cols = 0; |
| 1698 |
foreach ($non_empty_cols as $c) { |
| 1699 |
if (preg_match('/col="(\d+)"/', $c['atts'], $cm)) { |
| 1700 |
$total_cols += intval($cm[1]); |
| 1701 |
} |
| 1702 |
} |
| 1703 |
|
| 1704 |
// Only redistribute when a small row is missing widths (spacer |
| 1705 |
// columns removed). Never force 4+ columns to share a single 12. |
| 1706 |
$count = count($non_empty_cols); |
| 1707 |
if ($total_cols !== 12 && $count <= 3) { |
| 1708 |
$each_col = (int) floor(12 / $count); |
| 1709 |
foreach ($non_empty_cols as $idx => $c) { |
| 1710 |
$this_col = ($idx === 0) ? (12 - ($each_col * ($count - 1))) : $each_col; |
| 1711 |
$non_empty_cols[$idx] = $set_col_width($c, $this_col); |
| 1712 |
} |
| 1713 |
} |
| 1714 |
|
| 1715 |
return $emit_row($row_atts, $non_empty_cols); |
| 1716 |
} |
| 1717 |
} |
| 1718 |
|
| 1719 |
// If row has no columns or all columns were empty, wrap content in full 12-column |
| 1720 |
if (strpos($row_content, '[pl_col') === false && strpos($row_content, '[pagelayer_col') === false) { |
| 1721 |
return '[pl_row' . $row_atts . '][pl_col col="12"]' . $row_content . '[/pl_col][/pl_row]'; |
| 1722 |
} |
| 1723 |
|
| 1724 |
return $row_match[0]; |
| 1725 |
}, $text); |
| 1726 |
|
| 1727 |
// Extract inline styles (color, align) from leaf widgets into shortcode attributes and strip style="..." |
| 1728 |
$catalog = Pagelayer_AI_Layout_Engine::instance()->get_widget_catalog(); |
| 1729 |
$leaf_list = array(); |
| 1730 |
foreach ($catalog as $tag => $line) { |
| 1731 |
if (strpos($line, '|children') !== false || $tag === 'pl_row' || $tag === 'pl_col' || $tag === 'pl_inner_row' || $tag === 'pl_inner_col') { |
| 1732 |
continue; |
| 1733 |
} |
| 1734 |
$leaf_list[] = preg_quote($tag, '/'); |
| 1735 |
} |
| 1736 |
$leaf_tags = !empty($leaf_list) ? implode('|', $leaf_list) : 'pl_heading|pl_text|pl_btn|pl_image'; |
| 1737 |
$text = preg_replace_callback('/\[(' . $leaf_tags . ')([^\]]*)\](.*?)\[\/\1\]/is', function($m) { |
| 1738 |
$tag = $m[1]; |
| 1739 |
$atts = $m[2]; |
| 1740 |
$inner = $m[3]; |
| 1741 |
|
| 1742 |
if (preg_match('/style=[\"\']([^\"\']+)[\"\']/i', $inner, $sm)) { |
| 1743 |
$style_str = $sm[1]; |
| 1744 |
|
| 1745 |
// Extract color if not already present in attributes. |
| 1746 |
// pl_text has no `color` prop — put the value in ele_css so it actually renders. |
| 1747 |
if (preg_match('/(?:^|;)\s*color\s*:\s*([^;]+)/i', $style_str, $cm)) { |
| 1748 |
$extracted = trim($cm[1]); |
| 1749 |
$no_color_prop = ($tag === 'pl_text' || $tag === 'pl_iconbox' || $tag === 'pl_service'); |
| 1750 |
if ($no_color_prop) { |
| 1751 |
if (strpos($atts, 'ele_css=') === false) { |
| 1752 |
$atts .= ' ele_css="{{element}},{{element}} .pagelayer-text-holder,{{element}} .pagelayer-service-text{color:' . $extracted . '}"'; |
| 1753 |
} |
| 1754 |
} elseif (strpos($atts, 'color=') === false) { |
| 1755 |
$atts .= ' color="' . $extracted . '"'; |
| 1756 |
} |
| 1757 |
} |
| 1758 |
|
| 1759 |
// Extract text-align if not already present in attributes |
| 1760 |
if (strpos($atts, 'align=') === false && preg_match('/text-align\s*:\s*(left|center|right|justify)/i', $style_str, $am)) { |
| 1761 |
$atts .= ' align="' . trim($am[1]) . '"'; |
| 1762 |
} |
| 1763 |
} |
| 1764 |
|
| 1765 |
// Clean all style attributes from inner HTML |
| 1766 |
$inner = preg_replace('/\s*style=[\"\'][^\"\']*[\"\']/i', '', $inner); |
| 1767 |
|
| 1768 |
return '[' . $tag . $atts . ']' . $inner . '[/' . $tag . ']'; |
| 1769 |
}, $text); |
| 1770 |
|
| 1771 |
// Final pass to clean any remaining style attributes from any HTML tags |
| 1772 |
$text = preg_replace('/(\sstyle=[\"\'][^\"\']*[\"\'])/i', '', $text); |
| 1773 |
|
| 1774 |
return $this->ensure_counter_start($this->ensure_accordion_contrast($this->ensure_social_contrast($this->ensure_slider_image_size($this->ensure_iconbox_center($this->ensure_row_section_padding($this->ensure_dark_band_contrast($text))))))); |
| 1775 |
} |
| 1776 |
|
| 1777 |
/** |
| 1778 |
* Social Profile: icon glyph and circle background must contrast. |
| 1779 |
*/ |
| 1780 |
private function ensure_social_contrast($text) { |
| 1781 |
if (!is_string($text) || $text === '' || (stripos($text, 'social_grp') === false && stripos($text, 'social-grp') === false)) { |
| 1782 |
return $text; |
| 1783 |
} |
| 1784 |
|
| 1785 |
$set = function ($atts, $key, $val) { |
| 1786 |
if (preg_match('/\s' . preg_quote($key, '/') . '="/i', $atts)) { |
| 1787 |
return preg_replace('/\s' . preg_quote($key, '/') . '="[^"]*"/i', ' ' . $key . '="' . $val . '"', $atts, 1); |
| 1788 |
} |
| 1789 |
return $atts . ' ' . $key . '="' . $val . '"'; |
| 1790 |
}; |
| 1791 |
|
| 1792 |
$rule = '{{element}}{display:inline-flex;flex-direction:row;flex-wrap:wrap;align-items:center;justify-content:center;gap:14px;}' |
| 1793 |
. '{{element}} .pagelayer-icon-holder{background-color:var(--pagelayer-color-primary,#4f46e5)!important;}' |
| 1794 |
. '{{element}} .pagelayer-social-fa{color:#ffffff!important;}'; |
| 1795 |
|
| 1796 |
return preg_replace_callback('/\[(pl_social_grp|pagelayer_social_grp)([^\]]*)\]/i', function ($m) use ($set, $rule) { |
| 1797 |
$atts = $m[2]; |
| 1798 |
$icon = ''; |
| 1799 |
$bg = ''; |
| 1800 |
if (preg_match('/icon_color="([^"]*)"/i', $atts, $im)) { |
| 1801 |
$icon = strtolower(trim($im[1])); |
| 1802 |
} |
| 1803 |
if (preg_match('/icon_bg_color="([^"]*)"/i', $atts, $bm)) { |
| 1804 |
$bg = strtolower(trim($bm[1])); |
| 1805 |
} |
| 1806 |
$clash = ($icon === '' || $bg === '' || $icon === $bg); |
| 1807 |
|
| 1808 |
$atts = $set($atts, 'color_scheme', ''); |
| 1809 |
$atts = $set($atts, 'bg_shape', 'pagelayer-social-shape-circle'); |
| 1810 |
if (!preg_match('/icon_size="/i', $atts) || preg_match('/icon_size="(?:0|1|2|3|4|5|6|7|8|9|10|12|14)"/i', $atts)) { |
| 1811 |
$atts = $set($atts, 'icon_size', '20'); |
| 1812 |
} |
| 1813 |
if ($clash) { |
| 1814 |
$atts = $set($atts, 'icon_color', '#ffffff'); |
| 1815 |
$atts = $set($atts, 'icon_bg_color', '$primary'); |
| 1816 |
} |
| 1817 |
if (strpos($atts, 'pagelayer-social-fa') === false) { |
| 1818 |
if (preg_match('/ele_css="([^"]*)"/i', $atts, $cm)) { |
| 1819 |
$atts = preg_replace('/ele_css="([^"]*)"/i', 'ele_css="' . esc_attr($cm[1] . $rule) . '"', $atts, 1); |
| 1820 |
} else { |
| 1821 |
$atts .= ' ele_css="' . esc_attr($rule) . '"'; |
| 1822 |
} |
| 1823 |
} |
| 1824 |
return '[' . $m[1] . $atts . ']'; |
| 1825 |
}, $text); |
| 1826 |
} |
| 1827 |
|
| 1828 |
/** |
| 1829 |
* Accordion / Tabs: tab text, tab background, and panel must contrast. |
| 1830 |
*/ |
| 1831 |
private function ensure_accordion_contrast($text) { |
| 1832 |
if (!is_string($text) || $text === '' || (stripos($text, 'accordion') === false && stripos($text, 'pl_tabs') === false)) { |
| 1833 |
return $text; |
| 1834 |
} |
| 1835 |
|
| 1836 |
$set = function ($atts, $key, $val) { |
| 1837 |
if (preg_match('/\s' . preg_quote($key, '/') . '="/i', $atts)) { |
| 1838 |
return preg_replace('/\s' . preg_quote($key, '/') . '="[^"]*"/i', ' ' . $key . '="' . $val . '"', $atts, 1); |
| 1839 |
} |
| 1840 |
return $atts . ' ' . $key . '="' . $val . '"'; |
| 1841 |
}; |
| 1842 |
|
| 1843 |
$same = function ($a, $b) { |
| 1844 |
$a = strtolower(trim((string) $a)); |
| 1845 |
$b = strtolower(trim((string) $b)); |
| 1846 |
return ($a === '' || $b === '' || $a === $b); |
| 1847 |
}; |
| 1848 |
|
| 1849 |
return preg_replace_callback('/\[(pl_accordion|pl_tabs|pagelayer_accordion|pagelayer_tabs)([^\]]*)\]/i', function ($m) use ($set, $same) { |
| 1850 |
$tag = strtolower($m[1]); |
| 1851 |
$atts = $m[2]; |
| 1852 |
$is_tabs = (strpos($tag, 'tabs') !== false); |
| 1853 |
|
| 1854 |
$tab_fg = $this->shortcode_att($atts, 'tabs_color'); |
| 1855 |
$tab_bg = $this->shortcode_att($atts, 'tabs_bg_color'); |
| 1856 |
$act_fg = $this->shortcode_att($atts, 'tabs_active_color'); |
| 1857 |
$act_bg = $this->shortcode_att($atts, 'tabs_active_bg_color'); |
| 1858 |
$row_bg = $this->shortcode_att($atts, 'ele_bg_color'); |
| 1859 |
$dark = $this->hex_looks_dark($row_bg); |
| 1860 |
|
| 1861 |
if ($same($tab_fg, $tab_bg) || $same($tab_fg, $row_bg) || $same($tab_bg, $row_bg)) { |
| 1862 |
$atts = $set($atts, 'tabs_color', $dark ? '#ffffff' : '#111827'); |
| 1863 |
$atts = $set($atts, 'tabs_bg_color', $dark ? 'rgba(255,255,255,0.14)' : '#f1f5f9'); |
| 1864 |
} |
| 1865 |
if ($same($act_fg, $act_bg)) { |
| 1866 |
$atts = $set($atts, 'tabs_active_color', '#ffffff'); |
| 1867 |
$atts = $set($atts, 'tabs_active_bg_color', '$primary'); |
| 1868 |
} |
| 1869 |
$atts = $set($atts, 'tabs_content_bg_color', $dark ? 'rgba(255,255,255,0.08)' : '#ffffff'); |
| 1870 |
if ($is_tabs) { |
| 1871 |
$atts = $set($atts, 'tabs_content_color', $dark ? '#ffffff' : '#111827'); |
| 1872 |
} |
| 1873 |
|
| 1874 |
$sel_tab = $is_tabs ? '{{element}} .pagelayer-tablinks' : '{{element}} .pagelayer-accordion-tabs'; |
| 1875 |
$sel_panel = $is_tabs ? '{{element}} .pagelayer-tab' : '{{element}} .pagelayer-accordion-panel'; |
| 1876 |
$rule = $sel_tab . '{color:' . ($dark ? '#ffffff' : '#111827') . '!important;background-color:' . ($dark ? 'rgba(255,255,255,0.14)' : '#f1f5f9') . '!important;}' |
| 1877 |
. '{{element}} .active ' . ($is_tabs ? '.pagelayer-tablinks' : '.pagelayer-accordion-tabs') . '{color:#ffffff!important;background-color:var(--pagelayer-color-primary,#4f46e5)!important;}' |
| 1878 |
. $sel_panel . '{color:' . ($dark ? '#ffffff' : '#111827') . '!important;background-color:' . ($dark ? 'rgba(255,255,255,0.08)' : '#ffffff') . '!important;}'; |
| 1879 |
if (strpos($atts, 'pagelayer-accordion-tabs') === false && strpos($atts, 'pagelayer-tablinks') === false) { |
| 1880 |
if (preg_match('/ele_css="([^"]*)"/i', $atts, $cm)) { |
| 1881 |
$atts = preg_replace('/ele_css="([^"]*)"/i', 'ele_css="' . esc_attr($cm[1] . $rule) . '"', $atts, 1); |
| 1882 |
} else { |
| 1883 |
$atts .= ' ele_css="' . esc_attr($rule) . '"'; |
| 1884 |
} |
| 1885 |
} |
| 1886 |
return '[' . $m[1] . $atts . ']'; |
| 1887 |
}, $text); |
| 1888 |
} |
| 1889 |
|
| 1890 |
private function shortcode_att($atts, $key) { |
| 1891 |
if (preg_match('/\s' . preg_quote($key, '/') . '="([^"]*)"/i', (string) $atts, $m)) { |
| 1892 |
return trim($m[1]); |
| 1893 |
} |
| 1894 |
return ''; |
| 1895 |
} |
| 1896 |
|
| 1897 |
/** |
| 1898 |
* Counter start must be >= 1 — 0 hides the widget (if="{{counter_start_number}}"). |
| 1899 |
*/ |
| 1900 |
private function ensure_counter_start($text) { |
| 1901 |
if (!is_string($text) || $text === '' || stripos($text, 'counter') === false) { |
| 1902 |
return $text; |
| 1903 |
} |
| 1904 |
|
| 1905 |
return preg_replace_callback('/\[(pl_counter|pagelayer_counter)([^\]]*)\]/i', function ($m) { |
| 1906 |
$atts = $m[2]; |
| 1907 |
$start = 0; |
| 1908 |
if (preg_match('/counter_start_number="([^"]*)"/i', $atts, $sm)) { |
| 1909 |
$start = (int) $sm[1]; |
| 1910 |
} |
| 1911 |
if ($start < 1) { |
| 1912 |
if (preg_match('/counter_start_number="/i', $atts)) { |
| 1913 |
$atts = preg_replace('/counter_start_number="[^"]*"/i', 'counter_start_number="1"', $atts, 1); |
| 1914 |
} else { |
| 1915 |
$atts .= ' counter_start_number="1"'; |
| 1916 |
} |
| 1917 |
$start = 1; |
| 1918 |
} |
| 1919 |
$end = 0; |
| 1920 |
if (preg_match('/counter_end_number="([^"]*)"/i', $atts, $em)) { |
| 1921 |
$end = (int) $em[1]; |
| 1922 |
} |
| 1923 |
if ($end <= $start) { |
| 1924 |
$new_end = (string) max(100, $start + 99); |
| 1925 |
if (preg_match('/counter_end_number="/i', $atts)) { |
| 1926 |
$atts = preg_replace('/counter_end_number="[^"]*"/i', 'counter_end_number="' . $new_end . '"', $atts, 1); |
| 1927 |
} else { |
| 1928 |
$atts .= ' counter_end_number="' . $new_end . '"'; |
| 1929 |
} |
| 1930 |
} |
| 1931 |
return '[' . $m[1] . $atts . ']'; |
| 1932 |
}, $text); |
| 1933 |
} |
| 1934 |
|
| 1935 |
/** |
| 1936 |
* Tag dark (and nested light) surfaces so canvas CSS can keep text readable |
| 1937 |
* when a widget has no color prop of its own. |
| 1938 |
*/ |
| 1939 |
private function ensure_dark_band_contrast($text) { |
| 1940 |
if (!is_string($text) || $text === '') { |
| 1941 |
return $text; |
| 1942 |
} |
| 1943 |
|
| 1944 |
$engine = class_exists('Pagelayer_AI_Layout_Engine') ? Pagelayer_AI_Layout_Engine::instance() : null; |
| 1945 |
|
| 1946 |
return preg_replace_callback( |
| 1947 |
'/\[(pl_(?:inner_)?(?:row|col|iconbox|service|testimonial|quote|counter|pricing|flipbox|review|author_box|call|countdown|heading|text|list|accordion|tabs|alert|address|phone|email))([^\]]*)\]/i', |
| 1948 |
function ($m) use ($engine) { |
| 1949 |
$tag = $m[1]; |
| 1950 |
$atts = $m[2]; |
| 1951 |
if (strpos($atts, 'pagelayer-ai-on-dark') !== false || strpos($atts, 'pagelayer-ai-on-light') !== false) { |
| 1952 |
return $m[0]; |
| 1953 |
} |
| 1954 |
if (!preg_match('/ele_bg_color="([^"]*)"/i', $atts, $bm) || trim($bm[1]) === '') { |
| 1955 |
return $m[0]; |
| 1956 |
} |
| 1957 |
$dark = $engine |
| 1958 |
? $engine->is_dark_color($bm[1], false) |
| 1959 |
: $this->hex_looks_dark($bm[1]); |
| 1960 |
$cls = $dark ? 'pagelayer-ai-on-dark' : 'pagelayer-ai-on-light'; |
| 1961 |
if (preg_match('/ele_classes="/i', $atts)) { |
| 1962 |
$atts = preg_replace('/ele_classes="/i', 'ele_classes="' . $cls . ' ', $atts, 1); |
| 1963 |
} else { |
| 1964 |
$atts .= ' ele_classes="' . $cls . '"'; |
| 1965 |
} |
| 1966 |
return '[' . $tag . $atts . ']'; |
| 1967 |
}, |
| 1968 |
$text |
| 1969 |
); |
| 1970 |
} |
| 1971 |
|
| 1972 |
private function hex_looks_dark($val) { |
| 1973 |
$val = strtolower(trim((string) $val)); |
| 1974 |
if ($val === '' || $val === 'transparent') { |
| 1975 |
return false; |
| 1976 |
} |
| 1977 |
if (in_array($val, array('black', 'navy', '#000', '#000000', '#111', '#111111'), true) || $val === '$text') { |
| 1978 |
return true; |
| 1979 |
} |
| 1980 |
if ($val === 'white' || $val === '#fff' || $val === '#ffffff') { |
| 1981 |
return false; |
| 1982 |
} |
| 1983 |
if (preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $val, $m)) { |
| 1984 |
$hex = $m[1]; |
| 1985 |
if (strlen($hex) === 3) { |
| 1986 |
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; |
| 1987 |
} |
| 1988 |
$r = hexdec(substr($hex, 0, 2)); |
| 1989 |
$g = hexdec(substr($hex, 2, 2)); |
| 1990 |
$b = hexdec(substr($hex, 4, 2)); |
| 1991 |
return ((0.299 * $r + 0.587 * $g + 0.114 * $b) / 255) < 0.55; |
| 1992 |
} |
| 1993 |
return false; |
| 1994 |
} |
| 1995 |
|
| 1996 |
/** |
| 1997 |
* Every outer section row keeps 50px top and bottom padding so the next |
| 1998 |
* band does not sit flush against the columns above it. |
| 1999 |
*/ |
| 2000 |
private function ensure_row_section_padding($text) { |
| 2001 |
if (!is_string($text) || $text === '') { |
| 2002 |
return $text; |
| 2003 |
} |
| 2004 |
|
| 2005 |
$engine = class_exists('Pagelayer_AI_Layout_Engine') ? Pagelayer_AI_Layout_Engine::instance() : null; |
| 2006 |
|
| 2007 |
return preg_replace_callback('/\[pl_row([^\]]*)\]/i', function ($m) use ($engine) { |
| 2008 |
$atts = $m[1]; |
| 2009 |
$pad = ''; |
| 2010 |
if (preg_match('/ele_padding="([^"]*)"/i', $atts, $pm)) { |
| 2011 |
$pad = $pm[1]; |
| 2012 |
} |
| 2013 |
$fixed = $engine |
| 2014 |
? $engine->format_section_row_padding($pad) |
| 2015 |
: '50px,20px,50px,20px'; |
| 2016 |
if (preg_match('/ele_padding="/i', $atts)) { |
| 2017 |
$atts = preg_replace('/ele_padding="[^"]*"/i', 'ele_padding="' . $fixed . '"', $atts, 1); |
| 2018 |
} else { |
| 2019 |
$atts .= ' ele_padding="' . $fixed . '"'; |
| 2020 |
} |
| 2021 |
return '[pl_row' . $atts . ']'; |
| 2022 |
}, $text); |
| 2023 |
} |
| 2024 |
|
| 2025 |
/** |
| 2026 |
* Icon / image boxes: centered icon (Horizontal Position), heading, and copy. |
| 2027 |
*/ |
| 2028 |
private function ensure_iconbox_center($text) { |
| 2029 |
if (!is_string($text) || $text === '') { |
| 2030 |
return $text; |
| 2031 |
} |
| 2032 |
|
| 2033 |
$set = function ($atts, $key, $val) { |
| 2034 |
if (preg_match('/\s' . preg_quote($key, '/') . '="/i', $atts)) { |
| 2035 |
return preg_replace('/\s' . preg_quote($key, '/') . '="[^"]*"/i', ' ' . $key . '="' . $val . '"', $atts, 1); |
| 2036 |
} |
| 2037 |
return $atts . ' ' . $key . '="' . $val . '"'; |
| 2038 |
}; |
| 2039 |
|
| 2040 |
$append_css = function ($atts, $rule) { |
| 2041 |
if (strpos($atts, $rule) !== false) { |
| 2042 |
return $atts; |
| 2043 |
} |
| 2044 |
if (preg_match('/ele_css="([^"]*)"/i', $atts, $cm)) { |
| 2045 |
return preg_replace('/ele_css="([^"]*)"/i', 'ele_css="$1' . $rule . '"', $atts, 1); |
| 2046 |
} |
| 2047 |
return $atts . ' ele_css="' . $rule . '"'; |
| 2048 |
}; |
| 2049 |
|
| 2050 |
return preg_replace_callback('/\[(pl_iconbox|pl_service)([^\]]*)\]/i', function ($m) use ($set, $append_css) { |
| 2051 |
$tag = strtolower($m[1]); |
| 2052 |
$atts = $m[2]; |
| 2053 |
$atts = $set($atts, 'service_alignment', 'top'); |
| 2054 |
$atts = $set($atts, ($tag === 'pl_iconbox') ? 'service_icon_alignment' : 'service_img_alignment', 'center'); |
| 2055 |
$atts = $set($atts, 'heading_alignment', 'center'); |
| 2056 |
$atts = $set($atts, 'service_text_alignment', 'center'); |
| 2057 |
if (strpos($atts, 'font_size=') === false) { |
| 2058 |
$atts = $set($atts, 'font_size', '15'); |
| 2059 |
} |
| 2060 |
if (strpos($atts, 'line_height=') === false) { |
| 2061 |
$atts = $set($atts, 'line_height', '1.65'); |
| 2062 |
} |
| 2063 |
if ($tag === 'pl_service') { |
| 2064 |
if (strpos($atts, 'service_image_height=') === false) { |
| 2065 |
$atts = $set($atts, 'service_image_height', '220'); |
| 2066 |
} |
| 2067 |
if (strpos($atts, 'service_image_object_fit=') === false) { |
| 2068 |
$atts = $set($atts, 'service_image_object_fit', 'cover'); |
| 2069 |
} |
| 2070 |
if (strpos($atts, 'service_image_object_pos=') === false) { |
| 2071 |
$atts = $set($atts, 'service_image_object_pos', 'center'); |
| 2072 |
} |
| 2073 |
if (strpos($atts, 'service_image_border_radius=') === false) { |
| 2074 |
$atts = $set($atts, 'service_image_border_radius', '8,8,8,8'); |
| 2075 |
} |
| 2076 |
$rule = '{{element}} .pagelayer-service-image{width:100%!important;overflow:hidden;margin:0 auto 16px auto;border-radius:8px;}' |
| 2077 |
. '{{element}} .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;display:block;margin:0 auto;}' |
| 2078 |
. '{{element}} .pagelayer-service-container{display:flex;flex-direction:column;height:100%;}' |
| 2079 |
. '{{element}} .pagelayer-service-details{display:flex;flex-direction:column;flex-grow:1;}' |
| 2080 |
. '{{element}} .pagelayer-service-heading{text-align:center;font-weight:700;line-height:1.3;margin-bottom:8px;}' |
| 2081 |
. '{{element}} .pagelayer-service-text{text-align:center;font-size:15px;line-height:1.7;font-weight:400;flex-grow:1;margin-bottom:16px;}' |
| 2082 |
. '{{element}} .pagelayer-service-btn{margin-top:auto;align-self:center;}'; |
| 2083 |
$atts = $append_css($atts, $rule); |
| 2084 |
} else { |
| 2085 |
if (strpos($atts, 'service_icon_font_size=') === false) { |
| 2086 |
$atts = $set($atts, 'service_icon_font_size', '28'); |
| 2087 |
} |
| 2088 |
$rule = '{{element}} .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;}' |
| 2089 |
. '{{element}} .pagelayer-service-icon i,{{element}} .pagelayer-service-icon svg{font-size:28px!important;width:28px!important;height:28px!important;line-height:28px!important;display:inline-block!important;}' |
| 2090 |
. '{{element}} .pagelayer-service-container{display:flex;flex-direction:column;height:100%;}' |
| 2091 |
. '{{element}} .pagelayer-service-details{display:flex;flex-direction:column;flex-grow:1;}' |
| 2092 |
. '{{element}} .pagelayer-service-heading{text-align:center;font-weight:700;line-height:1.3;margin-bottom:8px;}' |
| 2093 |
. '{{element}} .pagelayer-service-text{text-align:center;font-size:15px;line-height:1.7;font-weight:400;flex-grow:1;margin-bottom:16px;}' |
| 2094 |
. '{{element}} .pagelayer-service-btn{margin-top:auto;align-self:center;}'; |
| 2095 |
$atts = $append_css($atts, $rule); |
| 2096 |
} |
| 2097 |
return '[' . $m[1] . $atts . ']'; |
| 2098 |
}, $text); |
| 2099 |
} |
| 2100 |
|
| 2101 |
/** |
| 2102 |
* Ensure image slider images are constrained to a balanced height (420px, object-fit: cover) |
| 2103 |
* instead of blowing up to full natural image height. |
| 2104 |
*/ |
| 2105 |
private function ensure_slider_image_size($text) { |
| 2106 |
if (empty($text) || (stripos($text, 'image_slider') === false && stripos($text, 'image-slider') === false)) { |
| 2107 |
return $text; |
| 2108 |
} |
| 2109 |
|
| 2110 |
$rule = '{{element}}{max-width:1000px!important;margin:0 auto!important;overflow:visible!important;}' |
| 2111 |
. '{{element}} .pagelayer-image-slider-div{max-width:1000px!important;margin:0 auto!important;overflow:visible!important;border-radius:12px!important;}' |
| 2112 |
. '{{element}} .pagelayer-image-slider-ul{margin:0 auto!important;}' |
| 2113 |
. '{{element}} .pagelayer-slider-item{overflow:hidden!important;border-radius:12px!important;}' |
| 2114 |
. '{{element}} .pagelayer-slider-item img,{{element}} img.pagelayer-img,{{element}} .pagelayer-image-slider-div 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;}' |
| 2115 |
. '{{element}} .pagelayer-owl-nav{z-index:5!important;}' |
| 2116 |
. '{{element}} .pagelayer-owl-prev,{{element}} .pagelayer-owl-next{width:48px!important;height:48px!important;min-width:48px!important;min-height:48px!important;border-radius:50%!important;background:rgba(15,23,42,0.72)!important;color:#ffffff!important;display:flex!important;align-items:center!important;justify-content:center!important;opacity:1!important;}' |
| 2117 |
. '{{element}} .pagelayer-owl-prev span,{{element}} .pagelayer-owl-next span,{{element}} .pagelayer-owl-prev i,{{element}} .pagelayer-owl-next i{font-size:28px!important;line-height:1!important;color:#ffffff!important;}'; |
| 2118 |
|
| 2119 |
$set = function ($atts, $key, $val) { |
| 2120 |
if (preg_match('/\s' . preg_quote($key, '/') . '="/i', $atts)) { |
| 2121 |
return preg_replace('/\s' . preg_quote($key, '/') . '="[^"]*"/i', ' ' . $key . '="' . $val . '"', $atts, 1); |
| 2122 |
} |
| 2123 |
return $atts . ' ' . $key . '="' . $val . '"'; |
| 2124 |
}; |
| 2125 |
|
| 2126 |
return preg_replace_callback('/\[(pl_image_slider|pagelayer-image[_-]slider)(\s+[^\]]*)?\]/i', function ($m) use ($rule, $set) { |
| 2127 |
$atts = isset($m[2]) ? $m[2] : ''; |
| 2128 |
$atts = $set($atts, 'controls', 'arrows'); |
| 2129 |
if (!preg_match('/nav_size="/i', $atts) || preg_match('/nav_size="(?:0|1|2|3|4|5|6|7|8|9|10|12|14|16)"/i', $atts)) { |
| 2130 |
$atts = $set($atts, 'nav_size', '28'); |
| 2131 |
} |
| 2132 |
if (!preg_match('/arraow_bg_size="/i', $atts) || preg_match('/arraow_bg_size="(?:0|1[0-9]|2[0-9]|3[0-5])"/i', $atts)) { |
| 2133 |
$atts = $set($atts, 'arraow_bg_size', '48'); |
| 2134 |
} |
| 2135 |
if (!preg_match('/arraow_bg_shape="/i', $atts)) { |
| 2136 |
$atts = $set($atts, 'arraow_bg_shape', '50'); |
| 2137 |
} |
| 2138 |
$arrow_fg = ''; |
| 2139 |
$arrow_bg = ''; |
| 2140 |
if (preg_match('/arraow_color="([^"]*)"/i', $atts, $fm)) { |
| 2141 |
$arrow_fg = strtolower(trim($fm[1])); |
| 2142 |
} |
| 2143 |
if (preg_match('/arrows_bg="([^"]*)"/i', $atts, $bm)) { |
| 2144 |
$arrow_bg = strtolower(trim($bm[1])); |
| 2145 |
} |
| 2146 |
if ($arrow_fg === '' || $arrow_bg === '' || $arrow_fg === $arrow_bg) { |
| 2147 |
$atts = $set($atts, 'arrows_bg', 'rgba(15,23,42,0.72)'); |
| 2148 |
$atts = $set($atts, 'arraow_color', '#ffffff'); |
| 2149 |
} |
| 2150 |
|
| 2151 |
if (strpos($atts, 'font-size:28px') === false) { |
| 2152 |
if (preg_match('/ele_css="([^"]*)"/', $atts, $m_css)) { |
| 2153 |
$existing_css = $m_css[1]; |
| 2154 |
$new_css = $existing_css . $rule; |
| 2155 |
$atts = str_replace($m_css[0], 'ele_css="' . esc_attr($new_css) . '"', $atts); |
| 2156 |
} else { |
| 2157 |
$atts .= ' ele_css="' . esc_attr($rule) . '"'; |
| 2158 |
} |
| 2159 |
} |
| 2160 |
|
| 2161 |
return '[' . $m[1] . $atts . ']'; |
| 2162 |
}, $text); |
| 2163 |
} |
| 2164 |
|
| 2165 |
/** |
| 2166 |
* Render shortcode to live canvas HTML with data-attributes and Pagelayer editor structure |
| 2167 |
*/ |
| 2168 |
public function render_shortcodes_for_canvas($shortcode, $post_id = 0) { |
| 2169 |
global $post, $wp_query; |
| 2170 |
|
| 2171 |
// Set live mode flag so pagelayer_is_live() returns true and creates pagelayer-ele, pagelayer-tag, and data comments |
| 2172 |
$_REQUEST['pagelayer-live'] = 1; |
| 2173 |
if (!empty($post_id)) { |
| 2174 |
$_REQUEST['postID'] = $post_id; |
| 2175 |
} |
| 2176 |
|
| 2177 |
if (!empty($post_id)) { |
| 2178 |
$post_obj = get_post($post_id); |
| 2179 |
if (!empty($post_obj)) { |
| 2180 |
$post = $post_obj; |
| 2181 |
$GLOBALS['post'] = $post_obj; |
| 2182 |
$GLOBALS['wp_query'] = new WP_Query(array( |
| 2183 |
'post_type' => $post_obj->post_type, |
| 2184 |
'post__in' => array($post_id), |
| 2185 |
)); |
| 2186 |
} |
| 2187 |
} |
| 2188 |
|
| 2189 |
if (empty($GLOBALS['post'])) { |
| 2190 |
$posts = get_posts(array('numberposts' => 1, 'post_status' => 'any')); |
| 2191 |
if (!empty($posts)) { |
| 2192 |
$post = $posts[0]; |
| 2193 |
$GLOBALS['post'] = $posts[0]; |
| 2194 |
} |
| 2195 |
} |
| 2196 |
|
| 2197 |
if (function_exists('pagelayer_load_shortcodes')) { |
| 2198 |
pagelayer_load_shortcodes(); |
| 2199 |
} |
| 2200 |
|
| 2201 |
// Ensure pagelayer_* shortcode aliases exist as well |
| 2202 |
$this->register_pagelayer_aliases(); |
| 2203 |
|
| 2204 |
if (function_exists('pagelayer_the_content')) { |
| 2205 |
$rendered = pagelayer_the_content($shortcode, true); |
| 2206 |
return $rendered; |
| 2207 |
} |
| 2208 |
|
| 2209 |
return do_shortcode($shortcode); |
| 2210 |
} |
| 2211 |
|
| 2212 |
/** |
| 2213 |
* Register pagelayer_* alias tags pointing to pl_* shortcodes |
| 2214 |
*/ |
| 2215 |
private function register_pagelayer_aliases() { |
| 2216 |
global $shortcode_tags; |
| 2217 |
if (!is_array($shortcode_tags)) return; |
| 2218 |
|
| 2219 |
foreach ($shortcode_tags as $tag => $callback) { |
| 2220 |
if (strpos($tag, 'pl_') !== 0) { |
| 2221 |
continue; |
| 2222 |
} |
| 2223 |
$alias = 'pagelayer_' . substr($tag, 3); |
| 2224 |
if (!isset($shortcode_tags[$alias])) { |
| 2225 |
add_shortcode($alias, $callback); |
| 2226 |
} |
| 2227 |
} |
| 2228 |
} |
| 2229 |
|
| 2230 |
/** |
| 2231 |
* Params for admin-ajax fallbacks (JSON body or $_POST). |
| 2232 |
*/ |
| 2233 |
public function ajax_request_params() { |
| 2234 |
$raw = file_get_contents('php://input'); |
| 2235 |
if (is_string($raw) && $raw !== '') { |
| 2236 |
$decoded = json_decode($raw, true); |
| 2237 |
if (is_array($decoded) && !empty($decoded)) { |
| 2238 |
return $decoded; |
| 2239 |
} |
| 2240 |
} |
| 2241 |
return is_array($_POST) ? $_POST : array(); |
| 2242 |
} |
| 2243 |
|
| 2244 |
/** |
| 2245 |
* Unwrap a REST callback result to an array or WP_Error. |
| 2246 |
*/ |
| 2247 |
private function unwrap_rest_result($response) { |
| 2248 |
if (is_wp_error($response)) { |
| 2249 |
return $response; |
| 2250 |
} |
| 2251 |
if ($response instanceof WP_REST_Response) { |
| 2252 |
$data = $response->get_data(); |
| 2253 |
$status = $response->get_status(); |
| 2254 |
if ($status >= 400) { |
| 2255 |
$msg = is_array($data) && !empty($data['message']) ? $data['message'] : __('Request failed.', 'pagelayer'); |
| 2256 |
return new WP_Error('pagelayer_ai_error', $msg, array('status' => $status)); |
| 2257 |
} |
| 2258 |
return $data; |
| 2259 |
} |
| 2260 |
return $response; |
| 2261 |
} |
| 2262 |
|
| 2263 |
public function process_generate($params = null) { |
| 2264 |
$request = new WP_REST_Request('POST', '/' . self::REST_NAMESPACE . '/ai/generate'); |
| 2265 |
$request->set_body_params(is_array($params) ? $params : $this->ajax_request_params()); |
| 2266 |
return $this->unwrap_rest_result($this->rest_generate($request)); |
| 2267 |
} |
| 2268 |
|
| 2269 |
public function process_get_settings() { |
| 2270 |
$request = new WP_REST_Request('GET', '/' . self::REST_NAMESPACE . '/ai/settings'); |
| 2271 |
return $this->unwrap_rest_result($this->rest_get_settings($request)); |
| 2272 |
} |
| 2273 |
|
| 2274 |
public function process_save_settings($params = null) { |
| 2275 |
$request = new WP_REST_Request('POST', '/' . self::REST_NAMESPACE . '/ai/settings'); |
| 2276 |
$request->set_body_params(is_array($params) ? $params : $this->ajax_request_params()); |
| 2277 |
return $this->unwrap_rest_result($this->rest_save_settings($request)); |
| 2278 |
} |
| 2279 |
} |
| 2280 |
|
| 2281 |
// Instantiate |
| 2282 |
Pagelayer_AI_Controller::get_instance(); |
| 2283 |
|