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

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

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