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

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

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