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

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