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

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