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

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