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

3,424 lines 132.5 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
1856 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
1857 // ONLY check Pinecone if it's enabled
1858 $processed_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
1859 } else {
1860 // ONLY check WordPress DB if Pinecone is not enabled
1861 global $wpdb;
1862 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1863 $processed_items = $wpdb->get_results("SELECT id, source_url, timestamp FROM {$table_name}");
1864
1865 if (!empty($processed_items)) {
1866 foreach ($processed_items as $item) {
1867 $post_id = url_to_postid($item->source_url);
1868 if ($post_id) {
1869 $processed_data[$post_id] = array(
1870 'db_id' => $item->id,
1871 'timestamp' => $item->timestamp,
1872 'url' => $item->source_url,
1873 'source' => 'wordpress'
1874 );
1875 }
1876 }
1877 }
1878 }
1879
1880 // ================================
1881
1882 // Get processed IDs as a simple array for in_array checks
1883 $processed_ids = array_keys($processed_data);
1884
1885 // Handle processed/unprocessed filter
1886 if ($processed_filter === 'processed' && !empty($processed_ids)) {
1887 $args['post__in'] = $processed_ids;
1888 } elseif ($processed_filter === 'unprocessed' && !empty($processed_ids)) {
1889 $args['post__not_in'] = $processed_ids;
1890 }
1891
1892 // Run the query
1893 $query = new WP_Query($args);
1894 $content_items = array();
1895
1896 if ($query->have_posts()) {
1897 while ($query->have_posts()) {
1898 $query->the_post();
1899 $id = get_the_ID();
1900 $post_date = get_the_date();
1901 $excerpt = wp_trim_words(get_the_excerpt(), 20, '...');
1902 $word_count = str_word_count(strip_tags(get_the_content()));
1903
1904 $is_processed = in_array($id, $processed_ids);
1905 $processed_date = '';
1906 $db_record_id = 0;
1907 $data_source = 'none';
1908
1909 if ($is_processed && isset($processed_data[$id])) {
1910 $item_data = $processed_data[$id];
1911 $data_source = $item_data['source'];
1912
1913 if ($data_source === 'wordpress' && isset($item_data['timestamp'])) {
1914 // WordPress DB format
1915 $timestamp = strtotime($item_data['timestamp']);
1916 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
1917 $db_record_id = $item_data['db_id'];
1918 } elseif ($data_source === 'pinecone') {
1919 // Pinecone format
1920 $processed_date = $item_data['processed_date'];
1921 $db_record_id = $item_data['db_id'];
1922 }
1923 }
1924
1925 $content_items[] = array(
1926 'id' => $id,
1927 'title' => get_the_title(),
1928 'permalink' => get_permalink(),
1929 'date' => $post_date,
1930 'type' => get_post_type(),
1931 'status' => get_post_status(),
1932 'excerpt' => $excerpt,
1933 'word_count' => $word_count,
1934 'already_processed' => $is_processed,
1935 'processed_date' => $processed_date,
1936 'db_record_id' => $db_record_id,
1937 'data_source' => $data_source
1938 );
1939 }
1940 wp_reset_postdata();
1941 }
1942
1943 $response = array(
1944 'items' => $content_items,
1945 'total' => $query->found_posts,
1946 'total_pages' => $query->max_num_pages,
1947 'current_page' => $page,
1948 'processed_count' => count($processed_ids)
1949 );
1950
1951 wp_send_json_success($response);
1952 exit;
1953 }
1954
1955 public function ajax_mxchat_process_selected_content() {
1956 // Basic request validation
1957 if (!check_ajax_referer('mxchat_content_selector_nonce', 'nonce', false)) {
1958 wp_send_json_error('Invalid nonce');
1959 exit;
1960 }
1961
1962 if (!current_user_can('manage_options')) {
1963 wp_send_json_error('Unauthorized access');
1964 exit;
1965 }
1966
1967 // Get post IDs - safely parse the array
1968 $post_ids = array();
1969 if (isset($_POST['post_ids']) && is_array($_POST['post_ids'])) {
1970 foreach ($_POST['post_ids'] as $id) {
1971 $post_ids[] = absint($id);
1972 }
1973 }
1974
1975 if (empty($post_ids)) {
1976 wp_send_json_error('No content selected');
1977 exit;
1978 }
1979
1980 // Process only ONE post at a time to avoid request size issues
1981 $post_id = reset($post_ids);
1982 $post = get_post($post_id);
1983
1984 if (!$post) {
1985 wp_send_json_error('Post not found');
1986 exit;
1987 }
1988
1989 // Get minimal content
1990 $content = $post->post_title . "\n\n" . wp_strip_all_tags($post->post_content);
1991 $content = substr($content, 0, 10000); // Limit content size
1992
1993 // Get API key with proper model detection
1994 $options = get_option('mxchat_options');
1995 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
1996
1997 if (strpos($selected_model, 'voyage') === 0) {
1998 $api_key = $options['voyage_api_key'] ?? '';
1999 $provider_name = 'Voyage AI';
2000 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2001 $api_key = $options['gemini_api_key'] ?? '';
2002 $provider_name = 'Google Gemini';
2003 } else {
2004 $api_key = $options['api_key'] ?? '';
2005 $provider_name = 'OpenAI';
2006 }
2007
2008 if (empty($api_key)) {
2009 wp_send_json_error($provider_name . ' API key not configured');
2010 exit;
2011 }
2012
2013 $source_url = get_permalink($post_id);
2014 $vector_id = md5($source_url); // Vector ID for Pinecone
2015
2016 // ================================
2017 // FIXED: Check for existing content in ONLY the active storage method
2018 // ================================
2019
2020 $is_update = false;
2021
2022 // Check if Pinecone is enabled
2023 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
2024 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2025
2026 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
2027 // ONLY check Pinecone if it's enabled
2028 $pinecone_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
2029 if (isset($pinecone_data[$post_id])) {
2030 $is_update = true;
2031 }
2032 } else {
2033 // ONLY check WordPress DB if Pinecone is not enabled
2034 global $wpdb;
2035 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2036 $existing_record = $wpdb->get_row($wpdb->prepare(
2037 "SELECT id FROM $table_name WHERE source_url = %s",
2038 $source_url
2039 ));
2040
2041 if ($existing_record) {
2042 $is_update = true;
2043 }
2044 }
2045
2046 // Use the centralized utility function for storage
2047 $result = MxChat_Utils::submit_content_to_db(
2048 $content,
2049 $source_url,
2050 $api_key,
2051 $vector_id
2052 );
2053
2054 if (is_wp_error($result)) {
2055 wp_send_json_error('Storage failed: ' . $result->get_error_message());
2056 exit;
2057 }
2058
2059 // ================================
2060 // UPDATE: Only update caches if Pinecone is enabled
2061 // ================================
2062
2063 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
2064 // Update vector ID cache for improved fetching
2065 $this->mxchat_update_pinecone_vector_cache($vector_id);
2066
2067 // Update local processed content cache for immediate UI feedback
2068 $pinecone_cache = get_option('mxchat_pinecone_processed_cache', array());
2069 $pinecone_cache[$post_id] = array(
2070 'db_id' => $vector_id,
2071 'processed_date' => 'Just now',
2072 'url' => $source_url,
2073 'source' => 'pinecone',
2074 'timestamp' => current_time('timestamp')
2075 );
2076 update_option('mxchat_pinecone_processed_cache', $pinecone_cache);
2077
2078 // Also update the general processed content cache
2079 $processed_cache = get_option('mxchat_processed_content_cache', array());
2080 $processed_cache[$post_id] = array(
2081 'db_id' => $vector_id,
2082 'timestamp' => current_time('timestamp'),
2083 'url' => $source_url,
2084 'source' => 'pinecone'
2085 );
2086 update_option('mxchat_processed_content_cache', $processed_cache);
2087 }
2088
2089 $operation_type = $is_update ? 'update' : 'new';
2090
2091 // Success response with minimal data
2092 wp_send_json_success(array(
2093 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully',
2094 'post_id' => $post_id,
2095 'title' => $post->post_title,
2096 'operation_type' => $operation_type,
2097 'vector_id' => $vector_id, // Include vector ID for debugging
2098 'cache_updated' => $use_pinecone // Indicate if cache was updated
2099 ));
2100 exit;
2101 }
2102
2103
2104
2105 /**
2106 * Updates cache with new vector ID if absent
2107 */
2108 public function mxchat_update_pinecone_vector_cache($vector_id) {
2109 $cached_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
2110 if (!in_array($vector_id, $cached_ids)) {
2111 $cached_ids[] = $vector_id;
2112 update_option('mxchat_pinecone_vector_ids_cache', $cached_ids);
2113 }
2114 }
2115 public function mxchat_get_public_post_types() {
2116 $post_types = get_post_types(array('public' => true), 'objects');
2117 $post_type_options = array();
2118
2119 foreach ($post_types as $post_type) {
2120 $post_type_options[$post_type->name] = $post_type->label;
2121 }
2122
2123 return $post_type_options;
2124 }
2125 public function mxchat_get_pinecone_processed_content($pinecone_options) {
2126 //error_log('=== DEBUG: Starting mxchat_get_pinecone_processed_content ===');
2127
2128 // First check local cache for immediate updates
2129 $cached_data = get_option('mxchat_pinecone_processed_cache', array());
2130 //error_log('DEBUG: Found ' . count($cached_data) . ' items in local cache');
2131
2132 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2133 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2134
2135 //error_log('DEBUG: API key present: ' . (!empty($api_key) ? 'YES' : 'NO'));
2136 //error_log('DEBUG: Host: ' . $host);
2137
2138 if (empty($api_key) || empty($host)) {
2139 //error_log('DEBUG: Missing API credentials, returning cached data only');
2140 return $cached_data;
2141 }
2142
2143 $pinecone_data = array();
2144
2145 try {
2146 // Method 1: Try to get vectors using cached vector IDs first
2147 $cached_vector_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
2148 //error_log('DEBUG: Found ' . count($cached_vector_ids) . ' cached vector IDs');
2149
2150 if (!empty($cached_vector_ids)) {
2151 //error_log('DEBUG: Trying to fetch by cached vector IDs...');
2152 $pinecone_data = $this->mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $cached_vector_ids);
2153 //error_log('DEBUG: Fetch by IDs returned ' . count($pinecone_data) . ' items');
2154 }
2155
2156 // Method 2: If no cached IDs or fetch failed, use scanning approach
2157 if (empty($pinecone_data)) {
2158 //error_log('DEBUG: Trying scanning approach...');
2159 $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
2160 //error_log('DEBUG: Scanning returned ' . count($pinecone_data) . ' items');
2161 }
2162
2163 // Method 3: Final fallback - try stats endpoint
2164 if (empty($pinecone_data)) {
2165 //error_log('DEBUG: Trying stats endpoint...');
2166 $stats_url = "https://{$host}/describe_index_stats";
2167
2168 $response = wp_remote_post($stats_url, array(
2169 'headers' => array(
2170 'Api-Key' => $api_key,
2171 'Content-Type' => 'application/json'
2172 ),
2173 'body' => json_encode(array()),
2174 'timeout' => 30
2175 ));
2176
2177 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2178 $body = wp_remote_retrieve_body($response);
2179 $stats_data = json_decode($body, true);
2180 //error_log('DEBUG: Pinecone stats: ' . print_r($stats_data, true));
2181 } else {
2182 if (is_wp_error($response)) {
2183 //error_log('DEBUG: Stats endpoint error: ' . $response->get_error_message());
2184 } else {
2185 //error_log('DEBUG: Stats endpoint failed with code: ' . wp_remote_retrieve_response_code($response));
2186 }
2187 }
2188 }
2189
2190 } catch (Exception $e) {
2191 //error_log('DEBUG: Exception in get_pinecone_processed_content: ' . $e->getMessage());
2192 }
2193
2194 // Merge cached data with Pinecone data
2195 $merged_data = $pinecone_data;
2196
2197 foreach ($cached_data as $post_id => $cache_item) {
2198 $cache_timestamp = $cache_item['timestamp'] ?? 0;
2199 $time_diff = current_time('timestamp') - $cache_timestamp;
2200
2201 if ($time_diff < 300) { // 5 minutes = 300 seconds
2202 $merged_data[$post_id] = $cache_item;
2203 } else {
2204 if (!isset($merged_data[$post_id])) {
2205 $merged_data[$post_id] = $cache_item;
2206 }
2207 }
2208 }
2209
2210 //error_log('DEBUG: Final merged data count: ' . count($merged_data));
2211 //error_log('=== DEBUG: End mxchat_get_pinecone_processed_content ===');
2212
2213 return $merged_data;
2214 }
2215
2216 public function mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
2217 //error_log('=== DEBUG: Starting mxchat_fetch_pinecone_vectors_by_ids ===');
2218
2219 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2220 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2221
2222 if (empty($api_key) || empty($host) || empty($vector_ids)) {
2223 //error_log('DEBUG: Missing parameters for fetch by IDs');
2224 return array();
2225 }
2226
2227 try {
2228 $fetch_url = "https://{$host}/vectors/fetch";
2229 //error_log('DEBUG: Fetch URL: ' . $fetch_url);
2230 //error_log('DEBUG: Fetching ' . count($vector_ids) . ' vector IDs');
2231
2232 // Pinecone fetch API allows fetching specific vectors by ID
2233 $fetch_data = array(
2234 'ids' => array_values($vector_ids)
2235 );
2236
2237 $response = wp_remote_post($fetch_url, array(
2238 'headers' => array(
2239 'Api-Key' => $api_key,
2240 'Content-Type' => 'application/json'
2241 ),
2242 'body' => json_encode($fetch_data),
2243 'timeout' => 30
2244 ));
2245
2246 if (is_wp_error($response)) {
2247 //error_log('DEBUG: Fetch by IDs WP error: ' . $response->get_error_message());
2248 return array();
2249 }
2250
2251 $response_code = wp_remote_retrieve_response_code($response);
2252 //error_log('DEBUG: Fetch response code: ' . $response_code);
2253
2254 if ($response_code !== 200) {
2255 $error_body = wp_remote_retrieve_body($response);
2256 //error_log('DEBUG: Fetch failed with body: ' . $error_body);
2257 return array();
2258 }
2259
2260 $body = wp_remote_retrieve_body($response);
2261 $data = json_decode($body, true);
2262
2263 //error_log('DEBUG: Fetch response structure: ' . print_r(array_keys($data), true));
2264
2265 if (!isset($data['vectors'])) {
2266 //error_log('DEBUG: No vectors key in response');
2267 return array();
2268 }
2269
2270 $processed_data = array();
2271
2272 foreach ($data['vectors'] as $vector_id => $vector_data) {
2273 $metadata = $vector_data['metadata'] ?? array();
2274 $source_url = $metadata['source_url'] ?? '';
2275
2276 if (!empty($source_url)) {
2277 $post_id = url_to_postid($source_url);
2278 if ($post_id) {
2279 $created_at = $metadata['created_at'] ?? '';
2280 $processed_date = 'Recently';
2281
2282 if (!empty($created_at)) {
2283 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
2284 if ($timestamp) {
2285 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2286 }
2287 }
2288
2289 $processed_data[$post_id] = array(
2290 'db_id' => $vector_id,
2291 'processed_date' => $processed_date,
2292 'url' => $source_url,
2293 'source' => 'pinecone',
2294 'timestamp' => $timestamp ?? current_time('timestamp')
2295 );
2296 }
2297 }
2298 }
2299
2300 //error_log('DEBUG: Processed ' . count($processed_data) . ' vectors from fetch');
2301 return $processed_data;
2302
2303 } catch (Exception $e) {
2304 //error_log('DEBUG: Exception in fetch_pinecone_vectors_by_ids: ' . $e->getMessage());
2305 return array();
2306 }
2307 }
2308
2309 public function mxchat_scan_pinecone_for_processed_content($pinecone_options) {
2310 //error_log('=== DEBUG: Starting mxchat_scan_pinecone_for_processed_content ===');
2311
2312 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2313 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2314
2315 if (empty($api_key) || empty($host)) {
2316 //error_log('DEBUG: Missing API credentials for scanning');
2317 return array();
2318 }
2319
2320 try {
2321 // Use multiple random vectors to get better coverage
2322 $all_matches = array();
2323 $seen_ids = array();
2324
2325 // Try 3 different random vectors to get better coverage
2326 for ($i = 0; $i < 3; $i++) {
2327 //error_log('DEBUG: Scanning attempt ' . ($i + 1) . '/3');
2328
2329 $query_url = "https://{$host}/query";
2330
2331 // Generate a random unit vector instead of zeros
2332 $random_vector = array();
2333 for ($j = 0; $j < 1536; $j++) {
2334 $random_vector[] = (rand(-1000, 1000) / 1000.0);
2335 }
2336
2337 // Normalize the vector to unit length
2338 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
2339 if ($magnitude > 0) {
2340 $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
2341 }
2342
2343 $query_data = array(
2344 'includeMetadata' => true,
2345 'includeValues' => false,
2346 'topK' => 10000,
2347 'vector' => $random_vector
2348 );
2349
2350 $response = wp_remote_post($query_url, array(
2351 'headers' => array(
2352 'Api-Key' => $api_key,
2353 'Content-Type' => 'application/json'
2354 ),
2355 'body' => json_encode($query_data),
2356 'timeout' => 30
2357 ));
2358
2359 if (is_wp_error($response)) {
2360 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' WP error: ' . $response->get_error_message());
2361 continue;
2362 }
2363
2364 $response_code = wp_remote_retrieve_response_code($response);
2365 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' response code: ' . $response_code);
2366
2367 if ($response_code !== 200) {
2368 $error_body = wp_remote_retrieve_body($response);
2369 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' failed with body: ' . substr($error_body, 0, 500));
2370 continue;
2371 }
2372
2373 $body = wp_remote_retrieve_body($response);
2374 $data = json_decode($body, true);
2375
2376 if (isset($data['matches'])) {
2377 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' returned ' . count($data['matches']) . ' matches');
2378 foreach ($data['matches'] as $match) {
2379 $match_id = $match['id'] ?? '';
2380 if (!empty($match_id) && !isset($seen_ids[$match_id])) {
2381 $all_matches[] = $match;
2382 $seen_ids[$match_id] = true;
2383 }
2384 }
2385 } else {
2386 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' - no matches key in response');
2387 }
2388 }
2389
2390 //error_log('DEBUG: Total unique matches found: ' . count($all_matches));
2391
2392 // Convert matches to processed data format
2393 $processed_data = array();
2394 $vector_ids_for_cache = array();
2395
2396 foreach ($all_matches as $match) {
2397 $metadata = $match['metadata'] ?? array();
2398 $source_url = $metadata['source_url'] ?? '';
2399 $match_id = $match['id'] ?? '';
2400
2401 if (!empty($source_url) && !empty($match_id)) {
2402 $post_id = url_to_postid($source_url);
2403 if ($post_id) {
2404 $created_at = $metadata['created_at'] ?? '';
2405 $processed_date = 'Recently';
2406
2407 if (!empty($created_at)) {
2408 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
2409 if ($timestamp) {
2410 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2411 }
2412 }
2413
2414 $processed_data[$post_id] = array(
2415 'db_id' => $match_id,
2416 'processed_date' => $processed_date,
2417 'url' => $source_url,
2418 'source' => 'pinecone',
2419 'timestamp' => $timestamp ?? current_time('timestamp')
2420 );
2421
2422 $vector_ids_for_cache[] = $match_id;
2423 }
2424 }
2425 }
2426
2427 // Update the vector IDs cache for future use
2428 if (!empty($vector_ids_for_cache)) {
2429 update_option('mxchat_pinecone_vector_ids_cache', $vector_ids_for_cache);
2430 //error_log('DEBUG: Updated vector IDs cache with ' . count($vector_ids_for_cache) . ' IDs');
2431 }
2432
2433 //error_log('DEBUG: Returning ' . count($processed_data) . ' processed items from scanning');
2434 return $processed_data;
2435
2436 } catch (Exception $e) {
2437 //error_log('DEBUG: Exception in scan_pinecone_for_processed_content: ' . $e->getMessage());
2438 return array();
2439 }
2440 }
2441
2442 /**
2443 * Generates embeddings from input text for MXChat
2444 */
2445 private function mxchat_generate_embedding($text) {
2446 // Enable detailed logging for debugging
2447 //error_log('[MXCHAT-EMBED] Starting embedding generation. Text length: ' . strlen($text) . ' bytes');
2448 //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
2449
2450 $options = get_option('mxchat_options');
2451 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
2452 //error_log('[MXCHAT-EMBED] Selected embedding model: ' . $selected_model);
2453
2454 // Determine provider and endpoint
2455 if (strpos($selected_model, 'voyage') === 0) {
2456 $api_key = $options['voyage_api_key'] ?? '';
2457 $endpoint = 'https://api.voyageai.com/v1/embeddings';
2458 $provider_name = 'Voyage AI';
2459 //error_log('[MXCHAT-EMBED] Using Voyage AI API');
2460 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2461 $api_key = $options['gemini_api_key'] ?? '';
2462 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
2463 $provider_name = 'Google Gemini';
2464 //error_log('[MXCHAT-EMBED] Using Google Gemini API');
2465 } else {
2466 $api_key = $options['api_key'] ?? '';
2467 $endpoint = 'https://api.openai.com/v1/embeddings';
2468 $provider_name = 'OpenAI';
2469 //error_log('[MXCHAT-EMBED] Using OpenAI API');
2470 }
2471
2472 //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
2473
2474 if (empty($api_key)) {
2475 $error_message = sprintf('Missing %s API key. Please configure your API key in the MxChat settings.', $provider_name);
2476 //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
2477 return $error_message;
2478 }
2479
2480 // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
2481 $estimated_tokens = ceil(str_word_count($text) / 0.75);
2482 //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
2483
2484 if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
2485 //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
2486 // Consider truncating text here
2487 }
2488
2489 // Prepare request body based on provider
2490 if (strpos($selected_model, 'gemini-embedding') === 0) {
2491 // Gemini API format
2492 $request_body = array(
2493 'model' => 'models/' . $selected_model,
2494 'content' => array(
2495 'parts' => array(
2496 array('text' => $text)
2497 )
2498 )
2499 );
2500
2501 // Set output dimensionality to 1536 for consistency with other models
2502 $request_body['outputDimensionality'] = 1536;
2503 } else {
2504 // OpenAI/Voyage API format
2505 $request_body = array(
2506 'model' => $selected_model,
2507 'input' => $text
2508 );
2509
2510 // Add output_dimension for voyage-3-large model
2511 if ($selected_model === 'voyage-3-large') {
2512 $request_body['output_dimension'] = 2048;
2513 }
2514 }
2515
2516 //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
2517
2518 // Prepare headers based on provider
2519 if (strpos($selected_model, 'gemini-embedding') === 0) {
2520 // Gemini uses API key as query parameter
2521 $endpoint .= '?key=' . $api_key;
2522 $headers = array(
2523 'Content-Type' => 'application/json'
2524 );
2525 } else {
2526 // OpenAI/Voyage use Bearer token
2527 $headers = array(
2528 'Authorization' => 'Bearer ' . $api_key,
2529 'Content-Type' => 'application/json'
2530 );
2531 }
2532
2533 // Make API request
2534 //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
2535 $response = wp_remote_post($endpoint, array(
2536 'body' => wp_json_encode($request_body),
2537 'headers' => $headers,
2538 'timeout' => 60 // Increased timeout for large inputs
2539 ));
2540
2541 // Handle wp_remote_post errors
2542 if (is_wp_error($response)) {
2543 $error_message = $response->get_error_message();
2544 //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
2545 return 'Connection error: ' . $error_message;
2546 }
2547
2548 // Get and check HTTP response code
2549 $http_code = wp_remote_retrieve_response_code($response);
2550 //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
2551
2552 if ($http_code !== 200) {
2553 $error_body = wp_remote_retrieve_body($response);
2554 //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
2555
2556 // Try to parse error for more details
2557 $error_json = json_decode($error_body, true);
2558 if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
2559 $error_type = $error_json['error']['type'] ?? 'unknown';
2560 $error_message = $error_json['error']['message'] ?? 'No message';
2561 //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
2562 //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
2563
2564 // Customize error message for common API errors
2565 if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
2566 $error_message = sprintf('Invalid %s API key. Please check your API key in the MxChat settings.', $provider_name);
2567 } elseif ($error_type === 'authentication_error') {
2568 $error_message = sprintf('%s authentication failed. Please verify your API key in the MxChat settings.', $provider_name);
2569 }
2570
2571 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
2572 return $error_message;
2573 }
2574
2575 $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding", $http_code);
2576 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
2577 return $error_message;
2578 }
2579
2580 // Parse response body
2581 $response_body = wp_remote_retrieve_body($response);
2582 //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
2583
2584 $response_data = json_decode($response_body, true);
2585
2586 if (json_last_error() !== JSON_ERROR_NONE) {
2587 $error = json_last_error_msg();
2588 //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
2589 //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
2590 return "Failed to parse API response: $error";
2591 }
2592
2593 // Handle different response formats based on provider
2594 if (strpos($selected_model, 'gemini-embedding') === 0) {
2595 // Gemini API response format
2596 if (isset($response_data['embedding']['values'])) {
2597 $embedding_dimensions = count($response_data['embedding']['values']);
2598 //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
2599
2600 // Check if embedding dimensions are as expected (should be 1536)
2601 if ($embedding_dimensions !== 1536) {
2602 //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
2603 }
2604
2605 return $response_data['embedding']['values'];
2606 } else {
2607 //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
2608 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
2609
2610 if (isset($response_data['error'])) {
2611 $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
2612 //error_log('[MXCHAT-EMBED] ' . $error_message);
2613 return $error_message;
2614 }
2615
2616 $error_message = "Invalid Gemini API response format: No embedding found";
2617 //error_log('[MXCHAT-EMBED] ' . $error_message);
2618 return $error_message;
2619 }
2620 } else {
2621 // OpenAI/Voyage API response format
2622 if (isset($response_data['data'][0]['embedding'])) {
2623 $embedding_dimensions = count($response_data['data'][0]['embedding']);
2624 //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
2625
2626 // Check if embedding dimensions are as expected
2627 if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
2628 ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
2629 //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
2630 }
2631
2632 return $response_data['data'][0]['embedding'];
2633 } else {
2634 //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
2635 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
2636
2637 if (isset($response_data['error'])) {
2638 $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
2639 //error_log('[MXCHAT-EMBED] ' . $error_message);
2640 return $error_message;
2641 }
2642
2643 $error_message = "Invalid API response format: No embedding found";
2644 //error_log('[MXCHAT-EMBED] ' . $error_message);
2645 return $error_message;
2646 }
2647 }
2648 }
2649 public function mxchat_ajax_dismiss_completed_status() {
2650 try {
2651 // Verify the request
2652 check_ajax_referer('mxchat_status_nonce', 'nonce');
2653
2654 if (!current_user_can('manage_options')) {
2655 wp_send_json_error('Unauthorized access');
2656 exit;
2657 }
2658
2659 $card_type = isset($_POST['card_type']) ? sanitize_text_field($_POST['card_type']) : '';
2660
2661 if ($card_type === 'pdf') {
2662 // Clear PDF status
2663 $pdf_url = get_transient('mxchat_last_pdf_url');
2664 if ($pdf_url) {
2665 delete_transient('mxchat_pdf_status_' . md5($pdf_url));
2666 delete_transient('mxchat_last_pdf_url');
2667 }
2668 } elseif ($card_type === 'sitemap') {
2669 // Clear sitemap status
2670 $sitemap_url = get_transient('mxchat_last_sitemap_url');
2671 if ($sitemap_url) {
2672 delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
2673 delete_transient('mxchat_last_sitemap_url');
2674 }
2675 }
2676
2677 wp_send_json_success(array('message' => 'Status dismissed successfully'));
2678
2679 } catch (Exception $e) {
2680 wp_send_json_error(array('message' => 'Error dismissing status: ' . $e->getMessage()));
2681 }
2682 }
2683
2684 /**
2685 * Render completed status cards on page load
2686 * This ensures completed processing status persists through page refreshes
2687 */
2688 public function mxchat_render_completed_status_cards() {
2689 $output = '';
2690
2691 // Check for completed PDF status
2692 $pdf_url = get_transient('mxchat_last_pdf_url');
2693 if ($pdf_url) {
2694 $pdf_status = $this->mxchat_get_pdf_processing_status($pdf_url);
2695 if ($pdf_status && ($pdf_status['status'] === 'complete' || $pdf_status['status'] === 'error')) {
2696 $output .= $this->mxchat_render_pdf_status_card($pdf_status, $pdf_url);
2697 }
2698 }
2699
2700 // Check for completed sitemap status
2701 $sitemap_url = get_transient('mxchat_last_sitemap_url');
2702 if ($sitemap_url) {
2703 $sitemap_status = $this->mxchat_get_sitemap_processing_status($sitemap_url);
2704 if ($sitemap_status && ($sitemap_status['status'] === 'complete' || $sitemap_status['status'] === 'error')) {
2705 $output .= $this->mxchat_render_sitemap_status_card($sitemap_status, $sitemap_url);
2706 }
2707 }
2708
2709 return $output;
2710 }
2711
2712 /**
2713 * Render PDF status card HTML
2714 */
2715 private function mxchat_render_pdf_status_card($status, $pdf_url) {
2716 $html = '<div class="mxchat-status-card" data-card-type="pdf">';
2717 $html .= '<div class="mxchat-status-header">';
2718 $html .= '<h4>' . esc_html__('PDF Processing Status', 'mxchat') . '</h4>';
2719
2720 // Add dismiss button for completed status
2721 if ($status['status'] === 'complete' || $status['status'] === 'error') {
2722 $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
2723 }
2724
2725 // Process Batch button for processing status
2726 if ($status['status'] === 'processing') {
2727 $html .= '<button type="button" class="mxchat-manual-batch-btn"
2728 data-process-type="pdf"
2729 data-url="' . esc_attr($pdf_url) . '">
2730 ' . esc_html__('Process Batch', 'mxchat') . '</button>';
2731 }
2732
2733 // Add status badges
2734 if ($status['status'] === 'error') {
2735 $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
2736 } elseif ($status['status'] === 'complete') {
2737 if ($status['failed_pages'] > 0) {
2738 $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
2739 sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_pages']) . '</span>';
2740 } else {
2741 $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
2742 }
2743 }
2744
2745 $html .= '</div>'; // End header
2746
2747 // Progress bar
2748 $html .= '<div class="mxchat-progress-bar">';
2749 $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
2750 $html .= '</div>';
2751
2752 // Status details
2753 $html .= '<div class="mxchat-status-details">';
2754 $html .= '<p>' . sprintf(
2755 esc_html__('Progress: %d of %d pages (%d%%)', 'mxchat'),
2756 $status['processed_pages'],
2757 $status['total_pages'],
2758 $status['percentage']
2759 ) . '</p>';
2760
2761 // Show failed pages count if any
2762 if ($status['failed_pages'] > 0) {
2763 $html .= '<p><strong>' . esc_html__('Failed pages:', 'mxchat') . '</strong> ' . esc_html($status['failed_pages']) . '</p>';
2764 }
2765
2766 $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
2767 $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
2768
2769 // Add completion summary if available AND it's an array
2770 if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
2771 $summary = $status['completion_summary'];
2772 $html .= '<div class="mxchat-completion-summary">';
2773 $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
2774 $html .= '<p><strong>' . esc_html__('Total Pages:', 'mxchat') . '</strong> ' . esc_html($summary['total_pages']) . '</p>';
2775 $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_pages']) . '</p>';
2776 $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_pages']) . '</p>';
2777 $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
2778 $html .= '</div>';
2779 }
2780
2781 // Add failed pages list if any AND it's an array
2782 if (isset($status['failed_pages_list']) && is_array($status['failed_pages_list']) && !empty($status['failed_pages_list'])) {
2783 $html .= $this->mxchat_render_failed_pages_list($status['failed_pages_list']);
2784 }
2785
2786 // Add error message if any
2787 if (isset($status['error']) && !empty($status['error'])) {
2788 $html .= '<div class="mxchat-error-notice">';
2789 $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
2790 $html .= '</div>';
2791 }
2792
2793 $html .= '</div>'; // End details
2794 $html .= '</div>'; // End card
2795
2796 return $html;
2797 }
2798 /**
2799 * Render sitemap status card HTML
2800 */
2801 private function mxchat_render_sitemap_status_card($status, $sitemap_url) {
2802 $html = '<div class="mxchat-status-card" data-card-type="sitemap">';
2803 $html .= '<div class="mxchat-status-header">';
2804 $html .= '<h4>' . esc_html__('Sitemap Processing Status', 'mxchat') . '</h4>';
2805
2806 // Add dismiss button for completed status
2807 if ($status['status'] === 'complete' || $status['status'] === 'error') {
2808 $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
2809 }
2810
2811 // Process Batch button for processing status
2812 if ($status['status'] === 'processing') {
2813 $html .= '<button type="button" class="mxchat-manual-batch-btn"
2814 data-process-type="sitemap"
2815 data-url="' . esc_attr($sitemap_url) . '">
2816 ' . esc_html__('Process Batch', 'mxchat') . '</button>';
2817 }
2818
2819 // Add status badges
2820 if ($status['status'] === 'error') {
2821 $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
2822 } elseif ($status['status'] === 'complete') {
2823 if ($status['failed_urls'] > 0) {
2824 $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
2825 sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_urls']) . '</span>';
2826 } else {
2827 $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
2828 }
2829 }
2830
2831 $html .= '</div>'; // End header
2832
2833 // Progress bar
2834 $html .= '<div class="mxchat-progress-bar">';
2835 $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
2836 $html .= '</div>';
2837
2838 // Status details
2839 $html .= '<div class="mxchat-status-details">';
2840 $html .= '<p>' . sprintf(
2841 esc_html__('Progress: %d of %d URLs (%d%%)', 'mxchat'),
2842 $status['processed_urls'],
2843 $status['total_urls'],
2844 $status['percentage']
2845 ) . '</p>';
2846
2847 // Show failed URLs count if any
2848 if ($status['failed_urls'] > 0) {
2849 $html .= '<p><strong>' . esc_html__('Failed URLs:', 'mxchat') . '</strong> ' . esc_html($status['failed_urls']) . '</p>';
2850 }
2851
2852 $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
2853 $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
2854
2855 // Add completion summary if available AND it's an array
2856 if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
2857 $summary = $status['completion_summary'];
2858 $html .= '<div class="mxchat-completion-summary">';
2859 $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
2860 $html .= '<p><strong>' . esc_html__('Total URLs:', 'mxchat') . '</strong> ' . esc_html($summary['total_urls']) . '</p>';
2861 $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_urls']) . '</p>';
2862 $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_urls']) . '</p>';
2863 $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
2864 $html .= '</div>';
2865 }
2866
2867 // Add error messages if any (but not the failed URLs list)
2868 if (!empty($status['error']) || !empty($status['last_error'])) {
2869 $html .= '<div class="mxchat-error-notice">';
2870
2871 if (!empty($status['error'])) {
2872 $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
2873 }
2874
2875 if (!empty($status['last_error'])) {
2876 $html .= '<p class="last-error">' . esc_html__('Last error:', 'mxchat') . ' ' . esc_html($status['last_error']) . '</p>';
2877 }
2878
2879 $html .= '</div>';
2880 }
2881
2882 $html .= '</div>'; // End details
2883 $html .= '</div>'; // End card
2884
2885 return $html;
2886 }
2887
2888
2889 /**
2890 * Render failed pages list
2891 */
2892 private function mxchat_render_failed_pages_list($failed_pages_list) {
2893 // Validate that $failed_pages_list is an array and not empty
2894 if (!is_array($failed_pages_list) || empty($failed_pages_list)) {
2895 return '';
2896 }
2897
2898 $html = '<div class="mxchat-error-notice">';
2899 $html .= '<div class="mxchat-failed-pages-container">';
2900 $html .= '<h5>' . sprintf(esc_html__('Failed Pages (%d)', 'mxchat'), count($failed_pages_list)) . '</h5>';
2901 $html .= '<details>';
2902 $html .= '<summary>' . esc_html__('Show Failed Pages', 'mxchat') . '</summary>';
2903 $html .= '<div class="mxchat-failed-pages-list">';
2904
2905 // Create table for failed pages
2906 $html .= '<table class="widefat striped">';
2907 $html .= '<thead><tr>';
2908 $html .= '<th>' . esc_html__('Page', 'mxchat') . '</th>';
2909 $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
2910 $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
2911 $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
2912 $html .= '</tr></thead><tbody>';
2913
2914 // Sort failed pages by most recent
2915 $sorted_failed_pages = $failed_pages_list;
2916 usort($sorted_failed_pages, function($a, $b) {
2917 return ($b['time'] ?? 0) - ($a['time'] ?? 0);
2918 });
2919
2920 foreach ($sorted_failed_pages as $item) {
2921 // Ensure $item is an array before accessing its elements
2922 if (!is_array($item)) {
2923 continue;
2924 }
2925
2926 $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
2927 $html .= '<tr>';
2928 $html .= '<td>' . esc_html__('Page', 'mxchat') . ' ' . esc_html($item['page'] ?? 'Unknown') . '</td>';
2929 $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
2930 $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
2931 $html .= '<td>' . esc_html($time_ago) . '</td>';
2932 $html .= '</tr>';
2933 }
2934
2935 $html .= '</tbody></table>';
2936 $html .= '</div></details></div></div>';
2937
2938 return $html;
2939 }
2940
2941 /**
2942 * Render failed URLs list
2943 */
2944 private function mxchat_render_failed_urls_list($failed_urls_list) {
2945 // Validate that $failed_urls_list is an array and not empty
2946 if (!is_array($failed_urls_list) || empty($failed_urls_list)) {
2947 return '';
2948 }
2949
2950 $html = '<div class="mxchat-failed-urls-container">';
2951 $html .= '<h5>' . sprintf(esc_html__('Failed URLs (%d)', 'mxchat'), count($failed_urls_list)) . '</h5>';
2952 $html .= '<details>';
2953 $html .= '<summary>' . esc_html__('Show Failed URLs', 'mxchat') . '</summary>';
2954 $html .= '<div class="mxchat-failed-urls-list">';
2955
2956 // Create table for failed URLs
2957 $html .= '<table class="widefat striped">';
2958 $html .= '<thead><tr>';
2959 $html .= '<th>' . esc_html__('URL', 'mxchat') . '</th>';
2960 $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
2961 $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
2962 $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
2963 $html .= '</tr></thead><tbody>';
2964
2965 // Sort failed URLs by most recent
2966 $sorted_failed_urls = $failed_urls_list;
2967 usort($sorted_failed_urls, function($a, $b) {
2968 return ($b['time'] ?? 0) - ($a['time'] ?? 0);
2969 });
2970
2971 // Show up to 50 failed URLs
2972 $display_urls = array_slice($sorted_failed_urls, 0, 50);
2973
2974 foreach ($display_urls as $item) {
2975 // Ensure $item is an array before accessing its elements
2976 if (!is_array($item)) {
2977 continue;
2978 }
2979
2980 $url = $item['url'] ?? '';
2981 $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
2982
2983 // Truncate URL for display
2984 $display_url = strlen($url) > 50 ? substr($url, 0, 47) . '...' : $url;
2985
2986 $html .= '<tr>';
2987 $html .= '<td style="word-break: break-all;">';
2988 if (!empty($url)) {
2989 $html .= '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($display_url) . '</a>';
2990 } else {
2991 $html .= esc_html__('Unknown URL', 'mxchat');
2992 }
2993 $html .= '</td>';
2994 $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
2995 $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
2996 $html .= '<td>' . esc_html($time_ago) . '</td>';
2997 $html .= '</tr>';
2998 }
2999
3000 $html .= '</tbody></table>';
3001
3002 if (count($failed_urls_list) > 50) {
3003 $html .= '<div class="mxchat-failed-urls-more">+ ' .
3004 (count($failed_urls_list) - 50) .
3005 ' ' . esc_html__('more failed URLs not shown', 'mxchat') . '</div>';
3006 }
3007
3008 $html .= '</div></details></div>';
3009
3010 return $html;
3011 }
3012
3013 public function mxchat_handle_post_update($post_id, $post, $update) {
3014 // Basic validation checks
3015 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
3016 return;
3017 }
3018
3019 // Only process published content
3020 if ($post->post_status !== 'publish') {
3021 return;
3022 }
3023
3024 $post_type = $post->post_type;
3025
3026 // Check if sync is enabled for this post type
3027 $should_sync = false;
3028
3029 // Check built-in post types first
3030 if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
3031 $should_sync = true;
3032 } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
3033 $should_sync = true;
3034 } else {
3035 // Check custom post types
3036 $option_name = 'mxchat_auto_sync_' . $post_type;
3037 if (get_option($option_name) === '1') {
3038 $should_sync = true;
3039 }
3040 }
3041
3042 if (!$should_sync) {
3043 return;
3044 }
3045
3046 // Get content with proper formatting (matching ajax_mxchat_process_selected_content)
3047 $title = get_the_title($post_id);
3048 $content = get_post_field('post_content', $post_id);
3049
3050 // Apply WordPress content filters to get properly formatted content
3051 $content = apply_filters('the_content', $content);
3052
3053 // Strip tags but preserve structure
3054 $content = wp_strip_all_tags($content);
3055
3056 // Combine title and content
3057 $final_content = $title . "\n\n" . $content;
3058
3059 // For custom post types like job_listing, include additional fields
3060 if ($post_type === 'job_listing') {
3061 // Add job-specific meta if available
3062 $job_location = get_post_meta($post_id, '_job_location', true);
3063 if (!empty($job_location)) {
3064 $final_content .= "\n\nLocation: " . $job_location;
3065 }
3066
3067 // Get job type terms
3068 $job_types = get_the_terms($post_id, 'job_listing_type');
3069 if (!empty($job_types) && !is_wp_error($job_types)) {
3070 $types = array();
3071 foreach ($job_types as $type) {
3072 $types[] = $type->name;
3073 }
3074 $final_content .= "\n\nJob Type: " . implode(', ', $types);
3075 }
3076
3077 // Get company name if available
3078 $company_name = get_post_meta($post_id, '_company_name', true);
3079 if (!empty($company_name)) {
3080 $final_content .= "\n\nCompany: " . $company_name;
3081 }
3082 }
3083
3084 // Get the source URL
3085 $source_url = get_permalink($post_id);
3086
3087 // Get API key with proper model detection
3088 $options = get_option('mxchat_options');
3089 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3090
3091 if (strpos($selected_model, 'voyage') === 0) {
3092 $api_key = $options['voyage_api_key'] ?? '';
3093 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3094 $api_key = $options['gemini_api_key'] ?? '';
3095 } else {
3096 $api_key = $options['api_key'] ?? '';
3097 }
3098
3099 if (empty($api_key)) {
3100 //error_log('MxChat Auto-sync: No API key configured for embedding model');
3101 return;
3102 }
3103
3104 // Use the centralized utility function for storage
3105 $result = MxChat_Utils::submit_content_to_db(
3106 $final_content,
3107 $source_url,
3108 $api_key,
3109 md5($source_url) // Vector ID for Pinecone
3110 );
3111
3112 if (is_wp_error($result)) {
3113 //error_log('MxChat Auto-sync failed for post ' . $post_id . ': ' . $result->get_error_message());
3114 }
3115 }
3116
3117
3118 public function mxchat_handle_post_delete($post_id) {
3119 // Get post data before it's deleted
3120 $post = get_post($post_id);
3121
3122 // Basic validation
3123 if (!$post || wp_is_post_revision($post_id)) {
3124 return;
3125 }
3126
3127 $post_type = $post->post_type;
3128
3129 // Check if sync is enabled for this post type
3130 $should_sync = false;
3131
3132 // Check built-in post types first
3133 if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
3134 $should_sync = true;
3135 } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
3136 $should_sync = true;
3137 } else {
3138 // Check custom post types
3139 $option_name = 'mxchat_auto_sync_' . $post_type;
3140 if (get_option($option_name) === '1') {
3141 $should_sync = true;
3142 }
3143 }
3144
3145 if (!$should_sync) {
3146 return;
3147 }
3148
3149 // Get the URL before post is deleted
3150 $source_url = get_permalink($post_id);
3151 if (!$source_url) {
3152 //error_log('MXChat: Failed to get permalink for post ' . $post_id);
3153 return;
3154 }
3155
3156 // Check if Pinecone is enabled
3157 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3158 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3159
3160 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3161 // Delete from Pinecone
3162 $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
3163 } else {
3164 // Delete from WordPress DB
3165 global $wpdb;
3166 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3167
3168 $result = $wpdb->delete(
3169 $table_name,
3170 array('source_url' => $source_url),
3171 array('%s')
3172 );
3173
3174 if ($result === false) {
3175 //error_log('MXChat: WordPress DB deletion failed for URL: ' . $source_url);
3176 }
3177 }
3178 }
3179
3180
3181 /**
3182 * Deletes data from Pinecone using a source URL
3183 */
3184 public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
3185 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3186 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3187
3188 if (empty($host) || empty($api_key)) {
3189 //error_log('MXChat: Pinecone deletion failed - missing configuration');
3190 return false;
3191 }
3192
3193 $api_endpoint = "https://{$host}/vectors/delete";
3194 $vector_id = md5($source_url);
3195
3196 $request_body = array(
3197 'ids' => array($vector_id)
3198 );
3199
3200 $response = wp_remote_post($api_endpoint, array(
3201 'headers' => array(
3202 'Api-Key' => $api_key,
3203 'accept' => 'application/json',
3204 'content-type' => 'application/json'
3205 ),
3206 'body' => wp_json_encode($request_body),
3207 'timeout' => 30
3208 ));
3209
3210 if (is_wp_error($response)) {
3211 //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
3212 return false;
3213 }
3214
3215 $response_code = wp_remote_retrieve_response_code($response);
3216 if ($response_code !== 200) {
3217 //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
3218 return false;
3219 }
3220
3221 return true;
3222 }
3223
3224
3225
3226 public function mxchat_handle_product_change($post_id, $post, $update) {
3227 if ($post->post_type !== 'product') {
3228 return;
3229 }
3230
3231 if ($post->post_status === 'publish') {
3232 add_action('shutdown', function() use ($post_id) {
3233 $product = wc_get_product($post_id);
3234 if ($product) {
3235 $this->mxchat_store_product_embedding($product);
3236 }
3237 });
3238 }
3239 }
3240
3241 /**
3242 * Store WooCommerce product embeddings
3243 */
3244 private function mxchat_store_product_embedding($product) {
3245 if (!isset($this->options['enable_woocommerce_integration']) ||
3246 !in_array($this->options['enable_woocommerce_integration'], ['1', 'on'])) {
3247 return;
3248 }
3249
3250 $source_url = get_permalink($product->get_id());
3251
3252 // Build product content
3253 $title = $product->get_name();
3254 $description = $product->get_description();
3255 $short_description = $product->get_short_description();
3256 $regular_price = $product->get_regular_price();
3257 $sale_price = $product->get_sale_price();
3258 $sku = $product->get_sku();
3259
3260 // Format content consistently
3261 $content = $title . "\n\n";
3262
3263 if (!empty($description)) {
3264 $content .= wp_strip_all_tags($description) . "\n\n";
3265 }
3266
3267 if (!empty($short_description)) {
3268 $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
3269 }
3270
3271 $content .= "Price: $" . $regular_price . "\n";
3272
3273 if (!empty($sale_price)) {
3274 $content .= "Sale Price: $" . $sale_price . "\n";
3275 }
3276
3277 if (!empty($sku)) {
3278 $content .= "SKU: " . $sku . "\n";
3279 }
3280
3281 // Get API key with proper model detection
3282 $options = get_option('mxchat_options');
3283 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3284
3285 if (strpos($selected_model, 'voyage') === 0) {
3286 $api_key = $options['voyage_api_key'] ?? '';
3287 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3288 $api_key = $options['gemini_api_key'] ?? '';
3289 } else {
3290 $api_key = $options['api_key'] ?? '';
3291 }
3292
3293 if (empty($api_key)) {
3294 //error_log('MxChat Auto-sync: No API key configured for embedding model');
3295 return;
3296 }
3297
3298 // Use the centralized utility function for storage
3299 $result = MxChat_Utils::submit_content_to_db(
3300 $content,
3301 $source_url,
3302 $api_key,
3303 md5($source_url) // Vector ID for Pinecone
3304 );
3305
3306 if (is_wp_error($result)) {
3307 //error_log('MxChat WooCommerce sync failed for product ' . $product->get_id() . ': ' . $result->get_error_message());
3308 }
3309 }
3310
3311 public function mxchat_handle_product_delete($post_id) {
3312 if (get_post_type($post_id) !== 'product') {
3313 return;
3314 }
3315
3316 $source_url = get_permalink($post_id);
3317
3318 // Check if Pinecone is enabled
3319 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3320 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3321
3322 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3323 // Delete from Pinecone
3324 $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
3325 } else {
3326 // Delete from WordPress DB
3327 global $wpdb;
3328 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3329
3330 $wpdb->delete(
3331 $table_name,
3332 array('source_url' => $source_url),
3333 array('%s')
3334 );
3335 }
3336 }
3337
3338 // ========================================
3339 // HELPER METHODS
3340 // ========================================
3341
3342 /**
3343 * Check if user has required permissions for content processing
3344 */
3345 private function mxchat_check_user_permissions() {
3346 if (!current_user_can('manage_options')) {
3347 wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
3348 }
3349 }
3350
3351 /**
3352 * Validate nonce for security
3353 */
3354 private function mxchat_validate_nonce($nonce_name, $nonce_action) {
3355 if (!isset($_POST[$nonce_name]) || !wp_verify_nonce($_POST[$nonce_name], $nonce_action)) {
3356 wp_die(esc_html__('Security check failed.', 'mxchat'));
3357 }
3358 }
3359
3360 /**
3361 * Get embedding API credentials
3362 */
3363 private function mxchat_get_embedding_credentials() {
3364 $embedding_model = $this->options['embedding_model'] ?? 'text-embedding-ada-002';
3365
3366 if (strpos($embedding_model, 'text-embedding-') !== false) {
3367 return array(
3368 'type' => 'openai',
3369 'api_key' => $this->options['api_key'] ?? ''
3370 );
3371 } elseif (strpos($embedding_model, 'voyage-') !== false) {
3372 return array(
3373 'type' => 'voyage',
3374 'api_key' => $this->options['voyage_api_key'] ?? ''
3375 );
3376 } elseif (strpos($embedding_model, 'gemini-embedding-') !== false) {
3377 return array(
3378 'type' => 'gemini',
3379 'api_key' => $this->options['gemini_api_key'] ?? ''
3380 );
3381 }
3382
3383 return array('type' => 'unknown', 'api_key' => '');
3384 }
3385
3386 /**
3387 * Log processing errors
3388 */
3389 private function mxchat_log_processing_error($operation, $error_message) {
3390 //error_log("MxChat Knowledge Processing {$operation} Error: " . $error_message);
3391 }
3392
3393 /**
3394 * Set admin notice transient
3395 */
3396 private function mxchat_set_admin_notice($type, $message) {
3397 set_transient("mxchat_admin_notice_{$type}", $message, 30);
3398 }
3399
3400 /**
3401 * Get Pinecone manager instance for vector operations
3402 */
3403 private function mxchat_get_pinecone_manager() {
3404 return MxChat_Pinecone_Manager::get_instance();
3405 }
3406
3407 // ========================================
3408 // STATIC ACCESS METHODS
3409 // ========================================
3410
3411 /**
3412 * Get singleton instance
3413 */
3414 public static function get_instance() {
3415 static $instance = null;
3416 if ($instance === null) {
3417 $instance = new self();
3418 }
3419 return $instance;
3420 }
3421 }
3422
3423 // Initialize the Knowledge manager
3424 $mxchat_knowledge_manager = MxChat_Knowledge_Manager::get_instance();