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

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