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

4,526 lines 175.8 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'));
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 - UPDATED FOR BETTER STATUS TRACKING
47 add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
48 add_action('post_updated', array($this, 'mxchat_handle_post_update'), 10, 3);
49 add_action('before_delete_post', array($this, 'mxchat_handle_post_delete'));
50 add_action('wp_trash_post', array($this, 'mxchat_handle_post_delete'));
51 add_action('wp_ajax_mxchat_save_inline_prompt', array($this, 'mxchat_save_inline_prompt'));
52 add_action('admin_post_mxchat_delete_pinecone_prompt', array($this, 'mxchat_handle_pinecone_prompt_delete'));
53 add_action('wp_ajax_mxchat_delete_pinecone_prompt', array($this, 'ajax_mxchat_delete_pinecone_prompt'));
54 add_action('wp_ajax_mxchat_update_role_restriction', array($this, 'ajax_mxchat_update_role_restriction'));
55
56 // WooCommerce product hooks (if WooCommerce is active)
57 if (class_exists('WooCommerce')) {
58 add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2); // Same hook for products
59 add_action('save_post_product', array($this, 'mxchat_handle_product_change'), 10, 3);
60 add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
61 add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
62 }
63
64 }
65
66 /**
67 * Get current options (refreshed)
68 */
69 private function mxchat_get_options() {
70 if (empty($this->options)) {
71 $this->options = get_option('mxchat_options', array());
72 }
73 return $this->options;
74 }
75
76 public function ajax_manual_batch_process() {
77 try {
78 // Verify nonce and permissions
79 check_ajax_referer('mxchat_status_nonce', 'nonce');
80
81 if (!current_user_can('manage_options')) {
82 wp_send_json_error('Unauthorized access');
83 }
84
85 $process_type = sanitize_text_field($_POST['process_type'] ?? '');
86 $url = sanitize_text_field($_POST['url'] ?? '');
87
88 if (empty($process_type) || empty($url)) {
89 wp_send_json_error('Missing required parameters');
90 }
91
92 // Debug logging
93 //error_log('MANUAL BATCH DEBUG: Process type: ' . $process_type);
94 //error_log('MANUAL BATCH DEBUG: URL: ' . $url);
95
96 // FIXED: Extract bot_id from stored status instead of POST data
97 $bot_id = 'default';
98
99 if ($process_type === 'pdf') {
100 $status_key = sanitize_key('mxchat_pdf_status_' . md5($url));
101 $status = get_transient($status_key);
102 //error_log('MANUAL BATCH DEBUG: Status key: ' . $status_key);
103 //error_log('MANUAL BATCH DEBUG: Status data: ' . print_r($status, true));
104
105 if ($status && isset($status['bot_id'])) {
106 $bot_id = $status['bot_id'];
107 //error_log('MANUAL BATCH DEBUG: Bot ID from status: ' . $bot_id);
108 } else {
109 //error_log('MANUAL BATCH DEBUG: No bot_id in status, using default');
110 }
111 } elseif ($process_type === 'sitemap') {
112 $status_key = sanitize_key('mxchat_sitemap_status_' . md5($url));
113 $status = get_transient($status_key);
114 if ($status && isset($status['bot_id'])) {
115 $bot_id = $status['bot_id'];
116 }
117 }
118
119 //error_log('MANUAL BATCH DEBUG: Final bot_id: ' . $bot_id);
120
121 $processed = 0;
122
123 if ($process_type === 'pdf') {
124 $processed = $this->mxchat_manual_process_pdf_batch($url);
125 //error_log('MANUAL BATCH DEBUG: PDF processing returned: ' . $processed);
126 } elseif ($process_type === 'sitemap') {
127 $processed = $this->mxchat_manual_process_sitemap_batch($url);
128 }
129
130 if ($processed > 0) {
131 wp_send_json_success(array(
132 'message' => "Processed {$processed} items successfully",
133 'processed' => $processed,
134 'bot_id' => $bot_id
135 ));
136 } else {
137 // Enhanced error response with debugging info
138 wp_send_json_error(array(
139 'message' => 'No items were processed',
140 'debug_info' => array(
141 'process_type' => $process_type,
142 'url' => $url,
143 'bot_id' => $bot_id,
144 'status_exists' => !empty($status),
145 'status_data' => $status
146 )
147 ));
148 }
149
150 } catch (Exception $e) {
151 //error_log('MANUAL BATCH DEBUG: Exception: ' . $e->getMessage());
152 wp_send_json_error('Processing failed: ' . $e->getMessage());
153 }
154 }
155
156 private function mxchat_manual_process_pdf_batch($pdf_url) {
157 try {
158 //error_log('MANUAL PDF DEBUG: Starting batch processing for: ' . $pdf_url);
159
160 $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
161 $status = get_transient($status_key);
162
163 //error_log('MANUAL PDF DEBUG: Status key: ' . $status_key);
164 //error_log('MANUAL PDF DEBUG: Status data: ' . print_r($status, true));
165
166 if (!$status || $status['status'] !== 'processing') {
167 //error_log('MANUAL PDF DEBUG: No processing status found or status is not processing');
168 //error_log('MANUAL PDF DEBUG: Status: ' . ($status ? $status['status'] : 'NULL'));
169 return 0;
170 }
171
172 // FIXED: Extract bot_id from status
173 $bot_id = $status['bot_id'] ?? 'default';
174 //error_log('MANUAL PDF DEBUG: Bot ID from status: ' . $bot_id);
175
176 // Get current progress
177 $current_page = $status['processed_pages'] ?? 0;
178 $total_pages = $status['total_pages'] ?? 0;
179
180 //error_log('MANUAL PDF DEBUG: Current page: ' . $current_page . ', Total pages: ' . $total_pages);
181
182 if ($current_page >= $total_pages) {
183 //error_log('MANUAL PDF DEBUG: Already completed');
184 return 0;
185 }
186
187 // Try to download the PDF again for processing
188 //error_log('MANUAL PDF DEBUG: Attempting to download PDF');
189 $response = wp_remote_get($pdf_url, array('timeout' => 30));
190
191 if (is_wp_error($response)) {
192 //error_log('MANUAL PDF DEBUG: Failed to download PDF: ' . $response->get_error_message());
193 return 0;
194 }
195
196 $pdf_content = wp_remote_retrieve_body($response);
197 if (empty($pdf_content)) {
198 //error_log('MANUAL PDF DEBUG: Empty PDF content');
199 return 0;
200 }
201
202 //error_log('MANUAL PDF DEBUG: PDF content size: ' . strlen($pdf_content) . ' bytes');
203
204 // Save PDF temporarily
205 $upload_dir = wp_upload_dir();
206 $temp_pdf_path = trailingslashit($upload_dir['path']) . 'temp_manual_' . time() . '.pdf';
207 file_put_contents($temp_pdf_path, $pdf_content);
208
209 //error_log('MANUAL PDF DEBUG: Temp PDF saved to: ' . $temp_pdf_path);
210
211 // Process 5 pages directly with bot_id
212 $processed = $this->mxchat_process_pdf_pages_direct($temp_pdf_path, $pdf_url, $current_page, 5, $bot_id);
213
214 //error_log('MANUAL PDF DEBUG: Direct processing returned: ' . $processed);
215
216 // Clean up temp file
217 if (file_exists($temp_pdf_path)) {
218 wp_delete_file($temp_pdf_path);
219 //error_log('MANUAL PDF DEBUG: Cleaned up temp file');
220 }
221
222 return $processed;
223
224 } catch (Exception $e) {
225 //error_log('MANUAL PDF DEBUG: Exception in manual batch: ' . $e->getMessage());
226 return 0;
227 }
228 }
229
230 /**
231 * Process PDF pages directly without cron
232 */
233 private function mxchat_process_pdf_pages_direct($pdf_path, $pdf_url, $start_page, $batch_size, $bot_id = 'default') {
234 try {
235 if (!file_exists($pdf_path)) {
236 //error_log('Direct PDF: File not found at ' . $pdf_path);
237 return 0;
238 }
239
240 $parser = new \Smalot\PdfParser\Parser();
241 $pdf = $parser->parseFile($pdf_path);
242 $pages = $pdf->getPages();
243
244 $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
245 $status = get_transient($status_key);
246
247 if (!$status) {
248 return 0;
249 }
250
251 // UPDATED: Get bot-specific options
252 $bot_options = $this->get_bot_options($bot_id);
253 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
254 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
255
256 if (strpos($selected_model, 'voyage') === 0) {
257 $api_key = $options['voyage_api_key'] ?? '';
258 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
259 $api_key = $options['gemini_api_key'] ?? '';
260 } else {
261 $api_key = $options['api_key'] ?? '';
262 }
263
264 if (empty($api_key)) {
265 //error_log('Direct PDF: No API key for bot: ' . $bot_id);
266 return 0;
267 }
268
269 $processed = 0;
270 $end_page = min($start_page + $batch_size, count($pages));
271
272 for ($i = $start_page; $i < $end_page; $i++) {
273 try {
274 $page_number = $i + 1;
275 $text = $pages[$i]->getText();
276
277 if (empty($text)) {
278 //error_log('Direct PDF: Empty text on page ' . $page_number);
279 continue;
280 }
281
282 $sanitized_content = $this->mxchat_sanitize_content_for_api($text);
283 if (empty($sanitized_content)) {
284 //error_log('Direct PDF: No content after sanitization on page ' . $page_number);
285 continue;
286 }
287
288 // UPDATED: Use bot-specific embedding generation
289 $embedding_vector = $this->mxchat_generate_embedding($sanitized_content, $bot_id);
290 if (is_string($embedding_vector)) {
291 //error_log('Direct PDF: Embedding failed on page ' . $page_number . ': ' . $embedding_vector);
292 continue;
293 }
294
295 // Create metadata
296 $metadata = array(
297 'document_type' => 'pdf',
298 'total_pages' => count($pages),
299 'current_page' => $page_number,
300 'source_url' => $pdf_url
301 );
302
303 $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized_content;
304 $page_url = esc_url($pdf_url . "#page=" . $page_number);
305
306 // UPDATED: Pass bot_id to database submission
307 $db_result = MxChat_Utils::submit_content_to_db($content_with_metadata, $page_url, $api_key, null, $bot_id);
308
309 if (is_wp_error($db_result)) {
310 //error_log('Direct PDF: DB error on page ' . $page_number . ': ' . $db_result->get_error_message());
311 continue;
312 }
313
314 $processed++;
315 //error_log('Direct PDF: Successfully processed page ' . $page_number . ' for bot: ' . $bot_id);
316
317 // Update status
318 $status['processed_pages'] = $i + 1;
319 $status['last_update'] = time();
320 $status['percentage'] = round(($status['processed_pages'] / $status['total_pages']) * 100);
321 set_transient($status_key, $status, DAY_IN_SECONDS);
322
323 } catch (Exception $e) {
324 //error_log('Direct PDF: Error processing page ' . ($i + 1) . ': ' . $e->getMessage());
325 continue;
326 }
327 }
328
329 // Check if completed
330 if ($status['processed_pages'] >= $status['total_pages']) {
331 $status['status'] = 'complete';
332 set_transient($status_key, $status, DAY_IN_SECONDS);
333 //error_log('Direct PDF: Processing completed for bot: ' . $bot_id);
334 }
335
336 return $processed;
337
338 } catch (Exception $e) {
339 //error_log('Direct PDF processing error: ' . $e->getMessage());
340 return 0;
341 }
342 }
343
344
345 /**
346 * Process a small sitemap batch manually - DIRECT PROCESSING
347 */
348 private function mxchat_manual_process_sitemap_batch($sitemap_url) {
349 try {
350 $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
351 $status = get_transient($status_key);
352
353 if (!$status || $status['status'] !== 'processing') {
354 return 0;
355 }
356
357 // FIXED: Extract bot_id from status
358 $bot_id = $status['bot_id'] ?? 'default';
359
360 //error_log('Manual Sitemap: Starting direct processing for ' . $sitemap_url . ' with bot: ' . $bot_id);
361
362 // Re-fetch the sitemap to get URLs
363 $response = wp_remote_get($sitemap_url, array('timeout' => 30));
364 if (is_wp_error($response)) {
365 //error_log('Manual Sitemap: Failed to fetch sitemap');
366 return 0;
367 }
368
369 $sitemap_content = wp_remote_retrieve_body($response);
370 $xml = simplexml_load_string($sitemap_content);
371
372 if (!$xml) {
373 //error_log('Manual Sitemap: Invalid XML');
374 return 0;
375 }
376
377 $urls = array();
378 foreach ($xml->url as $url_element) {
379 $urls[] = (string)$url_element->loc;
380 }
381
382 $current_processed = $status['processed_urls'] ?? 0;
383 $batch_size = 50;
384 $processed = 0;
385
386 // Process next batch of URLs
387 for ($i = $current_processed; $i < min($current_processed + $batch_size, count($urls)); $i++) {
388 $url = $urls[$i];
389
390 // UPDATED: Pass bot_id to single URL processing
391 if ($this->mxchat_process_single_url_direct($url, $bot_id)) {
392 $processed++;
393 }
394
395 // Update status
396 $status['processed_urls'] = $i + 1;
397 $status['last_update'] = time();
398 $status['percentage'] = round(($status['processed_urls'] / $status['total_urls']) * 100);
399 set_transient($status_key, $status, DAY_IN_SECONDS);
400 }
401
402 // Check if completed
403 if ($status['processed_urls'] >= $status['total_urls']) {
404 $status['status'] = 'complete';
405 set_transient($status_key, $status, DAY_IN_SECONDS);
406 }
407
408 //error_log('Manual Sitemap: Processed ' . $processed . ' URLs for bot: ' . $bot_id);
409 return $processed;
410
411 } catch (Exception $e) {
412 //error_log('Manual sitemap batch error: ' . $e->getMessage());
413 return 0;
414 }
415 }
416
417
418 /**
419 * Process a single URL directly
420 */
421 private function mxchat_process_single_url_direct($url, $bot_id = 'default') {
422 try {
423 $response = wp_remote_get($url, array('timeout' => 30));
424 if (is_wp_error($response)) {
425 //error_log('Single URL processing failed for ' . $url . ': ' . $response->get_error_message());
426 return false;
427 }
428
429 $html = wp_remote_retrieve_body($response);
430 $content = $this->mxchat_extract_main_content($html);
431 $sanitized = $this->mxchat_sanitize_content_for_api($content);
432
433 if (empty($sanitized)) {
434 //error_log('Single URL processing: No content found for ' . $url);
435 return false;
436 }
437
438 // UPDATED: Get bot-specific options and API key
439 $bot_options = $this->get_bot_options($bot_id);
440 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
441 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
442
443 if (strpos($selected_model, 'voyage') === 0) {
444 $api_key = $options['voyage_api_key'] ?? '';
445 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
446 $api_key = $options['gemini_api_key'] ?? '';
447 } else {
448 $api_key = $options['api_key'] ?? '';
449 }
450
451 if (empty($api_key)) {
452 //error_log('Single URL processing: No API key configured for bot: ' . $bot_id);
453 return false;
454 }
455
456 // UPDATED: Pass bot_id to database submission
457 $result = MxChat_Utils::submit_content_to_db($sanitized, $url, $api_key, null, $bot_id);
458
459 $success = !is_wp_error($result);
460
461 if ($success) {
462 //error_log('Single URL processing: Successfully processed ' . $url . ' for bot: ' . $bot_id);
463 } else {
464 //error_log('Single URL processing: Failed to store ' . $url . ' for bot: ' . $bot_id . ': ' . $result->get_error_message());
465 }
466
467 return $success;
468
469 } catch (Exception $e) {
470 //error_log('Single URL processing error: ' . $e->getMessage());
471 return false;
472 }
473 }
474
475 // ========================================
476 // MAIN CONTENT SUBMISSION HANDLERS
477 // ========================================
478
479 public function mxchat_handle_content_submission() {
480 // Check if the form was submitted and the user has permission.
481 if (!isset($_POST['submit_content']) || !current_user_can('manage_options')) {
482 return;
483 }
484
485 // Verify the nonce.
486 $nonce = isset($_POST['mxchat_submit_content_nonce']) ? sanitize_text_field(wp_unslash($_POST['mxchat_submit_content_nonce'])) : '';
487 if (!wp_verify_nonce($nonce, 'mxchat_submit_content_action')) {
488 wp_die(esc_html__('Nonce verification failed.', 'mxchat'));
489 }
490
491 // Sanitize the inputs.
492 $article_content = sanitize_textarea_field($_POST['article_content']);
493 $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
494
495 // UPDATED: Get bot_id from form submission
496 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
497
498 // UPDATED: Get bot-specific options and API key
499 $bot_options = $this->get_bot_options($bot_id);
500 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
501 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
502
503 if (strpos($selected_model, 'voyage') === 0) {
504 $api_key = $options['voyage_api_key'] ?? '';
505 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
506 $api_key = $options['gemini_api_key'] ?? '';
507 } else {
508 $api_key = $options['api_key'] ?? '';
509 }
510
511 if (empty($api_key)) {
512 set_transient('mxchat_admin_notice_error',
513 esc_html__('API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
514 30
515 );
516 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
517 exit;
518 }
519
520 // UPDATED: Use centralized utility function with bot_id
521 $result = MxChat_Utils::submit_content_to_db($article_content, $article_url, $api_key, null, $bot_id);
522
523 if (is_wp_error($result)) {
524 set_transient('mxchat_admin_notice_error',
525 esc_html__('Error storing content: ', 'mxchat') . $result->get_error_message(),
526 30
527 );
528 } else {
529 set_transient('mxchat_admin_notice_success',
530 esc_html__('Content successfully submitted!', 'mxchat'),
531 30
532 );
533 }
534
535 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
536 exit;
537 }
538
539 public function mxchat_is_pdf_url($url, $response) {
540 $content_type = wp_remote_retrieve_header($response, 'content-type');
541 $file_extension = strtolower(pathinfo($url, PATHINFO_EXTENSION));
542
543 return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf';
544 }
545
546
547 public function mxchat_handle_pdf_for_knowledge_base($pdf_url, $response, $bot_id = 'default') {
548 if (!current_user_can('manage_options')) {
549 //error_log('[PDF DEBUG] Unauthorized PDF processing attempt');
550 return false;
551 }
552
553 //error_log('[PDF DEBUG] Starting PDF processing for bot: ' . $bot_id);
554 //error_log('[PDF DEBUG] PDF URL: ' . $pdf_url);
555
556 $pdf_url = esc_url_raw($pdf_url);
557 $upload_dir = wp_upload_dir();
558
559 if (isset($upload_dir['error']) && $upload_dir['error'] !== false) {
560 //error_log('[PDF DEBUG] Upload directory error: ' . $upload_dir['error']);
561 return false;
562 }
563
564 $pdf_filename = sanitize_file_name('mxchat_kb_' . time() . '.pdf');
565 $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
566
567 $response_body = wp_remote_retrieve_body($response);
568 if (empty($response_body)) {
569 //error_log('[PDF DEBUG] Empty PDF response body');
570 return false;
571 }
572
573 if (!wp_mkdir_p(dirname($pdf_path))) {
574 //error_log('[PDF DEBUG] Failed to create directory for PDF: ' . $pdf_path);
575 return false;
576 }
577
578 try {
579 file_put_contents($pdf_path, $response_body);
580
581 if (!file_exists($pdf_path)) {
582 throw new Exception(__('Failed to save PDF file', 'mxchat'));
583 }
584
585 $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
586
587 if ($total_pages === false || $total_pages < 1) {
588 throw new Exception(__('Invalid PDF: Unable to parse or no pages found', 'mxchat'));
589 }
590
591 //error_log('[PDF DEBUG] PDF validated successfully with ' . $total_pages . ' pages');
592
593 // UPDATED: Pass bot_id to PDF processing cron
594 wp_schedule_single_event(time(), 'mxchat_process_pdf_pages', array(
595 $pdf_path, // Position 0
596 $pdf_url, // Position 1
597 $total_pages, // Position 2
598 absint(15), // Position 3 (batch_size)
599 absint(10), // Position 4 (batch_pause)
600 $bot_id // Position 5 (bot_id)
601 ));
602
603 //error_log('[PDF DEBUG] Cron job scheduled with bot_id: ' . $bot_id);
604
605 // UPDATED: Store bot_id in status data
606 $status_data = array(
607 'total_pages' => $total_pages,
608 'processed_pages' => 0,
609 'status' => 'processing',
610 'last_update' => time(),
611 'bot_id' => $bot_id // CRITICAL: Store the bot_id
612 );
613
614 $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
615 set_transient($status_key, $status_data, DAY_IN_SECONDS);
616
617 //error_log('[PDF DEBUG] Status stored with bot_id: ' . $bot_id . ' using key: ' . $status_key);
618
619 return __('scheduled', 'mxchat');
620
621 } catch (Exception $e) {
622 //error_log('[PDF DEBUG] Error preparing PDF for processing: ' . $e->getMessage());
623 if (file_exists($pdf_path)) {
624 wp_delete_file($pdf_path);
625 }
626 return false;
627 }
628 }
629
630 /**
631 * NEW: Validate PDF and count pages with multiple parser attempts
632 */
633 private function mxchat_validate_and_count_pdf_pages($pdf_path) {
634 // Method 1: Try with Smalot PDF Parser (your current method)
635 try {
636 $parser = new \Smalot\PdfParser\Parser();
637 $pdf = $parser->parseFile($pdf_path);
638 $pages = $pdf->getPages();
639 $page_count = count($pages);
640
641 if ($page_count > 0) {
642 //error_log('PDF parsed successfully with Smalot parser: ' . $page_count . ' pages');
643 return $page_count;
644 }
645 } catch (Exception $e) {
646 //error_log('Smalot PDF parser failed: ' . $e->getMessage());
647 }
648
649 // Method 2: Try with pdfinfo command (if available)
650 if (function_exists('shell_exec') && !$this->mxchat_is_shell_disabled()) {
651 try {
652 $command = 'pdfinfo ' . escapeshellarg($pdf_path) . ' 2>&1';
653 $output = shell_exec($command);
654
655 if ($output && preg_match('/Pages:\s*(\d+)/', $output, $matches)) {
656 $page_count = intval($matches[1]);
657 if ($page_count > 0) {
658 //error_log('PDF parsed successfully with pdfinfo: ' . $page_count . ' pages');
659 return $page_count;
660 }
661 }
662 } catch (Exception $e) {
663 //error_log('pdfinfo command failed: ' . $e->getMessage());
664 }
665 }
666
667 // Method 3: Try to repair PDF and parse again
668 try {
669 $repaired_path = $this->mxchat_attempt_pdf_repair($pdf_path);
670 if ($repaired_path && $repaired_path !== $pdf_path) {
671 $parser = new \Smalot\PdfParser\Parser();
672 $pdf = $parser->parseFile($repaired_path);
673 $pages = $pdf->getPages();
674 $page_count = count($pages);
675
676 if ($page_count > 0) {
677 // Replace original with repaired version
678 copy($repaired_path, $pdf_path);
679 unlink($repaired_path);
680 //error_log('PDF repaired and parsed successfully: ' . $page_count . ' pages');
681 return $page_count;
682 }
683
684 // Clean up repaired file if it didn't work
685 unlink($repaired_path);
686 }
687 } catch (Exception $e) {
688 //error_log('PDF repair attempt failed: ' . $e->getMessage());
689 }
690
691 // Method 4: Manual PDF structure analysis (basic page count)
692 try {
693 $page_count = $this->mxchat_manual_pdf_page_count($pdf_path);
694 if ($page_count > 0) {
695 //error_log('PDF page count determined manually: ' . $page_count . ' pages');
696 return $page_count;
697 }
698 } catch (Exception $e) {
699 //error_log('Manual PDF analysis failed: ' . $e->getMessage());
700 }
701
702 //error_log('All PDF parsing methods failed for: ' . $pdf_path);
703 return false;
704 }
705
706 /**
707 * NEW: Check if shell_exec is disabled
708 */
709 private function mxchat_is_shell_disabled() {
710 $disabled = explode(',', ini_get('disable_functions'));
711 return in_array('shell_exec', $disabled);
712 }
713
714 /**
715 * NEW: Attempt to repair PDF using basic methods
716 */
717 private function mxchat_attempt_pdf_repair($pdf_path) {
718 try {
719 $content = file_get_contents($pdf_path);
720 if (!$content) {
721 return false;
722 }
723
724 // Check if PDF starts with proper header
725 if (substr($content, 0, 4) !== '%PDF') {
726 // Try to find PDF header in the content
727 $header_pos = strpos($content, '%PDF');
728 if ($header_pos !== false && $header_pos < 1024) {
729 // Remove junk before PDF header
730 $content = substr($content, $header_pos);
731 $repaired_path = $pdf_path . '.repaired';
732 file_put_contents($repaired_path, $content);
733 return $repaired_path;
734 }
735 }
736
737 // Check for EOF marker
738 $content = rtrim($content);
739 if (!preg_match('/%%EOF\s*$/', $content)) {
740 // Add EOF marker if missing
741 $content .= "\n%%EOF";
742 $repaired_path = $pdf_path . '.repaired';
743 file_put_contents($repaired_path, $content);
744 return $repaired_path;
745 }
746
747 } catch (Exception $e) {
748 //error_log('PDF repair error: ' . $e->getMessage());
749 }
750
751 return false;
752 }
753
754 /**
755 * NEW: Manual PDF page counting by analyzing PDF structure
756 */
757 private function mxchat_manual_pdf_page_count($pdf_path) {
758 try {
759 $content = file_get_contents($pdf_path);
760 if (!$content) {
761 return 0;
762 }
763
764 // Method 1: Count /Type /Page objects
765 $page_count = preg_match_all('/\/Type\s*\/Page[^s]/', $content);
766 if ($page_count > 0) {
767 return $page_count;
768 }
769
770 // Method 2: Look for /Count in pages object
771 if (preg_match('/\/Type\s*\/Pages.*?\/Count\s+(\d+)/', $content, $matches)) {
772 return intval($matches[1]);
773 }
774
775 // Method 3: Count page references
776 $page_count = preg_match_all('/\d+\s+0\s+obj\s*<<[^>]*\/Type\s*\/Page/', $content);
777 if ($page_count > 0) {
778 return $page_count;
779 }
780
781 } catch (Exception $e) {
782 //error_log('Manual PDF analysis error: ' . $e->getMessage());
783 }
784
785 return 0;
786 }
787
788 public function mxchat_process_pdf_pages_cron($pdf_path, $pdf_url, $total_pages, $batch_size, $batch_pause, $bot_id = 'default') {
789 // ADD THIS DEBUG SECTION AT THE VERY BEGINNING
790 //error_log('[PDF CRON DEBUG] ===== PDF Cron Job Started =====');
791 //error_log('[PDF CRON DEBUG] Initial bot_id parameter: ' . $bot_id);
792 //error_log('[PDF CRON DEBUG] Received parameters:');
793 //error_log('[PDF CRON DEBUG] - pdf_path: ' . $pdf_path);
794 //error_log('[PDF CRON DEBUG] - pdf_url: ' . $pdf_url);
795 //error_log('[PDF CRON DEBUG] - total_pages: ' . $total_pages);
796 //error_log('[PDF CRON DEBUG] - batch_size: ' . $batch_size);
797 //error_log('[PDF CRON DEBUG] - batch_pause: ' . $batch_pause);
798 //error_log('[PDF CRON DEBUG] - Total args received: ' . func_num_args());
799 //error_log('[PDF CRON DEBUG] - All args: ' . print_r(func_get_args(), true));
800
801 // FIXED: Get the correct bot_id from stored status instead of relying on cron parameters
802 $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
803 $status = get_transient($status_key);
804
805 if ($status && isset($status['bot_id'])) {
806 $bot_id = $status['bot_id'];
807 //error_log('[PDF CRON DEBUG] Using bot_id from status: ' . $bot_id);
808 } else {
809 //error_log('[PDF CRON DEBUG] No bot_id in status, using default: ' . $bot_id);
810 }
811
812 // Validate inputs
813 $pdf_path = sanitize_text_field($pdf_path);
814 $pdf_url = esc_url_raw($pdf_url);
815 $total_pages = absint($total_pages);
816 $batch_size = absint($batch_size);
817 $batch_pause = absint($batch_pause);
818 $bot_id = sanitize_key($bot_id);
819
820 try {
821 if (!file_exists($pdf_path)) {
822 throw new Exception(sprintf('PDF file not found at path: %s', esc_html($pdf_path)));
823 }
824
825 // Try to parse PDF with error recovery
826 $pdf = null;
827 $pages = null;
828
829 try {
830 $parser = new \Smalot\PdfParser\Parser();
831 $pdf = $parser->parseFile($pdf_path);
832 $pages = $pdf->getPages();
833 } catch (Exception $e) {
834 //error_log('Primary PDF parsing failed, attempting recovery: ' . $e->getMessage());
835
836 $repaired_path = $this->mxchat_attempt_pdf_repair($pdf_path);
837 if ($repaired_path) {
838 try {
839 $parser = new \Smalot\PdfParser\Parser();
840 $pdf = $parser->parseFile($repaired_path);
841 $pages = $pdf->getPages();
842
843 copy($repaired_path, $pdf_path);
844 unlink($repaired_path);
845 //error_log('PDF successfully repaired and parsed');
846 } catch (Exception $e2) {
847 if (file_exists($repaired_path)) {
848 unlink($repaired_path);
849 }
850 throw new Exception('PDF parsing failed even after repair attempt: ' . $e2->getMessage());
851 }
852 } else {
853 throw new Exception('PDF parsing failed and repair was unsuccessful: ' . $e->getMessage());
854 }
855 }
856
857 if (!$pages || count($pages) === 0) {
858 throw new Exception('No pages found in PDF after parsing');
859 }
860
861 // Get current progress (already fetched above for bot_id)
862 if (!$status || !is_array($status)) {
863 throw new Exception('Invalid status data retrieved from transient');
864 }
865
866 // Initialize failed pages list if it doesn't exist
867 if (!isset($status['failed_pages_list']) || !is_array($status['failed_pages_list'])) {
868 $status['failed_pages_list'] = [];
869 }
870
871 $start_page = absint($status['processed_pages']);
872 $end_page = min($start_page + $batch_size, $total_pages);
873
874 // UPDATED: Get bot-specific options using the correct bot_id
875 $bot_options = $this->get_bot_options($bot_id);
876 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
877
878 //error_log('[PDF CRON DEBUG] Using bot options for bot: ' . $bot_id);
879
880 if (empty($options['api_key'])) {
881 throw new Exception('API key is missing or invalid for bot: ' . $bot_id);
882 }
883
884 $successful_pages = 0;
885 $failed_pages = 0;
886
887 for ($i = $start_page; $i < $end_page; $i++) {
888 $page_number = $i + 1;
889 $max_retries = 3;
890 $retry_count = 0;
891 $page_processed = false;
892 $last_error = '';
893
894 while (!$page_processed && $retry_count < $max_retries) {
895 try {
896 $text = $pages[$i]->getText();
897
898 if (empty($text)) {
899 throw new Exception("Empty text on page {$page_number}");
900 }
901
902 $sanitized_content = $this->mxchat_sanitize_content_for_api($text);
903
904 if (empty($sanitized_content)) {
905 throw new Exception("No valid content after sanitization on page {$page_number}");
906 }
907
908 // UPDATED: Use bot-specific embedding generation with correct bot_id
909 $embedding_vector = $this->mxchat_generate_embedding($sanitized_content, $bot_id);
910
911 if (is_string($embedding_vector)) {
912 throw new Exception("Embedding generation failed: " . $embedding_vector);
913 }
914
915 if (!is_array($embedding_vector)) {
916 throw new Exception("Embedding generation returned unexpected result type: " . gettype($embedding_vector));
917 }
918
919 $metadata = array(
920 'document_type' => 'pdf',
921 'total_pages' => $total_pages,
922 'current_page' => $page_number,
923 'prev_page' => $i > 0 ? $i : null,
924 'next_page' => $i < ($total_pages - 1) ? $i + 2 : null,
925 'source_url' => $pdf_url
926 );
927
928 $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized_content;
929 $page_url = esc_url($pdf_url . "#page=" . $page_number);
930
931 // UPDATED: Pass correct bot_id to database submission
932 $db_result = MxChat_Utils::submit_content_to_db($content_with_metadata, $page_url, $options['api_key'], null, $bot_id);
933
934 if (is_wp_error($db_result)) {
935 throw new Exception("Database submission failed: " . $db_result->get_error_message());
936 }
937
938 // Success!
939 $page_processed = true;
940 $successful_pages++;
941 //error_log('[PDF CRON DEBUG] Successfully processed page ' . $page_number . ' for bot: ' . $bot_id);
942
943 } catch (Exception $e) {
944 $retry_count++;
945 $last_error = $e->getMessage();
946
947 //error_log("PDF page {$page_number} failed (attempt {$retry_count}/{$max_retries}): " . $last_error);
948
949 if ($retry_count < $max_retries) {
950 sleep(pow(2, $retry_count - 1));
951 }
952 }
953 }
954
955 // If page still not processed after all retries, mark as failed
956 if (!$page_processed) {
957 $failed_pages++;
958 $status['failed_pages_list'][] = [
959 'page' => $page_number,
960 'error' => $last_error,
961 'time' => time(),
962 'retries' => $max_retries
963 ];
964
965 if (count($status['failed_pages_list']) > 50) {
966 $status['failed_pages_list'] = array_slice($status['failed_pages_list'], -50);
967 }
968
969 //error_log("PDF page {$page_number} permanently failed after {$max_retries} attempts: " . $last_error);
970 }
971
972 // Update progress
973 $status['processed_pages'] = absint($page_number);
974 $status['last_update'] = time();
975 $status['failed_pages'] = absint($status['failed_pages'] ?? 0) + ($page_processed ? 0 : 1);
976
977 set_transient($status_key, $status, DAY_IN_SECONDS);
978 }
979
980 // Schedule next batch if needed
981 if ($end_page < $total_pages) {
982 // Use the same indexed array format (though bot_id still won't pass correctly)
983 wp_schedule_single_event(time() + $batch_pause, 'mxchat_process_pdf_pages', array(
984 $pdf_path,
985 $pdf_url,
986 $total_pages,
987 $batch_size,
988 $batch_pause,
989 $bot_id // This still won't work, but we're now getting bot_id from status
990 ));
991 } else {
992 // Processing complete
993 $status['status'] = 'complete';
994 $status['processed_pages'] = $total_pages;
995
996 $status['completion_summary'] = [
997 'total_pages' => $total_pages,
998 'successful_pages' => $total_pages - absint($status['failed_pages'] ?? 0),
999 'failed_pages' => absint($status['failed_pages'] ?? 0),
1000 'completion_time' => current_time('mysql')
1001 ];
1002
1003 set_transient($status_key, $status, DAY_IN_SECONDS);
1004
1005 if (file_exists($pdf_path)) {
1006 wp_delete_file($pdf_path);
1007 }
1008
1009 //error_log('[PDF CRON DEBUG] PDF processing completed for bot: ' . $bot_id);
1010 }
1011
1012 } catch (\Exception $e) {
1013 //error_log(sprintf('[MXCHAT-PDF] Error processing PDF for bot %s: %s', $bot_id, $e->getMessage()));
1014
1015 $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
1016 $status = get_transient($status_key);
1017
1018 if (!$status || !is_array($status)) {
1019 $status = array(
1020 'total_pages' => $total_pages,
1021 'processed_pages' => 0,
1022 'status' => 'error',
1023 'error' => sanitize_text_field($e->getMessage()),
1024 'last_update' => time(),
1025 'bot_id' => $bot_id
1026 );
1027 } else {
1028 $status['status'] = 'error';
1029 $status['error'] = sanitize_text_field($e->getMessage());
1030 $status['last_update'] = time();
1031 }
1032
1033 set_transient($status_key, $status, DAY_IN_SECONDS);
1034
1035 if (file_exists($pdf_path)) {
1036 wp_delete_file($pdf_path);
1037 }
1038 }
1039 }
1040
1041 public function mxchat_save_inline_prompt() {
1042 // DEBUG: Log what we're receiving
1043 //error_log('=== MXCHAT DEBUG ===');
1044 //error_log('POST data: ' . print_r($_POST, true));
1045 //error_log('Nonce from POST: ' . ($_POST['_ajax_nonce'] ?? 'NOT FOUND'));
1046
1047 // Check for nonce security
1048 check_ajax_referer('mxchat_save_inline_nonce', '_ajax_nonce');
1049
1050 // If we get here, nonce passed
1051 //error_log('Nonce verification PASSED');
1052
1053 // Verify permissions
1054 if (!current_user_can('manage_options')) {
1055 wp_send_json_error(esc_html__('Permission denied.', 'mxchat'));
1056 return;
1057 }
1058
1059 global $wpdb;
1060 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1061
1062 // Validate and sanitize input data - FIXED LINE BELOW
1063 $prompt_id = isset($_POST['id']) ? absint($_POST['id']) : 0;
1064 $article_content = isset($_POST['article_content']) ? sanitize_textarea_field(wp_unslash($_POST['article_content'])) : '';
1065 $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
1066
1067 if ($prompt_id > 0 && !empty($article_content)) {
1068 // Re-generate the embedding vector for the updated content
1069 $embedding_vector = $this->mxchat_generate_embedding($article_content);
1070 if (is_array($embedding_vector)) {
1071 // Serialize the embedding vector before storing it
1072 $embedding_vector_serialized = serialize($embedding_vector);
1073 // Update the prompt in the database
1074 $updated = $wpdb->update(
1075 $table_name,
1076 array(
1077 'article_content' => $article_content,
1078 'embedding_vector' => $embedding_vector_serialized,
1079 'source_url' => $article_url,
1080 ),
1081 array('id' => $prompt_id),
1082 array('%s', '%s', '%s'),
1083 array('%d')
1084 );
1085 if ($updated !== false) {
1086 wp_send_json_success();
1087 } else {
1088 wp_send_json_error(esc_html__('Database update failed.', 'mxchat'));
1089 }
1090 } else {
1091 wp_send_json_error(esc_html__('Embedding generation failed.', 'mxchat'));
1092 }
1093 } else {
1094 wp_send_json_error(esc_html__('Invalid data.', 'mxchat'));
1095 }
1096 }
1097
1098
1099 public function mxchat_get_pdf_processing_status($pdf_url) {
1100 $pdf_url = esc_url_raw($pdf_url);
1101 $status = get_transient(sanitize_key('mxchat_pdf_status_' . md5($pdf_url)));
1102
1103 if (!$status || !is_array($status)) {
1104 return false;
1105 }
1106
1107 // Check for stalled processing (no updates for 5 minutes)
1108 if ($status['status'] === 'processing' && (time() - absint($status['last_update'])) > 300) {
1109 $status['status'] = 'error';
1110 $status['error'] = __('PDF processing appears to be stalled. No updates for over 5 minutes.', 'mxchat');
1111
1112 // Save the updated status
1113 set_transient(
1114 sanitize_key('mxchat_pdf_status_' . md5($pdf_url)),
1115 array_map('sanitize_text_field', $status),
1116 DAY_IN_SECONDS
1117 );
1118 }
1119
1120 $result = array(
1121 'total_pages' => absint($status['total_pages']),
1122 'processed_pages' => absint($status['processed_pages']),
1123 'failed_pages' => absint($status['failed_pages'] ?? 0),
1124 'percentage' => ($status['total_pages'] > 0)
1125 ? round((absint($status['processed_pages']) / absint($status['total_pages'])) * 100)
1126 : 0,
1127 'status' => sanitize_text_field($status['status']),
1128 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
1129 'failed_pages_list' => isset($status['failed_pages_list']) ? $status['failed_pages_list'] : array(),
1130 'completion_summary' => isset($status['completion_summary']) ? $status['completion_summary'] : null
1131 );
1132
1133 // Add error message if present
1134 if (isset($status['error']) && !empty($status['error'])) {
1135 $result['error'] = sanitize_text_field($status['error']);
1136 }
1137
1138 return $result;
1139 }
1140
1141
1142 public function mxchat_handle_sitemap_submission() {
1143 // START DEBUG
1144 //error_log('[SITEMAP DEBUG] ===== Starting URL submission process =====');
1145 //error_log('[SITEMAP DEBUG] POST data: ' . print_r($_POST, true));
1146
1147 // Get bot_id from form submission EARLY for debugging
1148 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
1149 //error_log('[SITEMAP DEBUG] Extracted bot_id: ' . $bot_id);
1150 //error_log('[SITEMAP DEBUG] Class exists MxChat_Multi_Bot_Manager: ' . (class_exists('MxChat_Multi_Bot_Manager') ? 'YES' : 'NO'));
1151 // END DEBUG
1152
1153 // Check if the form was submitted and verify permissions
1154 if (!isset($_POST['submit_sitemap']) || !current_user_can('manage_options')) {
1155 //error_log('[SITEMAP DEBUG] Error: Unauthorized access or form not submitted properly');
1156 wp_die(esc_html__('Unauthorized access', 'mxchat'));
1157 }
1158
1159 // Verify nonce
1160 //error_log('[SITEMAP DEBUG] Verifying nonce');
1161 check_admin_referer('mxchat_submit_sitemap_action', 'mxchat_submit_sitemap_nonce');
1162
1163 // Validate URL
1164 if (!isset($_POST['sitemap_url']) || empty($_POST['sitemap_url'])) {
1165 //error_log('[SITEMAP DEBUG] Error: Empty or missing URL');
1166 set_transient('mxchat_admin_notice_error',
1167 esc_html__('Please provide a valid URL.', 'mxchat'),
1168 30
1169 );
1170 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1171 exit;
1172 }
1173
1174 $submitted_url = esc_url_raw($_POST['sitemap_url']);
1175
1176 // Continue processing with already extracted bot_id
1177 //error_log('[SITEMAP DEBUG] Processing URL: ' . $submitted_url . ' for bot: ' . $bot_id);
1178
1179 // UPDATED: Get bot-specific options and validate API key
1180 $bot_options = $this->get_bot_options($bot_id);
1181 //error_log('[SITEMAP DEBUG] Bot options retrieved: ' . print_r($bot_options, true));
1182
1183 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
1184 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
1185
1186 //error_log('[SITEMAP DEBUG] Selected embedding model: ' . $selected_model);
1187
1188 if (strpos($selected_model, 'voyage') === 0) {
1189 $api_key = $options['voyage_api_key'] ?? '';
1190 $provider_name = 'Voyage AI';
1191 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
1192 $api_key = $options['gemini_api_key'] ?? '';
1193 $provider_name = 'Google Gemini';
1194 } else {
1195 $api_key = $options['api_key'] ?? '';
1196 $provider_name = 'OpenAI';
1197 }
1198
1199 //error_log('[SITEMAP DEBUG] Provider: ' . $provider_name . ', Has API key: ' . (!empty($api_key) ? 'YES' : 'NO'));
1200
1201 if (empty($api_key)) {
1202 $error_message = sprintf(
1203 esc_html__('%s API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
1204 $provider_name
1205 );
1206 //error_log('[SITEMAP DEBUG] Error: ' . $error_message);
1207 set_transient('mxchat_admin_notice_error', $error_message, 30);
1208 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1209 exit;
1210 }
1211
1212 //error_log('[SITEMAP DEBUG] Fetching URL content');
1213 $response = wp_remote_get($submitted_url, array('timeout' => 30));
1214
1215 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1216 $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response);
1217 //error_log('[SITEMAP DEBUG] Error fetching URL: ' . $error_message);
1218 set_transient('mxchat_admin_notice_error',
1219 sprintf(
1220 esc_html__('Failed to fetch the URL: %s', 'mxchat'),
1221 esc_html($error_message)
1222 ),
1223 30
1224 );
1225 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1226 exit;
1227 }
1228
1229 $content_type = wp_remote_retrieve_header($response, 'content-type');
1230 //error_log('[SITEMAP DEBUG] Content type: ' . $content_type);
1231 $body_content = wp_remote_retrieve_body($response);
1232
1233 if (empty($body_content)) {
1234 //error_log('[SITEMAP DEBUG] Error: Empty response body');
1235 set_transient('mxchat_admin_notice_error',
1236 esc_html__('Empty response received from URL.', 'mxchat'),
1237 30
1238 );
1239 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1240 exit;
1241 }
1242 //error_log('[SITEMAP DEBUG] Retrieved body content length: ' . strlen($body_content) . ' bytes');
1243
1244 // Handle PDF URL
1245 if ($this->mxchat_is_pdf_url($submitted_url, $response)) {
1246 //error_log('[SITEMAP DEBUG] Detected PDF URL, handling PDF for knowledge base');
1247 //error_log('[SITEMAP DEBUG] About to call PDF handler with bot_id: ' . $bot_id);
1248
1249 // UPDATED: Pass bot_id to PDF handler
1250 $result = $this->mxchat_handle_pdf_for_knowledge_base($submitted_url, $response, $bot_id);
1251 //error_log('[SITEMAP DEBUG] PDF handling result: ' . $result);
1252
1253 if ($result === 'scheduled') {
1254 set_transient(
1255 'mxchat_last_pdf_url',
1256 sanitize_text_field($submitted_url),
1257 DAY_IN_SECONDS
1258 );
1259 // UPDATED: Store bot_id for PDF processing
1260 set_transient(
1261 'mxchat_last_pdf_bot_id',
1262 $bot_id,
1263 DAY_IN_SECONDS
1264 );
1265 //error_log('[SITEMAP DEBUG] PDF processing scheduled successfully for bot: ' . $bot_id);
1266 set_transient('mxchat_admin_notice_info',
1267 esc_html__('PDF processing has started in the background. You can check the progress in the Knowledge Base section.', 'mxchat'),
1268 30
1269 );
1270 } else {
1271 //error_log('[SITEMAP DEBUG] PDF processing failed: ' . $result);
1272 set_transient('mxchat_admin_notice_error',
1273 esc_html__('Failed to start PDF processing: ', 'mxchat') . esc_html($result),
1274 30
1275 );
1276 }
1277
1278 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1279 exit;
1280 }
1281
1282 // Handle Sitemap XML
1283 if (strpos($content_type, 'xml') !== false || strpos($body_content, '<urlset') !== false) {
1284 //error_log('[SITEMAP DEBUG] Detected XML content, processing as sitemap');
1285 libxml_use_internal_errors(true);
1286 $xml = simplexml_load_string($body_content);
1287 $xml_errors = libxml_get_errors();
1288 libxml_clear_errors();
1289
1290 if ($xml === false || !empty($xml_errors)) {
1291 //error_log('[SITEMAP DEBUG] Error: Invalid XML format');
1292 if (!empty($xml_errors)) {
1293 foreach ($xml_errors as $error) {
1294 //error_log('[SITEMAP DEBUG] XML Error: ' . $error->message);
1295 }
1296 }
1297
1298 set_transient('mxchat_admin_notice_error',
1299 esc_html__('Invalid sitemap XML. Please provide a valid sitemap.', 'mxchat'),
1300 30
1301 );
1302 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1303 exit;
1304 }
1305
1306 //error_log('[SITEMAP DEBUG] Valid XML found, handling sitemap for knowledge base');
1307 // UPDATED: Pass bot_id to sitemap handler
1308 $result = $this->mxchat_handle_sitemap_for_knowledge_base($xml, $submitted_url, $bot_id);
1309 //error_log('[SITEMAP DEBUG] Sitemap handling result: ' . $result);
1310
1311 if ($result === 'scheduled') {
1312 set_transient(
1313 'mxchat_last_sitemap_url',
1314 sanitize_text_field($submitted_url),
1315 DAY_IN_SECONDS
1316 );
1317 // UPDATED: Store bot_id for sitemap processing
1318 set_transient(
1319 'mxchat_last_sitemap_bot_id',
1320 $bot_id,
1321 DAY_IN_SECONDS
1322 );
1323 set_transient('mxchat_admin_notice_info',
1324 esc_html__('Sitemap processing has started in the background. You can check the progress in the Knowledge Base section.', 'mxchat'),
1325 30
1326 );
1327 } else {
1328 set_transient('mxchat_admin_notice_error',
1329 esc_html__('Failed to start sitemap processing. Please check the status below for details.', 'mxchat'),
1330 30
1331 );
1332 }
1333
1334 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1335 exit;
1336 }
1337
1338 // Handle Regular URL
1339 //error_log('[SITEMAP DEBUG] Processing as regular webpage');
1340 $page_content = $this->mxchat_extract_main_content($body_content);
1341 //error_log('[SITEMAP DEBUG] Extracted content length: ' . strlen($page_content) . ' bytes');
1342
1343 $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
1344 //error_log('[SITEMAP DEBUG] Sanitized content length: ' . strlen($sanitized_content) . ' bytes');
1345
1346 if (empty($sanitized_content)) {
1347 //error_log('[SITEMAP DEBUG] Error: No valid content after sanitization');
1348
1349 set_transient('mxchat_admin_notice_error',
1350 esc_html__('No valid content found on the provided URL.', 'mxchat'),
1351 30
1352 );
1353
1354 set_transient('mxchat_single_url_status', [
1355 'url' => $submitted_url,
1356 'timestamp' => current_time('mysql'),
1357 'status' => 'failed',
1358 'error' => esc_html__('No valid content found on the provided URL.', 'mxchat')
1359 ], DAY_IN_SECONDS);
1360
1361 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1362 exit;
1363 }
1364
1365 //error_log('[SITEMAP DEBUG] Generating embedding for content');
1366 $embedding_vector = $this->mxchat_generate_embedding($sanitized_content, $bot_id);
1367
1368 if (is_string($embedding_vector)) {
1369 //error_log('[SITEMAP DEBUG] Error generating embedding: ' . $embedding_vector);
1370 $error_message = esc_html__('Failed to generate embedding: ', 'mxchat') . esc_html($embedding_vector);
1371
1372 set_transient('mxchat_admin_notice_error', $error_message, 30);
1373
1374 set_transient('mxchat_single_url_status', [
1375 'url' => $submitted_url,
1376 'timestamp' => current_time('mysql'),
1377 'status' => 'failed',
1378 'error' => $error_message
1379 ], DAY_IN_SECONDS);
1380
1381 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1382 exit;
1383 }
1384
1385 if (is_array($embedding_vector)) {
1386 //error_log('[SITEMAP DEBUG] Successfully generated embedding with ' . count($embedding_vector) . ' dimensions');
1387
1388 // UPDATED: Pass bot_id to database submission
1389 $db_result = MxChat_Utils::submit_content_to_db(
1390 $sanitized_content,
1391 $submitted_url,
1392 $api_key,
1393 null,
1394 $bot_id
1395 );
1396
1397 if (is_wp_error($db_result)) {
1398 //error_log('[SITEMAP DEBUG] Error: Failed to store content in database: ' . $db_result->get_error_message());
1399 $error_message = esc_html__('Failed to store content in database: ', 'mxchat') . esc_html($db_result->get_error_message());
1400
1401 set_transient('mxchat_admin_notice_error', $error_message, 30);
1402
1403 set_transient('mxchat_single_url_status', [
1404 'url' => $submitted_url,
1405 'timestamp' => current_time('mysql'),
1406 'status' => 'failed',
1407 'error' => $error_message
1408 ], DAY_IN_SECONDS);
1409
1410 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1411 exit;
1412 }
1413
1414 //error_log('[SITEMAP DEBUG] Successfully stored content in database');
1415 $success_message = esc_html__('URL content successfully submitted!', 'mxchat');
1416
1417 set_transient('mxchat_admin_notice_success', $success_message, 30);
1418
1419 set_transient('mxchat_single_url_status', [
1420 'url' => $submitted_url,
1421 'timestamp' => current_time('mysql'),
1422 'status' => 'complete',
1423 'content_length' => strlen($sanitized_content),
1424 'embedding_dimensions' => count($embedding_vector)
1425 ], DAY_IN_SECONDS);
1426
1427 } else {
1428 //error_log('[SITEMAP DEBUG] Error: Failed to generate embedding. Unexpected result type: ' . gettype($embedding_vector));
1429 $error_message = esc_html__('Failed to generate embedding: Unexpected result type. Please check your API key and try again.', 'mxchat');
1430
1431 set_transient('mxchat_admin_notice_error', $error_message, 30);
1432
1433 set_transient('mxchat_single_url_status', [
1434 'url' => $submitted_url,
1435 'timestamp' => current_time('mysql'),
1436 'status' => 'failed',
1437 'error' => $error_message
1438 ], DAY_IN_SECONDS);
1439 }
1440
1441 //error_log('[SITEMAP DEBUG] ===== Completed URL submission process =====');
1442 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1443 exit;
1444 }
1445
1446 public function mxchat_get_single_url_status() {
1447 $status = get_transient('mxchat_single_url_status');
1448 if (!$status) {
1449 return null;
1450 }
1451
1452 // Add human-readable time
1453 if (isset($status['timestamp'])) {
1454 $status['human_time'] = human_time_diff(strtotime($status['timestamp']), current_time('timestamp')) . ' ' . __('ago', 'mxchat');
1455 }
1456
1457 return $status;
1458 }
1459
1460 public function mxchat_handle_sitemap_for_knowledge_base($xml, $sitemap_url, $bot_id = 'default') {
1461 delete_transient('mxchat_single_url_status');
1462 if (!current_user_can('manage_options')) {
1463 //error_log(esc_html__('Unauthorized sitemap processing attempt', 'mxchat'));
1464 return false;
1465 }
1466
1467 try {
1468 $sitemap_url = esc_url_raw($sitemap_url);
1469
1470 if (!$xml || !is_object($xml)) {
1471 throw new Exception(__('Invalid XML object provided', 'mxchat'));
1472 }
1473
1474 // UPDATED: Get bot-specific embedding API for validation
1475 $bot_options = $this->get_bot_options($bot_id);
1476 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
1477
1478 // ADD THIS: Test the embedding API before processing
1479 $test_phrase = "Test embedding generation for MxChat";
1480 $test_result = $this->mxchat_generate_embedding($test_phrase, $bot_id);
1481
1482 if (is_string($test_result)) {
1483 //error_log('[MXCHAT-URL] Embedding API validation failed: ' . $test_result);
1484
1485 $status_data = array(
1486 'total_urls' => 0,
1487 'processed_urls' => 0,
1488 'status' => 'error',
1489 'error' => __('Embedding API validation failed: ', 'mxchat') . $test_result,
1490 'last_update' => time(),
1491 'bot_id' => $bot_id // ADDED
1492 );
1493
1494 set_transient(
1495 sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)),
1496 array_map('sanitize_text_field', $status_data),
1497 DAY_IN_SECONDS
1498 );
1499
1500 throw new Exception(__('Embedding API validation failed: ', 'mxchat') . $test_result);
1501 }
1502
1503 if (!is_array($test_result)) {
1504 //error_log('[MXCHAT-URL] Embedding API returned unexpected result type: ' . gettype($test_result));
1505 throw new Exception(__('Embedding API returned unexpected result type. Please check your configuration.', 'mxchat'));
1506 }
1507
1508 $urls = [];
1509 foreach ($xml->url as $url_element) {
1510 $url = esc_url_raw((string)$url_element->loc);
1511 if ($url) {
1512 $urls[] = $url;
1513 }
1514 }
1515
1516 $total_urls = absint(count($urls));
1517
1518 if ($total_urls < 1) {
1519 throw new Exception(__('No valid URLs found in sitemap', 'mxchat'));
1520 }
1521
1522 // UPDATED: Pass bot_id to sitemap processing cron
1523 wp_schedule_single_event(time(), 'mxchat_process_sitemap_urls', array(
1524 'urls' => $urls,
1525 'sitemap_url' => $sitemap_url,
1526 'total_urls' => $total_urls,
1527 'batch_size' => absint(10),
1528 'batch_pause' => absint(5),
1529 'bot_id' => $bot_id // ADDED
1530 ));
1531
1532 $status_data = array(
1533 'total_urls' => $total_urls,
1534 'processed_urls' => 0,
1535 'status' => 'processing',
1536 'last_update' => time(),
1537 'bot_id' => $bot_id // ADDED
1538 );
1539
1540 set_transient(
1541 sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)),
1542 array_map('sanitize_text_field', $status_data),
1543 DAY_IN_SECONDS
1544 );
1545
1546 return __('scheduled', 'mxchat');
1547
1548 } catch (\Exception $e) {
1549 $error_message = $e->getMessage();
1550 //error_log(sprintf(esc_html__('Error preparing sitemap for processing: %s', 'mxchat'), esc_html($error_message)));
1551
1552 set_transient(
1553 'mxchat_last_sitemap_url',
1554 sanitize_text_field($sitemap_url),
1555 DAY_IN_SECONDS
1556 );
1557
1558 $status_data = array(
1559 'total_urls' => 0,
1560 'processed_urls' => 0,
1561 'status' => 'error',
1562 'error' => $error_message,
1563 'last_update' => time(),
1564 'bot_id' => $bot_id // ADDED
1565 );
1566
1567 set_transient(
1568 sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)),
1569 array_map('sanitize_text_field', $status_data),
1570 DAY_IN_SECONDS
1571 );
1572
1573 return $error_message;
1574 }
1575 }
1576 public function mxchat_process_sitemap_urls_cron($urls, $sitemap_url, $total_urls, $batch_size, $batch_pause, $bot_id = 'default') {
1577 // Validate inputs
1578 $sitemap_url = esc_url_raw($sitemap_url);
1579 $total_urls = absint($total_urls);
1580 $batch_size = absint($batch_size);
1581 $batch_pause = absint($batch_pause);
1582 $bot_id = sanitize_key($bot_id);
1583
1584 if (!is_array($urls) || empty($urls)) {
1585 return;
1586 }
1587
1588 try {
1589 $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
1590 $status = get_transient($status_key);
1591
1592 if (!$status || !is_array($status)) {
1593 throw new Exception('Invalid status data retrieved from transient');
1594 }
1595
1596 // Initialize failed_urls array if it doesn't exist
1597 if (!isset($status['failed_urls_list']) || !is_array($status['failed_urls_list'])) {
1598 $status['failed_urls_list'] = [];
1599 }
1600
1601 $start_url = absint($status['processed_urls']);
1602 $end_url = min($start_url + $batch_size, $total_urls);
1603
1604 // UPDATED: Get bot-specific options
1605 $bot_options = $this->get_bot_options($bot_id);
1606 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
1607
1608 // Track batch statistics
1609 $batch_stats = [
1610 'processed' => 0,
1611 'failed' => 0,
1612 'last_error' => '',
1613 'embedding_errors' => 0,
1614 'network_errors' => 0,
1615 'timeout_errors' => 0
1616 ];
1617
1618 @set_time_limit(300);
1619
1620 for ($i = $start_url; $i < $end_url; $i++) {
1621 $page_url = esc_url_raw($urls[$i]);
1622 $max_retries = 5;
1623 $retry_count = 0;
1624 $url_processed = false;
1625 $last_error = '';
1626
1627 if (memory_get_usage(true) > (1024 * 1024 * 100)) {
1628 //error_log('MxChat: Memory usage high, taking break');
1629 sleep(2);
1630 }
1631
1632 while (!$url_processed && $retry_count < $max_retries) {
1633 try {
1634 $timeout = 30 + ($retry_count * 10);
1635
1636 $page_response = wp_remote_get($page_url, array(
1637 'timeout' => $timeout,
1638 'redirection' => 5,
1639 'user-agent' => 'MxChat/1.0'
1640 ));
1641
1642 if (is_wp_error($page_response)) {
1643 $error_msg = $page_response->get_error_message();
1644
1645 if (strpos($error_msg, 'timeout') !== false) {
1646 $batch_stats['timeout_errors']++;
1647 } else {
1648 $batch_stats['network_errors']++;
1649 }
1650
1651 throw new Exception('HTTP request failed: ' . $error_msg);
1652 }
1653
1654 $response_code = wp_remote_retrieve_response_code($page_response);
1655
1656 if (!in_array($response_code, [200, 201, 202])) {
1657 if ($response_code >= 400 && $response_code < 500) {
1658 throw new Exception('HTTP Status: ' . $response_code . ' (permanent failure)');
1659 }
1660 throw new Exception('HTTP Status: ' . $response_code);
1661 }
1662
1663 $page_html = wp_remote_retrieve_body($page_response);
1664
1665 if (empty($page_html)) {
1666 throw new Exception('Empty response body');
1667 }
1668
1669 $page_content = $this->mxchat_extract_main_content($page_html);
1670 $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
1671
1672 if (empty($sanitized_content)) {
1673 //error_log("MxChat: No content found for URL: {$page_url}");
1674 $url_processed = true;
1675 $batch_stats['processed']++;
1676 break;
1677 }
1678
1679 // UPDATED: Use bot-specific embedding generation
1680 $embedding_vector = $this->mxchat_generate_embedding($sanitized_content, $bot_id);
1681
1682 if (is_string($embedding_vector)) {
1683 $batch_stats['embedding_errors']++;
1684
1685 if (strpos($embedding_vector, 'rate limit') !== false ||
1686 strpos($embedding_vector, 'quota') !== false) {
1687 sleep(30 + ($retry_count * 10));
1688 }
1689
1690 throw new Exception('Embedding generation failed: ' . $embedding_vector);
1691 }
1692
1693 if (!is_array($embedding_vector)) {
1694 throw new Exception('Embedding generation returned unexpected result type: ' . gettype($embedding_vector));
1695 }
1696
1697 // UPDATED: Pass bot_id to database submission
1698 $submission_result = MxChat_Utils::submit_content_to_db($sanitized_content, $page_url, $options['api_key'], null, $bot_id);
1699
1700 if (is_wp_error($submission_result)) {
1701 throw new Exception('Database submission failed: ' . $submission_result->get_error_message());
1702 }
1703
1704 // Success!
1705 $url_processed = true;
1706 $batch_stats['processed']++;
1707
1708 } catch (Exception $e) {
1709 $retry_count++;
1710 $last_error = $e->getMessage();
1711
1712 if (strpos($last_error, 'rate limit') !== false) {
1713 sleep(60);
1714 } elseif (strpos($last_error, 'timeout') !== false) {
1715 sleep(10);
1716 } elseif (strpos($last_error, 'permanent failure') !== false) {
1717 break;
1718 } else {
1719 sleep(pow(2, $retry_count - 1));
1720 }
1721 }
1722 }
1723
1724 // If URL still not processed after all retries, mark as failed
1725 if (!$url_processed) {
1726 $batch_stats['failed']++;
1727 $batch_stats['last_error'] = $last_error;
1728
1729 $status['failed_urls_list'][] = [
1730 'url' => $page_url,
1731 'error' => $last_error,
1732 'time' => time(),
1733 'retries' => $max_retries
1734 ];
1735
1736 if (count($status['failed_urls_list']) > 100) {
1737 $status['failed_urls_list'] = array_slice($status['failed_urls_list'], -100);
1738 }
1739 }
1740
1741 // Update progress after each URL
1742 $status['processed_urls'] = absint($i + 1);
1743 $status['last_update'] = time();
1744 $status['failed_urls'] = absint($status['failed_urls'] ?? 0) + ($url_processed ? 0 : 1);
1745 $status['last_error'] = $batch_stats['last_error'];
1746
1747 set_transient($status_key, $status, DAY_IN_SECONDS);
1748 }
1749
1750 $failure_rate = $batch_stats['failed'] / max(1, $batch_stats['processed'] + $batch_stats['failed']);
1751
1752 if ($batch_stats['processed'] === 0 && $batch_stats['failed'] >= 5) {
1753 $status['status'] = 'error';
1754 $status['error'] = sprintf(
1755 'Processing stopped after %d consecutive failures. Last error: %s',
1756 $batch_stats['failed'],
1757 $batch_stats['last_error']
1758 );
1759 set_transient($status_key, $status, DAY_IN_SECONDS);
1760 return;
1761 }
1762
1763 // Update final progress
1764 $status['processed_urls'] = min($end_url, $total_urls);
1765 $status['last_update'] = time();
1766 $status['batch_stats'] = $batch_stats;
1767 set_transient($status_key, $status, DAY_IN_SECONDS);
1768
1769 // Check if we've processed all URLs
1770 if ($end_url >= $total_urls) {
1771 // All URLs have been processed - mark as complete
1772 $status['status'] = 'complete';
1773 $status['processed_urls'] = $total_urls;
1774
1775 $status['completion_summary'] = [
1776 'total_urls' => $total_urls,
1777 'successful_urls' => $total_urls - absint($status['failed_urls'] ?? 0),
1778 'failed_urls' => absint($status['failed_urls'] ?? 0),
1779 'completion_time' => current_time('mysql'),
1780 'final_batch_stats' => $batch_stats
1781 ];
1782
1783 set_transient($status_key, $status, DAY_IN_SECONDS);
1784 } else {
1785 $dynamic_pause = $batch_pause;
1786
1787 if ($failure_rate > 0.5) {
1788 $dynamic_pause *= 3;
1789 } elseif ($batch_stats['embedding_errors'] > 3) {
1790 $dynamic_pause *= 2;
1791 }
1792
1793 // UPDATED: Pass bot_id to next batch
1794 wp_schedule_single_event(time() + $dynamic_pause, 'mxchat_process_sitemap_urls', array(
1795 'urls' => $urls,
1796 'sitemap_url' => $sitemap_url,
1797 'total_urls' => $total_urls,
1798 'batch_size' => $batch_size,
1799 'batch_pause' => $batch_pause,
1800 'bot_id' => $bot_id // ADDED
1801 ));
1802 }
1803 } catch (\Exception $e) {
1804 $status['last_error'] = $e->getMessage();
1805 $status['error_count'] = ($status['error_count'] ?? 0) + 1;
1806
1807 if ($status['error_count'] >= 5) {
1808 $status['status'] = 'error';
1809 $status['error'] = 'Too many batch failures: ' . $e->getMessage();
1810 } else {
1811 // UPDATED: Pass bot_id to retry batch
1812 wp_schedule_single_event(time() + 300, 'mxchat_process_sitemap_urls', array(
1813 'urls' => $urls,
1814 'sitemap_url' => $sitemap_url,
1815 'total_urls' => $total_urls,
1816 'batch_size' => max(5, $batch_size / 2),
1817 'batch_pause' => $batch_pause * 2,
1818 'bot_id' => $bot_id // ADDED
1819 ));
1820 }
1821
1822 set_transient($status_key, $status, DAY_IN_SECONDS);
1823 }
1824 }
1825
1826
1827 public function mxchat_sanitize_content_for_api($content) {
1828 //error_log('[MXCHAT-SANITIZE] Original content preview: ' . substr($content, 0, 500) . '...');
1829
1830 // Remove script, style tags, and HTML comments
1831 $content = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $content);
1832 $content = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $content);
1833 $content = preg_replace('/<!--(.|\s)*?-->/', '', $content);
1834
1835 // Remove all HTML tags and decode HTML entities
1836 $content = wp_strip_all_tags($content);
1837 $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5);
1838
1839 // Normalize whitespace but preserve paragraph breaks
1840 // First, normalize line endings to \n
1841 $content = str_replace(["\r\n", "\r"], "\n", $content);
1842 // Replace multiple spaces/tabs with single space, but preserve newlines
1843 $content = preg_replace('/[ \t]+/', ' ', $content);
1844 // Replace 3+ newlines with 2 newlines (max 2 blank lines)
1845 $content = preg_replace('/\n{3,}/', "\n\n", $content);
1846 // Trim each line
1847 $lines = explode("\n", $content);
1848 $lines = array_map('trim', $lines);
1849 $content = implode("\n", $lines);
1850 // Final trim
1851 $content = trim($content);
1852
1853 // Remove control characters (which can cause database issues) but preserve \n (0x0A) and \r (0x0D)
1854 $content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $content);
1855
1856 // Remove NULL bytes which can cause database errors
1857 $content = str_replace("\0", "", $content);
1858
1859 // Ensure valid UTF-8 encoding
1860 $content = wp_check_invalid_utf8($content);
1861
1862 // Remove any extremely long strings without spaces (often garbage)
1863 $content = preg_replace('/\S{300,}/', ' ', $content);
1864
1865 // Replace problematic characters that often cause database issues
1866 $content = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $content); // Remove emoji and other high Unicode characters
1867
1868 // Replace any remaining potentially problematic characters with spaces
1869 // BUT preserve newlines by temporarily replacing them
1870 $content = str_replace("\n", "NEWLINE_PLACEHOLDER", $content);
1871 $content = preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\p{Sm}]/u', ' ', $content);
1872 $content = str_replace("NEWLINE_PLACEHOLDER", "\n", $content);
1873
1874 // Limit to reasonable length if needed
1875 $max_length = 65000; // Just under MySQL TEXT field limit
1876 if (strlen($content) > $max_length) {
1877 $content = substr($content, 0, $max_length);
1878 }
1879
1880 //error_log('[MXCHAT-SANITIZE] Sanitized content preview: ' . substr($content, 0, 500) . '...');
1881 return $content;
1882 }
1883 public function mxchat_extract_main_content($html) {
1884 if (empty($html)) {
1885 return '';
1886 }
1887 try {
1888 $dom = new DOMDocument;
1889 libxml_use_internal_errors(true); // Suppress HTML parsing errors
1890 @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
1891 $xpath = new DOMXPath($dom);
1892
1893 // For debugging purposes
1894 $debugEnabled = false; // Set to true to enable debugging output
1895 $debug = function($message) use ($debugEnabled) {
1896 if ($debugEnabled) {
1897 //error_log('[MXCHAT-DEBUG] ' . $message);
1898 }
1899 };
1900
1901 // Direct targeting for Gerow theme posts
1902 $post_text = $xpath->query('//div[contains(@class, "post-text")]');
1903 if ($post_text && $post_text->length > 0) {
1904 $debug("Found post-text directly");
1905 $content = '';
1906 foreach ($post_text as $node) {
1907 $content .= $dom->saveHTML($node);
1908 }
1909 if (!empty($content)) {
1910 $debug("Returning post-text content");
1911 return $content;
1912 }
1913 }
1914
1915 // Try to get the blog details content which contains the post-text
1916 $blog_details = $xpath->query('//div[contains(@class, "blog-details-content")]');
1917 if ($blog_details && $blog_details->length > 0) {
1918 $debug("Found blog-details-content");
1919 $content = '';
1920 foreach ($blog_details as $node) {
1921 $content .= $dom->saveHTML($node);
1922 }
1923 if (!empty($content)) {
1924 $debug("Returning blog-details-content");
1925 return $content;
1926 }
1927 }
1928
1929 // Try to get the article which contains the blog details
1930 $article = $xpath->query('//article[contains(@class, "blog-details-wrap")]');
1931 if ($article && $article->length > 0) {
1932 $debug("Found article with blog-details-wrap");
1933 $content = '';
1934 foreach ($article as $node) {
1935 $content .= $dom->saveHTML($node);
1936 }
1937 if (!empty($content)) {
1938 $debug("Returning article content");
1939 return $content;
1940 }
1941 }
1942
1943 // Try even broader with the blog-item-wrap
1944 $blog_item = $xpath->query('//div[contains(@class, "blog-item-wrap")]');
1945 if ($blog_item && $blog_item->length > 0) {
1946 $debug("Found blog-item-wrap");
1947 $content = '';
1948 foreach ($blog_item as $node) {
1949 $content .= $dom->saveHTML($node);
1950 }
1951 if (!empty($content)) {
1952 $debug("Returning blog-item-wrap content");
1953 return $content;
1954 }
1955 }
1956
1957 // Specific Gerow theme path
1958 $gerow_path = $xpath->query('//section[contains(@class, "blog-area")]//div[contains(@class, "post-text")]');
1959 if ($gerow_path && $gerow_path->length > 0) {
1960 $debug("Found Gerow theme path to post-text");
1961 $content = '';
1962 foreach ($gerow_path as $node) {
1963 $content .= $dom->saveHTML($node);
1964 }
1965 if (!empty($content)) {
1966 $debug("Returning Gerow post-text content");
1967 return $content;
1968 }
1969 }
1970
1971 // Generic blog post selectors
1972 $selectors = [
1973 // Blog post specific selectors
1974 '//div[contains(@class, "post-text")]',
1975 '//article[contains(@class, "blog-post-item")]//div[contains(@class, "post-text")]',
1976 '//div[contains(@class, "blog-details-content")]',
1977 '//article[contains(@class, "blog-details-wrap")]',
1978 '//div[contains(@class, "entry-content")]',
1979 '//div[contains(@class, "blog-content")]',
1980 '//div[contains(@class, "blog-item-wrap")]',
1981
1982 // More general content selectors
1983 '//div[contains(@class, "page__content")]',
1984 '//div[contains(@class, "elementor-widget-container")]',
1985 '//div[contains(@class, "elementor-text-editor")]',
1986 '//div[contains(@class, "elementor-widget-text-editor")]',
1987 '//*[contains(@class, "entry-content")]',
1988 '//*[contains(@class, "post-content")]',
1989 '//*[contains(@class, "article-content")]',
1990 '//*[@id="content"]',
1991 '//*[@id="main-content"]',
1992 '//section[contains(@class, "blog-area")]',
1993 '//article',
1994 '//main',
1995 '//div[contains(@class, "content")]'
1996 ];
1997
1998 // First handle Elementor content
1999 $debug("Checking for Elementor content");
2000 $elementor_widgets = $xpath->query('//div[contains(@class, "elementor-element")]//div[contains(@class, "elementor-widget-container")]');
2001 if ($elementor_widgets && $elementor_widgets->length > 0) {
2002 $debug("Found Elementor widgets");
2003 $combined_content = '';
2004 foreach ($elementor_widgets as $widget) {
2005 $widget_content = $dom->saveHTML($widget);
2006 if (!empty($widget_content)) {
2007 $combined_content .= $widget_content;
2008 }
2009 }
2010 if (!empty($combined_content)) {
2011 $debug("Returning Elementor content");
2012 return $combined_content;
2013 }
2014 }
2015
2016 // Try standard selectors one by one
2017 foreach ($selectors as $selector) {
2018 $debug("Trying selector: " . $selector);
2019 $nodes = $xpath->query($selector);
2020 if ($nodes && $nodes->length > 0) {
2021 $debug("Found matches for selector: " . $selector);
2022 $content = '';
2023 foreach ($nodes as $node) {
2024 $content .= $dom->saveHTML($node);
2025 }
2026 if (!empty($content)) {
2027 $debug("Returning content from selector: " . $selector);
2028 return $content;
2029 }
2030 }
2031 }
2032
2033 // Manual regex fallback for post-text if DOM methods fail
2034 $debug("Trying regex fallback");
2035 if (preg_match('/<div class="post-text">(.*?)<\/div>\s*<\/div>\s*<\/div>/s', $html, $matches)) {
2036 $debug("Found post-text via regex");
2037 return '<div class="post-text">' . $matches[1] . '</div>';
2038 }
2039
2040 // Try to extract the blog section as a whole
2041 $blog_section = $xpath->query('//section[contains(@class, "blog-area")]');
2042 if ($blog_section && $blog_section->length > 0) {
2043 $debug("Found blog-area section");
2044 $content = '';
2045 foreach ($blog_section as $node) {
2046 $content .= $dom->saveHTML($node);
2047 }
2048 if (!empty($content)) {
2049 $debug("Returning blog-area section content");
2050 return $content;
2051 }
2052 }
2053
2054 // Fallback: Return the body content if no specific selector matches
2055 $debug("Using body fallback");
2056 $body = $dom->getElementsByTagName('body');
2057 if ($body->length > 0) {
2058 return $dom->saveHTML($body->item(0));
2059 }
2060
2061 // Last resort: return the original HTML
2062 $debug("Returning original HTML");
2063 return $html;
2064 } catch (Exception $e) {
2065 //error_log('[MXCHAT-ERROR] Content extraction failed: ' . $e->getMessage());
2066 return $html; // Return original HTML if parsing fails
2067 } finally {
2068 libxml_clear_errors();
2069 }
2070 }
2071 public function mxchat_get_sitemap_processing_status($sitemap_url) {
2072 $sitemap_url = esc_url_raw($sitemap_url);
2073 $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
2074 $status = get_transient($status_key);
2075
2076 if (!$status || !is_array($status)) {
2077 return false;
2078 }
2079
2080 // Auto-complete check: if all URLs are processed but status isn't complete
2081 if (isset($status['processed_urls']) && isset($status['total_urls']) &&
2082 $status['processed_urls'] >= $status['total_urls'] &&
2083 isset($status['status']) && $status['status'] !== 'complete' &&
2084 $status['status'] !== 'error') {
2085
2086 // Mark as complete
2087 $status['status'] = 'complete';
2088 $status['processed_urls'] = $status['total_urls']; // Ensure exact match
2089
2090 // Update the transient with the corrected status
2091 set_transient($status_key, $status, DAY_IN_SECONDS);
2092 }
2093
2094 return array(
2095 'total_urls' => absint($status['total_urls']),
2096 'processed_urls' => absint($status['processed_urls']),
2097 'failed_urls' => absint($status['failed_urls'] ?? 0),
2098 'percentage' => ($status['total_urls'] > 0)
2099 ? round((absint($status['processed_urls']) / absint($status['total_urls'])) * 100)
2100 : 0,
2101 'status' => sanitize_text_field($status['status']),
2102 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
2103 'error' => isset($status['error']) ? sanitize_text_field($status['error']) : '',
2104 'last_error' => isset($status['last_error']) ? sanitize_text_field($status['last_error']) : '',
2105 'failed_urls_list' => isset($status['failed_urls_list']) ? $status['failed_urls_list'] : array()
2106 );
2107 }
2108
2109 public function mxchat_ajax_get_status_updates() {
2110 try {
2111 // Verify the request
2112 check_ajax_referer('mxchat_status_nonce', 'nonce');
2113
2114 // Get the status just like in your admin page
2115 $pdf_url = get_transient('mxchat_last_pdf_url');
2116 $sitemap_url = get_transient('mxchat_last_sitemap_url');
2117
2118 $pdf_status = $pdf_url ? $this->mxchat_get_pdf_processing_status($pdf_url) : false;
2119 $sitemap_status = $sitemap_url ? $this->mxchat_get_sitemap_processing_status($sitemap_url) : false;
2120
2121 // Add the PDF URL to the status object
2122 if ($pdf_status && $pdf_url) {
2123 $pdf_status['pdf_url'] = $pdf_url;
2124 }
2125
2126 // Set the current PDF URL for the manual batch processing button
2127 $current_pdf_url = $pdf_url;
2128
2129 // Check for true processing status, not just presence of status
2130 $is_active_processing =
2131 ($sitemap_status && isset($sitemap_status['status']) && $sitemap_status['status'] === 'processing') ||
2132 ($pdf_status && isset($pdf_status['status']) && $pdf_status['status'] === 'processing');
2133
2134 // Get single URL status, but only if no processing is active
2135 $single_url_status = !$is_active_processing ? $this->mxchat_get_single_url_status() : false;
2136
2137 // REMOVED: Auto-clearing of completed status - now only done via dismiss button
2138
2139 // Return JSON response with the status data
2140 wp_send_json(array(
2141 'pdf_status' => $pdf_status,
2142 'sitemap_status' => $sitemap_status,
2143 'single_url_status' => $single_url_status,
2144 'is_processing' => $is_active_processing,
2145 'current_pdf_url' => $current_pdf_url
2146 ));
2147
2148 } catch (Exception $e) {
2149 // Log the error
2150 //error_log('MxChat Status Update Error: ' . $e->getMessage());
2151
2152 // Return a friendly error response
2153 wp_send_json_error(array(
2154 'message' => 'Error getting status updates: ' . $e->getMessage(),
2155 'status' => 'error'
2156 ));
2157 }
2158 }
2159 public function mxchat_stop_processing() {
2160 // Verify permissions
2161 if (!current_user_can('manage_options')) {
2162 wp_die(esc_html__('Unauthorized access', 'mxchat'));
2163 }
2164
2165 // Verify nonce
2166 check_admin_referer('mxchat_stop_processing_action', 'mxchat_stop_processing_nonce');
2167
2168 // Get the last sitemap URL and clear its transient
2169 $sitemap_url = get_transient('mxchat_last_sitemap_url');
2170 if ($sitemap_url) {
2171 delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
2172 delete_transient('mxchat_last_sitemap_url');
2173 }
2174
2175 // Get the last PDF URL and clear its transient
2176 $pdf_url = get_transient('mxchat_last_pdf_url');
2177 if ($pdf_url) {
2178 delete_transient('mxchat_pdf_status_' . md5($pdf_url));
2179 delete_transient('mxchat_last_pdf_url');
2180 }
2181
2182 // Unschedule any pending sitemap events
2183 $timestamp = wp_next_scheduled('mxchat_process_sitemap_urls');
2184 if ($timestamp) {
2185 wp_unschedule_event($timestamp, 'mxchat_process_sitemap_urls');
2186 }
2187
2188 // Redirect back with a success message
2189 set_transient('mxchat_admin_notice_success',
2190 esc_html__('Processing has been stopped successfully.', 'mxchat'),
2191 30
2192 );
2193 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
2194 exit;
2195 }
2196
2197
2198 /**
2199 * Get content list for processing
2200 */
2201 public function ajax_mxchat_get_content_list() {
2202 // Verify the nonce
2203 check_ajax_referer('mxchat_content_selector_nonce', 'nonce');
2204
2205 if (!current_user_can('manage_options')) {
2206 wp_send_json_error(__('Unauthorized access', 'mxchat'));
2207 }
2208
2209 $page = isset($_GET['page']) ? absint($_GET['page']) : 1;
2210 $per_page = isset($_GET['per_page']) ? absint($_GET['per_page']) : 50;
2211 $search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
2212 $post_type = isset($_GET['post_type']) ? sanitize_text_field($_GET['post_type']) : 'all';
2213 $post_status = isset($_GET['post_status']) ? sanitize_text_field($_GET['post_status']) : 'publish';
2214 $processed_filter = isset($_GET['processed_filter']) ? sanitize_text_field($_GET['processed_filter']) : 'all';
2215
2216 // Build query args
2217 $args = array(
2218 'posts_per_page' => $per_page,
2219 'paged' => $page,
2220 'post_status' => $post_status !== 'all' ? $post_status : array('publish', 'draft', 'pending'),
2221 'orderby' => 'date',
2222 'order' => 'DESC',
2223 );
2224
2225 // Handle post types - IMPROVED VERSION
2226 if ($post_type !== 'all') {
2227 $args['post_type'] = $post_type;
2228 } else {
2229 // Get all available post types that might contain content
2230 $all_post_types = array();
2231
2232 // First get all public post types
2233 $public_types = get_post_types(array('public' => true), 'names');
2234 $all_post_types = array_merge($all_post_types, $public_types);
2235
2236 // Add common forum/community post types
2237 $forum_types = array('topic', 'reply', 'forum', 'wpforo_topic', 'wpforo_post');
2238 foreach ($forum_types as $forum_type) {
2239 if (post_type_exists($forum_type)) {
2240 $all_post_types[] = $forum_type;
2241 }
2242 }
2243
2244 // Add other commonly used post types
2245 $common_types = array('product', 'job_listing', 'event', 'portfolio');
2246 foreach ($common_types as $common_type) {
2247 if (post_type_exists($common_type)) {
2248 $all_post_types[] = $common_type;
2249 }
2250 }
2251
2252 // Remove duplicates and ensure we have at least some post types
2253 $all_post_types = array_unique($all_post_types);
2254
2255 if (empty($all_post_types)) {
2256 // Fallback to basic post types
2257 $all_post_types = array('post', 'page');
2258 }
2259
2260 $args['post_type'] = $all_post_types;
2261
2262 // Debug logging to see what post types are being queried
2263 //error_log('MxChat Debug: Querying post types: ' . implode(', ', $all_post_types));
2264 }
2265
2266 if (!empty($search)) {
2267 $args['s'] = $search;
2268 }
2269
2270 // Get processed data from storage
2271 $processed_data = array();
2272
2273 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
2274 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2275
2276 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
2277 // Get fresh data from Pinecone - no caching
2278 $processed_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
2279 } else {
2280 // WordPress DB checking with better URL matching for all post types
2281 global $wpdb;
2282 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2283 $processed_items = $wpdb->get_results("SELECT id, source_url, timestamp FROM {$table_name}");
2284
2285 if (!empty($processed_items)) {
2286 foreach ($processed_items as $item) {
2287 // Use improved URL matching that works for all post types
2288 $post_id = $this->mxchat_url_to_post_id_improved($item->source_url);
2289
2290 if ($post_id) {
2291 $processed_data[$post_id] = array(
2292 'db_id' => $item->id,
2293 'timestamp' => $item->timestamp,
2294 'url' => $item->source_url,
2295 'source' => 'wordpress'
2296 );
2297 }
2298 }
2299 }
2300 }
2301
2302 // Get processed IDs as a simple array for in_array checks
2303 $processed_ids = array_keys($processed_data);
2304
2305 // Handle processed/unprocessed filter
2306 if ($processed_filter === 'processed' && !empty($processed_ids)) {
2307 $args['post__in'] = $processed_ids;
2308 } elseif ($processed_filter === 'unprocessed' && !empty($processed_ids)) {
2309 $args['post__not_in'] = $processed_ids;
2310 }
2311
2312 // Run the query
2313 $query = new WP_Query($args);
2314 $content_items = array();
2315
2316 if ($query->have_posts()) {
2317 while ($query->have_posts()) {
2318 $query->the_post();
2319 $id = get_the_ID();
2320 $post_date = get_the_date();
2321 $excerpt = wp_trim_words(get_the_excerpt(), 20, '...');
2322 $word_count = str_word_count(strip_tags(get_the_content()));
2323
2324 $is_processed = in_array($id, $processed_ids);
2325 $processed_date = '';
2326 $db_record_id = 0;
2327 $data_source = 'none';
2328
2329 if ($is_processed && isset($processed_data[$id])) {
2330 $item_data = $processed_data[$id];
2331 $data_source = $item_data['source'];
2332
2333 if ($data_source === 'wordpress' && isset($item_data['timestamp'])) {
2334 // WordPress DB format
2335 $timestamp = strtotime($item_data['timestamp']);
2336 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2337 $db_record_id = $item_data['db_id'];
2338 } elseif ($data_source === 'pinecone') {
2339 // Pinecone format
2340 $processed_date = $item_data['processed_date'];
2341 $db_record_id = $item_data['db_id'];
2342 }
2343 }
2344
2345 $content_items[] = array(
2346 'id' => $id,
2347 'title' => get_the_title(),
2348 'permalink' => get_permalink(),
2349 'date' => $post_date,
2350 'type' => get_post_type(),
2351 'status' => get_post_status(),
2352 'excerpt' => $excerpt,
2353 'word_count' => $word_count,
2354 'already_processed' => $is_processed,
2355 'processed_date' => $processed_date,
2356 'db_record_id' => $db_record_id,
2357 'data_source' => $data_source
2358 );
2359 }
2360 wp_reset_postdata();
2361 }
2362
2363 $response = array(
2364 'items' => $content_items,
2365 'total' => $query->found_posts,
2366 'total_pages' => $query->max_num_pages,
2367 'current_page' => $page,
2368 'processed_count' => count($processed_ids)
2369 );
2370
2371 wp_send_json_success($response);
2372 exit;
2373 }
2374
2375
2376 /**
2377 * This function handles various WooCommerce URL formats and permalink structures
2378 */
2379 private function mxchat_url_to_post_id_improved($url) {
2380 // First try the standard WordPress function
2381 $post_id = url_to_postid($url);
2382
2383 if ($post_id > 0) {
2384 return $post_id;
2385 }
2386
2387 // If that fails, try more aggressive URL matching
2388 // Remove trailing slashes and query parameters for better matching
2389 $clean_url = rtrim($url, '/');
2390 $clean_url = strtok($clean_url, '?'); // Remove query parameters
2391
2392 // Try again with cleaned URL
2393 $post_id = url_to_postid($clean_url);
2394 if ($post_id > 0) {
2395 return $post_id;
2396 }
2397
2398 // For bbPress forum topics, try extracting slug from URL
2399 if (strpos($url, '/topic/') !== false || strpos($url, '/forums/') !== false) {
2400 // Handle bbPress URLs: /forums/topic/topic-name/
2401 if (preg_match('/\/forums\/topic\/([^\/\?]+)/', $url, $matches)) {
2402 $topic_slug = $matches[1];
2403
2404 // Look up topic by slug
2405 $topic = get_page_by_path($topic_slug, OBJECT, 'topic');
2406 if ($topic) {
2407 return $topic->ID;
2408 }
2409
2410 // Alternative method: query by post_name
2411 global $wpdb;
2412 $post_id = $wpdb->get_var($wpdb->prepare(
2413 "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
2414 $topic_slug
2415 ));
2416
2417 if ($post_id) {
2418 return intval($post_id);
2419 }
2420 }
2421
2422 // Handle simpler topic URLs: /topic/topic-name/
2423 if (preg_match('/\/topic\/([^\/\?]+)/', $url, $matches)) {
2424 $topic_slug = $matches[1];
2425
2426 global $wpdb;
2427 $post_id = $wpdb->get_var($wpdb->prepare(
2428 "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
2429 $topic_slug
2430 ));
2431
2432 if ($post_id) {
2433 return intval($post_id);
2434 }
2435 }
2436 }
2437
2438 // For WooCommerce products
2439 if (strpos($url, '/product/') !== false || strpos($url, 'product=') !== false) {
2440 // Extract product slug from various URL formats
2441 $product_slug = '';
2442
2443 // Handle pretty permalinks: /product/product-name/
2444 if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
2445 $product_slug = $matches[1];
2446 }
2447 // Handle query parameters: ?product=product-name
2448 elseif (preg_match('/[\?&]product=([^&]+)/', $url, $matches)) {
2449 $product_slug = $matches[1];
2450 }
2451
2452 if (!empty($product_slug)) {
2453 // Look up product by slug
2454 $product = get_page_by_path($product_slug, OBJECT, 'product');
2455 if ($product) {
2456 return $product->ID;
2457 }
2458
2459 // Alternative method: query by post_name
2460 global $wpdb;
2461 $post_id = $wpdb->get_var($wpdb->prepare(
2462 "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'product' AND post_status = 'publish'",
2463 $product_slug
2464 ));
2465
2466 if ($post_id) {
2467 return intval($post_id);
2468 }
2469 }
2470 }
2471
2472 // Generic approach: try to extract slug and match against all post types
2473 $parsed_url = wp_parse_url($clean_url);
2474 $path = $parsed_url['path'] ?? '';
2475
2476 if (!empty($path)) {
2477 // Get the last part of the path as potential slug
2478 $path_parts = array_filter(explode('/', trim($path, '/')));
2479 $potential_slug = end($path_parts);
2480
2481 if (!empty($potential_slug)) {
2482 global $wpdb;
2483
2484 // Try to find any post with this slug
2485 $post_id = $wpdb->get_var($wpdb->prepare(
2486 "SELECT ID FROM {$wpdb->posts}
2487 WHERE post_name = %s
2488 AND post_status IN ('publish', 'closed', 'private')
2489 AND post_type NOT IN ('revision', 'attachment', 'nav_menu_item')
2490 ORDER BY CASE
2491 WHEN post_type = 'post' THEN 1
2492 WHEN post_type = 'page' THEN 2
2493 WHEN post_type = 'topic' THEN 3
2494 WHEN post_type = 'product' THEN 4
2495 ELSE 5
2496 END
2497 LIMIT 1",
2498 $potential_slug
2499 ));
2500
2501 if ($post_id) {
2502 return intval($post_id);
2503 }
2504 }
2505 }
2506
2507 // ADDITIONAL: Try direct database lookup by URL variations
2508 global $wpdb;
2509 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2510
2511 // Try variations of the URL (with/without trailing slash, http/https)
2512 $url_variations = array(
2513 $url,
2514 rtrim($url, '/'),
2515 $url . '/',
2516 str_replace('http://', 'https://', $url),
2517 str_replace('https://', 'http://', $url),
2518 str_replace('http://', 'https://', rtrim($url, '/')),
2519 str_replace('https://', 'http://', rtrim($url, '/'))
2520 );
2521
2522 // Remove duplicates
2523 $url_variations = array_unique($url_variations);
2524
2525 foreach ($url_variations as $variation) {
2526 $existing_record = $wpdb->get_row($wpdb->prepare(
2527 "SELECT id, source_url FROM $table_name WHERE source_url = %s",
2528 $variation
2529 ));
2530
2531 if ($existing_record) {
2532 // Try to get post ID from this stored URL
2533 $stored_post_id = url_to_postid($existing_record->source_url);
2534 if ($stored_post_id > 0) {
2535 return $stored_post_id;
2536 }
2537 }
2538 }
2539
2540 return 0; // No match found
2541 }
2542 /**
2543 * Process selected content via AJAX
2544 */
2545 public function ajax_mxchat_process_selected_content() {
2546 // Basic request validation
2547 if (!check_ajax_referer('mxchat_content_selector_nonce', 'nonce', false)) {
2548 wp_send_json_error('Invalid nonce');
2549 exit;
2550 }
2551
2552 if (!current_user_can('manage_options')) {
2553 wp_send_json_error('Unauthorized access');
2554 exit;
2555 }
2556
2557 // Get post IDs - safely parse the array
2558 $post_ids = array();
2559 if (isset($_POST['post_ids']) && is_array($_POST['post_ids'])) {
2560 foreach ($_POST['post_ids'] as $id) {
2561 $post_ids[] = absint($id);
2562 }
2563 }
2564
2565 if (empty($post_ids)) {
2566 wp_send_json_error('No content selected');
2567 exit;
2568 }
2569
2570 // UPDATED: Get bot_id from request
2571 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
2572
2573 // Process only ONE post at a time to avoid request size issues
2574 $post_id = reset($post_ids);
2575 $post = get_post($post_id);
2576
2577 if (!$post) {
2578 wp_send_json_error('Post not found');
2579 exit;
2580 }
2581
2582 // Get content including ACF fields
2583 $content = $post->post_title . "\n\n" . wp_strip_all_tags($post->post_content);
2584
2585 // ADD ACF FIELDS SUPPORT
2586 $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
2587 if (!empty($acf_fields)) {
2588 $acf_content_parts = array();
2589
2590 foreach ($acf_fields as $field_name => $field_value) {
2591 $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
2592
2593 if (!empty($formatted_value)) {
2594 $field_label = ucwords(str_replace('_', ' ', $field_name));
2595 $acf_content_parts[] = $field_label . ": " . $formatted_value;
2596 }
2597 }
2598
2599 if (!empty($acf_content_parts)) {
2600 $content .= "\n\n" . implode("\n", $acf_content_parts);
2601 }
2602 }
2603
2604 $content = substr($content, 0, 10000); // Limit content size
2605
2606 // UPDATED: Get bot-specific API key
2607 $bot_options = $this->get_bot_options($bot_id);
2608 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
2609 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
2610
2611 if (strpos($selected_model, 'voyage') === 0) {
2612 $api_key = $options['voyage_api_key'] ?? '';
2613 $provider_name = 'Voyage AI';
2614 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2615 $api_key = $options['gemini_api_key'] ?? '';
2616 $provider_name = 'Google Gemini';
2617 } else {
2618 $api_key = $options['api_key'] ?? '';
2619 $provider_name = 'OpenAI';
2620 }
2621
2622 if (empty($api_key)) {
2623 wp_send_json_error($provider_name . ' API key not configured');
2624 exit;
2625 }
2626
2627 $source_url = get_permalink($post_id);
2628 $vector_id = md5($source_url); // Vector ID for Pinecone
2629
2630 // UPDATED: Check for existing content in bot-specific storage
2631 $is_update = false;
2632
2633 // Get bot-specific Pinecone configuration
2634 $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
2635 $use_pinecone = !empty($bot_pinecone_config) && ($bot_pinecone_config['use_pinecone'] ?? false);
2636
2637 if ($use_pinecone && !empty($bot_pinecone_config['api_key'])) {
2638 // Check Pinecone for this bot
2639 $pinecone_data = $this->mxchat_get_pinecone_processed_content($bot_pinecone_config);
2640 if (isset($pinecone_data[$post_id])) {
2641 $is_update = true;
2642 }
2643 } else {
2644 // Check WordPress DB (same as before since it's shared)
2645 global $wpdb;
2646 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2647 $existing_record = $wpdb->get_row($wpdb->prepare(
2648 "SELECT id FROM $table_name WHERE source_url = %s",
2649 $source_url
2650 ));
2651
2652 if ($existing_record) {
2653 $is_update = true;
2654 }
2655 }
2656
2657 // UPDATED: Use the centralized utility function with bot_id
2658 $result = MxChat_Utils::submit_content_to_db(
2659 $content,
2660 $source_url,
2661 $api_key,
2662 $vector_id,
2663 $bot_id
2664 );
2665
2666 if (is_wp_error($result)) {
2667 wp_send_json_error('Storage failed: ' . $result->get_error_message());
2668 exit;
2669 }
2670
2671 $operation_type = $is_update ? 'update' : 'new';
2672
2673 // Count ACF fields for debugging
2674 $acf_field_count = count($acf_fields);
2675
2676 // Success response with minimal data
2677 wp_send_json_success(array(
2678 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully',
2679 'post_id' => $post_id,
2680 'title' => $post->post_title,
2681 'operation_type' => $operation_type,
2682 'vector_id' => $vector_id,
2683 'acf_fields_found' => $acf_field_count,
2684 'content_preview' => substr($content, 0, 100) . '...',
2685 'bot_id' => $bot_id
2686 ));
2687 exit;
2688 }
2689
2690 public function mxchat_get_public_post_types() {
2691 // Get all public post types
2692 $post_types = get_post_types(array('public' => true), 'objects');
2693 $post_type_options = array();
2694
2695 foreach ($post_types as $post_type) {
2696 $post_type_options[$post_type->name] = $post_type->label;
2697 }
2698
2699 // Also include common forum/community post types that might not be marked as public
2700 $additional_types = array(
2701 'topic' => 'Forum Topics (bbPress)',
2702 'reply' => 'Forum Replies (bbPress)',
2703 'forum' => 'Forums (bbPress)',
2704 'wpforo_topic' => 'wpForo Topics',
2705 'wpforo_post' => 'wpForo Posts'
2706 );
2707
2708 foreach ($additional_types as $type_name => $type_label) {
2709 if (post_type_exists($type_name) && !isset($post_type_options[$type_name])) {
2710 $post_type_options[$type_name] = $type_label;
2711 }
2712 }
2713
2714 return $post_type_options;
2715 }
2716
2717 /**
2718 * Retrieves processed content from Pinecone API
2719 */
2720 public function mxchat_get_pinecone_processed_content($pinecone_options) {
2721 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2722 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2723
2724 if (empty($api_key) || empty($host)) {
2725 return array();
2726 }
2727
2728 $pinecone_data = array();
2729
2730 try {
2731 // Always get fresh data from Pinecone
2732 $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
2733
2734 // Method 2: Final fallback - try stats endpoint (if available)
2735 if (empty($pinecone_data)) {
2736 $stats_url = "https://{$host}/describe_index_stats";
2737
2738 $response = wp_remote_post($stats_url, array(
2739 'headers' => array(
2740 'Api-Key' => $api_key,
2741 'Content-Type' => 'application/json'
2742 ),
2743 'body' => json_encode(array()),
2744 'timeout' => 30
2745 ));
2746
2747 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2748 $body = wp_remote_retrieve_body($response);
2749 $stats_data = json_decode($body, true);
2750 }
2751 }
2752
2753 } catch (Exception $e) {
2754 // Log error but return fresh data only
2755 }
2756
2757 return $pinecone_data;
2758 }
2759 public function mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
2760 //error_log('=== DEBUG: Starting mxchat_fetch_pinecone_vectors_by_ids ===');
2761
2762 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2763 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2764
2765 if (empty($api_key) || empty($host) || empty($vector_ids)) {
2766 //error_log('DEBUG: Missing parameters for fetch by IDs');
2767 return array();
2768 }
2769
2770 try {
2771 $fetch_url = "https://{$host}/vectors/fetch";
2772 //error_log('DEBUG: Fetch URL: ' . $fetch_url);
2773 //error_log('DEBUG: Fetching ' . count($vector_ids) . ' vector IDs');
2774
2775 // Pinecone fetch API allows fetching specific vectors by ID
2776 $fetch_data = array(
2777 'ids' => array_values($vector_ids)
2778 );
2779
2780 $response = wp_remote_post($fetch_url, array(
2781 'headers' => array(
2782 'Api-Key' => $api_key,
2783 'Content-Type' => 'application/json'
2784 ),
2785 'body' => json_encode($fetch_data),
2786 'timeout' => 30
2787 ));
2788
2789 if (is_wp_error($response)) {
2790 //error_log('DEBUG: Fetch by IDs WP error: ' . $response->get_error_message());
2791 return array();
2792 }
2793
2794 $response_code = wp_remote_retrieve_response_code($response);
2795 //error_log('DEBUG: Fetch response code: ' . $response_code);
2796
2797 if ($response_code !== 200) {
2798 $error_body = wp_remote_retrieve_body($response);
2799 //error_log('DEBUG: Fetch failed with body: ' . $error_body);
2800 return array();
2801 }
2802
2803 $body = wp_remote_retrieve_body($response);
2804 $data = json_decode($body, true);
2805
2806 //error_log('DEBUG: Fetch response structure: ' . print_r(array_keys($data), true));
2807
2808 if (!isset($data['vectors'])) {
2809 //error_log('DEBUG: No vectors key in response');
2810 return array();
2811 }
2812
2813 $processed_data = array();
2814
2815 foreach ($data['vectors'] as $vector_id => $vector_data) {
2816 $metadata = $vector_data['metadata'] ?? array();
2817 $source_url = $metadata['source_url'] ?? '';
2818
2819 if (!empty($source_url)) {
2820 $post_id = url_to_postid($source_url);
2821 if ($post_id) {
2822 $created_at = $metadata['created_at'] ?? '';
2823 $processed_date = 'Recently';
2824
2825 if (!empty($created_at)) {
2826 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
2827 if ($timestamp) {
2828 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2829 }
2830 }
2831
2832 $processed_data[$post_id] = array(
2833 'db_id' => $vector_id,
2834 'processed_date' => $processed_date,
2835 'url' => $source_url,
2836 'source' => 'pinecone',
2837 'timestamp' => $timestamp ?? current_time('timestamp')
2838 );
2839 }
2840 }
2841 }
2842
2843 //error_log('DEBUG: Processed ' . count($processed_data) . ' vectors from fetch');
2844 return $processed_data;
2845
2846 } catch (Exception $e) {
2847 //error_log('DEBUG: Exception in fetch_pinecone_vectors_by_ids: ' . $e->getMessage());
2848 return array();
2849 }
2850 }
2851
2852 /**
2853 * Scan Pinecone for processed content
2854 */
2855 public function mxchat_scan_pinecone_for_processed_content($pinecone_options) {
2856 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2857 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2858
2859 if (empty($api_key) || empty($host)) {
2860 return array();
2861 }
2862
2863 try {
2864 // Use multiple random vectors to get better coverage
2865 $all_matches = array();
2866 $seen_ids = array();
2867
2868 // Try 3 different random vectors to get better coverage
2869 for ($i = 0; $i < 3; $i++) {
2870 $query_url = "https://{$host}/query";
2871
2872 // Generate a random unit vector instead of zeros
2873 $random_vector = array();
2874 for ($j = 0; $j < 1536; $j++) {
2875 $random_vector[] = (rand(-1000, 1000) / 1000.0);
2876 }
2877
2878 // Normalize the vector to unit length
2879 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
2880 if ($magnitude > 0) {
2881 $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
2882 }
2883
2884 $query_data = array(
2885 'includeMetadata' => true,
2886 'includeValues' => false,
2887 'topK' => 10000,
2888 'vector' => $random_vector
2889 );
2890
2891 $response = wp_remote_post($query_url, array(
2892 'headers' => array(
2893 'Api-Key' => $api_key,
2894 'Content-Type' => 'application/json'
2895 ),
2896 'body' => json_encode($query_data),
2897 'timeout' => 30
2898 ));
2899
2900 if (is_wp_error($response)) {
2901 continue;
2902 }
2903
2904 $response_code = wp_remote_retrieve_response_code($response);
2905
2906 if ($response_code !== 200) {
2907 continue;
2908 }
2909
2910 $body = wp_remote_retrieve_body($response);
2911 $data = json_decode($body, true);
2912
2913 if (isset($data['matches'])) {
2914 foreach ($data['matches'] as $match) {
2915 $match_id = $match['id'] ?? '';
2916 if (!empty($match_id) && !isset($seen_ids[$match_id])) {
2917 $all_matches[] = $match;
2918 $seen_ids[$match_id] = true;
2919 }
2920 }
2921 }
2922 }
2923
2924 // Convert matches to processed data format
2925 $processed_data = array();
2926
2927 foreach ($all_matches as $match) {
2928 $metadata = $match['metadata'] ?? array();
2929 $source_url = $metadata['source_url'] ?? '';
2930 $match_id = $match['id'] ?? '';
2931
2932 if (!empty($source_url) && !empty($match_id)) {
2933 $post_id = url_to_postid($source_url);
2934 if ($post_id) {
2935 $created_at = $metadata['created_at'] ?? '';
2936 $processed_date = 'Recently';
2937
2938 if (!empty($created_at)) {
2939 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
2940 if ($timestamp) {
2941 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2942 }
2943 }
2944
2945 $processed_data[$post_id] = array(
2946 'db_id' => $match_id,
2947 'processed_date' => $processed_date,
2948 'url' => $source_url,
2949 'source' => 'pinecone',
2950 'timestamp' => $timestamp ?? current_time('timestamp')
2951 );
2952 }
2953 }
2954 }
2955
2956 return $processed_data;
2957
2958 } catch (Exception $e) {
2959 return array();
2960 }
2961 }
2962 /**
2963 * UPDATED: Generate embeddings from input text for MXChat with bot support
2964 */
2965 private function mxchat_generate_embedding($text, $bot_id = 'default') {
2966 // Enable detailed logging for debugging
2967 //error_log('[MXCHAT-EMBED] Starting embedding generation for bot: ' . $bot_id . '. Text length: ' . strlen($text) . ' bytes');
2968 //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
2969
2970 // UPDATED: Get bot-specific options
2971 $bot_options = $this->get_bot_options($bot_id);
2972 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
2973
2974 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
2975 //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model);
2976
2977 // Determine provider and endpoint
2978 if (strpos($selected_model, 'voyage') === 0) {
2979 $api_key = $options['voyage_api_key'] ?? '';
2980 $endpoint = 'https://api.voyageai.com/v1/embeddings';
2981 $provider_name = 'Voyage AI';
2982 //error_log('[MXCHAT-EMBED] Using Voyage AI API for bot ' . $bot_id);
2983 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2984 $api_key = $options['gemini_api_key'] ?? '';
2985 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
2986 $provider_name = 'Google Gemini';
2987 //error_log('[MXCHAT-EMBED] Using Google Gemini API for bot ' . $bot_id);
2988 } else {
2989 $api_key = $options['api_key'] ?? '';
2990 $endpoint = 'https://api.openai.com/v1/embeddings';
2991 $provider_name = 'OpenAI';
2992 //error_log('[MXCHAT-EMBED] Using OpenAI API for bot ' . $bot_id);
2993 }
2994
2995 //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
2996
2997 if (empty($api_key)) {
2998 $error_message = sprintf('Missing %s API key for bot %s. Please configure your API key in the bot settings.', $provider_name, $bot_id);
2999 //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
3000 return $error_message;
3001 }
3002
3003 // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
3004 $estimated_tokens = ceil(str_word_count($text) / 0.75);
3005 //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
3006
3007 if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
3008 //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
3009 // Consider truncating text here
3010 }
3011
3012 // Prepare request body based on provider
3013 if (strpos($selected_model, 'gemini-embedding') === 0) {
3014 // Gemini API format
3015 $request_body = array(
3016 'model' => 'models/' . $selected_model,
3017 'content' => array(
3018 'parts' => array(
3019 array('text' => $text)
3020 )
3021 )
3022 );
3023
3024 // Set output dimensionality to 1536 for consistency with other models
3025 $request_body['outputDimensionality'] = 1536;
3026 } else {
3027 // OpenAI/Voyage API format
3028 $request_body = array(
3029 'model' => $selected_model,
3030 'input' => $text
3031 );
3032
3033 // Add output_dimension for voyage-3-large model
3034 if ($selected_model === 'voyage-3-large') {
3035 $request_body['output_dimension'] = 2048;
3036 }
3037 }
3038
3039 //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
3040
3041 // Prepare headers based on provider
3042 if (strpos($selected_model, 'gemini-embedding') === 0) {
3043 // Gemini uses API key as query parameter
3044 $endpoint .= '?key=' . $api_key;
3045 $headers = array(
3046 'Content-Type' => 'application/json'
3047 );
3048 } else {
3049 // OpenAI/Voyage use Bearer token
3050 $headers = array(
3051 'Authorization' => 'Bearer ' . $api_key,
3052 'Content-Type' => 'application/json'
3053 );
3054 }
3055
3056 // Make API request
3057 //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
3058 $response = wp_remote_post($endpoint, array(
3059 'body' => wp_json_encode($request_body),
3060 'headers' => $headers,
3061 'timeout' => 60 // Increased timeout for large inputs
3062 ));
3063
3064 // Handle wp_remote_post errors
3065 if (is_wp_error($response)) {
3066 $error_message = $response->get_error_message();
3067 //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
3068 return 'Connection error: ' . $error_message;
3069 }
3070
3071 // Get and check HTTP response code
3072 $http_code = wp_remote_retrieve_response_code($response);
3073 //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
3074
3075 if ($http_code !== 200) {
3076 $error_body = wp_remote_retrieve_body($response);
3077 //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
3078
3079 // Try to parse error for more details
3080 $error_json = json_decode($error_body, true);
3081 if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
3082 $error_type = $error_json['error']['type'] ?? 'unknown';
3083 $error_message = $error_json['error']['message'] ?? 'No message';
3084 //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
3085 //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
3086
3087 // Customize error message for common API errors
3088 if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
3089 $error_message = sprintf('Invalid %s API key for bot %s. Please check your API key in the bot settings.', $provider_name, $bot_id);
3090 } elseif ($error_type === 'authentication_error') {
3091 $error_message = sprintf('%s authentication failed for bot %s. Please verify your API key in the bot settings.', $provider_name, $bot_id);
3092 }
3093
3094 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
3095 return $error_message;
3096 }
3097
3098 $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding for bot %s", $http_code, $bot_id);
3099 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
3100 return $error_message;
3101 }
3102
3103 // Parse response body
3104 $response_body = wp_remote_retrieve_body($response);
3105 //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
3106
3107 $response_data = json_decode($response_body, true);
3108
3109 if (json_last_error() !== JSON_ERROR_NONE) {
3110 $error = json_last_error_msg();
3111 //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
3112 //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
3113 return "Failed to parse API response: $error";
3114 }
3115
3116 // Handle different response formats based on provider
3117 if (strpos($selected_model, 'gemini-embedding') === 0) {
3118 // Gemini API response format
3119 if (isset($response_data['embedding']['values'])) {
3120 $embedding_dimensions = count($response_data['embedding']['values']);
3121 //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
3122
3123 // Check if embedding dimensions are as expected (should be 1536)
3124 if ($embedding_dimensions !== 1536) {
3125 //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
3126 }
3127
3128 return $response_data['embedding']['values'];
3129 } else {
3130 //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
3131 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
3132
3133 if (isset($response_data['error'])) {
3134 $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
3135 //error_log('[MXCHAT-EMBED] ' . $error_message);
3136 return $error_message;
3137 }
3138
3139 $error_message = "Invalid Gemini API response format: No embedding found";
3140 //error_log('[MXCHAT-EMBED] ' . $error_message);
3141 return $error_message;
3142 }
3143 } else {
3144 // OpenAI/Voyage API response format
3145 if (isset($response_data['data'][0]['embedding'])) {
3146 $embedding_dimensions = count($response_data['data'][0]['embedding']);
3147 //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
3148
3149 // Check if embedding dimensions are as expected
3150 if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
3151 ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
3152 //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
3153 }
3154
3155 return $response_data['data'][0]['embedding'];
3156 } else {
3157 //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
3158 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
3159
3160 if (isset($response_data['error'])) {
3161 $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
3162 //error_log('[MXCHAT-EMBED] ' . $error_message);
3163 return $error_message;
3164 }
3165
3166 $error_message = "Invalid API response format: No embedding found";
3167 //error_log('[MXCHAT-EMBED] ' . $error_message);
3168 return $error_message;
3169 }
3170 }
3171 }
3172
3173 /**
3174 * Get bot-specific options for multi-bot functionality
3175 * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
3176 */
3177 private function get_bot_options($bot_id = 'default') {
3178 //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
3179
3180 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
3181 //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
3182 return array();
3183 }
3184
3185 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
3186
3187 if (!empty($bot_options)) {
3188 //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
3189 if (isset($bot_options['similarity_threshold'])) {
3190 //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
3191 }
3192 }
3193
3194 return is_array($bot_options) ? $bot_options : array();
3195 }
3196
3197 /**
3198 * Get bot-specific Pinecone configuration
3199 * Used in the knowledge retrieval functions
3200 */
3201 // Also add debugging to your get_bot_pinecone_config function
3202 private function get_bot_pinecone_config($bot_id = 'default') {
3203 //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
3204
3205 // If default bot or multi-bot add-on not active, use default Pinecone config
3206 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
3207 //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
3208 $addon_options = get_option('mxchat_pinecone_addon_options', array());
3209 $config = array(
3210 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
3211 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
3212 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
3213 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
3214 );
3215 //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
3216 return $config;
3217 }
3218
3219 //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
3220
3221 // Hook for multi-bot add-on to provide bot-specific Pinecone config
3222 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
3223
3224 if (!empty($bot_pinecone_config)) {
3225 //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
3226 //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
3227 //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
3228 //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
3229 } else {
3230 //error_log("MXCHAT DEBUG: Filter returned empty config!");
3231 }
3232
3233 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
3234 }
3235
3236
3237 public function mxchat_ajax_dismiss_completed_status() {
3238 try {
3239 // Verify the request
3240 check_ajax_referer('mxchat_status_nonce', 'nonce');
3241
3242 if (!current_user_can('manage_options')) {
3243 wp_send_json_error('Unauthorized access');
3244 exit;
3245 }
3246
3247 $card_type = isset($_POST['card_type']) ? sanitize_text_field($_POST['card_type']) : '';
3248
3249 if ($card_type === 'pdf') {
3250 // Clear PDF status
3251 $pdf_url = get_transient('mxchat_last_pdf_url');
3252 if ($pdf_url) {
3253 delete_transient('mxchat_pdf_status_' . md5($pdf_url));
3254 delete_transient('mxchat_last_pdf_url');
3255 }
3256 } elseif ($card_type === 'sitemap') {
3257 // Clear sitemap status
3258 $sitemap_url = get_transient('mxchat_last_sitemap_url');
3259 if ($sitemap_url) {
3260 delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
3261 delete_transient('mxchat_last_sitemap_url');
3262 }
3263 }
3264
3265 wp_send_json_success(array('message' => 'Status dismissed successfully'));
3266
3267 } catch (Exception $e) {
3268 wp_send_json_error(array('message' => 'Error dismissing status: ' . $e->getMessage()));
3269 }
3270 }
3271
3272 /**
3273 * Render completed status cards on page load
3274 * This ensures completed processing status persists through page refreshes
3275 */
3276 public function mxchat_render_completed_status_cards() {
3277 $output = '';
3278
3279 // Check for completed PDF status
3280 $pdf_url = get_transient('mxchat_last_pdf_url');
3281 if ($pdf_url) {
3282 $pdf_status = $this->mxchat_get_pdf_processing_status($pdf_url);
3283 if ($pdf_status && ($pdf_status['status'] === 'complete' || $pdf_status['status'] === 'error')) {
3284 $output .= $this->mxchat_render_pdf_status_card($pdf_status, $pdf_url);
3285 }
3286 }
3287
3288 // Check for completed sitemap status
3289 $sitemap_url = get_transient('mxchat_last_sitemap_url');
3290 if ($sitemap_url) {
3291 $sitemap_status = $this->mxchat_get_sitemap_processing_status($sitemap_url);
3292 if ($sitemap_status && ($sitemap_status['status'] === 'complete' || $sitemap_status['status'] === 'error')) {
3293 $output .= $this->mxchat_render_sitemap_status_card($sitemap_status, $sitemap_url);
3294 }
3295 }
3296
3297 return $output;
3298 }
3299
3300 /**
3301 * Render PDF status card HTML
3302 */
3303 private function mxchat_render_pdf_status_card($status, $pdf_url) {
3304 $html = '<div class="mxchat-status-card" data-card-type="pdf">';
3305 $html .= '<div class="mxchat-status-header">';
3306 $html .= '<h4>' . esc_html__('PDF Processing Status', 'mxchat') . '</h4>';
3307
3308 // Add dismiss button for completed status
3309 if ($status['status'] === 'complete' || $status['status'] === 'error') {
3310 $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
3311 }
3312
3313 // Process Batch button for processing status
3314 if ($status['status'] === 'processing') {
3315 $html .= '<button type="button" class="mxchat-manual-batch-btn"
3316 data-process-type="pdf"
3317 data-url="' . esc_attr($pdf_url) . '">
3318 ' . esc_html__('Process Batch', 'mxchat') . '</button>';
3319 }
3320
3321 // Add status badges
3322 if ($status['status'] === 'error') {
3323 $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
3324 } elseif ($status['status'] === 'complete') {
3325 if ($status['failed_pages'] > 0) {
3326 $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
3327 sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_pages']) . '</span>';
3328 } else {
3329 $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
3330 }
3331 }
3332
3333 $html .= '</div>'; // End header
3334
3335 // Progress bar
3336 $html .= '<div class="mxchat-progress-bar">';
3337 $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
3338 $html .= '</div>';
3339
3340 // Status details
3341 $html .= '<div class="mxchat-status-details">';
3342 $html .= '<p>' . sprintf(
3343 esc_html__('Progress: %d of %d pages (%d%%)', 'mxchat'),
3344 $status['processed_pages'],
3345 $status['total_pages'],
3346 $status['percentage']
3347 ) . '</p>';
3348
3349 // Show failed pages count if any
3350 if ($status['failed_pages'] > 0) {
3351 $html .= '<p><strong>' . esc_html__('Failed pages:', 'mxchat') . '</strong> ' . esc_html($status['failed_pages']) . '</p>';
3352 }
3353
3354 $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
3355 $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
3356
3357 // Add completion summary if available AND it's an array
3358 if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
3359 $summary = $status['completion_summary'];
3360 $html .= '<div class="mxchat-completion-summary">';
3361 $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
3362 $html .= '<p><strong>' . esc_html__('Total Pages:', 'mxchat') . '</strong> ' . esc_html($summary['total_pages']) . '</p>';
3363 $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_pages']) . '</p>';
3364 $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_pages']) . '</p>';
3365 $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
3366 $html .= '</div>';
3367 }
3368
3369 // Add failed pages list if any AND it's an array
3370 if (isset($status['failed_pages_list']) && is_array($status['failed_pages_list']) && !empty($status['failed_pages_list'])) {
3371 $html .= $this->mxchat_render_failed_pages_list($status['failed_pages_list']);
3372 }
3373
3374 // Add error message if any
3375 if (isset($status['error']) && !empty($status['error'])) {
3376 $html .= '<div class="mxchat-error-notice">';
3377 $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
3378 $html .= '</div>';
3379 }
3380
3381 $html .= '</div>'; // End details
3382 $html .= '</div>'; // End card
3383
3384 return $html;
3385 }
3386 /**
3387 * Render sitemap status card HTML
3388 */
3389 private function mxchat_render_sitemap_status_card($status, $sitemap_url) {
3390 $html = '<div class="mxchat-status-card" data-card-type="sitemap">';
3391 $html .= '<div class="mxchat-status-header">';
3392 $html .= '<h4>' . esc_html__('Sitemap Processing Status', 'mxchat') . '</h4>';
3393
3394 // Add dismiss button for completed status
3395 if ($status['status'] === 'complete' || $status['status'] === 'error') {
3396 $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
3397 }
3398
3399 // Process Batch button for processing status
3400 if ($status['status'] === 'processing') {
3401 $html .= '<button type="button" class="mxchat-manual-batch-btn"
3402 data-process-type="sitemap"
3403 data-url="' . esc_attr($sitemap_url) . '">
3404 ' . esc_html__('Process Batch', 'mxchat') . '</button>';
3405 }
3406
3407 // Add status badges
3408 if ($status['status'] === 'error') {
3409 $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
3410 } elseif ($status['status'] === 'complete') {
3411 if ($status['failed_urls'] > 0) {
3412 $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
3413 sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_urls']) . '</span>';
3414 } else {
3415 $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
3416 }
3417 }
3418
3419 $html .= '</div>'; // End header
3420
3421 // Progress bar
3422 $html .= '<div class="mxchat-progress-bar">';
3423 $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
3424 $html .= '</div>';
3425
3426 // Status details
3427 $html .= '<div class="mxchat-status-details">';
3428 $html .= '<p>' . sprintf(
3429 esc_html__('Progress: %d of %d URLs (%d%%)', 'mxchat'),
3430 $status['processed_urls'],
3431 $status['total_urls'],
3432 $status['percentage']
3433 ) . '</p>';
3434
3435 // Show failed URLs count if any
3436 if ($status['failed_urls'] > 0) {
3437 $html .= '<p><strong>' . esc_html__('Failed URLs:', 'mxchat') . '</strong> ' . esc_html($status['failed_urls']) . '</p>';
3438 }
3439
3440 $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
3441 $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
3442
3443 // Add completion summary if available AND it's an array
3444 if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
3445 $summary = $status['completion_summary'];
3446 $html .= '<div class="mxchat-completion-summary">';
3447 $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
3448 $html .= '<p><strong>' . esc_html__('Total URLs:', 'mxchat') . '</strong> ' . esc_html($summary['total_urls']) . '</p>';
3449 $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_urls']) . '</p>';
3450 $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_urls']) . '</p>';
3451 $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
3452 $html .= '</div>';
3453 }
3454
3455 // Add error messages if any (but not the failed URLs list)
3456 if (!empty($status['error']) || !empty($status['last_error'])) {
3457 $html .= '<div class="mxchat-error-notice">';
3458
3459 if (!empty($status['error'])) {
3460 $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
3461 }
3462
3463 if (!empty($status['last_error'])) {
3464 $html .= '<p class="last-error">' . esc_html__('Last error:', 'mxchat') . ' ' . esc_html($status['last_error']) . '</p>';
3465 }
3466
3467 $html .= '</div>';
3468 }
3469
3470 $html .= '</div>'; // End details
3471 $html .= '</div>'; // End card
3472
3473 return $html;
3474 }
3475
3476
3477 /**
3478 * Render failed pages list
3479 */
3480 private function mxchat_render_failed_pages_list($failed_pages_list) {
3481 // Validate that $failed_pages_list is an array and not empty
3482 if (!is_array($failed_pages_list) || empty($failed_pages_list)) {
3483 return '';
3484 }
3485
3486 $html = '<div class="mxchat-error-notice">';
3487 $html .= '<div class="mxchat-failed-pages-container">';
3488 $html .= '<h5>' . sprintf(esc_html__('Failed Pages (%d)', 'mxchat'), count($failed_pages_list)) . '</h5>';
3489 $html .= '<details>';
3490 $html .= '<summary>' . esc_html__('Show Failed Pages', 'mxchat') . '</summary>';
3491 $html .= '<div class="mxchat-failed-pages-list">';
3492
3493 // Create table for failed pages
3494 $html .= '<table class="widefat striped">';
3495 $html .= '<thead><tr>';
3496 $html .= '<th>' . esc_html__('Page', 'mxchat') . '</th>';
3497 $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
3498 $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
3499 $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
3500 $html .= '</tr></thead><tbody>';
3501
3502 // Sort failed pages by most recent
3503 $sorted_failed_pages = $failed_pages_list;
3504 usort($sorted_failed_pages, function($a, $b) {
3505 return ($b['time'] ?? 0) - ($a['time'] ?? 0);
3506 });
3507
3508 foreach ($sorted_failed_pages as $item) {
3509 // Ensure $item is an array before accessing its elements
3510 if (!is_array($item)) {
3511 continue;
3512 }
3513
3514 $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
3515 $html .= '<tr>';
3516 $html .= '<td>' . esc_html__('Page', 'mxchat') . ' ' . esc_html($item['page'] ?? 'Unknown') . '</td>';
3517 $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
3518 $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
3519 $html .= '<td>' . esc_html($time_ago) . '</td>';
3520 $html .= '</tr>';
3521 }
3522
3523 $html .= '</tbody></table>';
3524 $html .= '</div></details></div></div>';
3525
3526 return $html;
3527 }
3528
3529 /**
3530 * Render failed URLs list
3531 */
3532 private function mxchat_render_failed_urls_list($failed_urls_list) {
3533 // Validate that $failed_urls_list is an array and not empty
3534 if (!is_array($failed_urls_list) || empty($failed_urls_list)) {
3535 return '';
3536 }
3537
3538 $html = '<div class="mxchat-failed-urls-container">';
3539 $html .= '<h5>' . sprintf(esc_html__('Failed URLs (%d)', 'mxchat'), count($failed_urls_list)) . '</h5>';
3540 $html .= '<details>';
3541 $html .= '<summary>' . esc_html__('Show Failed URLs', 'mxchat') . '</summary>';
3542 $html .= '<div class="mxchat-failed-urls-list">';
3543
3544 // Create table for failed URLs
3545 $html .= '<table class="widefat striped">';
3546 $html .= '<thead><tr>';
3547 $html .= '<th>' . esc_html__('URL', 'mxchat') . '</th>';
3548 $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
3549 $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
3550 $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
3551 $html .= '</tr></thead><tbody>';
3552
3553 // Sort failed URLs by most recent
3554 $sorted_failed_urls = $failed_urls_list;
3555 usort($sorted_failed_urls, function($a, $b) {
3556 return ($b['time'] ?? 0) - ($a['time'] ?? 0);
3557 });
3558
3559 // Show up to 50 failed URLs
3560 $display_urls = array_slice($sorted_failed_urls, 0, 50);
3561
3562 foreach ($display_urls as $item) {
3563 // Ensure $item is an array before accessing its elements
3564 if (!is_array($item)) {
3565 continue;
3566 }
3567
3568 $url = $item['url'] ?? '';
3569 $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
3570
3571 // Truncate URL for display
3572 $display_url = strlen($url) > 50 ? substr($url, 0, 47) . '...' : $url;
3573
3574 $html .= '<tr>';
3575 $html .= '<td style="word-break: break-all;">';
3576 if (!empty($url)) {
3577 $html .= '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($display_url) . '</a>';
3578 } else {
3579 $html .= esc_html__('Unknown URL', 'mxchat');
3580 }
3581 $html .= '</td>';
3582 $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
3583 $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
3584 $html .= '<td>' . esc_html($time_ago) . '</td>';
3585 $html .= '</tr>';
3586 }
3587
3588 $html .= '</tbody></table>';
3589
3590 if (count($failed_urls_list) > 50) {
3591 $html .= '<div class="mxchat-failed-urls-more">+ ' .
3592 (count($failed_urls_list) - 50) .
3593 ' ' . esc_html__('more failed URLs not shown', 'mxchat') . '</div>';
3594 }
3595
3596 $html .= '</div></details></div>';
3597
3598 return $html;
3599 }
3600
3601 /**
3602 * Get all ACF fields for a specific post
3603 */
3604 public function mxchat_get_acf_fields_for_post($post_id) {
3605 if (!function_exists('get_fields')) {
3606 return array();
3607 }
3608
3609 $fields = get_fields($post_id);
3610 if (!$fields || !is_array($fields)) {
3611 return array();
3612 }
3613
3614 return $fields;
3615 }
3616
3617 /**
3618 * Format ACF field values for content extraction
3619 */
3620 public function mxchat_format_acf_field_value($value, $field_name = '', $post_id = 0) {
3621 if (empty($value)) {
3622 return '';
3623 }
3624
3625 // Handle WP_Post objects first (THIS IS THE KEY FIX)
3626 if ($value instanceof WP_Post) {
3627 return $value->post_title ?: '';
3628 }
3629
3630 // Handle other WP objects
3631 if (is_object($value)) {
3632 if (isset($value->post_title)) {
3633 return $value->post_title;
3634 } elseif (isset($value->display_name)) {
3635 return $value->display_name;
3636 } elseif (isset($value->name)) {
3637 return $value->name;
3638 } elseif (method_exists($value, '__toString')) {
3639 try {
3640 return (string) $value;
3641 } catch (Exception $e) {
3642 return '';
3643 }
3644 }
3645 // For any other objects, return empty string
3646 return '';
3647 }
3648
3649 // Handle different ACF field types
3650 if (is_array($value)) {
3651 // Check if it's an image/file field
3652 if (isset($value['url'])) {
3653 // Image field - return alt text, title, or caption
3654 if (!empty($value['alt'])) {
3655 return $value['alt'];
3656 } elseif (!empty($value['title'])) {
3657 return $value['title'];
3658 } elseif (!empty($value['caption'])) {
3659 return $value['caption'];
3660 } else {
3661 return ''; // Don't include just the URL
3662 }
3663 }
3664
3665 // Check if it's a post object or relationship field
3666 if (isset($value['post_title'])) {
3667 return $value['post_title'];
3668 }
3669
3670 // Check if it's a user field
3671 if (isset($value['display_name'])) {
3672 return $value['display_name'];
3673 }
3674
3675 // Check if it's a taxonomy term
3676 if (isset($value['name']) && isset($value['taxonomy'])) {
3677 return $value['name'];
3678 }
3679
3680 // Check if it's a select field with label
3681 if (isset($value['label'])) {
3682 return $value['label'];
3683 }
3684
3685 // Check for repeater field or flexible content
3686 if (is_numeric(key($value))) {
3687 $sub_values = array();
3688 foreach ($value as $sub_item) {
3689 if (is_array($sub_item)) {
3690 // For repeater/flexible content, extract text values
3691 $sub_text = $this->mxchat_extract_text_from_acf_array($sub_item);
3692 if (!empty($sub_text)) {
3693 $sub_values[] = $sub_text;
3694 }
3695 } elseif ($sub_item instanceof WP_Post) {
3696 // Handle WP_Post objects in arrays
3697 $sub_values[] = $sub_item->post_title ?: '';
3698 } else {
3699 $sub_values[] = (string) $sub_item;
3700 }
3701 }
3702 return implode(', ', array_filter($sub_values));
3703 }
3704
3705 // For other arrays, try to extract meaningful text
3706 $text_values = array();
3707 foreach ($value as $key => $val) {
3708 if (is_string($val) && !empty(trim($val))) {
3709 $text_values[] = trim($val);
3710 } elseif ($val instanceof WP_Post) {
3711 // Handle WP_Post objects in associative arrays
3712 $text_values[] = $val->post_title ?: '';
3713 } elseif (is_array($val) && isset($val['post_title'])) {
3714 $text_values[] = $val['post_title'];
3715 } elseif (is_array($val) && isset($val['name'])) {
3716 $text_values[] = $val['name'];
3717 }
3718 }
3719
3720 return implode(', ', array_filter($text_values));
3721 }
3722
3723 // Handle boolean values
3724 if (is_bool($value)) {
3725 return $value ? 'Yes' : 'No';
3726 }
3727
3728 // Handle numeric values
3729 if (is_numeric($value)) {
3730 return (string) $value;
3731 }
3732
3733 // Handle string values
3734 if (is_string($value)) {
3735 return trim($value);
3736 }
3737
3738 // For anything else that we can't handle, return empty string
3739 // This prevents the "Object could not be converted to string" error
3740 return '';
3741 }
3742
3743 /**
3744 * Extract text from complex ACF array structures
3745 */
3746 private function mxchat_extract_text_from_acf_array($array) {
3747 if (!is_array($array)) {
3748 return '';
3749 }
3750
3751 $text_parts = array();
3752
3753 foreach ($array as $key => $value) {
3754 if (is_string($value) && !empty(trim($value))) {
3755 // Skip keys that are likely to be IDs or technical values
3756 if (!is_numeric($value) || strlen($value) > 10) {
3757 $text_parts[] = trim($value);
3758 }
3759 } elseif ($value instanceof WP_Post) {
3760 // Handle WP_Post objects
3761 $text_parts[] = $value->post_title ?: '';
3762 } elseif (is_array($value)) {
3763 if (isset($value['post_title'])) {
3764 $text_parts[] = $value['post_title'];
3765 } elseif (isset($value['name'])) {
3766 $text_parts[] = $value['name'];
3767 } elseif (isset($value['label'])) {
3768 $text_parts[] = $value['label'];
3769 }
3770 } elseif (is_object($value)) {
3771 // Handle other objects safely
3772 if (isset($value->post_title)) {
3773 $text_parts[] = $value->post_title;
3774 } elseif (isset($value->name)) {
3775 $text_parts[] = $value->name;
3776 } elseif (isset($value->display_name)) {
3777 $text_parts[] = $value->display_name;
3778 }
3779 }
3780 }
3781
3782 return implode(', ', array_filter($text_parts));
3783 }
3784
3785 public function mxchat_handle_post_update($post_id, $post, $update) {
3786 // Basic validation checks
3787 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
3788 return;
3789 }
3790
3791 $post_type = $post->post_type;
3792
3793 // Check if sync is enabled for this post type
3794 $should_sync = false;
3795
3796 // Check built-in post types first
3797 if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
3798 $should_sync = true;
3799 } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
3800 $should_sync = true;
3801 } else {
3802 // Check custom post types
3803 $option_name = 'mxchat_auto_sync_' . $post_type;
3804 if (get_option($option_name) === '1') {
3805 $should_sync = true;
3806 }
3807 }
3808
3809 if (!$should_sync) {
3810 return;
3811 }
3812
3813 // Check if we have stored the previous status and URL in our transients
3814 $previous_status_key = 'mxchat_prev_status_' . $post_id;
3815 $previous_status = get_transient($previous_status_key);
3816
3817 $previous_url_key = 'mxchat_prev_url_' . $post_id;
3818 $previous_url = get_transient($previous_url_key);
3819
3820 // If the post was previously published but is now not published, remove from knowledge base
3821 if ($previous_status === 'publish' && $post->post_status !== 'publish') {
3822 // Use the stored URL from when it was published, or fall back to current permalink
3823 $source_url = $previous_url ?: get_permalink($post_id);
3824
3825 if ($source_url) {
3826 // Check if Pinecone is enabled
3827 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3828 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3829
3830 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3831 // Delete from Pinecone
3832 $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
3833 } else {
3834 // Delete from WordPress DB
3835 global $wpdb;
3836 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3837
3838 $result = $wpdb->delete(
3839 $table_name,
3840 array('source_url' => $source_url),
3841 array('%s')
3842 );
3843 }
3844 }
3845
3846 // Clean up the transients and exit early
3847 delete_transient($previous_status_key);
3848 delete_transient($previous_url_key);
3849 return;
3850 }
3851
3852 // Store the current status for next time (if this is an update)
3853 if ($update) {
3854 set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
3855
3856 // If the post is currently published, also store its URL
3857 if ($post->post_status === 'publish') {
3858 $current_url = get_permalink($post_id);
3859 set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
3860 }
3861 }
3862
3863 // Only process currently published content for adding/updating
3864 if ($post->post_status === 'publish') {
3865 // Get the source URL
3866 $source_url = get_permalink($post_id);
3867
3868 // Get content with proper formatting (matching ajax_mxchat_process_selected_content)
3869 $title = get_the_title($post_id);
3870 $content = get_post_field('post_content', $post_id);
3871
3872 // Apply WordPress content filters to get properly formatted content
3873 $content = apply_filters('the_content', $content);
3874
3875 // Strip tags but preserve structure
3876 $content = wp_strip_all_tags($content);
3877
3878 // Combine title and content
3879 $final_content = $title . "\n\n" . $content;
3880
3881 // For custom post types like job_listing, include additional fields
3882 if ($post_type === 'job_listing') {
3883 // Add job-specific meta if available
3884 $job_location = get_post_meta($post_id, '_job_location', true);
3885 if (!empty($job_location)) {
3886 $final_content .= "\n\nLocation: " . $job_location;
3887 }
3888
3889 // Get job type terms
3890 $job_types = get_the_terms($post_id, 'job_listing_type');
3891 if (!empty($job_types) && !is_wp_error($job_types)) {
3892 $types = array();
3893 foreach ($job_types as $type) {
3894 $types[] = $type->name;
3895 }
3896 $final_content .= "\n\nJob Type: " . implode(', ', $types);
3897 }
3898
3899 // Get company name if available
3900 $company_name = get_post_meta($post_id, '_company_name', true);
3901 if (!empty($company_name)) {
3902 $final_content .= "\n\nCompany: " . $company_name;
3903 }
3904 }
3905
3906 // Get API key with proper model detection
3907 $options = get_option('mxchat_options');
3908 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3909
3910 if (strpos($selected_model, 'voyage') === 0) {
3911 $api_key = $options['voyage_api_key'] ?? '';
3912 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3913 $api_key = $options['gemini_api_key'] ?? '';
3914 } else {
3915 $api_key = $options['api_key'] ?? '';
3916 }
3917
3918 if (empty($api_key)) {
3919 return;
3920 }
3921
3922 // Use the centralized utility function for storage
3923 $result = MxChat_Utils::submit_content_to_db(
3924 $final_content,
3925 $source_url,
3926 $api_key,
3927 md5($source_url) // Vector ID for Pinecone
3928 );
3929 }
3930
3931 // Clean up the stored previous status if not used above
3932 if ($previous_status !== 'publish' || $post->post_status === 'publish') {
3933 delete_transient($previous_status_key);
3934 delete_transient($previous_url_key);
3935 }
3936 }
3937
3938 /**
3939 * Store the post status and URL before update to detect status transitions
3940 * This runs before the post is actually updated in the database
3941 */
3942 public function mxchat_store_pre_update_status($post_id, $data) {
3943 // Get the current post from database (before update)
3944 $current_post = get_post($post_id);
3945
3946 if ($current_post) {
3947 // Store the current status temporarily
3948 $status_key = 'mxchat_prev_status_' . $post_id;
3949 set_transient($status_key, $current_post->post_status, HOUR_IN_SECONDS);
3950
3951 // If the post is currently published, also store its URL
3952 if ($current_post->post_status === 'publish') {
3953 $url_key = 'mxchat_prev_url_' . $post_id;
3954 $current_url = get_permalink($post_id);
3955 set_transient($url_key, $current_url, HOUR_IN_SECONDS);
3956 }
3957 }
3958 }
3959
3960 public function mxchat_handle_post_delete($post_id) {
3961 // Get post data before it's deleted
3962 $post = get_post($post_id);
3963
3964 // Basic validation
3965 if (!$post || wp_is_post_revision($post_id)) {
3966 return;
3967 }
3968
3969 $post_type = $post->post_type;
3970
3971 // Check if sync is enabled for this post type
3972 $should_sync = false;
3973
3974 // Check built-in post types first
3975 if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
3976 $should_sync = true;
3977 } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
3978 $should_sync = true;
3979 } else {
3980 // Check custom post types
3981 $option_name = 'mxchat_auto_sync_' . $post_type;
3982 if (get_option($option_name) === '1') {
3983 $should_sync = true;
3984 }
3985 }
3986
3987 if (!$should_sync) {
3988 return;
3989 }
3990
3991 // Get the URL before post is deleted
3992 $source_url = get_permalink($post_id);
3993 if (!$source_url) {
3994 //error_log('MXChat: Failed to get permalink for post ' . $post_id);
3995 return;
3996 }
3997
3998 // Check if Pinecone is enabled
3999 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4000 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4001
4002 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
4003 // Delete from Pinecone
4004 $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
4005 } else {
4006 // Delete from WordPress DB
4007 global $wpdb;
4008 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4009
4010 $result = $wpdb->delete(
4011 $table_name,
4012 array('source_url' => $source_url),
4013 array('%s')
4014 );
4015
4016 if ($result === false) {
4017 //error_log('MXChat: WordPress DB deletion failed for URL: ' . $source_url);
4018 }
4019 }
4020 }
4021
4022
4023 /**
4024 * Deletes data from Pinecone using a source URL
4025 */
4026 public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
4027 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
4028 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
4029
4030 if (empty($host) || empty($api_key)) {
4031 //error_log('MXChat: Pinecone deletion failed - missing configuration');
4032 return false;
4033 }
4034
4035 $api_endpoint = "https://{$host}/vectors/delete";
4036 $vector_id = md5($source_url);
4037
4038 $request_body = array(
4039 'ids' => array($vector_id)
4040 );
4041
4042 $response = wp_remote_post($api_endpoint, array(
4043 'headers' => array(
4044 'Api-Key' => $api_key,
4045 'accept' => 'application/json',
4046 'content-type' => 'application/json'
4047 ),
4048 'body' => wp_json_encode($request_body),
4049 'timeout' => 30
4050 ));
4051
4052 if (is_wp_error($response)) {
4053 //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
4054 return false;
4055 }
4056
4057 $response_code = wp_remote_retrieve_response_code($response);
4058 if ($response_code !== 200) {
4059 //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
4060 return false;
4061 }
4062
4063 return true;
4064 }
4065
4066
4067
4068 public function mxchat_handle_product_change($post_id, $post, $update) {
4069 if ($post->post_type !== 'product') {
4070 return;
4071 }
4072
4073 if ($post->post_status === 'publish') {
4074 add_action('shutdown', function() use ($post_id) {
4075 $product = wc_get_product($post_id);
4076 if ($product) {
4077 $this->mxchat_store_product_embedding($product);
4078 }
4079 });
4080 }
4081 }
4082
4083 /**
4084 * Store WooCommerce product embeddings
4085 */
4086 private function mxchat_store_product_embedding($product) {
4087 if (!isset($this->options['enable_woocommerce_integration']) ||
4088 !in_array($this->options['enable_woocommerce_integration'], ['1', 'on'])) {
4089 return;
4090 }
4091
4092 $source_url = get_permalink($product->get_id());
4093
4094 // Build product content
4095 $title = $product->get_name();
4096 $description = $product->get_description();
4097 $short_description = $product->get_short_description();
4098 $regular_price = $product->get_regular_price();
4099 $sale_price = $product->get_sale_price();
4100 $sku = $product->get_sku();
4101
4102 // Format content consistently
4103 $content = $title . "\n\n";
4104
4105 if (!empty($description)) {
4106 $content .= wp_strip_all_tags($description) . "\n\n";
4107 }
4108
4109 if (!empty($short_description)) {
4110 $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
4111 }
4112
4113 $content .= "Price: $" . $regular_price . "\n";
4114
4115 if (!empty($sale_price)) {
4116 $content .= "Sale Price: $" . $sale_price . "\n";
4117 }
4118
4119 if (!empty($sku)) {
4120 $content .= "SKU: " . $sku . "\n";
4121 }
4122
4123 // Get API key with proper model detection
4124 $options = get_option('mxchat_options');
4125 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4126
4127 if (strpos($selected_model, 'voyage') === 0) {
4128 $api_key = $options['voyage_api_key'] ?? '';
4129 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
4130 $api_key = $options['gemini_api_key'] ?? '';
4131 } else {
4132 $api_key = $options['api_key'] ?? '';
4133 }
4134
4135 if (empty($api_key)) {
4136 //error_log('MxChat Auto-sync: No API key configured for embedding model');
4137 return;
4138 }
4139
4140 // Use the centralized utility function for storage
4141 $result = MxChat_Utils::submit_content_to_db(
4142 $content,
4143 $source_url,
4144 $api_key,
4145 md5($source_url) // Vector ID for Pinecone
4146 );
4147
4148 if (is_wp_error($result)) {
4149 //error_log('MxChat WooCommerce sync failed for product ' . $product->get_id() . ': ' . $result->get_error_message());
4150 }
4151 }
4152
4153 public function mxchat_handle_product_delete($post_id) {
4154 if (get_post_type($post_id) !== 'product') {
4155 return;
4156 }
4157
4158 $source_url = get_permalink($post_id);
4159
4160 // Check if Pinecone is enabled
4161 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4162 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4163
4164 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
4165 // Delete from Pinecone
4166 $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
4167 } else {
4168 // Delete from WordPress DB
4169 global $wpdb;
4170 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4171
4172 $wpdb->delete(
4173 $table_name,
4174 array('source_url' => $source_url),
4175 array('%s')
4176 );
4177 }
4178 }
4179
4180 /**
4181 * Handle individual Pinecone content deletion
4182 */
4183 public function mxchat_handle_pinecone_prompt_delete() {
4184 // Check permissions
4185 if (!current_user_can('manage_options')) {
4186 wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
4187 }
4188
4189 // Verify nonce
4190 if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'mxchat_delete_pinecone_prompt_nonce')) {
4191 wp_die(esc_html__('Security check failed.', 'mxchat'));
4192 }
4193
4194 $vector_id = isset($_GET['vector_id']) ? sanitize_text_field($_GET['vector_id']) : '';
4195
4196 if (empty($vector_id)) {
4197 set_transient('mxchat_admin_notice_error',
4198 esc_html__('Invalid vector ID.', 'mxchat'),
4199 30
4200 );
4201 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
4202 exit;
4203 }
4204
4205 // Get Pinecone settings
4206 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4207 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4208
4209 if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
4210 set_transient('mxchat_admin_notice_error',
4211 esc_html__('Pinecone is not properly configured.', 'mxchat'),
4212 30
4213 );
4214 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
4215 exit;
4216 }
4217
4218 // Delete from Pinecone
4219 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
4220 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
4221 $vector_id,
4222 $pinecone_options['mxchat_pinecone_api_key'],
4223 $pinecone_options['mxchat_pinecone_host']
4224 );
4225
4226 if ($result['success']) {
4227 // No cache clearing needed since we removed caching
4228 set_transient('mxchat_admin_notice_success',
4229 esc_html__('Entry deleted successfully from Pinecone.', 'mxchat'),
4230 30
4231 );
4232 } else {
4233 set_transient('mxchat_admin_notice_error',
4234 esc_html__('Failed to delete entry: ', 'mxchat') . $result['message'],
4235 30
4236 );
4237 }
4238
4239 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
4240 exit;
4241 }
4242 /**
4243 * Handle individual Pinecone content deletion via AJAX
4244 */
4245 public function ajax_mxchat_delete_pinecone_prompt() {
4246 // Verify nonce and permissions
4247 if (!check_ajax_referer('mxchat_delete_pinecone_prompt_nonce', 'nonce', false)) {
4248 wp_send_json_error('Invalid nonce');
4249 exit;
4250 }
4251
4252 if (!current_user_can('manage_options')) {
4253 wp_send_json_error('Unauthorized access');
4254 exit;
4255 }
4256
4257 $vector_id = isset($_POST['vector_id']) ? sanitize_text_field($_POST['vector_id']) : '';
4258 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
4259
4260 if (empty($vector_id)) {
4261 wp_send_json_error('Missing vector ID');
4262 exit;
4263 }
4264
4265 // Get bot-specific Pinecone settings
4266 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
4267 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
4268
4269 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4270
4271 if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
4272 wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
4273 exit;
4274 }
4275
4276 // Delete from the correct Pinecone index
4277 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
4278 $vector_id,
4279 $pinecone_options['mxchat_pinecone_api_key'],
4280 $pinecone_options['mxchat_pinecone_host']
4281 );
4282
4283 if ($result['success']) {
4284 // No cache clearing needed since we removed caching
4285 wp_send_json_success(array(
4286 'message' => 'Entry deleted successfully from Pinecone',
4287 'vector_id' => $vector_id,
4288 'bot_id' => $bot_id
4289 ));
4290 } else {
4291 wp_send_json_error('Failed to delete from Pinecone: ' . $result['message']);
4292 }
4293
4294 exit;
4295 }
4296
4297 /**
4298 * NEW: Get hierarchical roles for dropdown
4299 */
4300 public function mxchat_get_role_options() {
4301 return array(
4302 'public' => __('Public (Everyone)', 'mxchat'),
4303 'logged_in' => __('Logged In Users', 'mxchat'),
4304 'subscriber' => __('Subscribers & Above', 'mxchat'),
4305 'contributor' => __('Contributors & Above', 'mxchat'),
4306 'author' => __('Authors & Above', 'mxchat'),
4307 'editor' => __('Editors & Above', 'mxchat'),
4308 'administrator' => __('Administrators Only', 'mxchat')
4309 );
4310 }
4311
4312 /**
4313 * NEW: Check if user has access to content based on role restriction
4314 */
4315 public function mxchat_user_has_content_access($role_restriction) {
4316 // Public content is always accessible
4317 if ($role_restriction === 'public' || empty($role_restriction)) {
4318 return true;
4319 }
4320
4321 // Check if user is logged in for logged_in restriction
4322 if ($role_restriction === 'logged_in') {
4323 return is_user_logged_in();
4324 }
4325
4326 // If not logged in, no access to role-restricted content
4327 if (!is_user_logged_in()) {
4328 return false;
4329 }
4330
4331 $user = wp_get_current_user();
4332 $user_roles = $user->roles;
4333
4334 if (empty($user_roles)) {
4335 return false;
4336 }
4337
4338 // Define role hierarchy (higher number = higher access)
4339 $hierarchy = array(
4340 'subscriber' => 1,
4341 'contributor' => 2,
4342 'author' => 3,
4343 'editor' => 4,
4344 'administrator' => 5
4345 );
4346
4347 // Get required level
4348 $required_level = isset($hierarchy[$role_restriction]) ? $hierarchy[$role_restriction] : 0;
4349
4350 // Check if user has required level or higher
4351 foreach ($user_roles as $user_role) {
4352 $user_level = isset($hierarchy[$user_role]) ? $hierarchy[$user_role] : 0;
4353 if ($user_level >= $required_level) {
4354 return true;
4355 }
4356 }
4357
4358 return false;
4359 }
4360
4361 /**
4362 * Handle role restriction updates via AJAX
4363 * UPDATED: Removed cache clearing call since we removed caching
4364 */
4365 public function ajax_mxchat_update_role_restriction() {
4366 // Verify nonce and permissions
4367 if (!check_ajax_referer('mxchat_update_role_nonce', 'nonce', false)) {
4368 wp_send_json_error('Invalid nonce');
4369 exit;
4370 }
4371
4372 if (!current_user_can('manage_options')) {
4373 wp_send_json_error('Unauthorized access');
4374 exit;
4375 }
4376
4377 $entry_id = isset($_POST['entry_id']) ? sanitize_text_field($_POST['entry_id']) : '';
4378 $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
4379 $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
4380
4381 if (empty($entry_id)) {
4382 wp_send_json_error('Invalid entry ID');
4383 exit;
4384 }
4385
4386 // Get knowledge manager instance to validate role restriction
4387 $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
4388 $valid_roles = array_keys($knowledge_manager->mxchat_get_role_options());
4389 if (!in_array($role_restriction, $valid_roles)) {
4390 wp_send_json_error('Invalid role restriction');
4391 exit;
4392 }
4393
4394 global $wpdb;
4395
4396 if ($data_source === 'pinecone') {
4397 // Handle Pinecone role restriction (stored separately in WordPress table)
4398 $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
4399
4400 // Use REPLACE to insert or update the role restriction
4401 $result = $wpdb->replace(
4402 $roles_table,
4403 array(
4404 'vector_id' => $entry_id,
4405 'role_restriction' => $role_restriction,
4406 'updated_at' => current_time('mysql')
4407 ),
4408 array('%s', '%s', '%s')
4409 );
4410
4411 // No cache clearing needed since we removed caching
4412
4413 } else {
4414 // Handle WordPress database role restriction (existing functionality)
4415 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4416
4417 $result = $wpdb->update(
4418 $table_name,
4419 array('role_restriction' => $role_restriction),
4420 array('id' => absint($entry_id)),
4421 array('%s'),
4422 array('%d')
4423 );
4424 }
4425
4426 if ($result === false) {
4427 wp_send_json_error('Database update failed: ' . $wpdb->last_error);
4428 exit;
4429 }
4430
4431 wp_send_json_success(array(
4432 'message' => 'Role restriction updated successfully',
4433 'role_restriction' => $role_restriction,
4434 'data_source' => $data_source,
4435 'entry_id' => $entry_id
4436 ));
4437 exit;
4438 }
4439
4440 // ========================================
4441 // HELPER METHODS
4442 // ========================================
4443
4444 /**
4445 * Check if user has required permissions for content processing
4446 */
4447 private function mxchat_check_user_permissions() {
4448 if (!current_user_can('manage_options')) {
4449 wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
4450 }
4451 }
4452
4453 /**
4454 * Validate nonce for security
4455 */
4456 private function mxchat_validate_nonce($nonce_name, $nonce_action) {
4457 if (!isset($_POST[$nonce_name]) || !wp_verify_nonce($_POST[$nonce_name], $nonce_action)) {
4458 wp_die(esc_html__('Security check failed.', 'mxchat'));
4459 }
4460 }
4461
4462 /**
4463 * Get embedding API credentials
4464 */
4465 private function mxchat_get_embedding_credentials() {
4466 $embedding_model = $this->options['embedding_model'] ?? 'text-embedding-ada-002';
4467
4468 if (strpos($embedding_model, 'text-embedding-') !== false) {
4469 return array(
4470 'type' => 'openai',
4471 'api_key' => $this->options['api_key'] ?? ''
4472 );
4473 } elseif (strpos($embedding_model, 'voyage-') !== false) {
4474 return array(
4475 'type' => 'voyage',
4476 'api_key' => $this->options['voyage_api_key'] ?? ''
4477 );
4478 } elseif (strpos($embedding_model, 'gemini-embedding-') !== false) {
4479 return array(
4480 'type' => 'gemini',
4481 'api_key' => $this->options['gemini_api_key'] ?? ''
4482 );
4483 }
4484
4485 return array('type' => 'unknown', 'api_key' => '');
4486 }
4487
4488 /**
4489 * Log processing errors
4490 */
4491 private function mxchat_log_processing_error($operation, $error_message) {
4492 //error_log("MxChat Knowledge Processing {$operation} Error: " . $error_message);
4493 }
4494
4495 /**
4496 * Set admin notice transient
4497 */
4498 private function mxchat_set_admin_notice($type, $message) {
4499 set_transient("mxchat_admin_notice_{$type}", $message, 30);
4500 }
4501
4502 /**
4503 * Get Pinecone manager instance for vector operations
4504 */
4505 private function mxchat_get_pinecone_manager() {
4506 return MxChat_Pinecone_Manager::get_instance();
4507 }
4508
4509 // ========================================
4510 // STATIC ACCESS METHODS
4511 // ========================================
4512
4513 /**
4514 * Get singleton instance
4515 */
4516 public static function get_instance() {
4517 static $instance = null;
4518 if ($instance === null) {
4519 $instance = new self();
4520 }
4521 return $instance;
4522 }
4523 }
4524
4525 // Initialize the Knowledge manager
4526 $mxchat_knowledge_manager = MxChat_Knowledge_Manager::get_instance();