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

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