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

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

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