'home', 'title' => __('Home', 'ai-webmaster-studio'), 'type' => 'page', 'slug' => 'home', 'url' => '', 'parent' => '', 'order' => 10], ['id' => 'services', 'title' => __('Services', 'ai-webmaster-studio'), 'type' => 'page', 'slug' => 'services', 'url' => '', 'parent' => '', 'order' => 20], ['id' => 'guides', 'title' => __('Guides', 'ai-webmaster-studio'), 'type' => 'page', 'slug' => 'guides', 'url' => '', 'parent' => '', 'order' => 30], ['id' => 'knowledge-base', 'title' => __('Knowledge Base', 'ai-webmaster-studio'), 'type' => 'page', 'slug' => 'knowledge-base', 'url' => '', 'parent' => '', 'order' => 40], ['id' => 'about', 'title' => __('About', 'ai-webmaster-studio'), 'type' => 'page', 'slug' => 'about', 'url' => '', 'parent' => '', 'order' => 50], ['id' => 'contact', 'title' => __('Contact', 'ai-webmaster-studio'), 'type' => 'page', 'slug' => 'contact', 'url' => '', 'parent' => '', 'order' => 60], ]; } public static function get_saved_menu_plan() { $items = get_option('aiwest_site_menu_plan', []); return self::normalize_menu_plan(is_array($items) && $items ? $items : self::default_menu_items()); } public static function save_menu_plan($items, $sync_dropdown = true) { $clean = self::normalize_menu_plan($items); if (!$clean) $clean = self::default_menu_items(); update_option('aiwest_site_menu_plan', $clean, false); if ($sync_dropdown) { self::sync_menu_plan_to_ai_header_menu($clean, false); } return $clean; } /** * Normalize legacy flat plans and the current two-level parent/child model. * Stable item IDs keep parent assignments intact when labels or order change. */ public static function normalize_menu_plan($items) { $expanded = []; foreach ((array) $items as $item) { if (!is_array($item)) continue; $children = isset($item['children']) && is_array($item['children']) ? $item['children'] : []; unset($item['children']); $expanded[] = $item; $parent_hint = sanitize_key($item['id'] ?? $item['slug'] ?? $item['title'] ?? ''); foreach ($children as $child) { if (!is_array($child)) continue; $child['parent'] = $parent_hint; $expanded[] = $child; } } $clean = []; $used_ids = []; foreach ($expanded as $index => $item) { $title = sanitize_text_field($item['title'] ?? ''); if ($title === '') continue; $type = sanitize_key($item['type'] ?? 'page'); if (!in_array($type, ['page', 'custom'], true)) $type = 'page'; $slug = sanitize_title($item['slug'] ?? $title); $url = $type === 'custom' ? esc_url_raw($item['url'] ?? '') : ''; if ($type === 'page' && $slug === '') continue; if ($type === 'custom' && $url === '') continue; $base_id = sanitize_key($item['id'] ?? $slug ?? $title); if ($base_id === '') $base_id = 'menu-item-' . ($index + 1); $id = $base_id; $suffix = 2; while (isset($used_ids[$id])) { $id = $base_id . '-' . $suffix; $suffix++; } $used_ids[$id] = true; $clean[] = [ 'id' => $id, 'title' => $title, 'type' => $type, 'slug' => $type === 'page' ? $slug : '', 'url' => $url, 'parent' => sanitize_key($item['parent'] ?? ''), 'order' => max(0, intval($item['order'] ?? (($index + 1) * 10))), ]; } $index_by_id = []; foreach ($clean as $index => $item) $index_by_id[$item['id']] = $index; foreach ($clean as $index => $item) { $parent = $item['parent']; $seen = [$item['id'] => true]; while ($parent !== '') { if (isset($seen[$parent]) || !isset($index_by_id[$parent])) { $parent = ''; break; } $seen[$parent] = true; $candidate_parent = $clean[$index_by_id[$parent]]['parent']; if ($candidate_parent === '') break; $parent = $candidate_parent; } $clean[$index]['parent'] = $parent; } usort($clean, static function($a, $b) { $order = intval($a['order']) <=> intval($b['order']); return $order !== 0 ? $order : strcasecmp($a['title'], $b['title']); }); return array_values($clean); } public static function top_level_menu_items($items = null) { $items = $items === null ? self::get_saved_menu_plan() : self::normalize_menu_plan($items); return array_values(array_filter($items, static fn($item) => empty($item['parent']))); } public static function child_menu_items($parent_id, $items = null) { $items = $items === null ? self::get_saved_menu_plan() : self::normalize_menu_plan($items); return array_values(array_filter($items, static fn($item) => ($item['parent'] ?? '') === $parent_id)); } protected static function menu_item_url($item, $create_drafts = false) { if (($item['type'] ?? 'page') === 'custom') return esc_url_raw($item['url'] ?? ''); $slug = sanitize_title($item['slug'] ?? $item['title'] ?? ''); if ($create_drafts) { $page_id = self::ensure_page($item['title'], $slug); if (!is_wp_error($page_id)) return get_permalink($page_id); } $page = get_page_by_path($slug, OBJECT, 'page'); return $page && $page->post_status !== 'trash' ? get_permalink($page->ID) : home_url('/' . $slug . '/'); } public static function sync_menu_plan_to_ai_header_menu($items = null, $create_drafts = false) { if ($items === null) $items = self::get_saved_menu_plan(); $items = self::normalize_menu_plan($items); $synced = []; foreach (self::top_level_menu_items($items) as $item) { $children = []; foreach (self::child_menu_items($item['id'], $items) as $child) { $children[] = ['title' => $child['title'], 'url' => self::menu_item_url($child, $create_drafts)]; } $synced[] = [ 'title' => $item['title'], 'url' => self::menu_item_url($item, $create_drafts), 'columns' => $children ? [['heading' => __('Submenu', 'ai-webmaster-studio'), 'items' => $children]] : [], ]; } if (!$synced && class_exists('AIWEST_AI_Menu')) { $synced = AIWEST_AI_Menu::default_structure(); } update_option('aiwest_dropdown_menu_structure', $synced); update_option('aiwest_dropdown_menu_synced_from_plan', current_time('mysql')); return $synced; } public static function apply_menu_plan_to_ai_header($create_drafts = true) { self::sync_menu_plan_to_ai_header_menu(null, $create_drafts); if (class_exists('AIWEST_AI_Menu')) { return AIWEST_AI_Menu::inject_into_active_header($create_drafts, false); } return new WP_Error('aiwest_menu_missing', 'The AI dropdown menu module is not available.'); } public static function generate_menu_plan($instruction = '') { $lovable = class_exists('AIWEST_Memory') ? AIWEST_Memory::get('lovable_project_summary', '') : ''; $goals = class_exists('AIWEST_Goals') ? AIWEST_Goals::active() : []; $messages = [ ['role' => 'system', 'content' => AIWEST_OpenAI::system_prompt()], ['role' => 'user', 'content' => "Prepare a target menu structure for this WordPress website: " . get_bloginfo('name') . ".\n" . "Administrator instruction: {$instruction}\n\n" . "Project goals:\n" . wp_json_encode($goals, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n" . "Imported design reference:\n" . ($lovable ?: 'No imported design reference.') . "\n\n" . "Return JSON only, without markdown:\n" . "{ \"items\": [ {\"id\":\"services\",\"title\":\"Services\",\"type\":\"page\",\"slug\":\"services\",\"children\":[{\"id\":\"consulting\",\"title\":\"Consulting\",\"type\":\"page\",\"slug\":\"consulting\"}]} ], \"admin_note\":\"short recommendation\" }\n" . "Use at most 10 top-level items and one submenu level. Use page links by default. Use a custom URL only when the administrator explicitly requests it. Keep the structure practical, concise and clear." ], ]; $result = AIWEST_OpenAI::chat($messages, 0.25); if (is_wp_error($result)) return $result; $data = self::decode_json($result); if (!is_array($data) || empty($data['items']) || !is_array($data['items'])) { return new WP_Error('aiwest_menu_bad_json', 'AI did not return a valid menu structure.'); } $items = self::save_menu_plan($data['items']); update_option('aiwest_site_menu_note', sanitize_textarea_field($data['admin_note'] ?? '')); return ['items' => $items, 'admin_note' => sanitize_textarea_field($data['admin_note'] ?? '')]; } public static function ensure_page($title, $slug, $content = '', $status = 'draft') { $slug = sanitize_title($slug); if ($slug === 'ai-directory-map' && class_exists('AIWEST_Directory_Map')) { return AIWEST_Directory_Map::ensure_map_page(false); } $existing = get_page_by_path($slug, OBJECT, 'page'); if ($existing) return intval($existing->ID); $status = sanitize_key($status); if (!in_array($status, ['draft', 'publish'], true)) $status = 'draft'; if ($status === 'publish') { if (get_option('aiwest_auto_publish_posts', 'no') !== 'yes') { $status = 'draft'; } } $aiwest_operator_decision = class_exists('AIWEST_Technical_Operator') ? AIWEST_Technical_Operator::explain_decision('create_page', 'content_page_create', ['post_type' => 'page', 'post_status' => $status]) : []; $post_title = sanitize_text_field($title); $post_content = $content ?: '

Page prepared by AI Webmaster Studio. Content requires administrator review and completion.

'; $heading_normalization = ['content' => $post_content, 'report' => []]; if (class_exists('AIWEST_Page_Title_Heading_Normalizer')) { $heading_normalization = AIWEST_Page_Title_Heading_Normalizer::normalize(0, $post_content, [ 'post_title' => $post_title, 'post_type' => 'page', 'mode' => 'standard', 'theme_title_visible' => true, ]); $post_content = $heading_normalization['content']; } $post_id = wp_insert_post([ 'post_title' => $post_title, 'post_name' => sanitize_title($slug), 'post_content' => $post_content, 'post_status' => $status, 'post_type' => 'page', 'post_author' => class_exists('AIWEST_Technical_Operator') ? AIWEST_Technical_Operator::post_author_id(get_current_user_id()) : (get_current_user_id() ?: 1), ], true); if (is_wp_error($post_id)) return $post_id; update_post_meta($post_id, '_aiwest_generated', 'yes'); update_post_meta($post_id, '_aiwest_source', 'site_structure'); if (class_exists('AIWEST_Page_Title_Heading_Normalizer')) { AIWEST_Page_Title_Heading_Normalizer::record_report($post_id, $heading_normalization['report']); } if (class_exists('AIWEST_Technical_Operator')) { AIWEST_Technical_Operator::annotate_post($post_id, 'create_page', $aiwest_operator_decision); } return intval($post_id); } public static function create_pages_from_menu_plan($status = 'draft') { $items = self::get_saved_menu_plan(); $created = []; foreach ($items as $item) { if (($item['type'] ?? 'page') !== 'page') continue; $id = self::ensure_page($item['title'], $item['slug'], '', $status); if (is_wp_error($id)) return $id; $created[] = ['id' => $id, 'title' => $item['title'], 'slug' => $item['slug'], 'status' => get_post_status($id)]; } return $created; } public static function create_or_update_wp_menu($menu_name = 'AI Webmaster Studio — main menu') { $items = self::get_saved_menu_plan(); $menu = wp_get_nav_menu_object($menu_name); $menu_id = $menu ? intval($menu->term_id) : wp_create_nav_menu($menu_name); if (is_wp_error($menu_id)) return $menu_id; $existing_items = wp_get_nav_menu_items($menu_id); if ($existing_items) { foreach ($existing_items as $existing) { wp_delete_post($existing->ID, true); } } $created_pages = []; $created_menu_items = []; $ordered_items = array_merge(self::top_level_menu_items($items), array_values(array_filter($items, static fn($item) => !empty($item['parent'])))); foreach ($ordered_items as $item) { $page_id = 0; if (($item['type'] ?? 'page') === 'page') { $page_id = self::ensure_page($item['title'], $item['slug']); if (is_wp_error($page_id)) return $page_id; $created_pages[] = $page_id; } $menu_args = [ 'menu-item-title' => $item['title'], 'menu-item-status' => 'publish', 'menu-item-parent-id' => intval($created_menu_items[$item['parent'] ?? ''] ?? 0), ]; if (($item['type'] ?? 'page') === 'custom') { $menu_args['menu-item-type'] = 'custom'; $menu_args['menu-item-url'] = esc_url_raw($item['url']); } else { $menu_args['menu-item-object'] = 'page'; $menu_args['menu-item-object-id'] = $page_id; $menu_args['menu-item-type'] = 'post_type'; } $menu_item_id = wp_update_nav_menu_item($menu_id, 0, $menu_args); if (is_wp_error($menu_item_id)) return $menu_item_id; $created_menu_items[$item['id']] = intval($menu_item_id); } $locations = get_theme_mod('nav_menu_locations', []); $registered = get_registered_nav_menus(); $preferred = ''; foreach (['primary', 'menu-1', 'main', 'primary-menu', 'header-menu'] as $key) { if (isset($registered[$key])) { $preferred = $key; break; } } if (!$preferred && $registered) { $keys = array_keys($registered); $preferred = $keys[0]; } if ($preferred) { $locations[$preferred] = intval($menu_id); set_theme_mod('nav_menu_locations', $locations); } update_option('aiwest_site_menu_id', intval($menu_id)); update_option('aiwest_site_menu_location', sanitize_key($preferred)); self::sync_menu_plan_to_ai_header_menu($items, false); return [ 'menu_id' => intval($menu_id), 'location' => $preferred, 'pages' => $created_pages, 'items' => $created_menu_items, ]; } public static function generate_header_plan($instruction = '') { $brand = get_option('aiwest_visual_brand_style', ''); $items = self::get_saved_menu_plan(); $messages = [ ['role' => 'system', 'content' => AIWEST_OpenAI::system_prompt()], ['role' => 'user', 'content' => "Prepare a header structure for this WordPress website: " . get_bloginfo('name') . ".\n" . "Administrator instruction: {$instruction}\n" . "Brand style: {$brand}\n" . "Menu: " . wp_json_encode($items, JSON_UNESCAPED_UNICODE) . "\n\n" . "Return JSON only, without markdown:\n" . "{ \"site_name\":\"...\", \"tagline\":\"...\", \"cta_label\":\"...\", \"cta_target\":\"...\", \"announcement\":\"...\", \"admin_note\":\"...\" }\n" . "This must be a safe header recommendation and content draft, without changing theme settings." ], ]; $result = AIWEST_OpenAI::chat($messages, 0.25); if (is_wp_error($result)) return $result; $data = self::decode_json($result); if (!is_array($data)) return new WP_Error('aiwest_header_bad_json', 'AI did not return valid header JSON.'); update_option('aiwest_header_plan', $data); return $data; } public static function generate_footer_plan($instruction = '') { $items = self::get_saved_menu_plan(); $brand = get_option('aiwest_visual_brand_style', ''); $messages = [ ['role' => 'system', 'content' => AIWEST_OpenAI::system_prompt()], ['role' => 'user', 'content' => "Prepare footer content and structure for this WordPress website: " . get_bloginfo('name') . ".\n" . "Administrator instruction: {$instruction}\n" . "Brand style: {$brand}\n" . "Main menu: " . wp_json_encode($items, JSON_UNESCAPED_UNICODE) . "\n\n" . "Return JSON only, without markdown:\n" . "{ \"description\":\"short website description\", \"columns\":[{\"title\":\"...\",\"links\":[{\"label\":\"...\",\"url\":\"...\"}]}], \"cta\":\"...\", \"legal_note\":\"...\", \"admin_note\":\"...\" }\n" . "The footer must be practical, professional and ready for manual or later automated implementation in the theme." ], ]; $result = AIWEST_OpenAI::chat($messages, 0.25); if (is_wp_error($result)) return $result; $data = self::decode_json($result); if (!is_array($data)) return new WP_Error('aiwest_footer_bad_json', 'AI did not return valid footer JSON.'); update_option('aiwest_footer_plan', $data); return $data; } public static function create_header_draft_page($status = 'draft') { $status = in_array($status, ['draft', 'publish'], true) ? $status : 'draft'; $status = in_array($status, ['draft', 'publish'], true) ? $status : 'draft'; $plan = get_option('aiwest_header_plan', []); if (!$plan || !is_array($plan)) { $plan = self::generate_header_plan('Prepare a basic website header.'); if (is_wp_error($plan)) return $plan; } $html = '
'; $html .= '
'; $html .= '
'; $html .= '

Header project

'; $html .= '

' . esc_html($plan['site_name'] ?? get_bloginfo('name')) . '

'; $html .= '

' . esc_html($plan['tagline'] ?? '') . '

'; $html .= '
'; $html .= '
'; foreach (self::top_level_menu_items() as $item) { $html .= '' . esc_html($item['title']) . ''; } $html .= '
'; if (!empty($plan['cta_label'])) { $html .= '' . esc_html($plan['cta_label']) . ''; } $html .= '
'; if (!empty($plan['announcement'])) { $html .= '
' . esc_html($plan['announcement']) . '
'; } $html .= '
'; $page_id = self::ensure_page(__('AI Webmaster Studio Header Project', 'ai-webmaster-studio'), 'aiwest-header-project', $html); if (is_wp_error($page_id)) return $page_id; wp_update_post([ 'ID' => $page_id, 'post_content' => wp_kses_post($html), 'post_status' => $status, ]); update_option('aiwest_active_header_page_id', intval($page_id)); return intval($page_id); } public static function create_footer_draft_page($status = 'draft') { $status = in_array($status, ['draft', 'publish'], true) ? $status : 'draft'; $status = in_array($status, ['draft', 'publish'], true) ? $status : 'draft'; $plan = get_option('aiwest_footer_plan', []); if (!$plan || !is_array($plan)) { $plan = self::generate_footer_plan('Prepare a basic website footer.'); if (is_wp_error($plan)) return $plan; } $html = ''; $page_id = self::ensure_page(__('AI Webmaster Studio Footer Project', 'ai-webmaster-studio'), 'aiwest-footer-project', $html); if (is_wp_error($page_id)) return $page_id; wp_update_post([ 'ID' => $page_id, 'post_content' => wp_kses_post($html), 'post_status' => $status, ]); update_option('aiwest_active_footer_page_id', intval($page_id)); return intval($page_id); } public static function site_status() { $front_id = intval(get_option('page_on_front', 0)); $menu_plan = self::get_saved_menu_plan(); $menu_id = intval(get_option('aiwest_site_menu_id', 0)); $header = get_option('aiwest_header_plan', []); $footer = get_option('aiwest_footer_plan', []); $pages_existing = 0; $pages_missing = []; foreach ($menu_plan as $item) { $existing = get_page_by_path($item['slug'], OBJECT, 'page'); if ($existing) { $pages_existing++; } else { $pages_missing[] = $item['title']; } } return [ 'front_id' => $front_id, 'front_title' => $front_id ? get_the_title($front_id) : '', 'menu_items' => count($menu_plan), 'menu_id' => $menu_id, 'menu_location' => get_option('aiwest_site_menu_location', ''), 'header_ready' => is_array($header) && !empty($header), 'footer_ready' => is_array($footer) && !empty($footer), 'pages_existing' => $pages_existing, 'pages_missing' => $pages_missing, ]; } public static function decode_json($result) { $clean = trim((string)$result); $clean = preg_replace('/^```json\s*/i', '', $clean); $clean = preg_replace('/^```\s*/', '', $clean); $clean = preg_replace('/\s*```$/', '', $clean); $data = json_decode($clean, true); if (!is_array($data)) { $first = strpos($clean, '{'); $last = strrpos($clean, '}'); if ($first !== false && $last !== false && $last > $first) { $data = json_decode(substr($clean, $first, $last - $first + 1), true); } } return $data; } }