options = get_option('mxchat_options', array()); $this->mxchat_init_hooks(); } /** * Initialize WordPress hooks for content processing */ private function mxchat_init_hooks() { // Admin post handlers for form submissions add_action('admin_post_mxchat_submit_content', array($this, 'mxchat_handle_content_submission')); add_action('admin_post_mxchat_submit_sitemap', array($this, 'mxchat_handle_sitemap_submission')); add_action('admin_post_mxchat_stop_processing', array($this, 'mxchat_stop_processing')); // AJAX handlers for real-time processing and status updates add_action('wp_ajax_mxchat_get_status_updates', array($this, 'mxchat_ajax_get_status_updates')); add_action('wp_ajax_mxchat_dismiss_completed_status', array($this, 'mxchat_ajax_dismiss_completed_status')); add_action('wp_ajax_mxchat_get_content_list', array($this, 'ajax_mxchat_get_content_list')); add_action('wp_ajax_mxchat_process_selected_content', array($this, 'ajax_mxchat_process_selected_content')); add_action('wp_ajax_mxchat_manual_batch_process', array($this, 'ajax_manual_batch_process')); add_action('mxchat_delete_content', array($this, 'mxchat_delete_from_pinecone_by_url'), 10, 1); // Cron handlers for background processing add_action('mxchat_process_sitemap_urls', array($this, 'mxchat_process_sitemap_urls_cron'), 10, 5); add_action('mxchat_process_pdf_pages', array($this, 'mxchat_process_pdf_pages_cron'), 10, 5); // WordPress post management hooks - UPDATED FOR BETTER STATUS TRACKING add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2); add_action('post_updated', array($this, 'mxchat_handle_post_update'), 10, 3); add_action('before_delete_post', array($this, 'mxchat_handle_post_delete')); add_action('wp_trash_post', array($this, 'mxchat_handle_post_delete')); add_action('wp_ajax_mxchat_save_inline_prompt', array($this, 'mxchat_save_inline_prompt')); add_action('admin_post_mxchat_delete_pinecone_prompt', array($this, 'mxchat_handle_pinecone_prompt_delete')); add_action('wp_ajax_mxchat_delete_pinecone_prompt', array($this, 'ajax_mxchat_delete_pinecone_prompt')); add_action('wp_ajax_mxchat_update_role_restriction', array($this, 'ajax_mxchat_update_role_restriction')); // WooCommerce product hooks (if WooCommerce is active) if (class_exists('WooCommerce')) { add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2); // Same hook for products add_action('save_post_product', array($this, 'mxchat_handle_product_change'), 10, 3); add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete')); add_action('before_delete_post', array($this, 'mxchat_handle_product_delete')); } } /** * Get current options (refreshed) */ private function mxchat_get_options() { if (empty($this->options)) { $this->options = get_option('mxchat_options', array()); } return $this->options; } public function ajax_manual_batch_process() { try { // Verify nonce and permissions check_ajax_referer('mxchat_status_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized access'); } $process_type = sanitize_text_field($_POST['process_type'] ?? ''); $url = sanitize_text_field($_POST['url'] ?? ''); if (empty($process_type) || empty($url)) { wp_send_json_error('Missing required parameters'); } // Debug logging //error_log('MANUAL BATCH DEBUG: Process type: ' . $process_type); //error_log('MANUAL BATCH DEBUG: URL: ' . $url); // FIXED: Extract bot_id from stored status instead of POST data $bot_id = 'default'; if ($process_type === 'pdf') { $status_key = sanitize_key('mxchat_pdf_status_' . md5($url)); $status = get_transient($status_key); //error_log('MANUAL BATCH DEBUG: Status key: ' . $status_key); //error_log('MANUAL BATCH DEBUG: Status data: ' . print_r($status, true)); if ($status && isset($status['bot_id'])) { $bot_id = $status['bot_id']; //error_log('MANUAL BATCH DEBUG: Bot ID from status: ' . $bot_id); } else { //error_log('MANUAL BATCH DEBUG: No bot_id in status, using default'); } } elseif ($process_type === 'sitemap') { $status_key = sanitize_key('mxchat_sitemap_status_' . md5($url)); $status = get_transient($status_key); if ($status && isset($status['bot_id'])) { $bot_id = $status['bot_id']; } } //error_log('MANUAL BATCH DEBUG: Final bot_id: ' . $bot_id); $processed = 0; if ($process_type === 'pdf') { $processed = $this->mxchat_manual_process_pdf_batch($url); //error_log('MANUAL BATCH DEBUG: PDF processing returned: ' . $processed); } elseif ($process_type === 'sitemap') { $processed = $this->mxchat_manual_process_sitemap_batch($url); } if ($processed > 0) { wp_send_json_success(array( 'message' => "Processed {$processed} items successfully", 'processed' => $processed, 'bot_id' => $bot_id )); } else { // Enhanced error response with debugging info wp_send_json_error(array( 'message' => 'No items were processed', 'debug_info' => array( 'process_type' => $process_type, 'url' => $url, 'bot_id' => $bot_id, 'status_exists' => !empty($status), 'status_data' => $status ) )); } } catch (Exception $e) { //error_log('MANUAL BATCH DEBUG: Exception: ' . $e->getMessage()); wp_send_json_error('Processing failed: ' . $e->getMessage()); } } private function mxchat_manual_process_pdf_batch($pdf_url) { try { //error_log('MANUAL PDF DEBUG: Starting batch processing for: ' . $pdf_url); $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url)); $status = get_transient($status_key); //error_log('MANUAL PDF DEBUG: Status key: ' . $status_key); //error_log('MANUAL PDF DEBUG: Status data: ' . print_r($status, true)); if (!$status || $status['status'] !== 'processing') { //error_log('MANUAL PDF DEBUG: No processing status found or status is not processing'); //error_log('MANUAL PDF DEBUG: Status: ' . ($status ? $status['status'] : 'NULL')); return 0; } // FIXED: Extract bot_id from status $bot_id = $status['bot_id'] ?? 'default'; //error_log('MANUAL PDF DEBUG: Bot ID from status: ' . $bot_id); // Get current progress $current_page = $status['processed_pages'] ?? 0; $total_pages = $status['total_pages'] ?? 0; //error_log('MANUAL PDF DEBUG: Current page: ' . $current_page . ', Total pages: ' . $total_pages); if ($current_page >= $total_pages) { //error_log('MANUAL PDF DEBUG: Already completed'); return 0; } // Try to download the PDF again for processing //error_log('MANUAL PDF DEBUG: Attempting to download PDF'); $response = wp_remote_get($pdf_url, array('timeout' => 30)); if (is_wp_error($response)) { //error_log('MANUAL PDF DEBUG: Failed to download PDF: ' . $response->get_error_message()); return 0; } $pdf_content = wp_remote_retrieve_body($response); if (empty($pdf_content)) { //error_log('MANUAL PDF DEBUG: Empty PDF content'); return 0; } //error_log('MANUAL PDF DEBUG: PDF content size: ' . strlen($pdf_content) . ' bytes'); // Save PDF temporarily $upload_dir = wp_upload_dir(); $temp_pdf_path = trailingslashit($upload_dir['path']) . 'temp_manual_' . time() . '.pdf'; file_put_contents($temp_pdf_path, $pdf_content); //error_log('MANUAL PDF DEBUG: Temp PDF saved to: ' . $temp_pdf_path); // Process 5 pages directly with bot_id $processed = $this->mxchat_process_pdf_pages_direct($temp_pdf_path, $pdf_url, $current_page, 5, $bot_id); //error_log('MANUAL PDF DEBUG: Direct processing returned: ' . $processed); // Clean up temp file if (file_exists($temp_pdf_path)) { wp_delete_file($temp_pdf_path); //error_log('MANUAL PDF DEBUG: Cleaned up temp file'); } return $processed; } catch (Exception $e) { //error_log('MANUAL PDF DEBUG: Exception in manual batch: ' . $e->getMessage()); return 0; } } /** * Process PDF pages directly without cron */ private function mxchat_process_pdf_pages_direct($pdf_path, $pdf_url, $start_page, $batch_size, $bot_id = 'default') { try { if (!file_exists($pdf_path)) { //error_log('Direct PDF: File not found at ' . $pdf_path); return 0; } $parser = new \Smalot\PdfParser\Parser(); $pdf = $parser->parseFile($pdf_path); $pages = $pdf->getPages(); $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url)); $status = get_transient($status_key); if (!$status) { return 0; } // UPDATED: Get bot-specific options $bot_options = $this->get_bot_options($bot_id); $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options'); $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002'; if (strpos($selected_model, 'voyage') === 0) { $api_key = $options['voyage_api_key'] ?? ''; } elseif (strpos($selected_model, 'gemini-embedding') === 0) { $api_key = $options['gemini_api_key'] ?? ''; } else { $api_key = $options['api_key'] ?? ''; } if (empty($api_key)) { //error_log('Direct PDF: No API key for bot: ' . $bot_id); return 0; } $processed = 0; $end_page = min($start_page + $batch_size, count($pages)); for ($i = $start_page; $i < $end_page; $i++) { try { $page_number = $i + 1; $text = $pages[$i]->getText(); if (empty($text)) { //error_log('Direct PDF: Empty text on page ' . $page_number); continue; } $sanitized_content = $this->mxchat_sanitize_content_for_api($text); if (empty($sanitized_content)) { //error_log('Direct PDF: No content after sanitization on page ' . $page_number); continue; } // UPDATED: Use bot-specific embedding generation $embedding_vector = $this->mxchat_generate_embedding($sanitized_content, $bot_id); if (is_string($embedding_vector)) { //error_log('Direct PDF: Embedding failed on page ' . $page_number . ': ' . $embedding_vector); continue; } // Create metadata $metadata = array( 'document_type' => 'pdf', 'total_pages' => count($pages), 'current_page' => $page_number, 'source_url' => $pdf_url ); $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized_content; $page_url = esc_url($pdf_url . "#page=" . $page_number); // UPDATED: Pass bot_id to database submission $db_result = MxChat_Utils::submit_content_to_db($content_with_metadata, $page_url, $api_key, null, $bot_id); if (is_wp_error($db_result)) { //error_log('Direct PDF: DB error on page ' . $page_number . ': ' . $db_result->get_error_message()); continue; } $processed++; //error_log('Direct PDF: Successfully processed page ' . $page_number . ' for bot: ' . $bot_id); // Update status $status['processed_pages'] = $i + 1; $status['last_update'] = time(); $status['percentage'] = round(($status['processed_pages'] / $status['total_pages']) * 100); set_transient($status_key, $status, DAY_IN_SECONDS); } catch (Exception $e) { //error_log('Direct PDF: Error processing page ' . ($i + 1) . ': ' . $e->getMessage()); continue; } } // Check if completed if ($status['processed_pages'] >= $status['total_pages']) { $status['status'] = 'complete'; set_transient($status_key, $status, DAY_IN_SECONDS); //error_log('Direct PDF: Processing completed for bot: ' . $bot_id); } return $processed; } catch (Exception $e) { //error_log('Direct PDF processing error: ' . $e->getMessage()); return 0; } } /** * Process a small sitemap batch manually - DIRECT PROCESSING */ private function mxchat_manual_process_sitemap_batch($sitemap_url) { try { $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)); $status = get_transient($status_key); if (!$status || $status['status'] !== 'processing') { return 0; } // FIXED: Extract bot_id from status $bot_id = $status['bot_id'] ?? 'default'; //error_log('Manual Sitemap: Starting direct processing for ' . $sitemap_url . ' with bot: ' . $bot_id); // Re-fetch the sitemap to get URLs $response = wp_remote_get($sitemap_url, array('timeout' => 30)); if (is_wp_error($response)) { //error_log('Manual Sitemap: Failed to fetch sitemap'); return 0; } $sitemap_content = wp_remote_retrieve_body($response); $xml = simplexml_load_string($sitemap_content); if (!$xml) { //error_log('Manual Sitemap: Invalid XML'); return 0; } $urls = array(); foreach ($xml->url as $url_element) { $urls[] = (string)$url_element->loc; } $current_processed = $status['processed_urls'] ?? 0; $batch_size = 50; $processed = 0; // Process next batch of URLs for ($i = $current_processed; $i < min($current_processed + $batch_size, count($urls)); $i++) { $url = $urls[$i]; // UPDATED: Pass bot_id to single URL processing if ($this->mxchat_process_single_url_direct($url, $bot_id)) { $processed++; } // Update status $status['processed_urls'] = $i + 1; $status['last_update'] = time(); $status['percentage'] = round(($status['processed_urls'] / $status['total_urls']) * 100); set_transient($status_key, $status, DAY_IN_SECONDS); } // Check if completed if ($status['processed_urls'] >= $status['total_urls']) { $status['status'] = 'complete'; set_transient($status_key, $status, DAY_IN_SECONDS); } //error_log('Manual Sitemap: Processed ' . $processed . ' URLs for bot: ' . $bot_id); return $processed; } catch (Exception $e) { //error_log('Manual sitemap batch error: ' . $e->getMessage()); return 0; } } /** * Process a single URL directly */ private function mxchat_process_single_url_direct($url, $bot_id = 'default') { try { $response = wp_remote_get($url, array('timeout' => 30)); if (is_wp_error($response)) { //error_log('Single URL processing failed for ' . $url . ': ' . $response->get_error_message()); return false; } $html = wp_remote_retrieve_body($response); $content = $this->mxchat_extract_main_content($html); $sanitized = $this->mxchat_sanitize_content_for_api($content); if (empty($sanitized)) { //error_log('Single URL processing: No content found for ' . $url); return false; } // UPDATED: Get bot-specific options and API key $bot_options = $this->get_bot_options($bot_id); $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options'); $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002'; if (strpos($selected_model, 'voyage') === 0) { $api_key = $options['voyage_api_key'] ?? ''; } elseif (strpos($selected_model, 'gemini-embedding') === 0) { $api_key = $options['gemini_api_key'] ?? ''; } else { $api_key = $options['api_key'] ?? ''; } if (empty($api_key)) { //error_log('Single URL processing: No API key configured for bot: ' . $bot_id); return false; } // UPDATED: Pass bot_id to database submission $result = MxChat_Utils::submit_content_to_db($sanitized, $url, $api_key, null, $bot_id); $success = !is_wp_error($result); if ($success) { //error_log('Single URL processing: Successfully processed ' . $url . ' for bot: ' . $bot_id); } else { //error_log('Single URL processing: Failed to store ' . $url . ' for bot: ' . $bot_id . ': ' . $result->get_error_message()); } return $success; } catch (Exception $e) { //error_log('Single URL processing error: ' . $e->getMessage()); return false; } } // ======================================== // MAIN CONTENT SUBMISSION HANDLERS // ======================================== public function mxchat_handle_content_submission() { // Check if the form was submitted and the user has permission. if (!isset($_POST['submit_content']) || !current_user_can('manage_options')) { return; } // Verify the nonce. $nonce = isset($_POST['mxchat_submit_content_nonce']) ? sanitize_text_field(wp_unslash($_POST['mxchat_submit_content_nonce'])) : ''; if (!wp_verify_nonce($nonce, 'mxchat_submit_content_action')) { wp_die(esc_html__('Nonce verification failed.', 'mxchat')); } // Sanitize the inputs. $article_content = sanitize_textarea_field($_POST['article_content']); $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : ''; // UPDATED: Get bot_id from form submission $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default'; // UPDATED: Get bot-specific options and API key $bot_options = $this->get_bot_options($bot_id); $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options'); $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002'; if (strpos($selected_model, 'voyage') === 0) { $api_key = $options['voyage_api_key'] ?? ''; } elseif (strpos($selected_model, 'gemini-embedding') === 0) { $api_key = $options['gemini_api_key'] ?? ''; } else { $api_key = $options['api_key'] ?? ''; } if (empty($api_key)) { set_transient('mxchat_admin_notice_error', esc_html__('API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'), 30 ); wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts'))); exit; } // UPDATED: Use centralized utility function with bot_id $result = MxChat_Utils::submit_content_to_db($article_content, $article_url, $api_key, null, $bot_id); if (is_wp_error($result)) { set_transient('mxchat_admin_notice_error', esc_html__('Error storing content: ', 'mxchat') . $result->get_error_message(), 30 ); } else { set_transient('mxchat_admin_notice_success', esc_html__('Content successfully submitted!', 'mxchat'), 30 ); } wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts'))); exit; } public function mxchat_is_pdf_url($url, $response) { $content_type = wp_remote_retrieve_header($response, 'content-type'); $file_extension = strtolower(pathinfo($url, PATHINFO_EXTENSION)); return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf'; } public function mxchat_handle_pdf_for_knowledge_base($pdf_url, $response, $bot_id = 'default') { if (!current_user_can('manage_options')) { //error_log('[PDF DEBUG] Unauthorized PDF processing attempt'); return false; } //error_log('[PDF DEBUG] Starting PDF processing for bot: ' . $bot_id); //error_log('[PDF DEBUG] PDF URL: ' . $pdf_url); $pdf_url = esc_url_raw($pdf_url); $upload_dir = wp_upload_dir(); if (isset($upload_dir['error']) && $upload_dir['error'] !== false) { //error_log('[PDF DEBUG] Upload directory error: ' . $upload_dir['error']); return false; } $pdf_filename = sanitize_file_name('mxchat_kb_' . time() . '.pdf'); $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename; $response_body = wp_remote_retrieve_body($response); if (empty($response_body)) { //error_log('[PDF DEBUG] Empty PDF response body'); return false; } if (!wp_mkdir_p(dirname($pdf_path))) { //error_log('[PDF DEBUG] Failed to create directory for PDF: ' . $pdf_path); return false; } try { file_put_contents($pdf_path, $response_body); if (!file_exists($pdf_path)) { throw new Exception(__('Failed to save PDF file', 'mxchat')); } $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path); if ($total_pages === false || $total_pages < 1) { throw new Exception(__('Invalid PDF: Unable to parse or no pages found', 'mxchat')); } //error_log('[PDF DEBUG] PDF validated successfully with ' . $total_pages . ' pages'); // UPDATED: Pass bot_id to PDF processing cron wp_schedule_single_event(time(), 'mxchat_process_pdf_pages', array( $pdf_path, // Position 0 $pdf_url, // Position 1 $total_pages, // Position 2 absint(15), // Position 3 (batch_size) absint(10), // Position 4 (batch_pause) $bot_id // Position 5 (bot_id) )); //error_log('[PDF DEBUG] Cron job scheduled with bot_id: ' . $bot_id); // UPDATED: Store bot_id in status data $status_data = array( 'total_pages' => $total_pages, 'processed_pages' => 0, 'status' => 'processing', 'last_update' => time(), 'bot_id' => $bot_id // CRITICAL: Store the bot_id ); $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url)); set_transient($status_key, $status_data, DAY_IN_SECONDS); //error_log('[PDF DEBUG] Status stored with bot_id: ' . $bot_id . ' using key: ' . $status_key); return __('scheduled', 'mxchat'); } catch (Exception $e) { //error_log('[PDF DEBUG] Error preparing PDF for processing: ' . $e->getMessage()); if (file_exists($pdf_path)) { wp_delete_file($pdf_path); } return false; } } /** * NEW: Validate PDF and count pages with multiple parser attempts */ private function mxchat_validate_and_count_pdf_pages($pdf_path) { // Method 1: Try with Smalot PDF Parser (your current method) try { $parser = new \Smalot\PdfParser\Parser(); $pdf = $parser->parseFile($pdf_path); $pages = $pdf->getPages(); $page_count = count($pages); if ($page_count > 0) { //error_log('PDF parsed successfully with Smalot parser: ' . $page_count . ' pages'); return $page_count; } } catch (Exception $e) { //error_log('Smalot PDF parser failed: ' . $e->getMessage()); } // Method 2: Try with pdfinfo command (if available) if (function_exists('shell_exec') && !$this->mxchat_is_shell_disabled()) { try { $command = 'pdfinfo ' . escapeshellarg($pdf_path) . ' 2>&1'; $output = shell_exec($command); if ($output && preg_match('/Pages:\s*(\d+)/', $output, $matches)) { $page_count = intval($matches[1]); if ($page_count > 0) { //error_log('PDF parsed successfully with pdfinfo: ' . $page_count . ' pages'); return $page_count; } } } catch (Exception $e) { //error_log('pdfinfo command failed: ' . $e->getMessage()); } } // Method 3: Try to repair PDF and parse again try { $repaired_path = $this->mxchat_attempt_pdf_repair($pdf_path); if ($repaired_path && $repaired_path !== $pdf_path) { $parser = new \Smalot\PdfParser\Parser(); $pdf = $parser->parseFile($repaired_path); $pages = $pdf->getPages(); $page_count = count($pages); if ($page_count > 0) { // Replace original with repaired version copy($repaired_path, $pdf_path); unlink($repaired_path); //error_log('PDF repaired and parsed successfully: ' . $page_count . ' pages'); return $page_count; } // Clean up repaired file if it didn't work unlink($repaired_path); } } catch (Exception $e) { //error_log('PDF repair attempt failed: ' . $e->getMessage()); } // Method 4: Manual PDF structure analysis (basic page count) try { $page_count = $this->mxchat_manual_pdf_page_count($pdf_path); if ($page_count > 0) { //error_log('PDF page count determined manually: ' . $page_count . ' pages'); return $page_count; } } catch (Exception $e) { //error_log('Manual PDF analysis failed: ' . $e->getMessage()); } //error_log('All PDF parsing methods failed for: ' . $pdf_path); return false; } /** * NEW: Check if shell_exec is disabled */ private function mxchat_is_shell_disabled() { $disabled = explode(',', ini_get('disable_functions')); return in_array('shell_exec', $disabled); } /** * NEW: Attempt to repair PDF using basic methods */ private function mxchat_attempt_pdf_repair($pdf_path) { try { $content = file_get_contents($pdf_path); if (!$content) { return false; } // Check if PDF starts with proper header if (substr($content, 0, 4) !== '%PDF') { // Try to find PDF header in the content $header_pos = strpos($content, '%PDF'); if ($header_pos !== false && $header_pos < 1024) { // Remove junk before PDF header $content = substr($content, $header_pos); $repaired_path = $pdf_path . '.repaired'; file_put_contents($repaired_path, $content); return $repaired_path; } } // Check for EOF marker $content = rtrim($content); if (!preg_match('/%%EOF\s*$/', $content)) { // Add EOF marker if missing $content .= "\n%%EOF"; $repaired_path = $pdf_path . '.repaired'; file_put_contents($repaired_path, $content); return $repaired_path; } } catch (Exception $e) { //error_log('PDF repair error: ' . $e->getMessage()); } return false; } /** * NEW: Manual PDF page counting by analyzing PDF structure */ private function mxchat_manual_pdf_page_count($pdf_path) { try { $content = file_get_contents($pdf_path); if (!$content) { return 0; } // Method 1: Count /Type /Page objects $page_count = preg_match_all('/\/Type\s*\/Page[^s]/', $content); if ($page_count > 0) { return $page_count; } // Method 2: Look for /Count in pages object if (preg_match('/\/Type\s*\/Pages.*?\/Count\s+(\d+)/', $content, $matches)) { return intval($matches[1]); } // Method 3: Count page references $page_count = preg_match_all('/\d+\s+0\s+obj\s*<<[^>]*\/Type\s*\/Page/', $content); if ($page_count > 0) { return $page_count; } } catch (Exception $e) { //error_log('Manual PDF analysis error: ' . $e->getMessage()); } return 0; } public function mxchat_process_pdf_pages_cron($pdf_path, $pdf_url, $total_pages, $batch_size, $batch_pause, $bot_id = 'default') { // ADD THIS DEBUG SECTION AT THE VERY BEGINNING //error_log('[PDF CRON DEBUG] ===== PDF Cron Job Started ====='); //error_log('[PDF CRON DEBUG] Initial bot_id parameter: ' . $bot_id); //error_log('[PDF CRON DEBUG] Received parameters:'); //error_log('[PDF CRON DEBUG] - pdf_path: ' . $pdf_path); //error_log('[PDF CRON DEBUG] - pdf_url: ' . $pdf_url); //error_log('[PDF CRON DEBUG] - total_pages: ' . $total_pages); //error_log('[PDF CRON DEBUG] - batch_size: ' . $batch_size); //error_log('[PDF CRON DEBUG] - batch_pause: ' . $batch_pause); //error_log('[PDF CRON DEBUG] - Total args received: ' . func_num_args()); //error_log('[PDF CRON DEBUG] - All args: ' . print_r(func_get_args(), true)); // FIXED: Get the correct bot_id from stored status instead of relying on cron parameters $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url)); $status = get_transient($status_key); if ($status && isset($status['bot_id'])) { $bot_id = $status['bot_id']; //error_log('[PDF CRON DEBUG] Using bot_id from status: ' . $bot_id); } else { //error_log('[PDF CRON DEBUG] No bot_id in status, using default: ' . $bot_id); } // Validate inputs $pdf_path = sanitize_text_field($pdf_path); $pdf_url = esc_url_raw($pdf_url); $total_pages = absint($total_pages); $batch_size = absint($batch_size); $batch_pause = absint($batch_pause); $bot_id = sanitize_key($bot_id); try { if (!file_exists($pdf_path)) { throw new Exception(sprintf('PDF file not found at path: %s', esc_html($pdf_path))); } // Try to parse PDF with error recovery $pdf = null; $pages = null; try { $parser = new \Smalot\PdfParser\Parser(); $pdf = $parser->parseFile($pdf_path); $pages = $pdf->getPages(); } catch (Exception $e) { //error_log('Primary PDF parsing failed, attempting recovery: ' . $e->getMessage()); $repaired_path = $this->mxchat_attempt_pdf_repair($pdf_path); if ($repaired_path) { try { $parser = new \Smalot\PdfParser\Parser(); $pdf = $parser->parseFile($repaired_path); $pages = $pdf->getPages(); copy($repaired_path, $pdf_path); unlink($repaired_path); //error_log('PDF successfully repaired and parsed'); } catch (Exception $e2) { if (file_exists($repaired_path)) { unlink($repaired_path); } throw new Exception('PDF parsing failed even after repair attempt: ' . $e2->getMessage()); } } else { throw new Exception('PDF parsing failed and repair was unsuccessful: ' . $e->getMessage()); } } if (!$pages || count($pages) === 0) { throw new Exception('No pages found in PDF after parsing'); } // Get current progress (already fetched above for bot_id) if (!$status || !is_array($status)) { throw new Exception('Invalid status data retrieved from transient'); } // Initialize failed pages list if it doesn't exist if (!isset($status['failed_pages_list']) || !is_array($status['failed_pages_list'])) { $status['failed_pages_list'] = []; } $start_page = absint($status['processed_pages']); $end_page = min($start_page + $batch_size, $total_pages); // UPDATED: Get bot-specific options using the correct bot_id $bot_options = $this->get_bot_options($bot_id); $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options'); //error_log('[PDF CRON DEBUG] Using bot options for bot: ' . $bot_id); if (empty($options['api_key'])) { throw new Exception('API key is missing or invalid for bot: ' . $bot_id); } $successful_pages = 0; $failed_pages = 0; for ($i = $start_page; $i < $end_page; $i++) { $page_number = $i + 1; $max_retries = 3; $retry_count = 0; $page_processed = false; $last_error = ''; while (!$page_processed && $retry_count < $max_retries) { try { $text = $pages[$i]->getText(); if (empty($text)) { throw new Exception("Empty text on page {$page_number}"); } $sanitized_content = $this->mxchat_sanitize_content_for_api($text); if (empty($sanitized_content)) { throw new Exception("No valid content after sanitization on page {$page_number}"); } // UPDATED: Use bot-specific embedding generation with correct bot_id $embedding_vector = $this->mxchat_generate_embedding($sanitized_content, $bot_id); if (is_string($embedding_vector)) { throw new Exception("Embedding generation failed: " . $embedding_vector); } if (!is_array($embedding_vector)) { throw new Exception("Embedding generation returned unexpected result type: " . gettype($embedding_vector)); } $metadata = array( 'document_type' => 'pdf', 'total_pages' => $total_pages, 'current_page' => $page_number, 'prev_page' => $i > 0 ? $i : null, 'next_page' => $i < ($total_pages - 1) ? $i + 2 : null, 'source_url' => $pdf_url ); $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized_content; $page_url = esc_url($pdf_url . "#page=" . $page_number); // UPDATED: Pass correct bot_id to database submission $db_result = MxChat_Utils::submit_content_to_db($content_with_metadata, $page_url, $options['api_key'], null, $bot_id); if (is_wp_error($db_result)) { throw new Exception("Database submission failed: " . $db_result->get_error_message()); } // Success! $page_processed = true; $successful_pages++; //error_log('[PDF CRON DEBUG] Successfully processed page ' . $page_number . ' for bot: ' . $bot_id); } catch (Exception $e) { $retry_count++; $last_error = $e->getMessage(); //error_log("PDF page {$page_number} failed (attempt {$retry_count}/{$max_retries}): " . $last_error); if ($retry_count < $max_retries) { sleep(pow(2, $retry_count - 1)); } } } // If page still not processed after all retries, mark as failed if (!$page_processed) { $failed_pages++; $status['failed_pages_list'][] = [ 'page' => $page_number, 'error' => $last_error, 'time' => time(), 'retries' => $max_retries ]; if (count($status['failed_pages_list']) > 50) { $status['failed_pages_list'] = array_slice($status['failed_pages_list'], -50); } //error_log("PDF page {$page_number} permanently failed after {$max_retries} attempts: " . $last_error); } // Update progress $status['processed_pages'] = absint($page_number); $status['last_update'] = time(); $status['failed_pages'] = absint($status['failed_pages'] ?? 0) + ($page_processed ? 0 : 1); set_transient($status_key, $status, DAY_IN_SECONDS); } // Schedule next batch if needed if ($end_page < $total_pages) { // Use the same indexed array format (though bot_id still won't pass correctly) wp_schedule_single_event(time() + $batch_pause, 'mxchat_process_pdf_pages', array( $pdf_path, $pdf_url, $total_pages, $batch_size, $batch_pause, $bot_id // This still won't work, but we're now getting bot_id from status )); } else { // Processing complete $status['status'] = 'complete'; $status['processed_pages'] = $total_pages; $status['completion_summary'] = [ 'total_pages' => $total_pages, 'successful_pages' => $total_pages - absint($status['failed_pages'] ?? 0), 'failed_pages' => absint($status['failed_pages'] ?? 0), 'completion_time' => current_time('mysql') ]; set_transient($status_key, $status, DAY_IN_SECONDS); if (file_exists($pdf_path)) { wp_delete_file($pdf_path); } //error_log('[PDF CRON DEBUG] PDF processing completed for bot: ' . $bot_id); } } catch (\Exception $e) { //error_log(sprintf('[MXCHAT-PDF] Error processing PDF for bot %s: %s', $bot_id, $e->getMessage())); $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url)); $status = get_transient($status_key); if (!$status || !is_array($status)) { $status = array( 'total_pages' => $total_pages, 'processed_pages' => 0, 'status' => 'error', 'error' => sanitize_text_field($e->getMessage()), 'last_update' => time(), 'bot_id' => $bot_id ); } else { $status['status'] = 'error'; $status['error'] = sanitize_text_field($e->getMessage()); $status['last_update'] = time(); } set_transient($status_key, $status, DAY_IN_SECONDS); if (file_exists($pdf_path)) { wp_delete_file($pdf_path); } } } public function mxchat_save_inline_prompt() { // DEBUG: Log what we're receiving //error_log('=== MXCHAT DEBUG ==='); //error_log('POST data: ' . print_r($_POST, true)); //error_log('Nonce from POST: ' . ($_POST['_ajax_nonce'] ?? 'NOT FOUND')); // Check for nonce security check_ajax_referer('mxchat_save_inline_nonce', '_ajax_nonce'); // If we get here, nonce passed //error_log('Nonce verification PASSED'); // Verify permissions if (!current_user_can('manage_options')) { wp_send_json_error(esc_html__('Permission denied.', 'mxchat')); return; } global $wpdb; $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; // Validate and sanitize input data - FIXED LINE BELOW $prompt_id = isset($_POST['id']) ? absint($_POST['id']) : 0; $article_content = isset($_POST['article_content']) ? sanitize_textarea_field(wp_unslash($_POST['article_content'])) : ''; $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : ''; if ($prompt_id > 0 && !empty($article_content)) { // Re-generate the embedding vector for the updated content $embedding_vector = $this->mxchat_generate_embedding($article_content); if (is_array($embedding_vector)) { // Serialize the embedding vector before storing it $embedding_vector_serialized = serialize($embedding_vector); // Update the prompt in the database $updated = $wpdb->update( $table_name, array( 'article_content' => $article_content, 'embedding_vector' => $embedding_vector_serialized, 'source_url' => $article_url, ), array('id' => $prompt_id), array('%s', '%s', '%s'), array('%d') ); if ($updated !== false) { wp_send_json_success(); } else { wp_send_json_error(esc_html__('Database update failed.', 'mxchat')); } } else { wp_send_json_error(esc_html__('Embedding generation failed.', 'mxchat')); } } else { wp_send_json_error(esc_html__('Invalid data.', 'mxchat')); } } public function mxchat_get_pdf_processing_status($pdf_url) { $pdf_url = esc_url_raw($pdf_url); $status = get_transient(sanitize_key('mxchat_pdf_status_' . md5($pdf_url))); if (!$status || !is_array($status)) { return false; } // Check for stalled processing (no updates for 5 minutes) if ($status['status'] === 'processing' && (time() - absint($status['last_update'])) > 300) { $status['status'] = 'error'; $status['error'] = __('PDF processing appears to be stalled. No updates for over 5 minutes.', 'mxchat'); // Save the updated status set_transient( sanitize_key('mxchat_pdf_status_' . md5($pdf_url)), array_map('sanitize_text_field', $status), DAY_IN_SECONDS ); } $result = array( 'total_pages' => absint($status['total_pages']), 'processed_pages' => absint($status['processed_pages']), 'failed_pages' => absint($status['failed_pages'] ?? 0), 'percentage' => ($status['total_pages'] > 0) ? round((absint($status['processed_pages']) / absint($status['total_pages'])) * 100) : 0, 'status' => sanitize_text_field($status['status']), 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'), 'failed_pages_list' => isset($status['failed_pages_list']) ? $status['failed_pages_list'] : array(), 'completion_summary' => isset($status['completion_summary']) ? $status['completion_summary'] : null ); // Add error message if present if (isset($status['error']) && !empty($status['error'])) { $result['error'] = sanitize_text_field($status['error']); } return $result; } public function mxchat_handle_sitemap_submission() { // START DEBUG //error_log('[SITEMAP DEBUG] ===== Starting URL submission process ====='); //error_log('[SITEMAP DEBUG] POST data: ' . print_r($_POST, true)); // Get bot_id from form submission EARLY for debugging $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default'; //error_log('[SITEMAP DEBUG] Extracted bot_id: ' . $bot_id); //error_log('[SITEMAP DEBUG] Class exists MxChat_Multi_Bot_Manager: ' . (class_exists('MxChat_Multi_Bot_Manager') ? 'YES' : 'NO')); // END DEBUG // Check if the form was submitted and verify permissions if (!isset($_POST['submit_sitemap']) || !current_user_can('manage_options')) { //error_log('[SITEMAP DEBUG] Error: Unauthorized access or form not submitted properly'); wp_die(esc_html__('Unauthorized access', 'mxchat')); } // Verify nonce //error_log('[SITEMAP DEBUG] Verifying nonce'); check_admin_referer('mxchat_submit_sitemap_action', 'mxchat_submit_sitemap_nonce'); // Validate URL if (!isset($_POST['sitemap_url']) || empty($_POST['sitemap_url'])) { //error_log('[SITEMAP DEBUG] Error: Empty or missing URL'); set_transient('mxchat_admin_notice_error', esc_html__('Please provide a valid URL.', 'mxchat'), 30 ); wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts'))); exit; } $submitted_url = esc_url_raw($_POST['sitemap_url']); // Continue processing with already extracted bot_id //error_log('[SITEMAP DEBUG] Processing URL: ' . $submitted_url . ' for bot: ' . $bot_id); // UPDATED: Get bot-specific options and validate API key $bot_options = $this->get_bot_options($bot_id); //error_log('[SITEMAP DEBUG] Bot options retrieved: ' . print_r($bot_options, true)); $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options'); $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002'; //error_log('[SITEMAP DEBUG] Selected embedding model: ' . $selected_model); if (strpos($selected_model, 'voyage') === 0) { $api_key = $options['voyage_api_key'] ?? ''; $provider_name = 'Voyage AI'; } elseif (strpos($selected_model, 'gemini-embedding') === 0) { $api_key = $options['gemini_api_key'] ?? ''; $provider_name = 'Google Gemini'; } else { $api_key = $options['api_key'] ?? ''; $provider_name = 'OpenAI'; } //error_log('[SITEMAP DEBUG] Provider: ' . $provider_name . ', Has API key: ' . (!empty($api_key) ? 'YES' : 'NO')); if (empty($api_key)) { $error_message = sprintf( esc_html__('%s API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'), $provider_name ); //error_log('[SITEMAP DEBUG] Error: ' . $error_message); set_transient('mxchat_admin_notice_error', $error_message, 30); wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts'))); exit; } //error_log('[SITEMAP DEBUG] Fetching URL content'); $response = wp_remote_get($submitted_url, array('timeout' => 30)); if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response); //error_log('[SITEMAP DEBUG] Error fetching URL: ' . $error_message); set_transient('mxchat_admin_notice_error', sprintf( esc_html__('Failed to fetch the URL: %s', 'mxchat'), esc_html($error_message) ), 30 ); wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts'))); exit; } $content_type = wp_remote_retrieve_header($response, 'content-type'); //error_log('[SITEMAP DEBUG] Content type: ' . $content_type); $body_content = wp_remote_retrieve_body($response); if (empty($body_content)) { //error_log('[SITEMAP DEBUG] Error: Empty response body'); set_transient('mxchat_admin_notice_error', esc_html__('Empty response received from URL.', 'mxchat'), 30 ); wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts'))); exit; } //error_log('[SITEMAP DEBUG] Retrieved body content length: ' . strlen($body_content) . ' bytes'); // Handle PDF URL if ($this->mxchat_is_pdf_url($submitted_url, $response)) { //error_log('[SITEMAP DEBUG] Detected PDF URL, handling PDF for knowledge base'); //error_log('[SITEMAP DEBUG] About to call PDF handler with bot_id: ' . $bot_id); // UPDATED: Pass bot_id to PDF handler $result = $this->mxchat_handle_pdf_for_knowledge_base($submitted_url, $response, $bot_id); //error_log('[SITEMAP DEBUG] PDF handling result: ' . $result); if ($result === 'scheduled') { set_transient( 'mxchat_last_pdf_url', sanitize_text_field($submitted_url), DAY_IN_SECONDS ); // UPDATED: Store bot_id for PDF processing set_transient( 'mxchat_last_pdf_bot_id', $bot_id, DAY_IN_SECONDS ); //error_log('[SITEMAP DEBUG] PDF processing scheduled successfully for bot: ' . $bot_id); set_transient('mxchat_admin_notice_info', esc_html__('PDF processing has started in the background. You can check the progress in the Knowledge Base section.', 'mxchat'), 30 ); } else { //error_log('[SITEMAP DEBUG] PDF processing failed: ' . $result); set_transient('mxchat_admin_notice_error', esc_html__('Failed to start PDF processing: ', 'mxchat') . esc_html($result), 30 ); } wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts'))); exit; } // Handle Sitemap XML if (strpos($content_type, 'xml') !== false || strpos($body_content, 'message); } } set_transient('mxchat_admin_notice_error', esc_html__('Invalid sitemap XML. Please provide a valid sitemap.', 'mxchat'), 30 ); wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts'))); exit; } //error_log('[SITEMAP DEBUG] Valid XML found, handling sitemap for knowledge base'); // UPDATED: Pass bot_id to sitemap handler $result = $this->mxchat_handle_sitemap_for_knowledge_base($xml, $submitted_url, $bot_id); //error_log('[SITEMAP DEBUG] Sitemap handling result: ' . $result); if ($result === 'scheduled') { set_transient( 'mxchat_last_sitemap_url', sanitize_text_field($submitted_url), DAY_IN_SECONDS ); // UPDATED: Store bot_id for sitemap processing set_transient( 'mxchat_last_sitemap_bot_id', $bot_id, DAY_IN_SECONDS ); set_transient('mxchat_admin_notice_info', esc_html__('Sitemap processing has started in the background. You can check the progress in the Knowledge Base section.', 'mxchat'), 30 ); } else { set_transient('mxchat_admin_notice_error', esc_html__('Failed to start sitemap processing. Please check the status below for details.', 'mxchat'), 30 ); } wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts'))); exit; } // Handle Regular URL //error_log('[SITEMAP DEBUG] Processing as regular webpage'); $page_content = $this->mxchat_extract_main_content($body_content); //error_log('[SITEMAP DEBUG] Extracted content length: ' . strlen($page_content) . ' bytes'); $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content); //error_log('[SITEMAP DEBUG] Sanitized content length: ' . strlen($sanitized_content) . ' bytes'); if (empty($sanitized_content)) { //error_log('[SITEMAP DEBUG] Error: No valid content after sanitization'); set_transient('mxchat_admin_notice_error', esc_html__('No valid content found on the provided URL.', 'mxchat'), 30 ); set_transient('mxchat_single_url_status', [ 'url' => $submitted_url, 'timestamp' => current_time('mysql'), 'status' => 'failed', 'error' => esc_html__('No valid content found on the provided URL.', 'mxchat') ], DAY_IN_SECONDS); wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts'))); exit; } //error_log('[SITEMAP DEBUG] Generating embedding for content'); $embedding_vector = $this->mxchat_generate_embedding($sanitized_content, $bot_id); if (is_string($embedding_vector)) { //error_log('[SITEMAP DEBUG] Error generating embedding: ' . $embedding_vector); $error_message = esc_html__('Failed to generate embedding: ', 'mxchat') . esc_html($embedding_vector); set_transient('mxchat_admin_notice_error', $error_message, 30); set_transient('mxchat_single_url_status', [ 'url' => $submitted_url, 'timestamp' => current_time('mysql'), 'status' => 'failed', 'error' => $error_message ], DAY_IN_SECONDS); wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts'))); exit; } if (is_array($embedding_vector)) { //error_log('[SITEMAP DEBUG] Successfully generated embedding with ' . count($embedding_vector) . ' dimensions'); // UPDATED: Pass bot_id to database submission $db_result = MxChat_Utils::submit_content_to_db( $sanitized_content, $submitted_url, $api_key, null, $bot_id ); if (is_wp_error($db_result)) { //error_log('[SITEMAP DEBUG] Error: Failed to store content in database: ' . $db_result->get_error_message()); $error_message = esc_html__('Failed to store content in database: ', 'mxchat') . esc_html($db_result->get_error_message()); set_transient('mxchat_admin_notice_error', $error_message, 30); set_transient('mxchat_single_url_status', [ 'url' => $submitted_url, 'timestamp' => current_time('mysql'), 'status' => 'failed', 'error' => $error_message ], DAY_IN_SECONDS); wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts'))); exit; } //error_log('[SITEMAP DEBUG] Successfully stored content in database'); $success_message = esc_html__('URL content successfully submitted!', 'mxchat'); set_transient('mxchat_admin_notice_success', $success_message, 30); set_transient('mxchat_single_url_status', [ 'url' => $submitted_url, 'timestamp' => current_time('mysql'), 'status' => 'complete', 'content_length' => strlen($sanitized_content), 'embedding_dimensions' => count($embedding_vector) ], DAY_IN_SECONDS); } else { //error_log('[SITEMAP DEBUG] Error: Failed to generate embedding. Unexpected result type: ' . gettype($embedding_vector)); $error_message = esc_html__('Failed to generate embedding: Unexpected result type. Please check your API key and try again.', 'mxchat'); set_transient('mxchat_admin_notice_error', $error_message, 30); set_transient('mxchat_single_url_status', [ 'url' => $submitted_url, 'timestamp' => current_time('mysql'), 'status' => 'failed', 'error' => $error_message ], DAY_IN_SECONDS); } //error_log('[SITEMAP DEBUG] ===== Completed URL submission process ====='); wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts'))); exit; } public function mxchat_get_single_url_status() { $status = get_transient('mxchat_single_url_status'); if (!$status) { return null; } // Add human-readable time if (isset($status['timestamp'])) { $status['human_time'] = human_time_diff(strtotime($status['timestamp']), current_time('timestamp')) . ' ' . __('ago', 'mxchat'); } return $status; } public function mxchat_handle_sitemap_for_knowledge_base($xml, $sitemap_url, $bot_id = 'default') { delete_transient('mxchat_single_url_status'); if (!current_user_can('manage_options')) { //error_log(esc_html__('Unauthorized sitemap processing attempt', 'mxchat')); return false; } try { $sitemap_url = esc_url_raw($sitemap_url); if (!$xml || !is_object($xml)) { throw new Exception(__('Invalid XML object provided', 'mxchat')); } // UPDATED: Get bot-specific embedding API for validation $bot_options = $this->get_bot_options($bot_id); $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options'); // ADD THIS: Test the embedding API before processing $test_phrase = "Test embedding generation for MxChat"; $test_result = $this->mxchat_generate_embedding($test_phrase, $bot_id); if (is_string($test_result)) { //error_log('[MXCHAT-URL] Embedding API validation failed: ' . $test_result); $status_data = array( 'total_urls' => 0, 'processed_urls' => 0, 'status' => 'error', 'error' => __('Embedding API validation failed: ', 'mxchat') . $test_result, 'last_update' => time(), 'bot_id' => $bot_id // ADDED ); set_transient( sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)), array_map('sanitize_text_field', $status_data), DAY_IN_SECONDS ); throw new Exception(__('Embedding API validation failed: ', 'mxchat') . $test_result); } if (!is_array($test_result)) { //error_log('[MXCHAT-URL] Embedding API returned unexpected result type: ' . gettype($test_result)); throw new Exception(__('Embedding API returned unexpected result type. Please check your configuration.', 'mxchat')); } $urls = []; foreach ($xml->url as $url_element) { $url = esc_url_raw((string)$url_element->loc); if ($url) { $urls[] = $url; } } $total_urls = absint(count($urls)); if ($total_urls < 1) { throw new Exception(__('No valid URLs found in sitemap', 'mxchat')); } // UPDATED: Pass bot_id to sitemap processing cron wp_schedule_single_event(time(), 'mxchat_process_sitemap_urls', array( 'urls' => $urls, 'sitemap_url' => $sitemap_url, 'total_urls' => $total_urls, 'batch_size' => absint(10), 'batch_pause' => absint(5), 'bot_id' => $bot_id // ADDED )); $status_data = array( 'total_urls' => $total_urls, 'processed_urls' => 0, 'status' => 'processing', 'last_update' => time(), 'bot_id' => $bot_id // ADDED ); set_transient( sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)), array_map('sanitize_text_field', $status_data), DAY_IN_SECONDS ); return __('scheduled', 'mxchat'); } catch (\Exception $e) { $error_message = $e->getMessage(); //error_log(sprintf(esc_html__('Error preparing sitemap for processing: %s', 'mxchat'), esc_html($error_message))); set_transient( 'mxchat_last_sitemap_url', sanitize_text_field($sitemap_url), DAY_IN_SECONDS ); $status_data = array( 'total_urls' => 0, 'processed_urls' => 0, 'status' => 'error', 'error' => $error_message, 'last_update' => time(), 'bot_id' => $bot_id // ADDED ); set_transient( sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)), array_map('sanitize_text_field', $status_data), DAY_IN_SECONDS ); return $error_message; } } public function mxchat_process_sitemap_urls_cron($urls, $sitemap_url, $total_urls, $batch_size, $batch_pause, $bot_id = 'default') { // Validate inputs $sitemap_url = esc_url_raw($sitemap_url); $total_urls = absint($total_urls); $batch_size = absint($batch_size); $batch_pause = absint($batch_pause); $bot_id = sanitize_key($bot_id); if (!is_array($urls) || empty($urls)) { return; } try { $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)); $status = get_transient($status_key); if (!$status || !is_array($status)) { throw new Exception('Invalid status data retrieved from transient'); } // Initialize failed_urls array if it doesn't exist if (!isset($status['failed_urls_list']) || !is_array($status['failed_urls_list'])) { $status['failed_urls_list'] = []; } $start_url = absint($status['processed_urls']); $end_url = min($start_url + $batch_size, $total_urls); // UPDATED: Get bot-specific options $bot_options = $this->get_bot_options($bot_id); $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options'); // Track batch statistics $batch_stats = [ 'processed' => 0, 'failed' => 0, 'last_error' => '', 'embedding_errors' => 0, 'network_errors' => 0, 'timeout_errors' => 0 ]; @set_time_limit(300); for ($i = $start_url; $i < $end_url; $i++) { $page_url = esc_url_raw($urls[$i]); $max_retries = 5; $retry_count = 0; $url_processed = false; $last_error = ''; if (memory_get_usage(true) > (1024 * 1024 * 100)) { //error_log('MxChat: Memory usage high, taking break'); sleep(2); } while (!$url_processed && $retry_count < $max_retries) { try { $timeout = 30 + ($retry_count * 10); $page_response = wp_remote_get($page_url, array( 'timeout' => $timeout, 'redirection' => 5, 'user-agent' => 'MxChat/1.0' )); if (is_wp_error($page_response)) { $error_msg = $page_response->get_error_message(); if (strpos($error_msg, 'timeout') !== false) { $batch_stats['timeout_errors']++; } else { $batch_stats['network_errors']++; } throw new Exception('HTTP request failed: ' . $error_msg); } $response_code = wp_remote_retrieve_response_code($page_response); if (!in_array($response_code, [200, 201, 202])) { if ($response_code >= 400 && $response_code < 500) { throw new Exception('HTTP Status: ' . $response_code . ' (permanent failure)'); } throw new Exception('HTTP Status: ' . $response_code); } $page_html = wp_remote_retrieve_body($page_response); if (empty($page_html)) { throw new Exception('Empty response body'); } $page_content = $this->mxchat_extract_main_content($page_html); $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content); if (empty($sanitized_content)) { //error_log("MxChat: No content found for URL: {$page_url}"); $url_processed = true; $batch_stats['processed']++; break; } // UPDATED: Use bot-specific embedding generation $embedding_vector = $this->mxchat_generate_embedding($sanitized_content, $bot_id); if (is_string($embedding_vector)) { $batch_stats['embedding_errors']++; if (strpos($embedding_vector, 'rate limit') !== false || strpos($embedding_vector, 'quota') !== false) { sleep(30 + ($retry_count * 10)); } throw new Exception('Embedding generation failed: ' . $embedding_vector); } if (!is_array($embedding_vector)) { throw new Exception('Embedding generation returned unexpected result type: ' . gettype($embedding_vector)); } // UPDATED: Pass bot_id to database submission $submission_result = MxChat_Utils::submit_content_to_db($sanitized_content, $page_url, $options['api_key'], null, $bot_id); if (is_wp_error($submission_result)) { throw new Exception('Database submission failed: ' . $submission_result->get_error_message()); } // Success! $url_processed = true; $batch_stats['processed']++; } catch (Exception $e) { $retry_count++; $last_error = $e->getMessage(); if (strpos($last_error, 'rate limit') !== false) { sleep(60); } elseif (strpos($last_error, 'timeout') !== false) { sleep(10); } elseif (strpos($last_error, 'permanent failure') !== false) { break; } else { sleep(pow(2, $retry_count - 1)); } } } // If URL still not processed after all retries, mark as failed if (!$url_processed) { $batch_stats['failed']++; $batch_stats['last_error'] = $last_error; $status['failed_urls_list'][] = [ 'url' => $page_url, 'error' => $last_error, 'time' => time(), 'retries' => $max_retries ]; if (count($status['failed_urls_list']) > 100) { $status['failed_urls_list'] = array_slice($status['failed_urls_list'], -100); } } // Update progress after each URL $status['processed_urls'] = absint($i + 1); $status['last_update'] = time(); $status['failed_urls'] = absint($status['failed_urls'] ?? 0) + ($url_processed ? 0 : 1); $status['last_error'] = $batch_stats['last_error']; set_transient($status_key, $status, DAY_IN_SECONDS); } $failure_rate = $batch_stats['failed'] / max(1, $batch_stats['processed'] + $batch_stats['failed']); if ($batch_stats['processed'] === 0 && $batch_stats['failed'] >= 5) { $status['status'] = 'error'; $status['error'] = sprintf( 'Processing stopped after %d consecutive failures. Last error: %s', $batch_stats['failed'], $batch_stats['last_error'] ); set_transient($status_key, $status, DAY_IN_SECONDS); return; } // Update final progress $status['processed_urls'] = min($end_url, $total_urls); $status['last_update'] = time(); $status['batch_stats'] = $batch_stats; set_transient($status_key, $status, DAY_IN_SECONDS); // Check if we've processed all URLs if ($end_url >= $total_urls) { // All URLs have been processed - mark as complete $status['status'] = 'complete'; $status['processed_urls'] = $total_urls; $status['completion_summary'] = [ 'total_urls' => $total_urls, 'successful_urls' => $total_urls - absint($status['failed_urls'] ?? 0), 'failed_urls' => absint($status['failed_urls'] ?? 0), 'completion_time' => current_time('mysql'), 'final_batch_stats' => $batch_stats ]; set_transient($status_key, $status, DAY_IN_SECONDS); } else { $dynamic_pause = $batch_pause; if ($failure_rate > 0.5) { $dynamic_pause *= 3; } elseif ($batch_stats['embedding_errors'] > 3) { $dynamic_pause *= 2; } // UPDATED: Pass bot_id to next batch wp_schedule_single_event(time() + $dynamic_pause, 'mxchat_process_sitemap_urls', array( 'urls' => $urls, 'sitemap_url' => $sitemap_url, 'total_urls' => $total_urls, 'batch_size' => $batch_size, 'batch_pause' => $batch_pause, 'bot_id' => $bot_id // ADDED )); } } catch (\Exception $e) { $status['last_error'] = $e->getMessage(); $status['error_count'] = ($status['error_count'] ?? 0) + 1; if ($status['error_count'] >= 5) { $status['status'] = 'error'; $status['error'] = 'Too many batch failures: ' . $e->getMessage(); } else { // UPDATED: Pass bot_id to retry batch wp_schedule_single_event(time() + 300, 'mxchat_process_sitemap_urls', array( 'urls' => $urls, 'sitemap_url' => $sitemap_url, 'total_urls' => $total_urls, 'batch_size' => max(5, $batch_size / 2), 'batch_pause' => $batch_pause * 2, 'bot_id' => $bot_id // ADDED )); } set_transient($status_key, $status, DAY_IN_SECONDS); } } public function mxchat_sanitize_content_for_api($content) { //error_log('[MXCHAT-SANITIZE] Original content preview: ' . substr($content, 0, 500) . '...'); // Remove script, style tags, and HTML comments $content = preg_replace('/]*>(.*?)<\/script>/is', '', $content); $content = preg_replace('/]*>(.*?)<\/style>/is', '', $content); $content = preg_replace('//', '', $content); // Remove all HTML tags and decode HTML entities $content = wp_strip_all_tags($content); $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5); // Normalize whitespace but preserve paragraph breaks // First, normalize line endings to \n $content = str_replace(["\r\n", "\r"], "\n", $content); // Replace multiple spaces/tabs with single space, but preserve newlines $content = preg_replace('/[ \t]+/', ' ', $content); // Replace 3+ newlines with 2 newlines (max 2 blank lines) $content = preg_replace('/\n{3,}/', "\n\n", $content); // Trim each line $lines = explode("\n", $content); $lines = array_map('trim', $lines); $content = implode("\n", $lines); // Final trim $content = trim($content); // Remove control characters (which can cause database issues) but preserve \n (0x0A) and \r (0x0D) $content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $content); // Remove NULL bytes which can cause database errors $content = str_replace("\0", "", $content); // Ensure valid UTF-8 encoding $content = wp_check_invalid_utf8($content); // Remove any extremely long strings without spaces (often garbage) $content = preg_replace('/\S{300,}/', ' ', $content); // Replace problematic characters that often cause database issues $content = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $content); // Remove emoji and other high Unicode characters // Replace any remaining potentially problematic characters with spaces // BUT preserve newlines by temporarily replacing them $content = str_replace("\n", "NEWLINE_PLACEHOLDER", $content); $content = preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\p{Sm}]/u', ' ', $content); $content = str_replace("NEWLINE_PLACEHOLDER", "\n", $content); // Limit to reasonable length if needed $max_length = 65000; // Just under MySQL TEXT field limit if (strlen($content) > $max_length) { $content = substr($content, 0, $max_length); } //error_log('[MXCHAT-SANITIZE] Sanitized content preview: ' . substr($content, 0, 500) . '...'); return $content; } public function mxchat_extract_main_content($html) { if (empty($html)) { return ''; } try { $dom = new DOMDocument; libxml_use_internal_errors(true); // Suppress HTML parsing errors @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); $xpath = new DOMXPath($dom); // For debugging purposes $debugEnabled = false; // Set to true to enable debugging output $debug = function($message) use ($debugEnabled) { if ($debugEnabled) { //error_log('[MXCHAT-DEBUG] ' . $message); } }; // Direct targeting for Gerow theme posts $post_text = $xpath->query('//div[contains(@class, "post-text")]'); if ($post_text && $post_text->length > 0) { $debug("Found post-text directly"); $content = ''; foreach ($post_text as $node) { $content .= $dom->saveHTML($node); } if (!empty($content)) { $debug("Returning post-text content"); return $content; } } // Try to get the blog details content which contains the post-text $blog_details = $xpath->query('//div[contains(@class, "blog-details-content")]'); if ($blog_details && $blog_details->length > 0) { $debug("Found blog-details-content"); $content = ''; foreach ($blog_details as $node) { $content .= $dom->saveHTML($node); } if (!empty($content)) { $debug("Returning blog-details-content"); return $content; } } // Try to get the article which contains the blog details $article = $xpath->query('//article[contains(@class, "blog-details-wrap")]'); if ($article && $article->length > 0) { $debug("Found article with blog-details-wrap"); $content = ''; foreach ($article as $node) { $content .= $dom->saveHTML($node); } if (!empty($content)) { $debug("Returning article content"); return $content; } } // Try even broader with the blog-item-wrap $blog_item = $xpath->query('//div[contains(@class, "blog-item-wrap")]'); if ($blog_item && $blog_item->length > 0) { $debug("Found blog-item-wrap"); $content = ''; foreach ($blog_item as $node) { $content .= $dom->saveHTML($node); } if (!empty($content)) { $debug("Returning blog-item-wrap content"); return $content; } } // Specific Gerow theme path $gerow_path = $xpath->query('//section[contains(@class, "blog-area")]//div[contains(@class, "post-text")]'); if ($gerow_path && $gerow_path->length > 0) { $debug("Found Gerow theme path to post-text"); $content = ''; foreach ($gerow_path as $node) { $content .= $dom->saveHTML($node); } if (!empty($content)) { $debug("Returning Gerow post-text content"); return $content; } } // Generic blog post selectors $selectors = [ // Blog post specific selectors '//div[contains(@class, "post-text")]', '//article[contains(@class, "blog-post-item")]//div[contains(@class, "post-text")]', '//div[contains(@class, "blog-details-content")]', '//article[contains(@class, "blog-details-wrap")]', '//div[contains(@class, "entry-content")]', '//div[contains(@class, "blog-content")]', '//div[contains(@class, "blog-item-wrap")]', // More general content selectors '//div[contains(@class, "page__content")]', '//div[contains(@class, "elementor-widget-container")]', '//div[contains(@class, "elementor-text-editor")]', '//div[contains(@class, "elementor-widget-text-editor")]', '//*[contains(@class, "entry-content")]', '//*[contains(@class, "post-content")]', '//*[contains(@class, "article-content")]', '//*[@id="content"]', '//*[@id="main-content"]', '//section[contains(@class, "blog-area")]', '//article', '//main', '//div[contains(@class, "content")]' ]; // First handle Elementor content $debug("Checking for Elementor content"); $elementor_widgets = $xpath->query('//div[contains(@class, "elementor-element")]//div[contains(@class, "elementor-widget-container")]'); if ($elementor_widgets && $elementor_widgets->length > 0) { $debug("Found Elementor widgets"); $combined_content = ''; foreach ($elementor_widgets as $widget) { $widget_content = $dom->saveHTML($widget); if (!empty($widget_content)) { $combined_content .= $widget_content; } } if (!empty($combined_content)) { $debug("Returning Elementor content"); return $combined_content; } } // Try standard selectors one by one foreach ($selectors as $selector) { $debug("Trying selector: " . $selector); $nodes = $xpath->query($selector); if ($nodes && $nodes->length > 0) { $debug("Found matches for selector: " . $selector); $content = ''; foreach ($nodes as $node) { $content .= $dom->saveHTML($node); } if (!empty($content)) { $debug("Returning content from selector: " . $selector); return $content; } } } // Manual regex fallback for post-text if DOM methods fail $debug("Trying regex fallback"); if (preg_match('/
(.*?)<\/div>\s*<\/div>\s*<\/div>/s', $html, $matches)) { $debug("Found post-text via regex"); return '
' . $matches[1] . '
'; } // Try to extract the blog section as a whole $blog_section = $xpath->query('//section[contains(@class, "blog-area")]'); if ($blog_section && $blog_section->length > 0) { $debug("Found blog-area section"); $content = ''; foreach ($blog_section as $node) { $content .= $dom->saveHTML($node); } if (!empty($content)) { $debug("Returning blog-area section content"); return $content; } } // Fallback: Return the body content if no specific selector matches $debug("Using body fallback"); $body = $dom->getElementsByTagName('body'); if ($body->length > 0) { return $dom->saveHTML($body->item(0)); } // Last resort: return the original HTML $debug("Returning original HTML"); return $html; } catch (Exception $e) { //error_log('[MXCHAT-ERROR] Content extraction failed: ' . $e->getMessage()); return $html; // Return original HTML if parsing fails } finally { libxml_clear_errors(); } } public function mxchat_get_sitemap_processing_status($sitemap_url) { $sitemap_url = esc_url_raw($sitemap_url); $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)); $status = get_transient($status_key); if (!$status || !is_array($status)) { return false; } // Auto-complete check: if all URLs are processed but status isn't complete if (isset($status['processed_urls']) && isset($status['total_urls']) && $status['processed_urls'] >= $status['total_urls'] && isset($status['status']) && $status['status'] !== 'complete' && $status['status'] !== 'error') { // Mark as complete $status['status'] = 'complete'; $status['processed_urls'] = $status['total_urls']; // Ensure exact match // Update the transient with the corrected status set_transient($status_key, $status, DAY_IN_SECONDS); } return array( 'total_urls' => absint($status['total_urls']), 'processed_urls' => absint($status['processed_urls']), 'failed_urls' => absint($status['failed_urls'] ?? 0), 'percentage' => ($status['total_urls'] > 0) ? round((absint($status['processed_urls']) / absint($status['total_urls'])) * 100) : 0, 'status' => sanitize_text_field($status['status']), 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'), 'error' => isset($status['error']) ? sanitize_text_field($status['error']) : '', 'last_error' => isset($status['last_error']) ? sanitize_text_field($status['last_error']) : '', 'failed_urls_list' => isset($status['failed_urls_list']) ? $status['failed_urls_list'] : array() ); } public function mxchat_ajax_get_status_updates() { try { // Verify the request check_ajax_referer('mxchat_status_nonce', 'nonce'); // Get the status just like in your admin page $pdf_url = get_transient('mxchat_last_pdf_url'); $sitemap_url = get_transient('mxchat_last_sitemap_url'); $pdf_status = $pdf_url ? $this->mxchat_get_pdf_processing_status($pdf_url) : false; $sitemap_status = $sitemap_url ? $this->mxchat_get_sitemap_processing_status($sitemap_url) : false; // Add the PDF URL to the status object if ($pdf_status && $pdf_url) { $pdf_status['pdf_url'] = $pdf_url; } // Set the current PDF URL for the manual batch processing button $current_pdf_url = $pdf_url; // Check for true processing status, not just presence of status $is_active_processing = ($sitemap_status && isset($sitemap_status['status']) && $sitemap_status['status'] === 'processing') || ($pdf_status && isset($pdf_status['status']) && $pdf_status['status'] === 'processing'); // Get single URL status, but only if no processing is active $single_url_status = !$is_active_processing ? $this->mxchat_get_single_url_status() : false; // REMOVED: Auto-clearing of completed status - now only done via dismiss button // Return JSON response with the status data wp_send_json(array( 'pdf_status' => $pdf_status, 'sitemap_status' => $sitemap_status, 'single_url_status' => $single_url_status, 'is_processing' => $is_active_processing, 'current_pdf_url' => $current_pdf_url )); } catch (Exception $e) { // Log the error //error_log('MxChat Status Update Error: ' . $e->getMessage()); // Return a friendly error response wp_send_json_error(array( 'message' => 'Error getting status updates: ' . $e->getMessage(), 'status' => 'error' )); } } public function mxchat_stop_processing() { // Verify permissions if (!current_user_can('manage_options')) { wp_die(esc_html__('Unauthorized access', 'mxchat')); } // Verify nonce check_admin_referer('mxchat_stop_processing_action', 'mxchat_stop_processing_nonce'); // Get the last sitemap URL and clear its transient $sitemap_url = get_transient('mxchat_last_sitemap_url'); if ($sitemap_url) { delete_transient('mxchat_sitemap_status_' . md5($sitemap_url)); delete_transient('mxchat_last_sitemap_url'); } // Get the last PDF URL and clear its transient $pdf_url = get_transient('mxchat_last_pdf_url'); if ($pdf_url) { delete_transient('mxchat_pdf_status_' . md5($pdf_url)); delete_transient('mxchat_last_pdf_url'); } // Unschedule any pending sitemap events $timestamp = wp_next_scheduled('mxchat_process_sitemap_urls'); if ($timestamp) { wp_unschedule_event($timestamp, 'mxchat_process_sitemap_urls'); } // Redirect back with a success message set_transient('mxchat_admin_notice_success', esc_html__('Processing has been stopped successfully.', 'mxchat'), 30 ); wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts')); exit; } /** * Get content list for processing */ public function ajax_mxchat_get_content_list() { // Verify the nonce check_ajax_referer('mxchat_content_selector_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Unauthorized access', 'mxchat')); } $page = isset($_GET['page']) ? absint($_GET['page']) : 1; $per_page = isset($_GET['per_page']) ? absint($_GET['per_page']) : 50; $search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : ''; $post_type = isset($_GET['post_type']) ? sanitize_text_field($_GET['post_type']) : 'all'; $post_status = isset($_GET['post_status']) ? sanitize_text_field($_GET['post_status']) : 'publish'; $processed_filter = isset($_GET['processed_filter']) ? sanitize_text_field($_GET['processed_filter']) : 'all'; // Build query args $args = array( 'posts_per_page' => $per_page, 'paged' => $page, 'post_status' => $post_status !== 'all' ? $post_status : array('publish', 'draft', 'pending'), 'orderby' => 'date', 'order' => 'DESC', ); // Handle post types - IMPROVED VERSION if ($post_type !== 'all') { $args['post_type'] = $post_type; } else { // Get all available post types that might contain content $all_post_types = array(); // First get all public post types $public_types = get_post_types(array('public' => true), 'names'); $all_post_types = array_merge($all_post_types, $public_types); // Add common forum/community post types $forum_types = array('topic', 'reply', 'forum', 'wpforo_topic', 'wpforo_post'); foreach ($forum_types as $forum_type) { if (post_type_exists($forum_type)) { $all_post_types[] = $forum_type; } } // Add other commonly used post types $common_types = array('product', 'job_listing', 'event', 'portfolio'); foreach ($common_types as $common_type) { if (post_type_exists($common_type)) { $all_post_types[] = $common_type; } } // Remove duplicates and ensure we have at least some post types $all_post_types = array_unique($all_post_types); if (empty($all_post_types)) { // Fallback to basic post types $all_post_types = array('post', 'page'); } $args['post_type'] = $all_post_types; // Debug logging to see what post types are being queried //error_log('MxChat Debug: Querying post types: ' . implode(', ', $all_post_types)); } if (!empty($search)) { $args['s'] = $search; } // Get processed data from storage $processed_data = array(); $pinecone_options = get_option('mxchat_pinecone_addon_options', array()); $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1'; if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) { // Get fresh data from Pinecone - no caching $processed_data = $this->mxchat_get_pinecone_processed_content($pinecone_options); } else { // WordPress DB checking with better URL matching for all post types global $wpdb; $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; $processed_items = $wpdb->get_results("SELECT id, source_url, timestamp FROM {$table_name}"); if (!empty($processed_items)) { foreach ($processed_items as $item) { // Use improved URL matching that works for all post types $post_id = $this->mxchat_url_to_post_id_improved($item->source_url); if ($post_id) { $processed_data[$post_id] = array( 'db_id' => $item->id, 'timestamp' => $item->timestamp, 'url' => $item->source_url, 'source' => 'wordpress' ); } } } } // Get processed IDs as a simple array for in_array checks $processed_ids = array_keys($processed_data); // Handle processed/unprocessed filter if ($processed_filter === 'processed' && !empty($processed_ids)) { $args['post__in'] = $processed_ids; } elseif ($processed_filter === 'unprocessed' && !empty($processed_ids)) { $args['post__not_in'] = $processed_ids; } // Run the query $query = new WP_Query($args); $content_items = array(); if ($query->have_posts()) { while ($query->have_posts()) { $query->the_post(); $id = get_the_ID(); $post_date = get_the_date(); $excerpt = wp_trim_words(get_the_excerpt(), 20, '...'); $word_count = str_word_count(strip_tags(get_the_content())); $is_processed = in_array($id, $processed_ids); $processed_date = ''; $db_record_id = 0; $data_source = 'none'; if ($is_processed && isset($processed_data[$id])) { $item_data = $processed_data[$id]; $data_source = $item_data['source']; if ($data_source === 'wordpress' && isset($item_data['timestamp'])) { // WordPress DB format $timestamp = strtotime($item_data['timestamp']); $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago'; $db_record_id = $item_data['db_id']; } elseif ($data_source === 'pinecone') { // Pinecone format $processed_date = $item_data['processed_date']; $db_record_id = $item_data['db_id']; } } $content_items[] = array( 'id' => $id, 'title' => get_the_title(), 'permalink' => get_permalink(), 'date' => $post_date, 'type' => get_post_type(), 'status' => get_post_status(), 'excerpt' => $excerpt, 'word_count' => $word_count, 'already_processed' => $is_processed, 'processed_date' => $processed_date, 'db_record_id' => $db_record_id, 'data_source' => $data_source ); } wp_reset_postdata(); } $response = array( 'items' => $content_items, 'total' => $query->found_posts, 'total_pages' => $query->max_num_pages, 'current_page' => $page, 'processed_count' => count($processed_ids) ); wp_send_json_success($response); exit; } /** * This function handles various WooCommerce URL formats and permalink structures */ private function mxchat_url_to_post_id_improved($url) { // First try the standard WordPress function $post_id = url_to_postid($url); if ($post_id > 0) { return $post_id; } // If that fails, try more aggressive URL matching // Remove trailing slashes and query parameters for better matching $clean_url = rtrim($url, '/'); $clean_url = strtok($clean_url, '?'); // Remove query parameters // Try again with cleaned URL $post_id = url_to_postid($clean_url); if ($post_id > 0) { return $post_id; } // For bbPress forum topics, try extracting slug from URL if (strpos($url, '/topic/') !== false || strpos($url, '/forums/') !== false) { // Handle bbPress URLs: /forums/topic/topic-name/ if (preg_match('/\/forums\/topic\/([^\/\?]+)/', $url, $matches)) { $topic_slug = $matches[1]; // Look up topic by slug $topic = get_page_by_path($topic_slug, OBJECT, 'topic'); if ($topic) { return $topic->ID; } // Alternative method: query by post_name global $wpdb; $post_id = $wpdb->get_var($wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')", $topic_slug )); if ($post_id) { return intval($post_id); } } // Handle simpler topic URLs: /topic/topic-name/ if (preg_match('/\/topic\/([^\/\?]+)/', $url, $matches)) { $topic_slug = $matches[1]; global $wpdb; $post_id = $wpdb->get_var($wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')", $topic_slug )); if ($post_id) { return intval($post_id); } } } // For WooCommerce products if (strpos($url, '/product/') !== false || strpos($url, 'product=') !== false) { // Extract product slug from various URL formats $product_slug = ''; // Handle pretty permalinks: /product/product-name/ if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) { $product_slug = $matches[1]; } // Handle query parameters: ?product=product-name elseif (preg_match('/[\?&]product=([^&]+)/', $url, $matches)) { $product_slug = $matches[1]; } if (!empty($product_slug)) { // Look up product by slug $product = get_page_by_path($product_slug, OBJECT, 'product'); if ($product) { return $product->ID; } // Alternative method: query by post_name global $wpdb; $post_id = $wpdb->get_var($wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'product' AND post_status = 'publish'", $product_slug )); if ($post_id) { return intval($post_id); } } } // Generic approach: try to extract slug and match against all post types $parsed_url = wp_parse_url($clean_url); $path = $parsed_url['path'] ?? ''; if (!empty($path)) { // Get the last part of the path as potential slug $path_parts = array_filter(explode('/', trim($path, '/'))); $potential_slug = end($path_parts); if (!empty($potential_slug)) { global $wpdb; // Try to find any post with this slug $post_id = $wpdb->get_var($wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_status IN ('publish', 'closed', 'private') AND post_type NOT IN ('revision', 'attachment', 'nav_menu_item') ORDER BY CASE WHEN post_type = 'post' THEN 1 WHEN post_type = 'page' THEN 2 WHEN post_type = 'topic' THEN 3 WHEN post_type = 'product' THEN 4 ELSE 5 END LIMIT 1", $potential_slug )); if ($post_id) { return intval($post_id); } } } // ADDITIONAL: Try direct database lookup by URL variations global $wpdb; $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; // Try variations of the URL (with/without trailing slash, http/https) $url_variations = array( $url, rtrim($url, '/'), $url . '/', str_replace('http://', 'https://', $url), str_replace('https://', 'http://', $url), str_replace('http://', 'https://', rtrim($url, '/')), str_replace('https://', 'http://', rtrim($url, '/')) ); // Remove duplicates $url_variations = array_unique($url_variations); foreach ($url_variations as $variation) { $existing_record = $wpdb->get_row($wpdb->prepare( "SELECT id, source_url FROM $table_name WHERE source_url = %s", $variation )); if ($existing_record) { // Try to get post ID from this stored URL $stored_post_id = url_to_postid($existing_record->source_url); if ($stored_post_id > 0) { return $stored_post_id; } } } return 0; // No match found } /** * Process selected content via AJAX */ public function ajax_mxchat_process_selected_content() { // Basic request validation if (!check_ajax_referer('mxchat_content_selector_nonce', 'nonce', false)) { wp_send_json_error('Invalid nonce'); exit; } if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized access'); exit; } // Get post IDs - safely parse the array $post_ids = array(); if (isset($_POST['post_ids']) && is_array($_POST['post_ids'])) { foreach ($_POST['post_ids'] as $id) { $post_ids[] = absint($id); } } if (empty($post_ids)) { wp_send_json_error('No content selected'); exit; } // UPDATED: Get bot_id from request $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default'; // Process only ONE post at a time to avoid request size issues $post_id = reset($post_ids); $post = get_post($post_id); if (!$post) { wp_send_json_error('Post not found'); exit; } // Get content including ACF fields $content = $post->post_title . "\n\n" . wp_strip_all_tags($post->post_content); // ADD ACF FIELDS SUPPORT $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id); if (!empty($acf_fields)) { $acf_content_parts = array(); foreach ($acf_fields as $field_name => $field_value) { $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id); if (!empty($formatted_value)) { $field_label = ucwords(str_replace('_', ' ', $field_name)); $acf_content_parts[] = $field_label . ": " . $formatted_value; } } if (!empty($acf_content_parts)) { $content .= "\n\n" . implode("\n", $acf_content_parts); } } $content = substr($content, 0, 10000); // Limit content size // UPDATED: Get bot-specific API key $bot_options = $this->get_bot_options($bot_id); $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options'); $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002'; if (strpos($selected_model, 'voyage') === 0) { $api_key = $options['voyage_api_key'] ?? ''; $provider_name = 'Voyage AI'; } elseif (strpos($selected_model, 'gemini-embedding') === 0) { $api_key = $options['gemini_api_key'] ?? ''; $provider_name = 'Google Gemini'; } else { $api_key = $options['api_key'] ?? ''; $provider_name = 'OpenAI'; } if (empty($api_key)) { wp_send_json_error($provider_name . ' API key not configured'); exit; } $source_url = get_permalink($post_id); $vector_id = md5($source_url); // Vector ID for Pinecone // UPDATED: Check for existing content in bot-specific storage $is_update = false; // Get bot-specific Pinecone configuration $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id); $use_pinecone = !empty($bot_pinecone_config) && ($bot_pinecone_config['use_pinecone'] ?? false); if ($use_pinecone && !empty($bot_pinecone_config['api_key'])) { // Check Pinecone for this bot $pinecone_data = $this->mxchat_get_pinecone_processed_content($bot_pinecone_config); if (isset($pinecone_data[$post_id])) { $is_update = true; } } else { // Check WordPress DB (same as before since it's shared) global $wpdb; $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; $existing_record = $wpdb->get_row($wpdb->prepare( "SELECT id FROM $table_name WHERE source_url = %s", $source_url )); if ($existing_record) { $is_update = true; } } // UPDATED: Use the centralized utility function with bot_id $result = MxChat_Utils::submit_content_to_db( $content, $source_url, $api_key, $vector_id, $bot_id ); if (is_wp_error($result)) { wp_send_json_error('Storage failed: ' . $result->get_error_message()); exit; } $operation_type = $is_update ? 'update' : 'new'; // Count ACF fields for debugging $acf_field_count = count($acf_fields); // Success response with minimal data wp_send_json_success(array( 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully', 'post_id' => $post_id, 'title' => $post->post_title, 'operation_type' => $operation_type, 'vector_id' => $vector_id, 'acf_fields_found' => $acf_field_count, 'content_preview' => substr($content, 0, 100) . '...', 'bot_id' => $bot_id )); exit; } public function mxchat_get_public_post_types() { // Get all public post types $post_types = get_post_types(array('public' => true), 'objects'); $post_type_options = array(); foreach ($post_types as $post_type) { $post_type_options[$post_type->name] = $post_type->label; } // Also include common forum/community post types that might not be marked as public $additional_types = array( 'topic' => 'Forum Topics (bbPress)', 'reply' => 'Forum Replies (bbPress)', 'forum' => 'Forums (bbPress)', 'wpforo_topic' => 'wpForo Topics', 'wpforo_post' => 'wpForo Posts' ); foreach ($additional_types as $type_name => $type_label) { if (post_type_exists($type_name) && !isset($post_type_options[$type_name])) { $post_type_options[$type_name] = $type_label; } } return $post_type_options; } /** * Retrieves processed content from Pinecone API */ public function mxchat_get_pinecone_processed_content($pinecone_options) { $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; if (empty($api_key) || empty($host)) { return array(); } $pinecone_data = array(); try { // Always get fresh data from Pinecone $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options); // Method 2: Final fallback - try stats endpoint (if available) if (empty($pinecone_data)) { $stats_url = "https://{$host}/describe_index_stats"; $response = wp_remote_post($stats_url, array( 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ), 'body' => json_encode(array()), 'timeout' => 30 )); if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) { $body = wp_remote_retrieve_body($response); $stats_data = json_decode($body, true); } } } catch (Exception $e) { // Log error but return fresh data only } return $pinecone_data; } public function mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) { //error_log('=== DEBUG: Starting mxchat_fetch_pinecone_vectors_by_ids ==='); $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; if (empty($api_key) || empty($host) || empty($vector_ids)) { //error_log('DEBUG: Missing parameters for fetch by IDs'); return array(); } try { $fetch_url = "https://{$host}/vectors/fetch"; //error_log('DEBUG: Fetch URL: ' . $fetch_url); //error_log('DEBUG: Fetching ' . count($vector_ids) . ' vector IDs'); // Pinecone fetch API allows fetching specific vectors by ID $fetch_data = array( 'ids' => array_values($vector_ids) ); $response = wp_remote_post($fetch_url, array( 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ), 'body' => json_encode($fetch_data), 'timeout' => 30 )); if (is_wp_error($response)) { //error_log('DEBUG: Fetch by IDs WP error: ' . $response->get_error_message()); return array(); } $response_code = wp_remote_retrieve_response_code($response); //error_log('DEBUG: Fetch response code: ' . $response_code); if ($response_code !== 200) { $error_body = wp_remote_retrieve_body($response); //error_log('DEBUG: Fetch failed with body: ' . $error_body); return array(); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); //error_log('DEBUG: Fetch response structure: ' . print_r(array_keys($data), true)); if (!isset($data['vectors'])) { //error_log('DEBUG: No vectors key in response'); return array(); } $processed_data = array(); foreach ($data['vectors'] as $vector_id => $vector_data) { $metadata = $vector_data['metadata'] ?? array(); $source_url = $metadata['source_url'] ?? ''; if (!empty($source_url)) { $post_id = url_to_postid($source_url); if ($post_id) { $created_at = $metadata['created_at'] ?? ''; $processed_date = 'Recently'; if (!empty($created_at)) { $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at); if ($timestamp) { $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago'; } } $processed_data[$post_id] = array( 'db_id' => $vector_id, 'processed_date' => $processed_date, 'url' => $source_url, 'source' => 'pinecone', 'timestamp' => $timestamp ?? current_time('timestamp') ); } } } //error_log('DEBUG: Processed ' . count($processed_data) . ' vectors from fetch'); return $processed_data; } catch (Exception $e) { //error_log('DEBUG: Exception in fetch_pinecone_vectors_by_ids: ' . $e->getMessage()); return array(); } } /** * Scan Pinecone for processed content */ public function mxchat_scan_pinecone_for_processed_content($pinecone_options) { $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; if (empty($api_key) || empty($host)) { return array(); } try { // Use multiple random vectors to get better coverage $all_matches = array(); $seen_ids = array(); // Try 3 different random vectors to get better coverage for ($i = 0; $i < 3; $i++) { $query_url = "https://{$host}/query"; // Generate a random unit vector instead of zeros $random_vector = array(); for ($j = 0; $j < 1536; $j++) { $random_vector[] = (rand(-1000, 1000) / 1000.0); } // Normalize the vector to unit length $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector))); if ($magnitude > 0) { $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector); } $query_data = array( 'includeMetadata' => true, 'includeValues' => false, 'topK' => 10000, 'vector' => $random_vector ); $response = wp_remote_post($query_url, array( 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ), 'body' => json_encode($query_data), 'timeout' => 30 )); if (is_wp_error($response)) { continue; } $response_code = wp_remote_retrieve_response_code($response); if ($response_code !== 200) { continue; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (isset($data['matches'])) { foreach ($data['matches'] as $match) { $match_id = $match['id'] ?? ''; if (!empty($match_id) && !isset($seen_ids[$match_id])) { $all_matches[] = $match; $seen_ids[$match_id] = true; } } } } // Convert matches to processed data format $processed_data = array(); foreach ($all_matches as $match) { $metadata = $match['metadata'] ?? array(); $source_url = $metadata['source_url'] ?? ''; $match_id = $match['id'] ?? ''; if (!empty($source_url) && !empty($match_id)) { $post_id = url_to_postid($source_url); if ($post_id) { $created_at = $metadata['created_at'] ?? ''; $processed_date = 'Recently'; if (!empty($created_at)) { $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at); if ($timestamp) { $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago'; } } $processed_data[$post_id] = array( 'db_id' => $match_id, 'processed_date' => $processed_date, 'url' => $source_url, 'source' => 'pinecone', 'timestamp' => $timestamp ?? current_time('timestamp') ); } } } return $processed_data; } catch (Exception $e) { return array(); } } /** * UPDATED: Generate embeddings from input text for MXChat with bot support */ private function mxchat_generate_embedding($text, $bot_id = 'default') { // Enable detailed logging for debugging //error_log('[MXCHAT-EMBED] Starting embedding generation for bot: ' . $bot_id . '. Text length: ' . strlen($text) . ' bytes'); //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...'); // UPDATED: Get bot-specific options $bot_options = $this->get_bot_options($bot_id); $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options'); $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002'; //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model); // Determine provider and endpoint if (strpos($selected_model, 'voyage') === 0) { $api_key = $options['voyage_api_key'] ?? ''; $endpoint = 'https://api.voyageai.com/v1/embeddings'; $provider_name = 'Voyage AI'; //error_log('[MXCHAT-EMBED] Using Voyage AI API for bot ' . $bot_id); } elseif (strpos($selected_model, 'gemini-embedding') === 0) { $api_key = $options['gemini_api_key'] ?? ''; $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent'; $provider_name = 'Google Gemini'; //error_log('[MXCHAT-EMBED] Using Google Gemini API for bot ' . $bot_id); } else { $api_key = $options['api_key'] ?? ''; $endpoint = 'https://api.openai.com/v1/embeddings'; $provider_name = 'OpenAI'; //error_log('[MXCHAT-EMBED] Using OpenAI API for bot ' . $bot_id); } //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint); if (empty($api_key)) { $error_message = sprintf('Missing %s API key for bot %s. Please configure your API key in the bot settings.', $provider_name, $bot_id); //error_log('[MXCHAT-EMBED] Error: ' . $error_message); return $error_message; } // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75) $estimated_tokens = ceil(str_word_count($text) / 0.75); //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens); if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) { //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)'); // Consider truncating text here } // Prepare request body based on provider if (strpos($selected_model, 'gemini-embedding') === 0) { // Gemini API format $request_body = array( 'model' => 'models/' . $selected_model, 'content' => array( 'parts' => array( array('text' => $text) ) ) ); // Set output dimensionality to 1536 for consistency with other models $request_body['outputDimensionality'] = 1536; } else { // OpenAI/Voyage API format $request_body = array( 'model' => $selected_model, 'input' => $text ); // Add output_dimension for voyage-3-large model if ($selected_model === 'voyage-3-large') { $request_body['output_dimension'] = 2048; } } //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model); // Prepare headers based on provider if (strpos($selected_model, 'gemini-embedding') === 0) { // Gemini uses API key as query parameter $endpoint .= '?key=' . $api_key; $headers = array( 'Content-Type' => 'application/json' ); } else { // OpenAI/Voyage use Bearer token $headers = array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json' ); } // Make API request //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint); $response = wp_remote_post($endpoint, array( 'body' => wp_json_encode($request_body), 'headers' => $headers, 'timeout' => 60 // Increased timeout for large inputs )); // Handle wp_remote_post errors if (is_wp_error($response)) { $error_message = $response->get_error_message(); //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message); return 'Connection error: ' . $error_message; } // Get and check HTTP response code $http_code = wp_remote_retrieve_response_code($response); //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code); if ($http_code !== 200) { $error_body = wp_remote_retrieve_body($response); //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body); // Try to parse error for more details $error_json = json_decode($error_body, true); if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) { $error_type = $error_json['error']['type'] ?? 'unknown'; $error_message = $error_json['error']['message'] ?? 'No message'; //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type); //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message); // Customize error message for common API errors if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) { $error_message = sprintf('Invalid %s API key for bot %s. Please check your API key in the bot settings.', $provider_name, $bot_id); } elseif ($error_type === 'authentication_error') { $error_message = sprintf('%s authentication failed for bot %s. Please verify your API key in the bot settings.', $provider_name, $bot_id); } //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message); return $error_message; } $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding for bot %s", $http_code, $bot_id); //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message); return $error_message; } // Parse response body $response_body = wp_remote_retrieve_body($response); //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes'); $response_data = json_decode($response_body, true); if (json_last_error() !== JSON_ERROR_NONE) { $error = json_last_error_msg(); //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error); //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200)); return "Failed to parse API response: $error"; } // Handle different response formats based on provider if (strpos($selected_model, 'gemini-embedding') === 0) { // Gemini API response format if (isset($response_data['embedding']['values'])) { $embedding_dimensions = count($response_data['embedding']['values']); //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions'); // Check if embedding dimensions are as expected (should be 1536) if ($embedding_dimensions !== 1536) { //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions); } return $response_data['embedding']['values']; } else { //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response'); //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data))); if (isset($response_data['error'])) { $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']); //error_log('[MXCHAT-EMBED] ' . $error_message); return $error_message; } $error_message = "Invalid Gemini API response format: No embedding found"; //error_log('[MXCHAT-EMBED] ' . $error_message); return $error_message; } } else { // OpenAI/Voyage API response format if (isset($response_data['data'][0]['embedding'])) { $embedding_dimensions = count($response_data['data'][0]['embedding']); //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions'); // Check if embedding dimensions are as expected if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) || ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) { //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions'); } return $response_data['data'][0]['embedding']; } else { //error_log('[MXCHAT-EMBED] Error: No embedding found in response'); //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data))); if (isset($response_data['error'])) { $error_message = "API Error in response: " . wp_json_encode($response_data['error']); //error_log('[MXCHAT-EMBED] ' . $error_message); return $error_message; } $error_message = "Invalid API response format: No embedding found"; //error_log('[MXCHAT-EMBED] ' . $error_message); return $error_message; } } } /** * Get bot-specific options for multi-bot functionality * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active */ private function get_bot_options($bot_id = 'default') { //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id); if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) { //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')"); return array(); } $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id); if (!empty($bot_options)) { //error_log("MXCHAT DEBUG: Got bot-specific options from filter"); if (isset($bot_options['similarity_threshold'])) { //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']); } } return is_array($bot_options) ? $bot_options : array(); } /** * Get bot-specific Pinecone configuration * Used in the knowledge retrieval functions */ // Also add debugging to your get_bot_pinecone_config function private function get_bot_pinecone_config($bot_id = 'default') { //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id); // If default bot or multi-bot add-on not active, use default Pinecone config if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) { //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')"); $addon_options = get_option('mxchat_pinecone_addon_options', array()); $config = array( 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'), 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '', 'host' => $addon_options['mxchat_pinecone_host'] ?? '', 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? '' ); //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false')); return $config; } //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id); // Hook for multi-bot add-on to provide bot-specific Pinecone config $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id); if (!empty($bot_pinecone_config)) { //error_log("MXCHAT DEBUG: Got bot-specific config from filter"); //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set')); //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set')); //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set')); } else { //error_log("MXCHAT DEBUG: Filter returned empty config!"); } return is_array($bot_pinecone_config) ? $bot_pinecone_config : array(); } public function mxchat_ajax_dismiss_completed_status() { try { // Verify the request check_ajax_referer('mxchat_status_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized access'); exit; } $card_type = isset($_POST['card_type']) ? sanitize_text_field($_POST['card_type']) : ''; if ($card_type === 'pdf') { // Clear PDF status $pdf_url = get_transient('mxchat_last_pdf_url'); if ($pdf_url) { delete_transient('mxchat_pdf_status_' . md5($pdf_url)); delete_transient('mxchat_last_pdf_url'); } } elseif ($card_type === 'sitemap') { // Clear sitemap status $sitemap_url = get_transient('mxchat_last_sitemap_url'); if ($sitemap_url) { delete_transient('mxchat_sitemap_status_' . md5($sitemap_url)); delete_transient('mxchat_last_sitemap_url'); } } wp_send_json_success(array('message' => 'Status dismissed successfully')); } catch (Exception $e) { wp_send_json_error(array('message' => 'Error dismissing status: ' . $e->getMessage())); } } /** * Render completed status cards on page load * This ensures completed processing status persists through page refreshes */ public function mxchat_render_completed_status_cards() { $output = ''; // Check for completed PDF status $pdf_url = get_transient('mxchat_last_pdf_url'); if ($pdf_url) { $pdf_status = $this->mxchat_get_pdf_processing_status($pdf_url); if ($pdf_status && ($pdf_status['status'] === 'complete' || $pdf_status['status'] === 'error')) { $output .= $this->mxchat_render_pdf_status_card($pdf_status, $pdf_url); } } // Check for completed sitemap status $sitemap_url = get_transient('mxchat_last_sitemap_url'); if ($sitemap_url) { $sitemap_status = $this->mxchat_get_sitemap_processing_status($sitemap_url); if ($sitemap_status && ($sitemap_status['status'] === 'complete' || $sitemap_status['status'] === 'error')) { $output .= $this->mxchat_render_sitemap_status_card($sitemap_status, $sitemap_url); } } return $output; } /** * Render PDF status card HTML */ private function mxchat_render_pdf_status_card($status, $pdf_url) { $html = '
'; $html .= '
'; $html .= '

' . esc_html__('PDF Processing Status', 'mxchat') . '

'; // Add dismiss button for completed status if ($status['status'] === 'complete' || $status['status'] === 'error') { $html .= ''; } // Process Batch button for processing status if ($status['status'] === 'processing') { $html .= ''; } // Add status badges if ($status['status'] === 'error') { $html .= '' . esc_html__('Error', 'mxchat') . ''; } elseif ($status['status'] === 'complete') { if ($status['failed_pages'] > 0) { $html .= '' . sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_pages']) . ''; } else { $html .= '' . esc_html__('Complete', 'mxchat') . ''; } } $html .= '
'; // End header // Progress bar $html .= '
'; $html .= '
'; $html .= '
'; // Status details $html .= '
'; $html .= '

