1810 lines
73 KiB
PHP
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);
|
|
}
|
|
|
|
|