PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.4.3
MxChat – AI Chatbot & Content Generation for WordPress v2.4.3
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / admin / class-knowledge-manager.php

class-knowledge-manager.php in MxChat – AI Chatbot & Content Generation for WordPress 2.4.3, at admin/class-knowledge-manager.php

4,580 lines 179.2 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 /**
2200 * checking if WooCommerce products were already processed in the WordPress database.
2201 */
2202 public function ajax_mxchat_get_content_list() {
2203 // Verify the nonce
2204 check_ajax_referer('mxchat_content_selector_nonce', 'nonce');
2205
2206 if (!current_user_can('manage_options')) {
2207 wp_send_json_error(__('Unauthorized access', 'mxchat'));
2208 }
2209
2210 $page = isset($_GET['page']) ? absint($_GET['page']) : 1;
2211 $per_page = isset($_GET['per_page']) ? absint($_GET['per_page']) : 50;
2212 $search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
2213 $post_type = isset($_GET['post_type']) ? sanitize_text_field($_GET['post_type']) : 'all';
2214 $post_status = isset($_GET['post_status']) ? sanitize_text_field($_GET['post_status']) : 'publish';
2215 $processed_filter = isset($_GET['processed_filter']) ? sanitize_text_field($_GET['processed_filter']) : 'all';
2216
2217 // Build query args
2218 $args = array(
2219 'posts_per_page' => $per_page,
2220 'paged' => $page,
2221 'post_status' => $post_status !== 'all' ? $post_status : array('publish', 'draft', 'pending'),
2222 'orderby' => 'date',
2223 'order' => 'DESC',
2224 );
2225
2226 // Handle post types
2227 if ($post_type !== 'all') {
2228 $args['post_type'] = $post_type;
2229 } else {
2230 // Default to post and page if we can't get post types
2231 $args['post_type'] = array('post', 'page');
2232
2233 // Try to get public post types
2234 $public_types = $this->mxchat_get_public_post_types();
2235 if (is_array($public_types) && !empty($public_types)) {
2236 $args['post_type'] = array_keys($public_types);
2237 }
2238 }
2239
2240 if (!empty($search)) {
2241 $args['s'] = $search;
2242 }
2243
2244 // ================================
2245 // WordPress DB checking for WooCommerce products
2246 // ================================
2247
2248 $processed_data = array();
2249
2250 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
2251 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
2252 $pinecone_manager->mxchat_refresh_after_new_content($pinecone_options);
2253 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2254
2255 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
2256 // ONLY check Pinecone if it's enabled
2257 $processed_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
2258 } else {
2259 // IMPROVED: WordPress DB checking with better URL matching for WooCommerce
2260 global $wpdb;
2261 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2262 $processed_items = $wpdb->get_results("SELECT id, source_url, timestamp FROM {$table_name}");
2263
2264 if (!empty($processed_items)) {
2265 foreach ($processed_items as $item) {
2266 // FIXED: Use improved URL matching for WooCommerce products
2267 $post_id = $this->mxchat_url_to_post_id_improved($item->source_url);
2268
2269 if ($post_id) {
2270 $processed_data[$post_id] = array(
2271 'db_id' => $item->id,
2272 'timestamp' => $item->timestamp,
2273 'url' => $item->source_url,
2274 'source' => 'wordpress'
2275 );
2276 }
2277 }
2278 }
2279 }
2280
2281 // ================================
2282
2283 // Get processed IDs as a simple array for in_array checks
2284 $processed_ids = array_keys($processed_data);
2285
2286 // Handle processed/unprocessed filter
2287 if ($processed_filter === 'processed' && !empty($processed_ids)) {
2288 $args['post__in'] = $processed_ids;
2289 } elseif ($processed_filter === 'unprocessed' && !empty($processed_ids)) {
2290 $args['post__not_in'] = $processed_ids;
2291 }
2292
2293 // Run the query
2294 $query = new WP_Query($args);
2295 $content_items = array();
2296
2297 if ($query->have_posts()) {
2298 while ($query->have_posts()) {
2299 $query->the_post();
2300 $id = get_the_ID();
2301 $post_date = get_the_date();
2302 $excerpt = wp_trim_words(get_the_excerpt(), 20, '...');
2303 $word_count = str_word_count(strip_tags(get_the_content()));
2304
2305 $is_processed = in_array($id, $processed_ids);
2306 $processed_date = '';
2307 $db_record_id = 0;
2308 $data_source = 'none';
2309
2310 if ($is_processed && isset($processed_data[$id])) {
2311 $item_data = $processed_data[$id];
2312 $data_source = $item_data['source'];
2313
2314 if ($data_source === 'wordpress' && isset($item_data['timestamp'])) {
2315 // WordPress DB format
2316 $timestamp = strtotime($item_data['timestamp']);
2317 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2318 $db_record_id = $item_data['db_id'];
2319 } elseif ($data_source === 'pinecone') {
2320 // Pinecone format
2321 $processed_date = $item_data['processed_date'];
2322 $db_record_id = $item_data['db_id'];
2323 }
2324 }
2325
2326 $content_items[] = array(
2327 'id' => $id,
2328 'title' => get_the_title(),
2329 'permalink' => get_permalink(),
2330 'date' => $post_date,
2331 'type' => get_post_type(),
2332 'status' => get_post_status(),
2333 'excerpt' => $excerpt,
2334 'word_count' => $word_count,
2335 'already_processed' => $is_processed,
2336 'processed_date' => $processed_date,
2337 'db_record_id' => $db_record_id,
2338 'data_source' => $data_source
2339 );
2340 }
2341 wp_reset_postdata();
2342 }
2343
2344 $response = array(
2345 'items' => $content_items,
2346 'total' => $query->found_posts,
2347 'total_pages' => $query->max_num_pages,
2348 'current_page' => $page,
2349 'processed_count' => count($processed_ids)
2350 );
2351
2352 wp_send_json_success($response);
2353 exit;
2354 }
2355
2356 /**
2357 * This function handles various WooCommerce URL formats and permalink structures
2358 */
2359 private function mxchat_url_to_post_id_improved($url) {
2360 // First try the standard WordPress function
2361 $post_id = url_to_postid($url);
2362
2363 if ($post_id > 0) {
2364 return $post_id;
2365 }
2366
2367 // If that fails, try more aggressive URL matching for WooCommerce products
2368 // Remove trailing slashes and query parameters for better matching
2369 $clean_url = rtrim($url, '/');
2370 $clean_url = strtok($clean_url, '?'); // Remove query parameters
2371
2372 // Try again with cleaned URL
2373 $post_id = url_to_postid($clean_url);
2374 if ($post_id > 0) {
2375 return $post_id;
2376 }
2377
2378 // For WooCommerce products, try extracting slug from URL
2379 if (strpos($url, '/product/') !== false || strpos($url, 'product=') !== false) {
2380 // Extract product slug from various URL formats
2381 $product_slug = '';
2382
2383 // Handle pretty permalinks: /product/product-name/
2384 if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
2385 $product_slug = $matches[1];
2386 }
2387 // Handle query parameters: ?product=product-name
2388 elseif (preg_match('/[\?&]product=([^&]+)/', $url, $matches)) {
2389 $product_slug = $matches[1];
2390 }
2391
2392 if (!empty($product_slug)) {
2393 // Look up product by slug
2394 $product = get_page_by_path($product_slug, OBJECT, 'product');
2395 if ($product) {
2396 return $product->ID;
2397 }
2398
2399 // Alternative method: query by post_name
2400 global $wpdb;
2401 $post_id = $wpdb->get_var($wpdb->prepare(
2402 "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'product' AND post_status = 'publish'",
2403 $product_slug
2404 ));
2405
2406 if ($post_id) {
2407 return intval($post_id);
2408 }
2409 }
2410 }
2411
2412 // ADDITIONAL FIX: Try direct database lookup by URL variations
2413 global $wpdb;
2414 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2415
2416 // Try exact match first
2417 $existing_record = $wpdb->get_row($wpdb->prepare(
2418 "SELECT id, source_url FROM $table_name WHERE source_url = %s",
2419 $url
2420 ));
2421
2422 if ($existing_record) {
2423 // Found exact match, now convert the URL to post ID
2424 $post_id = url_to_postid($existing_record->source_url);
2425 if ($post_id > 0) {
2426 return $post_id;
2427 }
2428 }
2429
2430 // Try variations of the URL (with/without trailing slash, http/https)
2431 $url_variations = array(
2432 rtrim($url, '/'),
2433 $url . '/',
2434 str_replace('http://', 'https://', $url),
2435 str_replace('https://', 'http://', $url),
2436 str_replace('http://', 'https://', rtrim($url, '/')),
2437 str_replace('https://', 'http://', rtrim($url, '/'))
2438 );
2439
2440 foreach ($url_variations as $variation) {
2441 $existing_record = $wpdb->get_row($wpdb->prepare(
2442 "SELECT id, source_url FROM $table_name WHERE source_url = %s",
2443 $variation
2444 ));
2445
2446 if ($existing_record) {
2447 $post_id = url_to_postid($existing_record->source_url);
2448 if ($post_id > 0) {
2449 return $post_id;
2450 }
2451 }
2452 }
2453
2454 // Last resort: try to match against all published products by URL if WooCommerce is active
2455 if (function_exists('wc_get_products')) {
2456 // Get all published products (limited to avoid memory issues)
2457 $products = wc_get_products(array(
2458 'status' => 'publish',
2459 'limit' => 1000, // Reasonable limit
2460 'return' => 'ids'
2461 ));
2462
2463 foreach ($products as $product_id) {
2464 $product_url = get_permalink($product_id);
2465
2466 // Compare cleaned URLs
2467 $clean_product_url = rtrim($product_url, '/');
2468 $clean_product_url = strtok($clean_product_url, '?');
2469
2470 if ($clean_url === $clean_product_url) {
2471 return $product_id;
2472 }
2473
2474 // Also check if any of our URL variations match
2475 foreach ($url_variations as $variation) {
2476 $clean_variation = rtrim($variation, '/');
2477 $clean_variation = strtok($clean_variation, '?');
2478
2479 if ($clean_variation === $clean_product_url) {
2480 return $product_id;
2481 }
2482 }
2483 }
2484 }
2485
2486 return 0; // No match found
2487 }
2488
2489 public function ajax_mxchat_process_selected_content() {
2490 // Basic request validation
2491 if (!check_ajax_referer('mxchat_content_selector_nonce', 'nonce', false)) {
2492 wp_send_json_error('Invalid nonce');
2493 exit;
2494 }
2495
2496 if (!current_user_can('manage_options')) {
2497 wp_send_json_error('Unauthorized access');
2498 exit;
2499 }
2500
2501 // Get post IDs - safely parse the array
2502 $post_ids = array();
2503 if (isset($_POST['post_ids']) && is_array($_POST['post_ids'])) {
2504 foreach ($_POST['post_ids'] as $id) {
2505 $post_ids[] = absint($id);
2506 }
2507 }
2508
2509 if (empty($post_ids)) {
2510 wp_send_json_error('No content selected');
2511 exit;
2512 }
2513
2514 // UPDATED: Get bot_id from request
2515 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
2516
2517 // Process only ONE post at a time to avoid request size issues
2518 $post_id = reset($post_ids);
2519 $post = get_post($post_id);
2520
2521 if (!$post) {
2522 wp_send_json_error('Post not found');
2523 exit;
2524 }
2525
2526 // Get content including ACF fields
2527 $content = $post->post_title . "\n\n" . wp_strip_all_tags($post->post_content);
2528
2529 // ADD ACF FIELDS SUPPORT
2530 $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
2531 if (!empty($acf_fields)) {
2532 $acf_content_parts = array();
2533
2534 foreach ($acf_fields as $field_name => $field_value) {
2535 $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
2536
2537 if (!empty($formatted_value)) {
2538 $field_label = ucwords(str_replace('_', ' ', $field_name));
2539 $acf_content_parts[] = $field_label . ": " . $formatted_value;
2540 }
2541 }
2542
2543 if (!empty($acf_content_parts)) {
2544 $content .= "\n\n" . implode("\n", $acf_content_parts);
2545 }
2546 }
2547
2548 $content = substr($content, 0, 10000); // Limit content size
2549
2550 // UPDATED: Get bot-specific API key
2551 $bot_options = $this->get_bot_options($bot_id);
2552 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
2553 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
2554
2555 if (strpos($selected_model, 'voyage') === 0) {
2556 $api_key = $options['voyage_api_key'] ?? '';
2557 $provider_name = 'Voyage AI';
2558 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2559 $api_key = $options['gemini_api_key'] ?? '';
2560 $provider_name = 'Google Gemini';
2561 } else {
2562 $api_key = $options['api_key'] ?? '';
2563 $provider_name = 'OpenAI';
2564 }
2565
2566 if (empty($api_key)) {
2567 wp_send_json_error($provider_name . ' API key not configured');
2568 exit;
2569 }
2570
2571 $source_url = get_permalink($post_id);
2572 $vector_id = md5($source_url); // Vector ID for Pinecone
2573
2574 // UPDATED: Check for existing content in bot-specific storage
2575 $is_update = false;
2576
2577 // Get bot-specific Pinecone configuration
2578 $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
2579 $use_pinecone = !empty($bot_pinecone_config) && ($bot_pinecone_config['use_pinecone'] ?? false);
2580
2581 if ($use_pinecone && !empty($bot_pinecone_config['api_key'])) {
2582 // Check Pinecone for this bot
2583 $pinecone_data = $this->mxchat_get_pinecone_processed_content($bot_pinecone_config);
2584 if (isset($pinecone_data[$post_id])) {
2585 $is_update = true;
2586 }
2587 } else {
2588 // Check WordPress DB (same as before since it's shared)
2589 global $wpdb;
2590 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2591 $existing_record = $wpdb->get_row($wpdb->prepare(
2592 "SELECT id FROM $table_name WHERE source_url = %s",
2593 $source_url
2594 ));
2595
2596 if ($existing_record) {
2597 $is_update = true;
2598 }
2599 }
2600
2601 // UPDATED: Use the centralized utility function with bot_id
2602 $result = MxChat_Utils::submit_content_to_db(
2603 $content,
2604 $source_url,
2605 $api_key,
2606 $vector_id,
2607 $bot_id
2608 );
2609
2610 if (is_wp_error($result)) {
2611 wp_send_json_error('Storage failed: ' . $result->get_error_message());
2612 exit;
2613 }
2614
2615 // Update caches if Pinecone is enabled for this bot
2616 if ($use_pinecone && !empty($bot_pinecone_config['api_key'])) {
2617 // Update vector ID cache for improved fetching
2618 $this->mxchat_update_pinecone_vector_cache($vector_id);
2619
2620 // Update local processed content cache for immediate UI feedback
2621 $pinecone_cache = get_option('mxchat_pinecone_processed_cache', array());
2622 $pinecone_cache[$post_id] = array(
2623 'db_id' => $vector_id,
2624 'processed_date' => 'Just now',
2625 'url' => $source_url,
2626 'source' => 'pinecone',
2627 'timestamp' => current_time('timestamp')
2628 );
2629 update_option('mxchat_pinecone_processed_cache', $pinecone_cache);
2630
2631 // Also update the general processed content cache
2632 $processed_cache = get_option('mxchat_processed_content_cache', array());
2633 $processed_cache[$post_id] = array(
2634 'db_id' => $vector_id,
2635 'timestamp' => current_time('timestamp'),
2636 'url' => $source_url,
2637 'source' => 'pinecone'
2638 );
2639 update_option('mxchat_processed_content_cache', $processed_cache);
2640 }
2641
2642 $operation_type = $is_update ? 'update' : 'new';
2643
2644 // Count ACF fields for debugging
2645 $acf_field_count = count($acf_fields);
2646
2647 // Success response with minimal data
2648 wp_send_json_success(array(
2649 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully',
2650 'post_id' => $post_id,
2651 'title' => $post->post_title,
2652 'operation_type' => $operation_type,
2653 'vector_id' => $vector_id,
2654 'cache_updated' => $use_pinecone,
2655 'acf_fields_found' => $acf_field_count,
2656 'content_preview' => substr($content, 0, 100) . '...',
2657 'bot_id' => $bot_id
2658 ));
2659 exit;
2660 }
2661
2662
2663
2664
2665 /**
2666 * Updates cache with new vector ID if absent
2667 */
2668 public function mxchat_update_pinecone_vector_cache($vector_id) {
2669 $cached_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
2670 if (!in_array($vector_id, $cached_ids)) {
2671 $cached_ids[] = $vector_id;
2672 update_option('mxchat_pinecone_vector_ids_cache', $cached_ids);
2673 }
2674 }
2675 public function mxchat_get_public_post_types() {
2676 $post_types = get_post_types(array('public' => true), 'objects');
2677 $post_type_options = array();
2678
2679 foreach ($post_types as $post_type) {
2680 $post_type_options[$post_type->name] = $post_type->label;
2681 }
2682
2683 return $post_type_options;
2684 }
2685 public function mxchat_get_pinecone_processed_content($pinecone_options) {
2686 //error_log('=== DEBUG: Starting mxchat_get_pinecone_processed_content ===');
2687
2688 // First check local cache for immediate updates
2689 $cached_data = get_option('mxchat_pinecone_processed_cache', array());
2690 //error_log('DEBUG: Found ' . count($cached_data) . ' items in local cache');
2691
2692 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2693 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2694
2695 //error_log('DEBUG: API key present: ' . (!empty($api_key) ? 'YES' : 'NO'));
2696 //error_log('DEBUG: Host: ' . $host);
2697
2698 if (empty($api_key) || empty($host)) {
2699 //error_log('DEBUG: Missing API credentials, returning cached data only');
2700 return $cached_data;
2701 }
2702
2703 $pinecone_data = array();
2704
2705 try {
2706 // Method 1: Try to get vectors using cached vector IDs first
2707 $cached_vector_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
2708 //error_log('DEBUG: Found ' . count($cached_vector_ids) . ' cached vector IDs');
2709
2710 if (!empty($cached_vector_ids)) {
2711 //error_log('DEBUG: Trying to fetch by cached vector IDs...');
2712 $pinecone_data = $this->mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $cached_vector_ids);
2713 //error_log('DEBUG: Fetch by IDs returned ' . count($pinecone_data) . ' items');
2714 }
2715
2716 // Method 2: If no cached IDs or fetch failed, use scanning approach
2717 if (empty($pinecone_data)) {
2718 //error_log('DEBUG: Trying scanning approach...');
2719 $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
2720 //error_log('DEBUG: Scanning returned ' . count($pinecone_data) . ' items');
2721 }
2722
2723 // Method 3: Final fallback - try stats endpoint
2724 if (empty($pinecone_data)) {
2725 //error_log('DEBUG: Trying stats endpoint...');
2726 $stats_url = "https://{$host}/describe_index_stats";
2727
2728 $response = wp_remote_post($stats_url, array(
2729 'headers' => array(
2730 'Api-Key' => $api_key,
2731 'Content-Type' => 'application/json'
2732 ),
2733 'body' => json_encode(array()),
2734 'timeout' => 30
2735 ));
2736
2737 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2738 $body = wp_remote_retrieve_body($response);
2739 $stats_data = json_decode($body, true);
2740 //error_log('DEBUG: Pinecone stats: ' . print_r($stats_data, true));
2741 } else {
2742 if (is_wp_error($response)) {
2743 //error_log('DEBUG: Stats endpoint error: ' . $response->get_error_message());
2744 } else {
2745 //error_log('DEBUG: Stats endpoint failed with code: ' . wp_remote_retrieve_response_code($response));
2746 }
2747 }
2748 }
2749
2750 } catch (Exception $e) {
2751 //error_log('DEBUG: Exception in get_pinecone_processed_content: ' . $e->getMessage());
2752 }
2753
2754 // Merge cached data with Pinecone data
2755 $merged_data = $pinecone_data;
2756
2757 foreach ($cached_data as $post_id => $cache_item) {
2758 $cache_timestamp = $cache_item['timestamp'] ?? 0;
2759 $time_diff = current_time('timestamp') - $cache_timestamp;
2760
2761 if ($time_diff < 300) { // 5 minutes = 300 seconds
2762 $merged_data[$post_id] = $cache_item;
2763 } else {
2764 if (!isset($merged_data[$post_id])) {
2765 $merged_data[$post_id] = $cache_item;
2766 }
2767 }
2768 }
2769
2770 //error_log('DEBUG: Final merged data count: ' . count($merged_data));
2771 //error_log('=== DEBUG: End mxchat_get_pinecone_processed_content ===');
2772
2773 return $merged_data;
2774 }
2775
2776 public function mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
2777 //error_log('=== DEBUG: Starting mxchat_fetch_pinecone_vectors_by_ids ===');
2778
2779 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2780 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2781
2782 if (empty($api_key) || empty($host) || empty($vector_ids)) {
2783 //error_log('DEBUG: Missing parameters for fetch by IDs');
2784 return array();
2785 }
2786
2787 try {
2788 $fetch_url = "https://{$host}/vectors/fetch";
2789 //error_log('DEBUG: Fetch URL: ' . $fetch_url);
2790 //error_log('DEBUG: Fetching ' . count($vector_ids) . ' vector IDs');
2791
2792 // Pinecone fetch API allows fetching specific vectors by ID
2793 $fetch_data = array(
2794 'ids' => array_values($vector_ids)
2795 );
2796
2797 $response = wp_remote_post($fetch_url, array(
2798 'headers' => array(
2799 'Api-Key' => $api_key,
2800 'Content-Type' => 'application/json'
2801 ),
2802 'body' => json_encode($fetch_data),
2803 'timeout' => 30
2804 ));
2805
2806 if (is_wp_error($response)) {
2807 //error_log('DEBUG: Fetch by IDs WP error: ' . $response->get_error_message());
2808 return array();
2809 }
2810
2811 $response_code = wp_remote_retrieve_response_code($response);
2812 //error_log('DEBUG: Fetch response code: ' . $response_code);
2813
2814 if ($response_code !== 200) {
2815 $error_body = wp_remote_retrieve_body($response);
2816 //error_log('DEBUG: Fetch failed with body: ' . $error_body);
2817 return array();
2818 }
2819
2820 $body = wp_remote_retrieve_body($response);
2821 $data = json_decode($body, true);
2822
2823 //error_log('DEBUG: Fetch response structure: ' . print_r(array_keys($data), true));
2824
2825 if (!isset($data['vectors'])) {
2826 //error_log('DEBUG: No vectors key in response');
2827 return array();
2828 }
2829
2830 $processed_data = array();
2831
2832 foreach ($data['vectors'] as $vector_id => $vector_data) {
2833 $metadata = $vector_data['metadata'] ?? array();
2834 $source_url = $metadata['source_url'] ?? '';
2835
2836 if (!empty($source_url)) {
2837 $post_id = url_to_postid($source_url);
2838 if ($post_id) {
2839 $created_at = $metadata['created_at'] ?? '';
2840 $processed_date = 'Recently';
2841
2842 if (!empty($created_at)) {
2843 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
2844 if ($timestamp) {
2845 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2846 }
2847 }
2848
2849 $processed_data[$post_id] = array(
2850 'db_id' => $vector_id,
2851 'processed_date' => $processed_date,
2852 'url' => $source_url,
2853 'source' => 'pinecone',
2854 'timestamp' => $timestamp ?? current_time('timestamp')
2855 );
2856 }
2857 }
2858 }
2859
2860 //error_log('DEBUG: Processed ' . count($processed_data) . ' vectors from fetch');
2861 return $processed_data;
2862
2863 } catch (Exception $e) {
2864 //error_log('DEBUG: Exception in fetch_pinecone_vectors_by_ids: ' . $e->getMessage());
2865 return array();
2866 }
2867 }
2868
2869 public function mxchat_scan_pinecone_for_processed_content($pinecone_options) {
2870 //error_log('=== DEBUG: Starting mxchat_scan_pinecone_for_processed_content ===');
2871
2872 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2873 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2874
2875 if (empty($api_key) || empty($host)) {
2876 //error_log('DEBUG: Missing API credentials for scanning');
2877 return array();
2878 }
2879
2880 try {
2881 // Use multiple random vectors to get better coverage
2882 $all_matches = array();
2883 $seen_ids = array();
2884
2885 // Try 3 different random vectors to get better coverage
2886 for ($i = 0; $i < 3; $i++) {
2887 //error_log('DEBUG: Scanning attempt ' . ($i + 1) . '/3');
2888
2889 $query_url = "https://{$host}/query";
2890
2891 // Generate a random unit vector instead of zeros
2892 $random_vector = array();
2893 for ($j = 0; $j < 1536; $j++) {
2894 $random_vector[] = (rand(-1000, 1000) / 1000.0);
2895 }
2896
2897 // Normalize the vector to unit length
2898 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
2899 if ($magnitude > 0) {
2900 $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
2901 }
2902
2903 $query_data = array(
2904 'includeMetadata' => true,
2905 'includeValues' => false,
2906 'topK' => 10000,
2907 'vector' => $random_vector
2908 );
2909
2910 $response = wp_remote_post($query_url, array(
2911 'headers' => array(
2912 'Api-Key' => $api_key,
2913 'Content-Type' => 'application/json'
2914 ),
2915 'body' => json_encode($query_data),
2916 'timeout' => 30
2917 ));
2918
2919 if (is_wp_error($response)) {
2920 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' WP error: ' . $response->get_error_message());
2921 continue;
2922 }
2923
2924 $response_code = wp_remote_retrieve_response_code($response);
2925 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' response code: ' . $response_code);
2926
2927 if ($response_code !== 200) {
2928 $error_body = wp_remote_retrieve_body($response);
2929 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' failed with body: ' . substr($error_body, 0, 500));
2930 continue;
2931 }
2932
2933 $body = wp_remote_retrieve_body($response);
2934 $data = json_decode($body, true);
2935
2936 if (isset($data['matches'])) {
2937 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' returned ' . count($data['matches']) . ' matches');
2938 foreach ($data['matches'] as $match) {
2939 $match_id = $match['id'] ?? '';
2940 if (!empty($match_id) && !isset($seen_ids[$match_id])) {
2941 $all_matches[] = $match;
2942 $seen_ids[$match_id] = true;
2943 }
2944 }
2945 } else {
2946 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' - no matches key in response');
2947 }
2948 }
2949
2950 //error_log('DEBUG: Total unique matches found: ' . count($all_matches));
2951
2952 // Convert matches to processed data format
2953 $processed_data = array();
2954 $vector_ids_for_cache = array();
2955
2956 foreach ($all_matches as $match) {
2957 $metadata = $match['metadata'] ?? array();
2958 $source_url = $metadata['source_url'] ?? '';
2959 $match_id = $match['id'] ?? '';
2960
2961 if (!empty($source_url) && !empty($match_id)) {
2962 $post_id = url_to_postid($source_url);
2963 if ($post_id) {
2964 $created_at = $metadata['created_at'] ?? '';
2965 $processed_date = 'Recently';
2966
2967 if (!empty($created_at)) {
2968 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
2969 if ($timestamp) {
2970 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2971 }
2972 }
2973
2974 $processed_data[$post_id] = array(
2975 'db_id' => $match_id,
2976 'processed_date' => $processed_date,
2977 'url' => $source_url,
2978 'source' => 'pinecone',
2979 'timestamp' => $timestamp ?? current_time('timestamp')
2980 );
2981
2982 $vector_ids_for_cache[] = $match_id;
2983 }
2984 }
2985 }
2986
2987 // Update the vector IDs cache for future use
2988 if (!empty($vector_ids_for_cache)) {
2989 update_option('mxchat_pinecone_vector_ids_cache', $vector_ids_for_cache);
2990 //error_log('DEBUG: Updated vector IDs cache with ' . count($vector_ids_for_cache) . ' IDs');
2991 }
2992
2993 //error_log('DEBUG: Returning ' . count($processed_data) . ' processed items from scanning');
2994 return $processed_data;
2995
2996 } catch (Exception $e) {
2997 //error_log('DEBUG: Exception in scan_pinecone_for_processed_content: ' . $e->getMessage());
2998 return array();
2999 }
3000 }
3001
3002 /**
3003 * UPDATED: Generate embeddings from input text for MXChat with bot support
3004 */
3005 private function mxchat_generate_embedding($text, $bot_id = 'default') {
3006 // Enable detailed logging for debugging
3007 //error_log('[MXCHAT-EMBED] Starting embedding generation for bot: ' . $bot_id . '. Text length: ' . strlen($text) . ' bytes');
3008 //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
3009
3010 // UPDATED: Get bot-specific options
3011 $bot_options = $this->get_bot_options($bot_id);
3012 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
3013
3014 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3015 //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model);
3016
3017 // Determine provider and endpoint
3018 if (strpos($selected_model, 'voyage') === 0) {
3019 $api_key = $options['voyage_api_key'] ?? '';
3020 $endpoint = 'https://api.voyageai.com/v1/embeddings';
3021 $provider_name = 'Voyage AI';
3022 //error_log('[MXCHAT-EMBED] Using Voyage AI API for bot ' . $bot_id);
3023 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3024 $api_key = $options['gemini_api_key'] ?? '';
3025 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
3026 $provider_name = 'Google Gemini';
3027 //error_log('[MXCHAT-EMBED] Using Google Gemini API for bot ' . $bot_id);
3028 } else {
3029 $api_key = $options['api_key'] ?? '';
3030 $endpoint = 'https://api.openai.com/v1/embeddings';
3031 $provider_name = 'OpenAI';
3032 //error_log('[MXCHAT-EMBED] Using OpenAI API for bot ' . $bot_id);
3033 }
3034
3035 //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
3036
3037 if (empty($api_key)) {
3038 $error_message = sprintf('Missing %s API key for bot %s. Please configure your API key in the bot settings.', $provider_name, $bot_id);
3039 //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
3040 return $error_message;
3041 }
3042
3043 // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
3044 $estimated_tokens = ceil(str_word_count($text) / 0.75);
3045 //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
3046
3047 if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
3048 //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
3049 // Consider truncating text here
3050 }
3051
3052 // Prepare request body based on provider
3053 if (strpos($selected_model, 'gemini-embedding') === 0) {
3054 // Gemini API format
3055 $request_body = array(
3056 'model' => 'models/' . $selected_model,
3057 'content' => array(
3058 'parts' => array(
3059 array('text' => $text)
3060 )
3061 )
3062 );
3063
3064 // Set output dimensionality to 1536 for consistency with other models
3065 $request_body['outputDimensionality'] = 1536;
3066 } else {
3067 // OpenAI/Voyage API format
3068 $request_body = array(
3069 'model' => $selected_model,
3070 'input' => $text
3071 );
3072
3073 // Add output_dimension for voyage-3-large model
3074 if ($selected_model === 'voyage-3-large') {
3075 $request_body['output_dimension'] = 2048;
3076 }
3077 }
3078
3079 //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
3080
3081 // Prepare headers based on provider
3082 if (strpos($selected_model, 'gemini-embedding') === 0) {
3083 // Gemini uses API key as query parameter
3084 $endpoint .= '?key=' . $api_key;
3085 $headers = array(
3086 'Content-Type' => 'application/json'
3087 );
3088 } else {
3089 // OpenAI/Voyage use Bearer token
3090 $headers = array(
3091 'Authorization' => 'Bearer ' . $api_key,
3092 'Content-Type' => 'application/json'
3093 );
3094 }
3095
3096 // Make API request
3097 //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
3098 $response = wp_remote_post($endpoint, array(
3099 'body' => wp_json_encode($request_body),
3100 'headers' => $headers,
3101 'timeout' => 60 // Increased timeout for large inputs
3102 ));
3103
3104 // Handle wp_remote_post errors
3105 if (is_wp_error($response)) {
3106 $error_message = $response->get_error_message();
3107 //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
3108 return 'Connection error: ' . $error_message;
3109 }
3110
3111 // Get and check HTTP response code
3112 $http_code = wp_remote_retrieve_response_code($response);
3113 //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
3114
3115 if ($http_code !== 200) {
3116 $error_body = wp_remote_retrieve_body($response);
3117 //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
3118
3119 // Try to parse error for more details
3120 $error_json = json_decode($error_body, true);
3121 if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
3122 $error_type = $error_json['error']['type'] ?? 'unknown';
3123 $error_message = $error_json['error']['message'] ?? 'No message';
3124 //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
3125 //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
3126
3127 // Customize error message for common API errors
3128 if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
3129 $error_message = sprintf('Invalid %s API key for bot %s. Please check your API key in the bot settings.', $provider_name, $bot_id);
3130 } elseif ($error_type === 'authentication_error') {
3131 $error_message = sprintf('%s authentication failed for bot %s. Please verify your API key in the bot settings.', $provider_name, $bot_id);
3132 }
3133
3134 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
3135 return $error_message;
3136 }
3137
3138 $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding for bot %s", $http_code, $bot_id);
3139 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
3140 return $error_message;
3141 }
3142
3143 // Parse response body
3144 $response_body = wp_remote_retrieve_body($response);
3145 //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
3146
3147 $response_data = json_decode($response_body, true);
3148
3149 if (json_last_error() !== JSON_ERROR_NONE) {
3150 $error = json_last_error_msg();
3151 //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
3152 //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
3153 return "Failed to parse API response: $error";
3154 }
3155
3156 // Handle different response formats based on provider
3157 if (strpos($selected_model, 'gemini-embedding') === 0) {
3158 // Gemini API response format
3159 if (isset($response_data['embedding']['values'])) {
3160 $embedding_dimensions = count($response_data['embedding']['values']);
3161 //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
3162
3163 // Check if embedding dimensions are as expected (should be 1536)
3164 if ($embedding_dimensions !== 1536) {
3165 //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
3166 }
3167
3168 return $response_data['embedding']['values'];
3169 } else {
3170 //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
3171 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
3172
3173 if (isset($response_data['error'])) {
3174 $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
3175 //error_log('[MXCHAT-EMBED] ' . $error_message);
3176 return $error_message;
3177 }
3178
3179 $error_message = "Invalid Gemini API response format: No embedding found";
3180 //error_log('[MXCHAT-EMBED] ' . $error_message);
3181 return $error_message;
3182 }
3183 } else {
3184 // OpenAI/Voyage API response format
3185 if (isset($response_data['data'][0]['embedding'])) {
3186 $embedding_dimensions = count($response_data['data'][0]['embedding']);
3187 //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
3188
3189 // Check if embedding dimensions are as expected
3190 if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
3191 ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
3192 //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
3193 }
3194
3195 return $response_data['data'][0]['embedding'];
3196 } else {
3197 //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
3198 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
3199
3200 if (isset($response_data['error'])) {
3201 $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
3202 //error_log('[MXCHAT-EMBED] ' . $error_message);
3203 return $error_message;
3204 }
3205
3206 $error_message = "Invalid API response format: No embedding found";
3207 //error_log('[MXCHAT-EMBED] ' . $error_message);
3208 return $error_message;
3209 }
3210 }
3211 }
3212
3213 /**
3214 * Get bot-specific options for multi-bot functionality
3215 * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
3216 */
3217 private function get_bot_options($bot_id = 'default') {
3218 error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
3219
3220 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
3221 error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
3222 return array();
3223 }
3224
3225 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
3226
3227 if (!empty($bot_options)) {
3228 error_log("MXCHAT DEBUG: Got bot-specific options from filter");
3229 if (isset($bot_options['similarity_threshold'])) {
3230 error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
3231 }
3232 }
3233
3234 return is_array($bot_options) ? $bot_options : array();
3235 }
3236
3237 /**
3238 * Get bot-specific Pinecone configuration
3239 * Used in the knowledge retrieval functions
3240 */
3241 // Also add debugging to your get_bot_pinecone_config function
3242 private function get_bot_pinecone_config($bot_id = 'default') {
3243 error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
3244
3245 // If default bot or multi-bot add-on not active, use default Pinecone config
3246 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
3247 error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
3248 $addon_options = get_option('mxchat_pinecone_addon_options', array());
3249 $config = array(
3250 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
3251 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
3252 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
3253 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
3254 );
3255 error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
3256 return $config;
3257 }
3258
3259 error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
3260
3261 // Hook for multi-bot add-on to provide bot-specific Pinecone config
3262 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
3263
3264 if (!empty($bot_pinecone_config)) {
3265 error_log("MXCHAT DEBUG: Got bot-specific config from filter");
3266 error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
3267 error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
3268 error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
3269 } else {
3270 error_log("MXCHAT DEBUG: Filter returned empty config!");
3271 }
3272
3273 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
3274 }
3275
3276
3277 public function mxchat_ajax_dismiss_completed_status() {
3278 try {
3279 // Verify the request
3280 check_ajax_referer('mxchat_status_nonce', 'nonce');
3281
3282 if (!current_user_can('manage_options')) {
3283 wp_send_json_error('Unauthorized access');
3284 exit;
3285 }
3286
3287 $card_type = isset($_POST['card_type']) ? sanitize_text_field($_POST['card_type']) : '';
3288
3289 if ($card_type === 'pdf') {
3290 // Clear PDF status
3291 $pdf_url = get_transient('mxchat_last_pdf_url');
3292 if ($pdf_url) {
3293 delete_transient('mxchat_pdf_status_' . md5($pdf_url));
3294 delete_transient('mxchat_last_pdf_url');
3295 }
3296 } elseif ($card_type === 'sitemap') {
3297 // Clear sitemap status
3298 $sitemap_url = get_transient('mxchat_last_sitemap_url');
3299 if ($sitemap_url) {
3300 delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
3301 delete_transient('mxchat_last_sitemap_url');
3302 }
3303 }
3304
3305 wp_send_json_success(array('message' => 'Status dismissed successfully'));
3306
3307 } catch (Exception $e) {
3308 wp_send_json_error(array('message' => 'Error dismissing status: ' . $e->getMessage()));
3309 }
3310 }
3311
3312 /**
3313 * Render completed status cards on page load
3314 * This ensures completed processing status persists through page refreshes
3315 */
3316 public function mxchat_render_completed_status_cards() {
3317 $output = '';
3318
3319 // Check for completed PDF status
3320 $pdf_url = get_transient('mxchat_last_pdf_url');
3321 if ($pdf_url) {
3322 $pdf_status = $this->mxchat_get_pdf_processing_status($pdf_url);
3323 if ($pdf_status && ($pdf_status['status'] === 'complete' || $pdf_status['status'] === 'error')) {
3324 $output .= $this->mxchat_render_pdf_status_card($pdf_status, $pdf_url);
3325 }
3326 }
3327
3328 // Check for completed sitemap status
3329 $sitemap_url = get_transient('mxchat_last_sitemap_url');
3330 if ($sitemap_url) {
3331 $sitemap_status = $this->mxchat_get_sitemap_processing_status($sitemap_url);
3332 if ($sitemap_status && ($sitemap_status['status'] === 'complete' || $sitemap_status['status'] === 'error')) {
3333 $output .= $this->mxchat_render_sitemap_status_card($sitemap_status, $sitemap_url);
3334 }
3335 }
3336
3337 return $output;
3338 }
3339
3340 /**
3341 * Render PDF status card HTML
3342 */
3343 private function mxchat_render_pdf_status_card($status, $pdf_url) {
3344 $html = '<div class="mxchat-status-card" data-card-type="pdf">';
3345 $html .= '<div class="mxchat-status-header">';
3346 $html .= '<h4>' . esc_html__('PDF Processing Status', 'mxchat') . '</h4>';
3347
3348 // Add dismiss button for completed status
3349 if ($status['status'] === 'complete' || $status['status'] === 'error') {
3350 $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
3351 }
3352
3353 // Process Batch button for processing status
3354 if ($status['status'] === 'processing') {
3355 $html .= '<button type="button" class="mxchat-manual-batch-btn"
3356 data-process-type="pdf"
3357 data-url="' . esc_attr($pdf_url) . '">
3358 ' . esc_html__('Process Batch', 'mxchat') . '</button>';
3359 }
3360
3361 // Add status badges
3362 if ($status['status'] === 'error') {
3363 $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
3364 } elseif ($status['status'] === 'complete') {
3365 if ($status['failed_pages'] > 0) {
3366 $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
3367 sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_pages']) . '</span>';
3368 } else {
3369 $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
3370 }
3371 }
3372
3373 $html .= '</div>'; // End header
3374
3375 // Progress bar
3376 $html .= '<div class="mxchat-progress-bar">';
3377 $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
3378 $html .= '</div>';
3379
3380 // Status details
3381 $html .= '<div class="mxchat-status-details">';
3382 $html .= '<p>' . sprintf(
3383 esc_html__('Progress: %d of %d pages (%d%%)', 'mxchat'),
3384 $status['processed_pages'],
3385 $status['total_pages'],
3386 $status['percentage']
3387 ) . '</p>';
3388
3389 // Show failed pages count if any
3390 if ($status['failed_pages'] > 0) {
3391 $html .= '<p><strong>' . esc_html__('Failed pages:', 'mxchat') . '</strong> ' . esc_html($status['failed_pages']) . '</p>';
3392 }
3393
3394 $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
3395 $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
3396
3397 // Add completion summary if available AND it's an array
3398 if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
3399 $summary = $status['completion_summary'];
3400 $html .= '<div class="mxchat-completion-summary">';
3401 $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
3402 $html .= '<p><strong>' . esc_html__('Total Pages:', 'mxchat') . '</strong> ' . esc_html($summary['total_pages']) . '</p>';
3403 $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_pages']) . '</p>';
3404 $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_pages']) . '</p>';
3405 $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
3406 $html .= '</div>';
3407 }
3408
3409 // Add failed pages list if any AND it's an array
3410 if (isset($status['failed_pages_list']) && is_array($status['failed_pages_list']) && !empty($status['failed_pages_list'])) {
3411 $html .= $this->mxchat_render_failed_pages_list($status['failed_pages_list']);
3412 }
3413
3414 // Add error message if any
3415 if (isset($status['error']) && !empty($status['error'])) {
3416 $html .= '<div class="mxchat-error-notice">';
3417 $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
3418 $html .= '</div>';
3419 }
3420
3421 $html .= '</div>'; // End details
3422 $html .= '</div>'; // End card
3423
3424 return $html;
3425 }
3426 /**
3427 * Render sitemap status card HTML
3428 */
3429 private function mxchat_render_sitemap_status_card($status, $sitemap_url) {
3430 $html = '<div class="mxchat-status-card" data-card-type="sitemap">';
3431 $html .= '<div class="mxchat-status-header">';
3432 $html .= '<h4>' . esc_html__('Sitemap Processing Status', 'mxchat') . '</h4>';
3433
3434 // Add dismiss button for completed status
3435 if ($status['status'] === 'complete' || $status['status'] === 'error') {
3436 $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
3437 }
3438
3439 // Process Batch button for processing status
3440 if ($status['status'] === 'processing') {
3441 $html .= '<button type="button" class="mxchat-manual-batch-btn"
3442 data-process-type="sitemap"
3443 data-url="' . esc_attr($sitemap_url) . '">
3444 ' . esc_html__('Process Batch', 'mxchat') . '</button>';
3445 }
3446
3447 // Add status badges
3448 if ($status['status'] === 'error') {
3449 $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
3450 } elseif ($status['status'] === 'complete') {
3451 if ($status['failed_urls'] > 0) {
3452 $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
3453 sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_urls']) . '</span>';
3454 } else {
3455 $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
3456 }
3457 }
3458
3459 $html .= '</div>'; // End header
3460
3461 // Progress bar
3462 $html .= '<div class="mxchat-progress-bar">';
3463 $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
3464 $html .= '</div>';
3465
3466 // Status details
3467 $html .= '<div class="mxchat-status-details">';
3468 $html .= '<p>' . sprintf(
3469 esc_html__('Progress: %d of %d URLs (%d%%)', 'mxchat'),
3470 $status['processed_urls'],
3471 $status['total_urls'],
3472 $status['percentage']
3473 ) . '</p>';
3474
3475 // Show failed URLs count if any
3476 if ($status['failed_urls'] > 0) {
3477 $html .= '<p><strong>' . esc_html__('Failed URLs:', 'mxchat') . '</strong> ' . esc_html($status['failed_urls']) . '</p>';
3478 }
3479
3480 $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
3481 $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
3482
3483 // Add completion summary if available AND it's an array
3484 if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
3485 $summary = $status['completion_summary'];
3486 $html .= '<div class="mxchat-completion-summary">';
3487 $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
3488 $html .= '<p><strong>' . esc_html__('Total URLs:', 'mxchat') . '</strong> ' . esc_html($summary['total_urls']) . '</p>';
3489 $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_urls']) . '</p>';
3490 $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_urls']) . '</p>';
3491 $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
3492 $html .= '</div>';
3493 }
3494
3495 // Add error messages if any (but not the failed URLs list)
3496 if (!empty($status['error']) || !empty($status['last_error'])) {
3497 $html .= '<div class="mxchat-error-notice">';
3498
3499 if (!empty($status['error'])) {
3500 $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
3501 }
3502
3503 if (!empty($status['last_error'])) {
3504 $html .= '<p class="last-error">' . esc_html__('Last error:', 'mxchat') . ' ' . esc_html($status['last_error']) . '</p>';
3505 }
3506
3507 $html .= '</div>';
3508 }
3509
3510 $html .= '</div>'; // End details
3511 $html .= '</div>'; // End card
3512
3513 return $html;
3514 }
3515
3516
3517 /**
3518 * Render failed pages list
3519 */
3520 private function mxchat_render_failed_pages_list($failed_pages_list) {
3521 // Validate that $failed_pages_list is an array and not empty
3522 if (!is_array($failed_pages_list) || empty($failed_pages_list)) {
3523 return '';
3524 }
3525
3526 $html = '<div class="mxchat-error-notice">';
3527 $html .= '<div class="mxchat-failed-pages-container">';
3528 $html .= '<h5>' . sprintf(esc_html__('Failed Pages (%d)', 'mxchat'), count($failed_pages_list)) . '</h5>';
3529 $html .= '<details>';
3530 $html .= '<summary>' . esc_html__('Show Failed Pages', 'mxchat') . '</summary>';
3531 $html .= '<div class="mxchat-failed-pages-list">';
3532
3533 // Create table for failed pages
3534 $html .= '<table class="widefat striped">';
3535 $html .= '<thead><tr>';
3536 $html .= '<th>' . esc_html__('Page', 'mxchat') . '</th>';
3537 $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
3538 $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
3539 $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
3540 $html .= '</tr></thead><tbody>';
3541
3542 // Sort failed pages by most recent
3543 $sorted_failed_pages = $failed_pages_list;
3544 usort($sorted_failed_pages, function($a, $b) {
3545 return ($b['time'] ?? 0) - ($a['time'] ?? 0);
3546 });
3547
3548 foreach ($sorted_failed_pages as $item) {
3549 // Ensure $item is an array before accessing its elements
3550 if (!is_array($item)) {
3551 continue;
3552 }
3553
3554 $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
3555 $html .= '<tr>';
3556 $html .= '<td>' . esc_html__('Page', 'mxchat') . ' ' . esc_html($item['page'] ?? 'Unknown') . '</td>';
3557 $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
3558 $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
3559 $html .= '<td>' . esc_html($time_ago) . '</td>';
3560 $html .= '</tr>';
3561 }
3562
3563 $html .= '</tbody></table>';
3564 $html .= '</div></details></div></div>';
3565
3566 return $html;
3567 }
3568
3569 /**
3570 * Render failed URLs list
3571 */
3572 private function mxchat_render_failed_urls_list($failed_urls_list) {
3573 // Validate that $failed_urls_list is an array and not empty
3574 if (!is_array($failed_urls_list) || empty($failed_urls_list)) {
3575 return '';
3576 }
3577
3578 $html = '<div class="mxchat-failed-urls-container">';
3579 $html .= '<h5>' . sprintf(esc_html__('Failed URLs (%d)', 'mxchat'), count($failed_urls_list)) . '</h5>';
3580 $html .= '<details>';
3581 $html .= '<summary>' . esc_html__('Show Failed URLs', 'mxchat') . '</summary>';
3582 $html .= '<div class="mxchat-failed-urls-list">';
3583
3584 // Create table for failed URLs
3585 $html .= '<table class="widefat striped">';
3586 $html .= '<thead><tr>';
3587 $html .= '<th>' . esc_html__('URL', 'mxchat') . '</th>';
3588 $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
3589 $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
3590 $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
3591 $html .= '</tr></thead><tbody>';
3592
3593 // Sort failed URLs by most recent
3594 $sorted_failed_urls = $failed_urls_list;
3595 usort($sorted_failed_urls, function($a, $b) {
3596 return ($b['time'] ?? 0) - ($a['time'] ?? 0);
3597 });
3598
3599 // Show up to 50 failed URLs
3600 $display_urls = array_slice($sorted_failed_urls, 0, 50);
3601
3602 foreach ($display_urls as $item) {
3603 // Ensure $item is an array before accessing its elements
3604 if (!is_array($item)) {
3605 continue;
3606 }
3607
3608 $url = $item['url'] ?? '';
3609 $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
3610
3611 // Truncate URL for display
3612 $display_url = strlen($url) > 50 ? substr($url, 0, 47) . '...' : $url;
3613
3614 $html .= '<tr>';
3615 $html .= '<td style="word-break: break-all;">';
3616 if (!empty($url)) {
3617 $html .= '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($display_url) . '</a>';
3618 } else {
3619 $html .= esc_html__('Unknown URL', 'mxchat');
3620 }
3621 $html .= '</td>';
3622 $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
3623 $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
3624 $html .= '<td>' . esc_html($time_ago) . '</td>';
3625 $html .= '</tr>';
3626 }
3627
3628 $html .= '</tbody></table>';
3629
3630 if (count($failed_urls_list) > 50) {
3631 $html .= '<div class="mxchat-failed-urls-more">+ ' .
3632 (count($failed_urls_list) - 50) .
3633 ' ' . esc_html__('more failed URLs not shown', 'mxchat') . '</div>';
3634 }
3635
3636 $html .= '</div></details></div>';
3637
3638 return $html;
3639 }
3640
3641 /**
3642 * Get all ACF fields for a specific post
3643 */
3644 public function mxchat_get_acf_fields_for_post($post_id) {
3645 if (!function_exists('get_fields')) {
3646 return array();
3647 }
3648
3649 $fields = get_fields($post_id);
3650 if (!$fields || !is_array($fields)) {
3651 return array();
3652 }
3653
3654 return $fields;
3655 }
3656
3657 /**
3658 * Format ACF field values for content extraction
3659 */
3660 public function mxchat_format_acf_field_value($value, $field_name = '', $post_id = 0) {
3661 if (empty($value)) {
3662 return '';
3663 }
3664
3665 // Handle WP_Post objects first (THIS IS THE KEY FIX)
3666 if ($value instanceof WP_Post) {
3667 return $value->post_title ?: '';
3668 }
3669
3670 // Handle other WP objects
3671 if (is_object($value)) {
3672 if (isset($value->post_title)) {
3673 return $value->post_title;
3674 } elseif (isset($value->display_name)) {
3675 return $value->display_name;
3676 } elseif (isset($value->name)) {
3677 return $value->name;
3678 } elseif (method_exists($value, '__toString')) {
3679 try {
3680 return (string) $value;
3681 } catch (Exception $e) {
3682 return '';
3683 }
3684 }
3685 // For any other objects, return empty string
3686 return '';
3687 }
3688
3689 // Handle different ACF field types
3690 if (is_array($value)) {
3691 // Check if it's an image/file field
3692 if (isset($value['url'])) {
3693 // Image field - return alt text, title, or caption
3694 if (!empty($value['alt'])) {
3695 return $value['alt'];
3696 } elseif (!empty($value['title'])) {
3697 return $value['title'];
3698 } elseif (!empty($value['caption'])) {
3699 return $value['caption'];
3700 } else {
3701 return ''; // Don't include just the URL
3702 }
3703 }
3704
3705 // Check if it's a post object or relationship field
3706 if (isset($value['post_title'])) {
3707 return $value['post_title'];
3708 }
3709
3710 // Check if it's a user field
3711 if (isset($value['display_name'])) {
3712 return $value['display_name'];
3713 }
3714
3715 // Check if it's a taxonomy term
3716 if (isset($value['name']) && isset($value['taxonomy'])) {
3717 return $value['name'];
3718 }
3719
3720 // Check if it's a select field with label
3721 if (isset($value['label'])) {
3722 return $value['label'];
3723 }
3724
3725 // Check for repeater field or flexible content
3726 if (is_numeric(key($value))) {
3727 $sub_values = array();
3728 foreach ($value as $sub_item) {
3729 if (is_array($sub_item)) {
3730 // For repeater/flexible content, extract text values
3731 $sub_text = $this->mxchat_extract_text_from_acf_array($sub_item);
3732 if (!empty($sub_text)) {
3733 $sub_values[] = $sub_text;
3734 }
3735 } elseif ($sub_item instanceof WP_Post) {
3736 // Handle WP_Post objects in arrays
3737 $sub_values[] = $sub_item->post_title ?: '';
3738 } else {
3739 $sub_values[] = (string) $sub_item;
3740 }
3741 }
3742 return implode(', ', array_filter($sub_values));
3743 }
3744
3745 // For other arrays, try to extract meaningful text
3746 $text_values = array();
3747 foreach ($value as $key => $val) {
3748 if (is_string($val) && !empty(trim($val))) {
3749 $text_values[] = trim($val);
3750 } elseif ($val instanceof WP_Post) {
3751 // Handle WP_Post objects in associative arrays
3752 $text_values[] = $val->post_title ?: '';
3753 } elseif (is_array($val) && isset($val['post_title'])) {
3754 $text_values[] = $val['post_title'];
3755 } elseif (is_array($val) && isset($val['name'])) {
3756 $text_values[] = $val['name'];
3757 }
3758 }
3759
3760 return implode(', ', array_filter($text_values));
3761 }
3762
3763 // Handle boolean values
3764 if (is_bool($value)) {
3765 return $value ? 'Yes' : 'No';
3766 }
3767
3768 // Handle numeric values
3769 if (is_numeric($value)) {
3770 return (string) $value;
3771 }
3772
3773 // Handle string values
3774 if (is_string($value)) {
3775 return trim($value);
3776 }
3777
3778 // For anything else that we can't handle, return empty string
3779 // This prevents the "Object could not be converted to string" error
3780 return '';
3781 }
3782
3783 /**
3784 * Extract text from complex ACF array structures
3785 */
3786 private function mxchat_extract_text_from_acf_array($array) {
3787 if (!is_array($array)) {
3788 return '';
3789 }
3790
3791 $text_parts = array();
3792
3793 foreach ($array as $key => $value) {
3794 if (is_string($value) && !empty(trim($value))) {
3795 // Skip keys that are likely to be IDs or technical values
3796 if (!is_numeric($value) || strlen($value) > 10) {
3797 $text_parts[] = trim($value);
3798 }
3799 } elseif ($value instanceof WP_Post) {
3800 // Handle WP_Post objects
3801 $text_parts[] = $value->post_title ?: '';
3802 } elseif (is_array($value)) {
3803 if (isset($value['post_title'])) {
3804 $text_parts[] = $value['post_title'];
3805 } elseif (isset($value['name'])) {
3806 $text_parts[] = $value['name'];
3807 } elseif (isset($value['label'])) {
3808 $text_parts[] = $value['label'];
3809 }
3810 } elseif (is_object($value)) {
3811 // Handle other objects safely
3812 if (isset($value->post_title)) {
3813 $text_parts[] = $value->post_title;
3814 } elseif (isset($value->name)) {
3815 $text_parts[] = $value->name;
3816 } elseif (isset($value->display_name)) {
3817 $text_parts[] = $value->display_name;
3818 }
3819 }
3820 }
3821
3822 return implode(', ', array_filter($text_parts));
3823 }
3824
3825 public function mxchat_handle_post_update($post_id, $post, $update) {
3826 // Basic validation checks
3827 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
3828 return;
3829 }
3830
3831 $post_type = $post->post_type;
3832
3833 // Check if sync is enabled for this post type
3834 $should_sync = false;
3835
3836 // Check built-in post types first
3837 if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
3838 $should_sync = true;
3839 } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
3840 $should_sync = true;
3841 } else {
3842 // Check custom post types
3843 $option_name = 'mxchat_auto_sync_' . $post_type;
3844 if (get_option($option_name) === '1') {
3845 $should_sync = true;
3846 }
3847 }
3848
3849 if (!$should_sync) {
3850 return;
3851 }
3852
3853 // Check if we have stored the previous status and URL in our transients
3854 $previous_status_key = 'mxchat_prev_status_' . $post_id;
3855 $previous_status = get_transient($previous_status_key);
3856
3857 $previous_url_key = 'mxchat_prev_url_' . $post_id;
3858 $previous_url = get_transient($previous_url_key);
3859
3860 // If the post was previously published but is now not published, remove from knowledge base
3861 if ($previous_status === 'publish' && $post->post_status !== 'publish') {
3862 // Use the stored URL from when it was published, or fall back to current permalink
3863 $source_url = $previous_url ?: get_permalink($post_id);
3864
3865 if ($source_url) {
3866 // Check if Pinecone is enabled
3867 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3868 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3869
3870 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3871 // Delete from Pinecone
3872 $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
3873 } else {
3874 // Delete from WordPress DB
3875 global $wpdb;
3876 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3877
3878 $result = $wpdb->delete(
3879 $table_name,
3880 array('source_url' => $source_url),
3881 array('%s')
3882 );
3883 }
3884 }
3885
3886 // Clean up the transients and exit early
3887 delete_transient($previous_status_key);
3888 delete_transient($previous_url_key);
3889 return;
3890 }
3891
3892 // Store the current status for next time (if this is an update)
3893 if ($update) {
3894 set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
3895
3896 // If the post is currently published, also store its URL
3897 if ($post->post_status === 'publish') {
3898 $current_url = get_permalink($post_id);
3899 set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
3900 }
3901 }
3902
3903 // Only process currently published content for adding/updating
3904 if ($post->post_status === 'publish') {
3905 // Get the source URL
3906 $source_url = get_permalink($post_id);
3907
3908 // Get content with proper formatting (matching ajax_mxchat_process_selected_content)
3909 $title = get_the_title($post_id);
3910 $content = get_post_field('post_content', $post_id);
3911
3912 // Apply WordPress content filters to get properly formatted content
3913 $content = apply_filters('the_content', $content);
3914
3915 // Strip tags but preserve structure
3916 $content = wp_strip_all_tags($content);
3917
3918 // Combine title and content
3919 $final_content = $title . "\n\n" . $content;
3920
3921 // For custom post types like job_listing, include additional fields
3922 if ($post_type === 'job_listing') {
3923 // Add job-specific meta if available
3924 $job_location = get_post_meta($post_id, '_job_location', true);
3925 if (!empty($job_location)) {
3926 $final_content .= "\n\nLocation: " . $job_location;
3927 }
3928
3929 // Get job type terms
3930 $job_types = get_the_terms($post_id, 'job_listing_type');
3931 if (!empty($job_types) && !is_wp_error($job_types)) {
3932 $types = array();
3933 foreach ($job_types as $type) {
3934 $types[] = $type->name;
3935 }
3936 $final_content .= "\n\nJob Type: " . implode(', ', $types);
3937 }
3938
3939 // Get company name if available
3940 $company_name = get_post_meta($post_id, '_company_name', true);
3941 if (!empty($company_name)) {
3942 $final_content .= "\n\nCompany: " . $company_name;
3943 }
3944 }
3945
3946 // Get API key with proper model detection
3947 $options = get_option('mxchat_options');
3948 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3949
3950 if (strpos($selected_model, 'voyage') === 0) {
3951 $api_key = $options['voyage_api_key'] ?? '';
3952 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3953 $api_key = $options['gemini_api_key'] ?? '';
3954 } else {
3955 $api_key = $options['api_key'] ?? '';
3956 }
3957
3958 if (empty($api_key)) {
3959 return;
3960 }
3961
3962 // Use the centralized utility function for storage
3963 $result = MxChat_Utils::submit_content_to_db(
3964 $final_content,
3965 $source_url,
3966 $api_key,
3967 md5($source_url) // Vector ID for Pinecone
3968 );
3969 }
3970
3971 // Clean up the stored previous status if not used above
3972 if ($previous_status !== 'publish' || $post->post_status === 'publish') {
3973 delete_transient($previous_status_key);
3974 delete_transient($previous_url_key);
3975 }
3976 }
3977
3978 /**
3979 * Store the post status and URL before update to detect status transitions
3980 * This runs before the post is actually updated in the database
3981 */
3982 public function mxchat_store_pre_update_status($post_id, $data) {
3983 // Get the current post from database (before update)
3984 $current_post = get_post($post_id);
3985
3986 if ($current_post) {
3987 // Store the current status temporarily
3988 $status_key = 'mxchat_prev_status_' . $post_id;
3989 set_transient($status_key, $current_post->post_status, HOUR_IN_SECONDS);
3990
3991 // If the post is currently published, also store its URL
3992 if ($current_post->post_status === 'publish') {
3993 $url_key = 'mxchat_prev_url_' . $post_id;
3994 $current_url = get_permalink($post_id);
3995 set_transient($url_key, $current_url, HOUR_IN_SECONDS);
3996 }
3997 }
3998 }
3999
4000 public function mxchat_handle_post_delete($post_id) {
4001 // Get post data before it's deleted
4002 $post = get_post($post_id);
4003
4004 // Basic validation
4005 if (!$post || wp_is_post_revision($post_id)) {
4006 return;
4007 }
4008
4009 $post_type = $post->post_type;
4010
4011 // Check if sync is enabled for this post type
4012 $should_sync = false;
4013
4014 // Check built-in post types first
4015 if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
4016 $should_sync = true;
4017 } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
4018 $should_sync = true;
4019 } else {
4020 // Check custom post types
4021 $option_name = 'mxchat_auto_sync_' . $post_type;
4022 if (get_option($option_name) === '1') {
4023 $should_sync = true;
4024 }
4025 }
4026
4027 if (!$should_sync) {
4028 return;
4029 }
4030
4031 // Get the URL before post is deleted
4032 $source_url = get_permalink($post_id);
4033 if (!$source_url) {
4034 //error_log('MXChat: Failed to get permalink for post ' . $post_id);
4035 return;
4036 }
4037
4038 // Check if Pinecone is enabled
4039 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4040 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4041
4042 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
4043 // Delete from Pinecone
4044 $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
4045 } else {
4046 // Delete from WordPress DB
4047 global $wpdb;
4048 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4049
4050 $result = $wpdb->delete(
4051 $table_name,
4052 array('source_url' => $source_url),
4053 array('%s')
4054 );
4055
4056 if ($result === false) {
4057 //error_log('MXChat: WordPress DB deletion failed for URL: ' . $source_url);
4058 }
4059 }
4060 }
4061
4062
4063 /**
4064 * Deletes data from Pinecone using a source URL
4065 */
4066 public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
4067 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
4068 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
4069
4070 if (empty($host) || empty($api_key)) {
4071 //error_log('MXChat: Pinecone deletion failed - missing configuration');
4072 return false;
4073 }
4074
4075 $api_endpoint = "https://{$host}/vectors/delete";
4076 $vector_id = md5($source_url);
4077
4078 $request_body = array(
4079 'ids' => array($vector_id)
4080 );
4081
4082 $response = wp_remote_post($api_endpoint, array(
4083 'headers' => array(
4084 'Api-Key' => $api_key,
4085 'accept' => 'application/json',
4086 'content-type' => 'application/json'
4087 ),
4088 'body' => wp_json_encode($request_body),
4089 'timeout' => 30
4090 ));
4091
4092 if (is_wp_error($response)) {
4093 //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
4094 return false;
4095 }
4096
4097 $response_code = wp_remote_retrieve_response_code($response);
4098 if ($response_code !== 200) {
4099 //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
4100 return false;
4101 }
4102
4103 return true;
4104 }
4105
4106
4107
4108 public function mxchat_handle_product_change($post_id, $post, $update) {
4109 if ($post->post_type !== 'product') {
4110 return;
4111 }
4112
4113 if ($post->post_status === 'publish') {
4114 add_action('shutdown', function() use ($post_id) {
4115 $product = wc_get_product($post_id);
4116 if ($product) {
4117 $this->mxchat_store_product_embedding($product);
4118 }
4119 });
4120 }
4121 }
4122
4123 /**
4124 * Store WooCommerce product embeddings
4125 */
4126 private function mxchat_store_product_embedding($product) {
4127 if (!isset($this->options['enable_woocommerce_integration']) ||
4128 !in_array($this->options['enable_woocommerce_integration'], ['1', 'on'])) {
4129 return;
4130 }
4131
4132 $source_url = get_permalink($product->get_id());
4133
4134 // Build product content
4135 $title = $product->get_name();
4136 $description = $product->get_description();
4137 $short_description = $product->get_short_description();
4138 $regular_price = $product->get_regular_price();
4139 $sale_price = $product->get_sale_price();
4140 $sku = $product->get_sku();
4141
4142 // Format content consistently
4143 $content = $title . "\n\n";
4144
4145 if (!empty($description)) {
4146 $content .= wp_strip_all_tags($description) . "\n\n";
4147 }
4148
4149 if (!empty($short_description)) {
4150 $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
4151 }
4152
4153 $content .= "Price: $" . $regular_price . "\n";
4154
4155 if (!empty($sale_price)) {
4156 $content .= "Sale Price: $" . $sale_price . "\n";
4157 }
4158
4159 if (!empty($sku)) {
4160 $content .= "SKU: " . $sku . "\n";
4161 }
4162
4163 // Get API key with proper model detection
4164 $options = get_option('mxchat_options');
4165 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4166
4167 if (strpos($selected_model, 'voyage') === 0) {
4168 $api_key = $options['voyage_api_key'] ?? '';
4169 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
4170 $api_key = $options['gemini_api_key'] ?? '';
4171 } else {
4172 $api_key = $options['api_key'] ?? '';
4173 }
4174
4175 if (empty($api_key)) {
4176 //error_log('MxChat Auto-sync: No API key configured for embedding model');
4177 return;
4178 }
4179
4180 // Use the centralized utility function for storage
4181 $result = MxChat_Utils::submit_content_to_db(
4182 $content,
4183 $source_url,
4184 $api_key,
4185 md5($source_url) // Vector ID for Pinecone
4186 );
4187
4188 if (is_wp_error($result)) {
4189 //error_log('MxChat WooCommerce sync failed for product ' . $product->get_id() . ': ' . $result->get_error_message());
4190 }
4191 }
4192
4193 public function mxchat_handle_product_delete($post_id) {
4194 if (get_post_type($post_id) !== 'product') {
4195 return;
4196 }
4197
4198 $source_url = get_permalink($post_id);
4199
4200 // Check if Pinecone is enabled
4201 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4202 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4203
4204 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
4205 // Delete from Pinecone
4206 $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
4207 } else {
4208 // Delete from WordPress DB
4209 global $wpdb;
4210 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4211
4212 $wpdb->delete(
4213 $table_name,
4214 array('source_url' => $source_url),
4215 array('%s')
4216 );
4217 }
4218 }
4219
4220 /**
4221 * Handle individual Pinecone content deletion
4222 */
4223 public function mxchat_handle_pinecone_prompt_delete() {
4224 // Check permissions
4225 if (!current_user_can('manage_options')) {
4226 wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
4227 }
4228
4229 // Verify nonce
4230 if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'mxchat_delete_pinecone_prompt_nonce')) {
4231 wp_die(esc_html__('Security check failed.', 'mxchat'));
4232 }
4233
4234 $vector_id = isset($_GET['vector_id']) ? sanitize_text_field($_GET['vector_id']) : '';
4235
4236 if (empty($vector_id)) {
4237 set_transient('mxchat_admin_notice_error',
4238 esc_html__('Invalid vector ID.', 'mxchat'),
4239 30
4240 );
4241 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
4242 exit;
4243 }
4244
4245 // Get Pinecone settings
4246 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4247 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4248
4249 if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
4250 set_transient('mxchat_admin_notice_error',
4251 esc_html__('Pinecone is not properly configured.', 'mxchat'),
4252 30
4253 );
4254 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
4255 exit;
4256 }
4257
4258 // Delete from Pinecone
4259 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
4260 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
4261 $vector_id,
4262 $pinecone_options['mxchat_pinecone_api_key'],
4263 $pinecone_options['mxchat_pinecone_host']
4264 );
4265
4266 if ($result['success']) {
4267 // Remove from ALL caches
4268 $pinecone_manager->mxchat_remove_from_pinecone_vector_cache($vector_id);
4269 $pinecone_manager->mxchat_remove_from_processed_content_caches($vector_id);
4270
4271 // CLEAR ALL RELEVANT CACHES
4272 delete_transient('mxchat_pinecone_recent_1k_cache');
4273 delete_option('mxchat_pinecone_vector_ids_cache');
4274 delete_option('mxchat_pinecone_processed_cache');
4275 delete_option('mxchat_processed_content_cache');
4276
4277 // Also force refresh for next page load
4278 $pinecone_manager->mxchat_refresh_after_new_content($pinecone_options);
4279
4280 set_transient('mxchat_admin_notice_success',
4281 esc_html__('Entry deleted successfully from Pinecone.', 'mxchat'),
4282 30
4283 );
4284 } else {
4285 set_transient('mxchat_admin_notice_error',
4286 esc_html__('Failed to delete entry: ', 'mxchat') . $result['message'],
4287 30
4288 );
4289 }
4290
4291 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
4292 exit;
4293 }
4294
4295 public function ajax_mxchat_delete_pinecone_prompt() {
4296 // Verify nonce and permissions
4297 if (!check_ajax_referer('mxchat_delete_pinecone_prompt_nonce', 'nonce', false)) {
4298 wp_send_json_error('Invalid nonce');
4299 exit;
4300 }
4301
4302 if (!current_user_can('manage_options')) {
4303 wp_send_json_error('Unauthorized access');
4304 exit;
4305 }
4306
4307 $vector_id = isset($_POST['vector_id']) ? sanitize_text_field($_POST['vector_id']) : '';
4308 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
4309
4310 if (empty($vector_id)) {
4311 wp_send_json_error('Missing vector ID');
4312 exit;
4313 }
4314
4315 // Get bot-specific Pinecone settings
4316 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
4317 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
4318
4319 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4320
4321 if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
4322 wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
4323 exit;
4324 }
4325
4326 // Delete from the correct Pinecone index
4327 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
4328 $vector_id,
4329 $pinecone_options['mxchat_pinecone_api_key'],
4330 $pinecone_options['mxchat_pinecone_host']
4331 );
4332
4333 if ($result['success']) {
4334 // Clear bot-specific caches
4335 $pinecone_manager->mxchat_clear_bot_caches($bot_id);
4336
4337 wp_send_json_success(array(
4338 'message' => 'Entry deleted successfully from Pinecone',
4339 'vector_id' => $vector_id,
4340 'bot_id' => $bot_id
4341 ));
4342 } else {
4343 wp_send_json_error('Failed to delete from Pinecone: ' . $result['message']);
4344 }
4345
4346 exit;
4347 }
4348 /**
4349 * NEW: Get hierarchical roles for dropdown
4350 */
4351 public function mxchat_get_role_options() {
4352 return array(
4353 'public' => __('Public (Everyone)', 'mxchat'),
4354 'logged_in' => __('Logged In Users', 'mxchat'),
4355 'subscriber' => __('Subscribers & Above', 'mxchat'),
4356 'contributor' => __('Contributors & Above', 'mxchat'),
4357 'author' => __('Authors & Above', 'mxchat'),
4358 'editor' => __('Editors & Above', 'mxchat'),
4359 'administrator' => __('Administrators Only', 'mxchat')
4360 );
4361 }
4362
4363 /**
4364 * NEW: Check if user has access to content based on role restriction
4365 */
4366 public function mxchat_user_has_content_access($role_restriction) {
4367 // Public content is always accessible
4368 if ($role_restriction === 'public' || empty($role_restriction)) {
4369 return true;
4370 }
4371
4372 // Check if user is logged in for logged_in restriction
4373 if ($role_restriction === 'logged_in') {
4374 return is_user_logged_in();
4375 }
4376
4377 // If not logged in, no access to role-restricted content
4378 if (!is_user_logged_in()) {
4379 return false;
4380 }
4381
4382 $user = wp_get_current_user();
4383 $user_roles = $user->roles;
4384
4385 if (empty($user_roles)) {
4386 return false;
4387 }
4388
4389 // Define role hierarchy (higher number = higher access)
4390 $hierarchy = array(
4391 'subscriber' => 1,
4392 'contributor' => 2,
4393 'author' => 3,
4394 'editor' => 4,
4395 'administrator' => 5
4396 );
4397
4398 // Get required level
4399 $required_level = isset($hierarchy[$role_restriction]) ? $hierarchy[$role_restriction] : 0;
4400
4401 // Check if user has required level or higher
4402 foreach ($user_roles as $user_role) {
4403 $user_level = isset($hierarchy[$user_role]) ? $hierarchy[$user_role] : 0;
4404 if ($user_level >= $required_level) {
4405 return true;
4406 }
4407 }
4408
4409 return false;
4410 }
4411
4412 /**
4413 * Handle role restriction updates via AJAX
4414 */
4415 /**
4416 * UPDATED: Handle role restriction updates for both WordPress and Pinecone via AJAX
4417 */
4418 public function ajax_mxchat_update_role_restriction() {
4419 // Verify nonce and permissions
4420 if (!check_ajax_referer('mxchat_update_role_nonce', 'nonce', false)) {
4421 wp_send_json_error('Invalid nonce');
4422 exit;
4423 }
4424
4425 if (!current_user_can('manage_options')) {
4426 wp_send_json_error('Unauthorized access');
4427 exit;
4428 }
4429
4430 $entry_id = isset($_POST['entry_id']) ? sanitize_text_field($_POST['entry_id']) : '';
4431 $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
4432 $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
4433
4434 if (empty($entry_id)) {
4435 wp_send_json_error('Invalid entry ID');
4436 exit;
4437 }
4438
4439 // Get knowledge manager instance to validate role restriction
4440 $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
4441 $valid_roles = array_keys($knowledge_manager->mxchat_get_role_options());
4442 if (!in_array($role_restriction, $valid_roles)) {
4443 wp_send_json_error('Invalid role restriction');
4444 exit;
4445 }
4446
4447 global $wpdb;
4448
4449 if ($data_source === 'pinecone') {
4450 // Handle Pinecone role restriction (stored separately in WordPress table)
4451 $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
4452
4453 // Use REPLACE to insert or update the role restriction
4454 $result = $wpdb->replace(
4455 $roles_table,
4456 array(
4457 'vector_id' => $entry_id,
4458 'role_restriction' => $role_restriction,
4459 'updated_at' => current_time('mysql')
4460 ),
4461 array('%s', '%s', '%s')
4462 );
4463
4464 // Clear Pinecone cache to reflect changes
4465 delete_transient('mxchat_pinecone_recent_1k_cache');
4466
4467 } else {
4468 // Handle WordPress database role restriction (existing functionality)
4469 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4470
4471 $result = $wpdb->update(
4472 $table_name,
4473 array('role_restriction' => $role_restriction),
4474 array('id' => absint($entry_id)),
4475 array('%s'),
4476 array('%d')
4477 );
4478 }
4479
4480 if ($result === false) {
4481 wp_send_json_error('Database update failed: ' . $wpdb->last_error);
4482 exit;
4483 }
4484
4485 wp_send_json_success(array(
4486 'message' => 'Role restriction updated successfully',
4487 'role_restriction' => $role_restriction,
4488 'data_source' => $data_source,
4489 'entry_id' => $entry_id
4490 ));
4491 exit;
4492 }
4493
4494 // ========================================
4495 // HELPER METHODS
4496 // ========================================
4497
4498 /**
4499 * Check if user has required permissions for content processing
4500 */
4501 private function mxchat_check_user_permissions() {
4502 if (!current_user_can('manage_options')) {
4503 wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
4504 }
4505 }
4506
4507 /**
4508 * Validate nonce for security
4509 */
4510 private function mxchat_validate_nonce($nonce_name, $nonce_action) {
4511 if (!isset($_POST[$nonce_name]) || !wp_verify_nonce($_POST[$nonce_name], $nonce_action)) {
4512 wp_die(esc_html__('Security check failed.', 'mxchat'));
4513 }
4514 }
4515
4516 /**
4517 * Get embedding API credentials
4518 */
4519 private function mxchat_get_embedding_credentials() {
4520 $embedding_model = $this->options['embedding_model'] ?? 'text-embedding-ada-002';
4521
4522 if (strpos($embedding_model, 'text-embedding-') !== false) {
4523 return array(
4524 'type' => 'openai',
4525 'api_key' => $this->options['api_key'] ?? ''
4526 );
4527 } elseif (strpos($embedding_model, 'voyage-') !== false) {
4528 return array(
4529 'type' => 'voyage',
4530 'api_key' => $this->options['voyage_api_key'] ?? ''
4531 );
4532 } elseif (strpos($embedding_model, 'gemini-embedding-') !== false) {
4533 return array(
4534 'type' => 'gemini',
4535 'api_key' => $this->options['gemini_api_key'] ?? ''
4536 );
4537 }
4538
4539 return array('type' => 'unknown', 'api_key' => '');
4540 }
4541
4542 /**
4543 * Log processing errors
4544 */
4545 private function mxchat_log_processing_error($operation, $error_message) {
4546 //error_log("MxChat Knowledge Processing {$operation} Error: " . $error_message);
4547 }
4548
4549 /**
4550 * Set admin notice transient
4551 */
4552 private function mxchat_set_admin_notice($type, $message) {
4553 set_transient("mxchat_admin_notice_{$type}", $message, 30);
4554 }
4555
4556 /**
4557 * Get Pinecone manager instance for vector operations
4558 */
4559 private function mxchat_get_pinecone_manager() {
4560 return MxChat_Pinecone_Manager::get_instance();
4561 }
4562
4563 // ========================================
4564 // STATIC ACCESS METHODS
4565 // ========================================
4566
4567 /**
4568 * Get singleton instance
4569 */
4570 public static function get_instance() {
4571 static $instance = null;
4572 if ($instance === null) {
4573 $instance = new self();
4574 }
4575 return $instance;
4576 }
4577 }
4578
4579 // Initialize the Knowledge manager
4580 $mxchat_knowledge_manager = MxChat_Knowledge_Manager::get_instance();