' . sprintf( esc_html__('Progress: %d of %d pages (%d%%)', 'mxchat'), $status['processed_pages'], $status['total_pages'], $status['percentage'] ) . '

'; // Show failed pages count if any if ($status['failed_pages'] > 0) { $html .= '

' . esc_html__('Failed pages:', 'mxchat') . ' ' . esc_html($status['failed_pages']) . '

'; } $html .= '

' . esc_html__('Status:', 'mxchat') . ' ' . esc_html(ucfirst($status['status'])) . '

'; $html .= '

' . esc_html__('Last update:', 'mxchat') . ' ' . esc_html($status['last_update']) . '

'; // Add completion summary if available AND it's an array if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) { $summary = $status['completion_summary']; $html .= '
'; $html .= '
' . esc_html__('Processing Summary', 'mxchat') . '
'; $html .= '

' . esc_html__('Total Pages:', 'mxchat') . ' ' . esc_html($summary['total_pages']) . '

'; $html .= '

' . esc_html__('Successful:', 'mxchat') . ' ' . esc_html($summary['successful_pages']) . '

'; $html .= '

' . esc_html__('Failed:', 'mxchat') . ' ' . esc_html($summary['failed_pages']) . '

'; $html .= '

' . esc_html__('Completed:', 'mxchat') . ' ' . esc_html($summary['completion_time']) . '

'; $html .= '
'; } // Add failed pages list if any AND it's an array if (isset($status['failed_pages_list']) && is_array($status['failed_pages_list']) && !empty($status['failed_pages_list'])) { $html .= $this->mxchat_render_failed_pages_list($status['failed_pages_list']); } // Add error message if any if (isset($status['error']) && !empty($status['error'])) { $html .= '
'; $html .= '

' . esc_html($status['error']) . '

'; $html .= '
'; } $html .= '
'; // End details $html .= '
'; // End card return $html; } /** * Render sitemap status card HTML */ private function mxchat_render_sitemap_status_card($status, $sitemap_url) { $html = '
'; $html .= '
'; $html .= '

' . esc_html__('Sitemap Processing Status', 'mxchat') . '

'; // Add dismiss button for completed status if ($status['status'] === 'complete' || $status['status'] === 'error') { $html .= ''; } // Process Batch button for processing status if ($status['status'] === 'processing') { $html .= ''; } // Add status badges if ($status['status'] === 'error') { $html .= '' . esc_html__('Error', 'mxchat') . ''; } elseif ($status['status'] === 'complete') { if ($status['failed_urls'] > 0) { $html .= '' . sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_urls']) . ''; } else { $html .= '' . esc_html__('Complete', 'mxchat') . ''; } } $html .= '
'; // End header // Progress bar $html .= '
'; $html .= '
'; $html .= '
'; // Status details $html .= '
'; $html .= '

