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

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