PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.4.1
MxChat – AI Chatbot & Content Generation for WordPress v2.4.1
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / admin / class-knowledge-manager.php

class-knowledge-manager.php in MxChat – AI Chatbot & Content Generation for WordPress 2.4.1, at admin/class-knowledge-manager.php

4,381 lines 170.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * File: admin/class-knowledge-manager.php
4 *
5 * Handles all knowledge base content processing for MxChat
6 * Including PDF, sitemap, content processing, and WordPress post management
7 */
8 if (!defined('ABSPATH')) {
9 exit; // Exit if accessed directly
10 }
11
12 class MxChat_Knowledge_Manager {
13
14 private $options;
15
16 /**
17 * Constructor - Register hooks for content processing
18 */
19 public function __construct() {
20 $this->options = get_option('mxchat_options', array());
21 $this->mxchat_init_hooks();
22 }
23
24 /**
25 * Initialize WordPress hooks for content processing
26 */
27 private function mxchat_init_hooks() {
28 // Admin post handlers for form submissions
29 add_action('admin_post_mxchat_submit_content', array($this, 'mxchat_handle_content_submission'));
30 add_action('admin_post_mxchat_submit_sitemap', array($this, 'mxchat_handle_sitemap_submission'));
31 add_action('admin_post_mxchat_stop_processing', array($this, 'mxchat_stop_processing'));
32
33 // AJAX handlers for real-time processing and status updates
34 add_action('wp_ajax_mxchat_get_status_updates', array($this, 'mxchat_ajax_get_status_updates'));
35 add_action('wp_ajax_mxchat_dismiss_completed_status', array($this, 'mxchat_ajax_dismiss_completed_status'));
36 add_action('wp_ajax_mxchat_get_content_list', array($this, 'ajax_mxchat_get_content_list'));
37 add_action('wp_ajax_mxchat_process_selected_content', array($this, 'ajax_mxchat_process_selected_content'));
38 add_action('wp_ajax_mxchat_manual_batch_process', array($this, 'ajax_manual_batch_process'));
39 add_action('mxchat_delete_content', array($this, 'mxchat_delete_from_pinecone_by_url'), 10, 1);
40
41
42 // Cron handlers for background processing
43 add_action('mxchat_process_sitemap_urls', array($this, 'mxchat_process_sitemap_urls_cron'), 10, 5);
44 add_action('mxchat_process_pdf_pages', array($this, 'mxchat_process_pdf_pages_cron'), 10, 5);
45
46 // WordPress post management hooks - UPDATED FOR BETTER STATUS TRACKING
47 add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
48 add_action('post_updated', array($this, 'mxchat_handle_post_update'), 10, 3);
49 add_action('before_delete_post', array($this, 'mxchat_handle_post_delete'));
50 add_action('wp_trash_post', array($this, 'mxchat_handle_post_delete'));
51 add_action('wp_ajax_mxchat_save_inline_prompt', array($this, 'mxchat_save_inline_prompt'));
52 add_action('admin_post_mxchat_delete_pinecone_prompt', array($this, 'mxchat_handle_pinecone_prompt_delete'));
53 add_action('wp_ajax_mxchat_delete_pinecone_prompt', array($this, 'ajax_mxchat_delete_pinecone_prompt'));
54 add_action('wp_ajax_mxchat_update_role_restriction', array($this, 'ajax_mxchat_update_role_restriction'));
55
56 // WooCommerce product hooks (if WooCommerce is active)
57 if (class_exists('WooCommerce')) {
58 add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2); // Same hook for products
59 add_action('save_post_product', array($this, 'mxchat_handle_product_change'), 10, 3);
60 add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
61 add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
62 }
63
64 }
65
66 /**
67 * Get current options (refreshed)
68 */
69 private function mxchat_get_options() {
70 if (empty($this->options)) {
71 $this->options = get_option('mxchat_options', array());
72 }
73 return $this->options;
74 }
75
76
77 /**
78 * Handle manual batch processing via AJAX
79 */
80 public function ajax_manual_batch_process() {
81 try {
82 // Verify nonce and permissions
83 check_ajax_referer('mxchat_status_nonce', 'nonce');
84
85 if (!current_user_can('manage_options')) {
86 wp_send_json_error('Unauthorized access');
87 }
88
89 $process_type = sanitize_text_field($_POST['process_type'] ?? '');
90 $url = sanitize_text_field($_POST['url'] ?? '');
91
92 if (empty($process_type) || empty($url)) {
93 wp_send_json_error('Missing required parameters');
94 }
95
96 $processed = 0;
97
98 if ($process_type === 'pdf') {
99 $processed = $this->mxchat_manual_process_pdf_batch($url);
100 } elseif ($process_type === 'sitemap') {
101 $processed = $this->mxchat_manual_process_sitemap_batch($url);
102 }
103
104 if ($processed > 0) {
105 wp_send_json_success(array(
106 'message' => "Processed {$processed} items successfully",
107 'processed' => $processed
108 ));
109 } else {
110 wp_send_json_error('No items were processed');
111 }
112
113 } catch (Exception $e) {
114 //error_log('Manual batch process error: ' . $e->getMessage());
115 wp_send_json_error('Processing failed: ' . $e->getMessage());
116 }
117 }
118
119 /**
120 * Process a small PDF batch manually - DIRECT PROCESSING
121 */
122 private function mxchat_manual_process_pdf_batch($pdf_url) {
123 try {
124 $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
125 $status = get_transient($status_key);
126
127 if (!$status || $status['status'] !== 'processing') {
128 //error_log('Manual PDF: No processing status found');
129 return 0;
130 }
131
132 //error_log('Manual PDF: Starting direct processing for ' . $pdf_url);
133
134 // Get current progress
135 $current_page = $status['processed_pages'] ?? 0;
136 $total_pages = $status['total_pages'] ?? 0;
137
138 if ($current_page >= $total_pages) {
139 //error_log('Manual PDF: Already completed');
140 return 0;
141 }
142
143 // Try to download the PDF again for processing
144 $response = wp_remote_get($pdf_url, array('timeout' => 30));
145
146 if (is_wp_error($response)) {
147 //error_log('Manual PDF: Failed to download PDF: ' . $response->get_error_message());
148 return 0;
149 }
150
151 $pdf_content = wp_remote_retrieve_body($response);
152 if (empty($pdf_content)) {
153 //error_log('Manual PDF: Empty PDF content');
154 return 0;
155 }
156
157 // Save PDF temporarily
158 $upload_dir = wp_upload_dir();
159 $temp_pdf_path = trailingslashit($upload_dir['path']) . 'temp_manual_' . time() . '.pdf';
160 file_put_contents($temp_pdf_path, $pdf_content);
161
162 // Process 2 pages directly
163 $processed = $this->mxchat_process_pdf_pages_direct($temp_pdf_path, $pdf_url, $current_page, 5);
164
165 // Clean up temp file
166 if (file_exists($temp_pdf_path)) {
167 wp_delete_file($temp_pdf_path);
168 }
169
170 //error_log('Manual PDF: Processed ' . $processed . ' pages');
171 return $processed;
172
173 } catch (Exception $e) {
174 //error_log('Manual PDF batch error: ' . $e->getMessage());
175 return 0;
176 }
177 }
178
179 /**
180 * Process PDF pages directly without cron
181 */
182 private function mxchat_process_pdf_pages_direct($pdf_path, $pdf_url, $start_page, $batch_size) {
183 try {
184 if (!file_exists($pdf_path)) {
185 //error_log('Direct PDF: File not found at ' . $pdf_path);
186 return 0;
187 }
188
189 $parser = new \Smalot\PdfParser\Parser();
190 $pdf = $parser->parseFile($pdf_path);
191 $pages = $pdf->getPages();
192
193 $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
194 $status = get_transient($status_key);
195
196 if (!$status) {
197 return 0;
198 }
199
200 $options = get_option('mxchat_options');
201 $api_key = $options['api_key'] ?? '';
202
203 if (empty($api_key)) {
204 //error_log('Direct PDF: No API key');
205 return 0;
206 }
207
208 $processed = 0;
209 $end_page = min($start_page + $batch_size, count($pages));
210
211 for ($i = $start_page; $i < $end_page; $i++) {
212 try {
213 $page_number = $i + 1;
214 $text = $pages[$i]->getText();
215
216 if (empty($text)) {
217 //error_log('Direct PDF: Empty text on page ' . $page_number);
218 continue;
219 }
220
221 $sanitized_content = $this->mxchat_sanitize_content_for_api($text);
222 if (empty($sanitized_content)) {
223 //error_log('Direct PDF: No content after sanitization on page ' . $page_number);
224 continue;
225 }
226
227 // Generate embedding
228 $embedding_vector = $this->mxchat_generate_embedding($sanitized_content);
229 if (is_string($embedding_vector)) {
230 //error_log('Direct PDF: Embedding failed on page ' . $page_number . ': ' . $embedding_vector);
231 continue;
232 }
233
234 // Create metadata
235 $metadata = array(
236 'document_type' => 'pdf',
237 'total_pages' => count($pages),
238 'current_page' => $page_number,
239 'source_url' => $pdf_url
240 );
241
242 $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized_content;
243 $page_url = esc_url($pdf_url . "#page=" . $page_number);
244
245 // Store in database
246 $db_result = MxChat_Utils::submit_content_to_db($content_with_metadata, $page_url, $api_key);
247
248 if (is_wp_error($db_result)) {
249 //error_log('Direct PDF: DB error on page ' . $page_number . ': ' . $db_result->get_error_message());
250 continue;
251 }
252
253 $processed++;
254 //error_log('Direct PDF: Successfully processed page ' . $page_number);
255
256 // Update status
257 $status['processed_pages'] = $i + 1;
258 $status['last_update'] = time();
259 $status['percentage'] = round(($status['processed_pages'] / $status['total_pages']) * 100);
260 set_transient($status_key, $status, DAY_IN_SECONDS);
261
262 } catch (Exception $e) {
263 //error_log('Direct PDF: Error processing page ' . ($i + 1) . ': ' . $e->getMessage());
264 continue;
265 }
266 }
267
268 // Check if completed
269 if ($status['processed_pages'] >= $status['total_pages']) {
270 $status['status'] = 'complete';
271 set_transient($status_key, $status, DAY_IN_SECONDS);
272 //error_log('Direct PDF: Processing completed');
273 }
274
275 return $processed;
276
277 } catch (Exception $e) {
278 //error_log('Direct PDF processing error: ' . $e->getMessage());
279 return 0;
280 }
281 }
282
283 /**
284 * Process a small sitemap batch manually - DIRECT PROCESSING
285 */
286 private function mxchat_manual_process_sitemap_batch($sitemap_url) {
287 try {
288 $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
289 $status = get_transient($status_key);
290
291 if (!$status || $status['status'] !== 'processing') {
292 return 0;
293 }
294
295 //error_log('Manual Sitemap: Starting direct processing for ' . $sitemap_url);
296
297 // Re-fetch the sitemap to get URLs
298 $response = wp_remote_get($sitemap_url, array('timeout' => 30));
299 if (is_wp_error($response)) {
300 //error_log('Manual Sitemap: Failed to fetch sitemap');
301 return 0;
302 }
303
304 $sitemap_content = wp_remote_retrieve_body($response);
305 $xml = simplexml_load_string($sitemap_content);
306
307 if (!$xml) {
308 //error_log('Manual Sitemap: Invalid XML');
309 return 0;
310 }
311
312 $urls = array();
313 foreach ($xml->url as $url_element) {
314 $urls[] = (string)$url_element->loc;
315 }
316
317 $current_processed = $status['processed_urls'] ?? 0;
318 $batch_size = 50;
319 $processed = 0;
320
321 // Process next 2 URLs
322 for ($i = $current_processed; $i < min($current_processed + $batch_size, count($urls)); $i++) {
323 $url = $urls[$i];
324
325 if ($this->mxchat_process_single_url_direct($url)) {
326 $processed++;
327 }
328
329 // Update status
330 $status['processed_urls'] = $i + 1;
331 $status['last_update'] = time();
332 $status['percentage'] = round(($status['processed_urls'] / $status['total_urls']) * 100);
333 set_transient($status_key, $status, DAY_IN_SECONDS);
334 }
335
336 // Check if completed
337 if ($status['processed_urls'] >= $status['total_urls']) {
338 $status['status'] = 'complete';
339 set_transient($status_key, $status, DAY_IN_SECONDS);
340 }
341
342 //error_log('Manual Sitemap: Processed ' . $processed . ' URLs');
343 return $processed;
344
345 } catch (Exception $e) {
346 //error_log('Manual sitemap batch error: ' . $e->getMessage());
347 return 0;
348 }
349 }
350
351 /**
352 * Process a single URL directly
353 */
354 private function mxchat_process_single_url_direct($url) {
355 try {
356 $response = wp_remote_get($url, array('timeout' => 30));
357 if (is_wp_error($response)) {
358 return false;
359 }
360
361 $html = wp_remote_retrieve_body($response);
362 $content = $this->mxchat_extract_main_content($html);
363 $sanitized = $this->mxchat_sanitize_content_for_api($content);
364
365 if (empty($sanitized)) {
366 return false;
367 }
368
369 $options = get_option('mxchat_options');
370 $api_key = $options['api_key'] ?? '';
371
372 $result = MxChat_Utils::submit_content_to_db($sanitized, $url, $api_key);
373
374 return !is_wp_error($result);
375
376 } catch (Exception $e) {
377 //error_log('Single URL processing error: ' . $e->getMessage());
378 return false;
379 }
380 }
381
382
383 // ========================================
384 // MAIN CONTENT SUBMISSION HANDLERS
385 // ========================================
386
387 public function mxchat_handle_content_submission() {
388 // Check if the form was submitted and the user has permission.
389 if (!isset($_POST['submit_content']) || !current_user_can('manage_options')) {
390 return;
391 }
392
393 // Verify the nonce.
394 $nonce = isset($_POST['mxchat_submit_content_nonce']) ? sanitize_text_field(wp_unslash($_POST['mxchat_submit_content_nonce'])) : '';
395 if (!wp_verify_nonce($nonce, 'mxchat_submit_content_action')) {
396 wp_die(esc_html__('Nonce verification failed.', 'mxchat'));
397 }
398
399 // Sanitize the inputs.
400 $article_content = sanitize_textarea_field($_POST['article_content']);
401 $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
402
403 // Get API key for submission
404 $options = get_option('mxchat_options');
405 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
406
407 if (strpos($selected_model, 'voyage') === 0) {
408 $api_key = $options['voyage_api_key'] ?? '';
409 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
410 $api_key = $options['gemini_api_key'] ?? '';
411 } else {
412 $api_key = $options['api_key'] ?? '';
413 }
414
415 if (empty($api_key)) {
416 set_transient('mxchat_admin_notice_error',
417 esc_html__('API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
418 30
419 );
420 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
421 exit;
422 }
423
424 // Use centralized utility function for storage
425 $result = MxChat_Utils::submit_content_to_db($article_content, $article_url, $api_key);
426
427 if (is_wp_error($result)) {
428 set_transient('mxchat_admin_notice_error',
429 esc_html__('Error storing content: ', 'mxchat') . $result->get_error_message(),
430 30
431 );
432 } else {
433 set_transient('mxchat_admin_notice_success',
434 esc_html__('Content successfully submitted!', 'mxchat'),
435 30
436 );
437 }
438
439 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
440 exit;
441 }
442 public function mxchat_is_pdf_url($url, $response) {
443 $content_type = wp_remote_retrieve_header($response, 'content-type');
444 $file_extension = strtolower(pathinfo($url, PATHINFO_EXTENSION));
445
446 return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf';
447 }
448
449
450 public function mxchat_handle_pdf_for_knowledge_base($pdf_url, $response) {
451 if (!current_user_can('manage_options')) {
452 //error_log(esc_html__('Unauthorized PDF processing attempt', 'mxchat'));
453 return false;
454 }
455
456 $pdf_url = esc_url_raw($pdf_url);
457 $upload_dir = wp_upload_dir();
458
459 if (isset($upload_dir['error']) && $upload_dir['error'] !== false) {
460 //error_log(sprintf(esc_html__('Upload directory error: %s', 'mxchat'), esc_html($upload_dir['error'])));
461 return false;
462 }
463
464 $pdf_filename = sanitize_file_name('mxchat_kb_' . time() . '.pdf');
465 $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
466
467 $response_body = wp_remote_retrieve_body($response);
468 if (empty($response_body)) {
469 //error_log(esc_html__('Empty PDF response body', 'mxchat'));
470 return false;
471 }
472
473 if (!wp_mkdir_p(dirname($pdf_path))) {
474 //error_log(sprintf(esc_html__('Failed to create directory for PDF: %s', 'mxchat'), esc_html($pdf_path)));
475 return false;
476 }
477
478 try {
479 file_put_contents($pdf_path, $response_body);
480
481 if (!file_exists($pdf_path)) {
482 throw new Exception(__('Failed to save PDF file', 'mxchat'));
483 }
484
485 // IMPROVED: Add PDF validation and multiple parsing attempts
486 $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
487
488 if ($total_pages === false || $total_pages < 1) {
489 throw new Exception(__('Invalid PDF: Unable to parse or no pages found', 'mxchat'));
490 }
491
492 wp_schedule_single_event(time(), 'mxchat_process_pdf_pages', array(
493 'pdf_path' => $pdf_path,
494 'pdf_url' => $pdf_url,
495 'total_pages' => $total_pages,
496 'batch_size' => absint(15),
497 'batch_pause' => absint(10)
498 ));
499
500 $status_data = array(
501 'total_pages' => $total_pages,
502 'processed_pages' => 0,
503 'status' => 'processing',
504 'last_update' => time()
505 );
506
507 set_transient(
508 sanitize_key('mxchat_pdf_status_' . md5($pdf_url)),
509 array_map('sanitize_text_field', $status_data),
510 DAY_IN_SECONDS
511 );
512
513 return __('scheduled', 'mxchat');
514
515 } catch (Exception $e) {
516 //error_log(sprintf(esc_html__('Error preparing PDF for processing: %s', 'mxchat'), esc_html($e->getMessage())));
517 if (file_exists($pdf_path)) {
518 wp_delete_file($pdf_path);
519 }
520 return false;
521 }
522 }
523
524 /**
525 * NEW: Validate PDF and count pages with multiple parser attempts
526 */
527 private function mxchat_validate_and_count_pdf_pages($pdf_path) {
528 // Method 1: Try with Smalot PDF Parser (your current method)
529 try {
530 $parser = new \Smalot\PdfParser\Parser();
531 $pdf = $parser->parseFile($pdf_path);
532 $pages = $pdf->getPages();
533 $page_count = count($pages);
534
535 if ($page_count > 0) {
536 //error_log('PDF parsed successfully with Smalot parser: ' . $page_count . ' pages');
537 return $page_count;
538 }
539 } catch (Exception $e) {
540 //error_log('Smalot PDF parser failed: ' . $e->getMessage());
541 }
542
543 // Method 2: Try with pdfinfo command (if available)
544 if (function_exists('shell_exec') && !$this->mxchat_is_shell_disabled()) {
545 try {
546 $command = 'pdfinfo ' . escapeshellarg($pdf_path) . ' 2>&1';
547 $output = shell_exec($command);
548
549 if ($output && preg_match('/Pages:\s*(\d+)/', $output, $matches)) {
550 $page_count = intval($matches[1]);
551 if ($page_count > 0) {
552 //error_log('PDF parsed successfully with pdfinfo: ' . $page_count . ' pages');
553 return $page_count;
554 }
555 }
556 } catch (Exception $e) {
557 //error_log('pdfinfo command failed: ' . $e->getMessage());
558 }
559 }
560
561 // Method 3: Try to repair PDF and parse again
562 try {
563 $repaired_path = $this->mxchat_attempt_pdf_repair($pdf_path);
564 if ($repaired_path && $repaired_path !== $pdf_path) {
565 $parser = new \Smalot\PdfParser\Parser();
566 $pdf = $parser->parseFile($repaired_path);
567 $pages = $pdf->getPages();
568 $page_count = count($pages);
569
570 if ($page_count > 0) {
571 // Replace original with repaired version
572 copy($repaired_path, $pdf_path);
573 unlink($repaired_path);
574 //error_log('PDF repaired and parsed successfully: ' . $page_count . ' pages');
575 return $page_count;
576 }
577
578 // Clean up repaired file if it didn't work
579 unlink($repaired_path);
580 }
581 } catch (Exception $e) {
582 //error_log('PDF repair attempt failed: ' . $e->getMessage());
583 }
584
585 // Method 4: Manual PDF structure analysis (basic page count)
586 try {
587 $page_count = $this->mxchat_manual_pdf_page_count($pdf_path);
588 if ($page_count > 0) {
589 //error_log('PDF page count determined manually: ' . $page_count . ' pages');
590 return $page_count;
591 }
592 } catch (Exception $e) {
593 //error_log('Manual PDF analysis failed: ' . $e->getMessage());
594 }
595
596 //error_log('All PDF parsing methods failed for: ' . $pdf_path);
597 return false;
598 }
599
600 /**
601 * NEW: Check if shell_exec is disabled
602 */
603 private function mxchat_is_shell_disabled() {
604 $disabled = explode(',', ini_get('disable_functions'));
605 return in_array('shell_exec', $disabled);
606 }
607
608 /**
609 * NEW: Attempt to repair PDF using basic methods
610 */
611 private function mxchat_attempt_pdf_repair($pdf_path) {
612 try {
613 $content = file_get_contents($pdf_path);
614 if (!$content) {
615 return false;
616 }
617
618 // Check if PDF starts with proper header
619 if (substr($content, 0, 4) !== '%PDF') {
620 // Try to find PDF header in the content
621 $header_pos = strpos($content, '%PDF');
622 if ($header_pos !== false && $header_pos < 1024) {
623 // Remove junk before PDF header
624 $content = substr($content, $header_pos);
625 $repaired_path = $pdf_path . '.repaired';
626 file_put_contents($repaired_path, $content);
627 return $repaired_path;
628 }
629 }
630
631 // Check for EOF marker
632 $content = rtrim($content);
633 if (!preg_match('/%%EOF\s*$/', $content)) {
634 // Add EOF marker if missing
635 $content .= "\n%%EOF";
636 $repaired_path = $pdf_path . '.repaired';
637 file_put_contents($repaired_path, $content);
638 return $repaired_path;
639 }
640
641 } catch (Exception $e) {
642 //error_log('PDF repair error: ' . $e->getMessage());
643 }
644
645 return false;
646 }
647
648 /**
649 * NEW: Manual PDF page counting by analyzing PDF structure
650 */
651 private function mxchat_manual_pdf_page_count($pdf_path) {
652 try {
653 $content = file_get_contents($pdf_path);
654 if (!$content) {
655 return 0;
656 }
657
658 // Method 1: Count /Type /Page objects
659 $page_count = preg_match_all('/\/Type\s*\/Page[^s]/', $content);
660 if ($page_count > 0) {
661 return $page_count;
662 }
663
664 // Method 2: Look for /Count in pages object
665 if (preg_match('/\/Type\s*\/Pages.*?\/Count\s+(\d+)/', $content, $matches)) {
666 return intval($matches[1]);
667 }
668
669 // Method 3: Count page references
670 $page_count = preg_match_all('/\d+\s+0\s+obj\s*<<[^>]*\/Type\s*\/Page/', $content);
671 if ($page_count > 0) {
672 return $page_count;
673 }
674
675 } catch (Exception $e) {
676 //error_log('Manual PDF analysis error: ' . $e->getMessage());
677 }
678
679 return 0;
680 }
681
682
683 public function mxchat_process_pdf_pages_cron($pdf_path, $pdf_url, $total_pages, $batch_size, $batch_pause) {
684 // Validate inputs
685 $pdf_path = sanitize_text_field($pdf_path);
686 $pdf_url = esc_url_raw($pdf_url);
687 $total_pages = absint($total_pages);
688 $batch_size = absint($batch_size);
689 $batch_pause = absint($batch_pause);
690
691 try {
692 if (!file_exists($pdf_path)) {
693 throw new Exception(sprintf('PDF file not found at path: %s', esc_html($pdf_path)));
694 }
695
696 // IMPROVED: Try to parse PDF with error recovery
697 $pdf = null;
698 $pages = null;
699
700 try {
701 $parser = new \Smalot\PdfParser\Parser();
702 $pdf = $parser->parseFile($pdf_path);
703 $pages = $pdf->getPages();
704 } catch (Exception $e) {
705 //error_log('Primary PDF parsing failed, attempting recovery: ' . $e->getMessage());
706
707 // Try to repair and parse again
708 $repaired_path = $this->mxchat_attempt_pdf_repair($pdf_path);
709 if ($repaired_path) {
710 try {
711 $parser = new \Smalot\PdfParser\Parser();
712 $pdf = $parser->parseFile($repaired_path);
713 $pages = $pdf->getPages();
714
715 // Replace original with repaired version
716 copy($repaired_path, $pdf_path);
717 unlink($repaired_path);
718 //error_log('PDF successfully repaired and parsed');
719 } catch (Exception $e2) {
720 if (file_exists($repaired_path)) {
721 unlink($repaired_path);
722 }
723 throw new Exception('PDF parsing failed even after repair attempt: ' . $e2->getMessage());
724 }
725 } else {
726 throw new Exception('PDF parsing failed and repair was unsuccessful: ' . $e->getMessage());
727 }
728 }
729
730 if (!$pages || count($pages) === 0) {
731 throw new Exception('No pages found in PDF after parsing');
732 }
733
734 // Get current progress
735 $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
736 $status = get_transient($status_key);
737
738 if (!$status || !is_array($status)) {
739 throw new Exception('Invalid status data retrieved from transient');
740 }
741
742 // Initialize failed pages list if it doesn't exist
743 if (!isset($status['failed_pages_list']) || !is_array($status['failed_pages_list'])) {
744 $status['failed_pages_list'] = [];
745 }
746
747 $start_page = absint($status['processed_pages']);
748 $end_page = min($start_page + $batch_size, $total_pages);
749 $options = get_option('mxchat_options');
750
751 if (empty($options['api_key'])) {
752 throw new Exception('API key is missing or invalid');
753 }
754
755 $successful_pages = 0;
756 $failed_pages = 0;
757
758 for ($i = $start_page; $i < $end_page; $i++) {
759 $page_number = $i + 1;
760 $max_retries = 3;
761 $retry_count = 0;
762 $page_processed = false;
763 $last_error = '';
764
765 while (!$page_processed && $retry_count < $max_retries) {
766 try {
767 $text = $pages[$i]->getText();
768
769 if (empty($text)) {
770 throw new Exception("Empty text on page {$page_number}");
771 }
772
773 $sanitized_content = $this->mxchat_sanitize_content_for_api($text);
774
775 if (empty($sanitized_content)) {
776 throw new Exception("No valid content after sanitization on page {$page_number}");
777 }
778
779 $embedding_vector = $this->mxchat_generate_embedding($sanitized_content);
780
781 if (is_string($embedding_vector)) {
782 throw new Exception("Embedding generation failed: " . $embedding_vector);
783 }
784
785 if (!is_array($embedding_vector)) {
786 throw new Exception("Embedding generation returned unexpected result type: " . gettype($embedding_vector));
787 }
788
789 $metadata = array(
790 'document_type' => 'pdf',
791 'total_pages' => $total_pages,
792 'current_page' => $page_number,
793 'prev_page' => $i > 0 ? $i : null,
794 'next_page' => $i < ($total_pages - 1) ? $i + 2 : null,
795 'source_url' => $pdf_url
796 );
797
798 $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized_content;
799 $page_url = esc_url($pdf_url . "#page=" . $page_number);
800
801 $db_result = MxChat_Utils::submit_content_to_db($content_with_metadata, $page_url, $options['api_key']);
802
803 if (is_wp_error($db_result)) {
804 throw new Exception("Database submission failed: " . $db_result->get_error_message());
805 }
806
807 // Success!
808 $page_processed = true;
809 $successful_pages++;
810
811 } catch (Exception $e) {
812 $retry_count++;
813 $last_error = $e->getMessage();
814
815 //error_log("PDF page {$page_number} failed (attempt {$retry_count}/{$max_retries}): " . $last_error);
816
817 if ($retry_count < $max_retries) {
818 // Wait before retry (exponential backoff: 1s, 2s, 4s)
819 sleep(pow(2, $retry_count - 1));
820 }
821 }
822 }
823
824 // If page still not processed after all retries, mark as failed
825 if (!$page_processed) {
826 $failed_pages++;
827 $status['failed_pages_list'][] = [
828 'page' => $page_number,
829 'error' => $last_error,
830 'time' => time(),
831 'retries' => $max_retries
832 ];
833
834 // Limit failed pages list to prevent memory issues
835 if (count($status['failed_pages_list']) > 50) {
836 $status['failed_pages_list'] = array_slice($status['failed_pages_list'], -50);
837 }
838
839 //error_log("PDF page {$page_number} permanently failed after {$max_retries} attempts: " . $last_error);
840 }
841
842 // Update progress
843 $status['processed_pages'] = absint($page_number);
844 $status['last_update'] = time();
845 $status['failed_pages'] = absint($status['failed_pages'] ?? 0) + ($page_processed ? 0 : 1);
846
847 set_transient($status_key, array_map('sanitize_text_field', $status), DAY_IN_SECONDS);
848 }
849
850 // Schedule next batch if needed
851 if ($end_page < $total_pages) {
852 wp_schedule_single_event(time() + $batch_pause, 'mxchat_process_pdf_pages', array(
853 'pdf_path' => $pdf_path,
854 'pdf_url' => $pdf_url,
855 'total_pages' => $total_pages,
856 'batch_size' => $batch_size,
857 'batch_pause' => $batch_pause
858 ));
859 } else {
860 // Processing complete
861 $status['status'] = 'complete';
862 $status['processed_pages'] = $total_pages;
863
864 // Add completion summary
865 $status['completion_summary'] = [
866 'total_pages' => $total_pages,
867 'successful_pages' => $total_pages - absint($status['failed_pages'] ?? 0),
868 'failed_pages' => absint($status['failed_pages'] ?? 0),
869 'completion_time' => current_time('mysql')
870 ];
871
872 // Save the completed status (don't delete it - let user dismiss manually)
873 set_transient($status_key, array_map('sanitize_text_field', $status), DAY_IN_SECONDS);
874
875 // Clean up the temporary PDF file
876 if (file_exists($pdf_path)) {
877 wp_delete_file($pdf_path);
878 }
879
880 // DON'T delete the status transients here - let user dismiss manually
881 }
882
883 } catch (\Exception $e) {
884 //error_log(sprintf('[MXCHAT-PDF] Error processing PDF: %s', $e->getMessage()));
885
886 $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
887 $status = get_transient($status_key);
888
889 if (!$status || !is_array($status)) {
890 $status = array(
891 'total_pages' => $total_pages,
892 'processed_pages' => 0,
893 'status' => 'error',
894 'error' => sanitize_text_field($e->getMessage()),
895 'last_update' => time()
896 );
897 } else {
898 $status['status'] = 'error';
899 $status['error'] = sanitize_text_field($e->getMessage());
900 $status['last_update'] = time();
901 }
902
903 set_transient($status_key, array_map('sanitize_text_field', $status), DAY_IN_SECONDS);
904
905 if (file_exists($pdf_path)) {
906 wp_delete_file($pdf_path);
907 }
908 }
909 }
910 public function mxchat_save_inline_prompt() {
911 // DEBUG: Log what we're receiving
912 //error_log('=== MXCHAT DEBUG ===');
913 //error_log('POST data: ' . print_r($_POST, true));
914 //error_log('Nonce from POST: ' . ($_POST['_ajax_nonce'] ?? 'NOT FOUND'));
915
916 // Check for nonce security
917 check_ajax_referer('mxchat_save_inline_nonce', '_ajax_nonce');
918
919 // If we get here, nonce passed
920 //error_log('Nonce verification PASSED');
921
922 // Verify permissions
923 if (!current_user_can('manage_options')) {
924 wp_send_json_error(esc_html__('Permission denied.', 'mxchat'));
925 return;
926 }
927
928 global $wpdb;
929 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
930
931 // Validate and sanitize input data - FIXED LINE BELOW
932 $prompt_id = isset($_POST['id']) ? absint($_POST['id']) : 0;
933 $article_content = isset($_POST['article_content']) ? sanitize_textarea_field(wp_unslash($_POST['article_content'])) : '';
934 $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
935
936 if ($prompt_id > 0 && !empty($article_content)) {
937 // Re-generate the embedding vector for the updated content
938 $embedding_vector = $this->mxchat_generate_embedding($article_content);
939 if (is_array($embedding_vector)) {
940 // Serialize the embedding vector before storing it
941 $embedding_vector_serialized = serialize($embedding_vector);
942 // Update the prompt in the database
943 $updated = $wpdb->update(
944 $table_name,
945 array(
946 'article_content' => $article_content,
947 'embedding_vector' => $embedding_vector_serialized,
948 'source_url' => $article_url,
949 ),
950 array('id' => $prompt_id),
951 array('%s', '%s', '%s'),
952 array('%d')
953 );
954 if ($updated !== false) {
955 wp_send_json_success();
956 } else {
957 wp_send_json_error(esc_html__('Database update failed.', 'mxchat'));
958 }
959 } else {
960 wp_send_json_error(esc_html__('Embedding generation failed.', 'mxchat'));
961 }
962 } else {
963 wp_send_json_error(esc_html__('Invalid data.', 'mxchat'));
964 }
965 }
966
967
968 public function mxchat_get_pdf_processing_status($pdf_url) {
969 $pdf_url = esc_url_raw($pdf_url);
970 $status = get_transient(sanitize_key('mxchat_pdf_status_' . md5($pdf_url)));
971
972 if (!$status || !is_array($status)) {
973 return false;
974 }
975
976 // Check for stalled processing (no updates for 5 minutes)
977 if ($status['status'] === 'processing' && (time() - absint($status['last_update'])) > 300) {
978 $status['status'] = 'error';
979 $status['error'] = __('PDF processing appears to be stalled. No updates for over 5 minutes.', 'mxchat');
980
981 // Save the updated status
982 set_transient(
983 sanitize_key('mxchat_pdf_status_' . md5($pdf_url)),
984 array_map('sanitize_text_field', $status),
985 DAY_IN_SECONDS
986 );
987 }
988
989 $result = array(
990 'total_pages' => absint($status['total_pages']),
991 'processed_pages' => absint($status['processed_pages']),
992 'failed_pages' => absint($status['failed_pages'] ?? 0),
993 'percentage' => ($status['total_pages'] > 0)
994 ? round((absint($status['processed_pages']) / absint($status['total_pages'])) * 100)
995 : 0,
996 'status' => sanitize_text_field($status['status']),
997 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
998 'failed_pages_list' => isset($status['failed_pages_list']) ? $status['failed_pages_list'] : array(),
999 'completion_summary' => isset($status['completion_summary']) ? $status['completion_summary'] : null
1000 );
1001
1002 // Add error message if present
1003 if (isset($status['error']) && !empty($status['error'])) {
1004 $result['error'] = sanitize_text_field($status['error']);
1005 }
1006
1007 return $result;
1008 }
1009
1010
1011 public function mxchat_handle_sitemap_submission() {
1012 // Start logging the submission process
1013 //error_log('[MXCHAT-URL] ===== Starting URL submission process =====');
1014
1015 // Check if the form was submitted and verify permissions
1016 if (!isset($_POST['submit_sitemap']) || !current_user_can('manage_options')) {
1017 //error_log('[MXCHAT-URL] Error: Unauthorized access or form not submitted properly');
1018 wp_die(esc_html__('Unauthorized access', 'mxchat'));
1019 }
1020
1021 // Verify nonce
1022 //error_log('[MXCHAT-URL] Verifying nonce');
1023 check_admin_referer('mxchat_submit_sitemap_action', 'mxchat_submit_sitemap_nonce');
1024
1025 // Validate URL
1026 if (!isset($_POST['sitemap_url']) || empty($_POST['sitemap_url'])) {
1027 //error_log('[MXCHAT-URL] Error: Empty or missing URL');
1028 set_transient('mxchat_admin_notice_error',
1029 esc_html__('Please provide a valid URL.', 'mxchat'),
1030 30
1031 );
1032 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1033 exit;
1034 }
1035
1036 $submitted_url = esc_url_raw($_POST['sitemap_url']);
1037 //error_log('[MXCHAT-URL] Processing URL: ' . $submitted_url);
1038
1039 // Validate API key first
1040 $options = get_option('mxchat_options');
1041 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
1042
1043 if (strpos($selected_model, 'voyage') === 0) {
1044 $api_key = $options['voyage_api_key'] ?? '';
1045 $provider_name = 'Voyage AI';
1046 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
1047 $api_key = $options['gemini_api_key'] ?? '';
1048 $provider_name = 'Google Gemini';
1049 } else {
1050 $api_key = $options['api_key'] ?? '';
1051 $provider_name = 'OpenAI';
1052 }
1053
1054 if (empty($api_key)) {
1055 $error_message = sprintf(
1056 esc_html__('%s API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
1057 $provider_name
1058 );
1059 //error_log('[MXCHAT-URL] Error: ' . $error_message);
1060 set_transient('mxchat_admin_notice_error', $error_message, 30);
1061 //error_log('[MXCHAT-URL] Set error transient: ' . $error_message);
1062 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1063 exit;
1064 }
1065
1066 //error_log('[MXCHAT-URL] Fetching URL content');
1067 $response = wp_remote_get($submitted_url, array('timeout' => 30));
1068
1069 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1070 $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response);
1071 //error_log('[MXCHAT-URL] Error fetching URL: ' . $error_message);
1072 set_transient('mxchat_admin_notice_error',
1073 sprintf(
1074 esc_html__('Failed to fetch the URL: %s', 'mxchat'),
1075 esc_html($error_message)
1076 ),
1077 30
1078 );
1079 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1080 exit;
1081 }
1082
1083 $content_type = wp_remote_retrieve_header($response, 'content-type');
1084 //error_log('[MXCHAT-URL] Content type: ' . $content_type);
1085 $body_content = wp_remote_retrieve_body($response);
1086
1087 if (empty($body_content)) {
1088 //error_log('[MXCHAT-URL] Error: Empty response body');
1089 set_transient('mxchat_admin_notice_error',
1090 esc_html__('Empty response received from URL.', 'mxchat'),
1091 30
1092 );
1093 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1094 exit;
1095 }
1096 //error_log('[MXCHAT-URL] Retrieved body content length: ' . strlen($body_content) . ' bytes');
1097
1098 // Handle PDF URL
1099 if ($this->mxchat_is_pdf_url($submitted_url, $response)) {
1100 //error_log('[MXCHAT-URL] Detected PDF URL, handling PDF for knowledge base');
1101 $result = $this->mxchat_handle_pdf_for_knowledge_base($submitted_url, $response);
1102 //error_log('[MXCHAT-URL] PDF handling result: ' . $result);
1103
1104 if ($result === 'scheduled') {
1105 set_transient(
1106 'mxchat_last_pdf_url',
1107 sanitize_text_field($submitted_url),
1108 DAY_IN_SECONDS
1109 );
1110 set_transient('mxchat_admin_notice_info',
1111 esc_html__('PDF processing has started in the background. You can check the progress in the Knowledge Base section.', 'mxchat'),
1112 30
1113 );
1114 } else {
1115 set_transient('mxchat_admin_notice_error',
1116 esc_html__('Failed to start PDF processing: ', 'mxchat') . esc_html($result),
1117 30
1118 );
1119 }
1120
1121 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1122 exit;
1123 }
1124
1125 // Handle Sitemap XML
1126 if (strpos($content_type, 'xml') !== false || strpos($body_content, '<urlset') !== false) {
1127 //error_log('[MXCHAT-URL] Detected XML content, processing as sitemap');
1128 libxml_use_internal_errors(true);
1129 $xml = simplexml_load_string($body_content);
1130 $xml_errors = libxml_get_errors();
1131 libxml_clear_errors();
1132
1133 if ($xml === false || !empty($xml_errors)) {
1134 //error_log('[MXCHAT-URL] Error: Invalid XML format');
1135 if (!empty($xml_errors)) {
1136 foreach ($xml_errors as $error) {
1137 //error_log('[MXCHAT-URL] XML Error: ' . $error->message);
1138 }
1139 }
1140
1141 set_transient('mxchat_admin_notice_error',
1142 esc_html__('Invalid sitemap XML. Please provide a valid sitemap.', 'mxchat'),
1143 30
1144 );
1145 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1146 exit;
1147 }
1148
1149 //error_log('[MXCHAT-URL] Valid XML found, handling sitemap for knowledge base');
1150 $result = $this->mxchat_handle_sitemap_for_knowledge_base($xml, $submitted_url);
1151 //error_log('[MXCHAT-URL] Sitemap handling result: ' . $result);
1152
1153 if ($result === 'scheduled') {
1154 set_transient(
1155 'mxchat_last_sitemap_url',
1156 sanitize_text_field($submitted_url),
1157 DAY_IN_SECONDS
1158 );
1159 set_transient('mxchat_admin_notice_info',
1160 esc_html__('Sitemap processing has started in the background. You can check the progress in the Knowledge Base section.', 'mxchat'),
1161 30
1162 );
1163 } else {
1164 // Return to the admin page without a redirect for better error display
1165 // The error is already stored in the sitemap status transient
1166 set_transient('mxchat_admin_notice_error',
1167 esc_html__('Failed to start sitemap processing. Please check the status below for details.', 'mxchat'),
1168 30
1169 );
1170 }
1171
1172 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1173 exit;
1174 }
1175
1176 // Handle Regular URL
1177 //error_log('[MXCHAT-URL] Processing as regular webpage');
1178 $page_content = $this->mxchat_extract_main_content($body_content);
1179 //error_log('[MXCHAT-URL] Extracted content length: ' . strlen($page_content) . ' bytes');
1180
1181 $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
1182 //error_log('[MXCHAT-URL] Sanitized content length: ' . strlen($sanitized_content) . ' bytes');
1183
1184 if (empty($sanitized_content)) {
1185 //error_log('[MXCHAT-URL] Error: No valid content after sanitization');
1186
1187 // Set both transients - the error notice and the URL status
1188 set_transient('mxchat_admin_notice_error',
1189 esc_html__('No valid content found on the provided URL.', 'mxchat'),
1190 30
1191 );
1192
1193 // Set URL status transient
1194 set_transient('mxchat_single_url_status', [
1195 'url' => $submitted_url,
1196 'timestamp' => current_time('mysql'),
1197 'status' => 'failed',
1198 'error' => esc_html__('No valid content found on the provided URL.', 'mxchat')
1199 ], DAY_IN_SECONDS);
1200
1201 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1202 exit;
1203 }
1204
1205 //error_log('[MXCHAT-URL] Generating embedding for content');
1206 $embedding_vector = $this->mxchat_generate_embedding($sanitized_content);
1207
1208 // Check if embedding_vector is a string (error message)
1209 if (is_string($embedding_vector)) {
1210 //error_log('[MXCHAT-URL] Error generating embedding: ' . $embedding_vector);
1211 $error_message = esc_html__('Failed to generate embedding: ', 'mxchat') . esc_html($embedding_vector);
1212 //error_log('[MXCHAT-URL] Setting error transient: ' . $error_message);
1213
1214 // Set both transients
1215 set_transient('mxchat_admin_notice_error', $error_message, 30);
1216
1217 // Set URL status transient
1218 set_transient('mxchat_single_url_status', [
1219 'url' => $submitted_url,
1220 'timestamp' => current_time('mysql'),
1221 'status' => 'failed',
1222 'error' => $error_message
1223 ], DAY_IN_SECONDS);
1224
1225 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1226 exit;
1227 }
1228
1229 if (is_array($embedding_vector)) {
1230 //error_log('[MXCHAT-URL] Successfully generated embedding with ' . count($embedding_vector) . ' dimensions');
1231
1232 $db_result = MxChat_Utils::submit_content_to_db(
1233 $sanitized_content,
1234 $submitted_url,
1235 $api_key
1236 );
1237
1238 if (is_wp_error($db_result)) {
1239 //error_log('[MXCHAT-URL] Error: Failed to store content in database: ' . $db_result->get_error_message());
1240 $error_message = esc_html__('Failed to store content in database: ', 'mxchat') . esc_html($db_result->get_error_message());
1241 //error_log('[MXCHAT-URL] Setting error transient: ' . $error_message);
1242
1243 // Set both transients
1244 set_transient('mxchat_admin_notice_error', $error_message, 30);
1245
1246 // Set URL status transient
1247 set_transient('mxchat_single_url_status', [
1248 'url' => $submitted_url,
1249 'timestamp' => current_time('mysql'),
1250 'status' => 'failed',
1251 'error' => $error_message
1252 ], DAY_IN_SECONDS);
1253
1254 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1255 exit;
1256 }
1257
1258 //error_log('[MXCHAT-URL] Successfully stored content in database');
1259 $success_message = esc_html__('URL content successfully submitted!', 'mxchat');
1260 //error_log('[MXCHAT-URL] Setting success transient: ' . $success_message);
1261
1262 // Set both transients
1263 set_transient('mxchat_admin_notice_success', $success_message, 30);
1264
1265 // Set URL status transient with success
1266 set_transient('mxchat_single_url_status', [
1267 'url' => $submitted_url,
1268 'timestamp' => current_time('mysql'),
1269 'status' => 'complete',
1270 'content_length' => strlen($sanitized_content),
1271 'embedding_dimensions' => count($embedding_vector)
1272 ], DAY_IN_SECONDS);
1273
1274 } else {
1275 //error_log('[MXCHAT-URL] Error: Failed to generate embedding. Unexpected result type: ' . gettype($embedding_vector));
1276 $error_message = esc_html__('Failed to generate embedding: Unexpected result type. Please check your API key and try again.', 'mxchat');
1277 //error_log('[MXCHAT-URL] Setting error transient: ' . $error_message);
1278
1279 // Set both transients
1280 set_transient('mxchat_admin_notice_error', $error_message, 30);
1281
1282 // Set URL status transient
1283 set_transient('mxchat_single_url_status', [
1284 'url' => $submitted_url,
1285 'timestamp' => current_time('mysql'),
1286 'status' => 'failed',
1287 'error' => $error_message
1288 ], DAY_IN_SECONDS);
1289 }
1290
1291 //error_log('[MXCHAT-URL] ===== Completed URL submission process =====');
1292 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1293 exit;
1294 }
1295 public function mxchat_get_single_url_status() {
1296 $status = get_transient('mxchat_single_url_status');
1297 if (!$status) {
1298 return null;
1299 }
1300
1301 // Add human-readable time
1302 if (isset($status['timestamp'])) {
1303 $status['human_time'] = human_time_diff(strtotime($status['timestamp']), current_time('timestamp')) . ' ' . __('ago', 'mxchat');
1304 }
1305
1306 return $status;
1307 }
1308 public function mxchat_handle_sitemap_for_knowledge_base($xml, $sitemap_url) {
1309 // Clear any single URL status when starting sitemap processing
1310 delete_transient('mxchat_single_url_status');
1311 if (!current_user_can('manage_options')) {
1312 //error_log(esc_html__('Unauthorized sitemap processing attempt', 'mxchat'));
1313 return false;
1314 }
1315
1316 try {
1317 $sitemap_url = esc_url_raw($sitemap_url);
1318
1319 if (!$xml || !is_object($xml)) {
1320 throw new Exception(__('Invalid XML object provided', 'mxchat'));
1321 }
1322
1323 // Add embedding validation before processing
1324 // Test embedding with a small sample text to verify API key is working
1325 $test_result = $this->mxchat_generate_embedding("This is a test to verify the embedding API key is working.");
1326
1327 // Check if test_result is a string (error message) rather than an array (valid embedding)
1328 if (is_string($test_result)) {
1329 //error_log('[MXCHAT-URL] Embedding API validation failed: ' . $test_result);
1330
1331 // Store the error in the status transient so it can be displayed later
1332 $status_data = array(
1333 'total_urls' => 0,
1334 'processed_urls' => 0,
1335 'status' => 'error',
1336 'error' => __('Embedding API validation failed: ', 'mxchat') . $test_result,
1337 'last_update' => time()
1338 );
1339
1340 set_transient(
1341 sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)),
1342 array_map('sanitize_text_field', $status_data),
1343 DAY_IN_SECONDS
1344 );
1345
1346 throw new Exception(__('Embedding API validation failed: ', 'mxchat') . $test_result);
1347 }
1348
1349 // Make sure it's an array (valid embedding)
1350 if (!is_array($test_result)) {
1351 //error_log('[MXCHAT-URL] Embedding API returned unexpected result type: ' . gettype($test_result));
1352 throw new Exception(__('Embedding API returned unexpected result type. Please check your configuration.', 'mxchat'));
1353 }
1354
1355 $urls = [];
1356 foreach ($xml->url as $url_element) {
1357 $url = esc_url_raw((string)$url_element->loc);
1358 if ($url) {
1359 $urls[] = $url;
1360 }
1361 }
1362
1363 $total_urls = absint(count($urls));
1364
1365 if ($total_urls < 1) {
1366 throw new Exception(__('No valid URLs found in sitemap', 'mxchat'));
1367 }
1368
1369 wp_schedule_single_event(time(), 'mxchat_process_sitemap_urls', array(
1370 'urls' => $urls,
1371 'sitemap_url' => $sitemap_url,
1372 'total_urls' => $total_urls,
1373 'batch_size' => absint(10),
1374 'batch_pause' => absint(5)
1375 ));
1376
1377 $status_data = array(
1378 'total_urls' => $total_urls,
1379 'processed_urls' => 0,
1380 'status' => 'processing',
1381 'last_update' => time()
1382 );
1383
1384 set_transient(
1385 sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)),
1386 array_map('sanitize_text_field', $status_data),
1387 DAY_IN_SECONDS
1388 );
1389
1390 return __('scheduled', 'mxchat');
1391
1392 } catch (\Exception $e) {
1393 $error_message = $e->getMessage();
1394 //error_log(sprintf(esc_html__('Error preparing sitemap for processing: %s', 'mxchat'), esc_html($error_message)));
1395
1396 // Store the sitemap URL and error in transients so they can be displayed
1397 set_transient(
1398 'mxchat_last_sitemap_url',
1399 sanitize_text_field($sitemap_url),
1400 DAY_IN_SECONDS
1401 );
1402
1403 $status_data = array(
1404 'total_urls' => 0,
1405 'processed_urls' => 0,
1406 'status' => 'error',
1407 'error' => $error_message,
1408 'last_update' => time()
1409 );
1410
1411 set_transient(
1412 sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)),
1413 array_map('sanitize_text_field', $status_data),
1414 DAY_IN_SECONDS
1415 );
1416
1417 return $error_message;
1418 }
1419 }
1420
1421 public function mxchat_process_sitemap_urls_cron($urls, $sitemap_url, $total_urls, $batch_size, $batch_pause) {
1422 // Validate inputs
1423 $sitemap_url = esc_url_raw($sitemap_url);
1424 $total_urls = absint($total_urls);
1425 $batch_size = absint($batch_size);
1426 $batch_pause = absint($batch_pause);
1427
1428 if (!is_array($urls) || empty($urls)) {
1429 return;
1430 }
1431
1432 try {
1433 $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
1434 $status = get_transient($status_key);
1435
1436 if (!$status || !is_array($status)) {
1437 throw new Exception('Invalid status data retrieved from transient');
1438 }
1439
1440 // Initialize failed_urls array if it doesn't exist
1441 if (!isset($status['failed_urls_list']) || !is_array($status['failed_urls_list'])) {
1442 $status['failed_urls_list'] = [];
1443 }
1444
1445 $start_url = absint($status['processed_urls']);
1446 $end_url = min($start_url + $batch_size, $total_urls);
1447
1448 // Track batch statistics - IMPROVED ERROR HANDLING
1449 $batch_stats = [
1450 'processed' => 0,
1451 'failed' => 0,
1452 'last_error' => '',
1453 'embedding_errors' => 0,
1454 'network_errors' => 0,
1455 'timeout_errors' => 0
1456 ];
1457
1458 // REMOVED: Embedding test on first batch - causes unnecessary failures
1459
1460 // Set execution time limit for this batch
1461 @set_time_limit(300); // 5 minutes max per batch
1462
1463 // Check available memory
1464 $memory_limit = ini_get('memory_limit');
1465 $memory_usage = memory_get_usage(true);
1466
1467 for ($i = $start_url; $i < $end_url; $i++) {
1468 $page_url = esc_url_raw($urls[$i]);
1469 $max_retries = 5; // INCREASED from 3 to 5
1470 $retry_count = 0;
1471 $url_processed = false;
1472 $last_error = '';
1473
1474 // Check memory usage before processing each URL
1475 if (memory_get_usage(true) > (1024 * 1024 * 100)) { // 100MB limit
1476 //error_log('MxChat: Memory usage high, taking break');
1477 sleep(2);
1478 }
1479
1480 while (!$url_processed && $retry_count < $max_retries) {
1481 try {
1482 // IMPROVED: More flexible timeout based on retry count
1483 $timeout = 30 + ($retry_count * 10); // 30s, 40s, 50s, etc.
1484
1485 // Attempt to fetch the URL
1486 $page_response = wp_remote_get($page_url, array(
1487 'timeout' => $timeout,
1488 'redirection' => 5,
1489 'user-agent' => 'MxChat/1.0'
1490 ));
1491
1492 if (is_wp_error($page_response)) {
1493 $error_msg = $page_response->get_error_message();
1494
1495 // Categorize network errors
1496 if (strpos($error_msg, 'timeout') !== false) {
1497 $batch_stats['timeout_errors']++;
1498 } else {
1499 $batch_stats['network_errors']++;
1500 }
1501
1502 throw new Exception('HTTP request failed: ' . $error_msg);
1503 }
1504
1505 $response_code = wp_remote_retrieve_response_code($page_response);
1506
1507 // IMPROVED: Handle more response codes gracefully
1508 if (!in_array($response_code, [200, 201, 202])) {
1509 // For 4xx errors, don't retry (permanent failures)
1510 if ($response_code >= 400 && $response_code < 500) {
1511 throw new Exception('HTTP Status: ' . $response_code . ' (permanent failure)');
1512 }
1513 throw new Exception('HTTP Status: ' . $response_code);
1514 }
1515
1516 $page_html = wp_remote_retrieve_body($page_response);
1517
1518 if (empty($page_html)) {
1519 throw new Exception('Empty response body');
1520 }
1521
1522 $page_content = $this->mxchat_extract_main_content($page_html);
1523 $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
1524
1525 if (empty($sanitized_content)) {
1526 // Don't retry for empty content - it's likely a permanent issue
1527 //error_log("MxChat: No content found for URL: {$page_url}");
1528 $url_processed = true; // Mark as "processed" to skip
1529 $batch_stats['processed']++; // Count as processed (even though skipped)
1530 break;
1531 }
1532
1533 // IMPROVED: More resilient embedding generation
1534 $embedding_vector = $this->mxchat_generate_embedding($sanitized_content);
1535
1536 if (is_string($embedding_vector)) {
1537 $batch_stats['embedding_errors']++;
1538
1539 // Special handling for different embedding errors
1540 if (strpos($embedding_vector, 'rate limit') !== false ||
1541 strpos($embedding_vector, 'quota') !== false) {
1542 // Rate limit - wait longer before retry
1543 sleep(30 + ($retry_count * 10));
1544 }
1545
1546 throw new Exception('Embedding generation failed: ' . $embedding_vector);
1547 }
1548
1549 if (!is_array($embedding_vector)) {
1550 throw new Exception('Embedding generation returned unexpected result type: ' . gettype($embedding_vector));
1551 }
1552
1553 // Submit to database
1554 $options = get_option('mxchat_options');
1555 $submission_result = MxChat_Utils::submit_content_to_db($sanitized_content, $page_url, $options['api_key']);
1556
1557 if (is_wp_error($submission_result)) {
1558 throw new Exception('Database submission failed: ' . $submission_result->get_error_message());
1559 }
1560
1561 // Success!
1562 $url_processed = true;
1563 $batch_stats['processed']++;
1564
1565 } catch (Exception $e) {
1566 $retry_count++;
1567 $last_error = $e->getMessage();
1568
1569 // IMPROVED: Different wait times based on error type
1570 if (strpos($last_error, 'rate limit') !== false) {
1571 sleep(60); // Wait 1 minute for rate limits
1572 } elseif (strpos($last_error, 'timeout') !== false) {
1573 sleep(10); // Wait 10 seconds for timeouts
1574 } elseif (strpos($last_error, 'permanent failure') !== false) {
1575 break; // Don't retry 4xx errors
1576 } else {
1577 // Exponential backoff for other errors
1578 sleep(pow(2, $retry_count - 1));
1579 }
1580 }
1581 }
1582
1583 // If URL still not processed after all retries, mark as failed
1584 if (!$url_processed) {
1585 $batch_stats['failed']++;
1586 $batch_stats['last_error'] = $last_error;
1587
1588 // Add to failed URLs list
1589 $status['failed_urls_list'][] = [
1590 'url' => $page_url,
1591 'error' => $last_error,
1592 'time' => time(),
1593 'retries' => $max_retries
1594 ];
1595
1596 // Limit the number of failed URLs we store
1597 if (count($status['failed_urls_list']) > 100) {
1598 $status['failed_urls_list'] = array_slice($status['failed_urls_list'], -100);
1599 }
1600 }
1601
1602 // Update progress after each URL
1603 $status['processed_urls'] = absint($i + 1);
1604 $status['last_update'] = time();
1605 $status['failed_urls'] = absint($status['failed_urls'] ?? 0) + ($url_processed ? 0 : 1);
1606 $status['last_error'] = $batch_stats['last_error'];
1607
1608 set_transient($status_key, $status, DAY_IN_SECONDS);
1609 }
1610
1611 // IMPROVED: More forgiving batch failure handling
1612 // Only stop if we have catastrophic failure rates
1613 $failure_rate = $batch_stats['failed'] / max(1, $batch_stats['processed'] + $batch_stats['failed']);
1614
1615 if ($batch_stats['processed'] === 0 && $batch_stats['failed'] >= 5) {
1616 // Only stop if we have 5+ complete failures in a row
1617 $status['status'] = 'error';
1618 $status['error'] = sprintf(
1619 'Processing stopped after %d consecutive failures. Last error: %s',
1620 $batch_stats['failed'],
1621 $batch_stats['last_error']
1622 );
1623 set_transient($status_key, $status, DAY_IN_SECONDS);
1624 return;
1625 }
1626
1627 // REMOVED: Embedding error threshold - too aggressive
1628
1629 // Update final progress
1630 $status['processed_urls'] = min($end_url, $total_urls);
1631 $status['last_update'] = time();
1632 $status['batch_stats'] = $batch_stats; // Store for debugging
1633 set_transient($status_key, $status, DAY_IN_SECONDS);
1634
1635 // Check if we've processed all URLs
1636 if ($end_url >= $total_urls) {
1637 // All URLs have been processed - mark as complete
1638 $status['status'] = 'complete';
1639 $status['processed_urls'] = $total_urls;
1640
1641 // Add completion summary
1642 $status['completion_summary'] = [
1643 'total_urls' => $total_urls,
1644 'successful_urls' => $total_urls - absint($status['failed_urls'] ?? 0),
1645 'failed_urls' => absint($status['failed_urls'] ?? 0),
1646 'completion_time' => current_time('mysql'),
1647 'final_batch_stats' => $batch_stats
1648 ];
1649
1650 set_transient($status_key, $status, DAY_IN_SECONDS);
1651 } else {
1652 // IMPROVED: Dynamic pause based on error rates
1653 $dynamic_pause = $batch_pause;
1654
1655 if ($failure_rate > 0.5) {
1656 $dynamic_pause *= 3; // Wait 3x longer if high failure rate
1657 } elseif ($batch_stats['embedding_errors'] > 3) {
1658 $dynamic_pause *= 2; // Wait 2x longer if embedding issues
1659 }
1660
1661 // Schedule next batch
1662 wp_schedule_single_event(time() + $dynamic_pause, 'mxchat_process_sitemap_urls', array(
1663 'urls' => $urls,
1664 'sitemap_url' => $sitemap_url,
1665 'total_urls' => $total_urls,
1666 'batch_size' => $batch_size,
1667 'batch_pause' => $batch_pause,
1668 ));
1669 }
1670 } catch (\Exception $e) {
1671 // IMPROVED: Don't fail permanently on exceptions
1672 $status['last_error'] = $e->getMessage();
1673 $status['error_count'] = ($status['error_count'] ?? 0) + 1;
1674
1675 // Only mark as permanent error after multiple batch failures
1676 if ($status['error_count'] >= 5) {
1677 $status['status'] = 'error';
1678 $status['error'] = 'Too many batch failures: ' . $e->getMessage();
1679 } else {
1680 // Retry the batch after a longer pause
1681 wp_schedule_single_event(time() + 300, 'mxchat_process_sitemap_urls', array(
1682 'urls' => $urls,
1683 'sitemap_url' => $sitemap_url,
1684 'total_urls' => $total_urls,
1685 'batch_size' => max(5, $batch_size / 2), // Reduce batch size on error
1686 'batch_pause' => $batch_pause * 2, // Double the pause
1687 ));
1688 }
1689
1690 set_transient($status_key, $status, DAY_IN_SECONDS);
1691 }
1692 }
1693
1694 public function mxchat_sanitize_content_for_api($content) {
1695 //error_log('[MXCHAT-SANITIZE] Original content preview: ' . substr($content, 0, 500) . '...');
1696
1697 // Remove script, style tags, and HTML comments
1698 $content = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $content);
1699 $content = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $content);
1700 $content = preg_replace('/<!--(.|\s)*?-->/', '', $content);
1701
1702 // Remove all HTML tags and decode HTML entities
1703 $content = wp_strip_all_tags($content);
1704 $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5);
1705
1706 // Normalize whitespace but preserve paragraph breaks
1707 // First, normalize line endings to \n
1708 $content = str_replace(["\r\n", "\r"], "\n", $content);
1709 // Replace multiple spaces/tabs with single space, but preserve newlines
1710 $content = preg_replace('/[ \t]+/', ' ', $content);
1711 // Replace 3+ newlines with 2 newlines (max 2 blank lines)
1712 $content = preg_replace('/\n{3,}/', "\n\n", $content);
1713 // Trim each line
1714 $lines = explode("\n", $content);
1715 $lines = array_map('trim', $lines);
1716 $content = implode("\n", $lines);
1717 // Final trim
1718 $content = trim($content);
1719
1720 // Remove control characters (which can cause database issues) but preserve \n (0x0A) and \r (0x0D)
1721 $content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $content);
1722
1723 // Remove NULL bytes which can cause database errors
1724 $content = str_replace("\0", "", $content);
1725
1726 // Ensure valid UTF-8 encoding
1727 $content = wp_check_invalid_utf8($content);
1728
1729 // Remove any extremely long strings without spaces (often garbage)
1730 $content = preg_replace('/\S{300,}/', ' ', $content);
1731
1732 // Replace problematic characters that often cause database issues
1733 $content = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $content); // Remove emoji and other high Unicode characters
1734
1735 // Replace any remaining potentially problematic characters with spaces
1736 // BUT preserve newlines by temporarily replacing them
1737 $content = str_replace("\n", "NEWLINE_PLACEHOLDER", $content);
1738 $content = preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\p{Sm}]/u', ' ', $content);
1739 $content = str_replace("NEWLINE_PLACEHOLDER", "\n", $content);
1740
1741 // Limit to reasonable length if needed
1742 $max_length = 65000; // Just under MySQL TEXT field limit
1743 if (strlen($content) > $max_length) {
1744 $content = substr($content, 0, $max_length);
1745 }
1746
1747 //error_log('[MXCHAT-SANITIZE] Sanitized content preview: ' . substr($content, 0, 500) . '...');
1748 return $content;
1749 }
1750 public function mxchat_extract_main_content($html) {
1751 if (empty($html)) {
1752 return '';
1753 }
1754 try {
1755 $dom = new DOMDocument;
1756 libxml_use_internal_errors(true); // Suppress HTML parsing errors
1757 @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
1758 $xpath = new DOMXPath($dom);
1759
1760 // For debugging purposes
1761 $debugEnabled = false; // Set to true to enable debugging output
1762 $debug = function($message) use ($debugEnabled) {
1763 if ($debugEnabled) {
1764 //error_log('[MXCHAT-DEBUG] ' . $message);
1765 }
1766 };
1767
1768 // Direct targeting for Gerow theme posts
1769 $post_text = $xpath->query('//div[contains(@class, "post-text")]');
1770 if ($post_text && $post_text->length > 0) {
1771 $debug("Found post-text directly");
1772 $content = '';
1773 foreach ($post_text as $node) {
1774 $content .= $dom->saveHTML($node);
1775 }
1776 if (!empty($content)) {
1777 $debug("Returning post-text content");
1778 return $content;
1779 }
1780 }
1781
1782 // Try to get the blog details content which contains the post-text
1783 $blog_details = $xpath->query('//div[contains(@class, "blog-details-content")]');
1784 if ($blog_details && $blog_details->length > 0) {
1785 $debug("Found blog-details-content");
1786 $content = '';
1787 foreach ($blog_details as $node) {
1788 $content .= $dom->saveHTML($node);
1789 }
1790 if (!empty($content)) {
1791 $debug("Returning blog-details-content");
1792 return $content;
1793 }
1794 }
1795
1796 // Try to get the article which contains the blog details
1797 $article = $xpath->query('//article[contains(@class, "blog-details-wrap")]');
1798 if ($article && $article->length > 0) {
1799 $debug("Found article with blog-details-wrap");
1800 $content = '';
1801 foreach ($article as $node) {
1802 $content .= $dom->saveHTML($node);
1803 }
1804 if (!empty($content)) {
1805 $debug("Returning article content");
1806 return $content;
1807 }
1808 }
1809
1810 // Try even broader with the blog-item-wrap
1811 $blog_item = $xpath->query('//div[contains(@class, "blog-item-wrap")]');
1812 if ($blog_item && $blog_item->length > 0) {
1813 $debug("Found blog-item-wrap");
1814 $content = '';
1815 foreach ($blog_item as $node) {
1816 $content .= $dom->saveHTML($node);
1817 }
1818 if (!empty($content)) {
1819 $debug("Returning blog-item-wrap content");
1820 return $content;
1821 }
1822 }
1823
1824 // Specific Gerow theme path
1825 $gerow_path = $xpath->query('//section[contains(@class, "blog-area")]//div[contains(@class, "post-text")]');
1826 if ($gerow_path && $gerow_path->length > 0) {
1827 $debug("Found Gerow theme path to post-text");
1828 $content = '';
1829 foreach ($gerow_path as $node) {
1830 $content .= $dom->saveHTML($node);
1831 }
1832 if (!empty($content)) {
1833 $debug("Returning Gerow post-text content");
1834 return $content;
1835 }
1836 }
1837
1838 // Generic blog post selectors
1839 $selectors = [
1840 // Blog post specific selectors
1841 '//div[contains(@class, "post-text")]',
1842 '//article[contains(@class, "blog-post-item")]//div[contains(@class, "post-text")]',
1843 '//div[contains(@class, "blog-details-content")]',
1844 '//article[contains(@class, "blog-details-wrap")]',
1845 '//div[contains(@class, "entry-content")]',
1846 '//div[contains(@class, "blog-content")]',
1847 '//div[contains(@class, "blog-item-wrap")]',
1848
1849 // More general content selectors
1850 '//div[contains(@class, "page__content")]',
1851 '//div[contains(@class, "elementor-widget-container")]',
1852 '//div[contains(@class, "elementor-text-editor")]',
1853 '//div[contains(@class, "elementor-widget-text-editor")]',
1854 '//*[contains(@class, "entry-content")]',
1855 '//*[contains(@class, "post-content")]',
1856 '//*[contains(@class, "article-content")]',
1857 '//*[@id="content"]',
1858 '//*[@id="main-content"]',
1859 '//section[contains(@class, "blog-area")]',
1860 '//article',
1861 '//main',
1862 '//div[contains(@class, "content")]'
1863 ];
1864
1865 // First handle Elementor content
1866 $debug("Checking for Elementor content");
1867 $elementor_widgets = $xpath->query('//div[contains(@class, "elementor-element")]//div[contains(@class, "elementor-widget-container")]');
1868 if ($elementor_widgets && $elementor_widgets->length > 0) {
1869 $debug("Found Elementor widgets");
1870 $combined_content = '';
1871 foreach ($elementor_widgets as $widget) {
1872 $widget_content = $dom->saveHTML($widget);
1873 if (!empty($widget_content)) {
1874 $combined_content .= $widget_content;
1875 }
1876 }
1877 if (!empty($combined_content)) {
1878 $debug("Returning Elementor content");
1879 return $combined_content;
1880 }
1881 }
1882
1883 // Try standard selectors one by one
1884 foreach ($selectors as $selector) {
1885 $debug("Trying selector: " . $selector);
1886 $nodes = $xpath->query($selector);
1887 if ($nodes && $nodes->length > 0) {
1888 $debug("Found matches for selector: " . $selector);
1889 $content = '';
1890 foreach ($nodes as $node) {
1891 $content .= $dom->saveHTML($node);
1892 }
1893 if (!empty($content)) {
1894 $debug("Returning content from selector: " . $selector);
1895 return $content;
1896 }
1897 }
1898 }
1899
1900 // Manual regex fallback for post-text if DOM methods fail
1901 $debug("Trying regex fallback");
1902 if (preg_match('/<div class="post-text">(.*?)<\/div>\s*<\/div>\s*<\/div>/s', $html, $matches)) {
1903 $debug("Found post-text via regex");
1904 return '<div class="post-text">' . $matches[1] . '</div>';
1905 }
1906
1907 // Try to extract the blog section as a whole
1908 $blog_section = $xpath->query('//section[contains(@class, "blog-area")]');
1909 if ($blog_section && $blog_section->length > 0) {
1910 $debug("Found blog-area section");
1911 $content = '';
1912 foreach ($blog_section as $node) {
1913 $content .= $dom->saveHTML($node);
1914 }
1915 if (!empty($content)) {
1916 $debug("Returning blog-area section content");
1917 return $content;
1918 }
1919 }
1920
1921 // Fallback: Return the body content if no specific selector matches
1922 $debug("Using body fallback");
1923 $body = $dom->getElementsByTagName('body');
1924 if ($body->length > 0) {
1925 return $dom->saveHTML($body->item(0));
1926 }
1927
1928 // Last resort: return the original HTML
1929 $debug("Returning original HTML");
1930 return $html;
1931 } catch (Exception $e) {
1932 //error_log('[MXCHAT-ERROR] Content extraction failed: ' . $e->getMessage());
1933 return $html; // Return original HTML if parsing fails
1934 } finally {
1935 libxml_clear_errors();
1936 }
1937 }
1938 public function mxchat_get_sitemap_processing_status($sitemap_url) {
1939 $sitemap_url = esc_url_raw($sitemap_url);
1940 $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
1941 $status = get_transient($status_key);
1942
1943 if (!$status || !is_array($status)) {
1944 return false;
1945 }
1946
1947 // Auto-complete check: if all URLs are processed but status isn't complete
1948 if (isset($status['processed_urls']) && isset($status['total_urls']) &&
1949 $status['processed_urls'] >= $status['total_urls'] &&
1950 isset($status['status']) && $status['status'] !== 'complete' &&
1951 $status['status'] !== 'error') {
1952
1953 // Mark as complete
1954 $status['status'] = 'complete';
1955 $status['processed_urls'] = $status['total_urls']; // Ensure exact match
1956
1957 // Update the transient with the corrected status
1958 set_transient($status_key, $status, DAY_IN_SECONDS);
1959 }
1960
1961 return array(
1962 'total_urls' => absint($status['total_urls']),
1963 'processed_urls' => absint($status['processed_urls']),
1964 'failed_urls' => absint($status['failed_urls'] ?? 0),
1965 'percentage' => ($status['total_urls'] > 0)
1966 ? round((absint($status['processed_urls']) / absint($status['total_urls'])) * 100)
1967 : 0,
1968 'status' => sanitize_text_field($status['status']),
1969 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
1970 'error' => isset($status['error']) ? sanitize_text_field($status['error']) : '',
1971 'last_error' => isset($status['last_error']) ? sanitize_text_field($status['last_error']) : '',
1972 'failed_urls_list' => isset($status['failed_urls_list']) ? $status['failed_urls_list'] : array()
1973 );
1974 }
1975
1976 public function mxchat_ajax_get_status_updates() {
1977 try {
1978 // Verify the request
1979 check_ajax_referer('mxchat_status_nonce', 'nonce');
1980
1981 // Get the status just like in your admin page
1982 $pdf_url = get_transient('mxchat_last_pdf_url');
1983 $sitemap_url = get_transient('mxchat_last_sitemap_url');
1984
1985 $pdf_status = $pdf_url ? $this->mxchat_get_pdf_processing_status($pdf_url) : false;
1986 $sitemap_status = $sitemap_url ? $this->mxchat_get_sitemap_processing_status($sitemap_url) : false;
1987
1988 // Add the PDF URL to the status object
1989 if ($pdf_status && $pdf_url) {
1990 $pdf_status['pdf_url'] = $pdf_url;
1991 }
1992
1993 // Set the current PDF URL for the manual batch processing button
1994 $current_pdf_url = $pdf_url;
1995
1996 // Check for true processing status, not just presence of status
1997 $is_active_processing =
1998 ($sitemap_status && isset($sitemap_status['status']) && $sitemap_status['status'] === 'processing') ||
1999 ($pdf_status && isset($pdf_status['status']) && $pdf_status['status'] === 'processing');
2000
2001 // Get single URL status, but only if no processing is active
2002 $single_url_status = !$is_active_processing ? $this->mxchat_get_single_url_status() : false;
2003
2004 // REMOVED: Auto-clearing of completed status - now only done via dismiss button
2005
2006 // Return JSON response with the status data
2007 wp_send_json(array(
2008 'pdf_status' => $pdf_status,
2009 'sitemap_status' => $sitemap_status,
2010 'single_url_status' => $single_url_status,
2011 'is_processing' => $is_active_processing,
2012 'current_pdf_url' => $current_pdf_url
2013 ));
2014
2015 } catch (Exception $e) {
2016 // Log the error
2017 //error_log('MxChat Status Update Error: ' . $e->getMessage());
2018
2019 // Return a friendly error response
2020 wp_send_json_error(array(
2021 'message' => 'Error getting status updates: ' . $e->getMessage(),
2022 'status' => 'error'
2023 ));
2024 }
2025 }
2026 public function mxchat_stop_processing() {
2027 // Verify permissions
2028 if (!current_user_can('manage_options')) {
2029 wp_die(esc_html__('Unauthorized access', 'mxchat'));
2030 }
2031
2032 // Verify nonce
2033 check_admin_referer('mxchat_stop_processing_action', 'mxchat_stop_processing_nonce');
2034
2035 // Get the last sitemap URL and clear its transient
2036 $sitemap_url = get_transient('mxchat_last_sitemap_url');
2037 if ($sitemap_url) {
2038 delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
2039 delete_transient('mxchat_last_sitemap_url');
2040 }
2041
2042 // Get the last PDF URL and clear its transient
2043 $pdf_url = get_transient('mxchat_last_pdf_url');
2044 if ($pdf_url) {
2045 delete_transient('mxchat_pdf_status_' . md5($pdf_url));
2046 delete_transient('mxchat_last_pdf_url');
2047 }
2048
2049 // Unschedule any pending sitemap events
2050 $timestamp = wp_next_scheduled('mxchat_process_sitemap_urls');
2051 if ($timestamp) {
2052 wp_unschedule_event($timestamp, 'mxchat_process_sitemap_urls');
2053 }
2054
2055 // Redirect back with a success message
2056 set_transient('mxchat_admin_notice_success',
2057 esc_html__('Processing has been stopped successfully.', 'mxchat'),
2058 30
2059 );
2060 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
2061 exit;
2062 }
2063
2064
2065
2066 /**
2067 * checking if WooCommerce products were already processed in the WordPress database.
2068 */
2069 public function ajax_mxchat_get_content_list() {
2070 // Verify the nonce
2071 check_ajax_referer('mxchat_content_selector_nonce', 'nonce');
2072
2073 if (!current_user_can('manage_options')) {
2074 wp_send_json_error(__('Unauthorized access', 'mxchat'));
2075 }
2076
2077 $page = isset($_GET['page']) ? absint($_GET['page']) : 1;
2078 $per_page = isset($_GET['per_page']) ? absint($_GET['per_page']) : 50;
2079 $search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
2080 $post_type = isset($_GET['post_type']) ? sanitize_text_field($_GET['post_type']) : 'all';
2081 $post_status = isset($_GET['post_status']) ? sanitize_text_field($_GET['post_status']) : 'publish';
2082 $processed_filter = isset($_GET['processed_filter']) ? sanitize_text_field($_GET['processed_filter']) : 'all';
2083
2084 // Build query args
2085 $args = array(
2086 'posts_per_page' => $per_page,
2087 'paged' => $page,
2088 'post_status' => $post_status !== 'all' ? $post_status : array('publish', 'draft', 'pending'),
2089 'orderby' => 'date',
2090 'order' => 'DESC',
2091 );
2092
2093 // Handle post types
2094 if ($post_type !== 'all') {
2095 $args['post_type'] = $post_type;
2096 } else {
2097 // Default to post and page if we can't get post types
2098 $args['post_type'] = array('post', 'page');
2099
2100 // Try to get public post types
2101 $public_types = $this->mxchat_get_public_post_types();
2102 if (is_array($public_types) && !empty($public_types)) {
2103 $args['post_type'] = array_keys($public_types);
2104 }
2105 }
2106
2107 if (!empty($search)) {
2108 $args['s'] = $search;
2109 }
2110
2111 // ================================
2112 // WordPress DB checking for WooCommerce products
2113 // ================================
2114
2115 $processed_data = array();
2116
2117 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
2118 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
2119 $pinecone_manager->mxchat_refresh_after_new_content($pinecone_options);
2120 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2121
2122 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
2123 // ONLY check Pinecone if it's enabled
2124 $processed_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
2125 } else {
2126 // IMPROVED: WordPress DB checking with better URL matching for WooCommerce
2127 global $wpdb;
2128 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2129 $processed_items = $wpdb->get_results("SELECT id, source_url, timestamp FROM {$table_name}");
2130
2131 if (!empty($processed_items)) {
2132 foreach ($processed_items as $item) {
2133 // FIXED: Use improved URL matching for WooCommerce products
2134 $post_id = $this->mxchat_url_to_post_id_improved($item->source_url);
2135
2136 if ($post_id) {
2137 $processed_data[$post_id] = array(
2138 'db_id' => $item->id,
2139 'timestamp' => $item->timestamp,
2140 'url' => $item->source_url,
2141 'source' => 'wordpress'
2142 );
2143 }
2144 }
2145 }
2146 }
2147
2148 // ================================
2149
2150 // Get processed IDs as a simple array for in_array checks
2151 $processed_ids = array_keys($processed_data);
2152
2153 // Handle processed/unprocessed filter
2154 if ($processed_filter === 'processed' && !empty($processed_ids)) {
2155 $args['post__in'] = $processed_ids;
2156 } elseif ($processed_filter === 'unprocessed' && !empty($processed_ids)) {
2157 $args['post__not_in'] = $processed_ids;
2158 }
2159
2160 // Run the query
2161 $query = new WP_Query($args);
2162 $content_items = array();
2163
2164 if ($query->have_posts()) {
2165 while ($query->have_posts()) {
2166 $query->the_post();
2167 $id = get_the_ID();
2168 $post_date = get_the_date();
2169 $excerpt = wp_trim_words(get_the_excerpt(), 20, '...');
2170 $word_count = str_word_count(strip_tags(get_the_content()));
2171
2172 $is_processed = in_array($id, $processed_ids);
2173 $processed_date = '';
2174 $db_record_id = 0;
2175 $data_source = 'none';
2176
2177 if ($is_processed && isset($processed_data[$id])) {
2178 $item_data = $processed_data[$id];
2179 $data_source = $item_data['source'];
2180
2181 if ($data_source === 'wordpress' && isset($item_data['timestamp'])) {
2182 // WordPress DB format
2183 $timestamp = strtotime($item_data['timestamp']);
2184 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2185 $db_record_id = $item_data['db_id'];
2186 } elseif ($data_source === 'pinecone') {
2187 // Pinecone format
2188 $processed_date = $item_data['processed_date'];
2189 $db_record_id = $item_data['db_id'];
2190 }
2191 }
2192
2193 $content_items[] = array(
2194 'id' => $id,
2195 'title' => get_the_title(),
2196 'permalink' => get_permalink(),
2197 'date' => $post_date,
2198 'type' => get_post_type(),
2199 'status' => get_post_status(),
2200 'excerpt' => $excerpt,
2201 'word_count' => $word_count,
2202 'already_processed' => $is_processed,
2203 'processed_date' => $processed_date,
2204 'db_record_id' => $db_record_id,
2205 'data_source' => $data_source
2206 );
2207 }
2208 wp_reset_postdata();
2209 }
2210
2211 $response = array(
2212 'items' => $content_items,
2213 'total' => $query->found_posts,
2214 'total_pages' => $query->max_num_pages,
2215 'current_page' => $page,
2216 'processed_count' => count($processed_ids)
2217 );
2218
2219 wp_send_json_success($response);
2220 exit;
2221 }
2222
2223 /**
2224 * This function handles various WooCommerce URL formats and permalink structures
2225 */
2226 private function mxchat_url_to_post_id_improved($url) {
2227 // First try the standard WordPress function
2228 $post_id = url_to_postid($url);
2229
2230 if ($post_id > 0) {
2231 return $post_id;
2232 }
2233
2234 // If that fails, try more aggressive URL matching for WooCommerce products
2235 // Remove trailing slashes and query parameters for better matching
2236 $clean_url = rtrim($url, '/');
2237 $clean_url = strtok($clean_url, '?'); // Remove query parameters
2238
2239 // Try again with cleaned URL
2240 $post_id = url_to_postid($clean_url);
2241 if ($post_id > 0) {
2242 return $post_id;
2243 }
2244
2245 // For WooCommerce products, try extracting slug from URL
2246 if (strpos($url, '/product/') !== false || strpos($url, 'product=') !== false) {
2247 // Extract product slug from various URL formats
2248 $product_slug = '';
2249
2250 // Handle pretty permalinks: /product/product-name/
2251 if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
2252 $product_slug = $matches[1];
2253 }
2254 // Handle query parameters: ?product=product-name
2255 elseif (preg_match('/[\?&]product=([^&]+)/', $url, $matches)) {
2256 $product_slug = $matches[1];
2257 }
2258
2259 if (!empty($product_slug)) {
2260 // Look up product by slug
2261 $product = get_page_by_path($product_slug, OBJECT, 'product');
2262 if ($product) {
2263 return $product->ID;
2264 }
2265
2266 // Alternative method: query by post_name
2267 global $wpdb;
2268 $post_id = $wpdb->get_var($wpdb->prepare(
2269 "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'product' AND post_status = 'publish'",
2270 $product_slug
2271 ));
2272
2273 if ($post_id) {
2274 return intval($post_id);
2275 }
2276 }
2277 }
2278
2279 // ADDITIONAL FIX: Try direct database lookup by URL variations
2280 global $wpdb;
2281 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2282
2283 // Try exact match first
2284 $existing_record = $wpdb->get_row($wpdb->prepare(
2285 "SELECT id, source_url FROM $table_name WHERE source_url = %s",
2286 $url
2287 ));
2288
2289 if ($existing_record) {
2290 // Found exact match, now convert the URL to post ID
2291 $post_id = url_to_postid($existing_record->source_url);
2292 if ($post_id > 0) {
2293 return $post_id;
2294 }
2295 }
2296
2297 // Try variations of the URL (with/without trailing slash, http/https)
2298 $url_variations = array(
2299 rtrim($url, '/'),
2300 $url . '/',
2301 str_replace('http://', 'https://', $url),
2302 str_replace('https://', 'http://', $url),
2303 str_replace('http://', 'https://', rtrim($url, '/')),
2304 str_replace('https://', 'http://', rtrim($url, '/'))
2305 );
2306
2307 foreach ($url_variations as $variation) {
2308 $existing_record = $wpdb->get_row($wpdb->prepare(
2309 "SELECT id, source_url FROM $table_name WHERE source_url = %s",
2310 $variation
2311 ));
2312
2313 if ($existing_record) {
2314 $post_id = url_to_postid($existing_record->source_url);
2315 if ($post_id > 0) {
2316 return $post_id;
2317 }
2318 }
2319 }
2320
2321 // Last resort: try to match against all published products by URL if WooCommerce is active
2322 if (function_exists('wc_get_products')) {
2323 // Get all published products (limited to avoid memory issues)
2324 $products = wc_get_products(array(
2325 'status' => 'publish',
2326 'limit' => 1000, // Reasonable limit
2327 'return' => 'ids'
2328 ));
2329
2330 foreach ($products as $product_id) {
2331 $product_url = get_permalink($product_id);
2332
2333 // Compare cleaned URLs
2334 $clean_product_url = rtrim($product_url, '/');
2335 $clean_product_url = strtok($clean_product_url, '?');
2336
2337 if ($clean_url === $clean_product_url) {
2338 return $product_id;
2339 }
2340
2341 // Also check if any of our URL variations match
2342 foreach ($url_variations as $variation) {
2343 $clean_variation = rtrim($variation, '/');
2344 $clean_variation = strtok($clean_variation, '?');
2345
2346 if ($clean_variation === $clean_product_url) {
2347 return $product_id;
2348 }
2349 }
2350 }
2351 }
2352
2353 return 0; // No match found
2354 }
2355
2356 public function ajax_mxchat_process_selected_content() {
2357 // Basic request validation
2358 if (!check_ajax_referer('mxchat_content_selector_nonce', 'nonce', false)) {
2359 wp_send_json_error('Invalid nonce');
2360 exit;
2361 }
2362
2363 if (!current_user_can('manage_options')) {
2364 wp_send_json_error('Unauthorized access');
2365 exit;
2366 }
2367
2368 // Get post IDs - safely parse the array
2369 $post_ids = array();
2370 if (isset($_POST['post_ids']) && is_array($_POST['post_ids'])) {
2371 foreach ($_POST['post_ids'] as $id) {
2372 $post_ids[] = absint($id);
2373 }
2374 }
2375
2376 if (empty($post_ids)) {
2377 wp_send_json_error('No content selected');
2378 exit;
2379 }
2380
2381 // Process only ONE post at a time to avoid request size issues
2382 $post_id = reset($post_ids);
2383 $post = get_post($post_id);
2384
2385 if (!$post) {
2386 wp_send_json_error('Post not found');
2387 exit;
2388 }
2389
2390 // Get content including ACF fields
2391 $content = $post->post_title . "\n\n" . wp_strip_all_tags($post->post_content);
2392
2393 // ADD ACF FIELDS SUPPORT
2394 $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
2395 if (!empty($acf_fields)) {
2396 $acf_content_parts = array();
2397
2398 foreach ($acf_fields as $field_name => $field_value) {
2399 $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
2400
2401 if (!empty($formatted_value)) {
2402 // Convert field name to readable label
2403 $field_label = ucwords(str_replace('_', ' ', $field_name));
2404 $acf_content_parts[] = $field_label . ": " . $formatted_value;
2405 }
2406 }
2407
2408 if (!empty($acf_content_parts)) {
2409 $content .= "\n\n" . implode("\n", $acf_content_parts);
2410 }
2411 }
2412
2413 $content = substr($content, 0, 10000); // Limit content size
2414
2415 // Get API key with proper model detection
2416 $options = get_option('mxchat_options');
2417 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
2418
2419 if (strpos($selected_model, 'voyage') === 0) {
2420 $api_key = $options['voyage_api_key'] ?? '';
2421 $provider_name = 'Voyage AI';
2422 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2423 $api_key = $options['gemini_api_key'] ?? '';
2424 $provider_name = 'Google Gemini';
2425 } else {
2426 $api_key = $options['api_key'] ?? '';
2427 $provider_name = 'OpenAI';
2428 }
2429
2430 if (empty($api_key)) {
2431 wp_send_json_error($provider_name . ' API key not configured');
2432 exit;
2433 }
2434
2435 $source_url = get_permalink($post_id);
2436 $vector_id = md5($source_url); // Vector ID for Pinecone
2437
2438 // Check for existing content in ONLY the active storage method
2439 $is_update = false;
2440
2441 // Check if Pinecone is enabled
2442 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
2443 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2444
2445 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
2446 // ONLY check Pinecone if it's enabled
2447 $pinecone_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
2448 if (isset($pinecone_data[$post_id])) {
2449 $is_update = true;
2450 }
2451 } else {
2452 // ONLY check WordPress DB if Pinecone is not enabled
2453 global $wpdb;
2454 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2455 $existing_record = $wpdb->get_row($wpdb->prepare(
2456 "SELECT id FROM $table_name WHERE source_url = %s",
2457 $source_url
2458 ));
2459
2460 if ($existing_record) {
2461 $is_update = true;
2462 }
2463 }
2464
2465 // Use the centralized utility function for storage
2466 $result = MxChat_Utils::submit_content_to_db(
2467 $content,
2468 $source_url,
2469 $api_key,
2470 $vector_id
2471 );
2472
2473 if (is_wp_error($result)) {
2474 wp_send_json_error('Storage failed: ' . $result->get_error_message());
2475 exit;
2476 }
2477
2478 // Update caches if Pinecone is enabled
2479 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
2480 // Update vector ID cache for improved fetching
2481 $this->mxchat_update_pinecone_vector_cache($vector_id);
2482
2483 // Update local processed content cache for immediate UI feedback
2484 $pinecone_cache = get_option('mxchat_pinecone_processed_cache', array());
2485 $pinecone_cache[$post_id] = array(
2486 'db_id' => $vector_id,
2487 'processed_date' => 'Just now',
2488 'url' => $source_url,
2489 'source' => 'pinecone',
2490 'timestamp' => current_time('timestamp')
2491 );
2492 update_option('mxchat_pinecone_processed_cache', $pinecone_cache);
2493
2494 // Also update the general processed content cache
2495 $processed_cache = get_option('mxchat_processed_content_cache', array());
2496 $processed_cache[$post_id] = array(
2497 'db_id' => $vector_id,
2498 'timestamp' => current_time('timestamp'),
2499 'url' => $source_url,
2500 'source' => 'pinecone'
2501 );
2502 update_option('mxchat_processed_content_cache', $processed_cache);
2503 }
2504
2505 $operation_type = $is_update ? 'update' : 'new';
2506
2507 // Count ACF fields for debugging
2508 $acf_field_count = count($acf_fields);
2509
2510 // Success response with minimal data
2511 wp_send_json_success(array(
2512 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully',
2513 'post_id' => $post_id,
2514 'title' => $post->post_title,
2515 'operation_type' => $operation_type,
2516 'vector_id' => $vector_id,
2517 'cache_updated' => $use_pinecone,
2518 'acf_fields_found' => $acf_field_count,
2519 'content_preview' => substr($content, 0, 100) . '...'
2520 ));
2521 exit;
2522 }
2523
2524
2525
2526 /**
2527 * Updates cache with new vector ID if absent
2528 */
2529 public function mxchat_update_pinecone_vector_cache($vector_id) {
2530 $cached_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
2531 if (!in_array($vector_id, $cached_ids)) {
2532 $cached_ids[] = $vector_id;
2533 update_option('mxchat_pinecone_vector_ids_cache', $cached_ids);
2534 }
2535 }
2536 public function mxchat_get_public_post_types() {
2537 $post_types = get_post_types(array('public' => true), 'objects');
2538 $post_type_options = array();
2539
2540 foreach ($post_types as $post_type) {
2541 $post_type_options[$post_type->name] = $post_type->label;
2542 }
2543
2544 return $post_type_options;
2545 }
2546 public function mxchat_get_pinecone_processed_content($pinecone_options) {
2547 //error_log('=== DEBUG: Starting mxchat_get_pinecone_processed_content ===');
2548
2549 // First check local cache for immediate updates
2550 $cached_data = get_option('mxchat_pinecone_processed_cache', array());
2551 //error_log('DEBUG: Found ' . count($cached_data) . ' items in local cache');
2552
2553 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2554 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2555
2556 //error_log('DEBUG: API key present: ' . (!empty($api_key) ? 'YES' : 'NO'));
2557 //error_log('DEBUG: Host: ' . $host);
2558
2559 if (empty($api_key) || empty($host)) {
2560 //error_log('DEBUG: Missing API credentials, returning cached data only');
2561 return $cached_data;
2562 }
2563
2564 $pinecone_data = array();
2565
2566 try {
2567 // Method 1: Try to get vectors using cached vector IDs first
2568 $cached_vector_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
2569 //error_log('DEBUG: Found ' . count($cached_vector_ids) . ' cached vector IDs');
2570
2571 if (!empty($cached_vector_ids)) {
2572 //error_log('DEBUG: Trying to fetch by cached vector IDs...');
2573 $pinecone_data = $this->mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $cached_vector_ids);
2574 //error_log('DEBUG: Fetch by IDs returned ' . count($pinecone_data) . ' items');
2575 }
2576
2577 // Method 2: If no cached IDs or fetch failed, use scanning approach
2578 if (empty($pinecone_data)) {
2579 //error_log('DEBUG: Trying scanning approach...');
2580 $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
2581 //error_log('DEBUG: Scanning returned ' . count($pinecone_data) . ' items');
2582 }
2583
2584 // Method 3: Final fallback - try stats endpoint
2585 if (empty($pinecone_data)) {
2586 //error_log('DEBUG: Trying stats endpoint...');
2587 $stats_url = "https://{$host}/describe_index_stats";
2588
2589 $response = wp_remote_post($stats_url, array(
2590 'headers' => array(
2591 'Api-Key' => $api_key,
2592 'Content-Type' => 'application/json'
2593 ),
2594 'body' => json_encode(array()),
2595 'timeout' => 30
2596 ));
2597
2598 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2599 $body = wp_remote_retrieve_body($response);
2600 $stats_data = json_decode($body, true);
2601 //error_log('DEBUG: Pinecone stats: ' . print_r($stats_data, true));
2602 } else {
2603 if (is_wp_error($response)) {
2604 //error_log('DEBUG: Stats endpoint error: ' . $response->get_error_message());
2605 } else {
2606 //error_log('DEBUG: Stats endpoint failed with code: ' . wp_remote_retrieve_response_code($response));
2607 }
2608 }
2609 }
2610
2611 } catch (Exception $e) {
2612 //error_log('DEBUG: Exception in get_pinecone_processed_content: ' . $e->getMessage());
2613 }
2614
2615 // Merge cached data with Pinecone data
2616 $merged_data = $pinecone_data;
2617
2618 foreach ($cached_data as $post_id => $cache_item) {
2619 $cache_timestamp = $cache_item['timestamp'] ?? 0;
2620 $time_diff = current_time('timestamp') - $cache_timestamp;
2621
2622 if ($time_diff < 300) { // 5 minutes = 300 seconds
2623 $merged_data[$post_id] = $cache_item;
2624 } else {
2625 if (!isset($merged_data[$post_id])) {
2626 $merged_data[$post_id] = $cache_item;
2627 }
2628 }
2629 }
2630
2631 //error_log('DEBUG: Final merged data count: ' . count($merged_data));
2632 //error_log('=== DEBUG: End mxchat_get_pinecone_processed_content ===');
2633
2634 return $merged_data;
2635 }
2636
2637 public function mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
2638 //error_log('=== DEBUG: Starting mxchat_fetch_pinecone_vectors_by_ids ===');
2639
2640 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2641 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2642
2643 if (empty($api_key) || empty($host) || empty($vector_ids)) {
2644 //error_log('DEBUG: Missing parameters for fetch by IDs');
2645 return array();
2646 }
2647
2648 try {
2649 $fetch_url = "https://{$host}/vectors/fetch";
2650 //error_log('DEBUG: Fetch URL: ' . $fetch_url);
2651 //error_log('DEBUG: Fetching ' . count($vector_ids) . ' vector IDs');
2652
2653 // Pinecone fetch API allows fetching specific vectors by ID
2654 $fetch_data = array(
2655 'ids' => array_values($vector_ids)
2656 );
2657
2658 $response = wp_remote_post($fetch_url, array(
2659 'headers' => array(
2660 'Api-Key' => $api_key,
2661 'Content-Type' => 'application/json'
2662 ),
2663 'body' => json_encode($fetch_data),
2664 'timeout' => 30
2665 ));
2666
2667 if (is_wp_error($response)) {
2668 //error_log('DEBUG: Fetch by IDs WP error: ' . $response->get_error_message());
2669 return array();
2670 }
2671
2672 $response_code = wp_remote_retrieve_response_code($response);
2673 //error_log('DEBUG: Fetch response code: ' . $response_code);
2674
2675 if ($response_code !== 200) {
2676 $error_body = wp_remote_retrieve_body($response);
2677 //error_log('DEBUG: Fetch failed with body: ' . $error_body);
2678 return array();
2679 }
2680
2681 $body = wp_remote_retrieve_body($response);
2682 $data = json_decode($body, true);
2683
2684 //error_log('DEBUG: Fetch response structure: ' . print_r(array_keys($data), true));
2685
2686 if (!isset($data['vectors'])) {
2687 //error_log('DEBUG: No vectors key in response');
2688 return array();
2689 }
2690
2691 $processed_data = array();
2692
2693 foreach ($data['vectors'] as $vector_id => $vector_data) {
2694 $metadata = $vector_data['metadata'] ?? array();
2695 $source_url = $metadata['source_url'] ?? '';
2696
2697 if (!empty($source_url)) {
2698 $post_id = url_to_postid($source_url);
2699 if ($post_id) {
2700 $created_at = $metadata['created_at'] ?? '';
2701 $processed_date = 'Recently';
2702
2703 if (!empty($created_at)) {
2704 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
2705 if ($timestamp) {
2706 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2707 }
2708 }
2709
2710 $processed_data[$post_id] = array(
2711 'db_id' => $vector_id,
2712 'processed_date' => $processed_date,
2713 'url' => $source_url,
2714 'source' => 'pinecone',
2715 'timestamp' => $timestamp ?? current_time('timestamp')
2716 );
2717 }
2718 }
2719 }
2720
2721 //error_log('DEBUG: Processed ' . count($processed_data) . ' vectors from fetch');
2722 return $processed_data;
2723
2724 } catch (Exception $e) {
2725 //error_log('DEBUG: Exception in fetch_pinecone_vectors_by_ids: ' . $e->getMessage());
2726 return array();
2727 }
2728 }
2729
2730 public function mxchat_scan_pinecone_for_processed_content($pinecone_options) {
2731 //error_log('=== DEBUG: Starting mxchat_scan_pinecone_for_processed_content ===');
2732
2733 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2734 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2735
2736 if (empty($api_key) || empty($host)) {
2737 //error_log('DEBUG: Missing API credentials for scanning');
2738 return array();
2739 }
2740
2741 try {
2742 // Use multiple random vectors to get better coverage
2743 $all_matches = array();
2744 $seen_ids = array();
2745
2746 // Try 3 different random vectors to get better coverage
2747 for ($i = 0; $i < 3; $i++) {
2748 //error_log('DEBUG: Scanning attempt ' . ($i + 1) . '/3');
2749
2750 $query_url = "https://{$host}/query";
2751
2752 // Generate a random unit vector instead of zeros
2753 $random_vector = array();
2754 for ($j = 0; $j < 1536; $j++) {
2755 $random_vector[] = (rand(-1000, 1000) / 1000.0);
2756 }
2757
2758 // Normalize the vector to unit length
2759 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
2760 if ($magnitude > 0) {
2761 $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
2762 }
2763
2764 $query_data = array(
2765 'includeMetadata' => true,
2766 'includeValues' => false,
2767 'topK' => 10000,
2768 'vector' => $random_vector
2769 );
2770
2771 $response = wp_remote_post($query_url, array(
2772 'headers' => array(
2773 'Api-Key' => $api_key,
2774 'Content-Type' => 'application/json'
2775 ),
2776 'body' => json_encode($query_data),
2777 'timeout' => 30
2778 ));
2779
2780 if (is_wp_error($response)) {
2781 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' WP error: ' . $response->get_error_message());
2782 continue;
2783 }
2784
2785 $response_code = wp_remote_retrieve_response_code($response);
2786 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' response code: ' . $response_code);
2787
2788 if ($response_code !== 200) {
2789 $error_body = wp_remote_retrieve_body($response);
2790 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' failed with body: ' . substr($error_body, 0, 500));
2791 continue;
2792 }
2793
2794 $body = wp_remote_retrieve_body($response);
2795 $data = json_decode($body, true);
2796
2797 if (isset($data['matches'])) {
2798 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' returned ' . count($data['matches']) . ' matches');
2799 foreach ($data['matches'] as $match) {
2800 $match_id = $match['id'] ?? '';
2801 if (!empty($match_id) && !isset($seen_ids[$match_id])) {
2802 $all_matches[] = $match;
2803 $seen_ids[$match_id] = true;
2804 }
2805 }
2806 } else {
2807 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' - no matches key in response');
2808 }
2809 }
2810
2811 //error_log('DEBUG: Total unique matches found: ' . count($all_matches));
2812
2813 // Convert matches to processed data format
2814 $processed_data = array();
2815 $vector_ids_for_cache = array();
2816
2817 foreach ($all_matches as $match) {
2818 $metadata = $match['metadata'] ?? array();
2819 $source_url = $metadata['source_url'] ?? '';
2820 $match_id = $match['id'] ?? '';
2821
2822 if (!empty($source_url) && !empty($match_id)) {
2823 $post_id = url_to_postid($source_url);
2824 if ($post_id) {
2825 $created_at = $metadata['created_at'] ?? '';
2826 $processed_date = 'Recently';
2827
2828 if (!empty($created_at)) {
2829 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
2830 if ($timestamp) {
2831 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2832 }
2833 }
2834
2835 $processed_data[$post_id] = array(
2836 'db_id' => $match_id,
2837 'processed_date' => $processed_date,
2838 'url' => $source_url,
2839 'source' => 'pinecone',
2840 'timestamp' => $timestamp ?? current_time('timestamp')
2841 );
2842
2843 $vector_ids_for_cache[] = $match_id;
2844 }
2845 }
2846 }
2847
2848 // Update the vector IDs cache for future use
2849 if (!empty($vector_ids_for_cache)) {
2850 update_option('mxchat_pinecone_vector_ids_cache', $vector_ids_for_cache);
2851 //error_log('DEBUG: Updated vector IDs cache with ' . count($vector_ids_for_cache) . ' IDs');
2852 }
2853
2854 //error_log('DEBUG: Returning ' . count($processed_data) . ' processed items from scanning');
2855 return $processed_data;
2856
2857 } catch (Exception $e) {
2858 //error_log('DEBUG: Exception in scan_pinecone_for_processed_content: ' . $e->getMessage());
2859 return array();
2860 }
2861 }
2862
2863 /**
2864 * Generates embeddings from input text for MXChat
2865 */
2866 private function mxchat_generate_embedding($text) {
2867 // Enable detailed logging for debugging
2868 //error_log('[MXCHAT-EMBED] Starting embedding generation. Text length: ' . strlen($text) . ' bytes');
2869 //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
2870
2871 $options = get_option('mxchat_options');
2872 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
2873 //error_log('[MXCHAT-EMBED] Selected embedding model: ' . $selected_model);
2874
2875 // Determine provider and endpoint
2876 if (strpos($selected_model, 'voyage') === 0) {
2877 $api_key = $options['voyage_api_key'] ?? '';
2878 $endpoint = 'https://api.voyageai.com/v1/embeddings';
2879 $provider_name = 'Voyage AI';
2880 //error_log('[MXCHAT-EMBED] Using Voyage AI API');
2881 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2882 $api_key = $options['gemini_api_key'] ?? '';
2883 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
2884 $provider_name = 'Google Gemini';
2885 //error_log('[MXCHAT-EMBED] Using Google Gemini API');
2886 } else {
2887 $api_key = $options['api_key'] ?? '';
2888 $endpoint = 'https://api.openai.com/v1/embeddings';
2889 $provider_name = 'OpenAI';
2890 //error_log('[MXCHAT-EMBED] Using OpenAI API');
2891 }
2892
2893 //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
2894
2895 if (empty($api_key)) {
2896 $error_message = sprintf('Missing %s API key. Please configure your API key in the MxChat settings.', $provider_name);
2897 //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
2898 return $error_message;
2899 }
2900
2901 // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
2902 $estimated_tokens = ceil(str_word_count($text) / 0.75);
2903 //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
2904
2905 if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
2906 //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
2907 // Consider truncating text here
2908 }
2909
2910 // Prepare request body based on provider
2911 if (strpos($selected_model, 'gemini-embedding') === 0) {
2912 // Gemini API format
2913 $request_body = array(
2914 'model' => 'models/' . $selected_model,
2915 'content' => array(
2916 'parts' => array(
2917 array('text' => $text)
2918 )
2919 )
2920 );
2921
2922 // Set output dimensionality to 1536 for consistency with other models
2923 $request_body['outputDimensionality'] = 1536;
2924 } else {
2925 // OpenAI/Voyage API format
2926 $request_body = array(
2927 'model' => $selected_model,
2928 'input' => $text
2929 );
2930
2931 // Add output_dimension for voyage-3-large model
2932 if ($selected_model === 'voyage-3-large') {
2933 $request_body['output_dimension'] = 2048;
2934 }
2935 }
2936
2937 //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
2938
2939 // Prepare headers based on provider
2940 if (strpos($selected_model, 'gemini-embedding') === 0) {
2941 // Gemini uses API key as query parameter
2942 $endpoint .= '?key=' . $api_key;
2943 $headers = array(
2944 'Content-Type' => 'application/json'
2945 );
2946 } else {
2947 // OpenAI/Voyage use Bearer token
2948 $headers = array(
2949 'Authorization' => 'Bearer ' . $api_key,
2950 'Content-Type' => 'application/json'
2951 );
2952 }
2953
2954 // Make API request
2955 //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
2956 $response = wp_remote_post($endpoint, array(
2957 'body' => wp_json_encode($request_body),
2958 'headers' => $headers,
2959 'timeout' => 60 // Increased timeout for large inputs
2960 ));
2961
2962 // Handle wp_remote_post errors
2963 if (is_wp_error($response)) {
2964 $error_message = $response->get_error_message();
2965 //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
2966 return 'Connection error: ' . $error_message;
2967 }
2968
2969 // Get and check HTTP response code
2970 $http_code = wp_remote_retrieve_response_code($response);
2971 //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
2972
2973 if ($http_code !== 200) {
2974 $error_body = wp_remote_retrieve_body($response);
2975 //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
2976
2977 // Try to parse error for more details
2978 $error_json = json_decode($error_body, true);
2979 if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
2980 $error_type = $error_json['error']['type'] ?? 'unknown';
2981 $error_message = $error_json['error']['message'] ?? 'No message';
2982 //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
2983 //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
2984
2985 // Customize error message for common API errors
2986 if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
2987 $error_message = sprintf('Invalid %s API key. Please check your API key in the MxChat settings.', $provider_name);
2988 } elseif ($error_type === 'authentication_error') {
2989 $error_message = sprintf('%s authentication failed. Please verify your API key in the MxChat settings.', $provider_name);
2990 }
2991
2992 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
2993 return $error_message;
2994 }
2995
2996 $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding", $http_code);
2997 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
2998 return $error_message;
2999 }
3000
3001 // Parse response body
3002 $response_body = wp_remote_retrieve_body($response);
3003 //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
3004
3005 $response_data = json_decode($response_body, true);
3006
3007 if (json_last_error() !== JSON_ERROR_NONE) {
3008 $error = json_last_error_msg();
3009 //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
3010 //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
3011 return "Failed to parse API response: $error";
3012 }
3013
3014 // Handle different response formats based on provider
3015 if (strpos($selected_model, 'gemini-embedding') === 0) {
3016 // Gemini API response format
3017 if (isset($response_data['embedding']['values'])) {
3018 $embedding_dimensions = count($response_data['embedding']['values']);
3019 //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
3020
3021 // Check if embedding dimensions are as expected (should be 1536)
3022 if ($embedding_dimensions !== 1536) {
3023 //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
3024 }
3025
3026 return $response_data['embedding']['values'];
3027 } else {
3028 //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
3029 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
3030
3031 if (isset($response_data['error'])) {
3032 $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
3033 //error_log('[MXCHAT-EMBED] ' . $error_message);
3034 return $error_message;
3035 }
3036
3037 $error_message = "Invalid Gemini API response format: No embedding found";
3038 //error_log('[MXCHAT-EMBED] ' . $error_message);
3039 return $error_message;
3040 }
3041 } else {
3042 // OpenAI/Voyage API response format
3043 if (isset($response_data['data'][0]['embedding'])) {
3044 $embedding_dimensions = count($response_data['data'][0]['embedding']);
3045 //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
3046
3047 // Check if embedding dimensions are as expected
3048 if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
3049 ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
3050 //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
3051 }
3052
3053 return $response_data['data'][0]['embedding'];
3054 } else {
3055 //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
3056 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
3057
3058 if (isset($response_data['error'])) {
3059 $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
3060 //error_log('[MXCHAT-EMBED] ' . $error_message);
3061 return $error_message;
3062 }
3063
3064 $error_message = "Invalid API response format: No embedding found";
3065 //error_log('[MXCHAT-EMBED] ' . $error_message);
3066 return $error_message;
3067 }
3068 }
3069 }
3070 public function mxchat_ajax_dismiss_completed_status() {
3071 try {
3072 // Verify the request
3073 check_ajax_referer('mxchat_status_nonce', 'nonce');
3074
3075 if (!current_user_can('manage_options')) {
3076 wp_send_json_error('Unauthorized access');
3077 exit;
3078 }
3079
3080 $card_type = isset($_POST['card_type']) ? sanitize_text_field($_POST['card_type']) : '';
3081
3082 if ($card_type === 'pdf') {
3083 // Clear PDF status
3084 $pdf_url = get_transient('mxchat_last_pdf_url');
3085 if ($pdf_url) {
3086 delete_transient('mxchat_pdf_status_' . md5($pdf_url));
3087 delete_transient('mxchat_last_pdf_url');
3088 }
3089 } elseif ($card_type === 'sitemap') {
3090 // Clear sitemap status
3091 $sitemap_url = get_transient('mxchat_last_sitemap_url');
3092 if ($sitemap_url) {
3093 delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
3094 delete_transient('mxchat_last_sitemap_url');
3095 }
3096 }
3097
3098 wp_send_json_success(array('message' => 'Status dismissed successfully'));
3099
3100 } catch (Exception $e) {
3101 wp_send_json_error(array('message' => 'Error dismissing status: ' . $e->getMessage()));
3102 }
3103 }
3104
3105 /**
3106 * Render completed status cards on page load
3107 * This ensures completed processing status persists through page refreshes
3108 */
3109 public function mxchat_render_completed_status_cards() {
3110 $output = '';
3111
3112 // Check for completed PDF status
3113 $pdf_url = get_transient('mxchat_last_pdf_url');
3114 if ($pdf_url) {
3115 $pdf_status = $this->mxchat_get_pdf_processing_status($pdf_url);
3116 if ($pdf_status && ($pdf_status['status'] === 'complete' || $pdf_status['status'] === 'error')) {
3117 $output .= $this->mxchat_render_pdf_status_card($pdf_status, $pdf_url);
3118 }
3119 }
3120
3121 // Check for completed sitemap status
3122 $sitemap_url = get_transient('mxchat_last_sitemap_url');
3123 if ($sitemap_url) {
3124 $sitemap_status = $this->mxchat_get_sitemap_processing_status($sitemap_url);
3125 if ($sitemap_status && ($sitemap_status['status'] === 'complete' || $sitemap_status['status'] === 'error')) {
3126 $output .= $this->mxchat_render_sitemap_status_card($sitemap_status, $sitemap_url);
3127 }
3128 }
3129
3130 return $output;
3131 }
3132
3133 /**
3134 * Render PDF status card HTML
3135 */
3136 private function mxchat_render_pdf_status_card($status, $pdf_url) {
3137 $html = '<div class="mxchat-status-card" data-card-type="pdf">';
3138 $html .= '<div class="mxchat-status-header">';
3139 $html .= '<h4>' . esc_html__('PDF Processing Status', 'mxchat') . '</h4>';
3140
3141 // Add dismiss button for completed status
3142 if ($status['status'] === 'complete' || $status['status'] === 'error') {
3143 $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
3144 }
3145
3146 // Process Batch button for processing status
3147 if ($status['status'] === 'processing') {
3148 $html .= '<button type="button" class="mxchat-manual-batch-btn"
3149 data-process-type="pdf"
3150 data-url="' . esc_attr($pdf_url) . '">
3151 ' . esc_html__('Process Batch', 'mxchat') . '</button>';
3152 }
3153
3154 // Add status badges
3155 if ($status['status'] === 'error') {
3156 $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
3157 } elseif ($status['status'] === 'complete') {
3158 if ($status['failed_pages'] > 0) {
3159 $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
3160 sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_pages']) . '</span>';
3161 } else {
3162 $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
3163 }
3164 }
3165
3166 $html .= '</div>'; // End header
3167
3168 // Progress bar
3169 $html .= '<div class="mxchat-progress-bar">';
3170 $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
3171 $html .= '</div>';
3172
3173 // Status details
3174 $html .= '<div class="mxchat-status-details">';
3175 $html .= '<p>' . sprintf(
3176 esc_html__('Progress: %d of %d pages (%d%%)', 'mxchat'),
3177 $status['processed_pages'],
3178 $status['total_pages'],
3179 $status['percentage']
3180 ) . '</p>';
3181
3182 // Show failed pages count if any
3183 if ($status['failed_pages'] > 0) {
3184 $html .= '<p><strong>' . esc_html__('Failed pages:', 'mxchat') . '</strong> ' . esc_html($status['failed_pages']) . '</p>';
3185 }
3186
3187 $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
3188 $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
3189
3190 // Add completion summary if available AND it's an array
3191 if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
3192 $summary = $status['completion_summary'];
3193 $html .= '<div class="mxchat-completion-summary">';
3194 $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
3195 $html .= '<p><strong>' . esc_html__('Total Pages:', 'mxchat') . '</strong> ' . esc_html($summary['total_pages']) . '</p>';
3196 $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_pages']) . '</p>';
3197 $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_pages']) . '</p>';
3198 $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
3199 $html .= '</div>';
3200 }
3201
3202 // Add failed pages list if any AND it's an array
3203 if (isset($status['failed_pages_list']) && is_array($status['failed_pages_list']) && !empty($status['failed_pages_list'])) {
3204 $html .= $this->mxchat_render_failed_pages_list($status['failed_pages_list']);
3205 }
3206
3207 // Add error message if any
3208 if (isset($status['error']) && !empty($status['error'])) {
3209 $html .= '<div class="mxchat-error-notice">';
3210 $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
3211 $html .= '</div>';
3212 }
3213
3214 $html .= '</div>'; // End details
3215 $html .= '</div>'; // End card
3216
3217 return $html;
3218 }
3219 /**
3220 * Render sitemap status card HTML
3221 */
3222 private function mxchat_render_sitemap_status_card($status, $sitemap_url) {
3223 $html = '<div class="mxchat-status-card" data-card-type="sitemap">';
3224 $html .= '<div class="mxchat-status-header">';
3225 $html .= '<h4>' . esc_html__('Sitemap Processing Status', 'mxchat') . '</h4>';
3226
3227 // Add dismiss button for completed status
3228 if ($status['status'] === 'complete' || $status['status'] === 'error') {
3229 $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
3230 }
3231
3232 // Process Batch button for processing status
3233 if ($status['status'] === 'processing') {
3234 $html .= '<button type="button" class="mxchat-manual-batch-btn"
3235 data-process-type="sitemap"
3236 data-url="' . esc_attr($sitemap_url) . '">
3237 ' . esc_html__('Process Batch', 'mxchat') . '</button>';
3238 }
3239
3240 // Add status badges
3241 if ($status['status'] === 'error') {
3242 $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
3243 } elseif ($status['status'] === 'complete') {
3244 if ($status['failed_urls'] > 0) {
3245 $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
3246 sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_urls']) . '</span>';
3247 } else {
3248 $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
3249 }
3250 }
3251
3252 $html .= '</div>'; // End header
3253
3254 // Progress bar
3255 $html .= '<div class="mxchat-progress-bar">';
3256 $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
3257 $html .= '</div>';
3258
3259 // Status details
3260 $html .= '<div class="mxchat-status-details">';
3261 $html .= '<p>' . sprintf(
3262 esc_html__('Progress: %d of %d URLs (%d%%)', 'mxchat'),
3263 $status['processed_urls'],
3264 $status['total_urls'],
3265 $status['percentage']
3266 ) . '</p>';
3267
3268 // Show failed URLs count if any
3269 if ($status['failed_urls'] > 0) {
3270 $html .= '<p><strong>' . esc_html__('Failed URLs:', 'mxchat') . '</strong> ' . esc_html($status['failed_urls']) . '</p>';
3271 }
3272
3273 $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
3274 $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
3275
3276 // Add completion summary if available AND it's an array
3277 if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
3278 $summary = $status['completion_summary'];
3279 $html .= '<div class="mxchat-completion-summary">';
3280 $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
3281 $html .= '<p><strong>' . esc_html__('Total URLs:', 'mxchat') . '</strong> ' . esc_html($summary['total_urls']) . '</p>';
3282 $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_urls']) . '</p>';
3283 $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_urls']) . '</p>';
3284 $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
3285 $html .= '</div>';
3286 }
3287
3288 // Add error messages if any (but not the failed URLs list)
3289 if (!empty($status['error']) || !empty($status['last_error'])) {
3290 $html .= '<div class="mxchat-error-notice">';
3291
3292 if (!empty($status['error'])) {
3293 $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
3294 }
3295
3296 if (!empty($status['last_error'])) {
3297 $html .= '<p class="last-error">' . esc_html__('Last error:', 'mxchat') . ' ' . esc_html($status['last_error']) . '</p>';
3298 }
3299
3300 $html .= '</div>';
3301 }
3302
3303 $html .= '</div>'; // End details
3304 $html .= '</div>'; // End card
3305
3306 return $html;
3307 }
3308
3309
3310 /**
3311 * Render failed pages list
3312 */
3313 private function mxchat_render_failed_pages_list($failed_pages_list) {
3314 // Validate that $failed_pages_list is an array and not empty
3315 if (!is_array($failed_pages_list) || empty($failed_pages_list)) {
3316 return '';
3317 }
3318
3319 $html = '<div class="mxchat-error-notice">';
3320 $html .= '<div class="mxchat-failed-pages-container">';
3321 $html .= '<h5>' . sprintf(esc_html__('Failed Pages (%d)', 'mxchat'), count($failed_pages_list)) . '</h5>';
3322 $html .= '<details>';
3323 $html .= '<summary>' . esc_html__('Show Failed Pages', 'mxchat') . '</summary>';
3324 $html .= '<div class="mxchat-failed-pages-list">';
3325
3326 // Create table for failed pages
3327 $html .= '<table class="widefat striped">';
3328 $html .= '<thead><tr>';
3329 $html .= '<th>' . esc_html__('Page', 'mxchat') . '</th>';
3330 $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
3331 $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
3332 $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
3333 $html .= '</tr></thead><tbody>';
3334
3335 // Sort failed pages by most recent
3336 $sorted_failed_pages = $failed_pages_list;
3337 usort($sorted_failed_pages, function($a, $b) {
3338 return ($b['time'] ?? 0) - ($a['time'] ?? 0);
3339 });
3340
3341 foreach ($sorted_failed_pages as $item) {
3342 // Ensure $item is an array before accessing its elements
3343 if (!is_array($item)) {
3344 continue;
3345 }
3346
3347 $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
3348 $html .= '<tr>';
3349 $html .= '<td>' . esc_html__('Page', 'mxchat') . ' ' . esc_html($item['page'] ?? 'Unknown') . '</td>';
3350 $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
3351 $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
3352 $html .= '<td>' . esc_html($time_ago) . '</td>';
3353 $html .= '</tr>';
3354 }
3355
3356 $html .= '</tbody></table>';
3357 $html .= '</div></details></div></div>';
3358
3359 return $html;
3360 }
3361
3362 /**
3363 * Render failed URLs list
3364 */
3365 private function mxchat_render_failed_urls_list($failed_urls_list) {
3366 // Validate that $failed_urls_list is an array and not empty
3367 if (!is_array($failed_urls_list) || empty($failed_urls_list)) {
3368 return '';
3369 }
3370
3371 $html = '<div class="mxchat-failed-urls-container">';
3372 $html .= '<h5>' . sprintf(esc_html__('Failed URLs (%d)', 'mxchat'), count($failed_urls_list)) . '</h5>';
3373 $html .= '<details>';
3374 $html .= '<summary>' . esc_html__('Show Failed URLs', 'mxchat') . '</summary>';
3375 $html .= '<div class="mxchat-failed-urls-list">';
3376
3377 // Create table for failed URLs
3378 $html .= '<table class="widefat striped">';
3379 $html .= '<thead><tr>';
3380 $html .= '<th>' . esc_html__('URL', 'mxchat') . '</th>';
3381 $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
3382 $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
3383 $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
3384 $html .= '</tr></thead><tbody>';
3385
3386 // Sort failed URLs by most recent
3387 $sorted_failed_urls = $failed_urls_list;
3388 usort($sorted_failed_urls, function($a, $b) {
3389 return ($b['time'] ?? 0) - ($a['time'] ?? 0);
3390 });
3391
3392 // Show up to 50 failed URLs
3393 $display_urls = array_slice($sorted_failed_urls, 0, 50);
3394
3395 foreach ($display_urls as $item) {
3396 // Ensure $item is an array before accessing its elements
3397 if (!is_array($item)) {
3398 continue;
3399 }
3400
3401 $url = $item['url'] ?? '';
3402 $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
3403
3404 // Truncate URL for display
3405 $display_url = strlen($url) > 50 ? substr($url, 0, 47) . '...' : $url;
3406
3407 $html .= '<tr>';
3408 $html .= '<td style="word-break: break-all;">';
3409 if (!empty($url)) {
3410 $html .= '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($display_url) . '</a>';
3411 } else {
3412 $html .= esc_html__('Unknown URL', 'mxchat');
3413 }
3414 $html .= '</td>';
3415 $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
3416 $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
3417 $html .= '<td>' . esc_html($time_ago) . '</td>';
3418 $html .= '</tr>';
3419 }
3420
3421 $html .= '</tbody></table>';
3422
3423 if (count($failed_urls_list) > 50) {
3424 $html .= '<div class="mxchat-failed-urls-more">+ ' .
3425 (count($failed_urls_list) - 50) .
3426 ' ' . esc_html__('more failed URLs not shown', 'mxchat') . '</div>';
3427 }
3428
3429 $html .= '</div></details></div>';
3430
3431 return $html;
3432 }
3433
3434 /**
3435 * Get all ACF fields for a specific post
3436 */
3437 public function mxchat_get_acf_fields_for_post($post_id) {
3438 if (!function_exists('get_fields')) {
3439 return array();
3440 }
3441
3442 $fields = get_fields($post_id);
3443 if (!$fields || !is_array($fields)) {
3444 return array();
3445 }
3446
3447 return $fields;
3448 }
3449
3450 /**
3451 * Format ACF field values for content extraction
3452 */
3453 public function mxchat_format_acf_field_value($value, $field_name = '', $post_id = 0) {
3454 if (empty($value)) {
3455 return '';
3456 }
3457
3458 // Handle WP_Post objects first (THIS IS THE KEY FIX)
3459 if ($value instanceof WP_Post) {
3460 return $value->post_title ?: '';
3461 }
3462
3463 // Handle other WP objects
3464 if (is_object($value)) {
3465 if (isset($value->post_title)) {
3466 return $value->post_title;
3467 } elseif (isset($value->display_name)) {
3468 return $value->display_name;
3469 } elseif (isset($value->name)) {
3470 return $value->name;
3471 } elseif (method_exists($value, '__toString')) {
3472 try {
3473 return (string) $value;
3474 } catch (Exception $e) {
3475 return '';
3476 }
3477 }
3478 // For any other objects, return empty string
3479 return '';
3480 }
3481
3482 // Handle different ACF field types
3483 if (is_array($value)) {
3484 // Check if it's an image/file field
3485 if (isset($value['url'])) {
3486 // Image field - return alt text, title, or caption
3487 if (!empty($value['alt'])) {
3488 return $value['alt'];
3489 } elseif (!empty($value['title'])) {
3490 return $value['title'];
3491 } elseif (!empty($value['caption'])) {
3492 return $value['caption'];
3493 } else {
3494 return ''; // Don't include just the URL
3495 }
3496 }
3497
3498 // Check if it's a post object or relationship field
3499 if (isset($value['post_title'])) {
3500 return $value['post_title'];
3501 }
3502
3503 // Check if it's a user field
3504 if (isset($value['display_name'])) {
3505 return $value['display_name'];
3506 }
3507
3508 // Check if it's a taxonomy term
3509 if (isset($value['name']) && isset($value['taxonomy'])) {
3510 return $value['name'];
3511 }
3512
3513 // Check if it's a select field with label
3514 if (isset($value['label'])) {
3515 return $value['label'];
3516 }
3517
3518 // Check for repeater field or flexible content
3519 if (is_numeric(key($value))) {
3520 $sub_values = array();
3521 foreach ($value as $sub_item) {
3522 if (is_array($sub_item)) {
3523 // For repeater/flexible content, extract text values
3524 $sub_text = $this->mxchat_extract_text_from_acf_array($sub_item);
3525 if (!empty($sub_text)) {
3526 $sub_values[] = $sub_text;
3527 }
3528 } elseif ($sub_item instanceof WP_Post) {
3529 // Handle WP_Post objects in arrays
3530 $sub_values[] = $sub_item->post_title ?: '';
3531 } else {
3532 $sub_values[] = (string) $sub_item;
3533 }
3534 }
3535 return implode(', ', array_filter($sub_values));
3536 }
3537
3538 // For other arrays, try to extract meaningful text
3539 $text_values = array();
3540 foreach ($value as $key => $val) {
3541 if (is_string($val) && !empty(trim($val))) {
3542 $text_values[] = trim($val);
3543 } elseif ($val instanceof WP_Post) {
3544 // Handle WP_Post objects in associative arrays
3545 $text_values[] = $val->post_title ?: '';
3546 } elseif (is_array($val) && isset($val['post_title'])) {
3547 $text_values[] = $val['post_title'];
3548 } elseif (is_array($val) && isset($val['name'])) {
3549 $text_values[] = $val['name'];
3550 }
3551 }
3552
3553 return implode(', ', array_filter($text_values));
3554 }
3555
3556 // Handle boolean values
3557 if (is_bool($value)) {
3558 return $value ? 'Yes' : 'No';
3559 }
3560
3561 // Handle numeric values
3562 if (is_numeric($value)) {
3563 return (string) $value;
3564 }
3565
3566 // Handle string values
3567 if (is_string($value)) {
3568 return trim($value);
3569 }
3570
3571 // For anything else that we can't handle, return empty string
3572 // This prevents the "Object could not be converted to string" error
3573 return '';
3574 }
3575
3576 /**
3577 * Extract text from complex ACF array structures
3578 */
3579 private function mxchat_extract_text_from_acf_array($array) {
3580 if (!is_array($array)) {
3581 return '';
3582 }
3583
3584 $text_parts = array();
3585
3586 foreach ($array as $key => $value) {
3587 if (is_string($value) && !empty(trim($value))) {
3588 // Skip keys that are likely to be IDs or technical values
3589 if (!is_numeric($value) || strlen($value) > 10) {
3590 $text_parts[] = trim($value);
3591 }
3592 } elseif ($value instanceof WP_Post) {
3593 // Handle WP_Post objects
3594 $text_parts[] = $value->post_title ?: '';
3595 } elseif (is_array($value)) {
3596 if (isset($value['post_title'])) {
3597 $text_parts[] = $value['post_title'];
3598 } elseif (isset($value['name'])) {
3599 $text_parts[] = $value['name'];
3600 } elseif (isset($value['label'])) {
3601 $text_parts[] = $value['label'];
3602 }
3603 } elseif (is_object($value)) {
3604 // Handle other objects safely
3605 if (isset($value->post_title)) {
3606 $text_parts[] = $value->post_title;
3607 } elseif (isset($value->name)) {
3608 $text_parts[] = $value->name;
3609 } elseif (isset($value->display_name)) {
3610 $text_parts[] = $value->display_name;
3611 }
3612 }
3613 }
3614
3615 return implode(', ', array_filter($text_parts));
3616 }
3617
3618 public function mxchat_handle_post_update($post_id, $post, $update) {
3619 // Basic validation checks
3620 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
3621 return;
3622 }
3623
3624 $post_type = $post->post_type;
3625
3626 // Check if sync is enabled for this post type
3627 $should_sync = false;
3628
3629 // Check built-in post types first
3630 if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
3631 $should_sync = true;
3632 } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
3633 $should_sync = true;
3634 } else {
3635 // Check custom post types
3636 $option_name = 'mxchat_auto_sync_' . $post_type;
3637 if (get_option($option_name) === '1') {
3638 $should_sync = true;
3639 }
3640 }
3641
3642 if (!$should_sync) {
3643 return;
3644 }
3645
3646 // Check if we have stored the previous status and URL in our transients
3647 $previous_status_key = 'mxchat_prev_status_' . $post_id;
3648 $previous_status = get_transient($previous_status_key);
3649
3650 $previous_url_key = 'mxchat_prev_url_' . $post_id;
3651 $previous_url = get_transient($previous_url_key);
3652
3653 // If the post was previously published but is now not published, remove from knowledge base
3654 if ($previous_status === 'publish' && $post->post_status !== 'publish') {
3655 // Use the stored URL from when it was published, or fall back to current permalink
3656 $source_url = $previous_url ?: get_permalink($post_id);
3657
3658 if ($source_url) {
3659 // Check if Pinecone is enabled
3660 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3661 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3662
3663 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3664 // Delete from Pinecone
3665 $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
3666 } else {
3667 // Delete from WordPress DB
3668 global $wpdb;
3669 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3670
3671 $result = $wpdb->delete(
3672 $table_name,
3673 array('source_url' => $source_url),
3674 array('%s')
3675 );
3676 }
3677 }
3678
3679 // Clean up the transients and exit early
3680 delete_transient($previous_status_key);
3681 delete_transient($previous_url_key);
3682 return;
3683 }
3684
3685 // Store the current status for next time (if this is an update)
3686 if ($update) {
3687 set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
3688
3689 // If the post is currently published, also store its URL
3690 if ($post->post_status === 'publish') {
3691 $current_url = get_permalink($post_id);
3692 set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
3693 }
3694 }
3695
3696 // Only process currently published content for adding/updating
3697 if ($post->post_status === 'publish') {
3698 // Get the source URL
3699 $source_url = get_permalink($post_id);
3700
3701 // Get content with proper formatting (matching ajax_mxchat_process_selected_content)
3702 $title = get_the_title($post_id);
3703 $content = get_post_field('post_content', $post_id);
3704
3705 // Apply WordPress content filters to get properly formatted content
3706 $content = apply_filters('the_content', $content);
3707
3708 // Strip tags but preserve structure
3709 $content = wp_strip_all_tags($content);
3710
3711 // Combine title and content
3712 $final_content = $title . "\n\n" . $content;
3713
3714 // For custom post types like job_listing, include additional fields
3715 if ($post_type === 'job_listing') {
3716 // Add job-specific meta if available
3717 $job_location = get_post_meta($post_id, '_job_location', true);
3718 if (!empty($job_location)) {
3719 $final_content .= "\n\nLocation: " . $job_location;
3720 }
3721
3722 // Get job type terms
3723 $job_types = get_the_terms($post_id, 'job_listing_type');
3724 if (!empty($job_types) && !is_wp_error($job_types)) {
3725 $types = array();
3726 foreach ($job_types as $type) {
3727 $types[] = $type->name;
3728 }
3729 $final_content .= "\n\nJob Type: " . implode(', ', $types);
3730 }
3731
3732 // Get company name if available
3733 $company_name = get_post_meta($post_id, '_company_name', true);
3734 if (!empty($company_name)) {
3735 $final_content .= "\n\nCompany: " . $company_name;
3736 }
3737 }
3738
3739 // Get API key with proper model detection
3740 $options = get_option('mxchat_options');
3741 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3742
3743 if (strpos($selected_model, 'voyage') === 0) {
3744 $api_key = $options['voyage_api_key'] ?? '';
3745 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3746 $api_key = $options['gemini_api_key'] ?? '';
3747 } else {
3748 $api_key = $options['api_key'] ?? '';
3749 }
3750
3751 if (empty($api_key)) {
3752 return;
3753 }
3754
3755 // Use the centralized utility function for storage
3756 $result = MxChat_Utils::submit_content_to_db(
3757 $final_content,
3758 $source_url,
3759 $api_key,
3760 md5($source_url) // Vector ID for Pinecone
3761 );
3762 }
3763
3764 // Clean up the stored previous status if not used above
3765 if ($previous_status !== 'publish' || $post->post_status === 'publish') {
3766 delete_transient($previous_status_key);
3767 delete_transient($previous_url_key);
3768 }
3769 }
3770
3771 /**
3772 * Store the post status and URL before update to detect status transitions
3773 * This runs before the post is actually updated in the database
3774 */
3775 public function mxchat_store_pre_update_status($post_id, $data) {
3776 // Get the current post from database (before update)
3777 $current_post = get_post($post_id);
3778
3779 if ($current_post) {
3780 // Store the current status temporarily
3781 $status_key = 'mxchat_prev_status_' . $post_id;
3782 set_transient($status_key, $current_post->post_status, HOUR_IN_SECONDS);
3783
3784 // If the post is currently published, also store its URL
3785 if ($current_post->post_status === 'publish') {
3786 $url_key = 'mxchat_prev_url_' . $post_id;
3787 $current_url = get_permalink($post_id);
3788 set_transient($url_key, $current_url, HOUR_IN_SECONDS);
3789 }
3790 }
3791 }
3792
3793 public function mxchat_handle_post_delete($post_id) {
3794 // Get post data before it's deleted
3795 $post = get_post($post_id);
3796
3797 // Basic validation
3798 if (!$post || wp_is_post_revision($post_id)) {
3799 return;
3800 }
3801
3802 $post_type = $post->post_type;
3803
3804 // Check if sync is enabled for this post type
3805 $should_sync = false;
3806
3807 // Check built-in post types first
3808 if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
3809 $should_sync = true;
3810 } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
3811 $should_sync = true;
3812 } else {
3813 // Check custom post types
3814 $option_name = 'mxchat_auto_sync_' . $post_type;
3815 if (get_option($option_name) === '1') {
3816 $should_sync = true;
3817 }
3818 }
3819
3820 if (!$should_sync) {
3821 return;
3822 }
3823
3824 // Get the URL before post is deleted
3825 $source_url = get_permalink($post_id);
3826 if (!$source_url) {
3827 //error_log('MXChat: Failed to get permalink for post ' . $post_id);
3828 return;
3829 }
3830
3831 // Check if Pinecone is enabled
3832 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3833 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3834
3835 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3836 // Delete from Pinecone
3837 $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
3838 } else {
3839 // Delete from WordPress DB
3840 global $wpdb;
3841 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3842
3843 $result = $wpdb->delete(
3844 $table_name,
3845 array('source_url' => $source_url),
3846 array('%s')
3847 );
3848
3849 if ($result === false) {
3850 //error_log('MXChat: WordPress DB deletion failed for URL: ' . $source_url);
3851 }
3852 }
3853 }
3854
3855
3856 /**
3857 * Deletes data from Pinecone using a source URL
3858 */
3859 public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
3860 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3861 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3862
3863 if (empty($host) || empty($api_key)) {
3864 //error_log('MXChat: Pinecone deletion failed - missing configuration');
3865 return false;
3866 }
3867
3868 $api_endpoint = "https://{$host}/vectors/delete";
3869 $vector_id = md5($source_url);
3870
3871 $request_body = array(
3872 'ids' => array($vector_id)
3873 );
3874
3875 $response = wp_remote_post($api_endpoint, array(
3876 'headers' => array(
3877 'Api-Key' => $api_key,
3878 'accept' => 'application/json',
3879 'content-type' => 'application/json'
3880 ),
3881 'body' => wp_json_encode($request_body),
3882 'timeout' => 30
3883 ));
3884
3885 if (is_wp_error($response)) {
3886 //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
3887 return false;
3888 }
3889
3890 $response_code = wp_remote_retrieve_response_code($response);
3891 if ($response_code !== 200) {
3892 //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
3893 return false;
3894 }
3895
3896 return true;
3897 }
3898
3899
3900
3901 public function mxchat_handle_product_change($post_id, $post, $update) {
3902 if ($post->post_type !== 'product') {
3903 return;
3904 }
3905
3906 if ($post->post_status === 'publish') {
3907 add_action('shutdown', function() use ($post_id) {
3908 $product = wc_get_product($post_id);
3909 if ($product) {
3910 $this->mxchat_store_product_embedding($product);
3911 }
3912 });
3913 }
3914 }
3915
3916 /**
3917 * Store WooCommerce product embeddings
3918 */
3919 private function mxchat_store_product_embedding($product) {
3920 if (!isset($this->options['enable_woocommerce_integration']) ||
3921 !in_array($this->options['enable_woocommerce_integration'], ['1', 'on'])) {
3922 return;
3923 }
3924
3925 $source_url = get_permalink($product->get_id());
3926
3927 // Build product content
3928 $title = $product->get_name();
3929 $description = $product->get_description();
3930 $short_description = $product->get_short_description();
3931 $regular_price = $product->get_regular_price();
3932 $sale_price = $product->get_sale_price();
3933 $sku = $product->get_sku();
3934
3935 // Format content consistently
3936 $content = $title . "\n\n";
3937
3938 if (!empty($description)) {
3939 $content .= wp_strip_all_tags($description) . "\n\n";
3940 }
3941
3942 if (!empty($short_description)) {
3943 $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
3944 }
3945
3946 $content .= "Price: $" . $regular_price . "\n";
3947
3948 if (!empty($sale_price)) {
3949 $content .= "Sale Price: $" . $sale_price . "\n";
3950 }
3951
3952 if (!empty($sku)) {
3953 $content .= "SKU: " . $sku . "\n";
3954 }
3955
3956 // Get API key with proper model detection
3957 $options = get_option('mxchat_options');
3958 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3959
3960 if (strpos($selected_model, 'voyage') === 0) {
3961 $api_key = $options['voyage_api_key'] ?? '';
3962 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3963 $api_key = $options['gemini_api_key'] ?? '';
3964 } else {
3965 $api_key = $options['api_key'] ?? '';
3966 }
3967
3968 if (empty($api_key)) {
3969 //error_log('MxChat Auto-sync: No API key configured for embedding model');
3970 return;
3971 }
3972
3973 // Use the centralized utility function for storage
3974 $result = MxChat_Utils::submit_content_to_db(
3975 $content,
3976 $source_url,
3977 $api_key,
3978 md5($source_url) // Vector ID for Pinecone
3979 );
3980
3981 if (is_wp_error($result)) {
3982 //error_log('MxChat WooCommerce sync failed for product ' . $product->get_id() . ': ' . $result->get_error_message());
3983 }
3984 }
3985
3986 public function mxchat_handle_product_delete($post_id) {
3987 if (get_post_type($post_id) !== 'product') {
3988 return;
3989 }
3990
3991 $source_url = get_permalink($post_id);
3992
3993 // Check if Pinecone is enabled
3994 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3995 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3996
3997 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3998 // Delete from Pinecone
3999 $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
4000 } else {
4001 // Delete from WordPress DB
4002 global $wpdb;
4003 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4004
4005 $wpdb->delete(
4006 $table_name,
4007 array('source_url' => $source_url),
4008 array('%s')
4009 );
4010 }
4011 }
4012
4013 /**
4014 * Handle individual Pinecone content deletion
4015 */
4016 public function mxchat_handle_pinecone_prompt_delete() {
4017 // Check permissions
4018 if (!current_user_can('manage_options')) {
4019 wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
4020 }
4021
4022 // Verify nonce
4023 if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'mxchat_delete_pinecone_prompt_nonce')) {
4024 wp_die(esc_html__('Security check failed.', 'mxchat'));
4025 }
4026
4027 $vector_id = isset($_GET['vector_id']) ? sanitize_text_field($_GET['vector_id']) : '';
4028
4029 if (empty($vector_id)) {
4030 set_transient('mxchat_admin_notice_error',
4031 esc_html__('Invalid vector ID.', 'mxchat'),
4032 30
4033 );
4034 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
4035 exit;
4036 }
4037
4038 // Get Pinecone settings
4039 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4040 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4041
4042 if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
4043 set_transient('mxchat_admin_notice_error',
4044 esc_html__('Pinecone is not properly configured.', 'mxchat'),
4045 30
4046 );
4047 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
4048 exit;
4049 }
4050
4051 // Delete from Pinecone
4052 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
4053 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
4054 $vector_id,
4055 $pinecone_options['mxchat_pinecone_api_key'],
4056 $pinecone_options['mxchat_pinecone_host']
4057 );
4058
4059 if ($result['success']) {
4060 // Remove from ALL caches
4061 $pinecone_manager->mxchat_remove_from_pinecone_vector_cache($vector_id);
4062 $pinecone_manager->mxchat_remove_from_processed_content_caches($vector_id);
4063
4064 // CLEAR ALL RELEVANT CACHES
4065 delete_transient('mxchat_pinecone_recent_1k_cache');
4066 delete_option('mxchat_pinecone_vector_ids_cache');
4067 delete_option('mxchat_pinecone_processed_cache');
4068 delete_option('mxchat_processed_content_cache');
4069
4070 // Also force refresh for next page load
4071 $pinecone_manager->mxchat_refresh_after_new_content($pinecone_options);
4072
4073 set_transient('mxchat_admin_notice_success',
4074 esc_html__('Entry deleted successfully from Pinecone.', 'mxchat'),
4075 30
4076 );
4077 } else {
4078 set_transient('mxchat_admin_notice_error',
4079 esc_html__('Failed to delete entry: ', 'mxchat') . $result['message'],
4080 30
4081 );
4082 }
4083
4084 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
4085 exit;
4086 }
4087
4088 public function ajax_mxchat_delete_pinecone_prompt() {
4089 // Verify nonce and permissions
4090 if (!check_ajax_referer('mxchat_delete_pinecone_prompt_nonce', 'nonce', false)) {
4091 wp_send_json_error('Invalid nonce');
4092 exit;
4093 }
4094
4095 if (!current_user_can('manage_options')) {
4096 wp_send_json_error('Unauthorized access');
4097 exit;
4098 }
4099
4100 $vector_id = isset($_POST['vector_id']) ? sanitize_text_field($_POST['vector_id']) : '';
4101
4102 if (empty($vector_id)) {
4103 wp_send_json_error('Missing vector ID');
4104 exit;
4105 }
4106
4107 // Get Pinecone settings
4108 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4109 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4110
4111 if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
4112 wp_send_json_error('Pinecone is not properly configured');
4113 exit;
4114 }
4115
4116 // Delete from Pinecone
4117 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
4118 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
4119 $vector_id,
4120 $pinecone_options['mxchat_pinecone_api_key'],
4121 $pinecone_options['mxchat_pinecone_host']
4122 );
4123
4124 if ($result['success']) {
4125 // Remove from ALL caches
4126 $pinecone_manager->mxchat_remove_from_pinecone_vector_cache($vector_id);
4127 $pinecone_manager->mxchat_remove_from_processed_content_caches($vector_id);
4128
4129 // CLEAR ALL RELEVANT CACHES (ADD THESE LINES)
4130 delete_transient('mxchat_pinecone_recent_1k_cache');
4131 delete_option('mxchat_pinecone_vector_ids_cache');
4132 delete_option('mxchat_pinecone_processed_cache');
4133 delete_option('mxchat_processed_content_cache');
4134
4135 // Also force refresh for next page load
4136 $pinecone_manager->mxchat_refresh_after_new_content($pinecone_options);
4137
4138 wp_send_json_success(array(
4139 'message' => 'Entry deleted successfully from Pinecone',
4140 'vector_id' => $vector_id
4141 ));
4142 } else {
4143 wp_send_json_error('Failed to delete from Pinecone: ' . $result['message']);
4144 }
4145
4146 exit;
4147 }
4148
4149 /**
4150 * NEW: Get hierarchical roles for dropdown
4151 */
4152 public function mxchat_get_role_options() {
4153 return array(
4154 'public' => __('Public (Everyone)', 'mxchat'),
4155 'logged_in' => __('Logged In Users', 'mxchat'),
4156 'subscriber' => __('Subscribers & Above', 'mxchat'),
4157 'contributor' => __('Contributors & Above', 'mxchat'),
4158 'author' => __('Authors & Above', 'mxchat'),
4159 'editor' => __('Editors & Above', 'mxchat'),
4160 'administrator' => __('Administrators Only', 'mxchat')
4161 );
4162 }
4163
4164 /**
4165 * NEW: Check if user has access to content based on role restriction
4166 */
4167 public function mxchat_user_has_content_access($role_restriction) {
4168 // Public content is always accessible
4169 if ($role_restriction === 'public' || empty($role_restriction)) {
4170 return true;
4171 }
4172
4173 // Check if user is logged in for logged_in restriction
4174 if ($role_restriction === 'logged_in') {
4175 return is_user_logged_in();
4176 }
4177
4178 // If not logged in, no access to role-restricted content
4179 if (!is_user_logged_in()) {
4180 return false;
4181 }
4182
4183 $user = wp_get_current_user();
4184 $user_roles = $user->roles;
4185
4186 if (empty($user_roles)) {
4187 return false;
4188 }
4189
4190 // Define role hierarchy (higher number = higher access)
4191 $hierarchy = array(
4192 'subscriber' => 1,
4193 'contributor' => 2,
4194 'author' => 3,
4195 'editor' => 4,
4196 'administrator' => 5
4197 );
4198
4199 // Get required level
4200 $required_level = isset($hierarchy[$role_restriction]) ? $hierarchy[$role_restriction] : 0;
4201
4202 // Check if user has required level or higher
4203 foreach ($user_roles as $user_role) {
4204 $user_level = isset($hierarchy[$user_role]) ? $hierarchy[$user_role] : 0;
4205 if ($user_level >= $required_level) {
4206 return true;
4207 }
4208 }
4209
4210 return false;
4211 }
4212
4213 /**
4214 * Handle role restriction updates via AJAX
4215 */
4216 /**
4217 * UPDATED: Handle role restriction updates for both WordPress and Pinecone via AJAX
4218 */
4219 public function ajax_mxchat_update_role_restriction() {
4220 // Verify nonce and permissions
4221 if (!check_ajax_referer('mxchat_update_role_nonce', 'nonce', false)) {
4222 wp_send_json_error('Invalid nonce');
4223 exit;
4224 }
4225
4226 if (!current_user_can('manage_options')) {
4227 wp_send_json_error('Unauthorized access');
4228 exit;
4229 }
4230
4231 $entry_id = isset($_POST['entry_id']) ? sanitize_text_field($_POST['entry_id']) : '';
4232 $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
4233 $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
4234
4235 if (empty($entry_id)) {
4236 wp_send_json_error('Invalid entry ID');
4237 exit;
4238 }
4239
4240 // Get knowledge manager instance to validate role restriction
4241 $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
4242 $valid_roles = array_keys($knowledge_manager->mxchat_get_role_options());
4243 if (!in_array($role_restriction, $valid_roles)) {
4244 wp_send_json_error('Invalid role restriction');
4245 exit;
4246 }
4247
4248 global $wpdb;
4249
4250 if ($data_source === 'pinecone') {
4251 // Handle Pinecone role restriction (stored separately in WordPress table)
4252 $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
4253
4254 // Use REPLACE to insert or update the role restriction
4255 $result = $wpdb->replace(
4256 $roles_table,
4257 array(
4258 'vector_id' => $entry_id,
4259 'role_restriction' => $role_restriction,
4260 'updated_at' => current_time('mysql')
4261 ),
4262 array('%s', '%s', '%s')
4263 );
4264
4265 // Clear Pinecone cache to reflect changes
4266 delete_transient('mxchat_pinecone_recent_1k_cache');
4267
4268 } else {
4269 // Handle WordPress database role restriction (existing functionality)
4270 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4271
4272 $result = $wpdb->update(
4273 $table_name,
4274 array('role_restriction' => $role_restriction),
4275 array('id' => absint($entry_id)),
4276 array('%s'),
4277 array('%d')
4278 );
4279 }
4280
4281 if ($result === false) {
4282 wp_send_json_error('Database update failed: ' . $wpdb->last_error);
4283 exit;
4284 }
4285
4286 wp_send_json_success(array(
4287 'message' => 'Role restriction updated successfully',
4288 'role_restriction' => $role_restriction,
4289 'data_source' => $data_source,
4290 'entry_id' => $entry_id
4291 ));
4292 exit;
4293 }
4294
4295 // ========================================
4296 // HELPER METHODS
4297 // ========================================
4298
4299 /**
4300 * Check if user has required permissions for content processing
4301 */
4302 private function mxchat_check_user_permissions() {
4303 if (!current_user_can('manage_options')) {
4304 wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
4305 }
4306 }
4307
4308 /**
4309 * Validate nonce for security
4310 */
4311 private function mxchat_validate_nonce($nonce_name, $nonce_action) {
4312 if (!isset($_POST[$nonce_name]) || !wp_verify_nonce($_POST[$nonce_name], $nonce_action)) {
4313 wp_die(esc_html__('Security check failed.', 'mxchat'));
4314 }
4315 }
4316
4317 /**
4318 * Get embedding API credentials
4319 */
4320 private function mxchat_get_embedding_credentials() {
4321 $embedding_model = $this->options['embedding_model'] ?? 'text-embedding-ada-002';
4322
4323 if (strpos($embedding_model, 'text-embedding-') !== false) {
4324 return array(
4325 'type' => 'openai',
4326 'api_key' => $this->options['api_key'] ?? ''
4327 );
4328 } elseif (strpos($embedding_model, 'voyage-') !== false) {
4329 return array(
4330 'type' => 'voyage',
4331 'api_key' => $this->options['voyage_api_key'] ?? ''
4332 );
4333 } elseif (strpos($embedding_model, 'gemini-embedding-') !== false) {
4334 return array(
4335 'type' => 'gemini',
4336 'api_key' => $this->options['gemini_api_key'] ?? ''
4337 );
4338 }
4339
4340 return array('type' => 'unknown', 'api_key' => '');
4341 }
4342
4343 /**
4344 * Log processing errors
4345 */
4346 private function mxchat_log_processing_error($operation, $error_message) {
4347 //error_log("MxChat Knowledge Processing {$operation} Error: " . $error_message);
4348 }
4349
4350 /**
4351 * Set admin notice transient
4352 */
4353 private function mxchat_set_admin_notice($type, $message) {
4354 set_transient("mxchat_admin_notice_{$type}", $message, 30);
4355 }
4356
4357 /**
4358 * Get Pinecone manager instance for vector operations
4359 */
4360 private function mxchat_get_pinecone_manager() {
4361 return MxChat_Pinecone_Manager::get_instance();
4362 }
4363
4364 // ========================================
4365 // STATIC ACCESS METHODS
4366 // ========================================
4367
4368 /**
4369 * Get singleton instance
4370 */
4371 public static function get_instance() {
4372 static $instance = null;
4373 if ($instance === null) {
4374 $instance = new self();
4375 }
4376 return $instance;
4377 }
4378 }
4379
4380 // Initialize the Knowledge manager
4381 $mxchat_knowledge_manager = MxChat_Knowledge_Manager::get_instance();