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

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