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

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