Files
igny8/igny8-ai-seo-wp-plugin/ai/modules-ai.php
2025-11-11 21:16:37 +05:00

1810 lines
73 KiB
PHP

<?php
/**
* ==========================
* 🔐 IGNY8 FILE RULE HEADER
* ==========================
* @file : modules-ai.php
* @location : /ai/modules-ai.php
* @type : AI Integration
* @scope : Global
* @allowed : AI service abstraction, module AI interfaces
* @reusability : Globally Reusable
* @notes : Common AI interface for all modules
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
/**
* Get AI setting value
*/
function igny8_get_ai_setting($key, $default = null) {
$settings = get_option('igny8_ai_settings', []);
return isset($settings[$key]) ? $settings[$key] : $default;
}
/**
* Update AI setting value
*/
function igny8_update_ai_setting($key, $value) {
$settings = get_option('igny8_ai_settings', []);
$settings[$key] = $value;
update_option('igny8_ai_settings', $settings);
}
/**
* Get Planner AI settings
*/
function igny8_get_planner_ai_settings() {
return [
'planner_mode' => 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 "<strong>Igny8 DEBUG: Looking up task_id: " . intval($ai_response['task_id']) . "</strong><br>";
$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 "<strong>Igny8 DEBUG: Task lookup result: " . ($task ? 'Found task' : 'Task not found') . "</strong><br>";
if ($task) {
error_log('Igny8: Task cluster_id: ' . ($task->cluster_id ?: 'NULL'));
echo "<strong>Igny8 DEBUG: Task cluster_id: " . ($task->cluster_id ?: 'NULL') . "</strong><br>";
}
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 "<strong>Igny8 DEBUG: Found sector_id (term ID): " . $sector_id . "</strong><br>";
}
}
} else {
error_log('Igny8: No task_id in AI response');
echo "<strong>Igny8 DEBUG: No task_id in AI response</strong><br>";
}
// 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 "<strong>Igny8 DEBUG: New content action setting: " . $new_content_action . "</strong><br>";
echo "<strong>Igny8 DEBUG: Post status will be: " . $post_status . "</strong><br>";
$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 "<strong>Igny8 DEBUG: Attempting to associate cluster_id: " . intval($cluster_id) . "</strong><br>";
$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 "<strong>Igny8 DEBUG: Found cluster_term_id: " . ($cluster_term_id ?: 'NULL') . "</strong><br>";
if ($cluster_term_id) {
// Check if taxonomy exists
if (!taxonomy_exists('clusters')) {
error_log('Igny8: ERROR - clusters taxonomy does not exist!');
echo "<strong>Igny8 DEBUG: ERROR - clusters taxonomy does not exist!</strong><br>";
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 "<strong>Igny8 DEBUG: clusters taxonomy exists, attempting association...</strong><br>";
$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 "<strong>Igny8 DEBUG: Cluster association result: " . ($cluster_success ? 'SUCCESS' : 'FAILED - ' . ($cluster_result->get_error_message() ?? 'Unknown error')) . "</strong><br>";
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 "<strong>Igny8 DEBUG: Cluster term not found for cluster_id " . intval($cluster_id) . "</strong><br>";
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 "<strong>Igny8 DEBUG: No cluster_id found in task</strong><br>";
}
// === Associate Sector Term ===
$sector_success = false;
if (!empty($sector_id)) {
error_log('Igny8: Attempting to associate sector_id: ' . intval($sector_id));
echo "<strong>Igny8 DEBUG: Attempting to associate sector_id: " . intval($sector_id) . "</strong><br>";
// 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 "<strong>Igny8 DEBUG: Using sector_term_id directly: " . $sector_term_id . "</strong><br>";
// Check if taxonomy exists
if (!taxonomy_exists('sectors')) {
error_log('Igny8: ERROR - sectors taxonomy does not exist!');
echo "<strong>Igny8 DEBUG: ERROR - sectors taxonomy does not exist!</strong><br>";
} else {
error_log('Igny8: sectors taxonomy exists, attempting association...');
echo "<strong>Igny8 DEBUG: sectors taxonomy exists, attempting association...</strong><br>";
$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 "<strong>Igny8 DEBUG: Sector association result: " . ($sector_success ? 'SUCCESS' : 'FAILED - ' . ($sector_result->get_error_message() ?? 'Unknown error')) . "</strong><br>";
}
} else {
error_log('Igny8: No sector_id found in task');
echo "<strong>Igny8 DEBUG: No sector_id found in task</strong><br>";
}
// 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('/<h([1-6])[^>]*>/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[^>]*>.*?<\/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 <h2> 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[^>]*>(.*?)<\/h2>$/i', $line, $matches)) {
$block_content[] = '<!-- wp:heading {"level":2} -->' . $line . '<!-- /wp:heading -->';
} elseif (preg_match('/^<h3[^>]*>(.*?)<\/h3>$/i', $line, $matches)) {
$block_content[] = '<!-- wp:heading {"level":3} -->' . $line . '<!-- /wp:heading -->';
} elseif (preg_match('/^<p[^>]*>(.*?)<\/p>$/i', $line, $matches)) {
$block_content[] = '<!-- wp:paragraph -->' . $line . '<!-- /wp:paragraph -->';
} elseif (preg_match('/^<ul[^>]*>(.*?)<\/ul>$/i', $line, $matches)) {
$block_content[] = '<!-- wp:list -->' . $line . '<!-- /wp:list -->';
} elseif (preg_match('/^<ol[^>]*>(.*?)<\/ol>$/i', $line, $matches)) {
$block_content[] = '<!-- wp:list {"ordered":true} -->' . $line . '<!-- /wp:list -->';
} elseif (preg_match('/^<blockquote[^>]*>(.*?)<\/blockquote>$/i', $line, $matches)) {
$block_content[] = '<!-- wp:quote -->' . $line . '<!-- /wp:quote -->';
} elseif (preg_match('/^<table[^>]*>(.*?)<\/table>$/i', $line, $matches)) {
$block_content[] = '<!-- wp:table -->' . $line . '<!-- /wp:table -->';
} elseif (preg_match('/^\[igny8-image[^\]]*\]/', $line)) {
// Handle shortcodes - wrap in shortcode block
$block_content[] = '<!-- wp:shortcode -->' . $line . '<!-- /wp:shortcode -->';
} else {
// For any other content, wrap as paragraph
$block_content[] = '<!-- wp:paragraph --><p>' . $line . '</p><!-- /wp:paragraph -->';
}
}
return implode("\n", $block_content);
}