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

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

3,571 lines 137.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * File: admin/class-knowledge-manager.php
4 *
5 * Handles all knowledge base content processing for MxChat
6 * Including PDF, sitemap, content processing, and WordPress post management
7 */
8 if (!defined('ABSPATH')) {
9 exit; // Exit if accessed directly
10 }
11
12 class MxChat_Knowledge_Manager {
13
14 private $options;
15
16 /**
17 * Constructor - Register hooks for content processing
18 */
19 public function __construct() {
20 $this->options = get_option('mxchat_options', array());
21 $this->mxchat_init_hooks();
22 }
23
24 /**
25 * Initialize WordPress hooks for content processing
26 */
27 private function mxchat_init_hooks() {
28 // Admin post handlers for form submissions
29 add_action('admin_post_mxchat_submit_content', array($this, 'mxchat_handle_content_submission'));
30 add_action('admin_post_mxchat_submit_sitemap', array($this, 'mxchat_handle_sitemap_submission'));
31 add_action('admin_post_mxchat_stop_processing', array($this, 'mxchat_stop_processing'));
32
33 // AJAX handlers for real-time processing and status updates
34 add_action('wp_ajax_mxchat_get_status_updates', array($this, 'mxchat_ajax_get_status_updates'));
35 add_action('wp_ajax_mxchat_dismiss_completed_status', array($this, 'mxchat_ajax_dismiss_completed_status')); // 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 = 5;
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
1256 $batch_stats = [
1257 'processed' => 0,
1258 'failed' => 0,
1259 'last_error' => '',
1260 'embedding_errors' => 0
1261 ];
1262
1263 // Check embedding configuration with first URL (only on first batch)
1264 if ($start_url === 0) {
1265 $test_url = esc_url_raw($urls[0]);
1266 $test_response = wp_remote_get($test_url);
1267
1268 if (!is_wp_error($test_response) && wp_remote_retrieve_response_code($test_response) === 200) {
1269 $test_html = wp_remote_retrieve_body($test_response);
1270 $test_content = $this->mxchat_extract_main_content($test_html);
1271 $test_sanitized = $this->mxchat_sanitize_content_for_api($test_content);
1272
1273 if (!empty($test_sanitized)) {
1274 $test_embedding = $this->mxchat_generate_embedding($test_sanitized);
1275
1276 if (is_string($test_embedding)) {
1277 throw new Exception('Embedding generation failed: ' . $test_embedding);
1278 }
1279
1280 if (!is_array($test_embedding)) {
1281 throw new Exception('Embedding generation returned unexpected result type: ' . gettype($test_embedding));
1282 }
1283 }
1284 }
1285 }
1286
1287 for ($i = $start_url; $i < $end_url; $i++) {
1288 $page_url = esc_url_raw($urls[$i]);
1289 $max_retries = 3;
1290 $retry_count = 0;
1291 $url_processed = false;
1292 $last_error = '';
1293
1294 while (!$url_processed && $retry_count < $max_retries) {
1295 try {
1296 // Attempt to fetch the URL
1297 $page_response = wp_remote_get($page_url, array('timeout' => 30));
1298
1299 if (is_wp_error($page_response)) {
1300 throw new Exception('HTTP request failed: ' . $page_response->get_error_message());
1301 }
1302
1303 $response_code = wp_remote_retrieve_response_code($page_response);
1304 if ($response_code !== 200) {
1305 throw new Exception('HTTP Status: ' . $response_code);
1306 }
1307
1308 $page_html = wp_remote_retrieve_body($page_response);
1309
1310 if (empty($page_html)) {
1311 throw new Exception('Empty response body');
1312 }
1313
1314 $page_content = $this->mxchat_extract_main_content($page_html);
1315 $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
1316
1317 if (empty($sanitized_content)) {
1318 throw new Exception('No valid content found after processing');
1319 }
1320
1321 $embedding_vector = $this->mxchat_generate_embedding($sanitized_content);
1322
1323 if (is_string($embedding_vector)) {
1324 throw new Exception('Embedding generation failed: ' . $embedding_vector);
1325 }
1326
1327 if (!is_array($embedding_vector)) {
1328 throw new Exception('Embedding generation returned unexpected result type: ' . gettype($embedding_vector));
1329 }
1330
1331 // Submit to database
1332 $options = get_option('mxchat_options');
1333 $submission_result = MxChat_Utils::submit_content_to_db($sanitized_content, $page_url, $options['api_key']);
1334
1335 if (is_wp_error($submission_result)) {
1336 throw new Exception('Database submission failed: ' . $submission_result->get_error_message());
1337 }
1338
1339 // Success!
1340 $url_processed = true;
1341 $batch_stats['processed']++;
1342
1343 } catch (Exception $e) {
1344 $retry_count++;
1345 $last_error = $e->getMessage();
1346
1347 //error_log("URL {$page_url} failed (attempt {$retry_count}/{$max_retries}): " . $last_error);
1348
1349 // Track embedding errors specifically
1350 if (strpos($last_error, 'Embedding') !== false) {
1351 $batch_stats['embedding_errors']++;
1352 }
1353
1354 if ($retry_count < $max_retries) {
1355 // Wait before retry (exponential backoff: 1s, 2s, 4s)
1356 sleep(pow(2, $retry_count - 1));
1357 }
1358 }
1359 }
1360
1361 // If URL still not processed after all retries, mark as failed
1362 if (!$url_processed) {
1363 $batch_stats['failed']++;
1364 $batch_stats['last_error'] = $last_error;
1365
1366 // Add to failed URLs list
1367 $status['failed_urls_list'][] = [
1368 'url' => $page_url,
1369 'error' => $last_error,
1370 'time' => time(),
1371 'retries' => $max_retries
1372 ];
1373
1374 // Limit the number of failed URLs we store to prevent transient size issues
1375 if (count($status['failed_urls_list']) > 100) {
1376 $status['failed_urls_list'] = array_slice($status['failed_urls_list'], -100);
1377 }
1378 }
1379
1380 // Update progress
1381 $status['processed_urls'] = absint($i + 1);
1382 $status['last_update'] = time();
1383 $status['failed_urls'] = absint($status['failed_urls'] ?? 0) + ($url_processed ? 0 : 1);
1384 $status['last_error'] = $batch_stats['last_error'];
1385
1386 set_transient($status_key, $status, DAY_IN_SECONDS);
1387
1388 // If we have too many consecutive embedding errors, stop processing
1389 if ($batch_stats['embedding_errors'] >= 10) {
1390 throw new Exception('Too many consecutive embedding failures detected. Please check your API configuration.');
1391 }
1392 }
1393
1394 // If all URLs in this batch failed, stop processing
1395 if ($batch_stats['processed'] === 0 && $batch_stats['failed'] > 0) {
1396 $status['status'] = 'error';
1397 $status['error'] = sprintf(
1398 'Processing stopped: %d consecutive failures in batch. Last error: %s',
1399 $batch_stats['failed'],
1400 $batch_stats['last_error']
1401 );
1402 set_transient($status_key, $status, DAY_IN_SECONDS);
1403 return;
1404 }
1405
1406 // Update final progress
1407 $status['processed_urls'] = min($end_url, $total_urls);
1408 $status['last_update'] = time();
1409 set_transient($status_key, $status, DAY_IN_SECONDS);
1410
1411 // Check if we've processed all URLs
1412 if ($end_url >= $total_urls) {
1413 // All URLs have been processed - mark as complete
1414 $status['status'] = 'complete';
1415 $status['processed_urls'] = $total_urls;
1416
1417 // Add completion summary
1418 $status['completion_summary'] = [
1419 'total_urls' => $total_urls,
1420 'successful_urls' => $total_urls - absint($status['failed_urls'] ?? 0),
1421 'failed_urls' => absint($status['failed_urls'] ?? 0),
1422 'completion_time' => current_time('mysql')
1423 ];
1424
1425 // Save the completed status (don't delete it - let user dismiss manually)
1426 set_transient($status_key, $status, DAY_IN_SECONDS);
1427
1428 // DON'T delete the status transients here - let user dismiss manually
1429 } else {
1430 // Schedule next batch
1431 wp_schedule_single_event(time() + $batch_pause, 'mxchat_process_sitemap_urls', array(
1432 'urls' => $urls,
1433 'sitemap_url' => $sitemap_url,
1434 'total_urls' => $total_urls,
1435 'batch_size' => $batch_size,
1436 'batch_pause' => $batch_pause,
1437 ));
1438 }
1439 } catch (\Exception $e) {
1440 $status['status'] = 'error';
1441 $status['error'] = $e->getMessage();
1442 set_transient($status_key, $status, DAY_IN_SECONDS);
1443 }
1444 }
1445 public function mxchat_sanitize_content_for_api($content) {
1446 //error_log('[MXCHAT-SANITIZE] Original content preview: ' . substr($content, 0, 500) . '...');
1447
1448 // Remove script, style tags, and HTML comments
1449 $content = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $content);
1450 $content = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $content);
1451 $content = preg_replace('/<!--(.|\s)*?-->/', '', $content);
1452
1453 // Remove all HTML tags and decode HTML entities
1454 $content = wp_strip_all_tags($content);
1455 $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5);
1456
1457 // Normalize whitespace but preserve paragraph breaks
1458 // First, normalize line endings to \n
1459 $content = str_replace(["\r\n", "\r"], "\n", $content);
1460 // Replace multiple spaces/tabs with single space, but preserve newlines
1461 $content = preg_replace('/[ \t]+/', ' ', $content);
1462 // Replace 3+ newlines with 2 newlines (max 2 blank lines)
1463 $content = preg_replace('/\n{3,}/', "\n\n", $content);
1464 // Trim each line
1465 $lines = explode("\n", $content);
1466 $lines = array_map('trim', $lines);
1467 $content = implode("\n", $lines);
1468 // Final trim
1469 $content = trim($content);
1470
1471 // Remove control characters (which can cause database issues) but preserve \n (0x0A) and \r (0x0D)
1472 $content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $content);
1473
1474 // Remove NULL bytes which can cause database errors
1475 $content = str_replace("\0", "", $content);
1476
1477 // Ensure valid UTF-8 encoding
1478 $content = wp_check_invalid_utf8($content);
1479
1480 // Remove any extremely long strings without spaces (often garbage)
1481 $content = preg_replace('/\S{300,}/', ' ', $content);
1482
1483 // Replace problematic characters that often cause database issues
1484 $content = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $content); // Remove emoji and other high Unicode characters
1485
1486 // Replace any remaining potentially problematic characters with spaces
1487 // BUT preserve newlines by temporarily replacing them
1488 $content = str_replace("\n", "NEWLINE_PLACEHOLDER", $content);
1489 $content = preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\p{Sm}]/u', ' ', $content);
1490 $content = str_replace("NEWLINE_PLACEHOLDER", "\n", $content);
1491
1492 // Limit to reasonable length if needed
1493 $max_length = 65000; // Just under MySQL TEXT field limit
1494 if (strlen($content) > $max_length) {
1495 $content = substr($content, 0, $max_length);
1496 }
1497
1498 //error_log('[MXCHAT-SANITIZE] Sanitized content preview: ' . substr($content, 0, 500) . '...');
1499 return $content;
1500 }
1501 public function mxchat_extract_main_content($html) {
1502 if (empty($html)) {
1503 return '';
1504 }
1505 try {
1506 $dom = new DOMDocument;
1507 libxml_use_internal_errors(true); // Suppress HTML parsing errors
1508 @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
1509 $xpath = new DOMXPath($dom);
1510
1511 // For debugging purposes
1512 $debugEnabled = false; // Set to true to enable debugging output
1513 $debug = function($message) use ($debugEnabled) {
1514 if ($debugEnabled) {
1515 //error_log('[MXCHAT-DEBUG] ' . $message);
1516 }
1517 };
1518
1519 // Direct targeting for Gerow theme posts
1520 $post_text = $xpath->query('//div[contains(@class, "post-text")]');
1521 if ($post_text && $post_text->length > 0) {
1522 $debug("Found post-text directly");
1523 $content = '';
1524 foreach ($post_text as $node) {
1525 $content .= $dom->saveHTML($node);
1526 }
1527 if (!empty($content)) {
1528 $debug("Returning post-text content");
1529 return $content;
1530 }
1531 }
1532
1533 // Try to get the blog details content which contains the post-text
1534 $blog_details = $xpath->query('//div[contains(@class, "blog-details-content")]');
1535 if ($blog_details && $blog_details->length > 0) {
1536 $debug("Found blog-details-content");
1537 $content = '';
1538 foreach ($blog_details as $node) {
1539 $content .= $dom->saveHTML($node);
1540 }
1541 if (!empty($content)) {
1542 $debug("Returning blog-details-content");
1543 return $content;
1544 }
1545 }
1546
1547 // Try to get the article which contains the blog details
1548 $article = $xpath->query('//article[contains(@class, "blog-details-wrap")]');
1549 if ($article && $article->length > 0) {
1550 $debug("Found article with blog-details-wrap");
1551 $content = '';
1552 foreach ($article as $node) {
1553 $content .= $dom->saveHTML($node);
1554 }
1555 if (!empty($content)) {
1556 $debug("Returning article content");
1557 return $content;
1558 }
1559 }
1560
1561 // Try even broader with the blog-item-wrap
1562 $blog_item = $xpath->query('//div[contains(@class, "blog-item-wrap")]');
1563 if ($blog_item && $blog_item->length > 0) {
1564 $debug("Found blog-item-wrap");
1565 $content = '';
1566 foreach ($blog_item as $node) {
1567 $content .= $dom->saveHTML($node);
1568 }
1569 if (!empty($content)) {
1570 $debug("Returning blog-item-wrap content");
1571 return $content;
1572 }
1573 }
1574
1575 // Specific Gerow theme path
1576 $gerow_path = $xpath->query('//section[contains(@class, "blog-area")]//div[contains(@class, "post-text")]');
1577 if ($gerow_path && $gerow_path->length > 0) {
1578 $debug("Found Gerow theme path to post-text");
1579 $content = '';
1580 foreach ($gerow_path as $node) {
1581 $content .= $dom->saveHTML($node);
1582 }
1583 if (!empty($content)) {
1584 $debug("Returning Gerow post-text content");
1585 return $content;
1586 }
1587 }
1588
1589 // Generic blog post selectors
1590 $selectors = [
1591 // Blog post specific selectors
1592 '//div[contains(@class, "post-text")]',
1593 '//article[contains(@class, "blog-post-item")]//div[contains(@class, "post-text")]',
1594 '//div[contains(@class, "blog-details-content")]',
1595 '//article[contains(@class, "blog-details-wrap")]',
1596 '//div[contains(@class, "entry-content")]',
1597 '//div[contains(@class, "blog-content")]',
1598 '//div[contains(@class, "blog-item-wrap")]',
1599
1600 // More general content selectors
1601 '//div[contains(@class, "page__content")]',
1602 '//div[contains(@class, "elementor-widget-container")]',
1603 '//div[contains(@class, "elementor-text-editor")]',
1604 '//div[contains(@class, "elementor-widget-text-editor")]',
1605 '//*[contains(@class, "entry-content")]',
1606 '//*[contains(@class, "post-content")]',
1607 '//*[contains(@class, "article-content")]',
1608 '//*[@id="content"]',
1609 '//*[@id="main-content"]',
1610 '//section[contains(@class, "blog-area")]',
1611 '//article',
1612 '//main',
1613 '//div[contains(@class, "content")]'
1614 ];
1615
1616 // First handle Elementor content
1617 $debug("Checking for Elementor content");
1618 $elementor_widgets = $xpath->query('//div[contains(@class, "elementor-element")]//div[contains(@class, "elementor-widget-container")]');
1619 if ($elementor_widgets && $elementor_widgets->length > 0) {
1620 $debug("Found Elementor widgets");
1621 $combined_content = '';
1622 foreach ($elementor_widgets as $widget) {
1623 $widget_content = $dom->saveHTML($widget);
1624 if (!empty($widget_content)) {
1625 $combined_content .= $widget_content;
1626 }
1627 }
1628 if (!empty($combined_content)) {
1629 $debug("Returning Elementor content");
1630 return $combined_content;
1631 }
1632 }
1633
1634 // Try standard selectors one by one
1635 foreach ($selectors as $selector) {
1636 $debug("Trying selector: " . $selector);
1637 $nodes = $xpath->query($selector);
1638 if ($nodes && $nodes->length > 0) {
1639 $debug("Found matches for selector: " . $selector);
1640 $content = '';
1641 foreach ($nodes as $node) {
1642 $content .= $dom->saveHTML($node);
1643 }
1644 if (!empty($content)) {
1645 $debug("Returning content from selector: " . $selector);
1646 return $content;
1647 }
1648 }
1649 }
1650
1651 // Manual regex fallback for post-text if DOM methods fail
1652 $debug("Trying regex fallback");
1653 if (preg_match('/<div class="post-text">(.*?)<\/div>\s*<\/div>\s*<\/div>/s', $html, $matches)) {
1654 $debug("Found post-text via regex");
1655 return '<div class="post-text">' . $matches[1] . '</div>';
1656 }
1657
1658 // Try to extract the blog section as a whole
1659 $blog_section = $xpath->query('//section[contains(@class, "blog-area")]');
1660 if ($blog_section && $blog_section->length > 0) {
1661 $debug("Found blog-area section");
1662 $content = '';
1663 foreach ($blog_section as $node) {
1664 $content .= $dom->saveHTML($node);
1665 }
1666 if (!empty($content)) {
1667 $debug("Returning blog-area section content");
1668 return $content;
1669 }
1670 }
1671
1672 // Fallback: Return the body content if no specific selector matches
1673 $debug("Using body fallback");
1674 $body = $dom->getElementsByTagName('body');
1675 if ($body->length > 0) {
1676 return $dom->saveHTML($body->item(0));
1677 }
1678
1679 // Last resort: return the original HTML
1680 $debug("Returning original HTML");
1681 return $html;
1682 } catch (Exception $e) {
1683 //error_log('[MXCHAT-ERROR] Content extraction failed: ' . $e->getMessage());
1684 return $html; // Return original HTML if parsing fails
1685 } finally {
1686 libxml_clear_errors();
1687 }
1688 }
1689 public function mxchat_get_sitemap_processing_status($sitemap_url) {
1690 $sitemap_url = esc_url_raw($sitemap_url);
1691 $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
1692 $status = get_transient($status_key);
1693
1694 if (!$status || !is_array($status)) {
1695 return false;
1696 }
1697
1698 // Auto-complete check: if all URLs are processed but status isn't complete
1699 if (isset($status['processed_urls']) && isset($status['total_urls']) &&
1700 $status['processed_urls'] >= $status['total_urls'] &&
1701 isset($status['status']) && $status['status'] !== 'complete' &&
1702 $status['status'] !== 'error') {
1703
1704 // Mark as complete
1705 $status['status'] = 'complete';
1706 $status['processed_urls'] = $status['total_urls']; // Ensure exact match
1707
1708 // Update the transient with the corrected status
1709 set_transient($status_key, $status, DAY_IN_SECONDS);
1710 }
1711
1712 return array(
1713 'total_urls' => absint($status['total_urls']),
1714 'processed_urls' => absint($status['processed_urls']),
1715 'failed_urls' => absint($status['failed_urls'] ?? 0),
1716 'percentage' => ($status['total_urls'] > 0)
1717 ? round((absint($status['processed_urls']) / absint($status['total_urls'])) * 100)
1718 : 0,
1719 'status' => sanitize_text_field($status['status']),
1720 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
1721 'error' => isset($status['error']) ? sanitize_text_field($status['error']) : '',
1722 'last_error' => isset($status['last_error']) ? sanitize_text_field($status['last_error']) : '',
1723 'failed_urls_list' => isset($status['failed_urls_list']) ? $status['failed_urls_list'] : array()
1724 );
1725 }
1726
1727 public function mxchat_ajax_get_status_updates() {
1728 try {
1729 // Verify the request
1730 check_ajax_referer('mxchat_status_nonce', 'nonce');
1731
1732 // Get the status just like in your admin page
1733 $pdf_url = get_transient('mxchat_last_pdf_url');
1734 $sitemap_url = get_transient('mxchat_last_sitemap_url');
1735
1736 $pdf_status = $pdf_url ? $this->mxchat_get_pdf_processing_status($pdf_url) : false;
1737 $sitemap_status = $sitemap_url ? $this->mxchat_get_sitemap_processing_status($sitemap_url) : false;
1738
1739 // Add the PDF URL to the status object
1740 if ($pdf_status && $pdf_url) {
1741 $pdf_status['pdf_url'] = $pdf_url;
1742 }
1743
1744 // Set the current PDF URL for the manual batch processing button
1745 $current_pdf_url = $pdf_url;
1746
1747 // Check for true processing status, not just presence of status
1748 $is_active_processing =
1749 ($sitemap_status && isset($sitemap_status['status']) && $sitemap_status['status'] === 'processing') ||
1750 ($pdf_status && isset($pdf_status['status']) && $pdf_status['status'] === 'processing');
1751
1752 // Get single URL status, but only if no processing is active
1753 $single_url_status = !$is_active_processing ? $this->mxchat_get_single_url_status() : false;
1754
1755 // REMOVED: Auto-clearing of completed status - now only done via dismiss button
1756
1757 // Return JSON response with the status data
1758 wp_send_json(array(
1759 'pdf_status' => $pdf_status,
1760 'sitemap_status' => $sitemap_status,
1761 'single_url_status' => $single_url_status,
1762 'is_processing' => $is_active_processing,
1763 'current_pdf_url' => $current_pdf_url
1764 ));
1765
1766 } catch (Exception $e) {
1767 // Log the error
1768 //error_log('MxChat Status Update Error: ' . $e->getMessage());
1769
1770 // Return a friendly error response
1771 wp_send_json_error(array(
1772 'message' => 'Error getting status updates: ' . $e->getMessage(),
1773 'status' => 'error'
1774 ));
1775 }
1776 }
1777 public function mxchat_stop_processing() {
1778 // Verify permissions
1779 if (!current_user_can('manage_options')) {
1780 wp_die(esc_html__('Unauthorized access', 'mxchat'));
1781 }
1782
1783 // Verify nonce
1784 check_admin_referer('mxchat_stop_processing_action', 'mxchat_stop_processing_nonce');
1785
1786 // Get the last sitemap URL and clear its transient
1787 $sitemap_url = get_transient('mxchat_last_sitemap_url');
1788 if ($sitemap_url) {
1789 delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
1790 delete_transient('mxchat_last_sitemap_url');
1791 }
1792
1793 // Get the last PDF URL and clear its transient
1794 $pdf_url = get_transient('mxchat_last_pdf_url');
1795 if ($pdf_url) {
1796 delete_transient('mxchat_pdf_status_' . md5($pdf_url));
1797 delete_transient('mxchat_last_pdf_url');
1798 }
1799
1800 // Unschedule any pending sitemap events
1801 $timestamp = wp_next_scheduled('mxchat_process_sitemap_urls');
1802 if ($timestamp) {
1803 wp_unschedule_event($timestamp, 'mxchat_process_sitemap_urls');
1804 }
1805
1806 // Redirect back with a success message
1807 set_transient('mxchat_admin_notice_success',
1808 esc_html__('Processing has been stopped successfully.', 'mxchat'),
1809 30
1810 );
1811 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
1812 exit;
1813 }
1814 public function ajax_mxchat_get_content_list() {
1815 // Verify the nonce
1816 check_ajax_referer('mxchat_content_selector_nonce', 'nonce');
1817
1818 if (!current_user_can('manage_options')) {
1819 wp_send_json_error(__('Unauthorized access', 'mxchat'));
1820 }
1821
1822 $page = isset($_GET['page']) ? absint($_GET['page']) : 1;
1823 $per_page = isset($_GET['per_page']) ? absint($_GET['per_page']) : 20;
1824 $search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
1825 $post_type = isset($_GET['post_type']) ? sanitize_text_field($_GET['post_type']) : 'all';
1826 $post_status = isset($_GET['post_status']) ? sanitize_text_field($_GET['post_status']) : 'publish';
1827 $processed_filter = isset($_GET['processed_filter']) ? sanitize_text_field($_GET['processed_filter']) : 'all';
1828
1829 // Build query args
1830 $args = array(
1831 'posts_per_page' => $per_page,
1832 'paged' => $page,
1833 'post_status' => $post_status !== 'all' ? $post_status : array('publish', 'draft', 'pending'),
1834 'orderby' => 'date',
1835 'order' => 'DESC',
1836 );
1837
1838 // Handle post types
1839 if ($post_type !== 'all') {
1840 $args['post_type'] = $post_type;
1841 } else {
1842 // Default to post and page if we can't get post types
1843 $args['post_type'] = array('post', 'page');
1844
1845 // Try to get public post types
1846 $public_types = $this->mxchat_get_public_post_types();
1847 if (is_array($public_types) && !empty($public_types)) {
1848 $args['post_type'] = array_keys($public_types);
1849 }
1850 }
1851
1852 if (!empty($search)) {
1853 $args['s'] = $search;
1854 }
1855
1856 // ================================
1857 // FIXED: Check only the ACTIVE storage method
1858 // ================================
1859
1860 $processed_data = array();
1861
1862 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
1863 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
1864 $pinecone_manager->mxchat_refresh_after_new_content($pinecone_options);
1865 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1866
1867 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
1868 // ONLY check Pinecone if it's enabled
1869 $processed_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
1870 } else {
1871 // ONLY check WordPress DB if Pinecone is not enabled
1872 global $wpdb;
1873 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1874 $processed_items = $wpdb->get_results("SELECT id, source_url, timestamp FROM {$table_name}");
1875
1876 if (!empty($processed_items)) {
1877 foreach ($processed_items as $item) {
1878 $post_id = url_to_postid($item->source_url);
1879 if ($post_id) {
1880 $processed_data[$post_id] = array(
1881 'db_id' => $item->id,
1882 'timestamp' => $item->timestamp,
1883 'url' => $item->source_url,
1884 'source' => 'wordpress'
1885 );
1886 }
1887 }
1888 }
1889 }
1890
1891 // ================================
1892
1893 // Get processed IDs as a simple array for in_array checks
1894 $processed_ids = array_keys($processed_data);
1895
1896 // Handle processed/unprocessed filter
1897 if ($processed_filter === 'processed' && !empty($processed_ids)) {
1898 $args['post__in'] = $processed_ids;
1899 } elseif ($processed_filter === 'unprocessed' && !empty($processed_ids)) {
1900 $args['post__not_in'] = $processed_ids;
1901 }
1902
1903 // Run the query
1904 $query = new WP_Query($args);
1905 $content_items = array();
1906
1907 if ($query->have_posts()) {
1908 while ($query->have_posts()) {
1909 $query->the_post();
1910 $id = get_the_ID();
1911 $post_date = get_the_date();
1912 $excerpt = wp_trim_words(get_the_excerpt(), 20, '...');
1913 $word_count = str_word_count(strip_tags(get_the_content()));
1914
1915 $is_processed = in_array($id, $processed_ids);
1916 $processed_date = '';
1917 $db_record_id = 0;
1918 $data_source = 'none';
1919
1920 if ($is_processed && isset($processed_data[$id])) {
1921 $item_data = $processed_data[$id];
1922 $data_source = $item_data['source'];
1923
1924 if ($data_source === 'wordpress' && isset($item_data['timestamp'])) {
1925 // WordPress DB format
1926 $timestamp = strtotime($item_data['timestamp']);
1927 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
1928 $db_record_id = $item_data['db_id'];
1929 } elseif ($data_source === 'pinecone') {
1930 // Pinecone format
1931 $processed_date = $item_data['processed_date'];
1932 $db_record_id = $item_data['db_id'];
1933 }
1934 }
1935
1936 $content_items[] = array(
1937 'id' => $id,
1938 'title' => get_the_title(),
1939 'permalink' => get_permalink(),
1940 'date' => $post_date,
1941 'type' => get_post_type(),
1942 'status' => get_post_status(),
1943 'excerpt' => $excerpt,
1944 'word_count' => $word_count,
1945 'already_processed' => $is_processed,
1946 'processed_date' => $processed_date,
1947 'db_record_id' => $db_record_id,
1948 'data_source' => $data_source
1949 );
1950 }
1951 wp_reset_postdata();
1952 }
1953
1954 $response = array(
1955 'items' => $content_items,
1956 'total' => $query->found_posts,
1957 'total_pages' => $query->max_num_pages,
1958 'current_page' => $page,
1959 'processed_count' => count($processed_ids)
1960 );
1961
1962 wp_send_json_success($response);
1963 exit;
1964 }
1965
1966 public function ajax_mxchat_process_selected_content() {
1967 // Basic request validation
1968 if (!check_ajax_referer('mxchat_content_selector_nonce', 'nonce', false)) {
1969 wp_send_json_error('Invalid nonce');
1970 exit;
1971 }
1972
1973 if (!current_user_can('manage_options')) {
1974 wp_send_json_error('Unauthorized access');
1975 exit;
1976 }
1977
1978 // Get post IDs - safely parse the array
1979 $post_ids = array();
1980 if (isset($_POST['post_ids']) && is_array($_POST['post_ids'])) {
1981 foreach ($_POST['post_ids'] as $id) {
1982 $post_ids[] = absint($id);
1983 }
1984 }
1985
1986 if (empty($post_ids)) {
1987 wp_send_json_error('No content selected');
1988 exit;
1989 }
1990
1991 // Process only ONE post at a time to avoid request size issues
1992 $post_id = reset($post_ids);
1993 $post = get_post($post_id);
1994
1995 if (!$post) {
1996 wp_send_json_error('Post not found');
1997 exit;
1998 }
1999
2000 // Get minimal content
2001 $content = $post->post_title . "\n\n" . wp_strip_all_tags($post->post_content);
2002 $content = substr($content, 0, 10000); // Limit content size
2003
2004 // Get API key with proper model detection
2005 $options = get_option('mxchat_options');
2006 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
2007
2008 if (strpos($selected_model, 'voyage') === 0) {
2009 $api_key = $options['voyage_api_key'] ?? '';
2010 $provider_name = 'Voyage AI';
2011 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2012 $api_key = $options['gemini_api_key'] ?? '';
2013 $provider_name = 'Google Gemini';
2014 } else {
2015 $api_key = $options['api_key'] ?? '';
2016 $provider_name = 'OpenAI';
2017 }
2018
2019 if (empty($api_key)) {
2020 wp_send_json_error($provider_name . ' API key not configured');
2021 exit;
2022 }
2023
2024 $source_url = get_permalink($post_id);
2025 $vector_id = md5($source_url); // Vector ID for Pinecone
2026
2027 // ================================
2028 // FIXED: Check for existing content in ONLY the active storage method
2029 // ================================
2030
2031 $is_update = false;
2032
2033 // Check if Pinecone is enabled
2034 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
2035 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2036
2037 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
2038 // ONLY check Pinecone if it's enabled
2039 $pinecone_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
2040 if (isset($pinecone_data[$post_id])) {
2041 $is_update = true;
2042 }
2043 } else {
2044 // ONLY check WordPress DB if Pinecone is not enabled
2045 global $wpdb;
2046 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2047 $existing_record = $wpdb->get_row($wpdb->prepare(
2048 "SELECT id FROM $table_name WHERE source_url = %s",
2049 $source_url
2050 ));
2051
2052 if ($existing_record) {
2053 $is_update = true;
2054 }
2055 }
2056
2057 // Use the centralized utility function for storage
2058 $result = MxChat_Utils::submit_content_to_db(
2059 $content,
2060 $source_url,
2061 $api_key,
2062 $vector_id
2063 );
2064
2065 if (is_wp_error($result)) {
2066 wp_send_json_error('Storage failed: ' . $result->get_error_message());
2067 exit;
2068 }
2069
2070 // ================================
2071 // UPDATE: Only update caches if Pinecone is enabled
2072 // ================================
2073
2074 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
2075 // Update vector ID cache for improved fetching
2076 $this->mxchat_update_pinecone_vector_cache($vector_id);
2077
2078 // Update local processed content cache for immediate UI feedback
2079 $pinecone_cache = get_option('mxchat_pinecone_processed_cache', array());
2080 $pinecone_cache[$post_id] = array(
2081 'db_id' => $vector_id,
2082 'processed_date' => 'Just now',
2083 'url' => $source_url,
2084 'source' => 'pinecone',
2085 'timestamp' => current_time('timestamp')
2086 );
2087 update_option('mxchat_pinecone_processed_cache', $pinecone_cache);
2088
2089 // Also update the general processed content cache
2090 $processed_cache = get_option('mxchat_processed_content_cache', array());
2091 $processed_cache[$post_id] = array(
2092 'db_id' => $vector_id,
2093 'timestamp' => current_time('timestamp'),
2094 'url' => $source_url,
2095 'source' => 'pinecone'
2096 );
2097 update_option('mxchat_processed_content_cache', $processed_cache);
2098 }
2099
2100 $operation_type = $is_update ? 'update' : 'new';
2101
2102 // Success response with minimal data
2103 wp_send_json_success(array(
2104 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully',
2105 'post_id' => $post_id,
2106 'title' => $post->post_title,
2107 'operation_type' => $operation_type,
2108 'vector_id' => $vector_id, // Include vector ID for debugging
2109 'cache_updated' => $use_pinecone // Indicate if cache was updated
2110 ));
2111 exit;
2112 }
2113
2114
2115
2116 /**
2117 * Updates cache with new vector ID if absent
2118 */
2119 public function mxchat_update_pinecone_vector_cache($vector_id) {
2120 $cached_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
2121 if (!in_array($vector_id, $cached_ids)) {
2122 $cached_ids[] = $vector_id;
2123 update_option('mxchat_pinecone_vector_ids_cache', $cached_ids);
2124 }
2125 }
2126 public function mxchat_get_public_post_types() {
2127 $post_types = get_post_types(array('public' => true), 'objects');
2128 $post_type_options = array();
2129
2130 foreach ($post_types as $post_type) {
2131 $post_type_options[$post_type->name] = $post_type->label;
2132 }
2133
2134 return $post_type_options;
2135 }
2136 public function mxchat_get_pinecone_processed_content($pinecone_options) {
2137 //error_log('=== DEBUG: Starting mxchat_get_pinecone_processed_content ===');
2138
2139 // First check local cache for immediate updates
2140 $cached_data = get_option('mxchat_pinecone_processed_cache', array());
2141 //error_log('DEBUG: Found ' . count($cached_data) . ' items in local cache');
2142
2143 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2144 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2145
2146 //error_log('DEBUG: API key present: ' . (!empty($api_key) ? 'YES' : 'NO'));
2147 //error_log('DEBUG: Host: ' . $host);
2148
2149 if (empty($api_key) || empty($host)) {
2150 //error_log('DEBUG: Missing API credentials, returning cached data only');
2151 return $cached_data;
2152 }
2153
2154 $pinecone_data = array();
2155
2156 try {
2157 // Method 1: Try to get vectors using cached vector IDs first
2158 $cached_vector_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
2159 //error_log('DEBUG: Found ' . count($cached_vector_ids) . ' cached vector IDs');
2160
2161 if (!empty($cached_vector_ids)) {
2162 //error_log('DEBUG: Trying to fetch by cached vector IDs...');
2163 $pinecone_data = $this->mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $cached_vector_ids);
2164 //error_log('DEBUG: Fetch by IDs returned ' . count($pinecone_data) . ' items');
2165 }
2166
2167 // Method 2: If no cached IDs or fetch failed, use scanning approach
2168 if (empty($pinecone_data)) {
2169 //error_log('DEBUG: Trying scanning approach...');
2170 $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
2171 //error_log('DEBUG: Scanning returned ' . count($pinecone_data) . ' items');
2172 }
2173
2174 // Method 3: Final fallback - try stats endpoint
2175 if (empty($pinecone_data)) {
2176 //error_log('DEBUG: Trying stats endpoint...');
2177 $stats_url = "https://{$host}/describe_index_stats";
2178
2179 $response = wp_remote_post($stats_url, array(
2180 'headers' => array(
2181 'Api-Key' => $api_key,
2182 'Content-Type' => 'application/json'
2183 ),
2184 'body' => json_encode(array()),
2185 'timeout' => 30
2186 ));
2187
2188 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2189 $body = wp_remote_retrieve_body($response);
2190 $stats_data = json_decode($body, true);
2191 //error_log('DEBUG: Pinecone stats: ' . print_r($stats_data, true));
2192 } else {
2193 if (is_wp_error($response)) {
2194 //error_log('DEBUG: Stats endpoint error: ' . $response->get_error_message());
2195 } else {
2196 //error_log('DEBUG: Stats endpoint failed with code: ' . wp_remote_retrieve_response_code($response));
2197 }
2198 }
2199 }
2200
2201 } catch (Exception $e) {
2202 //error_log('DEBUG: Exception in get_pinecone_processed_content: ' . $e->getMessage());
2203 }
2204
2205 // Merge cached data with Pinecone data
2206 $merged_data = $pinecone_data;
2207
2208 foreach ($cached_data as $post_id => $cache_item) {
2209 $cache_timestamp = $cache_item['timestamp'] ?? 0;
2210 $time_diff = current_time('timestamp') - $cache_timestamp;
2211
2212 if ($time_diff < 300) { // 5 minutes = 300 seconds
2213 $merged_data[$post_id] = $cache_item;
2214 } else {
2215 if (!isset($merged_data[$post_id])) {
2216 $merged_data[$post_id] = $cache_item;
2217 }
2218 }
2219 }
2220
2221 //error_log('DEBUG: Final merged data count: ' . count($merged_data));
2222 //error_log('=== DEBUG: End mxchat_get_pinecone_processed_content ===');
2223
2224 return $merged_data;
2225 }
2226
2227 public function mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
2228 //error_log('=== DEBUG: Starting mxchat_fetch_pinecone_vectors_by_ids ===');
2229
2230 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2231 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2232
2233 if (empty($api_key) || empty($host) || empty($vector_ids)) {
2234 //error_log('DEBUG: Missing parameters for fetch by IDs');
2235 return array();
2236 }
2237
2238 try {
2239 $fetch_url = "https://{$host}/vectors/fetch";
2240 //error_log('DEBUG: Fetch URL: ' . $fetch_url);
2241 //error_log('DEBUG: Fetching ' . count($vector_ids) . ' vector IDs');
2242
2243 // Pinecone fetch API allows fetching specific vectors by ID
2244 $fetch_data = array(
2245 'ids' => array_values($vector_ids)
2246 );
2247
2248 $response = wp_remote_post($fetch_url, array(
2249 'headers' => array(
2250 'Api-Key' => $api_key,
2251 'Content-Type' => 'application/json'
2252 ),
2253 'body' => json_encode($fetch_data),
2254 'timeout' => 30
2255 ));
2256
2257 if (is_wp_error($response)) {
2258 //error_log('DEBUG: Fetch by IDs WP error: ' . $response->get_error_message());
2259 return array();
2260 }
2261
2262 $response_code = wp_remote_retrieve_response_code($response);
2263 //error_log('DEBUG: Fetch response code: ' . $response_code);
2264
2265 if ($response_code !== 200) {
2266 $error_body = wp_remote_retrieve_body($response);
2267 //error_log('DEBUG: Fetch failed with body: ' . $error_body);
2268 return array();
2269 }
2270
2271 $body = wp_remote_retrieve_body($response);
2272 $data = json_decode($body, true);
2273
2274 //error_log('DEBUG: Fetch response structure: ' . print_r(array_keys($data), true));
2275
2276 if (!isset($data['vectors'])) {
2277 //error_log('DEBUG: No vectors key in response');
2278 return array();
2279 }
2280
2281 $processed_data = array();
2282
2283 foreach ($data['vectors'] as $vector_id => $vector_data) {
2284 $metadata = $vector_data['metadata'] ?? array();
2285 $source_url = $metadata['source_url'] ?? '';
2286
2287 if (!empty($source_url)) {
2288 $post_id = url_to_postid($source_url);
2289 if ($post_id) {
2290 $created_at = $metadata['created_at'] ?? '';
2291 $processed_date = 'Recently';
2292
2293 if (!empty($created_at)) {
2294 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
2295 if ($timestamp) {
2296 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2297 }
2298 }
2299
2300 $processed_data[$post_id] = array(
2301 'db_id' => $vector_id,
2302 'processed_date' => $processed_date,
2303 'url' => $source_url,
2304 'source' => 'pinecone',
2305 'timestamp' => $timestamp ?? current_time('timestamp')
2306 );
2307 }
2308 }
2309 }
2310
2311 //error_log('DEBUG: Processed ' . count($processed_data) . ' vectors from fetch');
2312 return $processed_data;
2313
2314 } catch (Exception $e) {
2315 //error_log('DEBUG: Exception in fetch_pinecone_vectors_by_ids: ' . $e->getMessage());
2316 return array();
2317 }
2318 }
2319
2320 public function mxchat_scan_pinecone_for_processed_content($pinecone_options) {
2321 //error_log('=== DEBUG: Starting mxchat_scan_pinecone_for_processed_content ===');
2322
2323 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2324 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2325
2326 if (empty($api_key) || empty($host)) {
2327 //error_log('DEBUG: Missing API credentials for scanning');
2328 return array();
2329 }
2330
2331 try {
2332 // Use multiple random vectors to get better coverage
2333 $all_matches = array();
2334 $seen_ids = array();
2335
2336 // Try 3 different random vectors to get better coverage
2337 for ($i = 0; $i < 3; $i++) {
2338 //error_log('DEBUG: Scanning attempt ' . ($i + 1) . '/3');
2339
2340 $query_url = "https://{$host}/query";
2341
2342 // Generate a random unit vector instead of zeros
2343 $random_vector = array();
2344 for ($j = 0; $j < 1536; $j++) {
2345 $random_vector[] = (rand(-1000, 1000) / 1000.0);
2346 }
2347
2348 // Normalize the vector to unit length
2349 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
2350 if ($magnitude > 0) {
2351 $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
2352 }
2353
2354 $query_data = array(
2355 'includeMetadata' => true,
2356 'includeValues' => false,
2357 'topK' => 10000,
2358 'vector' => $random_vector
2359 );
2360
2361 $response = wp_remote_post($query_url, array(
2362 'headers' => array(
2363 'Api-Key' => $api_key,
2364 'Content-Type' => 'application/json'
2365 ),
2366 'body' => json_encode($query_data),
2367 'timeout' => 30
2368 ));
2369
2370 if (is_wp_error($response)) {
2371 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' WP error: ' . $response->get_error_message());
2372 continue;
2373 }
2374
2375 $response_code = wp_remote_retrieve_response_code($response);
2376 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' response code: ' . $response_code);
2377
2378 if ($response_code !== 200) {
2379 $error_body = wp_remote_retrieve_body($response);
2380 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' failed with body: ' . substr($error_body, 0, 500));
2381 continue;
2382 }
2383
2384 $body = wp_remote_retrieve_body($response);
2385 $data = json_decode($body, true);
2386
2387 if (isset($data['matches'])) {
2388 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' returned ' . count($data['matches']) . ' matches');
2389 foreach ($data['matches'] as $match) {
2390 $match_id = $match['id'] ?? '';
2391 if (!empty($match_id) && !isset($seen_ids[$match_id])) {
2392 $all_matches[] = $match;
2393 $seen_ids[$match_id] = true;
2394 }
2395 }
2396 } else {
2397 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' - no matches key in response');
2398 }
2399 }
2400
2401 //error_log('DEBUG: Total unique matches found: ' . count($all_matches));
2402
2403 // Convert matches to processed data format
2404 $processed_data = array();
2405 $vector_ids_for_cache = array();
2406
2407 foreach ($all_matches as $match) {
2408 $metadata = $match['metadata'] ?? array();
2409 $source_url = $metadata['source_url'] ?? '';
2410 $match_id = $match['id'] ?? '';
2411
2412 if (!empty($source_url) && !empty($match_id)) {
2413 $post_id = url_to_postid($source_url);
2414 if ($post_id) {
2415 $created_at = $metadata['created_at'] ?? '';
2416 $processed_date = 'Recently';
2417
2418 if (!empty($created_at)) {
2419 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
2420 if ($timestamp) {
2421 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2422 }
2423 }
2424
2425 $processed_data[$post_id] = array(
2426 'db_id' => $match_id,
2427 'processed_date' => $processed_date,
2428 'url' => $source_url,
2429 'source' => 'pinecone',
2430 'timestamp' => $timestamp ?? current_time('timestamp')
2431 );
2432
2433 $vector_ids_for_cache[] = $match_id;
2434 }
2435 }
2436 }
2437
2438 // Update the vector IDs cache for future use
2439 if (!empty($vector_ids_for_cache)) {
2440 update_option('mxchat_pinecone_vector_ids_cache', $vector_ids_for_cache);
2441 //error_log('DEBUG: Updated vector IDs cache with ' . count($vector_ids_for_cache) . ' IDs');
2442 }
2443
2444 //error_log('DEBUG: Returning ' . count($processed_data) . ' processed items from scanning');
2445 return $processed_data;
2446
2447 } catch (Exception $e) {
2448 //error_log('DEBUG: Exception in scan_pinecone_for_processed_content: ' . $e->getMessage());
2449 return array();
2450 }
2451 }
2452
2453 /**
2454 * Generates embeddings from input text for MXChat
2455 */
2456 private function mxchat_generate_embedding($text) {
2457 // Enable detailed logging for debugging
2458 //error_log('[MXCHAT-EMBED] Starting embedding generation. Text length: ' . strlen($text) . ' bytes');
2459 //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
2460
2461 $options = get_option('mxchat_options');
2462 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
2463 //error_log('[MXCHAT-EMBED] Selected embedding model: ' . $selected_model);
2464
2465 // Determine provider and endpoint
2466 if (strpos($selected_model, 'voyage') === 0) {
2467 $api_key = $options['voyage_api_key'] ?? '';
2468 $endpoint = 'https://api.voyageai.com/v1/embeddings';
2469 $provider_name = 'Voyage AI';
2470 //error_log('[MXCHAT-EMBED] Using Voyage AI API');
2471 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2472 $api_key = $options['gemini_api_key'] ?? '';
2473 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
2474 $provider_name = 'Google Gemini';
2475 //error_log('[MXCHAT-EMBED] Using Google Gemini API');
2476 } else {
2477 $api_key = $options['api_key'] ?? '';
2478 $endpoint = 'https://api.openai.com/v1/embeddings';
2479 $provider_name = 'OpenAI';
2480 //error_log('[MXCHAT-EMBED] Using OpenAI API');
2481 }
2482
2483 //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
2484
2485 if (empty($api_key)) {
2486 $error_message = sprintf('Missing %s API key. Please configure your API key in the MxChat settings.', $provider_name);
2487 //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
2488 return $error_message;
2489 }
2490
2491 // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
2492 $estimated_tokens = ceil(str_word_count($text) / 0.75);
2493 //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
2494
2495 if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
2496 //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
2497 // Consider truncating text here
2498 }
2499
2500 // Prepare request body based on provider
2501 if (strpos($selected_model, 'gemini-embedding') === 0) {
2502 // Gemini API format
2503 $request_body = array(
2504 'model' => 'models/' . $selected_model,
2505 'content' => array(
2506 'parts' => array(
2507 array('text' => $text)
2508 )
2509 )
2510 );
2511
2512 // Set output dimensionality to 1536 for consistency with other models
2513 $request_body['outputDimensionality'] = 1536;
2514 } else {
2515 // OpenAI/Voyage API format
2516 $request_body = array(
2517 'model' => $selected_model,
2518 'input' => $text
2519 );
2520
2521 // Add output_dimension for voyage-3-large model
2522 if ($selected_model === 'voyage-3-large') {
2523 $request_body['output_dimension'] = 2048;
2524 }
2525 }
2526
2527 //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
2528
2529 // Prepare headers based on provider
2530 if (strpos($selected_model, 'gemini-embedding') === 0) {
2531 // Gemini uses API key as query parameter
2532 $endpoint .= '?key=' . $api_key;
2533 $headers = array(
2534 'Content-Type' => 'application/json'
2535 );
2536 } else {
2537 // OpenAI/Voyage use Bearer token
2538 $headers = array(
2539 'Authorization' => 'Bearer ' . $api_key,
2540 'Content-Type' => 'application/json'
2541 );
2542 }
2543
2544 // Make API request
2545 //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
2546 $response = wp_remote_post($endpoint, array(
2547 'body' => wp_json_encode($request_body),
2548 'headers' => $headers,
2549 'timeout' => 60 // Increased timeout for large inputs
2550 ));
2551
2552 // Handle wp_remote_post errors
2553 if (is_wp_error($response)) {
2554 $error_message = $response->get_error_message();
2555 //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
2556 return 'Connection error: ' . $error_message;
2557 }
2558
2559 // Get and check HTTP response code
2560 $http_code = wp_remote_retrieve_response_code($response);
2561 //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
2562
2563 if ($http_code !== 200) {
2564 $error_body = wp_remote_retrieve_body($response);
2565 //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
2566
2567 // Try to parse error for more details
2568 $error_json = json_decode($error_body, true);
2569 if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
2570 $error_type = $error_json['error']['type'] ?? 'unknown';
2571 $error_message = $error_json['error']['message'] ?? 'No message';
2572 //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
2573 //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
2574
2575 // Customize error message for common API errors
2576 if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
2577 $error_message = sprintf('Invalid %s API key. Please check your API key in the MxChat settings.', $provider_name);
2578 } elseif ($error_type === 'authentication_error') {
2579 $error_message = sprintf('%s authentication failed. Please verify your API key in the MxChat settings.', $provider_name);
2580 }
2581
2582 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
2583 return $error_message;
2584 }
2585
2586 $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding", $http_code);
2587 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
2588 return $error_message;
2589 }
2590
2591 // Parse response body
2592 $response_body = wp_remote_retrieve_body($response);
2593 //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
2594
2595 $response_data = json_decode($response_body, true);
2596
2597 if (json_last_error() !== JSON_ERROR_NONE) {
2598 $error = json_last_error_msg();
2599 //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
2600 //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
2601 return "Failed to parse API response: $error";
2602 }
2603
2604 // Handle different response formats based on provider
2605 if (strpos($selected_model, 'gemini-embedding') === 0) {
2606 // Gemini API response format
2607 if (isset($response_data['embedding']['values'])) {
2608 $embedding_dimensions = count($response_data['embedding']['values']);
2609 //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
2610
2611 // Check if embedding dimensions are as expected (should be 1536)
2612 if ($embedding_dimensions !== 1536) {
2613 //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
2614 }
2615
2616 return $response_data['embedding']['values'];
2617 } else {
2618 //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
2619 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
2620
2621 if (isset($response_data['error'])) {
2622 $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
2623 //error_log('[MXCHAT-EMBED] ' . $error_message);
2624 return $error_message;
2625 }
2626
2627 $error_message = "Invalid Gemini API response format: No embedding found";
2628 //error_log('[MXCHAT-EMBED] ' . $error_message);
2629 return $error_message;
2630 }
2631 } else {
2632 // OpenAI/Voyage API response format
2633 if (isset($response_data['data'][0]['embedding'])) {
2634 $embedding_dimensions = count($response_data['data'][0]['embedding']);
2635 //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
2636
2637 // Check if embedding dimensions are as expected
2638 if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
2639 ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
2640 //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
2641 }
2642
2643 return $response_data['data'][0]['embedding'];
2644 } else {
2645 //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
2646 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
2647
2648 if (isset($response_data['error'])) {
2649 $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
2650 //error_log('[MXCHAT-EMBED] ' . $error_message);
2651 return $error_message;
2652 }
2653
2654 $error_message = "Invalid API response format: No embedding found";
2655 //error_log('[MXCHAT-EMBED] ' . $error_message);
2656 return $error_message;
2657 }
2658 }
2659 }
2660 public function mxchat_ajax_dismiss_completed_status() {
2661 try {
2662 // Verify the request
2663 check_ajax_referer('mxchat_status_nonce', 'nonce');
2664
2665 if (!current_user_can('manage_options')) {
2666 wp_send_json_error('Unauthorized access');
2667 exit;
2668 }
2669
2670 $card_type = isset($_POST['card_type']) ? sanitize_text_field($_POST['card_type']) : '';
2671
2672 if ($card_type === 'pdf') {
2673 // Clear PDF status
2674 $pdf_url = get_transient('mxchat_last_pdf_url');
2675 if ($pdf_url) {
2676 delete_transient('mxchat_pdf_status_' . md5($pdf_url));
2677 delete_transient('mxchat_last_pdf_url');
2678 }
2679 } elseif ($card_type === 'sitemap') {
2680 // Clear sitemap status
2681 $sitemap_url = get_transient('mxchat_last_sitemap_url');
2682 if ($sitemap_url) {
2683 delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
2684 delete_transient('mxchat_last_sitemap_url');
2685 }
2686 }
2687
2688 wp_send_json_success(array('message' => 'Status dismissed successfully'));
2689
2690 } catch (Exception $e) {
2691 wp_send_json_error(array('message' => 'Error dismissing status: ' . $e->getMessage()));
2692 }
2693 }
2694
2695 /**
2696 * Render completed status cards on page load
2697 * This ensures completed processing status persists through page refreshes
2698 */
2699 public function mxchat_render_completed_status_cards() {
2700 $output = '';
2701
2702 // Check for completed PDF status
2703 $pdf_url = get_transient('mxchat_last_pdf_url');
2704 if ($pdf_url) {
2705 $pdf_status = $this->mxchat_get_pdf_processing_status($pdf_url);
2706 if ($pdf_status && ($pdf_status['status'] === 'complete' || $pdf_status['status'] === 'error')) {
2707 $output .= $this->mxchat_render_pdf_status_card($pdf_status, $pdf_url);
2708 }
2709 }
2710
2711 // Check for completed sitemap status
2712 $sitemap_url = get_transient('mxchat_last_sitemap_url');
2713 if ($sitemap_url) {
2714 $sitemap_status = $this->mxchat_get_sitemap_processing_status($sitemap_url);
2715 if ($sitemap_status && ($sitemap_status['status'] === 'complete' || $sitemap_status['status'] === 'error')) {
2716 $output .= $this->mxchat_render_sitemap_status_card($sitemap_status, $sitemap_url);
2717 }
2718 }
2719
2720 return $output;
2721 }
2722
2723 /**
2724 * Render PDF status card HTML
2725 */
2726 private function mxchat_render_pdf_status_card($status, $pdf_url) {
2727 $html = '<div class="mxchat-status-card" data-card-type="pdf">';
2728 $html .= '<div class="mxchat-status-header">';
2729 $html .= '<h4>' . esc_html__('PDF Processing Status', 'mxchat') . '</h4>';
2730
2731 // Add dismiss button for completed status
2732 if ($status['status'] === 'complete' || $status['status'] === 'error') {
2733 $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
2734 }
2735
2736 // Process Batch button for processing status
2737 if ($status['status'] === 'processing') {
2738 $html .= '<button type="button" class="mxchat-manual-batch-btn"
2739 data-process-type="pdf"
2740 data-url="' . esc_attr($pdf_url) . '">
2741 ' . esc_html__('Process Batch', 'mxchat') . '</button>';
2742 }
2743
2744 // Add status badges
2745 if ($status['status'] === 'error') {
2746 $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
2747 } elseif ($status['status'] === 'complete') {
2748 if ($status['failed_pages'] > 0) {
2749 $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
2750 sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_pages']) . '</span>';
2751 } else {
2752 $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
2753 }
2754 }
2755
2756 $html .= '</div>'; // End header
2757
2758 // Progress bar
2759 $html .= '<div class="mxchat-progress-bar">';
2760 $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
2761 $html .= '</div>';
2762
2763 // Status details
2764 $html .= '<div class="mxchat-status-details">';
2765 $html .= '<p>' . sprintf(
2766 esc_html__('Progress: %d of %d pages (%d%%)', 'mxchat'),
2767 $status['processed_pages'],
2768 $status['total_pages'],
2769 $status['percentage']
2770 ) . '</p>';
2771
2772 // Show failed pages count if any
2773 if ($status['failed_pages'] > 0) {
2774 $html .= '<p><strong>' . esc_html__('Failed pages:', 'mxchat') . '</strong> ' . esc_html($status['failed_pages']) . '</p>';
2775 }
2776
2777 $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
2778 $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
2779
2780 // Add completion summary if available AND it's an array
2781 if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
2782 $summary = $status['completion_summary'];
2783 $html .= '<div class="mxchat-completion-summary">';
2784 $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
2785 $html .= '<p><strong>' . esc_html__('Total Pages:', 'mxchat') . '</strong> ' . esc_html($summary['total_pages']) . '</p>';
2786 $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_pages']) . '</p>';
2787 $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_pages']) . '</p>';
2788 $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
2789 $html .= '</div>';
2790 }
2791
2792 // Add failed pages list if any AND it's an array
2793 if (isset($status['failed_pages_list']) && is_array($status['failed_pages_list']) && !empty($status['failed_pages_list'])) {
2794 $html .= $this->mxchat_render_failed_pages_list($status['failed_pages_list']);
2795 }
2796
2797 // Add error message if any
2798 if (isset($status['error']) && !empty($status['error'])) {
2799 $html .= '<div class="mxchat-error-notice">';
2800 $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
2801 $html .= '</div>';
2802 }
2803
2804 $html .= '</div>'; // End details
2805 $html .= '</div>'; // End card
2806
2807 return $html;
2808 }
2809 /**
2810 * Render sitemap status card HTML
2811 */
2812 private function mxchat_render_sitemap_status_card($status, $sitemap_url) {
2813 $html = '<div class="mxchat-status-card" data-card-type="sitemap">';
2814 $html .= '<div class="mxchat-status-header">';
2815 $html .= '<h4>' . esc_html__('Sitemap Processing Status', 'mxchat') . '</h4>';
2816
2817 // Add dismiss button for completed status
2818 if ($status['status'] === 'complete' || $status['status'] === 'error') {
2819 $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
2820 }
2821
2822 // Process Batch button for processing status
2823 if ($status['status'] === 'processing') {
2824 $html .= '<button type="button" class="mxchat-manual-batch-btn"
2825 data-process-type="sitemap"
2826 data-url="' . esc_attr($sitemap_url) . '">
2827 ' . esc_html__('Process Batch', 'mxchat') . '</button>';
2828 }
2829
2830 // Add status badges
2831 if ($status['status'] === 'error') {
2832 $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
2833 } elseif ($status['status'] === 'complete') {
2834 if ($status['failed_urls'] > 0) {
2835 $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
2836 sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_urls']) . '</span>';
2837 } else {
2838 $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
2839 }
2840 }
2841
2842 $html .= '</div>'; // End header
2843
2844 // Progress bar
2845 $html .= '<div class="mxchat-progress-bar">';
2846 $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
2847 $html .= '</div>';
2848
2849 // Status details
2850 $html .= '<div class="mxchat-status-details">';
2851 $html .= '<p>' . sprintf(
2852 esc_html__('Progress: %d of %d URLs (%d%%)', 'mxchat'),
2853 $status['processed_urls'],
2854 $status['total_urls'],
2855 $status['percentage']
2856 ) . '</p>';
2857
2858 // Show failed URLs count if any
2859 if ($status['failed_urls'] > 0) {
2860 $html .= '<p><strong>' . esc_html__('Failed URLs:', 'mxchat') . '</strong> ' . esc_html($status['failed_urls']) . '</p>';
2861 }
2862
2863 $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
2864 $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
2865
2866 // Add completion summary if available AND it's an array
2867 if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
2868 $summary = $status['completion_summary'];
2869 $html .= '<div class="mxchat-completion-summary">';
2870 $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
2871 $html .= '<p><strong>' . esc_html__('Total URLs:', 'mxchat') . '</strong> ' . esc_html($summary['total_urls']) . '</p>';
2872 $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_urls']) . '</p>';
2873 $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_urls']) . '</p>';
2874 $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
2875 $html .= '</div>';
2876 }
2877
2878 // Add error messages if any (but not the failed URLs list)
2879 if (!empty($status['error']) || !empty($status['last_error'])) {
2880 $html .= '<div class="mxchat-error-notice">';
2881
2882 if (!empty($status['error'])) {
2883 $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
2884 }
2885
2886 if (!empty($status['last_error'])) {
2887 $html .= '<p class="last-error">' . esc_html__('Last error:', 'mxchat') . ' ' . esc_html($status['last_error']) . '</p>';
2888 }
2889
2890 $html .= '</div>';
2891 }
2892
2893 $html .= '</div>'; // End details
2894 $html .= '</div>'; // End card
2895
2896 return $html;
2897 }
2898
2899
2900 /**
2901 * Render failed pages list
2902 */
2903 private function mxchat_render_failed_pages_list($failed_pages_list) {
2904 // Validate that $failed_pages_list is an array and not empty
2905 if (!is_array($failed_pages_list) || empty($failed_pages_list)) {
2906 return '';
2907 }
2908
2909 $html = '<div class="mxchat-error-notice">';
2910 $html .= '<div class="mxchat-failed-pages-container">';
2911 $html .= '<h5>' . sprintf(esc_html__('Failed Pages (%d)', 'mxchat'), count($failed_pages_list)) . '</h5>';
2912 $html .= '<details>';
2913 $html .= '<summary>' . esc_html__('Show Failed Pages', 'mxchat') . '</summary>';
2914 $html .= '<div class="mxchat-failed-pages-list">';
2915
2916 // Create table for failed pages
2917 $html .= '<table class="widefat striped">';
2918 $html .= '<thead><tr>';
2919 $html .= '<th>' . esc_html__('Page', 'mxchat') . '</th>';
2920 $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
2921 $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
2922 $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
2923 $html .= '</tr></thead><tbody>';
2924
2925 // Sort failed pages by most recent
2926 $sorted_failed_pages = $failed_pages_list;
2927 usort($sorted_failed_pages, function($a, $b) {
2928 return ($b['time'] ?? 0) - ($a['time'] ?? 0);
2929 });
2930
2931 foreach ($sorted_failed_pages as $item) {
2932 // Ensure $item is an array before accessing its elements
2933 if (!is_array($item)) {
2934 continue;
2935 }
2936
2937 $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
2938 $html .= '<tr>';
2939 $html .= '<td>' . esc_html__('Page', 'mxchat') . ' ' . esc_html($item['page'] ?? 'Unknown') . '</td>';
2940 $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
2941 $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
2942 $html .= '<td>' . esc_html($time_ago) . '</td>';
2943 $html .= '</tr>';
2944 }
2945
2946 $html .= '</tbody></table>';
2947 $html .= '</div></details></div></div>';
2948
2949 return $html;
2950 }
2951
2952 /**
2953 * Render failed URLs list
2954 */
2955 private function mxchat_render_failed_urls_list($failed_urls_list) {
2956 // Validate that $failed_urls_list is an array and not empty
2957 if (!is_array($failed_urls_list) || empty($failed_urls_list)) {
2958 return '';
2959 }
2960
2961 $html = '<div class="mxchat-failed-urls-container">';
2962 $html .= '<h5>' . sprintf(esc_html__('Failed URLs (%d)', 'mxchat'), count($failed_urls_list)) . '</h5>';
2963 $html .= '<details>';
2964 $html .= '<summary>' . esc_html__('Show Failed URLs', 'mxchat') . '</summary>';
2965 $html .= '<div class="mxchat-failed-urls-list">';
2966
2967 // Create table for failed URLs
2968 $html .= '<table class="widefat striped">';
2969 $html .= '<thead><tr>';
2970 $html .= '<th>' . esc_html__('URL', 'mxchat') . '</th>';
2971 $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
2972 $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
2973 $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
2974 $html .= '</tr></thead><tbody>';
2975
2976 // Sort failed URLs by most recent
2977 $sorted_failed_urls = $failed_urls_list;
2978 usort($sorted_failed_urls, function($a, $b) {
2979 return ($b['time'] ?? 0) - ($a['time'] ?? 0);
2980 });
2981
2982 // Show up to 50 failed URLs
2983 $display_urls = array_slice($sorted_failed_urls, 0, 50);
2984
2985 foreach ($display_urls as $item) {
2986 // Ensure $item is an array before accessing its elements
2987 if (!is_array($item)) {
2988 continue;
2989 }
2990
2991 $url = $item['url'] ?? '';
2992 $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
2993
2994 // Truncate URL for display
2995 $display_url = strlen($url) > 50 ? substr($url, 0, 47) . '...' : $url;
2996
2997 $html .= '<tr>';
2998 $html .= '<td style="word-break: break-all;">';
2999 if (!empty($url)) {
3000 $html .= '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($display_url) . '</a>';
3001 } else {
3002 $html .= esc_html__('Unknown URL', 'mxchat');
3003 }
3004 $html .= '</td>';
3005 $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
3006 $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
3007 $html .= '<td>' . esc_html($time_ago) . '</td>';
3008 $html .= '</tr>';
3009 }
3010
3011 $html .= '</tbody></table>';
3012
3013 if (count($failed_urls_list) > 50) {
3014 $html .= '<div class="mxchat-failed-urls-more">+ ' .
3015 (count($failed_urls_list) - 50) .
3016 ' ' . esc_html__('more failed URLs not shown', 'mxchat') . '</div>';
3017 }
3018
3019 $html .= '</div></details></div>';
3020
3021 return $html;
3022 }
3023
3024 public function mxchat_handle_post_update($post_id, $post, $update) {
3025 // Basic validation checks
3026 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
3027 return;
3028 }
3029
3030 // Only process published content
3031 if ($post->post_status !== 'publish') {
3032 return;
3033 }
3034
3035 $post_type = $post->post_type;
3036
3037 // Check if sync is enabled for this post type
3038 $should_sync = false;
3039
3040 // Check built-in post types first
3041 if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
3042 $should_sync = true;
3043 } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
3044 $should_sync = true;
3045 } else {
3046 // Check custom post types
3047 $option_name = 'mxchat_auto_sync_' . $post_type;
3048 if (get_option($option_name) === '1') {
3049 $should_sync = true;
3050 }
3051 }
3052
3053 if (!$should_sync) {
3054 return;
3055 }
3056
3057 // Get content with proper formatting (matching ajax_mxchat_process_selected_content)
3058 $title = get_the_title($post_id);
3059 $content = get_post_field('post_content', $post_id);
3060
3061 // Apply WordPress content filters to get properly formatted content
3062 $content = apply_filters('the_content', $content);
3063
3064 // Strip tags but preserve structure
3065 $content = wp_strip_all_tags($content);
3066
3067 // Combine title and content
3068 $final_content = $title . "\n\n" . $content;
3069
3070 // For custom post types like job_listing, include additional fields
3071 if ($post_type === 'job_listing') {
3072 // Add job-specific meta if available
3073 $job_location = get_post_meta($post_id, '_job_location', true);
3074 if (!empty($job_location)) {
3075 $final_content .= "\n\nLocation: " . $job_location;
3076 }
3077
3078 // Get job type terms
3079 $job_types = get_the_terms($post_id, 'job_listing_type');
3080 if (!empty($job_types) && !is_wp_error($job_types)) {
3081 $types = array();
3082 foreach ($job_types as $type) {
3083 $types[] = $type->name;
3084 }
3085 $final_content .= "\n\nJob Type: " . implode(', ', $types);
3086 }
3087
3088 // Get company name if available
3089 $company_name = get_post_meta($post_id, '_company_name', true);
3090 if (!empty($company_name)) {
3091 $final_content .= "\n\nCompany: " . $company_name;
3092 }
3093 }
3094
3095 // Get the source URL
3096 $source_url = get_permalink($post_id);
3097
3098 // Get API key with proper model detection
3099 $options = get_option('mxchat_options');
3100 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3101
3102 if (strpos($selected_model, 'voyage') === 0) {
3103 $api_key = $options['voyage_api_key'] ?? '';
3104 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3105 $api_key = $options['gemini_api_key'] ?? '';
3106 } else {
3107 $api_key = $options['api_key'] ?? '';
3108 }
3109
3110 if (empty($api_key)) {
3111 //error_log('MxChat Auto-sync: No API key configured for embedding model');
3112 return;
3113 }
3114
3115 // Use the centralized utility function for storage
3116 $result = MxChat_Utils::submit_content_to_db(
3117 $final_content,
3118 $source_url,
3119 $api_key,
3120 md5($source_url) // Vector ID for Pinecone
3121 );
3122
3123 if (is_wp_error($result)) {
3124 //error_log('MxChat Auto-sync failed for post ' . $post_id . ': ' . $result->get_error_message());
3125 }
3126 }
3127
3128
3129 public function mxchat_handle_post_delete($post_id) {
3130 // Get post data before it's deleted
3131 $post = get_post($post_id);
3132
3133 // Basic validation
3134 if (!$post || wp_is_post_revision($post_id)) {
3135 return;
3136 }
3137
3138 $post_type = $post->post_type;
3139
3140 // Check if sync is enabled for this post type
3141 $should_sync = false;
3142
3143 // Check built-in post types first
3144 if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
3145 $should_sync = true;
3146 } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
3147 $should_sync = true;
3148 } else {
3149 // Check custom post types
3150 $option_name = 'mxchat_auto_sync_' . $post_type;
3151 if (get_option($option_name) === '1') {
3152 $should_sync = true;
3153 }
3154 }
3155
3156 if (!$should_sync) {
3157 return;
3158 }
3159
3160 // Get the URL before post is deleted
3161 $source_url = get_permalink($post_id);
3162 if (!$source_url) {
3163 //error_log('MXChat: Failed to get permalink for post ' . $post_id);
3164 return;
3165 }
3166
3167 // Check if Pinecone is enabled
3168 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3169 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3170
3171 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3172 // Delete from Pinecone
3173 $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
3174 } else {
3175 // Delete from WordPress DB
3176 global $wpdb;
3177 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3178
3179 $result = $wpdb->delete(
3180 $table_name,
3181 array('source_url' => $source_url),
3182 array('%s')
3183 );
3184
3185 if ($result === false) {
3186 //error_log('MXChat: WordPress DB deletion failed for URL: ' . $source_url);
3187 }
3188 }
3189 }
3190
3191
3192 /**
3193 * Deletes data from Pinecone using a source URL
3194 */
3195 public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
3196 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3197 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3198
3199 if (empty($host) || empty($api_key)) {
3200 //error_log('MXChat: Pinecone deletion failed - missing configuration');
3201 return false;
3202 }
3203
3204 $api_endpoint = "https://{$host}/vectors/delete";
3205 $vector_id = md5($source_url);
3206
3207 $request_body = array(
3208 'ids' => array($vector_id)
3209 );
3210
3211 $response = wp_remote_post($api_endpoint, array(
3212 'headers' => array(
3213 'Api-Key' => $api_key,
3214 'accept' => 'application/json',
3215 'content-type' => 'application/json'
3216 ),
3217 'body' => wp_json_encode($request_body),
3218 'timeout' => 30
3219 ));
3220
3221 if (is_wp_error($response)) {
3222 //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
3223 return false;
3224 }
3225
3226 $response_code = wp_remote_retrieve_response_code($response);
3227 if ($response_code !== 200) {
3228 //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
3229 return false;
3230 }
3231
3232 return true;
3233 }
3234
3235
3236
3237 public function mxchat_handle_product_change($post_id, $post, $update) {
3238 if ($post->post_type !== 'product') {
3239 return;
3240 }
3241
3242 if ($post->post_status === 'publish') {
3243 add_action('shutdown', function() use ($post_id) {
3244 $product = wc_get_product($post_id);
3245 if ($product) {
3246 $this->mxchat_store_product_embedding($product);
3247 }
3248 });
3249 }
3250 }
3251
3252 /**
3253 * Store WooCommerce product embeddings
3254 */
3255 private function mxchat_store_product_embedding($product) {
3256 if (!isset($this->options['enable_woocommerce_integration']) ||
3257 !in_array($this->options['enable_woocommerce_integration'], ['1', 'on'])) {
3258 return;
3259 }
3260
3261 $source_url = get_permalink($product->get_id());
3262
3263 // Build product content
3264 $title = $product->get_name();
3265 $description = $product->get_description();
3266 $short_description = $product->get_short_description();
3267 $regular_price = $product->get_regular_price();
3268 $sale_price = $product->get_sale_price();
3269 $sku = $product->get_sku();
3270
3271 // Format content consistently
3272 $content = $title . "\n\n";
3273
3274 if (!empty($description)) {
3275 $content .= wp_strip_all_tags($description) . "\n\n";
3276 }
3277
3278 if (!empty($short_description)) {
3279 $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
3280 }
3281
3282 $content .= "Price: $" . $regular_price . "\n";
3283
3284 if (!empty($sale_price)) {
3285 $content .= "Sale Price: $" . $sale_price . "\n";
3286 }
3287
3288 if (!empty($sku)) {
3289 $content .= "SKU: " . $sku . "\n";
3290 }
3291
3292 // Get API key with proper model detection
3293 $options = get_option('mxchat_options');
3294 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3295
3296 if (strpos($selected_model, 'voyage') === 0) {
3297 $api_key = $options['voyage_api_key'] ?? '';
3298 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3299 $api_key = $options['gemini_api_key'] ?? '';
3300 } else {
3301 $api_key = $options['api_key'] ?? '';
3302 }
3303
3304 if (empty($api_key)) {
3305 //error_log('MxChat Auto-sync: No API key configured for embedding model');
3306 return;
3307 }
3308
3309 // Use the centralized utility function for storage
3310 $result = MxChat_Utils::submit_content_to_db(
3311 $content,
3312 $source_url,
3313 $api_key,
3314 md5($source_url) // Vector ID for Pinecone
3315 );
3316
3317 if (is_wp_error($result)) {
3318 //error_log('MxChat WooCommerce sync failed for product ' . $product->get_id() . ': ' . $result->get_error_message());
3319 }
3320 }
3321
3322 public function mxchat_handle_product_delete($post_id) {
3323 if (get_post_type($post_id) !== 'product') {
3324 return;
3325 }
3326
3327 $source_url = get_permalink($post_id);
3328
3329 // Check if Pinecone is enabled
3330 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3331 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3332
3333 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3334 // Delete from Pinecone
3335 $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
3336 } else {
3337 // Delete from WordPress DB
3338 global $wpdb;
3339 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3340
3341 $wpdb->delete(
3342 $table_name,
3343 array('source_url' => $source_url),
3344 array('%s')
3345 );
3346 }
3347 }
3348
3349 /**
3350 * Handle individual Pinecone content deletion
3351 */
3352 public function mxchat_handle_pinecone_prompt_delete() {
3353 // Check permissions
3354 if (!current_user_can('manage_options')) {
3355 wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
3356 }
3357
3358 // Verify nonce
3359 if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'mxchat_delete_pinecone_prompt_nonce')) {
3360 wp_die(esc_html__('Security check failed.', 'mxchat'));
3361 }
3362
3363 $vector_id = isset($_GET['vector_id']) ? sanitize_text_field($_GET['vector_id']) : '';
3364
3365 if (empty($vector_id)) {
3366 set_transient('mxchat_admin_notice_error',
3367 esc_html__('Invalid vector ID.', 'mxchat'),
3368 30
3369 );
3370 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
3371 exit;
3372 }
3373
3374 // Get Pinecone settings
3375 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3376 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3377
3378 if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
3379 set_transient('mxchat_admin_notice_error',
3380 esc_html__('Pinecone is not properly configured.', 'mxchat'),
3381 30
3382 );
3383 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
3384 exit;
3385 }
3386
3387 // Delete from Pinecone
3388 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
3389 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
3390 $vector_id,
3391 $pinecone_options['mxchat_pinecone_api_key'],
3392 $pinecone_options['mxchat_pinecone_host']
3393 );
3394
3395 if ($result['success']) {
3396 // Remove from ALL caches
3397 $pinecone_manager->mxchat_remove_from_pinecone_vector_cache($vector_id);
3398 $pinecone_manager->mxchat_remove_from_processed_content_caches($vector_id);
3399
3400 // CLEAR ALL RELEVANT CACHES
3401 delete_transient('mxchat_pinecone_recent_1k_cache');
3402 delete_option('mxchat_pinecone_vector_ids_cache');
3403 delete_option('mxchat_pinecone_processed_cache');
3404 delete_option('mxchat_processed_content_cache');
3405
3406 // Also force refresh for next page load
3407 $pinecone_manager->mxchat_refresh_after_new_content($pinecone_options);
3408
3409 set_transient('mxchat_admin_notice_success',
3410 esc_html__('Entry deleted successfully from Pinecone.', 'mxchat'),
3411 30
3412 );
3413 } else {
3414 set_transient('mxchat_admin_notice_error',
3415 esc_html__('Failed to delete entry: ', 'mxchat') . $result['message'],
3416 30
3417 );
3418 }
3419
3420 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
3421 exit;
3422 }
3423
3424 public function ajax_mxchat_delete_pinecone_prompt() {
3425 // Verify nonce and permissions
3426 if (!check_ajax_referer('mxchat_delete_pinecone_prompt_nonce', 'nonce', false)) {
3427 wp_send_json_error('Invalid nonce');
3428 exit;
3429 }
3430
3431 if (!current_user_can('manage_options')) {
3432 wp_send_json_error('Unauthorized access');
3433 exit;
3434 }
3435
3436 $vector_id = isset($_POST['vector_id']) ? sanitize_text_field($_POST['vector_id']) : '';
3437
3438 if (empty($vector_id)) {
3439 wp_send_json_error('Missing vector ID');
3440 exit;
3441 }
3442
3443 // Get Pinecone settings
3444 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3445 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3446
3447 if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
3448 wp_send_json_error('Pinecone is not properly configured');
3449 exit;
3450 }
3451
3452 // Delete from Pinecone
3453 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
3454 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
3455 $vector_id,
3456 $pinecone_options['mxchat_pinecone_api_key'],
3457 $pinecone_options['mxchat_pinecone_host']
3458 );
3459
3460 if ($result['success']) {
3461 // Remove from ALL caches
3462 $pinecone_manager->mxchat_remove_from_pinecone_vector_cache($vector_id);
3463 $pinecone_manager->mxchat_remove_from_processed_content_caches($vector_id);
3464
3465 // CLEAR ALL RELEVANT CACHES (ADD THESE LINES)
3466 delete_transient('mxchat_pinecone_recent_1k_cache');
3467 delete_option('mxchat_pinecone_vector_ids_cache');
3468 delete_option('mxchat_pinecone_processed_cache');
3469 delete_option('mxchat_processed_content_cache');
3470
3471 // Also force refresh for next page load
3472 $pinecone_manager->mxchat_refresh_after_new_content($pinecone_options);
3473
3474 wp_send_json_success(array(
3475 'message' => 'Entry deleted successfully from Pinecone',
3476 'vector_id' => $vector_id
3477 ));
3478 } else {
3479 wp_send_json_error('Failed to delete from Pinecone: ' . $result['message']);
3480 }
3481
3482 exit;
3483 }
3484
3485 // ========================================
3486 // HELPER METHODS
3487 // ========================================
3488
3489 /**
3490 * Check if user has required permissions for content processing
3491 */
3492 private function mxchat_check_user_permissions() {
3493 if (!current_user_can('manage_options')) {
3494 wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
3495 }
3496 }
3497
3498 /**
3499 * Validate nonce for security
3500 */
3501 private function mxchat_validate_nonce($nonce_name, $nonce_action) {
3502 if (!isset($_POST[$nonce_name]) || !wp_verify_nonce($_POST[$nonce_name], $nonce_action)) {
3503 wp_die(esc_html__('Security check failed.', 'mxchat'));
3504 }
3505 }
3506
3507 /**
3508 * Get embedding API credentials
3509 */
3510 private function mxchat_get_embedding_credentials() {
3511 $embedding_model = $this->options['embedding_model'] ?? 'text-embedding-ada-002';
3512
3513 if (strpos($embedding_model, 'text-embedding-') !== false) {
3514 return array(
3515 'type' => 'openai',
3516 'api_key' => $this->options['api_key'] ?? ''
3517 );
3518 } elseif (strpos($embedding_model, 'voyage-') !== false) {
3519 return array(
3520 'type' => 'voyage',
3521 'api_key' => $this->options['voyage_api_key'] ?? ''
3522 );
3523 } elseif (strpos($embedding_model, 'gemini-embedding-') !== false) {
3524 return array(
3525 'type' => 'gemini',
3526 'api_key' => $this->options['gemini_api_key'] ?? ''
3527 );
3528 }
3529
3530 return array('type' => 'unknown', 'api_key' => '');
3531 }
3532
3533 /**
3534 * Log processing errors
3535 */
3536 private function mxchat_log_processing_error($operation, $error_message) {
3537 //error_log("MxChat Knowledge Processing {$operation} Error: " . $error_message);
3538 }
3539
3540 /**
3541 * Set admin notice transient
3542 */
3543 private function mxchat_set_admin_notice($type, $message) {
3544 set_transient("mxchat_admin_notice_{$type}", $message, 30);
3545 }
3546
3547 /**
3548 * Get Pinecone manager instance for vector operations
3549 */
3550 private function mxchat_get_pinecone_manager() {
3551 return MxChat_Pinecone_Manager::get_instance();
3552 }
3553
3554 // ========================================
3555 // STATIC ACCESS METHODS
3556 // ========================================
3557
3558 /**
3559 * Get singleton instance
3560 */
3561 public static function get_instance() {
3562 static $instance = null;
3563 if ($instance === null) {
3564 $instance = new self();
3565 }
3566 return $instance;
3567 }
3568 }
3569
3570 // Initialize the Knowledge manager
3571 $mxchat_knowledge_manager = MxChat_Knowledge_Manager::get_instance();