igny8_get_ai_setting('planner_mode', 'manual'), 'clustering' => igny8_get_ai_setting('clustering', 'enabled'), 'ideas' => igny8_get_ai_setting('ideas', 'enabled'), 'mapping' => igny8_get_ai_setting('mapping', 'enabled'), 'prompts' => [ 'clustering' => igny8_get_ai_setting('clustering_prompt', igny8_get_default_clustering_prompt()), 'ideas' => igny8_get_ai_setting('ideas_prompt', igny8_get_default_ideas_prompt()) ] ]; } /** * Get Writer AI settings */ function igny8_get_writer_ai_settings() { return [ 'writer_mode' => igny8_get_ai_setting('writer_mode', 'manual'), 'content_generation' => igny8_get_ai_setting('content_generation', 'enabled'), 'prompts' => [ 'content_generation' => get_option('igny8_content_generation_prompt', igny8_content_generation_prompt()) ] ]; } /** * Process AI request with prompt template */ function igny8_process_ai_request($action, $data, $prompt_template) { // Log AI processing start igny8_log_ai_event('AI Processing Started', 'ai', $action, 'info', 'Starting AI request processing', 'Action: ' . $action . ', Data count: ' . count($data)); // Replace shortcodes with actual data $prompt = $prompt_template; switch ($action) { case 'clustering': $keywords_data = igny8_format_keywords_for_ai($data); $prompt = str_replace('[IGNY8_KEYWORDS]', $keywords_data, $prompt); // Add sector information if multiple sectors are configured $sector_options = igny8_get_sector_options(); if (count($sector_options) > 1) { $sector_names = array_column($sector_options, 'label'); $sector_text = "\n\nAvailable sectors: " . implode(', ', $sector_names) . "\nAssign each cluster to the most suitable sector from the above list."; $prompt .= $sector_text; } igny8_log_ai_event('Prompt Preparation', 'ai', $action, 'info', 'Keywords data formatted for prompt', 'Keywords: ' . substr($keywords_data, 0, 100) . '...'); break; case 'ideas': $clusters_data = igny8_format_clusters_for_ai($data); $cluster_keywords_data = igny8_format_cluster_keywords_for_ai($data); $prompt = str_replace('[IGNY8_CLUSTERS]', $clusters_data, $prompt); $prompt = str_replace('[IGNY8_CLUSTER_KEYWORDS]', $cluster_keywords_data, $prompt); break; case 'mapping': $content_data = igny8_format_content_for_ai($data['content']); $clusters_data = igny8_format_clusters_for_ai($data['clusters']); $prompt = str_replace('[IGNY8_CONTENT]', $content_data, $prompt); $prompt = str_replace('[IGNY8_CLUSTERS]', $clusters_data, $prompt); break; case 'content_generation': $idea_data = igny8_format_idea_for_ai($data['idea']); $cluster_data = igny8_format_cluster_for_ai($data['cluster']); $keywords_data = igny8_format_keywords_for_ai($data['keywords']); $max_in_article_images = get_option('igny8_max_in_article_images', 1); // Safety check: Analyze outline to ensure we don't exceed available H2 sections $safe_max_images = igny8_calculate_safe_image_quantity($idea_data, $max_in_article_images); $image_prompts_data = igny8_format_image_prompts_for_ai($safe_max_images); $prompt = str_replace('[IGNY8_IDEA]', $idea_data, $prompt); $prompt = str_replace('[IGNY8_CLUSTER]', $cluster_data, $prompt); $prompt = str_replace('[IGNY8_KEYWORDS]', $keywords_data, $prompt); $prompt = str_replace('[IGNY8_DESKTOP_QUANTITY]', $safe_max_images, $prompt); $prompt = str_replace('[IMAGE_PROMPTS]', $image_prompts_data, $prompt); // Content generation prompt is now self-contained with 3-part structure igny8_log_ai_event('Prompt Preparation', 'ai', $action, 'info', 'Content generation data formatted', 'Idea: ' . substr($idea_data, 0, 100) . '..., Max In-Article Images: ' . $max_in_article_images . ', Safe Max: ' . $safe_max_images); break; } // Check if OpenAI function exists if (!function_exists('igny8_call_openai')) { if (defined('DOING_CRON') && DOING_CRON) { error_log("Igny8 AI Process: igny8_call_openai function not found"); } igny8_log_ai_event('AI Function Missing', 'ai', $action, 'error', 'igny8_call_openai function not found', 'OpenAI integration not available'); return false; } // Get API configuration $api_key = get_option('igny8_api_key'); $model = get_option('igny8_model', 'gpt-4.1'); if (empty($api_key)) { if (defined('DOING_CRON') && DOING_CRON) { error_log("Igny8 AI Process: API key is empty or missing"); } igny8_log_ai_event('API Key Missing', 'ai', $action, 'error', 'OpenAI API key not configured', 'Please configure API key in settings'); return false; } igny8_log_ai_event('OpenAI API Call', 'ai', $action, 'info', 'Calling OpenAI API', 'Model: ' . $model . ', Prompt length: ' . strlen($prompt)); // Debug logging for CRON context if (defined('DOING_CRON') && DOING_CRON) { error_log("Igny8 AI Process: Making OpenAI API call - Model: " . $model . ", Prompt length: " . strlen($prompt)); } // Call OpenAI API $response = igny8_call_openai($prompt, $api_key, $model); if (defined('DOING_CRON') && DOING_CRON) { error_log("Igny8 AI Process: OpenAI response received: " . ($response ? 'Success' : 'Failed')); if ($response && strlen($response) > 0) { error_log("Igny8 AI Process: Response length: " . strlen($response) . " characters"); } } if (!$response) { if (defined('DOING_CRON') && DOING_CRON) { error_log("Igny8 AI Process: OpenAI API returned no response - this is the failure point"); } igny8_log_ai_event('OpenAI API Failed', 'ai', $action, 'error', 'OpenAI API returned no response', 'Check API key and network connection'); return false; } // Check if response starts with "Error:" if (strpos($response, 'Error:') === 0) { $error_details = [ 'error_message' => $response, 'model' => $model, 'prompt_length' => strlen($prompt), 'api_key_configured' => !empty($api_key), 'timestamp' => current_time('mysql') ]; igny8_log_ai_event('OpenAI API Error', 'ai', $action, 'error', 'OpenAI API returned error', 'Error: ' . $response . ' | Details: ' . json_encode($error_details)); return false; } // igny8_call_openai returns the content directly, not wrapped in an array igny8_log_ai_event('OpenAI Response Received', 'ai', $action, 'info', 'Raw response from OpenAI', 'Response length: ' . strlen($response) . ', Preview: ' . substr($response, 0, 100) . '...'); // Parse JSON response - try to extract JSON from response $json_result = igny8_extract_json_from_response($response); if (!$json_result) { $error_details = [ 'raw_response' => $response, 'response_length' => strlen($response), 'model' => $model, 'action' => $action, 'timestamp' => current_time('mysql') ]; igny8_log_ai_event('JSON Parse Failed', 'ai', $action, 'error', 'Failed to parse OpenAI response as JSON', 'Raw response: ' . substr($response, 0, 500) . '... | Details: ' . json_encode($error_details)); return false; } igny8_log_ai_event('OpenAI Success', 'ai', $action, 'success', 'OpenAI API returned valid JSON', 'Result keys: ' . json_encode(array_keys($json_result))); // Normalize image prompts structure if needed // Handle case where AI returns featured_image and in_article_images at top level if (isset($json_result['featured_image']) && !isset($json_result['image_prompts'])) { $json_result['image_prompts'] = [ 'featured_image' => $json_result['featured_image'], 'in_article_images' => $json_result['in_article_images'] ?? [] ]; igny8_log_ai_event('Image Prompts Normalized', 'ai', $action, 'info', 'Image prompts structure normalized from top-level fields', 'Moved featured_image and in_article_images to image_prompts object'); } return $json_result; } /** * Parse 3-part response structure */ function igny8_parse_three_part_response($response) { $result = []; // Extract metadata JSON if (preg_match('/##Metadata Fields JSON##\s*(\{.*?\})/s', $response, $matches)) { $metadata = json_decode($matches[1], true); if ($metadata) { $result['metadata'] = $metadata; } } // Extract content-related JSON if (preg_match('/##Content-Related JSON##\s*(\{.*?\})/s', $response, $matches)) { $content_data = json_decode($matches[1], true); if ($content_data) { $result['content_data'] = $content_data; } } // Extract image prompts if (preg_match('/##Image Prompts Requirements:##.*?\[IMAGE_PROMPTS\]/s', $response, $matches)) { $result['image_prompts'] = $matches[0]; } // If we found at least one part, return the result if (!empty($result)) { return $result; } return null; } /** * Extract JSON from AI response (handles cases where AI adds extra text) */ function igny8_extract_json_from_response($response) { // First, try to parse the response directly as JSON $json_result = json_decode($response, true); if ($json_result) { return $json_result; } // Try to parse 3-part structure $three_part_result = igny8_parse_three_part_response($response); if ($three_part_result) { return $three_part_result; } // If that fails, try to find JSON within the response // Look for content between curly braces if (preg_match('/\{.*\}/s', $response, $matches)) { $json_result = json_decode($matches[0], true); if ($json_result) { return $json_result; } } // Handle GPT-4o-mini format: ```json { ... } ``` or """json { ... } """ if (preg_match('/```json\s*(\{.*?\})\s*```/s', $response, $matches)) { $json_result = json_decode($matches[1], true); if ($json_result) { return $json_result; } } if (preg_match('/"""json\s*(\{.*?\})\s*"""/s', $response, $matches)) { $json_result = json_decode($matches[1], true); if ($json_result) { return $json_result; } } // If still no luck, try to clean the response $cleaned_response = trim($response); // Remove common prefixes that AI might add $prefixes_to_remove = [ 'Here is the JSON response:', 'Here\'s the JSON:', 'JSON Response:', '```json', '```', '"""json', '"""', 'Here is the content in JSON format:', 'The JSON response is:' ]; foreach ($prefixes_to_remove as $prefix) { if (stripos($cleaned_response, $prefix) === 0) { $cleaned_response = trim(substr($cleaned_response, strlen($prefix))); } } // Remove common suffixes $suffixes_to_remove = [ '```', '"""', 'This JSON contains all the required fields.', 'Hope this helps!', 'Let me know if you need any modifications.' ]; foreach ($suffixes_to_remove as $suffix) { $pos = stripos($cleaned_response, $suffix); if ($pos !== false) { $cleaned_response = trim(substr($cleaned_response, 0, $pos)); } } // Try parsing the cleaned response $json_result = json_decode($cleaned_response, true); if ($json_result) { return $json_result; } // Last resort: try to find and extract JSON object if (preg_match('/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/', $response, $matches)) { $json_result = json_decode($matches[0], true); if ($json_result) { return $json_result; } } return false; } /** * Set post categories from AI response */ function igny8_set_post_categories($post_id, $categories) { if (empty($categories) || !is_array($categories)) { return; } $category_ids = []; foreach ($categories as $category) { $category_id = igny8_get_or_create_category($category); if ($category_id) { $category_ids[] = $category_id; } } if (!empty($category_ids)) { wp_set_post_categories($post_id, $category_ids); } } /** * Get or create category from category string */ function igny8_get_or_create_category($category_string) { if (empty($category_string)) { return false; } // Handle parent > child format if (strpos($category_string, ' > ') !== false) { $parts = explode(' > ', $category_string); $parent_name = trim($parts[0]); $child_name = trim($parts[1]); // Create or get parent category $parent_term = get_term_by('name', $parent_name, 'category'); if (!$parent_term) { $parent_result = wp_insert_term($parent_name, 'category'); if (!is_wp_error($parent_result)) { $parent_id = $parent_result['term_id']; } else { return false; } } else { $parent_id = $parent_term->term_id; } // Create or get child category $child_term = get_term_by('name', $child_name, 'category'); if (!$child_term) { $child_result = wp_insert_term($child_name, 'category', ['parent' => $parent_id]); if (!is_wp_error($child_result)) { return $child_result['term_id']; } } else { return $child_term->term_id; } } else { // Single category $term = get_term_by('name', $category_string, 'category'); if (!$term) { $result = wp_insert_term($category_string, 'category'); if (!is_wp_error($result)) { return $result['term_id']; } } else { return $term->term_id; } } return false; } /** * Store cluster and sector metadata */ function igny8_store_content_metadata($post_id, $ai_response) { global $wpdb; // Store cluster and sector info if available $cluster_id = $ai_response['cluster_id'] ?? null; $sector_id = $ai_response['sector_id'] ?? null; if ($cluster_id) { update_post_meta($post_id, '_igny8_cluster_id', $cluster_id); } if ($sector_id) { update_post_meta($post_id, '_igny8_sector_id', $sector_id); } // Store keywords if available if (!empty($ai_response['keywords_used'])) { update_post_meta($post_id, '_igny8_keywords_used', wp_json_encode($ai_response['keywords_used'])); } } /** * Log AI event for debugging */ function igny8_log_ai_event($event, $module, $action, $status = 'info', $message = '', $details = '') { $ai_logs = get_option('igny8_ai_logs', []); $log_entry = [ 'timestamp' => current_time('mysql'), 'event' => $event, 'module' => $module, 'action' => $action, 'status' => $status, 'message' => $message, 'details' => $details ]; // Add to beginning of array (newest first) array_unshift($ai_logs, $log_entry); // Keep only last 100 events $ai_logs = array_slice($ai_logs, 0, 100); update_option('igny8_ai_logs', $ai_logs); } /** * Format keywords data for AI processing */ function igny8_format_keywords_for_ai($keywords) { $formatted = []; foreach ($keywords as $keyword) { $formatted[] = [ 'id' => isset($keyword->id) ? $keyword->id : null, 'keyword' => isset($keyword->keyword) ? $keyword->keyword : '', 'search_volume' => isset($keyword->search_volume) ? $keyword->search_volume : 0, 'difficulty' => isset($keyword->difficulty) ? $keyword->difficulty : 0 ]; } return json_encode($formatted, JSON_PRETTY_PRINT); } /** * Format clusters data for AI processing */ function igny8_format_clusters_for_ai($clusters) { $formatted = []; foreach ($clusters as $cluster) { $formatted[] = [ 'id' => $cluster->id, 'name' => $cluster->cluster_name, 'sector_id' => $cluster->sector_id, 'keyword_count' => $cluster->keyword_count, 'keywords' => $cluster->keywords_list ?? '' ]; } return json_encode($formatted, JSON_PRETTY_PRINT); } /** * Format cluster keywords data for AI processing */ function igny8_format_cluster_keywords_for_ai($clusters) { $formatted = []; foreach ($clusters as $cluster) { $formatted[] = [ 'cluster_id' => $cluster->id, 'cluster_name' => $cluster->cluster_name, 'keywords' => $cluster->keywords_list ? explode(', ', $cluster->keywords_list) : [] ]; } return json_encode($formatted, JSON_PRETTY_PRINT); } /** * Format content data for AI processing */ function igny8_format_content_for_ai($content) { $formatted = []; foreach ($content as $item) { $formatted[] = [ 'id' => $item->ID, 'title' => $item->post_title, 'content' => wp_strip_all_tags($item->post_content), 'type' => $item->post_type, 'excerpt' => $item->post_excerpt ]; } return json_encode($formatted, JSON_PRETTY_PRINT); } /** * Add AI processing task to queue */ function igny8_add_ai_queue_task($action, $data, $user_id = null) { global $wpdb; $user_id = $user_id ?: get_current_user_id(); $result = $wpdb->insert( $wpdb->prefix . 'igny8_ai_queue', [ 'action' => $action, 'data' => json_encode($data), 'user_id' => $user_id, 'status' => 'pending', 'created_at' => current_time('mysql'), 'processed_at' => null, 'result' => null, 'error_message' => null ], ['%s', '%s', '%d', '%s', '%s', '%s', '%s', '%s'] ); return $result ? $wpdb->insert_id : false; } /** * Process AI queue tasks */ function igny8_process_ai_queue($limit = null) { if ($limit === null) { error_log('Igny8 AI Queue: No limit provided'); return 0; } global $wpdb; // Get pending tasks $tasks = $wpdb->get_results($wpdb->prepare(" SELECT * FROM {$wpdb->prefix}igny8_ai_queue WHERE status = 'pending' ORDER BY created_at ASC LIMIT %d ", $limit)); $processed = 0; foreach ($tasks as $task) { // Mark as processing $wpdb->update( $wpdb->prefix . 'igny8_ai_queue', ['status' => 'processing'], ['id' => $task->id], ['%s'], ['%d'] ); try { $data = json_decode($task->data, true); $result = igny8_process_ai_request($task->action, $data, igny8_get_ai_prompt_for_action($task->action)); if ($result) { // Mark as completed $wpdb->update( $wpdb->prefix . 'igny8_ai_queue', [ 'status' => 'completed', 'processed_at' => current_time('mysql'), 'result' => json_encode($result) ], ['id' => $task->id], ['%s', '%s', '%s'], ['%d'] ); // Process the result based on action igny8_process_ai_queue_result($task->action, $result); } else { // Mark as failed $wpdb->update( $wpdb->prefix . 'igny8_ai_queue', [ 'status' => 'failed', 'processed_at' => current_time('mysql'), 'error_message' => 'AI processing returned no result' ], ['id' => $task->id], ['%s', '%s', '%s'], ['%d'] ); } } catch (Exception $e) { // Mark as failed with error $wpdb->update( $wpdb->prefix . 'igny8_ai_queue', [ 'status' => 'failed', 'processed_at' => current_time('mysql'), 'error_message' => $e->getMessage() ], ['id' => $task->id], ['%s', '%s', '%s'], ['%d'] ); } $processed++; } return $processed; } /** * Get AI prompt for specific action */ function igny8_get_ai_prompt_for_action($action) { switch ($action) { case 'clustering': return igny8_get_ai_setting('clustering_prompt', igny8_get_default_clustering_prompt()); case 'ideas': return igny8_get_ai_setting('ideas_prompt', igny8_get_default_ideas_prompt()); case 'content_generation': return get_option('igny8_content_generation_prompt', igny8_content_generation_prompt()); default: return ''; } } /** * Format idea data for AI processing */ function igny8_format_idea_for_ai($idea) { if (is_object($idea)) { // Handle structured description (JSON) vs plain text $description = $idea->idea_description ?? ''; // Check if description is JSON and format it for AI if (!empty($description)) { $decoded = json_decode($description, true); if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { // Format structured description for AI $formatted_description = igny8_format_structured_description_for_ai($decoded); } else { // Use as plain text $formatted_description = $description; } } else { $formatted_description = ''; } return sprintf( "Title: %s\nDescription: %s\nStructure: %s\nType: %s\nPriority: %s\nEstimated Word Count: %s\nStatus: %s", $idea->idea_title ?? '', $formatted_description, $idea->content_structure ?? '', $idea->content_type ?? '', $idea->priority ?? '', $idea->estimated_word_count ?? '', $idea->status ?? '' ); } return 'Idea data not available'; } /** * Format structured description for AI processing */ function igny8_format_structured_description_for_ai($structured_description) { if (!is_array($structured_description) || empty($structured_description['H2'])) { return 'No structured outline available'; } $formatted = "Content Outline:\n\n"; foreach ($structured_description['H2'] as $h2_section) { $formatted .= "## " . $h2_section['heading'] . "\n"; if (!empty($h2_section['subsections'])) { foreach ($h2_section['subsections'] as $h3_section) { $formatted .= "### " . $h3_section['subheading'] . "\n"; $formatted .= "Content Type: " . $h3_section['content_type'] . "\n"; $formatted .= "Details: " . $h3_section['details'] . "\n\n"; } } } return $formatted; } /** * Format cluster data for AI processing */ function igny8_format_cluster_for_ai($cluster) { if (is_object($cluster)) { return sprintf( "Cluster Name: %s\nDescription: %s\nStatus: %s\nKeyword Count: %s\nTotal Volume: %s\nAverage Difficulty: %s", $cluster->cluster_name ?? '', $cluster->description ?? '', $cluster->status ?? '', $cluster->keyword_count ?? 0, $cluster->total_volume ?? 0, $cluster->avg_difficulty ?? 0 ); } return 'Cluster data not available'; } /** * Format image prompts structure for AI content generation * * @param int $max_in_article_images Number of in-article images to generate * @return string JSON structure for image prompts */ function igny8_format_image_prompts_for_ai($max_in_article_images = 1) { $image_prompts = [ 'featured_image' => '[Detailed prompt for featured/hero image based on the article title and main topic]', 'in_article_images' => [] ]; // Generate in-article image prompts based on quantity // Each prompt corresponds to H2 sections: 1st H2, 2nd H2, 3rd H2, etc. for ($i = 1; $i <= $max_in_article_images; $i++) { $section_number = $i; // Start from 1st H2 (section 1) $image_prompts['in_article_images'][] = [ 'prompt-img-' . $i => '[Detailed image prompt based on topics of section ' . $section_number . '(' . $section_number . getOrdinalSuffix($section_number) . ' H2 in outline)]' ]; } return wp_json_encode($image_prompts, JSON_PRETTY_PRINT); } /** * Get ordinal suffix for numbers (1st, 2nd, 3rd, 4th, etc.) * * @param int $number The number to get ordinal suffix for * @return string The ordinal suffix */ function getOrdinalSuffix($number) { $ends = ['th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th']; if ((($number % 100) >= 11) && (($number % 100) <= 13)) { return 'th'; } return $ends[$number % 10]; } /** * Create responsive image data for image tracking * * @param int $post_id WordPress post ID * @return void */ function igny8_create_responsive_image_data($post_id) { // Get article images data $article_images_data = get_post_meta($post_id, '_igny8_article_images_data', true); if (empty($article_images_data)) { return; } $article_images_data = json_decode($article_images_data, true); if (!is_array($article_images_data) || empty($article_images_data)) { return; } // Create responsive data structure $responsive_data = []; foreach ($article_images_data as $index => $image_data) { // Find the prompt key (prompt-img-X) $prompt_key = null; $prompt_value = null; foreach ($image_data as $key => $value) { if (strpos($key, 'prompt-img-') === 0) { $prompt_key = $key; $prompt_value = $value; break; } } if (!$prompt_key || !$prompt_value) { continue; } // Extract image number from prompt key $image_number = str_replace('prompt-img-', '', $prompt_key); $section_number = intval($image_number) + 1; // 2nd, 3rd, 4th H2, etc. // Create responsive data entry $responsive_data[] = [ 'desktop' => [ 'attachment_id' => null, // Will be filled when images are generated 'url' => null ], 'mobile' => [ 'attachment_id' => null, // Will be filled when images are generated 'url' => null ], 'section' => 'Section ' . $section_number, 'prompt' => $prompt_value ]; } // Save responsive data update_post_meta($post_id, '_igny8_article_images_responsive', wp_json_encode($responsive_data)); } /** * Calculate safe image quantity based on outline analysis * * @param string $idea_data The idea data containing the outline * @param int $max_in_article_images Maximum images requested * @return int Safe maximum images (max 1 less than total H2 sections) */ function igny8_calculate_safe_image_quantity($idea_data, $max_in_article_images) { // Count H2 sections in the outline $h2_count = igny8_count_h2_sections_in_outline($idea_data); // Safety rule: Max images = H2 count - 1 (since we start from 2nd H2) $max_possible_images = max(0, $h2_count - 1); // Use the smaller of requested max or possible max $safe_max = min($max_in_article_images, $max_possible_images); // Log the safety calculation igny8_log_ai_event('Image Quantity Safety Check', 'ai', 'content_generation', 'info', 'Calculated safe image quantity', 'H2 Sections: ' . $h2_count . ', Requested: ' . $max_in_article_images . ', Safe Max: ' . $safe_max ); return $safe_max; } /** * Count H2 sections in the outline * * @param string $idea_data The idea data containing the outline * @return int Number of H2 sections found */ function igny8_count_h2_sections_in_outline($idea_data) { // Look for H2 patterns in the outline // Common patterns: "## ", "**", "H2:", "Section", etc. $patterns = [ '/##\s+/', // Markdown H2: ## Section '/\*\*[^*]+\*\*/', // Bold text: **Section** '/H2:\s*[^\n]+/', // H2: Section '/Section\s+\d+/i', // Section 1, Section 2, etc. '/\d+\.\s+[A-Z][^\.]+\./', // Numbered sections: 1. Section Title. '/^[A-Z][^\.]+\.$/m' // Title case sections ending with period ]; $max_count = 0; foreach ($patterns as $pattern) { preg_match_all($pattern, $idea_data, $matches); $count = count($matches[0]); if ($count > $max_count) { $max_count = $count; } } // If no clear H2 patterns found, estimate based on content length if ($max_count === 0) { // Estimate: roughly 1 H2 per 200-300 words in outline $word_count = str_word_count($idea_data); $max_count = max(3, floor($word_count / 250)); // Minimum 3 sections } // Ensure we have at least 2 sections (1 for featured, 1 for in-article) return max(2, $max_count); } /** * Add in-article image to post meta for meta box integration * * @param int $post_id WordPress post ID * @param int $attachment_id WordPress attachment ID * @param string $label Image label (e.g., 'desktop-1', 'mobile-2') * @param string $device Device type ('desktop' or 'mobile') * @param int|null $section Section number (optional) * @return bool Success status */ function igny8_add_inarticle_image_meta($post_id, $attachment_id, $label, $device = 'desktop', $section = null) { error_log("[IGNY8 DEBUG] igny8_add_inarticle_image_meta called with post_id: $post_id, attachment_id: $attachment_id, label: $label, device: $device, section: $section"); $url = wp_get_attachment_url($attachment_id); if (!$url) { error_log("[IGNY8 DEBUG] Failed to get attachment URL for attachment ID: $attachment_id"); return false; } $images = get_post_meta($post_id, '_igny8_inarticle_images', true); if (!is_array($images)) { $images = []; } $images[$label] = [ 'label' => $label, 'attachment_id' => $attachment_id, 'url' => $url, 'device' => $device, 'section' => $section, ]; error_log("[IGNY8 DEBUG] About to save images meta for post $post_id: " . print_r($images, true)); $result = update_post_meta($post_id, '_igny8_inarticle_images', $images); error_log("[IGNY8 DEBUG] update_post_meta result: " . ($result ? 'SUCCESS' : 'FAILED')); return $result !== false; } /** * Process AI queue result and save to database */ function igny8_process_ai_queue_result($action, $result) { global $wpdb; switch ($action) { case 'clustering': if (isset($result['clusters'])) { // Get sector options for assignment logic $sector_options = igny8_get_sector_options(); $sector_count = count($sector_options); foreach ($result['clusters'] as $cluster_data) { // Determine sector_id based on sector count $sector_id = 1; // Default fallback if ($sector_count == 1) { // Only 1 sector: assign all clusters to that sector $sector_id = $sector_options[0]['value']; } elseif ($sector_count > 1) { // Multiple sectors: use AI response sector assignment if (isset($cluster_data['sector']) && !empty($cluster_data['sector'])) { // Find sector ID by matching sector name from AI response foreach ($sector_options as $sector) { if (strtolower(trim($sector['label'])) === strtolower(trim($cluster_data['sector']))) { $sector_id = $sector['value']; break; } } } // If no match found or no sector in AI response, use first sector as fallback if ($sector_id == 1 && !isset($cluster_data['sector'])) { $sector_id = $sector_options[0]['value']; } } $wpdb->insert( $wpdb->prefix . 'igny8_clusters', [ 'cluster_name' => sanitize_text_field($cluster_data['name']), 'sector_id' => $sector_id, 'status' => 'active', 'keyword_count' => count($cluster_data['keywords']), 'total_volume' => 0, 'avg_difficulty' => 0, 'mapped_pages_count' => 0, 'created_at' => current_time('mysql') ], ['%s', '%d', '%s', '%d', '%d', '%f', '%d', '%s'] ); $cluster_id = $wpdb->insert_id; // Trigger taxonomy term creation for AI-generated cluster do_action('igny8_cluster_added', $cluster_id); // Update keywords with cluster_id foreach ($cluster_data['keywords'] as $keyword_name) { $wpdb->update( $wpdb->prefix . 'igny8_keywords', ['cluster_id' => $cluster_id], ['keyword' => $keyword_name], ['%d'], ['%s'] ); } igny8_update_cluster_metrics($cluster_id); } } break; case 'ideas': if (isset($result['ideas'])) { foreach ($result['ideas'] as $idea_data) { $wpdb->insert( $wpdb->prefix . 'igny8_content_ideas', [ 'idea_title' => sanitize_text_field($idea_data['title']), 'idea_description' => sanitize_textarea_field($idea_data['description']), 'content_structure' => sanitize_text_field($idea_data['type']), 'content_type' => 'post', // Default to post for AI generated ideas 'keyword_cluster_id' => intval($idea_data['cluster_id']), 'priority' => sanitize_text_field($idea_data['priority']), 'status' => 'draft', 'estimated_word_count' => intval($idea_data['estimated_word_count']), 'ai_generated' => 1, 'created_at' => current_time('mysql') ], ['%s', '%s', '%s', '%d', '%s', '%s', '%d', '%d', '%s'] ); } } break; case 'mapping': if (isset($result['mappings'])) { foreach ($result['mappings'] as $mapping_data) { if ($mapping_data['relevance_score'] >= 0.7) { $cluster_term_id = $wpdb->get_var($wpdb->prepare(" SELECT cluster_term_id FROM {$wpdb->prefix}igny8_clusters WHERE id = %d ", $mapping_data['cluster_id'])); if ($cluster_term_id) { wp_set_object_terms( $mapping_data['content_id'], $cluster_term_id, 'clusters', true ); } } } } break; case 'content_generation': if (isset($result['title']) && isset($result['content'])) { // Pass task_id through the AI response for proper task updating if (isset($data['task_id'])) { $result['task_id'] = $data['task_id']; } // Pass cluster and sector data from original task if (isset($data['cluster']) && $data['cluster']) { $result['cluster_id'] = $data['cluster']->id; $result['sector_id'] = $data['cluster']->sector_id; } // Create WordPress post from AI response $post_id = igny8_create_post_from_ai_response($result); if ($post_id) { // Log successful content generation igny8_log_ai_event('content_created', 'writer', 'content_generation', 'success', 'Post created successfully', "Post ID: {$post_id}"); } else { // Log failure igny8_log_ai_event('content_failed', 'writer', 'content_generation', 'error', 'Failed to create post from AI response'); } } else { // Log invalid response format igny8_log_ai_event('invalid_response', 'writer', 'content_generation', 'error', 'AI response missing required fields (title, content)'); } break; } } /** * Create WordPress post from AI response */ function igny8_create_post_from_ai_response($ai_response) { global $wpdb; try { // Get cluster and sector data from the task, not from AI response $cluster_id = null; $sector_id = null; if (!empty($ai_response['task_id'])) { error_log('Igny8: Looking up task_id: ' . intval($ai_response['task_id'])); echo "Igny8 DEBUG: Looking up task_id: " . intval($ai_response['task_id']) . "
"; $task = $wpdb->get_row($wpdb->prepare( "SELECT cluster_id, content_structure, content_type FROM {$wpdb->prefix}igny8_tasks WHERE id = %d", intval($ai_response['task_id']) )); error_log('Igny8: Task lookup result: ' . ($task ? 'Found task' : 'Task not found')); echo "Igny8 DEBUG: Task lookup result: " . ($task ? 'Found task' : 'Task not found') . "
"; if ($task) { error_log('Igny8: Task cluster_id: ' . ($task->cluster_id ?: 'NULL')); echo "Igny8 DEBUG: Task cluster_id: " . ($task->cluster_id ?: 'NULL') . "
"; } if ($task && $task->cluster_id) { $cluster_id = $task->cluster_id; // Get sector_id from cluster (this is the sector taxonomy term ID) $cluster_data = $wpdb->get_row($wpdb->prepare( "SELECT sector_id FROM {$wpdb->prefix}igny8_clusters WHERE id = %d", intval($cluster_id) )); if ($cluster_data) { $sector_id = $cluster_data->sector_id; // This is already the taxonomy term ID error_log('Igny8: Found sector_id (term ID): ' . $sector_id); echo "Igny8 DEBUG: Found sector_id (term ID): " . $sector_id . "
"; } } } else { error_log('Igny8: No task_id in AI response'); echo "Igny8 DEBUG: No task_id in AI response
"; } // Get content structure and type from task (not from AI response) $content_structure = $task->content_structure ?? 'cluster_hub'; $content_type = $task->content_type ?? 'post'; $post_type = igny8_map_content_type_to_post_type($content_structure); // Prepare content for processing $content = $ai_response['content'] ?? ''; $editor_type = get_option('igny8_editor_type', 'block'); error_log("IGNY8 DEBUG - EDITOR TYPE FROM DB: " . $editor_type); // Content is now direct HTML from the new prompt format // No need to check for nested structures or convert from JSON igny8_log_ai_event('Content Format Detection', 'writer', 'content_generation', 'info', 'Using direct HTML content from AI response', 'Editor type: ' . $editor_type); // NEW PIPELINE: Process content through integrated pipeline // Step 1: Convert to Gutenberg blocks if using block editor if ($editor_type === 'block') { error_log("IGNY8 DEBUG: I AM ACTIVE AND RUNNING IN MODULE-AI.PHP - Block editor path selected"); $final_block_content = igny8_convert_to_wp_blocks($content); error_log("IGNY8 DEBUG - Conversion Completed"); // Step 1.5: Validate and fix block structure $final_block_content = igny8_validate_and_fix_blocks($final_block_content); error_log("IGNY8 DEBUG: I AM ACTIVE AND RUNNING IN MODULE-AI.PHP - About to call insert_igny8_shortcode_blocks_into_blocks()"); error_log("IGNY8 DEBUG: CALL LOCATION - igny8_create_post_from_ai_response() -> Block Editor Path -> Line 1186"); $final_block_content = insert_igny8_shortcode_blocks_into_blocks($final_block_content); // Check if shortcodes were successfully injected $has_shortcode = false; foreach (parse_blocks($final_block_content) as $block) { if ( $block['blockName'] === 'core/shortcode' && isset($block['innerContent']) && is_array($block['innerContent']) && preg_match('/\[igny8-image.*?\]/', implode('', $block['innerContent'])) ) { $has_shortcode = true; break; } } if (!$has_shortcode) { error_log("IGNY8 DEBUG - Shortcode injection failed: No shortcodes found in parsed blocks"); igny8_log_ai_event('Shortcode Injection Failed', 'writer', 'content_generation', 'warning', 'No shortcodes found after injection - proceeding without shortcodes', 'Editor type: ' . $editor_type); // FALLBACK: Continue with post creation without shortcodes $content = $final_block_content; } else { $content = $final_block_content; } igny8_log_ai_event('Content Wrapped as Blocks', 'writer', 'content_generation', 'info', 'HTML content wrapped as Gutenberg blocks', 'Editor type: ' . $editor_type); } else { // For classic editor, use plain shortcode logic error_log("IGNY8 DEBUG: I AM ACTIVE AND RUNNING IN MODULE-AI.PHP - Classic editor path selected"); error_log("IGNY8 DEBUG: I AM ACTIVE AND RUNNING IN MODULE-AI.PHP - About to call insert_igny8_image_shortcodes_classic()"); error_log("IGNY8 DEBUG: CALL LOCATION - igny8_create_post_from_ai_response() -> Classic Editor Path -> Line 1214"); $content = insert_igny8_image_shortcodes_classic($content); // Check if shortcodes were successfully injected if (strpos($content, '[igny8-image') === false) { error_log("IGNY8 DEBUG - Shortcode injection failed: No shortcodes found in content"); igny8_log_ai_event('Shortcode Injection Failed', 'writer', 'content_generation', 'warning', 'No shortcodes found after injection - proceeding without shortcodes', 'Editor type: ' . $editor_type); // FALLBACK: Continue with post creation without shortcodes } igny8_log_ai_event('Content Format Detection', 'writer', 'content_generation', 'info', 'Using HTML content with shortcodes for Classic Editor', 'Editor type: ' . $editor_type); } // Get new content decision setting AFTER content processing $new_content_action = get_option('igny8_new_content_action', 'draft'); $post_status = ($new_content_action === 'publish') ? 'publish' : 'draft'; // Debug logging error_log('Igny8 DEBUG: New content action setting: ' . $new_content_action); error_log('Igny8 DEBUG: Post status will be: ' . $post_status); error_log('Igny8 DEBUG: All options with igny8_new_content_action: ' . print_r(get_option('igny8_new_content_action'), true)); echo "Igny8 DEBUG: New content action setting: " . $new_content_action . "
"; echo "Igny8 DEBUG: Post status will be: " . $post_status . "
"; $post_data = [ 'post_title' => sanitize_text_field($ai_response['title'] ?? 'AI Generated Content'), 'post_content' => $content, 'post_excerpt' => sanitize_textarea_field($ai_response['meta_description'] ?? ''), 'post_status' => $post_status, // Use setting from New Content Decision 'post_type' => $post_type, 'post_author' => get_current_user_id(), 'post_date' => current_time('mysql'), 'meta_input' => [ '_igny8_ai_generated' => 1, '_igny8_content_type' => $content_structure, '_igny8_word_count' => intval($ai_response['word_count'] ?? 0), '_igny8_keywords_used' => wp_json_encode($ai_response['keywords_used'] ?? []), '_igny8_internal_links' => wp_json_encode($ai_response['internal_link_opportunities'] ?? []) ] ]; error_log("IGNY8 DEBUG - POST CONTENT ABOUT TO SAVE:\n" . $post_data['post_content']); // Optional debug file write if (defined('IGNY8_DEBUG_BLOCKS') && IGNY8_DEBUG_BLOCKS === true) { file_put_contents(WP_CONTENT_DIR . '/igny8-block-output.html', $content); } // Create the post $post_id = wp_insert_post($post_data); if (is_wp_error($post_id)) { error_log('Igny8: Failed to create post - ' . $post_id->get_error_message()); igny8_log_ai_event('WordPress Post Creation Failed', 'writer', 'content_generation', 'error', 'Failed to create WordPress post', 'Error: ' . $post_id->get_error_message()); return false; } igny8_log_ai_event('WordPress Post Created', 'writer', 'content_generation', 'success', 'WordPress post created successfully', 'Post ID: ' . $post_id . ', Title: ' . $ai_response['title']); // Note: Task record updating is handled by the AJAX handler // This function only creates the WordPress post and links it to the task // Save AI-generated meta fields to post meta if (!empty($ai_response['meta_title'])) { update_post_meta($post_id, '_igny8_meta_title', sanitize_text_field($ai_response['meta_title'])); igny8_log_ai_event('SEO Meta Title Saved', 'writer', 'content_generation', 'success', 'Meta title saved to post meta', 'Post ID: ' . $post_id . ', Field: _igny8_meta_title'); } if (!empty($ai_response['meta_description'])) { update_post_meta($post_id, '_igny8_meta_description', sanitize_textarea_field($ai_response['meta_description'])); igny8_log_ai_event('SEO Meta Description Saved', 'writer', 'content_generation', 'success', 'Meta description saved to post meta', 'Post ID: ' . $post_id . ', Field: _igny8_meta_description'); } // === Igny8 Keyword Meta === if (!empty($ai_response['primary_keyword'])) { update_post_meta($post_id, '_igny8_primary_keywords', sanitize_text_field($ai_response['primary_keyword'])); igny8_log_ai_event('Primary Keywords Saved', 'writer', 'content_generation', 'success', 'Primary keywords saved to post meta', 'Post ID: ' . $post_id . ', Field: _igny8_primary_keywords'); } if (!empty($ai_response['keywords'])) { update_post_meta($post_id, '_igny8_primary_keywords', sanitize_text_field($ai_response['keywords'])); igny8_log_ai_event('Primary Keywords Saved', 'writer', 'content_generation', 'success', 'Primary keywords saved to post meta', 'Post ID: ' . $post_id . ', Field: _igny8_primary_keywords'); } if (!empty($ai_response['secondary_keywords'])) { $secondary = is_array($ai_response['secondary_keywords']) ? implode(', ', array_map('sanitize_text_field', $ai_response['secondary_keywords'])) : sanitize_text_field($ai_response['secondary_keywords']); update_post_meta($post_id, '_igny8_secondary_keywords', $secondary); igny8_log_ai_event('Secondary Keywords Saved', 'writer', 'content_generation', 'success', 'Secondary keywords saved to post meta', 'Post ID: ' . $post_id . ', Field: _igny8_secondary_keywords'); } if (!empty($ai_response['word_count'])) { update_post_meta($post_id, '_igny8_word_count', intval($ai_response['word_count'])); } // === Save Image Prompts === // Handle featured image prompt (direct field in new format) if (!empty($ai_response['featured_image'])) { update_post_meta($post_id, '_igny8_featured_image_prompt', sanitize_textarea_field($ai_response['featured_image'])); igny8_log_ai_event('Featured Image Prompt Saved', 'writer', 'content_generation', 'success', 'Featured image prompt saved to post meta', 'Post ID: ' . $post_id . ', Field: _igny8_featured_image_prompt'); } // Handle in-article image prompts (direct field in new format) if (!empty($ai_response['in_article_images']) && is_array($ai_response['in_article_images'])) { $article_images_data = []; foreach ($ai_response['in_article_images'] as $index => $image_data) { // Handle both formats: array of strings or array of objects if (is_string($image_data)) { // Old format: array of strings $clean_value = wp_strip_all_tags($image_data); $article_images_data[] = [ 'prompt-img-' . ($index + 1) => sanitize_textarea_field($clean_value) ]; } elseif (is_array($image_data)) { // New format: array of objects with prompt-img-X keys $sanitized_data = []; foreach ($image_data as $key => $value) { if (strpos($key, 'prompt-img-') === 0) { // Strip HTML tags and sanitize to ensure only plain text $clean_value = wp_strip_all_tags($value); $sanitized_data[$key] = sanitize_textarea_field($clean_value); } } if (!empty($sanitized_data)) { $article_images_data[] = $sanitized_data; } } } if (!empty($article_images_data)) { update_post_meta($post_id, '_igny8_article_images_data', wp_json_encode($article_images_data)); igny8_log_ai_event('In-Article Image Prompts Saved', 'writer', 'content_generation', 'success', 'In-article image prompts saved to post meta', 'Post ID: ' . $post_id . ', Count: ' . count($article_images_data) . ', Field: _igny8_article_images_data'); } } // Handle legacy image_prompts format for backward compatibility // DISABLED: No longer saving to _igny8_image_prompts field // if (!empty($ai_response['image_prompts'])) { // update_post_meta($post_id, '_igny8_image_prompts', wp_json_encode($ai_response['image_prompts'])); // igny8_log_ai_event('Legacy Image Prompts Saved', 'writer', 'content_generation', 'info', 'Legacy image prompts format saved for backward compatibility', 'Post ID: ' . $post_id . ', Field: _igny8_image_prompts'); // } // === Associate Cluster Term === $cluster_success = false; if (!empty($cluster_id)) { global $wpdb; error_log('Igny8: Attempting to associate cluster_id: ' . intval($cluster_id)); echo "Igny8 DEBUG: Attempting to associate cluster_id: " . intval($cluster_id) . "
"; $cluster_term_id = $wpdb->get_var($wpdb->prepare(" SELECT cluster_term_id FROM {$wpdb->prefix}igny8_clusters WHERE id = %d ", intval($cluster_id))); error_log('Igny8: Found cluster_term_id: ' . ($cluster_term_id ?: 'NULL')); echo "Igny8 DEBUG: Found cluster_term_id: " . ($cluster_term_id ?: 'NULL') . "
"; if ($cluster_term_id) { // Check if taxonomy exists if (!taxonomy_exists('clusters')) { error_log('Igny8: ERROR - clusters taxonomy does not exist!'); echo "Igny8 DEBUG: ERROR - clusters taxonomy does not exist!
"; igny8_log_ai_event('Cluster Association Failed', 'writer', 'content_generation', 'error', 'Clusters taxonomy does not exist', 'Post ID: ' . $post_id . ', Cluster ID: ' . $cluster_id); } else { error_log('Igny8: clusters taxonomy exists, attempting association...'); echo "Igny8 DEBUG: clusters taxonomy exists, attempting association...
"; $cluster_result = wp_set_object_terms($post_id, intval($cluster_term_id), 'clusters', false); $cluster_success = !is_wp_error($cluster_result); error_log('Igny8: Cluster association result: ' . ($cluster_success ? 'SUCCESS' : 'FAILED - ' . ($cluster_result->get_error_message() ?? 'Unknown error'))); echo "Igny8 DEBUG: Cluster association result: " . ($cluster_success ? 'SUCCESS' : 'FAILED - ' . ($cluster_result->get_error_message() ?? 'Unknown error')) . "
"; if ($cluster_success) { igny8_log_ai_event('Cluster Associated', 'writer', 'content_generation', 'success', 'Post associated with cluster taxonomy', 'Post ID: ' . $post_id . ', Cluster ID: ' . $cluster_id . ', Term ID: ' . $cluster_term_id); } else { igny8_log_ai_event('Cluster Association Failed', 'writer', 'content_generation', 'error', 'Failed to associate cluster', 'Post ID: ' . $post_id . ', Error: ' . ($cluster_result->get_error_message() ?? 'Unknown')); } } } else { error_log('Igny8: Cluster term not found for cluster_id ' . intval($cluster_id)); echo "Igny8 DEBUG: Cluster term not found for cluster_id " . intval($cluster_id) . "
"; igny8_log_ai_event('Cluster Term Not Found', 'writer', 'content_generation', 'warning', 'Cluster term not found in database', 'Post ID: ' . $post_id . ', Cluster ID: ' . $cluster_id); } } else { error_log('Igny8: No cluster_id found in task'); echo "Igny8 DEBUG: No cluster_id found in task
"; } // === Associate Sector Term === $sector_success = false; if (!empty($sector_id)) { error_log('Igny8: Attempting to associate sector_id: ' . intval($sector_id)); echo "Igny8 DEBUG: Attempting to associate sector_id: " . intval($sector_id) . "
"; // sector_id is already the taxonomy term ID, no need to look it up $sector_term_id = intval($sector_id); error_log('Igny8: Using sector_term_id directly: ' . $sector_term_id); echo "Igny8 DEBUG: Using sector_term_id directly: " . $sector_term_id . "
"; // Check if taxonomy exists if (!taxonomy_exists('sectors')) { error_log('Igny8: ERROR - sectors taxonomy does not exist!'); echo "Igny8 DEBUG: ERROR - sectors taxonomy does not exist!
"; } else { error_log('Igny8: sectors taxonomy exists, attempting association...'); echo "Igny8 DEBUG: sectors taxonomy exists, attempting association...
"; $sector_result = wp_set_object_terms($post_id, $sector_term_id, 'sectors', false); $sector_success = !is_wp_error($sector_result); error_log('Igny8: Sector association result: ' . ($sector_success ? 'SUCCESS' : 'FAILED - ' . ($sector_result->get_error_message() ?? 'Unknown error'))); echo "Igny8 DEBUG: Sector association result: " . ($sector_success ? 'SUCCESS' : 'FAILED - ' . ($sector_result->get_error_message() ?? 'Unknown error')) . "
"; } } else { error_log('Igny8: No sector_id found in task'); echo "Igny8 DEBUG: No sector_id found in task
"; } // Handle tags if content type supports them if (in_array($post_type, ['post', 'product']) && !empty($ai_response['tags'])) { $tags = array_map('trim', $ai_response['tags']); wp_set_post_tags($post_id, $tags); igny8_log_ai_event('Tags Added', 'writer', 'content_generation', 'success', 'Post tags added', 'Post ID: ' . $post_id . ', Tags: ' . implode(', ', $tags)); } // Handle categories if (!empty($ai_response['categories'])) { igny8_set_post_categories($post_id, $ai_response['categories']); igny8_log_ai_event('Categories Added', 'writer', 'content_generation', 'success', 'Post categories added', 'Post ID: ' . $post_id); } // Store cluster and sector metadata igny8_store_content_metadata($post_id, $ai_response); // Add meta description if available if (!empty($ai_response['meta_description'])) { update_post_meta($post_id, '_yoast_wpseo_metadesc', $ai_response['meta_description']); } // Final summary log igny8_log_ai_event('Content Generation Complete', 'writer', 'content_generation', 'success', 'All content components saved successfully', 'Post ID: ' . $post_id . ', Status: ' . $post_status . ', Type: ' . $post_type); return $post_id; } catch (Exception $e) { error_log('Igny8: Exception creating post - ' . $e->getMessage()); return false; } } /** * Map AI content type to WordPress post type */ function igny8_map_content_type_to_post_type($content_type) { $mapping = [ 'blog_post' => 'post', 'landing_page' => 'page', 'product_page' => 'product', 'guide_tutorial' => 'post', 'news_article' => 'post', 'review' => 'post', 'comparison' => 'post', 'email' => 'post', 'social_media' => 'post', 'page' => 'page', 'product' => 'product', 'guide' => 'post', 'tutorial' => 'post' ]; return $mapping[$content_type] ?? 'post'; } /** * Get available models for content generation */ function igny8_get_available_models() { return [ 'gpt-4.1' => 'GPT-4.1 (Content creation, coding, analysis, high-quality content generation)', 'gpt-4o-mini' => 'GPT-4o mini (Bulk tasks, lightweight AI, cost-effective for high-volume operations)', 'gpt-4o' => 'GPT-4o (Advanced AI, better general performance, multimodal)' ]; } /** * Get queue status for user */ function igny8_get_ai_queue_status($user_id = null) { global $wpdb; $user_id = $user_id ?: get_current_user_id(); $status = $wpdb->get_row($wpdb->prepare(" SELECT COUNT(*) as total, SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending, SUM(CASE WHEN status = 'processing' THEN 1 ELSE 0 END) as processing, SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed, SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed FROM {$wpdb->prefix}igny8_ai_queue WHERE user_id = %d ", $user_id)); return $status; } /** * Get image dimensions based on size preset and provider * * @param string $size_preset Size preset (featured, desktop, mobile) * @param string $provider Image provider (runware, openai, dalle) * @return array ['width' => int, 'height' => int] */ function igny8_get_image_dimensions($size_preset = 'featured', $provider = 'runware') { // Size presets for different image types $size_presets = [ 'runware' => [ 'featured' => ['width' => 1280, 'height' => 832], 'desktop' => ['width' => 1024, 'height' => 1024], 'mobile' => ['width' => 960, 'height' => 1280] ], 'openai' => [ 'featured' => ['width' => 1024, 'height' => 1024], // OpenAI only supports square 'desktop' => ['width' => 1024, 'height' => 1024], 'mobile' => ['width' => 1024, 'height' => 1024] ], 'dalle' => [ 'featured' => ['width' => 1024, 'height' => 1024], // Placeholder for DALL-E 'desktop' => ['width' => 1024, 'height' => 1024], 'mobile' => ['width' => 1024, 'height' => 1024] ] ]; // Get dimensions for the provider and size if (isset($size_presets[$provider][$size_preset])) { return $size_presets[$provider][$size_preset]; } // Fallback to featured size for the provider if (isset($size_presets[$provider]['featured'])) { return $size_presets[$provider]['featured']; } // Ultimate fallback return ['width' => 1280, 'height' => 832]; } /** * DEPRECATED: Generate featured image for post from post meta prompt * * This function has been moved to ai/writer/images/image-generation.php * and is now included directly in the plugin bootstrap process. * * @deprecated 5.2.0 Function moved to ai/writer/images/image-generation.php */ /** * Validate and fix Gutenberg block structure * Ensures all heading blocks have proper level attributes */ function igny8_validate_and_fix_blocks($block_content) { if (empty($block_content)) { return $block_content; } $blocks = parse_blocks($block_content); $fixed_blocks = []; foreach ($blocks as $index => $block) { // Fix heading blocks missing level attribute if (($block['blockName'] ?? null) === 'core/heading') { $level = $block['attrs']['level'] ?? null; if ($level === null) { // Try to extract level from innerHTML $inner_html = $block['innerHTML'] ?? ''; if (preg_match('/]*>/i', $inner_html, $matches)) { $detected_level = intval($matches[1]); $block['attrs']['level'] = $detected_level; error_log("IGNY8 BLOCKS: Fixed heading block #$index - detected level $detected_level from innerHTML"); } else { // Default to H2 if we can't detect $block['attrs']['level'] = 2; error_log("IGNY8 BLOCKS: Fixed heading block #$index - defaulted to level 2"); } } } $fixed_blocks[] = $block; } return serialize_blocks($fixed_blocks); } /** * Inject plain Igny8 shortcodes after H2 for Classic Editor only (no block markup). */ function insert_igny8_image_shortcodes_classic($html_content) { error_log("IGNY8 DEBUG: I AM ACTIVE AND RUNNING IN MODULE-AI.PHP - insert_igny8_image_shortcodes_classic()"); error_log("IGNY8 DEBUG: CALLED FROM - igny8_create_post_from_ai_response() function in ai/modules-ai.php"); error_log("IGNY8 DEBUG - CLASSIC: Starting shortcode injection"); error_log("IGNY8 DEBUG - CLASSIC: Input content length: " . strlen($html_content)); if (empty($html_content)) { error_log("IGNY8 DEBUG - CLASSIC: Content is empty, returning"); return $html_content; } $pattern = '/(]*>.*?<\/h2>)/i'; $matches = []; preg_match_all($pattern, $html_content, $matches, PREG_OFFSET_CAPTURE); error_log("IGNY8 DEBUG - CLASSIC: Found " . count($matches[0]) . " H2 headings"); if (empty($matches[0])) { error_log("IGNY8 DEBUG - CLASSIC: No H2 headings found, returning original content"); return $html_content; } $offset = 0; $image_index = 0; foreach (array_reverse($matches[0]) as $match) { $image_index++; error_log("IGNY8 DEBUG - CLASSIC: Processing H2 #{$image_index}"); // Skip first H2 if ($image_index === count($matches[0])) { error_log("IGNY8 DEBUG - CLASSIC: Skipping first H2"); continue; } // Inject plain shortcodes (no Gutenberg markup) $shortcode = "\n\n[igny8-image id=\"desktop-{$image_index}\"] [igny8-image id=\"mobile-{$image_index}\"]\n\n"; error_log("IGNY8 DEBUG - CLASSIC: Injecting shortcode: " . trim($shortcode)); $insert_pos = $match[1] + strlen($match[0]) + $offset; $html_content = substr_replace($html_content, $shortcode, $insert_pos, 0); $offset += strlen($shortcode); } error_log("IGNY8 DEBUG - CLASSIC: Final content length: " . strlen($html_content)); error_log("IGNY8 DEBUG - CLASSIC: Shortcodes in final content: " . (strpos($html_content, '[igny8-image') !== false ? 'YES' : 'NO')); return $html_content; } /** * Inject Gutenberg shortcode blocks after each

heading block (core/heading, level 2) * Adds minimal, meaningful logs. Silences irrelevant debug spam. * * @param string $block_content Serialized Gutenberg block content * @return string|false Modified content or false if injection fails */ function insert_igny8_shortcode_blocks_into_blocks($block_content) { error_log("IGNY8 DEBUG: I AM ACTIVE AND RUNNING IN MODULE-AI.PHP - insert_igny8_shortcode_blocks_into_blocks()"); error_log("IGNY8 DEBUG: CALLED FROM - igny8_create_post_from_ai_response() function in ai/modules-ai.php"); if (empty($block_content)) { error_log("IGNY8 BLOCKS: No content passed to shortcode injector"); return $block_content; } $blocks = parse_blocks($block_content); $output = []; $h2_count = 0; $injected = 0; $heading_blocks_found = 0; $valid_h2_blocks = 0; error_log("IGNY8 BLOCKS: Parsed " . count($blocks) . " total blocks"); foreach ($blocks as $index => $block) { $output[] = $block; if (($block['blockName'] ?? null) === 'core/heading') { $heading_blocks_found++; $level = $block['attrs']['level'] ?? null; error_log("IGNY8 BLOCKS: Heading block #$index - level: " . ($level ?? 'NULL') . ", innerHTML: " . substr($block['innerHTML'] ?? '', 0, 50) . "..."); if ($level !== 2) { if ($level === null) { error_log("IGNY8 BLOCKS: Skipping heading block #$index — missing 'level' attribute"); } else { error_log("IGNY8 BLOCKS: Skipping heading block #$index — level $level (not H2)"); } continue; } $valid_h2_blocks++; $h2_count++; if ($h2_count === 1) { error_log("IGNY8 BLOCKS: Skipping first H2 (no shortcode)"); continue; } $shortcode = "[igny8-image id=\"desktop-{$h2_count}\"] [igny8-image id=\"mobile-{$h2_count}\"]"; error_log("IGNY8 BLOCKS: Injecting shortcode after H2 #{$h2_count}: " . $shortcode); $output[] = [ 'blockName' => 'core/shortcode', 'attrs' => [], 'innerBlocks' => [], 'innerHTML' => $shortcode, 'innerContent' => [$shortcode] ]; $injected++; } } error_log("IGNY8 BLOCKS: Summary - Total headings: $heading_blocks_found, Valid H2s: $valid_h2_blocks, Shortcodes injected: $injected"); $result = serialize_blocks($output); $parsed_result = parse_blocks($result); $confirmed = false; foreach ($parsed_result as $b) { if ( ($b['blockName'] ?? '') === 'core/shortcode' && strpos($b['innerContent'][0] ?? '', '[igny8-image') !== false ) { $confirmed = true; break; } } if (!$confirmed) { error_log("IGNY8 BLOCKS: ❌ Shortcode injection failed — no blocks found after serialization"); igny8_log_ai_event( 'Shortcode Injection Failed', 'writer', 'content_generation', 'error', 'No shortcodes found after injection (post-parse)', 'Editor type: block' ); return false; } error_log("IGNY8 BLOCKS: ✅ Injected {$injected} shortcode blocks after H2 headings"); return $result; } /** * Wrap plain HTML content as Gutenberg blocks * * @param string $html_content Plain HTML content * @return string Gutenberg block markup */ function wrap_html_as_blocks($html_content) { if (empty($html_content)) { return $html_content; } // Split content into lines for processing $lines = explode("\n", $html_content); $block_content = []; foreach ($lines as $line) { $line = trim($line); if (empty($line)) { continue; } // Wrap different HTML elements as Gutenberg blocks if (preg_match('/^]*>(.*?)<\/h2>$/i', $line, $matches)) { $block_content[] = '' . $line . ''; } elseif (preg_match('/^]*>(.*?)<\/h3>$/i', $line, $matches)) { $block_content[] = '' . $line . ''; } elseif (preg_match('/^]*>(.*?)<\/p>$/i', $line, $matches)) { $block_content[] = '' . $line . ''; } elseif (preg_match('/^]*>(.*?)<\/ul>$/i', $line, $matches)) { $block_content[] = '' . $line . ''; } elseif (preg_match('/^]*>(.*?)<\/ol>$/i', $line, $matches)) { $block_content[] = '' . $line . ''; } elseif (preg_match('/^]*>(.*?)<\/blockquote>$/i', $line, $matches)) { $block_content[] = '' . $line . ''; } elseif (preg_match('/^]*>(.*?)<\/table>$/i', $line, $matches)) { $block_content[] = '' . $line . ''; } elseif (preg_match('/^\[igny8-image[^\]]*\]/', $line)) { // Handle shortcodes - wrap in shortcode block $block_content[] = '' . $line . ''; } else { // For any other content, wrap as paragraph $block_content[] = '

' . $line . '

'; } } return implode("\n", $block_content); }