' . sprintf( esc_html__('Progress: %d of %d URLs (%d%%)', 'mxchat'), $status['processed_urls'], $status['total_urls'], $status['percentage'] ) . '

'; // Show failed URLs count if any if ($status['failed_urls'] > 0) { $html .= '

' . esc_html__('Failed URLs:', 'mxchat') . ' ' . esc_html($status['failed_urls']) . '

'; } $html .= '

' . esc_html__('Status:', 'mxchat') . ' ' . esc_html(ucfirst($status['status'])) . '

'; $html .= '

' . esc_html__('Last update:', 'mxchat') . ' ' . esc_html($status['last_update']) . '

'; // Add completion summary if available AND it's an array if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) { $summary = $status['completion_summary']; $html .= '
'; $html .= '
' . esc_html__('Processing Summary', 'mxchat') . '
'; $html .= '

' . esc_html__('Total URLs:', 'mxchat') . ' ' . esc_html($summary['total_urls']) . '

'; $html .= '

' . esc_html__('Successful:', 'mxchat') . ' ' . esc_html($summary['successful_urls']) . '

'; $html .= '

' . esc_html__('Failed:', 'mxchat') . ' ' . esc_html($summary['failed_urls']) . '

'; $html .= '

' . esc_html__('Completed:', 'mxchat') . ' ' . esc_html($summary['completion_time']) . '

'; $html .= '
'; } // Add error messages if any (but not the failed URLs list) if (!empty($status['error']) || !empty($status['last_error'])) { $html .= '
'; if (!empty($status['error'])) { $html .= '

' . esc_html($status['error']) . '

'; } if (!empty($status['last_error'])) { $html .= '

' . esc_html__('Last error:', 'mxchat') . ' ' . esc_html($status['last_error']) . '

'; } $html .= '
'; } $html .= '
'; // End details $html .= '
'; // End card return $html; } /** * Render failed pages list */ private function mxchat_render_failed_pages_list($failed_pages_list) { // Validate that $failed_pages_list is an array and not empty if (!is_array($failed_pages_list) || empty($failed_pages_list)) { return ''; } $html = '
'; $html .= '
'; $html .= '
' . sprintf(esc_html__('Failed Pages (%d)', 'mxchat'), count($failed_pages_list)) . '
'; $html .= '
'; $html .= '' . esc_html__('Show Failed Pages', 'mxchat') . ''; $html .= '
'; // Create table for failed pages $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; // Sort failed pages by most recent $sorted_failed_pages = $failed_pages_list; usort($sorted_failed_pages, function($a, $b) { return ($b['time'] ?? 0) - ($a['time'] ?? 0); }); foreach ($sorted_failed_pages as $item) { // Ensure $item is an array before accessing its elements if (!is_array($item)) { continue; } $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown'; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; } $html .= '
' . esc_html__('Page', 'mxchat') . '' . esc_html__('Error', 'mxchat') . '' . esc_html__('Retries', 'mxchat') . '' . esc_html__('Time', 'mxchat') . '
' . esc_html__('Page', 'mxchat') . ' ' . esc_html($item['page'] ?? 'Unknown') . '' . esc_html($item['error'] ?? 'Unknown error') . '' . esc_html($item['retries'] ?? 'N/A') . '' . esc_html($time_ago) . '
'; $html .= '
'; return $html; } /** * Render failed URLs list */ private function mxchat_render_failed_urls_list($failed_urls_list) { // Validate that $failed_urls_list is an array and not empty if (!is_array($failed_urls_list) || empty($failed_urls_list)) { return ''; } $html = '
'; $html .= '
' . sprintf(esc_html__('Failed URLs (%d)', 'mxchat'), count($failed_urls_list)) . '
'; $html .= '
'; $html .= '' . esc_html__('Show Failed URLs', 'mxchat') . ''; $html .= '
'; // Create table for failed URLs $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; // Sort failed URLs by most recent $sorted_failed_urls = $failed_urls_list; usort($sorted_failed_urls, function($a, $b) { return ($b['time'] ?? 0) - ($a['time'] ?? 0); }); // Show up to 50 failed URLs $display_urls = array_slice($sorted_failed_urls, 0, 50); foreach ($display_urls as $item) { // Ensure $item is an array before accessing its elements if (!is_array($item)) { continue; } $url = $item['url'] ?? ''; $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown'; // Truncate URL for display $display_url = strlen($url) > 50 ? substr($url, 0, 47) . '...' : $url; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; } $html .= '
' . esc_html__('URL', 'mxchat') . '' . esc_html__('Error', 'mxchat') . '' . esc_html__('Retries', 'mxchat') . '' . esc_html__('Time', 'mxchat') . '
'; if (!empty($url)) { $html .= '' . esc_html($display_url) . ''; } else { $html .= esc_html__('Unknown URL', 'mxchat'); } $html .= '' . esc_html($item['error'] ?? 'Unknown error') . '' . esc_html($item['retries'] ?? 'N/A') . '' . esc_html($time_ago) . '
'; if (count($failed_urls_list) > 50) { $html .= '
+ ' . (count($failed_urls_list) - 50) . ' ' . esc_html__('more failed URLs not shown', 'mxchat') . '
'; } $html .= '
'; return $html; } /** * Get all ACF fields for a specific post */ public function mxchat_get_acf_fields_for_post($post_id) { if (!function_exists('get_fields')) { return array(); } $fields = get_fields($post_id); if (!$fields || !is_array($fields)) { return array(); } return $fields; } /** * Format ACF field values for content extraction */ public function mxchat_format_acf_field_value($value, $field_name = '', $post_id = 0) { if (empty($value)) { return ''; } // Handle WP_Post objects first (THIS IS THE KEY FIX) if ($value instanceof WP_Post) { return $value->post_title ?: ''; } // Handle other WP objects if (is_object($value)) { if (isset($value->post_title)) { return $value->post_title; } elseif (isset($value->display_name)) { return $value->display_name; } elseif (isset($value->name)) { return $value->name; } elseif (method_exists($value, '__toString')) { try { return (string) $value; } catch (Exception $e) { return ''; } } // For any other objects, return empty string return ''; } // Handle different ACF field types if (is_array($value)) { // Check if it's an image/file field if (isset($value['url'])) { // Image field - return alt text, title, or caption if (!empty($value['alt'])) { return $value['alt']; } elseif (!empty($value['title'])) { return $value['title']; } elseif (!empty($value['caption'])) { return $value['caption']; } else { return ''; // Don't include just the URL } } // Check if it's a post object or relationship field if (isset($value['post_title'])) { return $value['post_title']; } // Check if it's a user field if (isset($value['display_name'])) { return $value['display_name']; } // Check if it's a taxonomy term if (isset($value['name']) && isset($value['taxonomy'])) { return $value['name']; } // Check if it's a select field with label if (isset($value['label'])) { return $value['label']; } // Check for repeater field or flexible content if (is_numeric(key($value))) { $sub_values = array(); foreach ($value as $sub_item) { if (is_array($sub_item)) { // For repeater/flexible content, extract text values $sub_text = $this->mxchat_extract_text_from_acf_array($sub_item); if (!empty($sub_text)) { $sub_values[] = $sub_text; } } elseif ($sub_item instanceof WP_Post) { // Handle WP_Post objects in arrays $sub_values[] = $sub_item->post_title ?: ''; } else { $sub_values[] = (string) $sub_item; } } return implode(', ', array_filter($sub_values)); } // For other arrays, try to extract meaningful text $text_values = array(); foreach ($value as $key => $val) { if (is_string($val) && !empty(trim($val))) { $text_values[] = trim($val); } elseif ($val instanceof WP_Post) { // Handle WP_Post objects in associative arrays $text_values[] = $val->post_title ?: ''; } elseif (is_array($val) && isset($val['post_title'])) { $text_values[] = $val['post_title']; } elseif (is_array($val) && isset($val['name'])) { $text_values[] = $val['name']; } } return implode(', ', array_filter($text_values)); } // Handle boolean values if (is_bool($value)) { return $value ? 'Yes' : 'No'; } // Handle numeric values if (is_numeric($value)) { return (string) $value; } // Handle string values if (is_string($value)) { return trim($value); } // For anything else that we can't handle, return empty string // This prevents the "Object could not be converted to string" error return ''; } /** * Extract text from complex ACF array structures */ private function mxchat_extract_text_from_acf_array($array) { if (!is_array($array)) { return ''; } $text_parts = array(); foreach ($array as $key => $value) { if (is_string($value) && !empty(trim($value))) { // Skip keys that are likely to be IDs or technical values if (!is_numeric($value) || strlen($value) > 10) { $text_parts[] = trim($value); } } elseif ($value instanceof WP_Post) { // Handle WP_Post objects $text_parts[] = $value->post_title ?: ''; } elseif (is_array($value)) { if (isset($value['post_title'])) { $text_parts[] = $value['post_title']; } elseif (isset($value['name'])) { $text_parts[] = $value['name']; } elseif (isset($value['label'])) { $text_parts[] = $value['label']; } } elseif (is_object($value)) { // Handle other objects safely if (isset($value->post_title)) { $text_parts[] = $value->post_title; } elseif (isset($value->name)) { $text_parts[] = $value->name; } elseif (isset($value->display_name)) { $text_parts[] = $value->display_name; } } } return implode(', ', array_filter($text_parts)); } public function mxchat_handle_post_update($post_id, $post, $update) { // Basic validation checks if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) { return; } $post_type = $post->post_type; // Check if sync is enabled for this post type $should_sync = false; // Check built-in post types first if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') { $should_sync = true; } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') { $should_sync = true; } else { // Check custom post types $option_name = 'mxchat_auto_sync_' . $post_type; if (get_option($option_name) === '1') { $should_sync = true; } } if (!$should_sync) { return; } // Check if we have stored the previous status and URL in our transients $previous_status_key = 'mxchat_prev_status_' . $post_id; $previous_status = get_transient($previous_status_key); $previous_url_key = 'mxchat_prev_url_' . $post_id; $previous_url = get_transient($previous_url_key); // If the post was previously published but is now not published, remove from knowledge base if ($previous_status === 'publish' && $post->post_status !== 'publish') { // Use the stored URL from when it was published, or fall back to current permalink $source_url = $previous_url ?: get_permalink($post_id); if ($source_url) { // Check if Pinecone is enabled $pinecone_options = get_option('mxchat_pinecone_addon_options', array()); $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1'; if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) { // Delete from Pinecone $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options); } else { // Delete from WordPress DB global $wpdb; $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; $result = $wpdb->delete( $table_name, array('source_url' => $source_url), array('%s') ); } } // Clean up the transients and exit early delete_transient($previous_status_key); delete_transient($previous_url_key); return; } // Store the current status for next time (if this is an update) if ($update) { set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS); // If the post is currently published, also store its URL if ($post->post_status === 'publish') { $current_url = get_permalink($post_id); set_transient($previous_url_key, $current_url, DAY_IN_SECONDS); } } // Only process currently published content for adding/updating if ($post->post_status === 'publish') { // Get the source URL $source_url = get_permalink($post_id); // Get content with proper formatting (matching ajax_mxchat_process_selected_content) $title = get_the_title($post_id); $content = get_post_field('post_content', $post_id); // Apply WordPress content filters to get properly formatted content $content = apply_filters('the_content', $content); // Strip tags but preserve structure $content = wp_strip_all_tags($content); // Combine title and content $final_content = $title . "\n\n" . $content; // For custom post types like job_listing, include additional fields if ($post_type === 'job_listing') { // Add job-specific meta if available $job_location = get_post_meta($post_id, '_job_location', true); if (!empty($job_location)) { $final_content .= "\n\nLocation: " . $job_location; } // Get job type terms $job_types = get_the_terms($post_id, 'job_listing_type'); if (!empty($job_types) && !is_wp_error($job_types)) { $types = array(); foreach ($job_types as $type) { $types[] = $type->name; } $final_content .= "\n\nJob Type: " . implode(', ', $types); } // Get company name if available $company_name = get_post_meta($post_id, '_company_name', true); if (!empty($company_name)) { $final_content .= "\n\nCompany: " . $company_name; } } // Get API key with proper model detection $options = get_option('mxchat_options'); $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002'; if (strpos($selected_model, 'voyage') === 0) { $api_key = $options['voyage_api_key'] ?? ''; } elseif (strpos($selected_model, 'gemini-embedding') === 0) { $api_key = $options['gemini_api_key'] ?? ''; } else { $api_key = $options['api_key'] ?? ''; } if (empty($api_key)) { return; } // Use the centralized utility function for storage $result = MxChat_Utils::submit_content_to_db( $final_content, $source_url, $api_key, md5($source_url) // Vector ID for Pinecone ); } // Clean up the stored previous status if not used above if ($previous_status !== 'publish' || $post->post_status === 'publish') { delete_transient($previous_status_key); delete_transient($previous_url_key); } } /** * Store the post status and URL before update to detect status transitions * This runs before the post is actually updated in the database */ public function mxchat_store_pre_update_status($post_id, $data) { // Get the current post from database (before update) $current_post = get_post($post_id); if ($current_post) { // Store the current status temporarily $status_key = 'mxchat_prev_status_' . $post_id; set_transient($status_key, $current_post->post_status, HOUR_IN_SECONDS); // If the post is currently published, also store its URL if ($current_post->post_status === 'publish') { $url_key = 'mxchat_prev_url_' . $post_id; $current_url = get_permalink($post_id); set_transient($url_key, $current_url, HOUR_IN_SECONDS); } } } public function mxchat_handle_post_delete($post_id) { // Get post data before it's deleted $post = get_post($post_id); // Basic validation if (!$post || wp_is_post_revision($post_id)) { return; } $post_type = $post->post_type; // Check if sync is enabled for this post type $should_sync = false; // Check built-in post types first if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') { $should_sync = true; } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') { $should_sync = true; } else { // Check custom post types $option_name = 'mxchat_auto_sync_' . $post_type; if (get_option($option_name) === '1') { $should_sync = true; } } if (!$should_sync) { return; } // Get the URL before post is deleted $source_url = get_permalink($post_id); if (!$source_url) { //error_log('MXChat: Failed to get permalink for post ' . $post_id); return; } // Check if Pinecone is enabled $pinecone_options = get_option('mxchat_pinecone_addon_options', array()); $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1'; if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) { // Delete from Pinecone $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options); } else { // Delete from WordPress DB global $wpdb; $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; $result = $wpdb->delete( $table_name, array('source_url' => $source_url), array('%s') ); if ($result === false) { //error_log('MXChat: WordPress DB deletion failed for URL: ' . $source_url); } } } /** * Deletes data from Pinecone using a source URL */ public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) { $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; if (empty($host) || empty($api_key)) { //error_log('MXChat: Pinecone deletion failed - missing configuration'); return false; } $api_endpoint = "https://{$host}/vectors/delete"; $vector_id = md5($source_url); $request_body = array( 'ids' => array($vector_id) ); $response = wp_remote_post($api_endpoint, array( 'headers' => array( 'Api-Key' => $api_key, 'accept' => 'application/json', 'content-type' => 'application/json' ), 'body' => wp_json_encode($request_body), 'timeout' => 30 )); if (is_wp_error($response)) { //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message()); return false; } $response_code = wp_remote_retrieve_response_code($response); if ($response_code !== 200) { //error_log('MXChat: Pinecone deletion failed with status ' . $response_code); return false; } return true; } public function mxchat_handle_product_change($post_id, $post, $update) { if ($post->post_type !== 'product') { return; } if ($post->post_status === 'publish') { add_action('shutdown', function() use ($post_id) { $product = wc_get_product($post_id); if ($product) { $this->mxchat_store_product_embedding($product); } }); } } /** * Store WooCommerce product embeddings */ private function mxchat_store_product_embedding($product) { if (!isset($this->options['enable_woocommerce_integration']) || !in_array($this->options['enable_woocommerce_integration'], ['1', 'on'])) { return; } $source_url = get_permalink($product->get_id()); // Build product content $title = $product->get_name(); $description = $product->get_description(); $short_description = $product->get_short_description(); $regular_price = $product->get_regular_price(); $sale_price = $product->get_sale_price(); $sku = $product->get_sku(); // Format content consistently $content = $title . "\n\n"; if (!empty($description)) { $content .= wp_strip_all_tags($description) . "\n\n"; } if (!empty($short_description)) { $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n"; } $content .= "Price: $" . $regular_price . "\n"; if (!empty($sale_price)) { $content .= "Sale Price: $" . $sale_price . "\n"; } if (!empty($sku)) { $content .= "SKU: " . $sku . "\n"; } // Get API key with proper model detection $options = get_option('mxchat_options'); $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002'; if (strpos($selected_model, 'voyage') === 0) { $api_key = $options['voyage_api_key'] ?? ''; } elseif (strpos($selected_model, 'gemini-embedding') === 0) { $api_key = $options['gemini_api_key'] ?? ''; } else { $api_key = $options['api_key'] ?? ''; } if (empty($api_key)) { //error_log('MxChat Auto-sync: No API key configured for embedding model'); return; } // Use the centralized utility function for storage $result = MxChat_Utils::submit_content_to_db( $content, $source_url, $api_key, md5($source_url) // Vector ID for Pinecone ); if (is_wp_error($result)) { //error_log('MxChat WooCommerce sync failed for product ' . $product->get_id() . ': ' . $result->get_error_message()); } } public function mxchat_handle_product_delete($post_id) { if (get_post_type($post_id) !== 'product') { return; } $source_url = get_permalink($post_id); // Check if Pinecone is enabled $pinecone_options = get_option('mxchat_pinecone_addon_options', array()); $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1'; if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) { // Delete from Pinecone $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options); } else { // Delete from WordPress DB global $wpdb; $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; $wpdb->delete( $table_name, array('source_url' => $source_url), array('%s') ); } } /** * Handle individual Pinecone content deletion */ public function mxchat_handle_pinecone_prompt_delete() { // Check permissions if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat')); } // Verify nonce if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'mxchat_delete_pinecone_prompt_nonce')) { wp_die(esc_html__('Security check failed.', 'mxchat')); } $vector_id = isset($_GET['vector_id']) ? sanitize_text_field($_GET['vector_id']) : ''; if (empty($vector_id)) { set_transient('mxchat_admin_notice_error', esc_html__('Invalid vector ID.', 'mxchat'), 30 ); wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts')); exit; } // Get Pinecone settings $pinecone_options = get_option('mxchat_pinecone_addon_options', array()); $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1'; if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) { set_transient('mxchat_admin_notice_error', esc_html__('Pinecone is not properly configured.', 'mxchat'), 30 ); wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts')); exit; } // Delete from Pinecone $pinecone_manager = MxChat_Pinecone_Manager::get_instance(); $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id( $vector_id, $pinecone_options['mxchat_pinecone_api_key'], $pinecone_options['mxchat_pinecone_host'] ); if ($result['success']) { // No cache clearing needed since we removed caching set_transient('mxchat_admin_notice_success', esc_html__('Entry deleted successfully from Pinecone.', 'mxchat'), 30 ); } else { set_transient('mxchat_admin_notice_error', esc_html__('Failed to delete entry: ', 'mxchat') . $result['message'], 30 ); } wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts')); exit; } /** * Handle individual Pinecone content deletion via AJAX */ public function ajax_mxchat_delete_pinecone_prompt() { // Verify nonce and permissions if (!check_ajax_referer('mxchat_delete_pinecone_prompt_nonce', 'nonce', false)) { wp_send_json_error('Invalid nonce'); exit; } if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized access'); exit; } $vector_id = isset($_POST['vector_id']) ? sanitize_text_field($_POST['vector_id']) : ''; $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default'; if (empty($vector_id)) { wp_send_json_error('Missing vector ID'); exit; } // Get bot-specific Pinecone settings $pinecone_manager = MxChat_Pinecone_Manager::get_instance(); $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id); $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1'; if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) { wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id); exit; } // Delete from the correct Pinecone index $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id( $vector_id, $pinecone_options['mxchat_pinecone_api_key'], $pinecone_options['mxchat_pinecone_host'] ); if ($result['success']) { // No cache clearing needed since we removed caching wp_send_json_success(array( 'message' => 'Entry deleted successfully from Pinecone', 'vector_id' => $vector_id, 'bot_id' => $bot_id )); } else { wp_send_json_error('Failed to delete from Pinecone: ' . $result['message']); } exit; } /** * NEW: Get hierarchical roles for dropdown */ public function mxchat_get_role_options() { return array( 'public' => __('Public (Everyone)', 'mxchat'), 'logged_in' => __('Logged In Users', 'mxchat'), 'subscriber' => __('Subscribers & Above', 'mxchat'), 'contributor' => __('Contributors & Above', 'mxchat'), 'author' => __('Authors & Above', 'mxchat'), 'editor' => __('Editors & Above', 'mxchat'), 'administrator' => __('Administrators Only', 'mxchat') ); } /** * NEW: Check if user has access to content based on role restriction */ public function mxchat_user_has_content_access($role_restriction) { // Public content is always accessible if ($role_restriction === 'public' || empty($role_restriction)) { return true; } // Check if user is logged in for logged_in restriction if ($role_restriction === 'logged_in') { return is_user_logged_in(); } // If not logged in, no access to role-restricted content if (!is_user_logged_in()) { return false; } $user = wp_get_current_user(); $user_roles = $user->roles; if (empty($user_roles)) { return false; } // Define role hierarchy (higher number = higher access) $hierarchy = array( 'subscriber' => 1, 'contributor' => 2, 'author' => 3, 'editor' => 4, 'administrator' => 5 ); // Get required level $required_level = isset($hierarchy[$role_restriction]) ? $hierarchy[$role_restriction] : 0; // Check if user has required level or higher foreach ($user_roles as $user_role) { $user_level = isset($hierarchy[$user_role]) ? $hierarchy[$user_role] : 0; if ($user_level >= $required_level) { return true; } } return false; } /** * Handle role restriction updates via AJAX * UPDATED: Removed cache clearing call since we removed caching */ public function ajax_mxchat_update_role_restriction() { // Verify nonce and permissions if (!check_ajax_referer('mxchat_update_role_nonce', 'nonce', false)) { wp_send_json_error('Invalid nonce'); exit; } if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized access'); exit; } $entry_id = isset($_POST['entry_id']) ? sanitize_text_field($_POST['entry_id']) : ''; $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public'; $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress'; if (empty($entry_id)) { wp_send_json_error('Invalid entry ID'); exit; } // Get knowledge manager instance to validate role restriction $knowledge_manager = MxChat_Knowledge_Manager::get_instance(); $valid_roles = array_keys($knowledge_manager->mxchat_get_role_options()); if (!in_array($role_restriction, $valid_roles)) { wp_send_json_error('Invalid role restriction'); exit; } global $wpdb; if ($data_source === 'pinecone') { // Handle Pinecone role restriction (stored separately in WordPress table) $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles'; // Use REPLACE to insert or update the role restriction $result = $wpdb->replace( $roles_table, array( 'vector_id' => $entry_id, 'role_restriction' => $role_restriction, 'updated_at' => current_time('mysql') ), array('%s', '%s', '%s') ); // No cache clearing needed since we removed caching } else { // Handle WordPress database role restriction (existing functionality) $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; $result = $wpdb->update( $table_name, array('role_restriction' => $role_restriction), array('id' => absint($entry_id)), array('%s'), array('%d') ); } if ($result === false) { wp_send_json_error('Database update failed: ' . $wpdb->last_error); exit; } wp_send_json_success(array( 'message' => 'Role restriction updated successfully', 'role_restriction' => $role_restriction, 'data_source' => $data_source, 'entry_id' => $entry_id )); exit; } // ======================================== // HELPER METHODS // ======================================== /** * Check if user has required permissions for content processing */ private function mxchat_check_user_permissions() { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat')); } } /** * Validate nonce for security */ private function mxchat_validate_nonce($nonce_name, $nonce_action) { if (!isset($_POST[$nonce_name]) || !wp_verify_nonce($_POST[$nonce_name], $nonce_action)) { wp_die(esc_html__('Security check failed.', 'mxchat')); } } /** * Get embedding API credentials */ private function mxchat_get_embedding_credentials() { $embedding_model = $this->options['embedding_model'] ?? 'text-embedding-ada-002'; if (strpos($embedding_model, 'text-embedding-') !== false) { return array( 'type' => 'openai', 'api_key' => $this->options['api_key'] ?? '' ); } elseif (strpos($embedding_model, 'voyage-') !== false) { return array( 'type' => 'voyage', 'api_key' => $this->options['voyage_api_key'] ?? '' ); } elseif (strpos($embedding_model, 'gemini-embedding-') !== false) { return array( 'type' => 'gemini', 'api_key' => $this->options['gemini_api_key'] ?? '' ); } return array('type' => 'unknown', 'api_key' => ''); } /** * Log processing errors */ private function mxchat_log_processing_error($operation, $error_message) { //error_log("MxChat Knowledge Processing {$operation} Error: " . $error_message); } /** * Set admin notice transient */ private function mxchat_set_admin_notice($type, $message) { set_transient("mxchat_admin_notice_{$type}", $message, 30); } /** * Get Pinecone manager instance for vector operations */ private function mxchat_get_pinecone_manager() { return MxChat_Pinecone_Manager::get_instance(); } // ======================================== // STATIC ACCESS METHODS // ======================================== /** * Get singleton instance */ public static function get_instance() { static $instance = null; if ($instance === null) { $instance = new self(); } return $instance; } } // Initialize the Knowledge manager $mxchat_knowledge_manager = MxChat_Knowledge_Manager::get_instance();