post_content) ? (string)$post->post_content : ''; $plain = trim(wp_strip_all_tags($content)); preg_match_all('/ 'system', 'content' => 'You select a visual presentation preset for existing WordPress content. You never write, quote, summarize, or transform page content. Return JSON only.', ], [ 'role' => 'user', 'content' => "Choose one safe layout preset.\n" . "Allowed profile: documentation, editorial, cards, landing.\n" . "Allowed density: compact, balanced, spacious.\n" . "Title: " . sanitize_text_field((string)($post->post_title ?? '')) . "\n" . "Post type: " . sanitize_key((string)($post->post_type ?? 'post')) . "\n" . "Visible word count: " . self::word_count($plain) . "\n" . "Heading count: " . count($heading_matches[0]) . "\n" . "List count: " . count($list_matches[0]) . "\n" . "Table count: " . count($table_matches[0]) . "\n" . "Image count: " . count($image_matches[0]) . "\n" . "Administrator layout instruction: " . sanitize_textarea_field((string)$instruction) . "\n" . (class_exists('AIWEST_Design_Contract') ? AIWEST_Design_Contract::planner_context($contract) : '') . "\n" . "The composition must obey that contract. Do not select or propose colors, fonts, radii, widths or shadows.\n" . 'Return exactly: {"profile":"...","density":"..."}', ], ]; $result = AIWEST_OpenAI::chat($messages, 0.1); if (is_wp_error($result)) { if (class_exists('AIWEST_Logger')) { AIWEST_Logger::warning('Visual layout planning failed; deterministic preset used.', [ 'post_id' => intval($post->ID ?? 0), 'error' => $result->get_error_message(), ]); } return $plan; } $decoded = self::decode_json($result); if (!is_array($decoded)) return $plan; $plan['profile'] = self::allowed_value($decoded['profile'] ?? '', ['documentation','editorial','cards','landing'], $default['profile']); $plan['density'] = self::allowed_value($decoded['density'] ?? '', ['compact','balanced','spacious'], $default['density']); $plan['engine'] = 'ai_layout_plan'; return $plan; } public static function apply($content, $plan) { unset($plan); /* * Layout is presentation metadata, not post content. Older releases * stored a synthetic core/group block around the content, which could * trigger Gutenberg's "unexpected or invalid content" warning. Keep * the approved content byte-for-byte, except for removing that legacy * outer wrapper when the administrator runs Visual Enhancement again. */ $content = self::unwrap((string)$content); return class_exists('AIWEST_Semantic_Layout_Composer') ? AIWEST_Semantic_Layout_Composer::decompose($content) : $content; } public static function apply_current_contract($content) { $content = (string) $content; if (is_admin() || is_feed()) return $content; $post_id = get_the_ID(); if (!$post_id) return $content; $plan = get_post_meta($post_id, self::META_PLAN, true); if (!is_array($plan) || empty($plan)) return $content; $content = self::unwrap($content); $contract = class_exists('AIWEST_Design_Contract') ? AIWEST_Design_Contract::resolve() : []; $fallback_profile = class_exists('AIWEST_Design_Contract') ? AIWEST_Design_Contract::default_profile($contract) : 'editorial'; $profile = self::allowed_value($plan['profile'] ?? '', ['documentation','editorial','cards','landing'], $fallback_profile); $density = self::allowed_value($plan['density'] ?? '', ['compact','balanced','spacious'], 'balanced'); $contract_hash = sanitize_key((string)($contract['hash'] ?? ($plan['design_contract_hash'] ?? ''))); $classes = 'aiwest-visual-layout aiwest-visual-contract-v2 aiwest-visual-profile-' . $profile . ' aiwest-visual-density-' . $density; if (class_exists('AIWEST_Semantic_Layout_Composer')) { $raw_content = AIWEST_Semantic_Layout_Composer::decompose($content); $composed = AIWEST_Semantic_Layout_Composer::compose($raw_content, $profile); $content = AIWEST_Semantic_Layout_Composer::identity_preserved($raw_content, $composed) ? $composed : $raw_content; } return '
' . '
' . $content . '
'; } public static function save_plan($post_id, $plan) { $post_id = absint($post_id); if (!$post_id) return false; $safe_plan = [ 'profile' => self::allowed_value($plan['profile'] ?? '', ['documentation','editorial','cards','landing'], 'editorial'), 'density' => self::allowed_value($plan['density'] ?? '', ['compact','balanced','spacious'], 'balanced'), 'layout_template' => sanitize_key((string)($plan['layout_template'] ?? 'standard_landing')), 'design_contract_hash' => sanitize_key((string)($plan['design_contract_hash'] ?? '')), 'engine' => sanitize_key((string)($plan['engine'] ?? 'deterministic_layout_plan')), 'engine_version' => self::ENGINE_VERSION, ]; update_post_meta($post_id, self::META_PLAN, $safe_plan); return $safe_plan; } public static function unwrap($content) { $content = (string)$content; if (strpos($content, 'aiwest-visual-layout') === false || strpos($content, 'data-aiwest-visual-layout-content') === false) { return $content; } if (!preg_match('/]*\bdata-aiwest-visual-layout-content\s*=\s*(["\'])1\1)[^>]*>/i', $content, $start, PREG_OFFSET_CAPTURE)) { return $content; } if (!preg_match('/<\/div>\s*\s*<\/div>\s*\s*$/is', $content, $end, PREG_OFFSET_CAPTURE)) { return $content; } $start_offset = $start[0][1] + strlen($start[0][0]); $end_offset = $end[0][1]; if ($end_offset < $start_offset) return $content; $inner = substr($content, $start_offset, $end_offset - $start_offset); if (substr($inner, 0, 1) === "\n") $inner = substr($inner, 1); if (substr($inner, -1) === "\n") $inner = substr($inner, 0, -1); return $inner; } public static function enqueue_frontend_styles() { wp_enqueue_style( 'aiwest-visual-layout', AIWEST_URL . 'assets/visual-layout.css', [], defined('AIWEST_VERSION') ? AIWEST_VERSION : null ); } public static function enqueue_editor_styles() { wp_enqueue_style( 'aiwest-visual-layout-editor', AIWEST_URL . 'assets/visual-layout.css', [], defined('AIWEST_VERSION') ? AIWEST_VERSION : null ); } protected static function heuristic_plan($post, $instruction, $contract) { $context = is_array($contract) ? (($contract['brand_style'] ?? '') . ' ' . ($contract['imported_reference'] ?? '')) : ''; $haystack = self::lower((string)($post->post_title ?? '') . ' ' . (string)$instruction . ' ' . $context); $profile = class_exists('AIWEST_Design_Contract') ? AIWEST_Design_Contract::default_profile(is_array($contract) ? $contract : null) : 'editorial'; if (preg_match('/documentation|docs|security|privacy|policy|terms|api|changelog|roadmap|support|faq|dokument|prywat|bezpieczeń|regulamin/u', $haystack)) $profile = 'documentation'; elseif (preg_match('/landing|hero|homepage|home page|strona główna/u', $haystack)) $profile = 'landing'; elseif (preg_match('/cards|grid|features|services|karty|siatka/u', $haystack)) $profile = 'cards'; $density = preg_match('/spacious|generous spacing|dużo przestrzeni|przestronn/u', $haystack) ? 'spacious' : (preg_match('/compact|dense|kompakt/u', $haystack) ? 'compact' : 'balanced'); return ['profile' => $profile, 'density' => $density]; } protected static function decode_json($result) { $clean = trim((string)$result); $clean = preg_replace('/^```(?:json)?\s*/i', '', $clean); $clean = preg_replace('/\s*```$/', '', $clean); $data = json_decode($clean, true); if (is_array($data)) return $data; $first = strpos($clean, '{'); $last = strrpos($clean, '}'); return ($first !== false && $last !== false && $last > $first) ? json_decode(substr($clean, $first, $last - $first + 1), true) : null; } protected static function allowed_value($value, $allowed, $fallback) { $value = sanitize_key((string)$value); return in_array($value, $allowed, true) ? $value : $fallback; } protected static function word_count($text) { preg_match_all('/[\p{L}\p{N}][\p{L}\p{N}\p{M}\x{2019}\'\-]*/u', (string)$text, $matches); return count($matches[0]); } protected static function lower($text) { return function_exists('mb_strtolower') ? mb_strtolower((string)$text, 'UTF-8') : strtolower((string)$text); } } AIWEST_Visual_Layout_Engine::init();