PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.2.2
MxChat – AI Chatbot & Content Generation for WordPress v2.2.2
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.2.2, at admin/class-knowledge-manager.php

2,986 lines 119.9 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->init_hooks();
22 }
23
24 /**
25 * Initialize WordPress hooks for content processing
26 */
27 private function 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, 'ajax_get_status_updates'));
35 add_action('wp_ajax_mxchat_dismiss_completed_status', array($this, 'ajax_dismiss_completed_status')); // NEW
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
40
41 // Cron handlers for background processing
42 add_action('mxchat_process_sitemap_urls', array($this, 'process_sitemap_urls_cron'), 10, 5);
43 add_action('mxchat_process_pdf_pages', array($this, 'process_pdf_pages_cron'), 10, 5);
44
45 // WordPress post management hooks
46 add_action('save_post', array($this, 'handle_post_update'), 10, 2);
47 add_action('post_updated', array($this, 'handle_post_update'), 10, 3);
48 add_action('before_delete_post', array($this, 'mxchat_handle_post_delete'));
49 add_action('wp_trash_post', array($this, 'mxchat_handle_post_delete'));
50
51 // WooCommerce product hooks (if WooCommerce is active)
52 if (class_exists('WooCommerce')) {
53 add_action('save_post_product', array($this, 'mxchat_handle_product_change'));
54 add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
55 add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
56 }
57
58 // Admin notices for processing status
59 add_action('admin_notices', array($this, 'display_processing_status'));
60 }
61
62 /**
63 * Get current options (refreshed)
64 */
65 private function get_options() {
66 if (empty($this->options)) {
67 $this->options = get_option('mxchat_options', array());
68 }
69 return $this->options;
70 }
71
72
73 /**
74 * Handle manual batch processing via AJAX
75 */
76 public function ajax_manual_batch_process() {
77 try {
78 // Verify nonce and permissions
79 check_ajax_referer('mxchat_status_nonce', 'nonce');
80
81 if (!current_user_can('manage_options')) {
82 wp_send_json_error('Unauthorized access');
83 }
84
85 $process_type = sanitize_text_field($_POST['process_type'] ?? '');
86 $url = sanitize_text_field($_POST['url'] ?? '');
87
88 if (empty($process_type) || empty($url)) {
89 wp_send_json_error('Missing required parameters');
90 }
91
92 $processed = 0;
93
94 if ($process_type === 'pdf') {
95 $processed = $this->manual_process_pdf_batch($url);
96 } elseif ($process_type === 'sitemap') {
97 $processed = $this->manual_process_sitemap_batch($url);
98 }
99
100 if ($processed > 0) {
101 wp_send_json_success(array(
102 'message' => "Processed {$processed} items successfully",
103 'processed' => $processed
104 ));
105 } else {
106 wp_send_json_error('No items were processed');
107 }
108
109 } catch (Exception $e) {
110 //error_log('Manual batch process error: ' . $e->getMessage());
111 wp_send_json_error('Processing failed: ' . $e->getMessage());
112 }
113 }
114
115 /**
116 * Process a small PDF batch manually - DIRECT PROCESSING
117 */
118 private function manual_process_pdf_batch($pdf_url) {
119 try {
120 $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
121 $status = get_transient($status_key);
122
123 if (!$status || $status['status'] !== 'processing') {
124 //error_log('Manual PDF: No processing status found');
125 return 0;
126 }
127
128 //error_log('Manual PDF: Starting direct processing for ' . $pdf_url);
129
130 // Get current progress
131 $current_page = $status['processed_pages'] ?? 0;
132 $total_pages = $status['total_pages'] ?? 0;
133
134 if ($current_page >= $total_pages) {
135 //error_log('Manual PDF: Already completed');
136 return 0;
137 }
138
139 // Try to download the PDF again for processing
140 $response = wp_remote_get($pdf_url, array('timeout' => 30));
141
142 if (is_wp_error($response)) {
143 //error_log('Manual PDF: Failed to download PDF: ' . $response->get_error_message());
144 return 0;
145 }
146
147 $pdf_content = wp_remote_retrieve_body($response);
148 if (empty($pdf_content)) {
149 //error_log('Manual PDF: Empty PDF content');
150 return 0;
151 }
152
153 // Save PDF temporarily
154 $upload_dir = wp_upload_dir();
155 $temp_pdf_path = trailingslashit($upload_dir['path']) . 'temp_manual_' . time() . '.pdf';
156 file_put_contents($temp_pdf_path, $pdf_content);
157
158 // Process 2 pages directly
159 $processed = $this->process_pdf_pages_direct($temp_pdf_path, $pdf_url, $current_page, 5);
160
161 // Clean up temp file
162 if (file_exists($temp_pdf_path)) {
163 wp_delete_file($temp_pdf_path);
164 }
165
166 //error_log('Manual PDF: Processed ' . $processed . ' pages');
167 return $processed;
168
169 } catch (Exception $e) {
170 //error_log('Manual PDF batch error: ' . $e->getMessage());
171 return 0;
172 }
173 }
174
175 /**
176 * Process PDF pages directly without cron
177 */
178 private function process_pdf_pages_direct($pdf_path, $pdf_url, $start_page, $batch_size) {
179 try {
180 if (!file_exists($pdf_path)) {
181 //error_log('Direct PDF: File not found at ' . $pdf_path);
182 return 0;
183 }
184
185 $parser = new \Smalot\PdfParser\Parser();
186 $pdf = $parser->parseFile($pdf_path);
187 $pages = $pdf->getPages();
188
189 $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
190 $status = get_transient($status_key);
191
192 if (!$status) {
193 return 0;
194 }
195
196 $options = get_option('mxchat_options');
197 $api_key = $options['api_key'] ?? '';
198
199 if (empty($api_key)) {
200 //error_log('Direct PDF: No API key');
201 return 0;
202 }
203
204 $processed = 0;
205 $end_page = min($start_page + $batch_size, count($pages));
206
207 for ($i = $start_page; $i < $end_page; $i++) {
208 try {
209 $page_number = $i + 1;
210 $text = $pages[$i]->getText();
211
212 if (empty($text)) {
213 //error_log('Direct PDF: Empty text on page ' . $page_number);
214 continue;
215 }
216
217 $sanitized_content = $this->mxchat_sanitize_content_for_api($text);
218 if (empty($sanitized_content)) {
219 //error_log('Direct PDF: No content after sanitization on page ' . $page_number);
220 continue;
221 }
222
223 // Generate embedding
224 $embedding_vector = $this->mxchat_generate_embedding($sanitized_content);
225 if (is_string($embedding_vector)) {
226 //error_log('Direct PDF: Embedding failed on page ' . $page_number . ': ' . $embedding_vector);
227 continue;
228 }
229
230 // Create metadata
231 $metadata = array(
232 'document_type' => 'pdf',
233 'total_pages' => count($pages),
234 'current_page' => $page_number,
235 'source_url' => $pdf_url
236 );
237
238 $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized_content;
239 $page_url = esc_url($pdf_url . "#page=" . $page_number);
240
241 // Store in database
242 $db_result = MxChat_Utils::submit_content_to_db($content_with_metadata, $page_url, $api_key);
243
244 if (is_wp_error($db_result)) {
245 //error_log('Direct PDF: DB error on page ' . $page_number . ': ' . $db_result->get_error_message());
246 continue;
247 }
248
249 $processed++;
250 //error_log('Direct PDF: Successfully processed page ' . $page_number);
251
252 // Update status
253 $status['processed_pages'] = $i + 1;
254 $status['last_update'] = time();
255 $status['percentage'] = round(($status['processed_pages'] / $status['total_pages']) * 100);
256 set_transient($status_key, $status, DAY_IN_SECONDS);
257
258 } catch (Exception $e) {
259 //error_log('Direct PDF: Error processing page ' . ($i + 1) . ': ' . $e->getMessage());
260 continue;
261 }
262 }
263
264 // Check if completed
265 if ($status['processed_pages'] >= $status['total_pages']) {
266 $status['status'] = 'complete';
267 set_transient($status_key, $status, DAY_IN_SECONDS);
268 //error_log('Direct PDF: Processing completed');
269 }
270
271 return $processed;
272
273 } catch (Exception $e) {
274 //error_log('Direct PDF processing error: ' . $e->getMessage());
275 return 0;
276 }
277 }
278
279 /**
280 * Process a small sitemap batch manually - DIRECT PROCESSING
281 */
282 private function manual_process_sitemap_batch($sitemap_url) {
283 try {
284 $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
285 $status = get_transient($status_key);
286
287 if (!$status || $status['status'] !== 'processing') {
288 return 0;
289 }
290
291 //error_log('Manual Sitemap: Starting direct processing for ' . $sitemap_url);
292
293 // Re-fetch the sitemap to get URLs
294 $response = wp_remote_get($sitemap_url, array('timeout' => 30));
295 if (is_wp_error($response)) {
296 //error_log('Manual Sitemap: Failed to fetch sitemap');
297 return 0;
298 }
299
300 $sitemap_content = wp_remote_retrieve_body($response);
301 $xml = simplexml_load_string($sitemap_content);
302
303 if (!$xml) {
304 //error_log('Manual Sitemap: Invalid XML');
305 return 0;
306 }
307
308 $urls = array();
309 foreach ($xml->url as $url_element) {
310 $urls[] = (string)$url_element->loc;
311 }
312
313 $current_processed = $status['processed_urls'] ?? 0;
314 $batch_size = 5;
315 $processed = 0;
316
317 // Process next 2 URLs
318 for ($i = $current_processed; $i < min($current_processed + $batch_size, count($urls)); $i++) {
319 $url = $urls[$i];
320
321 if ($this->process_single_url_direct($url)) {
322 $processed++;
323 }
324
325 // Update status
326 $status['processed_urls'] = $i + 1;
327 $status['last_update'] = time();
328 $status['percentage'] = round(($status['processed_urls'] / $status['total_urls']) * 100);
329 set_transient($status_key, $status, DAY_IN_SECONDS);
330 }
331
332 // Check if completed
333 if ($status['processed_urls'] >= $status['total_urls']) {
334 $status['status'] = 'complete';
335 set_transient($status_key, $status, DAY_IN_SECONDS);
336 }
337
338 //error_log('Manual Sitemap: Processed ' . $processed . ' URLs');
339 return $processed;
340
341 } catch (Exception $e) {
342 //error_log('Manual sitemap batch error: ' . $e->getMessage());
343 return 0;
344 }
345 }
346
347 /**
348 * Process a single URL directly
349 */
350 private function process_single_url_direct($url) {
351 try {
352 $response = wp_remote_get($url, array('timeout' => 30));
353 if (is_wp_error($response)) {
354 return false;
355 }
356
357 $html = wp_remote_retrieve_body($response);
358 $content = $this->mxchat_extract_main_content($html);
359 $sanitized = $this->mxchat_sanitize_content_for_api($content);
360
361 if (empty($sanitized)) {
362 return false;
363 }
364
365 $options = get_option('mxchat_options');
366 $api_key = $options['api_key'] ?? '';
367
368 $result = MxChat_Utils::submit_content_to_db($sanitized, $url, $api_key);
369
370 return !is_wp_error($result);
371
372 } catch (Exception $e) {
373 //error_log('Single URL processing error: ' . $e->getMessage());
374 return false;
375 }
376 }
377
378
379 // ========================================
380 // MAIN CONTENT SUBMISSION HANDLERS
381 // ========================================
382
383 public function mxchat_handle_content_submission() {
384 // Check if the form was submitted and the user has permission.
385 if (!isset($_POST['submit_content']) || !current_user_can('manage_options')) {
386 return;
387 }
388
389 // Verify the nonce.
390 $nonce = isset($_POST['mxchat_submit_content_nonce']) ? sanitize_text_field(wp_unslash($_POST['mxchat_submit_content_nonce'])) : '';
391 if (!wp_verify_nonce($nonce, 'mxchat_submit_content_action')) {
392 wp_die(esc_html__('Nonce verification failed.', 'mxchat'));
393 }
394
395 // Sanitize the inputs.
396 $article_content = sanitize_textarea_field($_POST['article_content']);
397 $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
398
399 // Get API key for submission
400 $options = get_option('mxchat_options');
401 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
402
403 if (strpos($selected_model, 'voyage') === 0) {
404 $api_key = $options['voyage_api_key'] ?? '';
405 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
406 $api_key = $options['gemini_api_key'] ?? '';
407 } else {
408 $api_key = $options['api_key'] ?? '';
409 }
410
411 if (empty($api_key)) {
412 set_transient('mxchat_admin_notice_error',
413 esc_html__('API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
414 30
415 );
416 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
417 exit;
418 }
419
420 // Use centralized utility function for storage
421 $result = MxChat_Utils::submit_content_to_db($article_content, $article_url, $api_key);
422
423 if (is_wp_error($result)) {
424 set_transient('mxchat_admin_notice_error',
425 esc_html__('Error storing content: ', 'mxchat') . $result->get_error_message(),
426 30
427 );
428 } else {
429 set_transient('mxchat_admin_notice_success',
430 esc_html__('Content successfully submitted!', 'mxchat'),
431 30
432 );
433 }
434
435 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
436 exit;
437 }
438 public function is_pdf_url($url, $response) {
439 $content_type = wp_remote_retrieve_header($response, 'content-type');
440 $file_extension = strtolower(pathinfo($url, PATHINFO_EXTENSION));
441
442 return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf';
443 }
444 public function handle_pdf_for_knowledge_base($pdf_url, $response) {
445 if (!current_user_can('manage_options')) {
446 //error_log(esc_html__('Unauthorized PDF processing attempt', 'mxchat'));
447 return false;
448 }
449
450 $pdf_url = esc_url_raw($pdf_url);
451 $upload_dir = wp_upload_dir();
452
453 if (isset($upload_dir['error']) && $upload_dir['error'] !== false) {
454 //error_log(sprintf(esc_html__('Upload directory error: %s', 'mxchat'), esc_html($upload_dir['error'])));
455 return false;
456 }
457
458 $pdf_filename = sanitize_file_name('mxchat_kb_' . time() . '.pdf');
459 $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
460
461 $response_body = wp_remote_retrieve_body($response);
462 if (empty($response_body)) {
463 //error_log(esc_html__('Empty PDF response body', 'mxchat'));
464 return false;
465 }
466
467 if (!wp_mkdir_p(dirname($pdf_path))) {
468 //error_log(sprintf(esc_html__('Failed to create directory for PDF: %s', 'mxchat'), esc_html($pdf_path)));
469 return false;
470 }
471
472 try {
473 file_put_contents($pdf_path, $response_body);
474
475 if (!file_exists($pdf_path)) {
476 throw new Exception(__('Failed to save PDF file', 'mxchat'));
477 }
478
479 $parser = new \Smalot\PdfParser\Parser();
480 $pdf = $parser->parseFile($pdf_path);
481 $total_pages = absint(count($pdf->getPages()));
482
483 if ($total_pages < 1) {
484 throw new Exception(__('Invalid PDF: no pages found', 'mxchat'));
485 }
486
487 wp_schedule_single_event(time(), 'mxchat_process_pdf_pages', array(
488 'pdf_path' => $pdf_path,
489 'pdf_url' => $pdf_url,
490 'total_pages' => $total_pages,
491 'batch_size' => absint(15),
492 'batch_pause' => absint(10)
493 ));
494
495 $status_data = array(
496 'total_pages' => $total_pages,
497 'processed_pages' => 0,
498 'status' => 'processing',
499 'last_update' => time()
500 );
501
502 set_transient(
503 sanitize_key('mxchat_pdf_status_' . md5($pdf_url)),
504 array_map('sanitize_text_field', $status_data),
505 DAY_IN_SECONDS
506 );
507
508 return __('scheduled', 'mxchat');
509
510 } catch (Exception $e) {
511 //error_log(sprintf(esc_html__('Error preparing PDF for processing: %s', 'mxchat'), esc_html($e->getMessage())));
512 if (file_exists($pdf_path)) {
513 wp_delete_file($pdf_path);
514 }
515 return false;
516 }
517 }
518
519 public function process_pdf_pages_cron($pdf_path, $pdf_url, $total_pages, $batch_size, $batch_pause) {
520 // Validate inputs
521 $pdf_path = sanitize_text_field($pdf_path);
522 $pdf_url = esc_url_raw($pdf_url);
523 $total_pages = absint($total_pages);
524 $batch_size = absint($batch_size);
525 $batch_pause = absint($batch_pause);
526
527 try {
528 if (!file_exists($pdf_path)) {
529 throw new Exception(sprintf('PDF file not found at path: %s', esc_html($pdf_path)));
530 }
531
532 $parser = new \Smalot\PdfParser\Parser();
533 $pdf = $parser->parseFile($pdf_path);
534 $pages = $pdf->getPages();
535
536 // Get current progress
537 $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
538 $status = get_transient($status_key);
539
540 if (!$status || !is_array($status)) {
541 throw new Exception('Invalid status data retrieved from transient');
542 }
543
544 // Initialize failed pages list if it doesn't exist
545 if (!isset($status['failed_pages_list']) || !is_array($status['failed_pages_list'])) {
546 $status['failed_pages_list'] = [];
547 }
548
549 $start_page = absint($status['processed_pages']);
550 $end_page = min($start_page + $batch_size, $total_pages);
551 $options = get_option('mxchat_options');
552
553 if (empty($options['api_key'])) {
554 throw new Exception('API key is missing or invalid');
555 }
556
557 $successful_pages = 0;
558 $failed_pages = 0;
559
560 for ($i = $start_page; $i < $end_page; $i++) {
561 $page_number = $i + 1;
562 $max_retries = 3;
563 $retry_count = 0;
564 $page_processed = false;
565 $last_error = '';
566
567 while (!$page_processed && $retry_count < $max_retries) {
568 try {
569 $text = $pages[$i]->getText();
570
571 if (empty($text)) {
572 throw new Exception("Empty text on page {$page_number}");
573 }
574
575 $sanitized_content = $this->mxchat_sanitize_content_for_api($text);
576
577 if (empty($sanitized_content)) {
578 throw new Exception("No valid content after sanitization on page {$page_number}");
579 }
580
581 $embedding_vector = $this->mxchat_generate_embedding($sanitized_content);
582
583 if (is_string($embedding_vector)) {
584 throw new Exception("Embedding generation failed: " . $embedding_vector);
585 }
586
587 if (!is_array($embedding_vector)) {
588 throw new Exception("Embedding generation returned unexpected result type: " . gettype($embedding_vector));
589 }
590
591 $metadata = array(
592 'document_type' => 'pdf',
593 'total_pages' => $total_pages,
594 'current_page' => $page_number,
595 'prev_page' => $i > 0 ? $i : null,
596 'next_page' => $i < ($total_pages - 1) ? $i + 2 : null,
597 'source_url' => $pdf_url
598 );
599
600 $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized_content;
601 $page_url = esc_url($pdf_url . "#page=" . $page_number);
602
603 $db_result = MxChat_Utils::submit_content_to_db($content_with_metadata, $page_url, $options['api_key']);
604
605 if (is_wp_error($db_result)) {
606 throw new Exception("Database submission failed: " . $db_result->get_error_message());
607 }
608
609 // Success!
610 $page_processed = true;
611 $successful_pages++;
612
613 } catch (Exception $e) {
614 $retry_count++;
615 $last_error = $e->getMessage();
616
617 //error_log("PDF page {$page_number} failed (attempt {$retry_count}/{$max_retries}): " . $last_error);
618
619 if ($retry_count < $max_retries) {
620 // Wait before retry (exponential backoff: 1s, 2s, 4s)
621 sleep(pow(2, $retry_count - 1));
622 }
623 }
624 }
625
626 // If page still not processed after all retries, mark as failed
627 if (!$page_processed) {
628 $failed_pages++;
629 $status['failed_pages_list'][] = [
630 'page' => $page_number,
631 'error' => $last_error,
632 'time' => time(),
633 'retries' => $max_retries
634 ];
635
636 // Limit failed pages list to prevent memory issues
637 if (count($status['failed_pages_list']) > 50) {
638 $status['failed_pages_list'] = array_slice($status['failed_pages_list'], -50);
639 }
640
641 //error_log("PDF page {$page_number} permanently failed after {$max_retries} attempts: " . $last_error);
642 }
643
644 // Update progress
645 $status['processed_pages'] = absint($page_number);
646 $status['last_update'] = time();
647 $status['failed_pages'] = absint($status['failed_pages'] ?? 0) + ($page_processed ? 0 : 1);
648
649 set_transient($status_key, array_map('sanitize_text_field', $status), DAY_IN_SECONDS);
650 }
651
652 // Schedule next batch if needed
653 if ($end_page < $total_pages) {
654 wp_schedule_single_event(time() + $batch_pause, 'mxchat_process_pdf_pages', array(
655 'pdf_path' => $pdf_path,
656 'pdf_url' => $pdf_url,
657 'total_pages' => $total_pages,
658 'batch_size' => $batch_size,
659 'batch_pause' => $batch_pause
660 ));
661 } else {
662 // Processing complete
663 $status['status'] = 'complete';
664 $status['processed_pages'] = $total_pages;
665
666 // Add completion summary
667 $status['completion_summary'] = [
668 'total_pages' => $total_pages,
669 'successful_pages' => $total_pages - absint($status['failed_pages'] ?? 0),
670 'failed_pages' => absint($status['failed_pages'] ?? 0),
671 'completion_time' => current_time('mysql')
672 ];
673
674 // Save the completed status (don't delete it - let user dismiss manually)
675 set_transient($status_key, array_map('sanitize_text_field', $status), DAY_IN_SECONDS);
676
677 // Clean up the temporary PDF file
678 if (file_exists($pdf_path)) {
679 wp_delete_file($pdf_path);
680 }
681
682 // DON'T delete the status transients here - let user dismiss manually
683 }
684
685 } catch (\Exception $e) {
686 //error_log(sprintf('[MXCHAT-PDF] Error processing PDF: %s', $e->getMessage()));
687
688 $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
689 $status = get_transient($status_key);
690
691 if (!$status || !is_array($status)) {
692 $status = array(
693 'total_pages' => $total_pages,
694 'processed_pages' => 0,
695 'status' => 'error',
696 'error' => sanitize_text_field($e->getMessage()),
697 'last_update' => time()
698 );
699 } else {
700 $status['status'] = 'error';
701 $status['error'] = sanitize_text_field($e->getMessage());
702 $status['last_update'] = time();
703 }
704
705 set_transient($status_key, array_map('sanitize_text_field', $status), DAY_IN_SECONDS);
706
707 if (file_exists($pdf_path)) {
708 wp_delete_file($pdf_path);
709 }
710 }
711 }
712
713
714 public function get_pdf_processing_status($pdf_url) {
715 $pdf_url = esc_url_raw($pdf_url);
716 $status = get_transient(sanitize_key('mxchat_pdf_status_' . md5($pdf_url)));
717
718 if (!$status || !is_array($status)) {
719 return false;
720 }
721
722 // Check for stalled processing (no updates for 5 minutes)
723 if ($status['status'] === 'processing' && (time() - absint($status['last_update'])) > 300) {
724 $status['status'] = 'error';
725 $status['error'] = __('PDF processing appears to be stalled. No updates for over 5 minutes.', 'mxchat');
726
727 // Save the updated status
728 set_transient(
729 sanitize_key('mxchat_pdf_status_' . md5($pdf_url)),
730 array_map('sanitize_text_field', $status),
731 DAY_IN_SECONDS
732 );
733 }
734
735 $result = array(
736 'total_pages' => absint($status['total_pages']),
737 'processed_pages' => absint($status['processed_pages']),
738 'failed_pages' => absint($status['failed_pages'] ?? 0),
739 'percentage' => ($status['total_pages'] > 0)
740 ? round((absint($status['processed_pages']) / absint($status['total_pages'])) * 100)
741 : 0,
742 'status' => sanitize_text_field($status['status']),
743 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
744 'failed_pages_list' => isset($status['failed_pages_list']) ? $status['failed_pages_list'] : array(),
745 'completion_summary' => isset($status['completion_summary']) ? $status['completion_summary'] : null
746 );
747
748 // Add error message if present
749 if (isset($status['error']) && !empty($status['error'])) {
750 $result['error'] = sanitize_text_field($status['error']);
751 }
752
753 return $result;
754 }
755
756
757 public function mxchat_handle_sitemap_submission() {
758 // Start logging the submission process
759 //error_log('[MXCHAT-URL] ===== Starting URL submission process =====');
760
761 // Check if the form was submitted and verify permissions
762 if (!isset($_POST['submit_sitemap']) || !current_user_can('manage_options')) {
763 //error_log('[MXCHAT-URL] Error: Unauthorized access or form not submitted properly');
764 wp_die(esc_html__('Unauthorized access', 'mxchat'));
765 }
766
767 // Verify nonce
768 //error_log('[MXCHAT-URL] Verifying nonce');
769 check_admin_referer('mxchat_submit_sitemap_action', 'mxchat_submit_sitemap_nonce');
770
771 // Validate URL
772 if (!isset($_POST['sitemap_url']) || empty($_POST['sitemap_url'])) {
773 //error_log('[MXCHAT-URL] Error: Empty or missing URL');
774 set_transient('mxchat_admin_notice_error',
775 esc_html__('Please provide a valid URL.', 'mxchat'),
776 30
777 );
778 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
779 exit;
780 }
781
782 $submitted_url = esc_url_raw($_POST['sitemap_url']);
783 //error_log('[MXCHAT-URL] Processing URL: ' . $submitted_url);
784
785 // Validate API key first
786 $options = get_option('mxchat_options');
787 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
788
789 if (strpos($selected_model, 'voyage') === 0) {
790 $api_key = $options['voyage_api_key'] ?? '';
791 $provider_name = 'Voyage AI';
792 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
793 $api_key = $options['gemini_api_key'] ?? '';
794 $provider_name = 'Google Gemini';
795 } else {
796 $api_key = $options['api_key'] ?? '';
797 $provider_name = 'OpenAI';
798 }
799
800 if (empty($api_key)) {
801 $error_message = sprintf(
802 esc_html__('%s API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
803 $provider_name
804 );
805 //error_log('[MXCHAT-URL] Error: ' . $error_message);
806 set_transient('mxchat_admin_notice_error', $error_message, 30);
807 //error_log('[MXCHAT-URL] Set error transient: ' . $error_message);
808 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
809 exit;
810 }
811
812 //error_log('[MXCHAT-URL] Fetching URL content');
813 $response = wp_remote_get($submitted_url, array('timeout' => 30));
814
815 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
816 $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response);
817 //error_log('[MXCHAT-URL] Error fetching URL: ' . $error_message);
818 set_transient('mxchat_admin_notice_error',
819 sprintf(
820 esc_html__('Failed to fetch the URL: %s', 'mxchat'),
821 esc_html($error_message)
822 ),
823 30
824 );
825 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
826 exit;
827 }
828
829 $content_type = wp_remote_retrieve_header($response, 'content-type');
830 //error_log('[MXCHAT-URL] Content type: ' . $content_type);
831 $body_content = wp_remote_retrieve_body($response);
832
833 if (empty($body_content)) {
834 //error_log('[MXCHAT-URL] Error: Empty response body');
835 set_transient('mxchat_admin_notice_error',
836 esc_html__('Empty response received from URL.', 'mxchat'),
837 30
838 );
839 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
840 exit;
841 }
842 //error_log('[MXCHAT-URL] Retrieved body content length: ' . strlen($body_content) . ' bytes');
843
844 // Handle PDF URL
845 if ($this->is_pdf_url($submitted_url, $response)) {
846 //error_log('[MXCHAT-URL] Detected PDF URL, handling PDF for knowledge base');
847 $result = $this->handle_pdf_for_knowledge_base($submitted_url, $response);
848 //error_log('[MXCHAT-URL] PDF handling result: ' . $result);
849
850 if ($result === 'scheduled') {
851 set_transient(
852 'mxchat_last_pdf_url',
853 sanitize_text_field($submitted_url),
854 DAY_IN_SECONDS
855 );
856 set_transient('mxchat_admin_notice_info',
857 esc_html__('PDF processing has started in the background. You can check the progress in the Knowledge Base section.', 'mxchat'),
858 30
859 );
860 } else {
861 set_transient('mxchat_admin_notice_error',
862 esc_html__('Failed to start PDF processing: ', 'mxchat') . esc_html($result),
863 30
864 );
865 }
866
867 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
868 exit;
869 }
870
871 // Handle Sitemap XML
872 if (strpos($content_type, 'xml') !== false || strpos($body_content, '<urlset') !== false) {
873 //error_log('[MXCHAT-URL] Detected XML content, processing as sitemap');
874 libxml_use_internal_errors(true);
875 $xml = simplexml_load_string($body_content);
876 $xml_errors = libxml_get_errors();
877 libxml_clear_errors();
878
879 if ($xml === false || !empty($xml_errors)) {
880 //error_log('[MXCHAT-URL] Error: Invalid XML format');
881 if (!empty($xml_errors)) {
882 foreach ($xml_errors as $error) {
883 //error_log('[MXCHAT-URL] XML Error: ' . $error->message);
884 }
885 }
886
887 set_transient('mxchat_admin_notice_error',
888 esc_html__('Invalid sitemap XML. Please provide a valid sitemap.', 'mxchat'),
889 30
890 );
891 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
892 exit;
893 }
894
895 //error_log('[MXCHAT-URL] Valid XML found, handling sitemap for knowledge base');
896 $result = $this->handle_sitemap_for_knowledge_base($xml, $submitted_url);
897 //error_log('[MXCHAT-URL] Sitemap handling result: ' . $result);
898
899 if ($result === 'scheduled') {
900 set_transient(
901 'mxchat_last_sitemap_url',
902 sanitize_text_field($submitted_url),
903 DAY_IN_SECONDS
904 );
905 set_transient('mxchat_admin_notice_info',
906 esc_html__('Sitemap processing has started in the background. You can check the progress in the Knowledge Base section.', 'mxchat'),
907 30
908 );
909 } else {
910 // Return to the admin page without a redirect for better error display
911 // The error is already stored in the sitemap status transient
912 set_transient('mxchat_admin_notice_error',
913 esc_html__('Failed to start sitemap processing. Please check the status below for details.', 'mxchat'),
914 30
915 );
916 }
917
918 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
919 exit;
920 }
921
922 // Handle Regular URL
923 //error_log('[MXCHAT-URL] Processing as regular webpage');
924 $page_content = $this->mxchat_extract_main_content($body_content);
925 //error_log('[MXCHAT-URL] Extracted content length: ' . strlen($page_content) . ' bytes');
926
927 $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
928 //error_log('[MXCHAT-URL] Sanitized content length: ' . strlen($sanitized_content) . ' bytes');
929
930 if (empty($sanitized_content)) {
931 //error_log('[MXCHAT-URL] Error: No valid content after sanitization');
932
933 // Set both transients - the error notice and the URL status
934 set_transient('mxchat_admin_notice_error',
935 esc_html__('No valid content found on the provided URL.', 'mxchat'),
936 30
937 );
938
939 // Set URL status transient
940 set_transient('mxchat_single_url_status', [
941 'url' => $submitted_url,
942 'timestamp' => current_time('mysql'),
943 'status' => 'failed',
944 'error' => esc_html__('No valid content found on the provided URL.', 'mxchat')
945 ], DAY_IN_SECONDS);
946
947 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
948 exit;
949 }
950
951 //error_log('[MXCHAT-URL] Generating embedding for content');
952 $embedding_vector = $this->mxchat_generate_embedding($sanitized_content);
953
954 // Check if embedding_vector is a string (error message)
955 if (is_string($embedding_vector)) {
956 //error_log('[MXCHAT-URL] Error generating embedding: ' . $embedding_vector);
957 $error_message = esc_html__('Failed to generate embedding: ', 'mxchat') . esc_html($embedding_vector);
958 //error_log('[MXCHAT-URL] Setting error transient: ' . $error_message);
959
960 // Set both transients
961 set_transient('mxchat_admin_notice_error', $error_message, 30);
962
963 // Set URL status transient
964 set_transient('mxchat_single_url_status', [
965 'url' => $submitted_url,
966 'timestamp' => current_time('mysql'),
967 'status' => 'failed',
968 'error' => $error_message
969 ], DAY_IN_SECONDS);
970
971 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
972 exit;
973 }
974
975 if (is_array($embedding_vector)) {
976 //error_log('[MXCHAT-URL] Successfully generated embedding with ' . count($embedding_vector) . ' dimensions');
977
978 $db_result = MxChat_Utils::submit_content_to_db(
979 $sanitized_content,
980 $submitted_url,
981 $api_key
982 );
983
984 if (is_wp_error($db_result)) {
985 //error_log('[MXCHAT-URL] Error: Failed to store content in database: ' . $db_result->get_error_message());
986 $error_message = esc_html__('Failed to store content in database: ', 'mxchat') . esc_html($db_result->get_error_message());
987 //error_log('[MXCHAT-URL] Setting error transient: ' . $error_message);
988
989 // Set both transients
990 set_transient('mxchat_admin_notice_error', $error_message, 30);
991
992 // Set URL status transient
993 set_transient('mxchat_single_url_status', [
994 'url' => $submitted_url,
995 'timestamp' => current_time('mysql'),
996 'status' => 'failed',
997 'error' => $error_message
998 ], DAY_IN_SECONDS);
999
1000 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1001 exit;
1002 }
1003
1004 //error_log('[MXCHAT-URL] Successfully stored content in database');
1005 $success_message = esc_html__('URL content successfully submitted!', 'mxchat');
1006 //error_log('[MXCHAT-URL] Setting success transient: ' . $success_message);
1007
1008 // Set both transients
1009 set_transient('mxchat_admin_notice_success', $success_message, 30);
1010
1011 // Set URL status transient with success
1012 set_transient('mxchat_single_url_status', [
1013 'url' => $submitted_url,
1014 'timestamp' => current_time('mysql'),
1015 'status' => 'complete',
1016 'content_length' => strlen($sanitized_content),
1017 'embedding_dimensions' => count($embedding_vector)
1018 ], DAY_IN_SECONDS);
1019
1020 } else {
1021 //error_log('[MXCHAT-URL] Error: Failed to generate embedding. Unexpected result type: ' . gettype($embedding_vector));
1022 $error_message = esc_html__('Failed to generate embedding: Unexpected result type. Please check your API key and try again.', 'mxchat');
1023 //error_log('[MXCHAT-URL] Setting error transient: ' . $error_message);
1024
1025 // Set both transients
1026 set_transient('mxchat_admin_notice_error', $error_message, 30);
1027
1028 // Set URL status transient
1029 set_transient('mxchat_single_url_status', [
1030 'url' => $submitted_url,
1031 'timestamp' => current_time('mysql'),
1032 'status' => 'failed',
1033 'error' => $error_message
1034 ], DAY_IN_SECONDS);
1035 }
1036
1037 //error_log('[MXCHAT-URL] ===== Completed URL submission process =====');
1038 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1039 exit;
1040 }
1041 public function get_single_url_status() {
1042 $status = get_transient('mxchat_single_url_status');
1043 if (!$status) {
1044 return null;
1045 }
1046
1047 // Add human-readable time
1048 if (isset($status['timestamp'])) {
1049 $status['human_time'] = human_time_diff(strtotime($status['timestamp']), current_time('timestamp')) . ' ' . __('ago', 'mxchat');
1050 }
1051
1052 return $status;
1053 }
1054 public function handle_sitemap_for_knowledge_base($xml, $sitemap_url) {
1055 // Clear any single URL status when starting sitemap processing
1056 delete_transient('mxchat_single_url_status');
1057 if (!current_user_can('manage_options')) {
1058 //error_log(esc_html__('Unauthorized sitemap processing attempt', 'mxchat'));
1059 return false;
1060 }
1061
1062 try {
1063 $sitemap_url = esc_url_raw($sitemap_url);
1064
1065 if (!$xml || !is_object($xml)) {
1066 throw new Exception(__('Invalid XML object provided', 'mxchat'));
1067 }
1068
1069 // Add embedding validation before processing
1070 // Test embedding with a small sample text to verify API key is working
1071 $test_result = $this->mxchat_generate_embedding("This is a test to verify the embedding API key is working.");
1072
1073 // Check if test_result is a string (error message) rather than an array (valid embedding)
1074 if (is_string($test_result)) {
1075 //error_log('[MXCHAT-URL] Embedding API validation failed: ' . $test_result);
1076
1077 // Store the error in the status transient so it can be displayed later
1078 $status_data = array(
1079 'total_urls' => 0,
1080 'processed_urls' => 0,
1081 'status' => 'error',
1082 'error' => __('Embedding API validation failed: ', 'mxchat') . $test_result,
1083 'last_update' => time()
1084 );
1085
1086 set_transient(
1087 sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)),
1088 array_map('sanitize_text_field', $status_data),
1089 DAY_IN_SECONDS
1090 );
1091
1092 throw new Exception(__('Embedding API validation failed: ', 'mxchat') . $test_result);
1093 }
1094
1095 // Make sure it's an array (valid embedding)
1096 if (!is_array($test_result)) {
1097 //error_log('[MXCHAT-URL] Embedding API returned unexpected result type: ' . gettype($test_result));
1098 throw new Exception(__('Embedding API returned unexpected result type. Please check your configuration.', 'mxchat'));
1099 }
1100
1101 $urls = [];
1102 foreach ($xml->url as $url_element) {
1103 $url = esc_url_raw((string)$url_element->loc);
1104 if ($url) {
1105 $urls[] = $url;
1106 }
1107 }
1108
1109 $total_urls = absint(count($urls));
1110
1111 if ($total_urls < 1) {
1112 throw new Exception(__('No valid URLs found in sitemap', 'mxchat'));
1113 }
1114
1115 wp_schedule_single_event(time(), 'mxchat_process_sitemap_urls', array(
1116 'urls' => $urls,
1117 'sitemap_url' => $sitemap_url,
1118 'total_urls' => $total_urls,
1119 'batch_size' => absint(10),
1120 'batch_pause' => absint(5)
1121 ));
1122
1123 $status_data = array(
1124 'total_urls' => $total_urls,
1125 'processed_urls' => 0,
1126 'status' => 'processing',
1127 'last_update' => time()
1128 );
1129
1130 set_transient(
1131 sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)),
1132 array_map('sanitize_text_field', $status_data),
1133 DAY_IN_SECONDS
1134 );
1135
1136 return __('scheduled', 'mxchat');
1137
1138 } catch (\Exception $e) {
1139 $error_message = $e->getMessage();
1140 //error_log(sprintf(esc_html__('Error preparing sitemap for processing: %s', 'mxchat'), esc_html($error_message)));
1141
1142 // Store the sitemap URL and error in transients so they can be displayed
1143 set_transient(
1144 'mxchat_last_sitemap_url',
1145 sanitize_text_field($sitemap_url),
1146 DAY_IN_SECONDS
1147 );
1148
1149 $status_data = array(
1150 'total_urls' => 0,
1151 'processed_urls' => 0,
1152 'status' => 'error',
1153 'error' => $error_message,
1154 'last_update' => time()
1155 );
1156
1157 set_transient(
1158 sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)),
1159 array_map('sanitize_text_field', $status_data),
1160 DAY_IN_SECONDS
1161 );
1162
1163 return $error_message;
1164 }
1165 }
1166
1167 public function process_sitemap_urls_cron($urls, $sitemap_url, $total_urls, $batch_size, $batch_pause) {
1168 // Validate inputs
1169 $sitemap_url = esc_url_raw($sitemap_url);
1170 $total_urls = absint($total_urls);
1171 $batch_size = absint($batch_size);
1172 $batch_pause = absint($batch_pause);
1173
1174 if (!is_array($urls) || empty($urls)) {
1175 return;
1176 }
1177
1178 try {
1179 $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
1180 $status = get_transient($status_key);
1181
1182 if (!$status || !is_array($status)) {
1183 throw new Exception('Invalid status data retrieved from transient');
1184 }
1185
1186 // Initialize failed_urls array if it doesn't exist
1187 if (!isset($status['failed_urls_list']) || !is_array($status['failed_urls_list'])) {
1188 $status['failed_urls_list'] = [];
1189 }
1190
1191 $start_url = absint($status['processed_urls']);
1192 $end_url = min($start_url + $batch_size, $total_urls);
1193
1194 // Track batch statistics
1195 $batch_stats = [
1196 'processed' => 0,
1197 'failed' => 0,
1198 'last_error' => '',
1199 'embedding_errors' => 0
1200 ];
1201
1202 // Check embedding configuration with first URL (only on first batch)
1203 if ($start_url === 0) {
1204 $test_url = esc_url_raw($urls[0]);
1205 $test_response = wp_remote_get($test_url);
1206
1207 if (!is_wp_error($test_response) && wp_remote_retrieve_response_code($test_response) === 200) {
1208 $test_html = wp_remote_retrieve_body($test_response);
1209 $test_content = $this->mxchat_extract_main_content($test_html);
1210 $test_sanitized = $this->mxchat_sanitize_content_for_api($test_content);
1211
1212 if (!empty($test_sanitized)) {
1213 $test_embedding = $this->mxchat_generate_embedding($test_sanitized);
1214
1215 if (is_string($test_embedding)) {
1216 throw new Exception('Embedding generation failed: ' . $test_embedding);
1217 }
1218
1219 if (!is_array($test_embedding)) {
1220 throw new Exception('Embedding generation returned unexpected result type: ' . gettype($test_embedding));
1221 }
1222 }
1223 }
1224 }
1225
1226 for ($i = $start_url; $i < $end_url; $i++) {
1227 $page_url = esc_url_raw($urls[$i]);
1228 $max_retries = 3;
1229 $retry_count = 0;
1230 $url_processed = false;
1231 $last_error = '';
1232
1233 while (!$url_processed && $retry_count < $max_retries) {
1234 try {
1235 // Attempt to fetch the URL
1236 $page_response = wp_remote_get($page_url, array('timeout' => 30));
1237
1238 if (is_wp_error($page_response)) {
1239 throw new Exception('HTTP request failed: ' . $page_response->get_error_message());
1240 }
1241
1242 $response_code = wp_remote_retrieve_response_code($page_response);
1243 if ($response_code !== 200) {
1244 throw new Exception('HTTP Status: ' . $response_code);
1245 }
1246
1247 $page_html = wp_remote_retrieve_body($page_response);
1248
1249 if (empty($page_html)) {
1250 throw new Exception('Empty response body');
1251 }
1252
1253 $page_content = $this->mxchat_extract_main_content($page_html);
1254 $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
1255
1256 if (empty($sanitized_content)) {
1257 throw new Exception('No valid content found after processing');
1258 }
1259
1260 $embedding_vector = $this->mxchat_generate_embedding($sanitized_content);
1261
1262 if (is_string($embedding_vector)) {
1263 throw new Exception('Embedding generation failed: ' . $embedding_vector);
1264 }
1265
1266 if (!is_array($embedding_vector)) {
1267 throw new Exception('Embedding generation returned unexpected result type: ' . gettype($embedding_vector));
1268 }
1269
1270 // Submit to database
1271 $options = get_option('mxchat_options');
1272 $submission_result = MxChat_Utils::submit_content_to_db($sanitized_content, $page_url, $options['api_key']);
1273
1274 if (is_wp_error($submission_result)) {
1275 throw new Exception('Database submission failed: ' . $submission_result->get_error_message());
1276 }
1277
1278 // Success!
1279 $url_processed = true;
1280 $batch_stats['processed']++;
1281
1282 } catch (Exception $e) {
1283 $retry_count++;
1284 $last_error = $e->getMessage();
1285
1286 //error_log("URL {$page_url} failed (attempt {$retry_count}/{$max_retries}): " . $last_error);
1287
1288 // Track embedding errors specifically
1289 if (strpos($last_error, 'Embedding') !== false) {
1290 $batch_stats['embedding_errors']++;
1291 }
1292
1293 if ($retry_count < $max_retries) {
1294 // Wait before retry (exponential backoff: 1s, 2s, 4s)
1295 sleep(pow(2, $retry_count - 1));
1296 }
1297 }
1298 }
1299
1300 // If URL still not processed after all retries, mark as failed
1301 if (!$url_processed) {
1302 $batch_stats['failed']++;
1303 $batch_stats['last_error'] = $last_error;
1304
1305 // Add to failed URLs list
1306 $status['failed_urls_list'][] = [
1307 'url' => $page_url,
1308 'error' => $last_error,
1309 'time' => time(),
1310 'retries' => $max_retries
1311 ];
1312
1313 // Limit the number of failed URLs we store to prevent transient size issues
1314 if (count($status['failed_urls_list']) > 100) {
1315 $status['failed_urls_list'] = array_slice($status['failed_urls_list'], -100);
1316 }
1317 }
1318
1319 // Update progress
1320 $status['processed_urls'] = absint($i + 1);
1321 $status['last_update'] = time();
1322 $status['failed_urls'] = absint($status['failed_urls'] ?? 0) + ($url_processed ? 0 : 1);
1323 $status['last_error'] = $batch_stats['last_error'];
1324
1325 set_transient($status_key, $status, DAY_IN_SECONDS);
1326
1327 // If we have too many consecutive embedding errors, stop processing
1328 if ($batch_stats['embedding_errors'] >= 10) {
1329 throw new Exception('Too many consecutive embedding failures detected. Please check your API configuration.');
1330 }
1331 }
1332
1333 // If all URLs in this batch failed, stop processing
1334 if ($batch_stats['processed'] === 0 && $batch_stats['failed'] > 0) {
1335 $status['status'] = 'error';
1336 $status['error'] = sprintf(
1337 'Processing stopped: %d consecutive failures in batch. Last error: %s',
1338 $batch_stats['failed'],
1339 $batch_stats['last_error']
1340 );
1341 set_transient($status_key, $status, DAY_IN_SECONDS);
1342 return;
1343 }
1344
1345 // Update final progress
1346 $status['processed_urls'] = min($end_url, $total_urls);
1347 $status['last_update'] = time();
1348 set_transient($status_key, $status, DAY_IN_SECONDS);
1349
1350 // Check if we've processed all URLs
1351 if ($end_url >= $total_urls) {
1352 // All URLs have been processed - mark as complete
1353 $status['status'] = 'complete';
1354 $status['processed_urls'] = $total_urls;
1355
1356 // Add completion summary
1357 $status['completion_summary'] = [
1358 'total_urls' => $total_urls,
1359 'successful_urls' => $total_urls - absint($status['failed_urls'] ?? 0),
1360 'failed_urls' => absint($status['failed_urls'] ?? 0),
1361 'completion_time' => current_time('mysql')
1362 ];
1363
1364 // Save the completed status (don't delete it - let user dismiss manually)
1365 set_transient($status_key, $status, DAY_IN_SECONDS);
1366
1367 // DON'T delete the status transients here - let user dismiss manually
1368 } else {
1369 // Schedule next batch
1370 wp_schedule_single_event(time() + $batch_pause, 'mxchat_process_sitemap_urls', array(
1371 'urls' => $urls,
1372 'sitemap_url' => $sitemap_url,
1373 'total_urls' => $total_urls,
1374 'batch_size' => $batch_size,
1375 'batch_pause' => $batch_pause,
1376 ));
1377 }
1378 } catch (\Exception $e) {
1379 $status['status'] = 'error';
1380 $status['error'] = $e->getMessage();
1381 set_transient($status_key, $status, DAY_IN_SECONDS);
1382 }
1383 }
1384 public function mxchat_sanitize_content_for_api($content) {
1385 //error_log('[MXCHAT-SANITIZE] Original content preview: ' . substr($content, 0, 500) . '...');
1386
1387 // Remove script, style tags, and HTML comments
1388 $content = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $content);
1389 $content = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $content);
1390 $content = preg_replace('/<!--(.|\s)*?-->/', '', $content);
1391
1392 // Remove all HTML tags and decode HTML entities
1393 $content = wp_strip_all_tags($content);
1394 $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5);
1395
1396 // Normalize whitespace but preserve paragraph breaks
1397 // First, normalize line endings to \n
1398 $content = str_replace(["\r\n", "\r"], "\n", $content);
1399 // Replace multiple spaces/tabs with single space, but preserve newlines
1400 $content = preg_replace('/[ \t]+/', ' ', $content);
1401 // Replace 3+ newlines with 2 newlines (max 2 blank lines)
1402 $content = preg_replace('/\n{3,}/', "\n\n", $content);
1403 // Trim each line
1404 $lines = explode("\n", $content);
1405 $lines = array_map('trim', $lines);
1406 $content = implode("\n", $lines);
1407 // Final trim
1408 $content = trim($content);
1409
1410 // Remove control characters (which can cause database issues) but preserve \n (0x0A) and \r (0x0D)
1411 $content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $content);
1412
1413 // Remove NULL bytes which can cause database errors
1414 $content = str_replace("\0", "", $content);
1415
1416 // Ensure valid UTF-8 encoding
1417 $content = wp_check_invalid_utf8($content);
1418
1419 // Remove any extremely long strings without spaces (often garbage)
1420 $content = preg_replace('/\S{300,}/', ' ', $content);
1421
1422 // Replace problematic characters that often cause database issues
1423 $content = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $content); // Remove emoji and other high Unicode characters
1424
1425 // Replace any remaining potentially problematic characters with spaces
1426 // BUT preserve newlines by temporarily replacing them
1427 $content = str_replace("\n", "NEWLINE_PLACEHOLDER", $content);
1428 $content = preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\p{Sm}]/u', ' ', $content);
1429 $content = str_replace("NEWLINE_PLACEHOLDER", "\n", $content);
1430
1431 // Limit to reasonable length if needed
1432 $max_length = 65000; // Just under MySQL TEXT field limit
1433 if (strlen($content) > $max_length) {
1434 $content = substr($content, 0, $max_length);
1435 }
1436
1437 //error_log('[MXCHAT-SANITIZE] Sanitized content preview: ' . substr($content, 0, 500) . '...');
1438 return $content;
1439 }
1440 public static function mxchat_extract_main_content($html) {
1441 if (empty($html)) {
1442 return '';
1443 }
1444 try {
1445 $dom = new DOMDocument;
1446 libxml_use_internal_errors(true); // Suppress HTML parsing errors
1447 @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
1448 $xpath = new DOMXPath($dom);
1449
1450 // For debugging purposes
1451 $debugEnabled = false; // Set to true to enable debugging output
1452 $debug = function($message) use ($debugEnabled) {
1453 if ($debugEnabled) {
1454 //error_log('[MXCHAT-DEBUG] ' . $message);
1455 }
1456 };
1457
1458 // Direct targeting for Gerow theme posts
1459 $post_text = $xpath->query('//div[contains(@class, "post-text")]');
1460 if ($post_text && $post_text->length > 0) {
1461 $debug("Found post-text directly");
1462 $content = '';
1463 foreach ($post_text as $node) {
1464 $content .= $dom->saveHTML($node);
1465 }
1466 if (!empty($content)) {
1467 $debug("Returning post-text content");
1468 return $content;
1469 }
1470 }
1471
1472 // Try to get the blog details content which contains the post-text
1473 $blog_details = $xpath->query('//div[contains(@class, "blog-details-content")]');
1474 if ($blog_details && $blog_details->length > 0) {
1475 $debug("Found blog-details-content");
1476 $content = '';
1477 foreach ($blog_details as $node) {
1478 $content .= $dom->saveHTML($node);
1479 }
1480 if (!empty($content)) {
1481 $debug("Returning blog-details-content");
1482 return $content;
1483 }
1484 }
1485
1486 // Try to get the article which contains the blog details
1487 $article = $xpath->query('//article[contains(@class, "blog-details-wrap")]');
1488 if ($article && $article->length > 0) {
1489 $debug("Found article with blog-details-wrap");
1490 $content = '';
1491 foreach ($article as $node) {
1492 $content .= $dom->saveHTML($node);
1493 }
1494 if (!empty($content)) {
1495 $debug("Returning article content");
1496 return $content;
1497 }
1498 }
1499
1500 // Try even broader with the blog-item-wrap
1501 $blog_item = $xpath->query('//div[contains(@class, "blog-item-wrap")]');
1502 if ($blog_item && $blog_item->length > 0) {
1503 $debug("Found blog-item-wrap");
1504 $content = '';
1505 foreach ($blog_item as $node) {
1506 $content .= $dom->saveHTML($node);
1507 }
1508 if (!empty($content)) {
1509 $debug("Returning blog-item-wrap content");
1510 return $content;
1511 }
1512 }
1513
1514 // Specific Gerow theme path
1515 $gerow_path = $xpath->query('//section[contains(@class, "blog-area")]//div[contains(@class, "post-text")]');
1516 if ($gerow_path && $gerow_path->length > 0) {
1517 $debug("Found Gerow theme path to post-text");
1518 $content = '';
1519 foreach ($gerow_path as $node) {
1520 $content .= $dom->saveHTML($node);
1521 }
1522 if (!empty($content)) {
1523 $debug("Returning Gerow post-text content");
1524 return $content;
1525 }
1526 }
1527
1528 // Generic blog post selectors
1529 $selectors = [
1530 // Blog post specific selectors
1531 '//div[contains(@class, "post-text")]',
1532 '//article[contains(@class, "blog-post-item")]//div[contains(@class, "post-text")]',
1533 '//div[contains(@class, "blog-details-content")]',
1534 '//article[contains(@class, "blog-details-wrap")]',
1535 '//div[contains(@class, "entry-content")]',
1536 '//div[contains(@class, "blog-content")]',
1537 '//div[contains(@class, "blog-item-wrap")]',
1538
1539 // More general content selectors
1540 '//div[contains(@class, "page__content")]',
1541 '//div[contains(@class, "elementor-widget-container")]',
1542 '//div[contains(@class, "elementor-text-editor")]',
1543 '//div[contains(@class, "elementor-widget-text-editor")]',
1544 '//*[contains(@class, "entry-content")]',
1545 '//*[contains(@class, "post-content")]',
1546 '//*[contains(@class, "article-content")]',
1547 '//*[@id="content"]',
1548 '//*[@id="main-content"]',
1549 '//section[contains(@class, "blog-area")]',
1550 '//article',
1551 '//main',
1552 '//div[contains(@class, "content")]'
1553 ];
1554
1555 // First handle Elementor content
1556 $debug("Checking for Elementor content");
1557 $elementor_widgets = $xpath->query('//div[contains(@class, "elementor-element")]//div[contains(@class, "elementor-widget-container")]');
1558 if ($elementor_widgets && $elementor_widgets->length > 0) {
1559 $debug("Found Elementor widgets");
1560 $combined_content = '';
1561 foreach ($elementor_widgets as $widget) {
1562 $widget_content = $dom->saveHTML($widget);
1563 if (!empty($widget_content)) {
1564 $combined_content .= $widget_content;
1565 }
1566 }
1567 if (!empty($combined_content)) {
1568 $debug("Returning Elementor content");
1569 return $combined_content;
1570 }
1571 }
1572
1573 // Try standard selectors one by one
1574 foreach ($selectors as $selector) {
1575 $debug("Trying selector: " . $selector);
1576 $nodes = $xpath->query($selector);
1577 if ($nodes && $nodes->length > 0) {
1578 $debug("Found matches for selector: " . $selector);
1579 $content = '';
1580 foreach ($nodes as $node) {
1581 $content .= $dom->saveHTML($node);
1582 }
1583 if (!empty($content)) {
1584 $debug("Returning content from selector: " . $selector);
1585 return $content;
1586 }
1587 }
1588 }
1589
1590 // Manual regex fallback for post-text if DOM methods fail
1591 $debug("Trying regex fallback");
1592 if (preg_match('/<div class="post-text">(.*?)<\/div>\s*<\/div>\s*<\/div>/s', $html, $matches)) {
1593 $debug("Found post-text via regex");
1594 return '<div class="post-text">' . $matches[1] . '</div>';
1595 }
1596
1597 // Try to extract the blog section as a whole
1598 $blog_section = $xpath->query('//section[contains(@class, "blog-area")]');
1599 if ($blog_section && $blog_section->length > 0) {
1600 $debug("Found blog-area section");
1601 $content = '';
1602 foreach ($blog_section as $node) {
1603 $content .= $dom->saveHTML($node);
1604 }
1605 if (!empty($content)) {
1606 $debug("Returning blog-area section content");
1607 return $content;
1608 }
1609 }
1610
1611 // Fallback: Return the body content if no specific selector matches
1612 $debug("Using body fallback");
1613 $body = $dom->getElementsByTagName('body');
1614 if ($body->length > 0) {
1615 return $dom->saveHTML($body->item(0));
1616 }
1617
1618 // Last resort: return the original HTML
1619 $debug("Returning original HTML");
1620 return $html;
1621 } catch (Exception $e) {
1622 //error_log('[MXCHAT-ERROR] Content extraction failed: ' . $e->getMessage());
1623 return $html; // Return original HTML if parsing fails
1624 } finally {
1625 libxml_clear_errors();
1626 }
1627 }
1628 public function get_sitemap_processing_status($sitemap_url) {
1629 $sitemap_url = esc_url_raw($sitemap_url);
1630 $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
1631 $status = get_transient($status_key);
1632
1633 if (!$status || !is_array($status)) {
1634 return false;
1635 }
1636
1637 // Auto-complete check: if all URLs are processed but status isn't complete
1638 if (isset($status['processed_urls']) && isset($status['total_urls']) &&
1639 $status['processed_urls'] >= $status['total_urls'] &&
1640 isset($status['status']) && $status['status'] !== 'complete' &&
1641 $status['status'] !== 'error') {
1642
1643 // Mark as complete
1644 $status['status'] = 'complete';
1645 $status['processed_urls'] = $status['total_urls']; // Ensure exact match
1646
1647 // Update the transient with the corrected status
1648 set_transient($status_key, $status, DAY_IN_SECONDS);
1649 }
1650
1651 return array(
1652 'total_urls' => absint($status['total_urls']),
1653 'processed_urls' => absint($status['processed_urls']),
1654 'failed_urls' => absint($status['failed_urls'] ?? 0),
1655 'percentage' => ($status['total_urls'] > 0)
1656 ? round((absint($status['processed_urls']) / absint($status['total_urls'])) * 100)
1657 : 0,
1658 'status' => sanitize_text_field($status['status']),
1659 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
1660 'error' => isset($status['error']) ? sanitize_text_field($status['error']) : '',
1661 'last_error' => isset($status['last_error']) ? sanitize_text_field($status['last_error']) : '',
1662 'failed_urls_list' => isset($status['failed_urls_list']) ? $status['failed_urls_list'] : array()
1663 );
1664 }
1665
1666 public function ajax_get_status_updates() {
1667 try {
1668 // Verify the request
1669 check_ajax_referer('mxchat_status_nonce', 'nonce');
1670
1671 // Get the status just like in your admin page
1672 $pdf_url = get_transient('mxchat_last_pdf_url');
1673 $sitemap_url = get_transient('mxchat_last_sitemap_url');
1674
1675 $pdf_status = $pdf_url ? $this->get_pdf_processing_status($pdf_url) : false;
1676 $sitemap_status = $sitemap_url ? $this->get_sitemap_processing_status($sitemap_url) : false;
1677
1678 // Add the PDF URL to the status object
1679 if ($pdf_status && $pdf_url) {
1680 $pdf_status['pdf_url'] = $pdf_url;
1681 }
1682
1683 // Set the current PDF URL for the manual batch processing button
1684 $current_pdf_url = $pdf_url;
1685
1686 // Check for true processing status, not just presence of status
1687 $is_active_processing =
1688 ($sitemap_status && isset($sitemap_status['status']) && $sitemap_status['status'] === 'processing') ||
1689 ($pdf_status && isset($pdf_status['status']) && $pdf_status['status'] === 'processing');
1690
1691 // Get single URL status, but only if no processing is active
1692 $single_url_status = !$is_active_processing ? $this->get_single_url_status() : false;
1693
1694 // REMOVED: Auto-clearing of completed status - now only done via dismiss button
1695
1696 // Return JSON response with the status data
1697 wp_send_json(array(
1698 'pdf_status' => $pdf_status,
1699 'sitemap_status' => $sitemap_status,
1700 'single_url_status' => $single_url_status,
1701 'is_processing' => $is_active_processing,
1702 'current_pdf_url' => $current_pdf_url
1703 ));
1704
1705 } catch (Exception $e) {
1706 // Log the error
1707 //error_log('MxChat Status Update Error: ' . $e->getMessage());
1708
1709 // Return a friendly error response
1710 wp_send_json_error(array(
1711 'message' => 'Error getting status updates: ' . $e->getMessage(),
1712 'status' => 'error'
1713 ));
1714 }
1715 }
1716 public function mxchat_stop_processing() {
1717 // Verify permissions
1718 if (!current_user_can('manage_options')) {
1719 wp_die(esc_html__('Unauthorized access', 'mxchat'));
1720 }
1721
1722 // Verify nonce
1723 check_admin_referer('mxchat_stop_processing_action', 'mxchat_stop_processing_nonce');
1724
1725 // Get the last sitemap URL and clear its transient
1726 $sitemap_url = get_transient('mxchat_last_sitemap_url');
1727 if ($sitemap_url) {
1728 delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
1729 delete_transient('mxchat_last_sitemap_url');
1730 }
1731
1732 // Get the last PDF URL and clear its transient
1733 $pdf_url = get_transient('mxchat_last_pdf_url');
1734 if ($pdf_url) {
1735 delete_transient('mxchat_pdf_status_' . md5($pdf_url));
1736 delete_transient('mxchat_last_pdf_url');
1737 }
1738
1739 // Unschedule any pending sitemap events
1740 $timestamp = wp_next_scheduled('mxchat_process_sitemap_urls');
1741 if ($timestamp) {
1742 wp_unschedule_event($timestamp, 'mxchat_process_sitemap_urls');
1743 }
1744
1745 // Redirect back with a success message
1746 set_transient('mxchat_admin_notice_success',
1747 esc_html__('Processing has been stopped successfully.', 'mxchat'),
1748 30
1749 );
1750 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
1751 exit;
1752 }
1753 public function ajax_mxchat_get_content_list() {
1754 // Verify the nonce
1755 check_ajax_referer('mxchat_content_selector_nonce', 'nonce');
1756
1757 if (!current_user_can('manage_options')) {
1758 wp_send_json_error(__('Unauthorized access', 'mxchat'));
1759 }
1760
1761 $page = isset($_GET['page']) ? absint($_GET['page']) : 1;
1762 $per_page = isset($_GET['per_page']) ? absint($_GET['per_page']) : 20;
1763 $search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
1764 $post_type = isset($_GET['post_type']) ? sanitize_text_field($_GET['post_type']) : 'all';
1765 $post_status = isset($_GET['post_status']) ? sanitize_text_field($_GET['post_status']) : 'publish';
1766 $processed_filter = isset($_GET['processed_filter']) ? sanitize_text_field($_GET['processed_filter']) : 'all';
1767
1768 // Build query args
1769 $args = array(
1770 'posts_per_page' => $per_page,
1771 'paged' => $page,
1772 'post_status' => $post_status !== 'all' ? $post_status : array('publish', 'draft', 'pending'),
1773 'orderby' => 'date',
1774 'order' => 'DESC',
1775 );
1776
1777 // Handle post types
1778 if ($post_type !== 'all') {
1779 $args['post_type'] = $post_type;
1780 } else {
1781 // Default to post and page if we can't get post types
1782 $args['post_type'] = array('post', 'page');
1783
1784 // Try to get public post types
1785 $public_types = $this->get_public_post_types();
1786 if (is_array($public_types) && !empty($public_types)) {
1787 $args['post_type'] = array_keys($public_types);
1788 }
1789 }
1790
1791 if (!empty($search)) {
1792 $args['s'] = $search;
1793 }
1794
1795 // ================================
1796 // UPDATED: Get already vectorized content from BOTH WordPress DB AND Pinecone
1797 // ================================
1798
1799 // FIRST: Get from WordPress DB (your original working code)
1800 global $wpdb;
1801 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1802 $processed_items = $wpdb->get_results("SELECT id, source_url, timestamp FROM {$table_name}");
1803
1804 // Create a lookup array of processed posts with timestamps
1805 $processed_data = array();
1806
1807 if (!empty($processed_items)) {
1808 foreach ($processed_items as $item) {
1809 $post_id = url_to_postid($item->source_url);
1810 if ($post_id) {
1811 $processed_data[$post_id] = array(
1812 'db_id' => $item->id,
1813 'timestamp' => $item->timestamp,
1814 'url' => $item->source_url,
1815 'source' => 'wordpress'
1816 );
1817 }
1818 }
1819 }
1820
1821 // SECOND: ALSO check Pinecone if it's enabled
1822 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
1823 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1824
1825 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
1826 $pinecone_data = $this->get_pinecone_processed_content($pinecone_options);
1827
1828 // Merge Pinecone data with WordPress data
1829 foreach ($pinecone_data as $post_id => $pinecone_info) {
1830 // If not already found in WordPress DB, add from Pinecone
1831 if (!isset($processed_data[$post_id])) {
1832 $processed_data[$post_id] = $pinecone_info;
1833 }
1834 }
1835 }
1836
1837 // ================================
1838
1839 // Get processed IDs as a simple array for in_array checks
1840 $processed_ids = array_keys($processed_data);
1841
1842 // Handle processed/unprocessed filter
1843 if ($processed_filter === 'processed' && !empty($processed_ids)) {
1844 $args['post__in'] = $processed_ids;
1845 } elseif ($processed_filter === 'unprocessed' && !empty($processed_ids)) {
1846 $args['post__not_in'] = $processed_ids;
1847 }
1848
1849 // Run the query
1850 $query = new WP_Query($args);
1851 $content_items = array();
1852
1853 if ($query->have_posts()) {
1854 while ($query->have_posts()) {
1855 $query->the_post();
1856 $id = get_the_ID();
1857 $post_date = get_the_date();
1858 $excerpt = wp_trim_words(get_the_excerpt(), 20, '...');
1859 $word_count = str_word_count(strip_tags(get_the_content()));
1860
1861 $is_processed = in_array($id, $processed_ids);
1862 $processed_date = '';
1863 $db_record_id = 0;
1864 $data_source = 'none';
1865
1866 if ($is_processed && isset($processed_data[$id])) {
1867 $item_data = $processed_data[$id];
1868 $data_source = $item_data['source'];
1869
1870 if ($data_source === 'wordpress' && isset($item_data['timestamp'])) {
1871 // WordPress DB format
1872 $timestamp = strtotime($item_data['timestamp']);
1873 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
1874 $db_record_id = $item_data['db_id'];
1875 } elseif ($data_source === 'pinecone') {
1876 // Pinecone format
1877 $processed_date = $item_data['processed_date'];
1878 $db_record_id = $item_data['db_id'];
1879 }
1880 }
1881
1882 $content_items[] = array(
1883 'id' => $id,
1884 'title' => get_the_title(),
1885 'permalink' => get_permalink(),
1886 'date' => $post_date,
1887 'type' => get_post_type(),
1888 'status' => get_post_status(),
1889 'excerpt' => $excerpt,
1890 'word_count' => $word_count,
1891 'already_processed' => $is_processed,
1892 'processed_date' => $processed_date,
1893 'db_record_id' => $db_record_id,
1894 'data_source' => $data_source
1895 );
1896 }
1897 wp_reset_postdata();
1898 }
1899
1900 $response = array(
1901 'items' => $content_items,
1902 'total' => $query->found_posts,
1903 'total_pages' => $query->max_num_pages,
1904 'current_page' => $page,
1905 'processed_count' => count($processed_ids)
1906 );
1907
1908 wp_send_json_success($response);
1909 exit;
1910 }
1911 public function ajax_mxchat_process_selected_content() {
1912 // Basic request validation
1913 if (!check_ajax_referer('mxchat_content_selector_nonce', 'nonce', false)) {
1914 wp_send_json_error('Invalid nonce');
1915 exit;
1916 }
1917
1918 if (!current_user_can('manage_options')) {
1919 wp_send_json_error('Unauthorized access');
1920 exit;
1921 }
1922
1923 // Get post IDs - safely parse the array
1924 $post_ids = array();
1925 if (isset($_POST['post_ids']) && is_array($_POST['post_ids'])) {
1926 foreach ($_POST['post_ids'] as $id) {
1927 $post_ids[] = absint($id);
1928 }
1929 }
1930
1931 if (empty($post_ids)) {
1932 wp_send_json_error('No content selected');
1933 exit;
1934 }
1935
1936 // Process only ONE post at a time to avoid request size issues
1937 $post_id = reset($post_ids);
1938 $post = get_post($post_id);
1939
1940 if (!$post) {
1941 wp_send_json_error('Post not found');
1942 exit;
1943 }
1944
1945 // Get minimal content
1946 $content = $post->post_title . "\n\n" . wp_strip_all_tags($post->post_content);
1947 $content = substr($content, 0, 10000); // Limit content size
1948
1949 // Get API key with proper model detection
1950 $options = get_option('mxchat_options');
1951 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
1952
1953 if (strpos($selected_model, 'voyage') === 0) {
1954 $api_key = $options['voyage_api_key'] ?? '';
1955 $provider_name = 'Voyage AI';
1956 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
1957 $api_key = $options['gemini_api_key'] ?? '';
1958 $provider_name = 'Google Gemini';
1959 } else {
1960 $api_key = $options['api_key'] ?? '';
1961 $provider_name = 'OpenAI';
1962 }
1963
1964 if (empty($api_key)) {
1965 wp_send_json_error($provider_name . ' API key not configured');
1966 exit;
1967 }
1968
1969 $source_url = get_permalink($post_id);
1970 $vector_id = md5($source_url); // Vector ID for Pinecone
1971
1972 // ================================
1973 // UPDATED: Check for existing content in BOTH sources for backwards compatibility
1974 // ================================
1975
1976 $is_update = false;
1977
1978 // Check WordPress DB first (backwards compatibility)
1979 global $wpdb;
1980 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1981 $existing_record = $wpdb->get_row($wpdb->prepare(
1982 "SELECT id FROM $table_name WHERE source_url = %s",
1983 $source_url
1984 ));
1985
1986 if ($existing_record) {
1987 $is_update = true;
1988 } else {
1989 // Also check Pinecone if enabled
1990 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
1991 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1992
1993 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
1994 $pinecone_data = $this->get_pinecone_processed_content($pinecone_options);
1995 if (isset($pinecone_data[$post_id])) {
1996 $is_update = true;
1997 }
1998 }
1999 }
2000
2001 // Use the centralized utility function for storage
2002 $result = MxChat_Utils::submit_content_to_db(
2003 $content,
2004 $source_url,
2005 $api_key,
2006 $vector_id
2007 );
2008
2009 if (is_wp_error($result)) {
2010 wp_send_json_error('Storage failed: ' . $result->get_error_message());
2011 exit;
2012 }
2013
2014 // ================================
2015 // NEW: Update caches immediately after successful storage
2016 // ================================
2017
2018 // Check if Pinecone is enabled and update caches
2019 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
2020 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2021
2022 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
2023 // Update vector ID cache for improved fetching
2024 $this->update_pinecone_vector_cache($vector_id);
2025
2026 // Update local processed content cache for immediate UI feedback
2027 $pinecone_cache = get_option('mxchat_pinecone_processed_cache', array());
2028 $pinecone_cache[$post_id] = array(
2029 'db_id' => $vector_id,
2030 'processed_date' => 'Just now',
2031 'url' => $source_url,
2032 'source' => 'pinecone',
2033 'timestamp' => current_time('timestamp')
2034 );
2035 update_option('mxchat_pinecone_processed_cache', $pinecone_cache);
2036
2037 // Also update the general processed content cache
2038 $processed_cache = get_option('mxchat_processed_content_cache', array());
2039 $processed_cache[$post_id] = array(
2040 'db_id' => $vector_id,
2041 'timestamp' => current_time('timestamp'),
2042 'url' => $source_url,
2043 'source' => 'pinecone'
2044 );
2045 update_option('mxchat_processed_content_cache', $processed_cache);
2046 }
2047
2048 $operation_type = $is_update ? 'update' : 'new';
2049
2050 // Success response with minimal data
2051 wp_send_json_success(array(
2052 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully',
2053 'post_id' => $post_id,
2054 'title' => $post->post_title,
2055 'operation_type' => $operation_type,
2056 'vector_id' => $vector_id, // Include vector ID for debugging
2057 'cache_updated' => $use_pinecone // Indicate if cache was updated
2058 ));
2059 exit;
2060 }
2061 public function get_public_post_types() {
2062 $post_types = get_post_types(array('public' => true), 'objects');
2063 $post_type_options = array();
2064
2065 foreach ($post_types as $post_type) {
2066 $post_type_options[$post_type->name] = $post_type->label;
2067 }
2068
2069 return $post_type_options;
2070 }
2071 public function get_pinecone_processed_content($pinecone_options) {
2072 // First check local cache for immediate updates
2073 $cached_data = get_option('mxchat_pinecone_processed_cache', array());
2074
2075 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2076 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2077
2078 if (empty($api_key) || empty($host)) {
2079 // Return only cached data if API credentials are missing
2080 return $cached_data;
2081 }
2082
2083 $pinecone_data = array();
2084
2085 try {
2086 // Method 1: Try to get vectors using cached vector IDs first
2087 $cached_vector_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
2088
2089 if (!empty($cached_vector_ids)) {
2090 $pinecone_data = $this->fetch_pinecone_vectors_by_ids($pinecone_options, $cached_vector_ids);
2091 }
2092
2093 // Method 2: If no cached IDs or fetch failed, use scanning approach
2094 if (empty($pinecone_data)) {
2095 $pinecone_data = $this->scan_pinecone_for_processed_content($pinecone_options);
2096 }
2097
2098 // Method 3: Final fallback - try stats endpoint (if available)
2099 if (empty($pinecone_data)) {
2100 $stats_url = "https://{$host}/describe_index_stats";
2101
2102 $response = wp_remote_post($stats_url, array(
2103 'headers' => array(
2104 'Api-Key' => $api_key,
2105 'Content-Type' => 'application/json'
2106 ),
2107 'body' => json_encode(array()),
2108 'timeout' => 30
2109 ));
2110
2111 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2112 $body = wp_remote_retrieve_body($response);
2113 $stats_data = json_decode($body, true);
2114
2115 // Log stats for debugging but don't rely on them for vector listing
2116 //error_log('Pinecone index stats: ' . print_r($stats_data, true));
2117 }
2118 }
2119
2120 } catch (Exception $e) {
2121 //error_log('Pinecone processed content exception: ' . $e->getMessage());
2122 }
2123
2124 // Merge cached data with Pinecone data
2125 // Cache takes priority for recent updates (within last 5 minutes)
2126 $merged_data = $pinecone_data;
2127
2128 foreach ($cached_data as $post_id => $cache_item) {
2129 $cache_timestamp = $cache_item['timestamp'] ?? 0;
2130 $time_diff = current_time('timestamp') - $cache_timestamp;
2131
2132 // If cache item is recent (less than 5 minutes), prioritize it
2133 if ($time_diff < 300) { // 5 minutes = 300 seconds
2134 $merged_data[$post_id] = $cache_item;
2135 } else {
2136 // If not in Pinecone data and cache is old, keep cache but mark as potentially stale
2137 if (!isset($merged_data[$post_id])) {
2138 $merged_data[$post_id] = $cache_item;
2139 }
2140 }
2141 }
2142
2143 return $merged_data;
2144 }
2145 public function fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
2146 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2147 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2148
2149 if (empty($api_key) || empty($host) || empty($vector_ids)) {
2150 return array();
2151 }
2152
2153 try {
2154 $fetch_url = "https://{$host}/vectors/fetch";
2155
2156 // Pinecone fetch API allows fetching specific vectors by ID
2157 $fetch_data = array(
2158 'ids' => array_values($vector_ids)
2159 );
2160
2161 $response = wp_remote_post($fetch_url, array(
2162 'headers' => array(
2163 'Api-Key' => $api_key,
2164 'Content-Type' => 'application/json'
2165 ),
2166 'body' => json_encode($fetch_data),
2167 'timeout' => 30
2168 ));
2169
2170 if (is_wp_error($response)) {
2171 //error_log('Pinecone fetch by IDs error: ' . $response->get_error_message());
2172 return array();
2173 }
2174
2175 $response_code = wp_remote_retrieve_response_code($response);
2176 if ($response_code !== 200) {
2177 //error_log('Pinecone fetch by IDs failed with code: ' . $response_code);
2178 return array();
2179 }
2180
2181 $body = wp_remote_retrieve_body($response);
2182 $data = json_decode($body, true);
2183
2184 if (!isset($data['vectors'])) {
2185 return array();
2186 }
2187
2188 $processed_data = array();
2189
2190 foreach ($data['vectors'] as $vector_id => $vector_data) {
2191 $metadata = $vector_data['metadata'] ?? array();
2192 $source_url = $metadata['source_url'] ?? '';
2193
2194 if (!empty($source_url)) {
2195 $post_id = url_to_postid($source_url);
2196 if ($post_id) {
2197 $created_at = $metadata['created_at'] ?? '';
2198 $processed_date = 'Recently'; // Default
2199
2200 if (!empty($created_at)) {
2201 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
2202 if ($timestamp) {
2203 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2204 }
2205 }
2206
2207 $processed_data[$post_id] = array(
2208 'db_id' => $vector_id,
2209 'processed_date' => $processed_date,
2210 'url' => $source_url,
2211 'source' => 'pinecone',
2212 'timestamp' => $timestamp ?? current_time('timestamp')
2213 );
2214 }
2215 }
2216 }
2217
2218 return $processed_data;
2219
2220 } catch (Exception $e) {
2221 //error_log('Pinecone fetch by IDs exception: ' . $e->getMessage());
2222 return array();
2223 }
2224 }
2225 public function scan_pinecone_for_processed_content($pinecone_options) {
2226 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2227 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2228
2229 if (empty($api_key) || empty($host)) {
2230 return array();
2231 }
2232
2233 try {
2234 // Use multiple random vectors to get better coverage
2235 $all_matches = array();
2236 $seen_ids = array();
2237
2238 // Try 3 different random vectors to get better coverage
2239 for ($i = 0; $i < 3; $i++) {
2240 $query_url = "https://{$host}/query";
2241
2242 // Generate a random unit vector instead of zeros
2243 $random_vector = array();
2244 for ($j = 0; $j < 1536; $j++) {
2245 $random_vector[] = (rand(-1000, 1000) / 1000.0); // Random values between -1 and 1
2246 }
2247
2248 // Normalize the vector to unit length
2249 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
2250 if ($magnitude > 0) {
2251 $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
2252 }
2253
2254 $query_data = array(
2255 'includeMetadata' => true,
2256 'includeValues' => false,
2257 'topK' => 10000, // Get many results
2258 'vector' => $random_vector
2259 );
2260
2261 $response = wp_remote_post($query_url, array(
2262 'headers' => array(
2263 'Api-Key' => $api_key,
2264 'Content-Type' => 'application/json'
2265 ),
2266 'body' => json_encode($query_data),
2267 'timeout' => 30
2268 ));
2269
2270 if (is_wp_error($response)) {
2271 continue;
2272 }
2273
2274 $response_code = wp_remote_retrieve_response_code($response);
2275 if ($response_code !== 200) {
2276 continue;
2277 }
2278
2279 $body = wp_remote_retrieve_body($response);
2280 $data = json_decode($body, true);
2281
2282 if (isset($data['matches'])) {
2283 foreach ($data['matches'] as $match) {
2284 $match_id = $match['id'] ?? '';
2285 if (!empty($match_id) && !isset($seen_ids[$match_id])) {
2286 $all_matches[] = $match;
2287 $seen_ids[$match_id] = true;
2288 }
2289 }
2290 }
2291 }
2292
2293 // Convert matches to processed data format
2294 $processed_data = array();
2295 $vector_ids_for_cache = array();
2296
2297 foreach ($all_matches as $match) {
2298 $metadata = $match['metadata'] ?? array();
2299 $source_url = $metadata['source_url'] ?? '';
2300 $match_id = $match['id'] ?? '';
2301
2302 if (!empty($source_url) && !empty($match_id)) {
2303 $post_id = url_to_postid($source_url);
2304 if ($post_id) {
2305 $created_at = $metadata['created_at'] ?? '';
2306 $processed_date = 'Recently'; // Default
2307
2308 if (!empty($created_at)) {
2309 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
2310 if ($timestamp) {
2311 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2312 }
2313 }
2314
2315 $processed_data[$post_id] = array(
2316 'db_id' => $match_id,
2317 'processed_date' => $processed_date,
2318 'url' => $source_url,
2319 'source' => 'pinecone',
2320 'timestamp' => $timestamp ?? current_time('timestamp')
2321 );
2322
2323 $vector_ids_for_cache[] = $match_id;
2324 }
2325 }
2326 }
2327
2328 // Update the vector IDs cache for future use
2329 if (!empty($vector_ids_for_cache)) {
2330 update_option('mxchat_pinecone_vector_ids_cache', $vector_ids_for_cache);
2331 }
2332
2333 return $processed_data;
2334
2335 } catch (Exception $e) {
2336 //error_log('Pinecone scan exception: ' . $e->getMessage());
2337 return array();
2338 }
2339 }
2340
2341 /**
2342 * Generates embeddings from input text for MXChat
2343 */
2344 private function mxchat_generate_embedding($text) {
2345 // Enable detailed logging for debugging
2346 //error_log('[MXCHAT-EMBED] Starting embedding generation. Text length: ' . strlen($text) . ' bytes');
2347 //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
2348
2349 $options = get_option('mxchat_options');
2350 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
2351 //error_log('[MXCHAT-EMBED] Selected embedding model: ' . $selected_model);
2352
2353 // Determine provider and endpoint
2354 if (strpos($selected_model, 'voyage') === 0) {
2355 $api_key = $options['voyage_api_key'] ?? '';
2356 $endpoint = 'https://api.voyageai.com/v1/embeddings';
2357 $provider_name = 'Voyage AI';
2358 //error_log('[MXCHAT-EMBED] Using Voyage AI API');
2359 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2360 $api_key = $options['gemini_api_key'] ?? '';
2361 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
2362 $provider_name = 'Google Gemini';
2363 //error_log('[MXCHAT-EMBED] Using Google Gemini API');
2364 } else {
2365 $api_key = $options['api_key'] ?? '';
2366 $endpoint = 'https://api.openai.com/v1/embeddings';
2367 $provider_name = 'OpenAI';
2368 //error_log('[MXCHAT-EMBED] Using OpenAI API');
2369 }
2370
2371 //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
2372
2373 if (empty($api_key)) {
2374 $error_message = sprintf('Missing %s API key. Please configure your API key in the MxChat settings.', $provider_name);
2375 //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
2376 return $error_message;
2377 }
2378
2379 // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
2380 $estimated_tokens = ceil(str_word_count($text) / 0.75);
2381 //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
2382
2383 if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
2384 //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
2385 // Consider truncating text here
2386 }
2387
2388 // Prepare request body based on provider
2389 if (strpos($selected_model, 'gemini-embedding') === 0) {
2390 // Gemini API format
2391 $request_body = array(
2392 'model' => 'models/' . $selected_model,
2393 'content' => array(
2394 'parts' => array(
2395 array('text' => $text)
2396 )
2397 )
2398 );
2399
2400 // Set output dimensionality to 1536 for consistency with other models
2401 $request_body['outputDimensionality'] = 1536;
2402 } else {
2403 // OpenAI/Voyage API format
2404 $request_body = array(
2405 'model' => $selected_model,
2406 'input' => $text
2407 );
2408
2409 // Add output_dimension for voyage-3-large model
2410 if ($selected_model === 'voyage-3-large') {
2411 $request_body['output_dimension'] = 2048;
2412 }
2413 }
2414
2415 //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
2416
2417 // Prepare headers based on provider
2418 if (strpos($selected_model, 'gemini-embedding') === 0) {
2419 // Gemini uses API key as query parameter
2420 $endpoint .= '?key=' . $api_key;
2421 $headers = array(
2422 'Content-Type' => 'application/json'
2423 );
2424 } else {
2425 // OpenAI/Voyage use Bearer token
2426 $headers = array(
2427 'Authorization' => 'Bearer ' . $api_key,
2428 'Content-Type' => 'application/json'
2429 );
2430 }
2431
2432 // Make API request
2433 //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
2434 $response = wp_remote_post($endpoint, array(
2435 'body' => wp_json_encode($request_body),
2436 'headers' => $headers,
2437 'timeout' => 60 // Increased timeout for large inputs
2438 ));
2439
2440 // Handle wp_remote_post errors
2441 if (is_wp_error($response)) {
2442 $error_message = $response->get_error_message();
2443 //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
2444 return 'Connection error: ' . $error_message;
2445 }
2446
2447 // Get and check HTTP response code
2448 $http_code = wp_remote_retrieve_response_code($response);
2449 //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
2450
2451 if ($http_code !== 200) {
2452 $error_body = wp_remote_retrieve_body($response);
2453 //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
2454
2455 // Try to parse error for more details
2456 $error_json = json_decode($error_body, true);
2457 if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
2458 $error_type = $error_json['error']['type'] ?? 'unknown';
2459 $error_message = $error_json['error']['message'] ?? 'No message';
2460 //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
2461 //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
2462
2463 // Customize error message for common API errors
2464 if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
2465 $error_message = sprintf('Invalid %s API key. Please check your API key in the MxChat settings.', $provider_name);
2466 } elseif ($error_type === 'authentication_error') {
2467 $error_message = sprintf('%s authentication failed. Please verify your API key in the MxChat settings.', $provider_name);
2468 }
2469
2470 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
2471 return $error_message;
2472 }
2473
2474 $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding", $http_code);
2475 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
2476 return $error_message;
2477 }
2478
2479 // Parse response body
2480 $response_body = wp_remote_retrieve_body($response);
2481 //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
2482
2483 $response_data = json_decode($response_body, true);
2484
2485 if (json_last_error() !== JSON_ERROR_NONE) {
2486 $error = json_last_error_msg();
2487 //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
2488 //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
2489 return "Failed to parse API response: $error";
2490 }
2491
2492 // Handle different response formats based on provider
2493 if (strpos($selected_model, 'gemini-embedding') === 0) {
2494 // Gemini API response format
2495 if (isset($response_data['embedding']['values'])) {
2496 $embedding_dimensions = count($response_data['embedding']['values']);
2497 //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
2498
2499 // Check if embedding dimensions are as expected (should be 1536)
2500 if ($embedding_dimensions !== 1536) {
2501 //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
2502 }
2503
2504 return $response_data['embedding']['values'];
2505 } else {
2506 //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
2507 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
2508
2509 if (isset($response_data['error'])) {
2510 $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
2511 //error_log('[MXCHAT-EMBED] ' . $error_message);
2512 return $error_message;
2513 }
2514
2515 $error_message = "Invalid Gemini API response format: No embedding found";
2516 //error_log('[MXCHAT-EMBED] ' . $error_message);
2517 return $error_message;
2518 }
2519 } else {
2520 // OpenAI/Voyage API response format
2521 if (isset($response_data['data'][0]['embedding'])) {
2522 $embedding_dimensions = count($response_data['data'][0]['embedding']);
2523 //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
2524
2525 // Check if embedding dimensions are as expected
2526 if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
2527 ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
2528 //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
2529 }
2530
2531 return $response_data['data'][0]['embedding'];
2532 } else {
2533 //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
2534 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
2535
2536 if (isset($response_data['error'])) {
2537 $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
2538 //error_log('[MXCHAT-EMBED] ' . $error_message);
2539 return $error_message;
2540 }
2541
2542 $error_message = "Invalid API response format: No embedding found";
2543 //error_log('[MXCHAT-EMBED] ' . $error_message);
2544 return $error_message;
2545 }
2546 }
2547 }
2548 public function ajax_dismiss_completed_status() {
2549 try {
2550 // Verify the request
2551 check_ajax_referer('mxchat_status_nonce', 'nonce');
2552
2553 if (!current_user_can('manage_options')) {
2554 wp_send_json_error('Unauthorized access');
2555 exit;
2556 }
2557
2558 $card_type = isset($_POST['card_type']) ? sanitize_text_field($_POST['card_type']) : '';
2559
2560 if ($card_type === 'pdf') {
2561 // Clear PDF status
2562 $pdf_url = get_transient('mxchat_last_pdf_url');
2563 if ($pdf_url) {
2564 delete_transient('mxchat_pdf_status_' . md5($pdf_url));
2565 delete_transient('mxchat_last_pdf_url');
2566 }
2567 } elseif ($card_type === 'sitemap') {
2568 // Clear sitemap status
2569 $sitemap_url = get_transient('mxchat_last_sitemap_url');
2570 if ($sitemap_url) {
2571 delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
2572 delete_transient('mxchat_last_sitemap_url');
2573 }
2574 }
2575
2576 wp_send_json_success(array('message' => 'Status dismissed successfully'));
2577
2578 } catch (Exception $e) {
2579 wp_send_json_error(array('message' => 'Error dismissing status: ' . $e->getMessage()));
2580 }
2581 }
2582
2583 /**
2584 * Render completed status cards on page load
2585 * This ensures completed processing status persists through page refreshes
2586 */
2587 public function render_completed_status_cards() {
2588 $output = '';
2589
2590 // Check for completed PDF status
2591 $pdf_url = get_transient('mxchat_last_pdf_url');
2592 if ($pdf_url) {
2593 $pdf_status = $this->get_pdf_processing_status($pdf_url);
2594 if ($pdf_status && ($pdf_status['status'] === 'complete' || $pdf_status['status'] === 'error')) {
2595 $output .= $this->render_pdf_status_card($pdf_status, $pdf_url);
2596 }
2597 }
2598
2599 // Check for completed sitemap status
2600 $sitemap_url = get_transient('mxchat_last_sitemap_url');
2601 if ($sitemap_url) {
2602 $sitemap_status = $this->get_sitemap_processing_status($sitemap_url);
2603 if ($sitemap_status && ($sitemap_status['status'] === 'complete' || $sitemap_status['status'] === 'error')) {
2604 $output .= $this->render_sitemap_status_card($sitemap_status, $sitemap_url);
2605 }
2606 }
2607
2608 return $output;
2609 }
2610
2611 /**
2612 * Render PDF status card HTML
2613 */
2614 private function render_pdf_status_card($status, $pdf_url) {
2615 $html = '<div class="mxchat-status-card" data-card-type="pdf">';
2616 $html .= '<div class="mxchat-status-header">';
2617 $html .= '<h4>' . esc_html__('PDF Processing Status', 'mxchat') . '</h4>';
2618
2619 // Add dismiss button for completed status
2620 if ($status['status'] === 'complete' || $status['status'] === 'error') {
2621 $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
2622 }
2623
2624 // Process Batch button for processing status
2625 if ($status['status'] === 'processing') {
2626 $html .= '<button type="button" class="mxchat-manual-batch-btn"
2627 data-process-type="pdf"
2628 data-url="' . esc_attr($pdf_url) . '">
2629 ' . esc_html__('Process Batch', 'mxchat') . '</button>';
2630 }
2631
2632 // Add status badges
2633 if ($status['status'] === 'error') {
2634 $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
2635 } elseif ($status['status'] === 'complete') {
2636 if ($status['failed_pages'] > 0) {
2637 $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
2638 sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_pages']) . '</span>';
2639 } else {
2640 $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
2641 }
2642 }
2643
2644 $html .= '</div>'; // End header
2645
2646 // Progress bar
2647 $html .= '<div class="mxchat-progress-bar">';
2648 $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
2649 $html .= '</div>';
2650
2651 // Status details
2652 $html .= '<div class="mxchat-status-details">';
2653 $html .= '<p>' . sprintf(
2654 esc_html__('Progress: %d of %d pages (%d%%)', 'mxchat'),
2655 $status['processed_pages'],
2656 $status['total_pages'],
2657 $status['percentage']
2658 ) . '</p>';
2659
2660 // Show failed pages count if any
2661 if ($status['failed_pages'] > 0) {
2662 $html .= '<p><strong>' . esc_html__('Failed pages:', 'mxchat') . '</strong> ' . esc_html($status['failed_pages']) . '</p>';
2663 }
2664
2665 $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
2666 $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
2667
2668 // Add completion summary if available
2669 if (isset($status['completion_summary'])) {
2670 $summary = $status['completion_summary'];
2671 $html .= '<div class="mxchat-completion-summary">';
2672 $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
2673 $html .= '<p><strong>' . esc_html__('Total Pages:', 'mxchat') . '</strong> ' . esc_html($summary['total_pages']) . '</p>';
2674 $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_pages']) . '</p>';
2675 $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_pages']) . '</p>';
2676 $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
2677 $html .= '</div>';
2678 }
2679
2680 // Add failed pages list if any
2681 if (!empty($status['failed_pages_list'])) {
2682 $html .= $this->render_failed_pages_list($status['failed_pages_list']);
2683 }
2684
2685 // Add error message if any
2686 if (isset($status['error']) && !empty($status['error'])) {
2687 $html .= '<div class="mxchat-error-notice">';
2688 $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
2689 $html .= '</div>';
2690 }
2691
2692 $html .= '</div>'; // End details
2693 $html .= '</div>'; // End card
2694
2695 return $html;
2696 }
2697
2698 /**
2699 * Render sitemap status card HTML
2700 */
2701 private function render_sitemap_status_card($status, $sitemap_url) {
2702 $html = '<div class="mxchat-status-card" data-card-type="sitemap">';
2703 $html .= '<div class="mxchat-status-header">';
2704 $html .= '<h4>' . esc_html__('Sitemap Processing Status', 'mxchat') . '</h4>';
2705
2706 // Add dismiss button for completed status
2707 if ($status['status'] === 'complete' || $status['status'] === 'error') {
2708 $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
2709 }
2710
2711 // Process Batch button for processing status
2712 if ($status['status'] === 'processing') {
2713 $html .= '<button type="button" class="mxchat-manual-batch-btn"
2714 data-process-type="sitemap"
2715 data-url="' . esc_attr($sitemap_url) . '">
2716 ' . esc_html__('Process Batch', 'mxchat') . '</button>';
2717 }
2718
2719 // Add status badges
2720 if ($status['status'] === 'error') {
2721 $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
2722 } elseif ($status['status'] === 'complete') {
2723 if ($status['failed_urls'] > 0) {
2724 $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
2725 sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_urls']) . '</span>';
2726 } else {
2727 $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
2728 }
2729 }
2730
2731 $html .= '</div>'; // End header
2732
2733 // Progress bar
2734 $html .= '<div class="mxchat-progress-bar">';
2735 $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
2736 $html .= '</div>';
2737
2738 // Status details
2739 $html .= '<div class="mxchat-status-details">';
2740 $html .= '<p>' . sprintf(
2741 esc_html__('Progress: %d of %d URLs (%d%%)', 'mxchat'),
2742 $status['processed_urls'],
2743 $status['total_urls'],
2744 $status['percentage']
2745 ) . '</p>';
2746
2747 // Show failed URLs count if any
2748 if ($status['failed_urls'] > 0) {
2749 $html .= '<p><strong>' . esc_html__('Failed URLs:', 'mxchat') . '</strong> ' . esc_html($status['failed_urls']) . '</p>';
2750 }
2751
2752 $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
2753 $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
2754
2755 // Add completion summary if available
2756 if (isset($status['completion_summary'])) {
2757 $summary = $status['completion_summary'];
2758 $html .= '<div class="mxchat-completion-summary">';
2759 $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
2760 $html .= '<p><strong>' . esc_html__('Total URLs:', 'mxchat') . '</strong> ' . esc_html($summary['total_urls']) . '</p>';
2761 $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_urls']) . '</p>';
2762 $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_urls']) . '</p>';
2763 $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
2764 $html .= '</div>';
2765 }
2766
2767 // Add error messages if any (but not the failed URLs list)
2768 if (!empty($status['error']) || !empty($status['last_error'])) {
2769 $html .= '<div class="mxchat-error-notice">';
2770
2771 if (!empty($status['error'])) {
2772 $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
2773 }
2774
2775 if (!empty($status['last_error'])) {
2776 $html .= '<p class="last-error">' . esc_html__('Last error:', 'mxchat') . ' ' . esc_html($status['last_error']) . '</p>';
2777 }
2778
2779 $html .= '</div>';
2780 }
2781
2782 $html .= '</div>'; // End details
2783 $html .= '</div>'; // End card
2784
2785 return $html;
2786 }
2787 /**
2788 * Render failed pages list
2789 */
2790 private function render_failed_pages_list($failed_pages_list) {
2791 if (empty($failed_pages_list)) {
2792 return '';
2793 }
2794
2795 $html = '<div class="mxchat-error-notice">';
2796 $html .= '<div class="mxchat-failed-pages-container">';
2797 $html .= '<h5>' . sprintf(esc_html__('Failed Pages (%d)', 'mxchat'), count($failed_pages_list)) . '</h5>';
2798 $html .= '<details>';
2799 $html .= '<summary>' . esc_html__('Show Failed Pages', 'mxchat') . '</summary>';
2800 $html .= '<div class="mxchat-failed-pages-list">';
2801
2802 // Create table for failed pages
2803 $html .= '<table class="widefat striped">';
2804 $html .= '<thead><tr>';
2805 $html .= '<th>' . esc_html__('Page', 'mxchat') . '</th>';
2806 $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
2807 $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
2808 $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
2809 $html .= '</tr></thead><tbody>';
2810
2811 // Sort failed pages by most recent
2812 $sorted_failed_pages = $failed_pages_list;
2813 usort($sorted_failed_pages, function($a, $b) {
2814 return ($b['time'] ?? 0) - ($a['time'] ?? 0);
2815 });
2816
2817 foreach ($sorted_failed_pages as $item) {
2818 $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
2819 $html .= '<tr>';
2820 $html .= '<td>' . esc_html__('Page', 'mxchat') . ' ' . esc_html($item['page'] ?? 'Unknown') . '</td>';
2821 $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
2822 $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
2823 $html .= '<td>' . esc_html($time_ago) . '</td>';
2824 $html .= '</tr>';
2825 }
2826
2827 $html .= '</tbody></table>';
2828 $html .= '</div></details></div></div>';
2829
2830 return $html;
2831 }
2832
2833 /**
2834 * Render failed URLs list
2835 */
2836 private function render_failed_urls_list($failed_urls_list) {
2837 if (empty($failed_urls_list)) {
2838 return '';
2839 }
2840
2841 $html = '<div class="mxchat-failed-urls-container">';
2842 $html .= '<h5>' . sprintf(esc_html__('Failed URLs (%d)', 'mxchat'), count($failed_urls_list)) . '</h5>';
2843 $html .= '<details>';
2844 $html .= '<summary>' . esc_html__('Show Failed URLs', 'mxchat') . '</summary>';
2845 $html .= '<div class="mxchat-failed-urls-list">';
2846
2847 // Create table for failed URLs
2848 $html .= '<table class="widefat striped">';
2849 $html .= '<thead><tr>';
2850 $html .= '<th>' . esc_html__('URL', 'mxchat') . '</th>';
2851 $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
2852 $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
2853 $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
2854 $html .= '</tr></thead><tbody>';
2855
2856 // Sort failed URLs by most recent
2857 $sorted_failed_urls = $failed_urls_list;
2858 usort($sorted_failed_urls, function($a, $b) {
2859 return ($b['time'] ?? 0) - ($a['time'] ?? 0);
2860 });
2861
2862 // Show up to 50 failed URLs
2863 $display_urls = array_slice($sorted_failed_urls, 0, 50);
2864
2865 foreach ($display_urls as $item) {
2866 $url = $item['url'] ?? '';
2867 $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
2868
2869 // Truncate URL for display
2870 $display_url = strlen($url) > 50 ? substr($url, 0, 47) . '...' : $url;
2871
2872 $html .= '<tr>';
2873 $html .= '<td style="word-break: break-all;">';
2874 if (!empty($url)) {
2875 $html .= '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($display_url) . '</a>';
2876 } else {
2877 $html .= esc_html__('Unknown URL', 'mxchat');
2878 }
2879 $html .= '</td>';
2880 $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
2881 $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
2882 $html .= '<td>' . esc_html($time_ago) . '</td>';
2883 $html .= '</tr>';
2884 }
2885
2886 $html .= '</tbody></table>';
2887
2888 if (count($failed_urls_list) > 50) {
2889 $html .= '<div class="mxchat-failed-urls-more">+ ' .
2890 (count($failed_urls_list) - 50) .
2891 ' ' . esc_html__('more failed URLs not shown', 'mxchat') . '</div>';
2892 }
2893
2894 $html .= '</div></details></div>';
2895
2896 return $html;
2897 }
2898
2899
2900 // ========================================
2901 // HELPER METHODS
2902 // ========================================
2903
2904 /**
2905 * Check if user has required permissions for content processing
2906 */
2907 private function check_user_permissions() {
2908 if (!current_user_can('manage_options')) {
2909 wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
2910 }
2911 }
2912
2913 /**
2914 * Validate nonce for security
2915 */
2916 private function validate_nonce($nonce_name, $nonce_action) {
2917 if (!isset($_POST[$nonce_name]) || !wp_verify_nonce($_POST[$nonce_name], $nonce_action)) {
2918 wp_die(esc_html__('Security check failed.', 'mxchat'));
2919 }
2920 }
2921
2922 /**
2923 * Get embedding API credentials
2924 */
2925 private function get_embedding_credentials() {
2926 $embedding_model = $this->options['embedding_model'] ?? 'text-embedding-ada-002';
2927
2928 if (strpos($embedding_model, 'text-embedding-') !== false) {
2929 return array(
2930 'type' => 'openai',
2931 'api_key' => $this->options['api_key'] ?? ''
2932 );
2933 } elseif (strpos($embedding_model, 'voyage-') !== false) {
2934 return array(
2935 'type' => 'voyage',
2936 'api_key' => $this->options['voyage_api_key'] ?? ''
2937 );
2938 } elseif (strpos($embedding_model, 'gemini-embedding-') !== false) {
2939 return array(
2940 'type' => 'gemini',
2941 'api_key' => $this->options['gemini_api_key'] ?? ''
2942 );
2943 }
2944
2945 return array('type' => 'unknown', 'api_key' => '');
2946 }
2947
2948 /**
2949 * Log processing errors
2950 */
2951 private function log_processing_error($operation, $error_message) {
2952 //error_log("MxChat Knowledge Processing {$operation} Error: " . $error_message);
2953 }
2954
2955 /**
2956 * Set admin notice transient
2957 */
2958 private function set_admin_notice($type, $message) {
2959 set_transient("mxchat_admin_notice_{$type}", $message, 30);
2960 }
2961
2962 /**
2963 * Get Pinecone manager instance for vector operations
2964 */
2965 private function get_pinecone_manager() {
2966 return MxChat_Pinecone_Manager::get_instance();
2967 }
2968
2969 // ========================================
2970 // STATIC ACCESS METHODS
2971 // ========================================
2972
2973 /**
2974 * Get singleton instance
2975 */
2976 public static function get_instance() {
2977 static $instance = null;
2978 if ($instance === null) {
2979 $instance = new self();
2980 }
2981 return $instance;
2982 }
2983 }
2984
2985 // Initialize the Knowledge manager
2986 $mxchat_knowledge_manager = MxChat_Knowledge_Manager::get_instance();