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

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

7,019 lines 267.9 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_delete_chunks_by_url', array($this, 'ajax_mxchat_delete_chunks_by_url'));
45 add_action('wp_ajax_mxchat_delete_wordpress_prompt', array($this, 'ajax_mxchat_delete_wordpress_prompt'));
46 add_action('wp_ajax_mxchat_bulk_delete_knowledge', array($this, 'ajax_mxchat_bulk_delete_knowledge'));
47 add_action('wp_ajax_mxchat_update_role_restriction', array($this, 'ajax_mxchat_update_role_restriction'));
48
49 // Queue-based processing AJAX handlers
50 add_action('wp_ajax_mxchat_get_next_queue_item', array($this, 'ajax_mxchat_get_next_queue_item'));
51 add_action('wp_ajax_mxchat_process_queue_item', array($this, 'ajax_mxchat_process_queue_item'));
52 add_action('wp_ajax_mxchat_get_queue_status', array($this, 'ajax_mxchat_get_queue_status'));
53 add_action('wp_ajax_mxchat_clear_queue', array($this, 'ajax_mxchat_clear_queue'));
54 add_action('wp_ajax_mxchat_retry_failed', array($this, 'ajax_mxchat_retry_failed'));
55 add_action('wp_ajax_mxchat_get_recent_entries', array($this, 'ajax_mxchat_get_recent_entries'));
56 add_action('wp_ajax_mxchat_detect_sitemaps', array($this, 'ajax_mxchat_detect_sitemaps'));
57 add_action('wp_ajax_mxchat_refresh_pinecone_entries', array($this, 'ajax_mxchat_refresh_pinecone_entries'));
58 add_action('wp_ajax_mxchat_paginate_entries', array($this, 'ajax_mxchat_paginate_entries'));
59
60 // Hook for content deletion
61 add_action('mxchat_delete_content', array($this, 'mxchat_delete_from_pinecone_by_url'), 10, 1);
62
63 // WordPress post management hooks
64 add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
65 add_action('post_updated', array($this, 'mxchat_handle_post_update'), 10, 3);
66 add_action('before_delete_post', array($this, 'mxchat_handle_post_delete'));
67 add_action('wp_trash_post', array($this, 'mxchat_handle_post_delete'));
68
69 // ACF hook - fires AFTER ACF fields are saved, ensuring ACF data is available
70 // Priority 20 to run after ACF's own save (which runs at priority 10)
71 add_action('acf/save_post', array($this, 'mxchat_handle_acf_save'), 20);
72
73 add_action('wp_ajax_mxchat_mark_queue_complete', array($this, 'ajax_mxchat_mark_queue_complete'));
74
75 // WooCommerce product hooks (if WooCommerce is active)
76 if (class_exists('WooCommerce')) {
77 add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
78 add_action('save_post_product', array($this, 'mxchat_handle_product_change'), 10, 3);
79 add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
80 add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
81 }
82 }
83
84 /**
85 * Get current options (refreshed)
86 */
87 private function mxchat_get_options() {
88 if (empty($this->options)) {
89 $this->options = get_option('mxchat_options', array());
90 }
91 return $this->options;
92 }
93
94
95 // ========================================
96 // MAIN CONTENT SUBMISSION HANDLERS
97 // ========================================
98
99 public function mxchat_handle_content_submission() {
100 // Check if the form was submitted and the user has permission.
101 if (!isset($_POST['submit_content']) || !current_user_can('manage_options')) {
102 return;
103 }
104
105 // Verify the nonce.
106 $nonce = isset($_POST['mxchat_submit_content_nonce']) ? sanitize_text_field(wp_unslash($_POST['mxchat_submit_content_nonce'])) : '';
107 if (!wp_verify_nonce($nonce, 'mxchat_submit_content_action')) {
108 wp_die(esc_html__('Nonce verification failed.', 'mxchat'));
109 }
110
111 // Sanitize the inputs.
112 // Use wp_kses_post to allow safe HTML and wp_unslash to remove WordPress added slashes
113 $article_content = wp_kses_post(wp_unslash($_POST['article_content']));
114 $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
115
116 // Get bot_id from form submission
117 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
118
119 // Get bot-specific options and API key
120 $bot_options = $this->get_bot_options($bot_id);
121 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
122 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
123
124 if (strpos($selected_model, 'voyage') === 0) {
125 $api_key = $options['voyage_api_key'] ?? '';
126 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
127 $api_key = $options['gemini_api_key'] ?? '';
128 } else {
129 $api_key = $options['api_key'] ?? '';
130 }
131
132 if (empty($api_key)) {
133 set_transient('mxchat_admin_notice_error',
134 esc_html__('API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
135 30
136 );
137 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
138 exit;
139 }
140
141 // Use centralized utility function with bot_id
142 $result = MxChat_Utils::submit_content_to_db($article_content, $article_url, $api_key, null, $bot_id);
143
144 if (is_wp_error($result)) {
145 set_transient('mxchat_admin_notice_error',
146 esc_html__('Error storing content: ', 'mxchat') . $result->get_error_message(),
147 30
148 );
149 } else {
150 set_transient('mxchat_admin_notice_success',
151 esc_html__('Content successfully submitted!', 'mxchat'),
152 30
153 );
154 }
155
156 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
157 exit;
158 }
159
160 public function mxchat_is_pdf_url($url, $response) {
161 $content_type = wp_remote_retrieve_header($response, 'content-type');
162 $file_extension = strtolower(pathinfo($url, PATHINFO_EXTENSION));
163
164 return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf';
165 }
166
167
168 public function mxchat_handle_pdf_for_knowledge_base($pdf_url, $response, $bot_id = 'default') {
169 if (!current_user_can('manage_options')) {
170 return false;
171 }
172
173 $pdf_url = esc_url_raw($pdf_url);
174 $upload_dir = wp_upload_dir();
175
176 if (isset($upload_dir['error']) && $upload_dir['error'] !== false) {
177 return false;
178 }
179
180 $pdf_filename = sanitize_file_name('mxchat_kb_' . time() . '.pdf');
181 $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
182
183 $response_body = wp_remote_retrieve_body($response);
184 if (empty($response_body)) {
185 return false;
186 }
187
188 if (!wp_mkdir_p(dirname($pdf_path))) {
189 return false;
190 }
191
192 try {
193 file_put_contents($pdf_path, $response_body);
194
195 if (!file_exists($pdf_path)) {
196 throw new Exception(__('Failed to save PDF file', 'mxchat'));
197 }
198
199 $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
200
201 if ($total_pages === false || $total_pages < 1) {
202 throw new Exception(__('Invalid PDF: Unable to parse or no pages found', 'mxchat'));
203 }
204
205 // Create unique queue ID
206 $queue_id = 'pdf_' . md5($pdf_url . time());
207
208 // Create array of pages to process
209 $pages = array();
210 for ($i = 1; $i <= $total_pages; $i++) {
211 $pages[] = array(
212 'pdf_path' => $pdf_path,
213 'pdf_url' => $pdf_url,
214 'page_number' => $i,
215 'total_pages' => $total_pages
216 );
217 }
218
219 // Add pages to queue
220 $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
221
222 if ($queued_count === 0) {
223 wp_delete_file($pdf_path);
224 throw new Exception(__('Failed to add PDF pages to processing queue', 'mxchat'));
225 }
226
227 // Store queue metadata
228 $this->mxchat_set_queue_meta($queue_id, 'source_url', $pdf_url);
229 $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'pdf');
230 $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_pages);
231 $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
232 $this->mxchat_set_queue_meta($queue_id, 'pdf_path', $pdf_path);
233 $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
234
235 // Store queue ID in transient for status tracking
236 set_transient('mxchat_active_queue_pdf', $queue_id, DAY_IN_SECONDS);
237 set_transient('mxchat_last_pdf_url', $pdf_url, DAY_IN_SECONDS);
238
239 return 'queued';
240
241 } catch (Exception $e) {
242 if (file_exists($pdf_path)) {
243 wp_delete_file($pdf_path);
244 }
245 return $e->getMessage();
246 }
247 }
248
249 /**
250 * Validate PDF and count pages with multiple parser attempts
251 */
252 private function mxchat_validate_and_count_pdf_pages($pdf_path) {
253 // Method 1: Try with Smalot PDF Parser (your current method)
254 try {
255 $parser = new \Smalot\PdfParser\Parser();
256 $pdf = $parser->parseFile($pdf_path);
257 $pages = $pdf->getPages();
258 $page_count = count($pages);
259
260 if ($page_count > 0) {
261 //error_log('PDF parsed successfully with Smalot parser: ' . $page_count . ' pages');
262 return $page_count;
263 }
264 } catch (Exception $e) {
265 //error_log('Smalot PDF parser failed: ' . $e->getMessage());
266 }
267
268 // Method 2: Try with pdfinfo command (if available)
269 if (function_exists('shell_exec') && !$this->mxchat_is_shell_disabled()) {
270 try {
271 $command = 'pdfinfo ' . escapeshellarg($pdf_path) . ' 2>&1';
272 $output = shell_exec($command);
273
274 if ($output && preg_match('/Pages:\s*(\d+)/', $output, $matches)) {
275 $page_count = intval($matches[1]);
276 if ($page_count > 0) {
277 //error_log('PDF parsed successfully with pdfinfo: ' . $page_count . ' pages');
278 return $page_count;
279 }
280 }
281 } catch (Exception $e) {
282 //error_log('pdfinfo command failed: ' . $e->getMessage());
283 }
284 }
285
286 // Method 3: Try to repair PDF and parse again
287 try {
288 $repaired_path = $this->mxchat_attempt_pdf_repair($pdf_path);
289 if ($repaired_path && $repaired_path !== $pdf_path) {
290 $parser = new \Smalot\PdfParser\Parser();
291 $pdf = $parser->parseFile($repaired_path);
292 $pages = $pdf->getPages();
293 $page_count = count($pages);
294
295 if ($page_count > 0) {
296 // Replace original with repaired version
297 copy($repaired_path, $pdf_path);
298 unlink($repaired_path);
299 //error_log('PDF repaired and parsed successfully: ' . $page_count . ' pages');
300 return $page_count;
301 }
302
303 // Clean up repaired file if it didn't work
304 unlink($repaired_path);
305 }
306 } catch (Exception $e) {
307 //error_log('PDF repair attempt failed: ' . $e->getMessage());
308 }
309
310 // Method 4: Manual PDF structure analysis (basic page count)
311 try {
312 $page_count = $this->mxchat_manual_pdf_page_count($pdf_path);
313 if ($page_count > 0) {
314 //error_log('PDF page count determined manually: ' . $page_count . ' pages');
315 return $page_count;
316 }
317 } catch (Exception $e) {
318 //error_log('Manual PDF analysis failed: ' . $e->getMessage());
319 }
320
321 //error_log('All PDF parsing methods failed for: ' . $pdf_path);
322 return false;
323 }
324
325 /**
326 * Check if shell_exec is disabled
327 */
328 private function mxchat_is_shell_disabled() {
329 $disabled = explode(',', ini_get('disable_functions'));
330 return in_array('shell_exec', $disabled);
331 }
332
333 /**
334 * Attempt to repair PDF using basic methods
335 */
336 private function mxchat_attempt_pdf_repair($pdf_path) {
337 try {
338 $content = file_get_contents($pdf_path);
339 if (!$content) {
340 return false;
341 }
342
343 // Check if PDF starts with proper header
344 if (substr($content, 0, 4) !== '%PDF') {
345 // Try to find PDF header in the content
346 $header_pos = strpos($content, '%PDF');
347 if ($header_pos !== false && $header_pos < 1024) {
348 // Remove junk before PDF header
349 $content = substr($content, $header_pos);
350 $repaired_path = $pdf_path . '.repaired';
351 file_put_contents($repaired_path, $content);
352 return $repaired_path;
353 }
354 }
355
356 // Check for EOF marker
357 $content = rtrim($content);
358 if (!preg_match('/%%EOF\s*$/', $content)) {
359 // Add EOF marker if missing
360 $content .= "\n%%EOF";
361 $repaired_path = $pdf_path . '.repaired';
362 file_put_contents($repaired_path, $content);
363 return $repaired_path;
364 }
365
366 } catch (Exception $e) {
367 //error_log('PDF repair error: ' . $e->getMessage());
368 }
369
370 return false;
371 }
372
373 /**
374 * Manual PDF page counting by analyzing PDF structure
375 */
376 private function mxchat_manual_pdf_page_count($pdf_path) {
377 try {
378 $content = file_get_contents($pdf_path);
379 if (!$content) {
380 return 0;
381 }
382
383 // Method 1: Count /Type /Page objects
384 $page_count = preg_match_all('/\/Type\s*\/Page[^s]/', $content);
385 if ($page_count > 0) {
386 return $page_count;
387 }
388
389 // Method 2: Look for /Count in pages object
390 if (preg_match('/\/Type\s*\/Pages.*?\/Count\s+(\d+)/', $content, $matches)) {
391 return intval($matches[1]);
392 }
393
394 // Method 3: Count page references
395 $page_count = preg_match_all('/\d+\s+0\s+obj\s*<<[^>]*\/Type\s*\/Page/', $content);
396 if ($page_count > 0) {
397 return $page_count;
398 }
399
400 } catch (Exception $e) {
401 //error_log('Manual PDF analysis error: ' . $e->getMessage());
402 }
403
404 return 0;
405 }
406
407
408 public function mxchat_save_inline_prompt() {
409 // DEBUG: Log what we're receiving
410 //error_log('=== MXCHAT DEBUG ===');
411 //error_log('POST data: ' . print_r($_POST, true));
412 //error_log('Nonce from POST: ' . ($_POST['_ajax_nonce'] ?? 'NOT FOUND'));
413
414 // Check for nonce security
415 check_ajax_referer('mxchat_save_inline_nonce', '_ajax_nonce');
416
417 // If we get here, nonce passed
418 //error_log('Nonce verification PASSED');
419
420 // Verify permissions
421 if (!current_user_can('manage_options')) {
422 wp_send_json_error(esc_html__('Permission denied.', 'mxchat'));
423 return;
424 }
425
426 global $wpdb;
427 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
428
429 // Validate and sanitize input data
430 $prompt_id = isset($_POST['id']) ? absint($_POST['id']) : 0;
431 $article_content = isset($_POST['article_content']) ? wp_kses_post(wp_unslash($_POST['article_content'])) : '';
432 $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
433
434 if ($prompt_id > 0 && !empty($article_content)) {
435 // Re-generate the embedding vector for the updated content
436 $embedding_vector = $this->mxchat_generate_embedding($article_content);
437 if (is_array($embedding_vector)) {
438 // Serialize the embedding vector before storing it
439 $embedding_vector_serialized = serialize($embedding_vector);
440 // Update the prompt in the database
441 $updated = $wpdb->update(
442 $table_name,
443 array(
444 'article_content' => $article_content,
445 'embedding_vector' => $embedding_vector_serialized,
446 'source_url' => $article_url,
447 ),
448 array('id' => $prompt_id),
449 array('%s', '%s', '%s'),
450 array('%d')
451 );
452 if ($updated !== false) {
453 wp_send_json_success();
454 } else {
455 wp_send_json_error(esc_html__('Database update failed.', 'mxchat'));
456 }
457 } else {
458 wp_send_json_error(esc_html__('Embedding generation failed.', 'mxchat'));
459 }
460 } else {
461 wp_send_json_error(esc_html__('Invalid data.', 'mxchat'));
462 }
463 }
464
465
466 public function mxchat_get_pdf_processing_status($pdf_url) {
467 $pdf_url = esc_url_raw($pdf_url);
468 $status = get_transient(sanitize_key('mxchat_pdf_status_' . md5($pdf_url)));
469
470 if (!$status || !is_array($status)) {
471 return false;
472 }
473
474 // Check for stalled processing (no updates for 5 minutes)
475 if ($status['status'] === 'processing' && (time() - absint($status['last_update'])) > 300) {
476 $status['status'] = 'error';
477 $status['error'] = __('PDF processing appears to be stalled. No updates for over 5 minutes.', 'mxchat');
478
479 // Save the updated status
480 set_transient(
481 sanitize_key('mxchat_pdf_status_' . md5($pdf_url)),
482 array_map('sanitize_text_field', $status),
483 DAY_IN_SECONDS
484 );
485 }
486
487 $result = array(
488 'total_pages' => absint($status['total_pages']),
489 'processed_pages' => absint($status['processed_pages']),
490 'failed_pages' => absint($status['failed_pages'] ?? 0),
491 'percentage' => ($status['total_pages'] > 0)
492 ? round((absint($status['processed_pages']) / absint($status['total_pages'])) * 100)
493 : 0,
494 'status' => sanitize_text_field($status['status']),
495 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
496 'failed_pages_list' => isset($status['failed_pages_list']) ? $status['failed_pages_list'] : array(),
497 'completion_summary' => isset($status['completion_summary']) ? $status['completion_summary'] : null
498 );
499
500 // Add error message if present
501 if (isset($status['error']) && !empty($status['error'])) {
502 $result['error'] = sanitize_text_field($status['error']);
503 }
504
505 return $result;
506 }
507
508
509 public function mxchat_handle_sitemap_submission() {
510 // Check if the form was submitted and verify permissions
511 if (!isset($_POST['submit_sitemap']) || !current_user_can('manage_options')) {
512 wp_die(esc_html__('Unauthorized access', 'mxchat'));
513 }
514
515 // Verify nonce
516 check_admin_referer('mxchat_submit_sitemap_action', 'mxchat_submit_sitemap_nonce');
517
518 // Validate URL
519 if (!isset($_POST['sitemap_url']) || empty($_POST['sitemap_url'])) {
520 set_transient('mxchat_admin_notice_error',
521 esc_html__('Please provide a valid URL.', 'mxchat'),
522 30
523 );
524 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
525 exit;
526 }
527
528 $submitted_url = esc_url_raw($_POST['sitemap_url']);
529
530 // Get bot_id from form submission
531 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
532
533 // Get bot-specific options and validate API key
534 $bot_options = $this->get_bot_options($bot_id);
535 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
536 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
537
538 if (strpos($selected_model, 'voyage') === 0) {
539 $api_key = $options['voyage_api_key'] ?? '';
540 $provider_name = 'Voyage AI';
541 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
542 $api_key = $options['gemini_api_key'] ?? '';
543 $provider_name = 'Google Gemini';
544 } else {
545 $api_key = $options['api_key'] ?? '';
546 $provider_name = 'OpenAI';
547 }
548
549 if (empty($api_key)) {
550 $error_message = sprintf(
551 esc_html__('%s API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
552 $provider_name
553 );
554 set_transient('mxchat_admin_notice_error', $error_message, 30);
555 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
556 exit;
557 }
558
559 // Fetch URL
560 $response = wp_remote_get($submitted_url, array('timeout' => 30));
561
562 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
563 $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response);
564 set_transient('mxchat_admin_notice_error',
565 sprintf(
566 esc_html__('Failed to fetch the URL: %s', 'mxchat'),
567 esc_html($error_message)
568 ),
569 30
570 );
571 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
572 exit;
573 }
574
575 $content_type = wp_remote_retrieve_header($response, 'content-type');
576 $body_content = wp_remote_retrieve_body($response);
577
578 if (empty($body_content)) {
579 set_transient('mxchat_admin_notice_error',
580 esc_html__('Empty response received from URL.', 'mxchat'),
581 30
582 );
583 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
584 exit;
585 }
586
587 // Handle PDF URL
588 if ($this->mxchat_is_pdf_url($submitted_url, $response)) {
589 $result = $this->mxchat_handle_pdf_for_knowledge_base($submitted_url, $response, $bot_id);
590
591 if ($result === 'queued') {
592 set_transient('mxchat_admin_notice_success',
593 esc_html__('PDF queued for processing. Processing will start automatically.', 'mxchat'),
594 30
595 );
596 } else {
597 set_transient('mxchat_admin_notice_error',
598 esc_html__('Failed to queue PDF processing: ', 'mxchat') . esc_html($result),
599 30
600 );
601 }
602
603 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
604 exit;
605 }
606
607 // Handle Sitemap XML
608 if (strpos($content_type, 'xml') !== false || strpos($body_content, '<urlset') !== false) {
609 libxml_use_internal_errors(true);
610 $xml = simplexml_load_string($body_content);
611 $xml_errors = libxml_get_errors();
612 libxml_clear_errors();
613
614 if ($xml === false || !empty($xml_errors)) {
615 set_transient('mxchat_admin_notice_error',
616 esc_html__('Invalid sitemap XML. Please provide a valid sitemap.', 'mxchat'),
617 30
618 );
619 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
620 exit;
621 }
622
623 $result = $this->mxchat_handle_sitemap_for_knowledge_base($xml, $submitted_url, $bot_id);
624
625 if ($result === 'queued') {
626 set_transient('mxchat_admin_notice_success',
627 esc_html__('Sitemap queued for processing. Processing will start automatically.', 'mxchat'),
628 30
629 );
630 } else {
631 set_transient('mxchat_admin_notice_error',
632 esc_html__('Failed to queue sitemap processing. Please check the status below for details.', 'mxchat'),
633 30
634 );
635 }
636
637 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
638 exit;
639 }
640
641 // Handle Regular URL (single page)
642 $page_content = $this->mxchat_extract_main_content($body_content);
643 $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
644
645 error_log('[MXCHAT-URL-DEBUG] URL: ' . $submitted_url);
646 error_log('[MXCHAT-URL-DEBUG] Extracted page_content length: ' . strlen($page_content));
647 error_log('[MXCHAT-URL-DEBUG] Sanitized content length: ' . strlen($sanitized_content));
648 error_log('[MXCHAT-URL-DEBUG] Content preview: ' . substr($sanitized_content, 0, 500));
649
650 if (empty($sanitized_content)) {
651 set_transient('mxchat_admin_notice_error',
652 esc_html__('No valid content found on the provided URL.', 'mxchat'),
653 30
654 );
655 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
656 exit;
657 }
658
659 // For single URLs, process immediately using submit_content_to_db
660 // This handles chunking automatically for large content
661 $db_result = MxChat_Utils::submit_content_to_db(
662 $sanitized_content,
663 $submitted_url,
664 $api_key,
665 null,
666 $bot_id,
667 'url' // content_type
668 );
669
670 if (is_wp_error($db_result)) {
671 $error_message = esc_html__('Failed to store content in database: ', 'mxchat') . esc_html($db_result->get_error_message());
672 set_transient('mxchat_admin_notice_error', $error_message, 30);
673 } else {
674 $success_message = esc_html__('URL content successfully submitted!', 'mxchat');
675 set_transient('mxchat_admin_notice_success', $success_message, 30);
676 }
677
678 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
679 exit;
680 }
681
682
683 public function mxchat_get_single_url_status() {
684 $status = get_transient('mxchat_single_url_status');
685 if (!$status) {
686 return null;
687 }
688
689 // Add human-readable time
690 if (isset($status['timestamp'])) {
691 $status['human_time'] = human_time_diff(strtotime($status['timestamp']), current_time('timestamp')) . ' ' . __('ago', 'mxchat');
692 }
693
694 return $status;
695 }
696
697 public function mxchat_handle_sitemap_for_knowledge_base($xml, $sitemap_url, $bot_id = 'default') {
698 if (!current_user_can('manage_options')) {
699 return false;
700 }
701
702 try {
703 $sitemap_url = esc_url_raw($sitemap_url);
704
705 if (!$xml || !is_object($xml)) {
706 throw new Exception(__('Invalid XML object provided', 'mxchat'));
707 }
708
709 // Get bot-specific embedding API for validation
710 $bot_options = $this->get_bot_options($bot_id);
711 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
712
713 // Test the embedding API before processing
714 $test_phrase = "Test embedding generation for MxChat";
715 $test_result = $this->mxchat_generate_embedding($test_phrase, $bot_id);
716
717 if (is_string($test_result)) {
718 throw new Exception(__('Embedding API validation failed: ', 'mxchat') . $test_result);
719 }
720
721 if (!is_array($test_result)) {
722 throw new Exception(__('Embedding API returned unexpected result type. Please check your configuration.', 'mxchat'));
723 }
724
725 // Extract URLs from sitemap
726 $urls = array();
727 foreach ($xml->url as $url_element) {
728 $url = esc_url_raw((string)$url_element->loc);
729 if ($url) {
730 $urls[] = array('url' => $url);
731 }
732 }
733
734 $total_urls = count($urls);
735
736 if ($total_urls < 1) {
737 throw new Exception(__('No valid URLs found in sitemap', 'mxchat'));
738 }
739
740 // Create unique queue ID
741 $queue_id = 'sitemap_' . md5($sitemap_url . time());
742
743 // Add URLs to queue
744 $queued_count = $this->mxchat_add_to_queue($queue_id, 'url', $urls, $bot_id);
745
746 if ($queued_count === 0) {
747 throw new Exception(__('Failed to add URLs to processing queue', 'mxchat'));
748 }
749
750 // Store queue metadata
751 $this->mxchat_set_queue_meta($queue_id, 'source_url', $sitemap_url);
752 $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'sitemap');
753 $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_urls);
754 $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
755 $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
756
757 // Store queue ID in transient for status tracking
758 set_transient('mxchat_active_queue_sitemap', $queue_id, DAY_IN_SECONDS);
759 set_transient('mxchat_last_sitemap_url', $sitemap_url, DAY_IN_SECONDS);
760
761 return 'queued';
762
763 } catch (Exception $e) {
764 $error_message = $e->getMessage();
765 error_log(sprintf(esc_html__('Error preparing sitemap for processing: %s', 'mxchat'), esc_html($error_message)));
766
767 return $error_message;
768 }
769
770 }
771
772 /**
773 * Remove shortcode tags but preserve the content inside them
774 * Example: [vc_column]Hello World[/vc_column] becomes "Hello World"
775 *
776 * @param string $content The content containing shortcodes
777 * @return string Content with shortcode tags removed but inner content preserved
778 */
779 private function strip_shortcode_tags_preserve_content($content) {
780 // Handle nested shortcodes by running multiple passes
781 $prev_content = '';
782 $max_iterations = 10; // Prevent infinite loops
783 $iteration = 0;
784 while ($prev_content !== $content && $iteration < $max_iterations) {
785 $prev_content = $content;
786 // Replace paired shortcodes [tag]content[/tag] with just the content
787 $content = preg_replace('/\[([a-zA-Z0-9_-]+)[^\]]*\](.*?)\[\/\1\]/s', '$2', $content);
788 $iteration++;
789 }
790 // Remove self-closing shortcodes [tag /] or [tag attr="val" /]
791 $content = preg_replace('/\[[a-zA-Z0-9_-]+[^\]]*\/\]/', '', $content);
792 // Remove any remaining opening shortcode tags [tag] or [tag attr="val"]
793 $content = preg_replace('/\[[a-zA-Z0-9_-]+[^\]]*\]/', '', $content);
794
795 return $content;
796 }
797
798 public function mxchat_sanitize_content_for_api($content) {
799 //error_log('[MXCHAT-SANITIZE] Original content preview: ' . substr($content, 0, 500) . '...');
800
801 // Remove shortcode tags but PRESERVE content inside them
802 $content = $this->strip_shortcode_tags_preserve_content($content);
803
804 // Remove script, style tags, and HTML comments
805 $content = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $content);
806 $content = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $content);
807 $content = preg_replace('/<!--(.|\s)*?-->/', '', $content);
808
809 // Remove all HTML tags and decode HTML entities
810 $content = wp_strip_all_tags($content);
811 $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5);
812
813 // Normalize whitespace but preserve paragraph breaks
814 // First, normalize line endings to \n
815 $content = str_replace(["\r\n", "\r"], "\n", $content);
816 // Replace multiple spaces/tabs with single space, but preserve newlines
817 $content = preg_replace('/[ \t]+/', ' ', $content);
818 // Replace 3+ newlines with 2 newlines (max 2 blank lines)
819 $content = preg_replace('/\n{3,}/', "\n\n", $content);
820 // Trim each line
821 $lines = explode("\n", $content);
822 $lines = array_map('trim', $lines);
823 $content = implode("\n", $lines);
824 // Final trim
825 $content = trim($content);
826
827 // Remove control characters (which can cause database issues) but preserve \n (0x0A) and \r (0x0D)
828 $content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $content);
829
830 // Remove NULL bytes which can cause database errors
831 $content = str_replace("\0", "", $content);
832
833 // Ensure valid UTF-8 encoding
834 $content = wp_check_invalid_utf8($content);
835
836 // Remove any extremely long strings without spaces (often garbage)
837 $content = preg_replace('/\S{300,}/', ' ', $content);
838
839 // Replace problematic characters that often cause database issues
840 $content = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $content); // Remove emoji and other high Unicode characters
841
842 // Replace any remaining potentially problematic characters with spaces
843 // BUT preserve newlines by temporarily replacing them
844 $content = str_replace("\n", "NEWLINE_PLACEHOLDER", $content);
845 $content = preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\p{Sm}]/u', ' ', $content);
846 $content = str_replace("NEWLINE_PLACEHOLDER", "\n", $content);
847
848 // Limit to reasonable length if needed
849 $max_length = 65000; // Just under MySQL TEXT field limit
850 if (strlen($content) > $max_length) {
851 $content = substr($content, 0, $max_length);
852 }
853
854 //error_log('[MXCHAT-SANITIZE] Sanitized content preview: ' . substr($content, 0, 500) . '...');
855 return $content;
856 }
857 public function mxchat_extract_main_content($html) {
858 if (empty($html)) {
859 return '';
860 }
861 try {
862 $dom = new DOMDocument;
863 libxml_use_internal_errors(true); // Suppress HTML parsing errors
864 @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
865 $xpath = new DOMXPath($dom);
866
867 // For debugging purposes
868 $debugEnabled = true; // Set to true to enable debugging output
869 $debug = function($message) use ($debugEnabled) {
870 if ($debugEnabled) {
871 error_log('[MXCHAT-EXTRACT-DEBUG] ' . $message);
872 }
873 };
874
875 // Direct targeting for Gerow theme posts
876 $post_text = $xpath->query('//div[contains(@class, "post-text")]');
877 if ($post_text && $post_text->length > 0) {
878 $debug("Found post-text directly");
879 $content = '';
880 foreach ($post_text as $node) {
881 $content .= $dom->saveHTML($node);
882 }
883 if (!empty($content)) {
884 $debug("Returning post-text content");
885 return $content;
886 }
887 }
888
889 // Try to get the blog details content which contains the post-text
890 $blog_details = $xpath->query('//div[contains(@class, "blog-details-content")]');
891 if ($blog_details && $blog_details->length > 0) {
892 $debug("Found blog-details-content");
893 $content = '';
894 foreach ($blog_details as $node) {
895 $content .= $dom->saveHTML($node);
896 }
897 if (!empty($content)) {
898 $debug("Returning blog-details-content");
899 return $content;
900 }
901 }
902
903 // Try to get the article which contains the blog details
904 $article = $xpath->query('//article[contains(@class, "blog-details-wrap")]');
905 if ($article && $article->length > 0) {
906 $debug("Found article with blog-details-wrap");
907 $content = '';
908 foreach ($article as $node) {
909 $content .= $dom->saveHTML($node);
910 }
911 if (!empty($content)) {
912 $debug("Returning article content");
913 return $content;
914 }
915 }
916
917 // Try even broader with the blog-item-wrap
918 $blog_item = $xpath->query('//div[contains(@class, "blog-item-wrap")]');
919 if ($blog_item && $blog_item->length > 0) {
920 $debug("Found blog-item-wrap");
921 $content = '';
922 foreach ($blog_item as $node) {
923 $content .= $dom->saveHTML($node);
924 }
925 if (!empty($content)) {
926 $debug("Returning blog-item-wrap content");
927 return $content;
928 }
929 }
930
931 // Specific Gerow theme path
932 $gerow_path = $xpath->query('//section[contains(@class, "blog-area")]//div[contains(@class, "post-text")]');
933 if ($gerow_path && $gerow_path->length > 0) {
934 $debug("Found Gerow theme path to post-text");
935 $content = '';
936 foreach ($gerow_path as $node) {
937 $content .= $dom->saveHTML($node);
938 }
939 if (!empty($content)) {
940 $debug("Returning Gerow post-text content");
941 return $content;
942 }
943 }
944
945 // Generic blog post selectors
946 $selectors = [
947 // Blog post specific selectors
948 '//div[contains(@class, "post-text")]',
949 '//article[contains(@class, "blog-post-item")]//div[contains(@class, "post-text")]',
950 '//div[contains(@class, "blog-details-content")]',
951 '//article[contains(@class, "blog-details-wrap")]',
952 '//div[contains(@class, "entry-content")]',
953 '//div[contains(@class, "blog-content")]',
954 '//div[contains(@class, "blog-item-wrap")]',
955
956 // More general content selectors
957 '//div[contains(@class, "page__content")]',
958 '//div[contains(@class, "elementor-widget-container")]',
959 '//div[contains(@class, "elementor-text-editor")]',
960 '//div[contains(@class, "elementor-widget-text-editor")]',
961 '//*[contains(@class, "entry-content")]',
962 '//*[contains(@class, "post-content")]',
963 '//*[contains(@class, "article-content")]',
964 '//*[@id="content"]',
965 '//*[@id="main-content"]',
966 '//section[contains(@class, "blog-area")]',
967 '//article',
968 '//main',
969 '//div[contains(@class, "content")]'
970 ];
971
972 // First handle Elementor content - get only leaf widget containers to avoid duplicates
973 $debug("Checking for Elementor content");
974 // Get widget containers that are direct children of widgets (not nested inside other widget containers)
975 $elementor_widgets = $xpath->query('//div[contains(@class, "elementor-widget")]//div[contains(@class, "elementor-widget-container")]');
976 if ($elementor_widgets && $elementor_widgets->length > 0) {
977 $debug("Found Elementor widgets");
978 $seen_content = array(); // Track seen content to avoid duplicates
979 $combined_content = '';
980 foreach ($elementor_widgets as $widget) {
981 $widget_content = $dom->saveHTML($widget);
982 if (!empty($widget_content)) {
983 // Create a hash of the content to detect duplicates
984 $content_hash = md5($widget_content);
985 if (!isset($seen_content[$content_hash])) {
986 $seen_content[$content_hash] = true;
987 $combined_content .= $widget_content;
988 }
989 }
990 }
991 if (!empty($combined_content)) {
992 $debug("Returning Elementor content");
993 return $combined_content;
994 }
995 }
996
997 // Try standard selectors one by one
998 foreach ($selectors as $selector) {
999 $debug("Trying selector: " . $selector);
1000 $nodes = $xpath->query($selector);
1001 if ($nodes && $nodes->length > 0) {
1002 $debug("Found " . $nodes->length . " matches for selector: " . $selector);
1003 // Only take the FIRST matching node to avoid duplicate content
1004 // (pages often have nested or multiple containers with same class)
1005 $content = $dom->saveHTML($nodes->item(0));
1006 if (!empty($content)) {
1007 $debug("Returning content from selector: " . $selector . " (first match only)");
1008 return $content;
1009 }
1010 }
1011 }
1012
1013 // Manual regex fallback for post-text if DOM methods fail
1014 $debug("Trying regex fallback");
1015 if (preg_match('/<div class="post-text">(.*?)<\/div>\s*<\/div>\s*<\/div>/s', $html, $matches)) {
1016 $debug("Found post-text via regex");
1017 return '<div class="post-text">' . $matches[1] . '</div>';
1018 }
1019
1020 // Try to extract the blog section as a whole
1021 $blog_section = $xpath->query('//section[contains(@class, "blog-area")]');
1022 if ($blog_section && $blog_section->length > 0) {
1023 $debug("Found blog-area section");
1024 $content = '';
1025 foreach ($blog_section as $node) {
1026 $content .= $dom->saveHTML($node);
1027 }
1028 if (!empty($content)) {
1029 $debug("Returning blog-area section content");
1030 return $content;
1031 }
1032 }
1033
1034 // Fallback: Return the body content if no specific selector matches
1035 $debug("Using body fallback");
1036 $body = $dom->getElementsByTagName('body');
1037 if ($body->length > 0) {
1038 return $dom->saveHTML($body->item(0));
1039 }
1040
1041 // Last resort: return the original HTML
1042 $debug("Returning original HTML");
1043 return $html;
1044 } catch (Exception $e) {
1045 //error_log('[MXCHAT-ERROR] Content extraction failed: ' . $e->getMessage());
1046 return $html; // Return original HTML if parsing fails
1047 } finally {
1048 libxml_clear_errors();
1049 }
1050 }
1051 public function mxchat_get_sitemap_processing_status($sitemap_url) {
1052 $sitemap_url = esc_url_raw($sitemap_url);
1053 $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
1054 $status = get_transient($status_key);
1055
1056 if (!$status || !is_array($status)) {
1057 return false;
1058 }
1059
1060 // Auto-complete check: if all URLs are processed but status isn't complete
1061 if (isset($status['processed_urls']) && isset($status['total_urls']) &&
1062 $status['processed_urls'] >= $status['total_urls'] &&
1063 isset($status['status']) && $status['status'] !== 'complete' &&
1064 $status['status'] !== 'error') {
1065
1066 // Mark as complete
1067 $status['status'] = 'complete';
1068 $status['processed_urls'] = $status['total_urls']; // Ensure exact match
1069
1070 // Update the transient with the corrected status
1071 set_transient($status_key, $status, DAY_IN_SECONDS);
1072 }
1073
1074 return array(
1075 'total_urls' => absint($status['total_urls']),
1076 'processed_urls' => absint($status['processed_urls']),
1077 'failed_urls' => absint($status['failed_urls'] ?? 0),
1078 'percentage' => ($status['total_urls'] > 0)
1079 ? round((absint($status['processed_urls']) / absint($status['total_urls'])) * 100)
1080 : 0,
1081 'status' => sanitize_text_field($status['status']),
1082 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
1083 'error' => isset($status['error']) ? sanitize_text_field($status['error']) : '',
1084 'last_error' => isset($status['last_error']) ? sanitize_text_field($status['last_error']) : '',
1085 'failed_urls_list' => isset($status['failed_urls_list']) ? $status['failed_urls_list'] : array()
1086 );
1087 }
1088
1089 public function mxchat_ajax_get_status_updates() {
1090 try {
1091 // Verify the request
1092 check_ajax_referer('mxchat_status_nonce', 'nonce');
1093
1094 // Get active queue IDs
1095 $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
1096 $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
1097
1098 $sitemap_status = false;
1099 $pdf_status = false;
1100
1101 // Get sitemap queue status
1102 if ($sitemap_queue_id) {
1103 $sitemap_status = $this->mxchat_get_queue_status_data($sitemap_queue_id, 'sitemap');
1104 }
1105
1106 // Get PDF queue status
1107 if ($pdf_queue_id) {
1108 $pdf_status = $this->mxchat_get_queue_status_data($pdf_queue_id, 'pdf');
1109 }
1110
1111 $is_active_processing =
1112 ($sitemap_status && $sitemap_status['status'] === 'processing') ||
1113 ($pdf_status && $pdf_status['status'] === 'processing');
1114
1115 // Return JSON response with the status data
1116 wp_send_json(array(
1117 'pdf_status' => $pdf_status,
1118 'sitemap_status' => $sitemap_status,
1119 'is_processing' => $is_active_processing,
1120 'sitemap_queue_id' => $sitemap_queue_id,
1121 'pdf_queue_id' => $pdf_queue_id
1122 ));
1123
1124 } catch (Exception $e) {
1125 error_log('MxChat Status Update Error: ' . $e->getMessage());
1126
1127 wp_send_json_error(array(
1128 'message' => 'Error getting status updates: ' . $e->getMessage(),
1129 'status' => 'error'
1130 ));
1131 }
1132 }
1133
1134 /**
1135 * Helper function to get queue status data
1136 */
1137 private function mxchat_get_queue_status_data($queue_id, $type = 'sitemap') {
1138 global $wpdb;
1139 $table_name = $wpdb->prefix . 'mxchat_processing_queue';
1140
1141 // Get counts by status
1142 $counts = $wpdb->get_results($wpdb->prepare(
1143 "SELECT status, COUNT(*) as count
1144 FROM $table_name
1145 WHERE queue_id = %s
1146 GROUP BY status",
1147 $queue_id
1148 ), OBJECT_K);
1149
1150 $total = 0;
1151 $completed = 0;
1152 $failed = 0;
1153 $processing = 0;
1154 $pending = 0;
1155
1156 foreach ($counts as $status => $data) {
1157 $count = absint($data->count);
1158 $total += $count;
1159
1160 switch ($status) {
1161 case 'completed':
1162 $completed = $count;
1163 break;
1164 case 'failed':
1165 $failed = $count;
1166 break;
1167 case 'processing':
1168 $processing = $count;
1169 break;
1170 case 'pending':
1171 $pending = $count;
1172 break;
1173 }
1174 }
1175
1176 if ($total === 0) {
1177 return false;
1178 }
1179
1180 // Calculate percentage
1181 $percentage = round((($completed + $failed) / $total) * 100);
1182
1183 // Get failed items details (limit to 50)
1184 $failed_items = array();
1185 if ($failed > 0) {
1186 $failed_results = $wpdb->get_results($wpdb->prepare(
1187 "SELECT item_type, item_data, error_message, attempts, completed_at
1188 FROM $table_name
1189 WHERE queue_id = %s
1190 AND status = 'failed'
1191 AND attempts >= max_attempts
1192 ORDER BY id DESC
1193 LIMIT 50",
1194 $queue_id
1195 ));
1196
1197 foreach ($failed_results as $item) {
1198 $data = json_decode($item->item_data, true);
1199 $url = $type === 'pdf' ? ($data['pdf_url'] ?? '') : ($data['url'] ?? '');
1200
1201 $failed_items[] = array(
1202 'url' => $url,
1203 'page' => $type === 'pdf' ? ($data['page_number'] ?? 0) : 0,
1204 'error' => $item->error_message,
1205 'retries' => $item->attempts,
1206 'time' => strtotime($item->completed_at)
1207 );
1208 }
1209 }
1210
1211 // Get queue metadata
1212 $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
1213
1214 // Determine if queue is complete
1215 $is_complete = ($pending === 0 && $processing === 0);
1216
1217 // Get last update time
1218 $last_update = $wpdb->get_var($wpdb->prepare(
1219 "SELECT MAX(UNIX_TIMESTAMP(COALESCE(completed_at, started_at, created_at)))
1220 FROM $table_name
1221 WHERE queue_id = %s",
1222 $queue_id
1223 ));
1224
1225 $last_update_text = $last_update ? human_time_diff($last_update, time()) . ' ' . __('ago', 'mxchat') : __('Just now', 'mxchat');
1226
1227 // Format based on type
1228 if ($type === 'pdf') {
1229 return array(
1230 'total_pages' => $total,
1231 'processed_pages' => $completed + $failed,
1232 'failed_pages' => $failed,
1233 'percentage' => $percentage,
1234 'status' => $is_complete ? 'complete' : 'processing',
1235 'last_update' => $last_update_text,
1236 'failed_pages_list' => $failed_items,
1237 'pdf_url' => $source_url,
1238 'queue_id' => $queue_id
1239 );
1240 } else {
1241 return array(
1242 'total_urls' => $total,
1243 'processed_urls' => $completed + $failed,
1244 'failed_urls' => $failed,
1245 'percentage' => $percentage,
1246 'status' => $is_complete ? 'complete' : 'processing',
1247 'last_update' => $last_update_text,
1248 'failed_urls_list' => $failed_items,
1249 'sitemap_url' => $source_url,
1250 'queue_id' => $queue_id
1251 );
1252 }
1253 }
1254
1255 /**
1256 * Public method to get processing status for both sitemap and PDF queues
1257 * Used by admin pages to display processing status
1258 *
1259 * @return array Array with 'sitemap_status', 'pdf_status', and 'is_processing' keys
1260 */
1261 public function mxchat_get_processing_statuses() {
1262 $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
1263 $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
1264
1265 $sitemap_status = false;
1266 $pdf_status = false;
1267
1268 if ($sitemap_queue_id) {
1269 $sitemap_status = $this->mxchat_get_queue_status_data($sitemap_queue_id, 'sitemap');
1270 }
1271
1272 if ($pdf_queue_id) {
1273 $pdf_status = $this->mxchat_get_queue_status_data($pdf_queue_id, 'pdf');
1274 }
1275
1276 $is_processing =
1277 ($sitemap_status && $sitemap_status['status'] === 'processing') ||
1278 ($pdf_status && $pdf_status['status'] === 'processing');
1279
1280 return array(
1281 'sitemap_status' => $sitemap_status,
1282 'pdf_status' => $pdf_status,
1283 'is_processing' => $is_processing
1284 );
1285 }
1286
1287 /**
1288 * AJAX handler to get recent knowledge entries for real-time table updates
1289 * UPDATED: Now supports both WordPress DB and Pinecone data sources
1290 */
1291 public function ajax_mxchat_get_recent_entries() {
1292 check_ajax_referer('mxchat_entries_nonce', 'nonce');
1293
1294 if (!current_user_can('manage_options')) {
1295 wp_send_json_error(array('message' => 'Unauthorized'));
1296 return;
1297 }
1298
1299 global $wpdb;
1300 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1301
1302 // Get parameters
1303 $last_id = isset($_POST['last_id']) ? absint($_POST['last_id']) : 0;
1304 $limit = isset($_POST['limit']) ? min(absint($_POST['limit']), 50) : 10;
1305 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
1306
1307 // Check if Pinecone is enabled for this bot
1308 $pinecone_manager = $this->mxchat_get_pinecone_manager();
1309 $pinecone_options = $pinecone_manager ? $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id) : array();
1310 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1311 $has_pinecone_api = !empty($pinecone_options['mxchat_pinecone_api_key']);
1312
1313 if ($use_pinecone && $has_pinecone_api) {
1314 // PINECONE DATA SOURCE - Get grouped entry count (not raw vector count)
1315 // Use mxchat_fetch_pinecone_records which returns total_unique_entries
1316 $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, '', 1, 10, $bot_id, '');
1317 $total_count = $records['total'] ?? 0;
1318
1319 // For Pinecone, we don't return individual entries during polling
1320 // (entries are already displayed on page load via mxchat_fetch_pinecone_records)
1321 // We just return the updated count
1322 wp_send_json_success(array(
1323 'entries' => array(),
1324 'total_count' => absint($total_count),
1325 'max_id' => $last_id,
1326 'data_source' => 'pinecone'
1327 ));
1328 return;
1329 }
1330
1331 // WORDPRESS DB DATA SOURCE
1332 // Build query to get entries newer than last_id
1333 $where_clauses = array('1=1');
1334 $where_values = array();
1335
1336 if ($last_id > 0) {
1337 $where_clauses[] = 'id > %d';
1338 $where_values[] = $last_id;
1339 }
1340
1341 // Note: WordPress DB table doesn't have bot_id column
1342 // Multi-bot filtering is handled via Pinecone namespaces
1343
1344 $where_sql = implode(' AND ', $where_clauses);
1345
1346 // Get recent entries
1347 $query = "SELECT id, article_content, source_url, timestamp
1348 FROM $table_name
1349 WHERE $where_sql
1350 ORDER BY id DESC
1351 LIMIT %d";
1352
1353 $where_values[] = $limit;
1354
1355 $entries = $wpdb->get_results($wpdb->prepare($query, $where_values));
1356
1357 // Get total count of GROUPED entries (by source_url) - matches pagination display
1358 // Count unique source_urls (excluding mxchat:// internal refs) + count of ungrouped rows
1359 $total_count = $wpdb->get_var(
1360 "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '' AND source_url NOT LIKE 'mxchat://%') +
1361 (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')"
1362 );
1363
1364 // Format entries for response
1365 $formatted_entries = array();
1366 $preview_length = 150;
1367 foreach ($entries as $entry) {
1368 // Parse chunk metadata using the proper chunker method (same as initial page load)
1369 if (class_exists('MxChat_Chunker')) {
1370 $chunk_meta = MxChat_Chunker::parse_stored_chunk($entry->article_content);
1371 $display_content = $chunk_meta['text'];
1372 $chunk_metadata = $chunk_meta['metadata'];
1373 } else {
1374 $display_content = $entry->article_content;
1375 $chunk_metadata = array();
1376 }
1377
1378 $content_preview = mb_strlen($display_content) > $preview_length
1379 ? mb_substr($display_content, 0, $preview_length) . '...'
1380 : $display_content;
1381
1382 $formatted_entries[] = array(
1383 'id' => $entry->id,
1384 'preview' => esc_html($content_preview),
1385 'full_content' => wp_kses_post(wpautop($display_content)),
1386 'content_length' => mb_strlen($display_content),
1387 'preview_length' => $preview_length,
1388 'source_url' => $entry->source_url,
1389 'has_link' => !empty($entry->source_url) && strpos($entry->source_url, 'mxchat://') !== 0,
1390 'chunk_metadata' => $chunk_metadata,
1391 'delete_nonce' => wp_create_nonce('mxchat_delete_prompt_nonce')
1392 );
1393 }
1394
1395 wp_send_json_success(array(
1396 'entries' => $formatted_entries,
1397 'total_count' => absint($total_count),
1398 'max_id' => !empty($entries) ? $entries[0]->id : $last_id,
1399 'data_source' => 'wordpress'
1400 ));
1401 }
1402
1403 /**
1404 * Get Pinecone total count from stats API
1405 * Helper function for ajax_mxchat_get_recent_entries
1406 */
1407 private function mxchat_get_pinecone_count_from_stats($pinecone_options) {
1408 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1409 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1410 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
1411
1412 if (empty($api_key) || empty($host)) {
1413 return 0;
1414 }
1415
1416 try {
1417 $stats_url = "https://{$host}/describe_index_stats";
1418
1419 $response = wp_remote_post($stats_url, array(
1420 'headers' => array(
1421 'Api-Key' => $api_key,
1422 'Content-Type' => 'application/json'
1423 ),
1424 'body' => '{}',
1425 'timeout' => 10
1426 ));
1427
1428 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
1429 $body = wp_remote_retrieve_body($response);
1430 $stats_data = json_decode($body, true);
1431
1432 // If namespace is specified, get count from that specific namespace
1433 if (!empty($namespace) && isset($stats_data['namespaces'][$namespace]['vectorCount'])) {
1434 return intval($stats_data['namespaces'][$namespace]['vectorCount']);
1435 }
1436
1437 // If no namespace specified or namespace not found in response, use total
1438 return intval($stats_data['totalVectorCount'] ?? 0);
1439 }
1440
1441 return 0;
1442
1443 } catch (Exception $e) {
1444 return 0;
1445 }
1446 }
1447
1448 /**
1449 * AJAX handler to refresh Pinecone entries table via AJAX
1450 * Returns the table HTML for updating the UI without a full page reload
1451 */
1452 public function ajax_mxchat_refresh_pinecone_entries() {
1453 check_ajax_referer('mxchat_entries_nonce', 'nonce');
1454
1455 if (!current_user_can('manage_options')) {
1456 wp_send_json_error(array('message' => 'Unauthorized'));
1457 return;
1458 }
1459
1460 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
1461 $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
1462 $per_page = 10;
1463
1464 // Get Pinecone manager and options
1465 $pinecone_manager = $this->mxchat_get_pinecone_manager();
1466 if (!$pinecone_manager) {
1467 wp_send_json_error(array('message' => 'Pinecone manager not available'));
1468 return;
1469 }
1470
1471 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
1472 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1473 $pinecone_api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1474
1475 if (!$use_pinecone || empty($pinecone_api_key)) {
1476 wp_send_json_error(array('message' => 'Pinecone not configured'));
1477 return;
1478 }
1479
1480 // Fetch records from Pinecone
1481 $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, '', $page, $per_page, $bot_id, '');
1482 $prompts = $records['data'] ?? array();
1483 $total_records = $records['total'] ?? 0;
1484
1485 // Group prompts by source_url
1486 $grouped_prompts = array();
1487 foreach ($prompts as $prompt) {
1488 $source_url = '';
1489 if (!empty($prompt->chunk_metadata['source_url'])) {
1490 $source_url = $prompt->chunk_metadata['source_url'];
1491 } elseif (!empty($prompt->source_url)) {
1492 $source_url = $prompt->source_url;
1493 }
1494
1495 if (!empty($source_url)) {
1496 if (!isset($grouped_prompts[$source_url])) {
1497 $grouped_prompts[$source_url] = array();
1498 }
1499 $grouped_prompts[$source_url][] = $prompt;
1500 } else {
1501 $grouped_prompts['_ungrouped_' . $prompt->id] = array($prompt);
1502 }
1503 }
1504
1505 // Sort each group by chunk_index
1506 foreach ($grouped_prompts as $source_url => &$group) {
1507 usort($group, function($a, $b) {
1508 $index_a = isset($a->chunk_metadata['chunk_index']) ? intval($a->chunk_metadata['chunk_index']) : 0;
1509 $index_b = isset($b->chunk_metadata['chunk_index']) ? intval($b->chunk_metadata['chunk_index']) : 0;
1510 return $index_a - $index_b;
1511 });
1512 }
1513 unset($group);
1514
1515 // Build HTML for the table rows - must match admin-knowledge-page.php structure exactly
1516 ob_start();
1517 $display_index = 0;
1518 $current_page = $page;
1519 $data_source = 'pinecone';
1520 $current_bot_id = $bot_id;
1521 $preview_length = 150;
1522
1523 if (empty($grouped_prompts)) {
1524 echo '<tr><td colspan="4" style="padding: 40px; text-align: center; color: var(--mxch-text-muted);">';
1525 esc_html_e('No knowledge entries found in Pinecone.', 'mxchat');
1526 echo '</td></tr>';
1527 } else {
1528 foreach ($grouped_prompts as $source_url => $group) {
1529 $chunk_count = count($group);
1530 $first_prompt = $group[0];
1531 $display_index++;
1532
1533 if ($chunk_count > 1) {
1534 // Multiple chunks - show grouped row with expand button
1535 $group_id = 'group-' . md5($source_url);
1536 ?>
1537 <tr id="prompt-<?php echo esc_attr($first_prompt->id); ?>"
1538 class="mxchat-chunk-group-header"
1539 data-source="<?php echo esc_attr($data_source); ?>"
1540 data-group-id="<?php echo esc_attr($group_id); ?>"
1541 style="border-bottom: 1px solid var(--mxch-card-border); background: rgba(33, 150, 243, 0.02);">
1542 <td style="padding: 12px 16px; font-size: 13px;">
1543 <?php echo esc_html($display_index + (($current_page - 1) * $per_page)); ?>
1544 </td>
1545 <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
1546 <div class="mxchat-chunk-group-info">
1547 <button type="button" class="mxchat-chunk-toggle" data-group-id="<?php echo esc_attr($group_id); ?>">
1548 <span class="dashicons dashicons-arrow-right-alt2"></span>
1549 </button>
1550 <span class="mxchat-chunk-badge"><?php echo esc_html($chunk_count); ?> <?php esc_html_e('chunks', 'mxchat'); ?></span>
1551 <span class="mxchat-chunk-preview">
1552 <?php
1553 $parent_content = isset($first_prompt->display_content) ? $first_prompt->display_content : (isset($first_prompt->article_content) ? $first_prompt->article_content : '');
1554 $content_preview = mb_substr($parent_content, 0, 100);
1555 echo esc_html($content_preview . '...');
1556 ?>
1557 </span>
1558 </div>
1559 </td>
1560 <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
1561 <?php if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0 && strpos($source_url, '_ungrouped_') !== 0) : ?>
1562 <a href="<?php echo esc_url($source_url); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
1563 <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
1564 <?php esc_html_e('View Source', 'mxchat'); ?>
1565 </a>
1566 <?php else : ?>
1567 <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
1568 <?php endif; ?>
1569 </td>
1570 <td class="mxchat-actions-cell" style="padding: 12px 16px;">
1571 <button type="button"
1572 class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
1573 data-source-url="<?php echo esc_attr($source_url); ?>"
1574 data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
1575 data-data-source="<?php echo esc_attr($data_source); ?>"
1576 data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
1577 data-nonce="<?php echo wp_create_nonce('mxchat_delete_chunks_nonce'); ?>"
1578 style="color: var(--mxch-error);"
1579 title="<?php esc_attr_e('Delete all chunks', 'mxchat'); ?>">
1580 <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
1581 </button>
1582 </td>
1583 </tr>
1584 <?php
1585 // Render hidden chunk rows
1586 foreach ($group as $chunk_index => $chunk) {
1587 $meta_chunk_index = isset($chunk->chunk_metadata['chunk_index']) ? intval($chunk->chunk_metadata['chunk_index']) : $chunk_index;
1588 $meta_total_chunks = isset($chunk->chunk_metadata['total_chunks']) ? intval($chunk->chunk_metadata['total_chunks']) : $chunk_count;
1589 $content = isset($chunk->display_content) ? $chunk->display_content : (isset($chunk->article_content) ? $chunk->article_content : '');
1590 $content_preview = mb_strlen($content) > $preview_length
1591 ? mb_substr($content, 0, $preview_length) . '...'
1592 : $content;
1593 ?>
1594 <tr id="prompt-<?php echo esc_attr($chunk->id); ?>"
1595 class="mxchat-chunk-row <?php echo esc_attr($group_id); ?>"
1596 data-source="<?php echo esc_attr($data_source); ?>"
1597 style="display: none; background: #f8f9fa; border-bottom: 1px solid var(--mxch-card-border);">
1598 <td style="padding: 12px 16px; text-align: center;">
1599 <!-- Checkbox column placeholder for chunks (managed by group) -->
1600 </td>
1601 <td style="padding: 12px 16px 12px 30px; font-size: 13px;">
1602 <!-- Hidden ID column for chunks -->
1603 </td>
1604 <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
1605 <div class="mxchat-accordion-wrapper">
1606 <div class="mxchat-content-preview">
1607 <span class="mxchat-chunk-indicator" style="margin-right: 10px; color: var(--mxch-text-secondary); font-size: 12px;">
1608 <?php printf(esc_html__('Chunk %d of %d', 'mxchat'), $meta_chunk_index + 1, $meta_total_chunks); ?>
1609 </span>
1610 <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
1611 <?php if (mb_strlen($content) > $preview_length) : ?>
1612 <button class="mxchat-expand-toggle" type="button">
1613 <span class="dashicons dashicons-arrow-down-alt2"></span>
1614 </button>
1615 <?php endif; ?>
1616 </div>
1617 <div class="mxchat-content-full" style="display: none;">
1618 <div class="content-view">
1619 <?php
1620 if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
1621 echo '<div dir="rtl" lang="he" class="rtl-content">';
1622 echo wp_kses_post(wpautop($content));
1623 echo '</div>';
1624 } else {
1625 echo wp_kses_post(wpautop($content));
1626 }
1627 ?>
1628 </div>
1629 </div>
1630 </div>
1631 </td>
1632 <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
1633 <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Same as parent', 'mxchat'); ?></span>
1634 </td>
1635 <td class="mxchat-actions-cell" style="padding: 12px 16px;">
1636 <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Managed by group', 'mxchat'); ?></span>
1637 </td>
1638 </tr>
1639 <?php
1640 }
1641 } else {
1642 // Single entry - display normally with accordion
1643 $prompt = $first_prompt;
1644 $content = isset($prompt->display_content) ? $prompt->display_content : (isset($prompt->article_content) ? $prompt->article_content : '');
1645 $content_preview = mb_strlen($content) > $preview_length
1646 ? mb_substr($content, 0, $preview_length) . '...'
1647 : $content;
1648 ?>
1649 <tr id="prompt-<?php echo esc_attr($prompt->id); ?>"
1650 data-source="<?php echo esc_attr($data_source); ?>"
1651 style="border-bottom: 1px solid var(--mxch-card-border); background: rgba(33, 150, 243, 0.02);">
1652 <td style="padding: 12px 16px; font-size: 13px;">
1653 <?php echo esc_html($display_index + (($current_page - 1) * $per_page)); ?>
1654 </td>
1655 <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
1656 <div class="mxchat-accordion-wrapper">
1657 <div class="mxchat-content-preview">
1658 <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
1659 <?php if (mb_strlen($content) > $preview_length) : ?>
1660 <button class="mxchat-expand-toggle" type="button">
1661 <span class="dashicons dashicons-arrow-down-alt2"></span>
1662 </button>
1663 <?php endif; ?>
1664 </div>
1665 <div class="mxchat-content-full" style="display: none;">
1666 <div class="content-view">
1667 <?php
1668 if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
1669 echo '<div dir="rtl" lang="he" class="rtl-content">';
1670 echo wp_kses_post(wpautop($content));
1671 echo '</div>';
1672 } else {
1673 echo wp_kses_post(wpautop($content));
1674 }
1675 ?>
1676 </div>
1677 </div>
1678 </div>
1679 </td>
1680 <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
1681 <?php
1682 $actual_source = $source_url;
1683 if (strpos($source_url, '_ungrouped_') === 0) {
1684 $actual_source = $prompt->source_url ?? '';
1685 }
1686 if (!empty($actual_source) && strpos($actual_source, 'mxchat://') !== 0) : ?>
1687 <a href="<?php echo esc_url($actual_source); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
1688 <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
1689 <?php esc_html_e('View', 'mxchat'); ?>
1690 </a>
1691 <?php else : ?>
1692 <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
1693 <?php endif; ?>
1694 </td>
1695 <td style="padding: 12px 16px;">
1696 <button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-ajax" data-vector-id="<?php echo esc_attr($prompt->id); ?>" data-bot-id="<?php echo esc_attr($current_bot_id); ?>" data-nonce="<?php echo wp_create_nonce('mxchat_delete_pinecone_prompt_nonce'); ?>" style="color: var(--mxch-error);">
1697 <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
1698 </button>
1699 </td>
1700 </tr>
1701 <?php
1702 }
1703 }
1704 }
1705 $html = ob_get_clean();
1706
1707 // Generate pagination HTML for Pinecone
1708 $total_pages = ceil($total_records / $per_page);
1709 $pagination_html = '';
1710 if ($total_pages > 1) {
1711 $pagination_html = '<div class="mxchat-ajax-pagination" data-current-page="' . esc_attr($page) . '" data-total-pages="' . esc_attr($total_pages) . '">';
1712
1713 // Previous button
1714 if ($page > 1) {
1715 $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page - 1) . '">' . esc_html__('&laquo; Previous', 'mxchat') . '</a> ';
1716 }
1717
1718 // Page numbers
1719 $start_page = max(1, $page - 2);
1720 $end_page = min($total_pages, $page + 2);
1721
1722 if ($start_page > 1) {
1723 $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="1">1</a> ';
1724 if ($start_page > 2) {
1725 $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
1726 }
1727 }
1728
1729 for ($i = $start_page; $i <= $end_page; $i++) {
1730 if ($i == $page) {
1731 $pagination_html .= '<span class="mxchat-page-current">' . $i . '</span> ';
1732 } else {
1733 $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $i . '">' . $i . '</a> ';
1734 }
1735 }
1736
1737 if ($end_page < $total_pages) {
1738 if ($end_page < $total_pages - 1) {
1739 $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
1740 }
1741 $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $total_pages . '">' . $total_pages . '</a> ';
1742 }
1743
1744 // Next button
1745 if ($page < $total_pages) {
1746 $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page + 1) . '">' . esc_html__('Next &raquo;', 'mxchat') . '</a>';
1747 }
1748
1749 $pagination_html .= '</div>';
1750 }
1751
1752 wp_send_json_success(array(
1753 'html' => $html,
1754 'pagination_html' => $pagination_html,
1755 'total_count' => $total_records,
1756 'total_pages' => $total_pages,
1757 'page' => $page,
1758 'per_page' => $per_page,
1759 'data_source' => 'pinecone'
1760 ));
1761 }
1762
1763 /**
1764 * AJAX handler for pagination - handles both WordPress DB and Pinecone data sources
1765 * Returns paginated entries without requiring a full page reload
1766 */
1767 public function ajax_mxchat_paginate_entries() {
1768 check_ajax_referer('mxchat_entries_nonce', 'nonce');
1769
1770 if (!current_user_can('manage_options')) {
1771 wp_send_json_error(array('message' => 'Unauthorized'));
1772 return;
1773 }
1774
1775 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
1776 $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
1777 $search_query = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
1778 $content_type_filter = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : '';
1779 $per_page = 25;
1780
1781 // Check if Pinecone is enabled for this bot
1782 $pinecone_manager = $this->mxchat_get_pinecone_manager();
1783 $pinecone_options = $pinecone_manager ? $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id) : array();
1784 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1785 $has_pinecone_api = !empty($pinecone_options['mxchat_pinecone_api_key']);
1786
1787 if ($use_pinecone && $has_pinecone_api) {
1788 // Delegate to Pinecone pagination handler (pass search params)
1789 $_POST['page'] = $page;
1790 $_POST['search'] = $search_query;
1791 $_POST['content_type'] = $content_type_filter;
1792 $this->ajax_mxchat_refresh_pinecone_entries();
1793 return;
1794 }
1795
1796 // WordPress DB pagination - MUST match initial page load logic exactly
1797 global $wpdb;
1798 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1799 $offset = ($page - 1) * $per_page;
1800
1801 // Build WHERE clause for search and content type filtering
1802 $where_clauses = array();
1803 $where_values = array();
1804
1805 if ($search_query) {
1806 $where_clauses[] = "article_content LIKE %s";
1807 $where_values[] = '%' . $wpdb->esc_like($search_query) . '%';
1808 }
1809
1810 if ($content_type_filter) {
1811 switch ($content_type_filter) {
1812 case 'manual':
1813 $where_clauses[] = "(source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')";
1814 break;
1815 case 'pdf':
1816 $where_clauses[] = "source_url LIKE '%.pdf'";
1817 break;
1818 case 'url':
1819 $where_clauses[] = "source_url != '' AND source_url IS NOT NULL AND source_url NOT LIKE 'mxchat://%' AND source_url NOT LIKE '%.pdf'";
1820 break;
1821 }
1822 }
1823
1824 $where_sql = !empty($where_clauses) ? 'WHERE ' . implode(' AND ', $where_clauses) : '';
1825
1826 // Count grouped entries with filters applied
1827 if (!empty($where_values)) {
1828 $count_args = array_merge($where_values, $where_values);
1829 $count_query = $wpdb->prepare(
1830 "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$where_sql} AND source_url != '' AND source_url NOT LIKE 'mxchat://%') +
1831 (SELECT COUNT(*) FROM {$table_name} {$where_sql} AND (source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%'))",
1832 ...$count_args
1833 );
1834 $total_records = $wpdb->get_var($count_query);
1835 } else if (!empty($where_sql)) {
1836 // Content type filter only (no search), no prepared values needed
1837 $total_records = $wpdb->get_var(
1838 "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$where_sql} AND source_url != '' AND source_url NOT LIKE 'mxchat://%') +
1839 (SELECT COUNT(*) FROM {$table_name} {$where_sql} AND (source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%'))"
1840 );
1841 } else {
1842 // No filters
1843 $total_records = $wpdb->get_var(
1844 "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '' AND source_url NOT LIKE 'mxchat://%') +
1845 (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')"
1846 );
1847 }
1848 $total_pages = ceil($total_records / $per_page);
1849
1850 // Step 1: Get unique source_urls for this page (ordered by latest timestamp) with filters
1851 if (!empty($where_values)) {
1852 $query_args = array_merge($where_values, array($per_page, $offset));
1853 $urls_query = $wpdb->prepare(
1854 "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
1855 {$where_sql}
1856 GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
1857 ...$query_args
1858 );
1859 } else if (!empty($where_sql)) {
1860 $urls_query = $wpdb->prepare(
1861 "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
1862 {$where_sql}
1863 GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
1864 $per_page, $offset
1865 );
1866 } else {
1867 $urls_query = $wpdb->prepare(
1868 "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
1869 GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
1870 $per_page, $offset
1871 );
1872 }
1873 $page_urls = $wpdb->get_results($urls_query);
1874
1875 // Step 2: Build list of source_urls to fetch
1876 $url_list = array();
1877 $url_order_map = array();
1878 $order_index = 0;
1879 foreach ($page_urls as $url_row) {
1880 $url = $url_row->source_url;
1881 $url_list[] = $url;
1882 $url_order_map[$url] = $order_index++;
1883 }
1884
1885 // Step 3: Fetch all rows for these source_urls (with search filter if applicable)
1886 $prompts = array();
1887 if (!empty($url_list)) {
1888 $placeholders = implode(',', array_fill(0, count($url_list), '%s'));
1889 if ($search_query) {
1890 // Include search filter in the final fetch
1891 $prompts_query = $wpdb->prepare(
1892 "SELECT id, article_content, source_url, timestamp, role_restriction
1893 FROM {$table_name}
1894 WHERE source_url IN ($placeholders) AND article_content LIKE %s
1895 ORDER BY timestamp DESC",
1896 ...array_merge($url_list, ['%' . $wpdb->esc_like($search_query) . '%'])
1897 );
1898 } else {
1899 $prompts_query = $wpdb->prepare(
1900 "SELECT id, article_content, source_url, timestamp, role_restriction
1901 FROM {$table_name}
1902 WHERE source_url IN ($placeholders)
1903 ORDER BY timestamp DESC",
1904 $url_list
1905 );
1906 }
1907 $prompts = $wpdb->get_results($prompts_query);
1908 }
1909
1910 // Group prompts by source_url for chunk display
1911 $grouped_prompts = array();
1912 foreach ($prompts as $prompt) {
1913 $source_url = $prompt->source_url ?? '';
1914
1915 // Parse chunk metadata using the proper chunker method (same as initial page load)
1916 if (class_exists('MxChat_Chunker')) {
1917 $chunk_meta = MxChat_Chunker::parse_stored_chunk($prompt->article_content);
1918 $prompt->chunk_metadata = $chunk_meta['metadata'];
1919 $prompt->display_content = $chunk_meta['text'];
1920 } else {
1921 $prompt->chunk_metadata = array();
1922 $prompt->display_content = $prompt->article_content;
1923 }
1924
1925 if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0) {
1926 if (!isset($grouped_prompts[$source_url])) {
1927 $grouped_prompts[$source_url] = array();
1928 }
1929 $grouped_prompts[$source_url][] = $prompt;
1930 } else {
1931 // Ungrouped entries
1932 $grouped_prompts['_ungrouped_' . $prompt->id] = array($prompt);
1933 }
1934 }
1935
1936 // Sort groups by the original URL order (newest first)
1937 uksort($grouped_prompts, function($a, $b) use ($url_order_map) {
1938 $order_a = isset($url_order_map[$a]) ? $url_order_map[$a] : PHP_INT_MAX;
1939 $order_b = isset($url_order_map[$b]) ? $url_order_map[$b] : PHP_INT_MAX;
1940 return $order_a - $order_b;
1941 });
1942
1943 // Sort each group internally by chunk_index
1944 foreach ($grouped_prompts as $source_url => &$group) {
1945 usort($group, function($a, $b) {
1946 $index_a = isset($a->chunk_metadata['chunk_index']) ? intval($a->chunk_metadata['chunk_index']) : 0;
1947 $index_b = isset($b->chunk_metadata['chunk_index']) ? intval($b->chunk_metadata['chunk_index']) : 0;
1948 return $index_a - $index_b;
1949 });
1950 }
1951 unset($group);
1952
1953 // Build HTML for the table rows
1954 ob_start();
1955 $display_index = 0;
1956 $current_page = $page;
1957 $data_source = 'wordpress';
1958 $current_bot_id = $bot_id;
1959 $preview_length = 150;
1960
1961 if (empty($grouped_prompts)) {
1962 echo '<tr><td colspan="5" style="padding: 40px; text-align: center; color: var(--mxch-text-muted);">';
1963 esc_html_e('No knowledge entries found. Use the Import Options to add content.', 'mxchat');
1964 echo '</td></tr>';
1965 } else {
1966 foreach ($grouped_prompts as $source_url => $group) {
1967 $chunk_count = count($group);
1968 $first_prompt = $group[0];
1969 $display_index++;
1970
1971 if ($chunk_count > 1) {
1972 // Multiple chunks - show grouped row with expand button
1973 $group_id = 'group-' . md5($source_url);
1974 ?>
1975 <tr id="prompt-<?php echo esc_attr($first_prompt->id); ?>"
1976 class="mxchat-chunk-group-header"
1977 data-source="<?php echo esc_attr($data_source); ?>"
1978 data-group-id="<?php echo esc_attr($group_id); ?>"
1979 style="border-bottom: 1px solid var(--mxch-card-border);">
1980 <td style="padding: 12px 16px; text-align: center;">
1981 <input type="checkbox"
1982 class="mxchat-entry-checkbox"
1983 data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
1984 data-source="<?php echo esc_attr($data_source); ?>"
1985 data-source-url="<?php echo esc_attr($source_url); ?>"
1986 data-is-group="true"
1987 data-chunk-count="<?php echo esc_attr($chunk_count); ?>">
1988 </td>
1989 <td style="padding: 12px 16px; font-size: 13px;">
1990 <?php echo esc_html($first_prompt->id); ?>
1991 </td>
1992 <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
1993 <div class="mxchat-chunk-group-info">
1994 <button type="button" class="mxchat-chunk-toggle" data-group-id="<?php echo esc_attr($group_id); ?>">
1995 <span class="dashicons dashicons-arrow-right-alt2"></span>
1996 </button>
1997 <span class="mxchat-chunk-badge"><?php echo esc_html($chunk_count); ?> <?php esc_html_e('chunks', 'mxchat'); ?></span>
1998 <span class="mxchat-chunk-preview">
1999 <?php
2000 $parent_content = isset($first_prompt->display_content) ? $first_prompt->display_content : $first_prompt->article_content;
2001 $content_preview = mb_substr($parent_content, 0, 100);
2002 echo esc_html($content_preview . '...');
2003 ?>
2004 </span>
2005 </div>
2006 </td>
2007 <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2008 <?php if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0 && strpos($source_url, '_ungrouped_') !== 0) : ?>
2009 <a href="<?php echo esc_url($source_url); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2010 <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2011 <?php esc_html_e('View Source', 'mxchat'); ?>
2012 </a>
2013 <?php else : ?>
2014 <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
2015 <?php endif; ?>
2016 </td>
2017 <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2018 <button type="button"
2019 class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
2020 data-source-url="<?php echo esc_attr($source_url); ?>"
2021 data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
2022 data-data-source="<?php echo esc_attr($data_source); ?>"
2023 data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2024 data-nonce="<?php echo wp_create_nonce('mxchat_delete_chunks_nonce'); ?>"
2025 style="color: var(--mxch-error);"
2026 title="<?php esc_attr_e('Delete all chunks', 'mxchat'); ?>">
2027 <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2028 </button>
2029 </td>
2030 </tr>
2031 <?php
2032 // Render hidden chunk rows
2033 foreach ($group as $chunk_index => $chunk) {
2034 $meta_chunk_index = isset($chunk->chunk_metadata['chunk_index']) ? intval($chunk->chunk_metadata['chunk_index']) : $chunk_index;
2035 $meta_total_chunks = isset($chunk->chunk_metadata['total_chunks']) ? intval($chunk->chunk_metadata['total_chunks']) : $chunk_count;
2036 $content = isset($chunk->display_content) ? $chunk->display_content : $chunk->article_content;
2037 $content_preview = mb_strlen($content) > $preview_length
2038 ? mb_substr($content, 0, $preview_length) . '...'
2039 : $content;
2040 ?>
2041 <tr id="prompt-<?php echo esc_attr($chunk->id); ?>"
2042 class="mxchat-chunk-row <?php echo esc_attr($group_id); ?>"
2043 data-source="<?php echo esc_attr($data_source); ?>"
2044 style="display: none; background: #f8f9fa; border-bottom: 1px solid var(--mxch-card-border);">
2045 <td style="padding: 12px 16px; text-align: center;">
2046 <!-- Checkbox column placeholder for chunks (managed by group) -->
2047 </td>
2048 <td style="padding: 12px 16px 12px 30px; font-size: 13px;">
2049 <!-- Hidden ID column for chunks -->
2050 </td>
2051 <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2052 <div class="mxchat-accordion-wrapper">
2053 <div class="mxchat-content-preview">
2054 <span class="mxchat-chunk-indicator" style="margin-right: 10px; color: var(--mxch-text-secondary); font-size: 12px;">
2055 <?php printf(esc_html__('Chunk %d of %d', 'mxchat'), $meta_chunk_index + 1, $meta_total_chunks); ?>
2056 </span>
2057 <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2058 <?php if (mb_strlen($content) > $preview_length) : ?>
2059 <button class="mxchat-expand-toggle" type="button">
2060 <span class="dashicons dashicons-arrow-down-alt2"></span>
2061 </button>
2062 <?php endif; ?>
2063 </div>
2064 <div class="mxchat-content-full" style="display: none;">
2065 <div class="content-view">
2066 <?php
2067 if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2068 echo '<div dir="rtl" lang="he" class="rtl-content">';
2069 echo wp_kses_post(wpautop($content));
2070 echo '</div>';
2071 } else {
2072 echo wp_kses_post(wpautop($content));
2073 }
2074 ?>
2075 </div>
2076 </div>
2077 </div>
2078 </td>
2079 <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2080 <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Same as parent', 'mxchat'); ?></span>
2081 </td>
2082 <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2083 <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Managed by group', 'mxchat'); ?></span>
2084 </td>
2085 </tr>
2086 <?php
2087 }
2088 } else {
2089 // Single entry - display normally with accordion
2090 $prompt = $first_prompt;
2091 $content = isset($prompt->display_content) ? $prompt->display_content : $prompt->article_content;
2092 $content_preview = mb_strlen($content) > $preview_length
2093 ? mb_substr($content, 0, $preview_length) . '...'
2094 : $content;
2095 ?>
2096 <tr id="prompt-<?php echo esc_attr($prompt->id); ?>"
2097 data-source="<?php echo esc_attr($data_source); ?>"
2098 style="border-bottom: 1px solid var(--mxch-card-border);">
2099 <td style="padding: 12px 16px; text-align: center;">
2100 <input type="checkbox"
2101 class="mxchat-entry-checkbox"
2102 data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2103 data-source="<?php echo esc_attr($data_source); ?>"
2104 data-source-url="<?php echo esc_attr($source_url); ?>"
2105 data-is-group="false">
2106 </td>
2107 <td style="padding: 12px 16px; font-size: 13px;">
2108 <?php echo esc_html($prompt->id); ?>
2109 </td>
2110 <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2111 <div class="mxchat-accordion-wrapper">
2112 <div class="mxchat-content-preview">
2113 <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2114 <?php if (mb_strlen($content) > $preview_length) : ?>
2115 <button class="mxchat-expand-toggle" type="button">
2116 <span class="dashicons dashicons-arrow-down-alt2"></span>
2117 </button>
2118 <?php endif; ?>
2119 </div>
2120 <div class="mxchat-content-full" style="display: none;">
2121 <div class="content-view">
2122 <?php
2123 if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2124 echo '<div dir="rtl" lang="he" class="rtl-content">';
2125 echo wp_kses_post(wpautop($content));
2126 echo '</div>';
2127 } else {
2128 echo wp_kses_post(wpautop($content));
2129 }
2130 ?>
2131 </div>
2132 </div>
2133 </div>
2134 </td>
2135 <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2136 <?php
2137 $actual_source = $source_url;
2138 if (strpos($source_url, '_ungrouped_') === 0) {
2139 $actual_source = $prompt->source_url ?? '';
2140 }
2141 if (!empty($actual_source) && strpos($actual_source, 'mxchat://') !== 0) : ?>
2142 <a href="<?php echo esc_url($actual_source); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2143 <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2144 <?php esc_html_e('View', 'mxchat'); ?>
2145 </a>
2146 <?php else : ?>
2147 <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
2148 <?php endif; ?>
2149 </td>
2150 <td style="padding: 12px 16px;">
2151 <button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-wordpress" data-entry-id="<?php echo esc_attr($prompt->id); ?>" data-bot-id="<?php echo esc_attr($current_bot_id); ?>" data-nonce="<?php echo wp_create_nonce('mxchat_delete_wordpress_prompt_nonce'); ?>" style="color: var(--mxch-error);">
2152 <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2153 </button>
2154 </td>
2155 </tr>
2156 <?php
2157 }
2158 }
2159 }
2160 $html = ob_get_clean();
2161
2162 // Generate pagination HTML (include search/filter data for subsequent pages)
2163 $pagination_html = '';
2164 if ($total_pages > 1) {
2165 $pagination_html = '<div class="mxchat-ajax-pagination" data-current-page="' . esc_attr($page) . '" data-total-pages="' . esc_attr($total_pages) . '" data-search="' . esc_attr($search_query) . '" data-content-type="' . esc_attr($content_type_filter) . '">';
2166
2167 // Previous button
2168 if ($page > 1) {
2169 $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page - 1) . '">' . esc_html__('&laquo; Previous', 'mxchat') . '</a> ';
2170 }
2171
2172 // Page numbers
2173 $start_page = max(1, $page - 2);
2174 $end_page = min($total_pages, $page + 2);
2175
2176 if ($start_page > 1) {
2177 $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="1">1</a> ';
2178 if ($start_page > 2) {
2179 $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2180 }
2181 }
2182
2183 for ($i = $start_page; $i <= $end_page; $i++) {
2184 if ($i == $page) {
2185 $pagination_html .= '<span class="mxchat-page-current">' . $i . '</span> ';
2186 } else {
2187 $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $i . '">' . $i . '</a> ';
2188 }
2189 }
2190
2191 if ($end_page < $total_pages) {
2192 if ($end_page < $total_pages - 1) {
2193 $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2194 }
2195 $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $total_pages . '">' . $total_pages . '</a> ';
2196 }
2197
2198 // Next button
2199 if ($page < $total_pages) {
2200 $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page + 1) . '">' . esc_html__('Next &raquo;', 'mxchat') . '</a>';
2201 }
2202
2203 $pagination_html .= '</div>';
2204 }
2205
2206 wp_send_json_success(array(
2207 'html' => $html,
2208 'pagination_html' => $pagination_html,
2209 'total_count' => $total_records,
2210 'total_pages' => $total_pages,
2211 'page' => $page,
2212 'per_page' => $per_page,
2213 'data_source' => 'wordpress'
2214 ));
2215 }
2216
2217 /**
2218 * AJAX handler to detect available sitemaps on the site
2219 * Optimized for speed - only checks primary sitemap indexes first
2220 */
2221 public function ajax_mxchat_detect_sitemaps() {
2222 check_ajax_referer('mxchat_detect_sitemaps_nonce', 'nonce');
2223
2224 if (!current_user_can('manage_options')) {
2225 wp_send_json_error(array('message' => 'Unauthorized'));
2226 return;
2227 }
2228
2229 $site_url = get_site_url();
2230 $sitemaps = array();
2231 $found_index = false;
2232
2233 // Only check the main sitemap index files first (much faster)
2234 // These are the primary entry points that contain sub-sitemaps
2235 $primary_indexes = array(
2236 'sitemap_index.xml' => 'Yoast SEO / Rank Math', // Yoast & Rank Math
2237 'wp-sitemap.xml' => 'WordPress Core', // WordPress Core
2238 'sitemap.xml' => 'Standard', // Generic/AIOSEO
2239 );
2240
2241 foreach ($primary_indexes as $path => $source) {
2242 $url = trailingslashit($site_url) . $path;
2243
2244 $response = wp_remote_head($url, array(
2245 'timeout' => 3, // Short timeout
2246 'sslverify' => false,
2247 'redirection' => 1
2248 ));
2249
2250 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2251 // Found a sitemap index - parse it to get sub-sitemaps
2252 $sub_sitemaps = $this->parse_sitemap_index($url);
2253 if (!empty($sub_sitemaps)) {
2254 $sitemaps[] = array(
2255 'url' => $url,
2256 'type' => 'index',
2257 'source' => $source,
2258 'sub_sitemaps' => $sub_sitemaps
2259 );
2260 $found_index = true;
2261 // Found a valid index, no need to check others
2262 break;
2263 }
2264 }
2265 }
2266
2267 // If no sitemap index found, check for standalone sitemaps
2268 if (!$found_index) {
2269 $standalone_sitemaps = array(
2270 'post-sitemap.xml' => array('type' => 'content', 'source' => 'Yoast SEO'),
2271 'page-sitemap.xml' => array('type' => 'content', 'source' => 'Yoast SEO'),
2272 );
2273
2274 foreach ($standalone_sitemaps as $path => $info) {
2275 $url = trailingslashit($site_url) . $path;
2276
2277 $response = wp_remote_head($url, array(
2278 'timeout' => 2,
2279 'sslverify' => false
2280 ));
2281
2282 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2283 $sitemaps[] = array(
2284 'url' => $url,
2285 'type' => $info['type'],
2286 'source' => $info['source'],
2287 'url_count' => 0 // Skip URL count for speed
2288 );
2289 }
2290 }
2291 }
2292
2293 wp_send_json_success(array(
2294 'sitemaps' => $sitemaps,
2295 'site_url' => $site_url
2296 ));
2297 }
2298
2299 /**
2300 * Parse a sitemap index to get sub-sitemaps
2301 * Optimized: doesn't fetch URL count for each sub-sitemap (too slow)
2302 */
2303 private function parse_sitemap_index($url) {
2304 $sub_sitemaps = array();
2305
2306 $response = wp_remote_get($url, array(
2307 'timeout' => 5,
2308 'sslverify' => false
2309 ));
2310
2311 if (is_wp_error($response)) {
2312 return $sub_sitemaps;
2313 }
2314
2315 $body = wp_remote_retrieve_body($response);
2316 if (empty($body)) {
2317 return $sub_sitemaps;
2318 }
2319
2320 // Suppress XML errors
2321 libxml_use_internal_errors(true);
2322 $xml = simplexml_load_string($body);
2323 libxml_clear_errors();
2324
2325 if ($xml === false) {
2326 return $sub_sitemaps;
2327 }
2328
2329 // Check if it's a sitemap index (contains <sitemap> elements)
2330 if (isset($xml->sitemap)) {
2331 foreach ($xml->sitemap as $sitemap) {
2332 $loc = (string) $sitemap->loc;
2333 if (!empty($loc)) {
2334 // Try to determine the type from the URL
2335 $type = 'content';
2336 if (strpos($loc, 'category') !== false || strpos($loc, 'tag') !== false || strpos($loc, 'taxonomy') !== false) {
2337 $type = 'taxonomy';
2338 } elseif (strpos($loc, 'author') !== false || strpos($loc, 'user') !== false) {
2339 $type = 'author';
2340 }
2341
2342 // Skip URL count - too slow to fetch for each sitemap
2343 $sub_sitemaps[] = array(
2344 'url' => $loc,
2345 'type' => $type,
2346 'url_count' => 0, // Don't fetch - takes too long
2347 'name' => basename(parse_url($loc, PHP_URL_PATH))
2348 );
2349 }
2350 }
2351 }
2352
2353 return $sub_sitemaps;
2354 }
2355
2356 /**
2357 * Get URL count from a sitemap
2358 */
2359 private function get_sitemap_url_count($url) {
2360 $response = wp_remote_get($url, array(
2361 'timeout' => 10,
2362 'sslverify' => false
2363 ));
2364
2365 if (is_wp_error($response)) {
2366 return 0;
2367 }
2368
2369 $body = wp_remote_retrieve_body($response);
2370 if (empty($body)) {
2371 return 0;
2372 }
2373
2374 // Count <url> or <loc> elements
2375 $count = preg_match_all('/<url>/i', $body, $matches);
2376 return $count ?: 0;
2377 }
2378
2379 /**
2380 * Get sitemaps declared in robots.txt
2381 */
2382 private function get_sitemaps_from_robots($site_url) {
2383 $sitemaps = array();
2384 $robots_url = trailingslashit($site_url) . 'robots.txt';
2385
2386 $response = wp_remote_get($robots_url, array(
2387 'timeout' => 5,
2388 'sslverify' => false
2389 ));
2390
2391 if (is_wp_error($response)) {
2392 return $sitemaps;
2393 }
2394
2395 $body = wp_remote_retrieve_body($response);
2396 if (empty($body)) {
2397 return $sitemaps;
2398 }
2399
2400 // Find Sitemap: declarations
2401 if (preg_match_all('/^Sitemap:\s*(.+)$/mi', $body, $matches)) {
2402 foreach ($matches[1] as $sitemap_url) {
2403 $sitemap_url = trim($sitemap_url);
2404 if (filter_var($sitemap_url, FILTER_VALIDATE_URL)) {
2405 $sitemaps[] = $sitemap_url;
2406 }
2407 }
2408 }
2409
2410 return $sitemaps;
2411 }
2412
2413 public function mxchat_stop_processing() {
2414 // Verify permissions
2415 if (!current_user_can('manage_options')) {
2416 wp_die(esc_html__('Unauthorized access', 'mxchat'));
2417 }
2418
2419 // Verify nonce
2420 check_admin_referer('mxchat_stop_processing_action', 'mxchat_stop_processing_nonce');
2421
2422 global $wpdb;
2423 $table_name = $wpdb->prefix . 'mxchat_processing_queue';
2424
2425 // Get active queue IDs
2426 $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
2427 $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
2428
2429 // Delete all pending items from active queues
2430 if ($sitemap_queue_id) {
2431 $wpdb->delete(
2432 $table_name,
2433 array(
2434 'queue_id' => $sitemap_queue_id,
2435 'status' => 'pending'
2436 ),
2437 array('%s', '%s')
2438 );
2439
2440 delete_transient('mxchat_active_queue_sitemap');
2441 delete_transient('mxchat_last_sitemap_url');
2442 }
2443
2444 if ($pdf_queue_id) {
2445 // Get PDF path before deleting
2446 $pdf_path = $this->mxchat_get_queue_meta($pdf_queue_id, 'pdf_path');
2447
2448 $wpdb->delete(
2449 $table_name,
2450 array(
2451 'queue_id' => $pdf_queue_id,
2452 'status' => 'pending'
2453 ),
2454 array('%s', '%s')
2455 );
2456
2457 // Delete PDF file
2458 if ($pdf_path && file_exists($pdf_path)) {
2459 wp_delete_file($pdf_path);
2460 }
2461
2462 delete_transient('mxchat_active_queue_pdf');
2463 delete_transient('mxchat_last_pdf_url');
2464 }
2465
2466 // Redirect back with a success message
2467 set_transient('mxchat_admin_notice_success',
2468 esc_html__('Processing has been stopped successfully.', 'mxchat'),
2469 30
2470 );
2471 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
2472 exit;
2473 }
2474
2475 /**
2476 * Get content list for processing
2477 */
2478 public function ajax_mxchat_get_content_list() {
2479 // Verify the nonce
2480 check_ajax_referer('mxchat_content_selector_nonce', 'nonce');
2481
2482 if (!current_user_can('manage_options')) {
2483 wp_send_json_error(__('Unauthorized access', 'mxchat'));
2484 }
2485
2486 $page = isset($_GET['page']) ? absint($_GET['page']) : 1;
2487 $per_page = isset($_GET['per_page']) ? absint($_GET['per_page']) : 100;
2488 $search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
2489 $post_type = isset($_GET['post_type']) ? sanitize_text_field($_GET['post_type']) : 'all';
2490 $post_status = isset($_GET['post_status']) ? sanitize_text_field($_GET['post_status']) : 'publish';
2491 $processed_filter = isset($_GET['processed_filter']) ? sanitize_text_field($_GET['processed_filter']) : 'all';
2492
2493 // Build query args
2494 $args = array(
2495 'posts_per_page' => $per_page,
2496 'paged' => $page,
2497 'post_status' => $post_status !== 'all' ? $post_status : array('publish', 'draft', 'pending'),
2498 'orderby' => 'date',
2499 'order' => 'DESC',
2500 );
2501
2502 // Handle post types - IMPROVED VERSION
2503 if ($post_type !== 'all') {
2504 $args['post_type'] = $post_type;
2505 } else {
2506 // Get all available post types that might contain content
2507 $all_post_types = array();
2508
2509 // First get all public post types
2510 $public_types = get_post_types(array('public' => true), 'names');
2511 $all_post_types = array_merge($all_post_types, $public_types);
2512
2513 // Add common forum/community post types
2514 $forum_types = array('topic', 'reply', 'forum', 'wpforo_topic', 'wpforo_post');
2515 foreach ($forum_types as $forum_type) {
2516 if (post_type_exists($forum_type)) {
2517 $all_post_types[] = $forum_type;
2518 }
2519 }
2520
2521 // Add other commonly used post types
2522 $common_types = array('product', 'job_listing', 'event', 'portfolio');
2523 foreach ($common_types as $common_type) {
2524 if (post_type_exists($common_type)) {
2525 $all_post_types[] = $common_type;
2526 }
2527 }
2528
2529 // Remove duplicates and ensure we have at least some post types
2530 $all_post_types = array_unique($all_post_types);
2531
2532 if (empty($all_post_types)) {
2533 // Fallback to basic post types
2534 $all_post_types = array('post', 'page');
2535 }
2536
2537 $args['post_type'] = $all_post_types;
2538
2539 // Debug logging to see what post types are being queried
2540 //error_log('MxChat Debug: Querying post types: ' . implode(', ', $all_post_types));
2541 }
2542
2543 if (!empty($search)) {
2544 $args['s'] = $search;
2545 }
2546
2547 // Get processed data from storage
2548 $processed_data = array();
2549
2550 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
2551 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2552
2553 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
2554 // Get fresh data from Pinecone - no caching
2555 $processed_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
2556 } else {
2557 // WordPress DB checking with better URL matching for all post types
2558 global $wpdb;
2559 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2560 $processed_items = $wpdb->get_results("SELECT id, source_url, article_content, timestamp FROM {$table_name}");
2561
2562 // Group items by source_url to count chunks
2563 $url_chunk_counts = array();
2564 $url_latest_timestamp = array();
2565 $url_first_id = array();
2566
2567 if (!empty($processed_items)) {
2568 foreach ($processed_items as $item) {
2569 $url = $item->source_url;
2570 if (empty($url)) continue;
2571
2572 // Count chunks per URL
2573 if (!isset($url_chunk_counts[$url])) {
2574 $url_chunk_counts[$url] = 0;
2575 $url_latest_timestamp[$url] = $item->timestamp;
2576 $url_first_id[$url] = $item->id;
2577 }
2578 $url_chunk_counts[$url]++;
2579
2580 // Track latest timestamp
2581 if (strtotime($item->timestamp) > strtotime($url_latest_timestamp[$url])) {
2582 $url_latest_timestamp[$url] = $item->timestamp;
2583 }
2584 }
2585
2586 // Now build processed_data with chunk counts
2587 foreach ($url_chunk_counts as $url => $chunk_count) {
2588 $post_id = $this->mxchat_url_to_post_id_improved($url);
2589
2590 if ($post_id) {
2591 $processed_data[$post_id] = array(
2592 'db_id' => $url_first_id[$url],
2593 'timestamp' => $url_latest_timestamp[$url],
2594 'url' => $url,
2595 'source' => 'wordpress',
2596 'chunk_count' => $chunk_count
2597 );
2598 }
2599 }
2600 }
2601 }
2602
2603 // Get processed IDs as a simple array for in_array checks
2604 $processed_ids = array_keys($processed_data);
2605
2606 // Handle processed/unprocessed filter
2607 if ($processed_filter === 'processed' && !empty($processed_ids)) {
2608 $args['post__in'] = $processed_ids;
2609 } elseif ($processed_filter === 'unprocessed' && !empty($processed_ids)) {
2610 $args['post__not_in'] = $processed_ids;
2611 }
2612
2613 // Run the query
2614 $query = new WP_Query($args);
2615 $content_items = array();
2616
2617 if ($query->have_posts()) {
2618 while ($query->have_posts()) {
2619 $query->the_post();
2620 $id = get_the_ID();
2621 $post_date = get_the_date();
2622 $excerpt = wp_trim_words(get_the_excerpt(), 20, '...');
2623 $word_count = str_word_count(strip_tags(get_the_content()));
2624
2625 $is_processed = in_array($id, $processed_ids);
2626 $processed_date = '';
2627 $db_record_id = 0;
2628 $data_source = 'none';
2629
2630 if ($is_processed && isset($processed_data[$id])) {
2631 $item_data = $processed_data[$id];
2632 $data_source = $item_data['source'];
2633
2634 if ($data_source === 'wordpress' && isset($item_data['timestamp'])) {
2635 // WordPress DB format
2636 $timestamp = strtotime($item_data['timestamp']);
2637 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2638 $db_record_id = $item_data['db_id'];
2639 } elseif ($data_source === 'pinecone') {
2640 // Pinecone format
2641 $processed_date = $item_data['processed_date'];
2642 $db_record_id = $item_data['db_id'];
2643 }
2644 }
2645
2646 // Get chunk count for this item
2647 $chunk_count = 0;
2648 if ($is_processed && isset($processed_data[$id]['chunk_count'])) {
2649 $chunk_count = intval($processed_data[$id]['chunk_count']);
2650 }
2651
2652 $content_items[] = array(
2653 'id' => $id,
2654 'title' => get_the_title(),
2655 'permalink' => get_permalink(),
2656 'date' => $post_date,
2657 'type' => get_post_type(),
2658 'status' => get_post_status(),
2659 'excerpt' => $excerpt,
2660 'word_count' => $word_count,
2661 'already_processed' => $is_processed,
2662 'processed_date' => $processed_date,
2663 'db_record_id' => $db_record_id,
2664 'data_source' => $data_source,
2665 'chunk_count' => $chunk_count
2666 );
2667 }
2668 wp_reset_postdata();
2669 }
2670
2671 $response = array(
2672 'items' => $content_items,
2673 'total' => $query->found_posts,
2674 'total_pages' => $query->max_num_pages,
2675 'current_page' => $page,
2676 'processed_count' => count($processed_ids)
2677 );
2678
2679 wp_send_json_success($response);
2680 exit;
2681 }
2682
2683
2684 /**
2685 * This function handles various WooCommerce URL formats and permalink structures
2686 */
2687 private function mxchat_url_to_post_id_improved($url) {
2688 // First try the standard WordPress function
2689 $post_id = url_to_postid($url);
2690
2691 if ($post_id > 0) {
2692 return $post_id;
2693 }
2694
2695 // If that fails, try more aggressive URL matching
2696 // Remove trailing slashes and query parameters for better matching
2697 $clean_url = rtrim($url, '/');
2698 $clean_url = strtok($clean_url, '?'); // Remove query parameters
2699
2700 // Try again with cleaned URL
2701 $post_id = url_to_postid($clean_url);
2702 if ($post_id > 0) {
2703 return $post_id;
2704 }
2705
2706 // For bbPress forum topics, try extracting slug from URL
2707 if (strpos($url, '/topic/') !== false || strpos($url, '/forums/') !== false) {
2708 // Handle bbPress URLs: /forums/topic/topic-name/
2709 if (preg_match('/\/forums\/topic\/([^\/\?]+)/', $url, $matches)) {
2710 $topic_slug = $matches[1];
2711
2712 // Look up topic by slug
2713 $topic = get_page_by_path($topic_slug, OBJECT, 'topic');
2714 if ($topic) {
2715 return $topic->ID;
2716 }
2717
2718 // Alternative method: query by post_name
2719 global $wpdb;
2720 $post_id = $wpdb->get_var($wpdb->prepare(
2721 "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
2722 $topic_slug
2723 ));
2724
2725 if ($post_id) {
2726 return intval($post_id);
2727 }
2728 }
2729
2730 // Handle simpler topic URLs: /topic/topic-name/
2731 if (preg_match('/\/topic\/([^\/\?]+)/', $url, $matches)) {
2732 $topic_slug = $matches[1];
2733
2734 global $wpdb;
2735 $post_id = $wpdb->get_var($wpdb->prepare(
2736 "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
2737 $topic_slug
2738 ));
2739
2740 if ($post_id) {
2741 return intval($post_id);
2742 }
2743 }
2744 }
2745
2746 // For WooCommerce products
2747 if (strpos($url, '/product/') !== false || strpos($url, 'product=') !== false) {
2748 // Extract product slug from various URL formats
2749 $product_slug = '';
2750
2751 // Handle pretty permalinks: /product/product-name/
2752 if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
2753 $product_slug = $matches[1];
2754 }
2755 // Handle query parameters: ?product=product-name
2756 elseif (preg_match('/[\?&]product=([^&]+)/', $url, $matches)) {
2757 $product_slug = $matches[1];
2758 }
2759
2760 if (!empty($product_slug)) {
2761 // Look up product by slug
2762 $product = get_page_by_path($product_slug, OBJECT, 'product');
2763 if ($product) {
2764 return $product->ID;
2765 }
2766
2767 // Alternative method: query by post_name
2768 global $wpdb;
2769 $post_id = $wpdb->get_var($wpdb->prepare(
2770 "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'product' AND post_status = 'publish'",
2771 $product_slug
2772 ));
2773
2774 if ($post_id) {
2775 return intval($post_id);
2776 }
2777 }
2778 }
2779
2780 // Generic approach: try to extract slug and match against all post types
2781 $parsed_url = wp_parse_url($clean_url);
2782 $path = $parsed_url['path'] ?? '';
2783
2784 if (!empty($path)) {
2785 // Get the last part of the path as potential slug
2786 $path_parts = array_filter(explode('/', trim($path, '/')));
2787 $potential_slug = end($path_parts);
2788
2789 if (!empty($potential_slug)) {
2790 global $wpdb;
2791
2792 // Try to find any post with this slug
2793 $post_id = $wpdb->get_var($wpdb->prepare(
2794 "SELECT ID FROM {$wpdb->posts}
2795 WHERE post_name = %s
2796 AND post_status IN ('publish', 'closed', 'private')
2797 AND post_type NOT IN ('revision', 'attachment', 'nav_menu_item')
2798 ORDER BY CASE
2799 WHEN post_type = 'post' THEN 1
2800 WHEN post_type = 'page' THEN 2
2801 WHEN post_type = 'topic' THEN 3
2802 WHEN post_type = 'product' THEN 4
2803 ELSE 5
2804 END
2805 LIMIT 1",
2806 $potential_slug
2807 ));
2808
2809 if ($post_id) {
2810 return intval($post_id);
2811 }
2812 }
2813 }
2814
2815 // ADDITIONAL: Try direct database lookup by URL variations
2816 global $wpdb;
2817 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2818
2819 // Try variations of the URL (with/without trailing slash, http/https)
2820 $url_variations = array(
2821 $url,
2822 rtrim($url, '/'),
2823 $url . '/',
2824 str_replace('http://', 'https://', $url),
2825 str_replace('https://', 'http://', $url),
2826 str_replace('http://', 'https://', rtrim($url, '/')),
2827 str_replace('https://', 'http://', rtrim($url, '/'))
2828 );
2829
2830 // Remove duplicates
2831 $url_variations = array_unique($url_variations);
2832
2833 foreach ($url_variations as $variation) {
2834 $existing_record = $wpdb->get_row($wpdb->prepare(
2835 "SELECT id, source_url FROM $table_name WHERE source_url = %s",
2836 $variation
2837 ));
2838
2839 if ($existing_record) {
2840 // Try to get post ID from this stored URL
2841 $stored_post_id = url_to_postid($existing_record->source_url);
2842 if ($stored_post_id > 0) {
2843 return $stored_post_id;
2844 }
2845 }
2846 }
2847
2848 return 0; // No match found
2849 }
2850 /**
2851 * Process selected content via AJAX
2852 */
2853 public function ajax_mxchat_process_selected_content() {
2854 // Basic request validation
2855 if (!check_ajax_referer('mxchat_content_selector_nonce', 'nonce', false)) {
2856 wp_send_json_error('Invalid nonce');
2857 exit;
2858 }
2859
2860 if (!current_user_can('manage_options')) {
2861 wp_send_json_error('Unauthorized access');
2862 exit;
2863 }
2864
2865 // Get post IDs - safely parse the array
2866 $post_ids = array();
2867 if (isset($_POST['post_ids']) && is_array($_POST['post_ids'])) {
2868 foreach ($_POST['post_ids'] as $id) {
2869 $post_ids[] = absint($id);
2870 }
2871 }
2872
2873 if (empty($post_ids)) {
2874 wp_send_json_error('No content selected');
2875 exit;
2876 }
2877
2878 // Get bot_id from request
2879 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
2880
2881 // Process only ONE post at a time to avoid request size issues
2882 $post_id = reset($post_ids);
2883 $post = get_post($post_id);
2884
2885 if (!$post) {
2886 wp_send_json_error('Post not found');
2887 exit;
2888 }
2889
2890 // Get content including title, short description (for WooCommerce), and main content
2891 $content = $post->post_title . "\n\n";
2892
2893 // Add short description if it exists (WooCommerce products use post_excerpt for short description)
2894 if (!empty($post->post_excerpt)) {
2895 // Remove shortcode tags but preserve content inside them
2896 $clean_excerpt = $this->strip_shortcode_tags_preserve_content($post->post_excerpt);
2897 $content .= "Short Description: " . wp_strip_all_tags($clean_excerpt) . "\n\n";
2898 }
2899
2900 // Add main content - remove shortcode tags but preserve content inside them
2901 $clean_content = $this->strip_shortcode_tags_preserve_content($post->post_content);
2902 $content .= wp_strip_all_tags($clean_content);
2903
2904 // ADD WOOCOMMERCE PRODUCT DATA (pricing, stock, categories, custom tabs)
2905 if (get_post_type($post_id) === 'product' && class_exists('WooCommerce')) {
2906 $product = wc_get_product($post_id);
2907
2908 if ($product) {
2909 // Get pricing information
2910 $regular_price = $product->get_regular_price();
2911 $sale_price = $product->get_sale_price();
2912 $price = $product->get_price();
2913 $sku = $product->get_sku();
2914
2915 // Get currency symbol
2916 $currency_symbol = get_woocommerce_currency_symbol();
2917
2918 // Add pricing information
2919 $content .= "\n";
2920 if (!empty($regular_price)) {
2921 $content .= "Price: " . $currency_symbol . $regular_price . "\n";
2922 } elseif (!empty($price)) {
2923 $content .= "Price: " . $currency_symbol . $price . "\n";
2924 }
2925
2926 if (!empty($sale_price) && $sale_price !== $regular_price) {
2927 $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
2928 }
2929
2930 // Handle variable products - show price range
2931 if ($product->is_type('variable')) {
2932 $min_price = $product->get_variation_price('min');
2933 $max_price = $product->get_variation_price('max');
2934 if ($min_price !== $max_price) {
2935 $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
2936 }
2937 }
2938
2939 if (!empty($sku)) {
2940 $content .= "SKU: " . $sku . "\n";
2941 }
2942
2943 // Get product categories
2944 $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
2945 if (!empty($categories) && !is_wp_error($categories)) {
2946 $content .= "Categories: " . implode(', ', $categories) . "\n";
2947 }
2948 }
2949
2950 // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
2951 $custom_tabs = get_post_meta($post_id, 'yikes_woo_products_tabs', true);
2952 if (!empty($custom_tabs) && is_array($custom_tabs)) {
2953 foreach ($custom_tabs as $tab) {
2954 $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
2955 $tab_content = isset($tab['content']) ? $tab['content'] : '';
2956
2957 if (!empty($tab_title) && !empty($tab_content)) {
2958 $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
2959 }
2960 }
2961 }
2962
2963 // Also check for reusable/saved tabs applied to this product
2964 $applied_saved_tabs = get_post_meta($post_id, 'yikes_woo_reusable_products_tabs_applied', true);
2965 if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
2966 $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
2967 if (!empty($saved_tabs) && is_array($saved_tabs)) {
2968 foreach ($applied_saved_tabs as $saved_tab_id) {
2969 if (isset($saved_tabs[$saved_tab_id])) {
2970 $tab = $saved_tabs[$saved_tab_id];
2971 $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
2972 $tab_content = isset($tab['content']) ? $tab['content'] : '';
2973
2974 if (!empty($tab_title) && !empty($tab_content)) {
2975 $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
2976 }
2977 }
2978 }
2979 }
2980 }
2981 }
2982
2983 // ADD ACF FIELDS SUPPORT
2984 $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
2985 if (!empty($acf_fields)) {
2986 $acf_content_parts = array();
2987
2988 foreach ($acf_fields as $field_name => $field_value) {
2989 $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
2990
2991 if (!empty($formatted_value)) {
2992 $field_label = ucwords(str_replace('_', ' ', $field_name));
2993 $acf_content_parts[] = $field_label . ": " . $formatted_value;
2994 }
2995 }
2996
2997 if (!empty($acf_content_parts)) {
2998 $content .= "\n\n" . implode("\n", $acf_content_parts);
2999 }
3000 }
3001
3002 // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
3003 $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
3004 if (!empty($custom_meta)) {
3005 $meta_content_parts = array();
3006
3007 foreach ($custom_meta as $meta_key => $meta_value) {
3008 // Convert meta key to readable label
3009 $meta_label = ucwords(str_replace(array('_', '-'), ' ', $meta_key));
3010 $meta_content_parts[] = $meta_label . ": " . $meta_value;
3011 }
3012
3013 if (!empty($meta_content_parts)) {
3014 $content .= "\n\n" . implode("\n", $meta_content_parts);
3015 }
3016 }
3017
3018 // Debug logging for WordPress Import content
3019 error_log('[MXCHAT-WP-IMPORT-DEBUG] Post ID: ' . $post_id . ' Title: ' . $post->post_title);
3020 error_log('[MXCHAT-WP-IMPORT-DEBUG] Raw post_content length: ' . strlen($post->post_content));
3021 error_log('[MXCHAT-WP-IMPORT-DEBUG] Final content length: ' . strlen($content));
3022 error_log('[MXCHAT-WP-IMPORT-DEBUG] Content preview: ' . substr($content, 0, 300));
3023
3024 // Note: Removed 10,000 char limit - chunking now handles large content properly
3025
3026 // Get bot-specific API key
3027 $bot_options = $this->get_bot_options($bot_id);
3028 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
3029 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3030
3031 if (strpos($selected_model, 'voyage') === 0) {
3032 $api_key = $options['voyage_api_key'] ?? '';
3033 $provider_name = 'Voyage AI';
3034 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3035 $api_key = $options['gemini_api_key'] ?? '';
3036 $provider_name = 'Google Gemini';
3037 } else {
3038 $api_key = $options['api_key'] ?? '';
3039 $provider_name = 'OpenAI';
3040 }
3041
3042 if (empty($api_key)) {
3043 wp_send_json_error($provider_name . ' API key not configured');
3044 exit;
3045 }
3046
3047 $source_url = get_permalink($post_id);
3048 $vector_id = md5($source_url); // Vector ID for Pinecone
3049
3050 // Check for existing content in bot-specific storage
3051 $is_update = false;
3052
3053 // Get bot-specific Pinecone configuration
3054 $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
3055 $use_pinecone = !empty($bot_pinecone_config) && ($bot_pinecone_config['use_pinecone'] ?? false);
3056
3057 if ($use_pinecone && !empty($bot_pinecone_config['api_key'])) {
3058 // Check Pinecone for this bot
3059 $pinecone_data = $this->mxchat_get_pinecone_processed_content($bot_pinecone_config);
3060 if (isset($pinecone_data[$post_id])) {
3061 $is_update = true;
3062 }
3063 } else {
3064 // Check WordPress DB (same as before since it's shared)
3065 global $wpdb;
3066 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3067 $existing_record = $wpdb->get_row($wpdb->prepare(
3068 "SELECT id FROM $table_name WHERE source_url = %s",
3069 $source_url
3070 ));
3071
3072 if ($existing_record) {
3073 $is_update = true;
3074 }
3075 }
3076
3077 // UPDATED 2.5.6: Determine content type based on post_type
3078 $post_type = $post->post_type;
3079 $content_type = 'content'; // Default fallback
3080
3081 // Map WordPress post types to content types
3082 switch ($post_type) {
3083 case 'post':
3084 $content_type = 'post';
3085 break;
3086 case 'page':
3087 $content_type = 'page';
3088 break;
3089 case 'product':
3090 $content_type = 'product';
3091 break;
3092 default:
3093 // For custom post types, use the post type name
3094 $content_type = sanitize_key($post_type);
3095 break;
3096 }
3097
3098 // Use the centralized utility function with bot_id and content_type
3099 $result = MxChat_Utils::submit_content_to_db(
3100 $content,
3101 $source_url,
3102 $api_key,
3103 $vector_id,
3104 $bot_id,
3105 $content_type
3106 );
3107
3108 if (is_wp_error($result)) {
3109 wp_send_json_error('Storage failed: ' . $result->get_error_message());
3110 exit;
3111 }
3112
3113 // Automatically apply role restriction based on tags
3114 $this->apply_role_restriction_to_post($post_id, $source_url);
3115
3116 $operation_type = $is_update ? 'update' : 'new';
3117
3118 // Count ACF fields for debugging
3119 $acf_field_count = count($acf_fields);
3120
3121 // Success response with minimal data
3122 wp_send_json_success(array(
3123 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully',
3124 'post_id' => $post_id,
3125 'title' => $post->post_title,
3126 'operation_type' => $operation_type,
3127 'vector_id' => $vector_id,
3128 'acf_fields_found' => $acf_field_count,
3129 'content_preview' => substr($content, 0, 100) . '...',
3130 'bot_id' => $bot_id
3131 ));
3132 exit;
3133 }
3134
3135 private function apply_role_restriction_to_post($post_id, $source_url) {
3136 // Get tag-role mappings
3137 $mappings = get_option('mxchat_tag_role_mappings', array());
3138
3139 if (empty($mappings)) {
3140 return; // No mappings, leave as public
3141 }
3142
3143 // Get all tags for the post
3144 $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
3145
3146 if (empty($post_tags)) {
3147 return; // No tags, leave as public
3148 }
3149
3150 // Determine the highest role restriction based on tags
3151 $highest_role = 'public';
3152 $role_hierarchy = array(
3153 'public' => 0,
3154 'logged_in' => 1,
3155 'subscriber' => 2,
3156 'contributor' => 3,
3157 'author' => 4,
3158 'editor' => 5,
3159 'administrator' => 6
3160 );
3161
3162 foreach ($post_tags as $tag_slug) {
3163 if (isset($mappings[$tag_slug])) {
3164 $role = $mappings[$tag_slug];
3165 if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
3166 $highest_role = $role;
3167 }
3168 }
3169 }
3170
3171 // If no restricted tags found, return (leave as public)
3172 if ($highest_role === 'public') {
3173 return;
3174 }
3175
3176 // Update the role restriction in the database
3177 global $wpdb;
3178
3179 // Check if using Pinecone
3180 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3181 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3182
3183 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3184 // Update Pinecone role restriction
3185 $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
3186 $vector_id = md5($source_url);
3187
3188 $wpdb->replace(
3189 $roles_table,
3190 array(
3191 'vector_id' => $vector_id,
3192 'role_restriction' => $highest_role,
3193 'updated_at' => current_time('mysql')
3194 ),
3195 array('%s', '%s', '%s')
3196 );
3197 } else {
3198 // Update WordPress DB
3199 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3200
3201 $wpdb->update(
3202 $table_name,
3203 array('role_restriction' => $highest_role),
3204 array('source_url' => $source_url),
3205 array('%s'),
3206 array('%s')
3207 );
3208 }
3209 }
3210
3211 public function mxchat_get_public_post_types() {
3212 // Get all public post types
3213 $post_types = get_post_types(array('public' => true), 'objects');
3214 $post_type_options = array();
3215
3216 foreach ($post_types as $post_type) {
3217 $post_type_options[$post_type->name] = $post_type->label;
3218 }
3219
3220 // Also include common forum/community post types that might not be marked as public
3221 $additional_types = array(
3222 'topic' => 'Forum Topics (bbPress)',
3223 'reply' => 'Forum Replies (bbPress)',
3224 'forum' => 'Forums (bbPress)',
3225 'wpforo_topic' => 'wpForo Topics',
3226 'wpforo_post' => 'wpForo Posts'
3227 );
3228
3229 foreach ($additional_types as $type_name => $type_label) {
3230 if (post_type_exists($type_name) && !isset($post_type_options[$type_name])) {
3231 $post_type_options[$type_name] = $type_label;
3232 }
3233 }
3234
3235 return $post_type_options;
3236 }
3237
3238 /**
3239 * Retrieves processed content from Pinecone API
3240 */
3241 public function mxchat_get_pinecone_processed_content($pinecone_options) {
3242 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3243 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3244
3245 if (empty($api_key) || empty($host)) {
3246 return array();
3247 }
3248
3249 $pinecone_data = array();
3250
3251 try {
3252 // Always get fresh data from Pinecone
3253 $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
3254
3255 // Method 2: Final fallback - try stats endpoint (if available)
3256 if (empty($pinecone_data)) {
3257 $stats_url = "https://{$host}/describe_index_stats";
3258
3259 $response = wp_remote_post($stats_url, array(
3260 'headers' => array(
3261 'Api-Key' => $api_key,
3262 'Content-Type' => 'application/json'
3263 ),
3264 'body' => json_encode(array()),
3265 'timeout' => 30
3266 ));
3267
3268 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
3269 $body = wp_remote_retrieve_body($response);
3270 $stats_data = json_decode($body, true);
3271 }
3272 }
3273
3274 } catch (Exception $e) {
3275 // Log error but return fresh data only
3276 }
3277
3278 return $pinecone_data;
3279 }
3280 public function mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
3281 //error_log('=== DEBUG: Starting mxchat_fetch_pinecone_vectors_by_ids ===');
3282
3283 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3284 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3285
3286 if (empty($api_key) || empty($host) || empty($vector_ids)) {
3287 //error_log('DEBUG: Missing parameters for fetch by IDs');
3288 return array();
3289 }
3290
3291 try {
3292 $fetch_url = "https://{$host}/vectors/fetch";
3293 //error_log('DEBUG: Fetch URL: ' . $fetch_url);
3294 //error_log('DEBUG: Fetching ' . count($vector_ids) . ' vector IDs');
3295
3296 // Pinecone fetch API allows fetching specific vectors by ID
3297 $fetch_data = array(
3298 'ids' => array_values($vector_ids)
3299 );
3300
3301 $response = wp_remote_post($fetch_url, array(
3302 'headers' => array(
3303 'Api-Key' => $api_key,
3304 'Content-Type' => 'application/json'
3305 ),
3306 'body' => json_encode($fetch_data),
3307 'timeout' => 30
3308 ));
3309
3310 if (is_wp_error($response)) {
3311 //error_log('DEBUG: Fetch by IDs WP error: ' . $response->get_error_message());
3312 return array();
3313 }
3314
3315 $response_code = wp_remote_retrieve_response_code($response);
3316 //error_log('DEBUG: Fetch response code: ' . $response_code);
3317
3318 if ($response_code !== 200) {
3319 $error_body = wp_remote_retrieve_body($response);
3320 //error_log('DEBUG: Fetch failed with body: ' . $error_body);
3321 return array();
3322 }
3323
3324 $body = wp_remote_retrieve_body($response);
3325 $data = json_decode($body, true);
3326
3327 //error_log('DEBUG: Fetch response structure: ' . print_r(array_keys($data), true));
3328
3329 if (!isset($data['vectors'])) {
3330 //error_log('DEBUG: No vectors key in response');
3331 return array();
3332 }
3333
3334 $processed_data = array();
3335
3336 foreach ($data['vectors'] as $vector_id => $vector_data) {
3337 $metadata = $vector_data['metadata'] ?? array();
3338 $source_url = $metadata['source_url'] ?? '';
3339
3340 if (!empty($source_url)) {
3341 $post_id = url_to_postid($source_url);
3342 if ($post_id) {
3343 $created_at = $metadata['created_at'] ?? '';
3344 $processed_date = 'Recently';
3345
3346 if (!empty($created_at)) {
3347 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
3348 if ($timestamp) {
3349 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
3350 }
3351 }
3352
3353 $processed_data[$post_id] = array(
3354 'db_id' => $vector_id,
3355 'processed_date' => $processed_date,
3356 'url' => $source_url,
3357 'source' => 'pinecone',
3358 'timestamp' => $timestamp ?? current_time('timestamp')
3359 );
3360 }
3361 }
3362 }
3363
3364 //error_log('DEBUG: Processed ' . count($processed_data) . ' vectors from fetch');
3365 return $processed_data;
3366
3367 } catch (Exception $e) {
3368 //error_log('DEBUG: Exception in fetch_pinecone_vectors_by_ids: ' . $e->getMessage());
3369 return array();
3370 }
3371 }
3372
3373 /**
3374 * Scan Pinecone for processed content
3375 */
3376 public function mxchat_scan_pinecone_for_processed_content($pinecone_options) {
3377 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3378 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3379
3380 if (empty($api_key) || empty($host)) {
3381 return array();
3382 }
3383
3384 try {
3385 // Use multiple random vectors to get better coverage
3386 $all_matches = array();
3387 $seen_ids = array();
3388
3389 // Try 3 different random vectors to get better coverage
3390 for ($i = 0; $i < 3; $i++) {
3391 $query_url = "https://{$host}/query";
3392
3393 // Generate a random unit vector instead of zeros
3394 $random_vector = array();
3395 for ($j = 0; $j < 1536; $j++) {
3396 $random_vector[] = (rand(-1000, 1000) / 1000.0);
3397 }
3398
3399 // Normalize the vector to unit length
3400 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
3401 if ($magnitude > 0) {
3402 $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
3403 }
3404
3405 $query_data = array(
3406 'includeMetadata' => true,
3407 'includeValues' => false,
3408 'topK' => 10000,
3409 'vector' => $random_vector
3410 );
3411
3412 $response = wp_remote_post($query_url, array(
3413 'headers' => array(
3414 'Api-Key' => $api_key,
3415 'Content-Type' => 'application/json'
3416 ),
3417 'body' => json_encode($query_data),
3418 'timeout' => 30
3419 ));
3420
3421 if (is_wp_error($response)) {
3422 continue;
3423 }
3424
3425 $response_code = wp_remote_retrieve_response_code($response);
3426
3427 if ($response_code !== 200) {
3428 continue;
3429 }
3430
3431 $body = wp_remote_retrieve_body($response);
3432 $data = json_decode($body, true);
3433
3434 if (isset($data['matches'])) {
3435 foreach ($data['matches'] as $match) {
3436 $match_id = $match['id'] ?? '';
3437 if (!empty($match_id) && !isset($seen_ids[$match_id])) {
3438 $all_matches[] = $match;
3439 $seen_ids[$match_id] = true;
3440 }
3441 }
3442 }
3443 }
3444
3445 // Convert matches to processed data format, grouping by URL to count chunks
3446 $processed_data = array();
3447 $url_chunk_counts = array();
3448
3449 foreach ($all_matches as $match) {
3450 $metadata = $match['metadata'] ?? array();
3451 $source_url = $metadata['source_url'] ?? '';
3452 $match_id = $match['id'] ?? '';
3453
3454 if (!empty($source_url) && !empty($match_id)) {
3455 $post_id = url_to_postid($source_url);
3456 if ($post_id) {
3457 // Count chunks per post_id
3458 if (!isset($url_chunk_counts[$post_id])) {
3459 $url_chunk_counts[$post_id] = 0;
3460 }
3461 $url_chunk_counts[$post_id]++;
3462
3463 $created_at = $metadata['created_at'] ?? '';
3464 $processed_date = 'Recently';
3465
3466 if (!empty($created_at)) {
3467 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
3468 if ($timestamp) {
3469 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
3470 }
3471 }
3472
3473 // Only store if not already set, or update with newer timestamp
3474 if (!isset($processed_data[$post_id]) ||
3475 ($timestamp ?? 0) > ($processed_data[$post_id]['timestamp'] ?? 0)) {
3476 $processed_data[$post_id] = array(
3477 'db_id' => $match_id,
3478 'processed_date' => $processed_date,
3479 'url' => $source_url,
3480 'source' => 'pinecone',
3481 'timestamp' => $timestamp ?? current_time('timestamp')
3482 );
3483 }
3484 }
3485 }
3486 }
3487
3488 // Add chunk counts to processed data
3489 foreach ($url_chunk_counts as $post_id => $chunk_count) {
3490 if (isset($processed_data[$post_id])) {
3491 $processed_data[$post_id]['chunk_count'] = $chunk_count;
3492 }
3493 }
3494
3495 return $processed_data;
3496
3497 } catch (Exception $e) {
3498 return array();
3499 }
3500 }
3501 /**
3502 * Generate embeddings from input text for MXChat with bot support
3503 */
3504 private function mxchat_generate_embedding($text, $bot_id = 'default') {
3505 // Enable detailed logging for debugging
3506 //error_log('[MXCHAT-EMBED] Starting embedding generation for bot: ' . $bot_id . '. Text length: ' . strlen($text) . ' bytes');
3507 //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
3508
3509 // Get bot-specific options
3510 $bot_options = $this->get_bot_options($bot_id);
3511 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
3512
3513 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3514 //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model);
3515
3516 // Determine provider and endpoint
3517 if (strpos($selected_model, 'voyage') === 0) {
3518 $api_key = $options['voyage_api_key'] ?? '';
3519 $endpoint = 'https://api.voyageai.com/v1/embeddings';
3520 $provider_name = 'Voyage AI';
3521 //error_log('[MXCHAT-EMBED] Using Voyage AI API for bot ' . $bot_id);
3522 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3523 $api_key = $options['gemini_api_key'] ?? '';
3524 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
3525 $provider_name = 'Google Gemini';
3526 //error_log('[MXCHAT-EMBED] Using Google Gemini API for bot ' . $bot_id);
3527 } else {
3528 $api_key = $options['api_key'] ?? '';
3529 $endpoint = 'https://api.openai.com/v1/embeddings';
3530 $provider_name = 'OpenAI';
3531 //error_log('[MXCHAT-EMBED] Using OpenAI API for bot ' . $bot_id);
3532 }
3533
3534 //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
3535
3536 if (empty($api_key)) {
3537 $error_message = sprintf('Missing %s API key for bot %s. Please configure your API key in the bot settings.', $provider_name, $bot_id);
3538 //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
3539 return $error_message;
3540 }
3541
3542 // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
3543 $estimated_tokens = ceil(str_word_count($text) / 0.75);
3544 //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
3545
3546 if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
3547 //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
3548 // Consider truncating text here
3549 }
3550
3551 // Prepare request body based on provider
3552 if (strpos($selected_model, 'gemini-embedding') === 0) {
3553 // Gemini API format
3554 $request_body = array(
3555 'model' => 'models/' . $selected_model,
3556 'content' => array(
3557 'parts' => array(
3558 array('text' => $text)
3559 )
3560 )
3561 );
3562
3563 // Set output dimensionality to 1536 for consistency with other models
3564 $request_body['outputDimensionality'] = 1536;
3565 } else {
3566 // OpenAI/Voyage API format
3567 $request_body = array(
3568 'model' => $selected_model,
3569 'input' => $text
3570 );
3571
3572 // Add output_dimension for voyage-3-large model
3573 if ($selected_model === 'voyage-3-large') {
3574 $request_body['output_dimension'] = 2048;
3575 }
3576 }
3577
3578 //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
3579
3580 // Prepare headers based on provider
3581 if (strpos($selected_model, 'gemini-embedding') === 0) {
3582 // Gemini uses API key as query parameter
3583 $endpoint .= '?key=' . $api_key;
3584 $headers = array(
3585 'Content-Type' => 'application/json'
3586 );
3587 } else {
3588 // OpenAI/Voyage use Bearer token
3589 $headers = array(
3590 'Authorization' => 'Bearer ' . $api_key,
3591 'Content-Type' => 'application/json'
3592 );
3593 }
3594
3595 // Make API request
3596 //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
3597 $response = wp_remote_post($endpoint, array(
3598 'body' => wp_json_encode($request_body),
3599 'headers' => $headers,
3600 'timeout' => 60 // Increased timeout for large inputs
3601 ));
3602
3603 // Handle wp_remote_post errors
3604 if (is_wp_error($response)) {
3605 $error_message = $response->get_error_message();
3606 //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
3607 return 'Connection error: ' . $error_message;
3608 }
3609
3610 // Get and check HTTP response code
3611 $http_code = wp_remote_retrieve_response_code($response);
3612 //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
3613
3614 if ($http_code !== 200) {
3615 $error_body = wp_remote_retrieve_body($response);
3616 //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
3617
3618 // Try to parse error for more details
3619 $error_json = json_decode($error_body, true);
3620 if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
3621 $error_type = $error_json['error']['type'] ?? 'unknown';
3622 $error_message = $error_json['error']['message'] ?? 'No message';
3623 //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
3624 //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
3625
3626 // Customize error message for common API errors
3627 if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
3628 $error_message = sprintf('Invalid %s API key for bot %s. Please check your API key in the bot settings.', $provider_name, $bot_id);
3629 } elseif ($error_type === 'authentication_error') {
3630 $error_message = sprintf('%s authentication failed for bot %s. Please verify your API key in the bot settings.', $provider_name, $bot_id);
3631 }
3632
3633 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
3634 return $error_message;
3635 }
3636
3637 $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding for bot %s", $http_code, $bot_id);
3638 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
3639 return $error_message;
3640 }
3641
3642 // Parse response body
3643 $response_body = wp_remote_retrieve_body($response);
3644 //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
3645
3646 $response_data = json_decode($response_body, true);
3647
3648 if (json_last_error() !== JSON_ERROR_NONE) {
3649 $error = json_last_error_msg();
3650 //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
3651 //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
3652 return "Failed to parse API response: $error";
3653 }
3654
3655 // Handle different response formats based on provider
3656 if (strpos($selected_model, 'gemini-embedding') === 0) {
3657 // Gemini API response format
3658 if (isset($response_data['embedding']['values'])) {
3659 $embedding_dimensions = count($response_data['embedding']['values']);
3660 //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
3661
3662 // Check if embedding dimensions are as expected (should be 1536)
3663 if ($embedding_dimensions !== 1536) {
3664 //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
3665 }
3666
3667 return $response_data['embedding']['values'];
3668 } else {
3669 //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
3670 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
3671
3672 if (isset($response_data['error'])) {
3673 $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
3674 //error_log('[MXCHAT-EMBED] ' . $error_message);
3675 return $error_message;
3676 }
3677
3678 $error_message = "Invalid Gemini API response format: No embedding found";
3679 //error_log('[MXCHAT-EMBED] ' . $error_message);
3680 return $error_message;
3681 }
3682 } else {
3683 // OpenAI/Voyage API response format
3684 if (isset($response_data['data'][0]['embedding'])) {
3685 $embedding_dimensions = count($response_data['data'][0]['embedding']);
3686 //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
3687
3688 // Check if embedding dimensions are as expected
3689 if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
3690 ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
3691 //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
3692 }
3693
3694 return $response_data['data'][0]['embedding'];
3695 } else {
3696 //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
3697 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
3698
3699 if (isset($response_data['error'])) {
3700 $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
3701 //error_log('[MXCHAT-EMBED] ' . $error_message);
3702 return $error_message;
3703 }
3704
3705 $error_message = "Invalid API response format: No embedding found";
3706 //error_log('[MXCHAT-EMBED] ' . $error_message);
3707 return $error_message;
3708 }
3709 }
3710 }
3711
3712 /**
3713 * Get bot-specific options for multi-bot functionality
3714 * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
3715 */
3716 private function get_bot_options($bot_id = 'default') {
3717 //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
3718
3719 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
3720 //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
3721 return array();
3722 }
3723
3724 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
3725
3726 if (!empty($bot_options)) {
3727 //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
3728 if (isset($bot_options['similarity_threshold'])) {
3729 //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
3730 }
3731 }
3732
3733 return is_array($bot_options) ? $bot_options : array();
3734 }
3735
3736 /**
3737 * Get bot-specific Pinecone configuration
3738 * Used in the knowledge retrieval functions
3739 */
3740 // Also add debugging to your get_bot_pinecone_config function
3741 private function get_bot_pinecone_config($bot_id = 'default') {
3742 //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
3743
3744 // If default bot or multi-bot add-on not active, use default Pinecone config
3745 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
3746 //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
3747 $addon_options = get_option('mxchat_pinecone_addon_options', array());
3748 $config = array(
3749 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
3750 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
3751 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
3752 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
3753 );
3754 //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
3755 return $config;
3756 }
3757
3758 //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
3759
3760 // Hook for multi-bot add-on to provide bot-specific Pinecone config
3761 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
3762
3763 if (!empty($bot_pinecone_config)) {
3764 //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
3765 //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
3766 //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
3767 //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
3768 } else {
3769 //error_log("MXCHAT DEBUG: Filter returned empty config!");
3770 }
3771
3772 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
3773 }
3774
3775
3776 public function mxchat_ajax_dismiss_completed_status() {
3777 try {
3778 // Verify the request
3779 check_ajax_referer('mxchat_status_nonce', 'nonce');
3780
3781 if (!current_user_can('manage_options')) {
3782 wp_send_json_error('Unauthorized access');
3783 exit;
3784 }
3785
3786 $card_type = isset($_POST['card_type']) ? sanitize_text_field($_POST['card_type']) : '';
3787
3788 if ($card_type === 'pdf') {
3789 // Clear PDF status
3790 $pdf_url = get_transient('mxchat_last_pdf_url');
3791 if ($pdf_url) {
3792 delete_transient('mxchat_pdf_status_' . md5($pdf_url));
3793 delete_transient('mxchat_last_pdf_url');
3794 }
3795 } elseif ($card_type === 'sitemap') {
3796 // Clear sitemap status
3797 $sitemap_url = get_transient('mxchat_last_sitemap_url');
3798 if ($sitemap_url) {
3799 delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
3800 delete_transient('mxchat_last_sitemap_url');
3801 }
3802 }
3803
3804 wp_send_json_success(array('message' => 'Status dismissed successfully'));
3805
3806 } catch (Exception $e) {
3807 wp_send_json_error(array('message' => 'Error dismissing status: ' . $e->getMessage()));
3808 }
3809 }
3810
3811 /**
3812 * Render completed status cards on page load
3813 * This ensures completed processing status persists through page refreshes
3814 */
3815 public function mxchat_render_completed_status_cards() {
3816 $output = '';
3817
3818 // Check for completed PDF status
3819 $pdf_url = get_transient('mxchat_last_pdf_url');
3820 if ($pdf_url) {
3821 $pdf_status = $this->mxchat_get_pdf_processing_status($pdf_url);
3822 if ($pdf_status && ($pdf_status['status'] === 'complete' || $pdf_status['status'] === 'error')) {
3823 $output .= $this->mxchat_render_pdf_status_card($pdf_status, $pdf_url);
3824 }
3825 }
3826
3827 // Check for completed sitemap status
3828 $sitemap_url = get_transient('mxchat_last_sitemap_url');
3829 if ($sitemap_url) {
3830 $sitemap_status = $this->mxchat_get_sitemap_processing_status($sitemap_url);
3831 if ($sitemap_status && ($sitemap_status['status'] === 'complete' || $sitemap_status['status'] === 'error')) {
3832 $output .= $this->mxchat_render_sitemap_status_card($sitemap_status, $sitemap_url);
3833 }
3834 }
3835
3836 return $output;
3837 }
3838
3839 /**
3840 * Render PDF status card HTML
3841 */
3842 private function mxchat_render_pdf_status_card($status, $pdf_url) {
3843 $html = '<div class="mxchat-status-card" data-card-type="pdf">';
3844 $html .= '<div class="mxchat-status-header">';
3845 $html .= '<h4>' . esc_html__('PDF Processing Status', 'mxchat') . '</h4>';
3846
3847 // Add dismiss button for completed status
3848 if ($status['status'] === 'complete' || $status['status'] === 'error') {
3849 $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
3850 }
3851
3852 // Process Batch button for processing status
3853 if ($status['status'] === 'processing') {
3854 $html .= '<button type="button" class="mxchat-manual-batch-btn"
3855 data-process-type="pdf"
3856 data-url="' . esc_attr($pdf_url) . '">
3857 ' . esc_html__('Process Batch', 'mxchat') . '</button>';
3858 }
3859
3860 // Add status badges
3861 if ($status['status'] === 'error') {
3862 $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
3863 } elseif ($status['status'] === 'complete') {
3864 if ($status['failed_pages'] > 0) {
3865 $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
3866 sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_pages']) . '</span>';
3867 } else {
3868 $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
3869 }
3870 }
3871
3872 $html .= '</div>'; // End header
3873
3874 // Progress bar
3875 $html .= '<div class="mxchat-progress-bar">';
3876 $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
3877 $html .= '</div>';
3878
3879 // Status details
3880 $html .= '<div class="mxchat-status-details">';
3881 $html .= '<p>' . sprintf(
3882 esc_html__('Progress: %d of %d pages (%d%%)', 'mxchat'),
3883 $status['processed_pages'],
3884 $status['total_pages'],
3885 $status['percentage']
3886 ) . '</p>';
3887
3888 // Show failed pages count if any
3889 if ($status['failed_pages'] > 0) {
3890 $html .= '<p><strong>' . esc_html__('Failed pages:', 'mxchat') . '</strong> ' . esc_html($status['failed_pages']) . '</p>';
3891 }
3892
3893 $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
3894 $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
3895
3896 // Add completion summary if available AND it's an array
3897 if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
3898 $summary = $status['completion_summary'];
3899 $html .= '<div class="mxchat-completion-summary">';
3900 $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
3901 $html .= '<p><strong>' . esc_html__('Total Pages:', 'mxchat') . '</strong> ' . esc_html($summary['total_pages']) . '</p>';
3902 $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_pages']) . '</p>';
3903 $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_pages']) . '</p>';
3904 $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
3905 $html .= '</div>';
3906 }
3907
3908 // Add failed pages list if any AND it's an array
3909 if (isset($status['failed_pages_list']) && is_array($status['failed_pages_list']) && !empty($status['failed_pages_list'])) {
3910 $html .= $this->mxchat_render_failed_pages_list($status['failed_pages_list']);
3911 }
3912
3913 // Add error message if any
3914 if (isset($status['error']) && !empty($status['error'])) {
3915 $html .= '<div class="mxchat-error-notice">';
3916 $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
3917 $html .= '</div>';
3918 }
3919
3920 $html .= '</div>'; // End details
3921 $html .= '</div>'; // End card
3922
3923 return $html;
3924 }
3925 /**
3926 * Render sitemap status card HTML
3927 */
3928 private function mxchat_render_sitemap_status_card($status, $sitemap_url) {
3929 $html = '<div class="mxchat-status-card" data-card-type="sitemap">';
3930 $html .= '<div class="mxchat-status-header">';
3931 $html .= '<h4>' . esc_html__('Sitemap Processing Status', 'mxchat') . '</h4>';
3932
3933 // Add dismiss button for completed status
3934 if ($status['status'] === 'complete' || $status['status'] === 'error') {
3935 $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
3936 }
3937
3938 // Process Batch button for processing status
3939 if ($status['status'] === 'processing') {
3940 $html .= '<button type="button" class="mxchat-manual-batch-btn"
3941 data-process-type="sitemap"
3942 data-url="' . esc_attr($sitemap_url) . '">
3943 ' . esc_html__('Process Batch', 'mxchat') . '</button>';
3944 }
3945
3946 // Add status badges
3947 if ($status['status'] === 'error') {
3948 $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
3949 } elseif ($status['status'] === 'complete') {
3950 if ($status['failed_urls'] > 0) {
3951 $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
3952 sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_urls']) . '</span>';
3953 } else {
3954 $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
3955 }
3956 }
3957
3958 $html .= '</div>'; // End header
3959
3960 // Progress bar
3961 $html .= '<div class="mxchat-progress-bar">';
3962 $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
3963 $html .= '</div>';
3964
3965 // Status details
3966 $html .= '<div class="mxchat-status-details">';
3967 $html .= '<p>' . sprintf(
3968 esc_html__('Progress: %d of %d URLs (%d%%)', 'mxchat'),
3969 $status['processed_urls'],
3970 $status['total_urls'],
3971 $status['percentage']
3972 ) . '</p>';
3973
3974 // Show failed URLs count if any
3975 if ($status['failed_urls'] > 0) {
3976 $html .= '<p><strong>' . esc_html__('Failed URLs:', 'mxchat') . '</strong> ' . esc_html($status['failed_urls']) . '</p>';
3977 }
3978
3979 $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
3980 $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
3981
3982 // Add completion summary if available AND it's an array
3983 if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
3984 $summary = $status['completion_summary'];
3985 $html .= '<div class="mxchat-completion-summary">';
3986 $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
3987 $html .= '<p><strong>' . esc_html__('Total URLs:', 'mxchat') . '</strong> ' . esc_html($summary['total_urls']) . '</p>';
3988 $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_urls']) . '</p>';
3989 $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_urls']) . '</p>';
3990 $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
3991 $html .= '</div>';
3992 }
3993
3994 // Add error messages if any (but not the failed URLs list)
3995 if (!empty($status['error']) || !empty($status['last_error'])) {
3996 $html .= '<div class="mxchat-error-notice">';
3997
3998 if (!empty($status['error'])) {
3999 $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
4000 }
4001
4002 if (!empty($status['last_error'])) {
4003 $html .= '<p class="last-error">' . esc_html__('Last error:', 'mxchat') . ' ' . esc_html($status['last_error']) . '</p>';
4004 }
4005
4006 $html .= '</div>';
4007 }
4008
4009 $html .= '</div>'; // End details
4010 $html .= '</div>'; // End card
4011
4012 return $html;
4013 }
4014
4015
4016 /**
4017 * Render failed pages list
4018 */
4019 private function mxchat_render_failed_pages_list($failed_pages_list) {
4020 // Validate that $failed_pages_list is an array and not empty
4021 if (!is_array($failed_pages_list) || empty($failed_pages_list)) {
4022 return '';
4023 }
4024
4025 $html = '<div class="mxchat-error-notice">';
4026 $html .= '<div class="mxchat-failed-pages-container">';
4027 $html .= '<h5>' . sprintf(esc_html__('Failed Pages (%d)', 'mxchat'), count($failed_pages_list)) . '</h5>';
4028 $html .= '<details>';
4029 $html .= '<summary>' . esc_html__('Show Failed Pages', 'mxchat') . '</summary>';
4030 $html .= '<div class="mxchat-failed-pages-list">';
4031
4032 // Create table for failed pages
4033 $html .= '<table class="widefat striped">';
4034 $html .= '<thead><tr>';
4035 $html .= '<th>' . esc_html__('Page', 'mxchat') . '</th>';
4036 $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
4037 $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
4038 $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
4039 $html .= '</tr></thead><tbody>';
4040
4041 // Sort failed pages by most recent
4042 $sorted_failed_pages = $failed_pages_list;
4043 usort($sorted_failed_pages, function($a, $b) {
4044 return ($b['time'] ?? 0) - ($a['time'] ?? 0);
4045 });
4046
4047 foreach ($sorted_failed_pages as $item) {
4048 // Ensure $item is an array before accessing its elements
4049 if (!is_array($item)) {
4050 continue;
4051 }
4052
4053 $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
4054 $html .= '<tr>';
4055 $html .= '<td>' . esc_html__('Page', 'mxchat') . ' ' . esc_html($item['page'] ?? 'Unknown') . '</td>';
4056 $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
4057 $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
4058 $html .= '<td>' . esc_html($time_ago) . '</td>';
4059 $html .= '</tr>';
4060 }
4061
4062 $html .= '</tbody></table>';
4063 $html .= '</div></details></div></div>';
4064
4065 return $html;
4066 }
4067
4068 /**
4069 * Render failed URLs list
4070 */
4071 private function mxchat_render_failed_urls_list($failed_urls_list) {
4072 // Validate that $failed_urls_list is an array and not empty
4073 if (!is_array($failed_urls_list) || empty($failed_urls_list)) {
4074 return '';
4075 }
4076
4077 $html = '<div class="mxchat-failed-urls-container">';
4078 $html .= '<h5>' . sprintf(esc_html__('Failed URLs (%d)', 'mxchat'), count($failed_urls_list)) . '</h5>';
4079 $html .= '<details>';
4080 $html .= '<summary>' . esc_html__('Show Failed URLs', 'mxchat') . '</summary>';
4081 $html .= '<div class="mxchat-failed-urls-list">';
4082
4083 // Create table for failed URLs
4084 $html .= '<table class="widefat striped">';
4085 $html .= '<thead><tr>';
4086 $html .= '<th>' . esc_html__('URL', 'mxchat') . '</th>';
4087 $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
4088 $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
4089 $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
4090 $html .= '</tr></thead><tbody>';
4091
4092 // Sort failed URLs by most recent
4093 $sorted_failed_urls = $failed_urls_list;
4094 usort($sorted_failed_urls, function($a, $b) {
4095 return ($b['time'] ?? 0) - ($a['time'] ?? 0);
4096 });
4097
4098 // Show up to 50 failed URLs
4099 $display_urls = array_slice($sorted_failed_urls, 0, 50);
4100
4101 foreach ($display_urls as $item) {
4102 // Ensure $item is an array before accessing its elements
4103 if (!is_array($item)) {
4104 continue;
4105 }
4106
4107 $url = $item['url'] ?? '';
4108 $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
4109
4110 // Truncate URL for display
4111 $display_url = strlen($url) > 50 ? substr($url, 0, 47) . '...' : $url;
4112
4113 $html .= '<tr>';
4114 $html .= '<td style="word-break: break-all;">';
4115 if (!empty($url)) {
4116 $html .= '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($display_url) . '</a>';
4117 } else {
4118 $html .= esc_html__('Unknown URL', 'mxchat');
4119 }
4120 $html .= '</td>';
4121 $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
4122 $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
4123 $html .= '<td>' . esc_html($time_ago) . '</td>';
4124 $html .= '</tr>';
4125 }
4126
4127 $html .= '</tbody></table>';
4128
4129 if (count($failed_urls_list) > 50) {
4130 $html .= '<div class="mxchat-failed-urls-more">+ ' .
4131 (count($failed_urls_list) - 50) .
4132 ' ' . esc_html__('more failed URLs not shown', 'mxchat') . '</div>';
4133 }
4134
4135 $html .= '</div></details></div>';
4136
4137 return $html;
4138 }
4139
4140 /**
4141 * Get all ACF fields for a specific post, excluding any fields the user has disabled
4142 */
4143 public function mxchat_get_acf_fields_for_post($post_id) {
4144 if (!function_exists('get_fields')) {
4145 return array();
4146 }
4147
4148 $fields = get_fields($post_id);
4149 if (!$fields || !is_array($fields)) {
4150 return array();
4151 }
4152
4153 // Get excluded fields from settings
4154 $excluded_fields = get_option('mxchat_acf_excluded_fields', array());
4155 if (!empty($excluded_fields) && is_array($excluded_fields)) {
4156 foreach ($excluded_fields as $excluded_field) {
4157 if (isset($fields[$excluded_field])) {
4158 unset($fields[$excluded_field]);
4159 }
4160 }
4161 }
4162
4163 return $fields;
4164 }
4165
4166 /**
4167 * Get all registered ACF field groups and their fields for the settings UI
4168 */
4169 public function mxchat_get_all_acf_fields() {
4170 if (!function_exists('acf_get_field_groups') || !function_exists('acf_get_fields')) {
4171 return array();
4172 }
4173
4174 $all_fields = array();
4175 $field_groups = acf_get_field_groups();
4176
4177 if (!empty($field_groups)) {
4178 foreach ($field_groups as $group) {
4179 $group_fields = acf_get_fields($group['key']);
4180 if (!empty($group_fields)) {
4181 $all_fields[$group['title']] = array();
4182 foreach ($group_fields as $field) {
4183 $all_fields[$group['title']][] = array(
4184 'name' => $field['name'],
4185 'label' => $field['label'],
4186 'type' => $field['type']
4187 );
4188 }
4189 }
4190 }
4191 }
4192
4193 return $all_fields;
4194 }
4195
4196 /**
4197 * Get whitelisted custom post meta for a given post
4198 * This allows non-ACF meta fields (like OptionTree, theme meta boxes) to be included in embeddings
4199 */
4200 public function mxchat_get_whitelisted_post_meta($post_id) {
4201 $whitelist = get_option('mxchat_custom_meta_whitelist', '');
4202
4203 if (empty($whitelist)) {
4204 return array();
4205 }
4206
4207 // Parse the whitelist - one meta key per line
4208 $meta_keys = array_filter(array_map('trim', explode("\n", $whitelist)));
4209
4210 if (empty($meta_keys)) {
4211 return array();
4212 }
4213
4214 $result = array();
4215
4216 foreach ($meta_keys as $key) {
4217 // Skip empty keys
4218 if (empty($key)) {
4219 continue;
4220 }
4221
4222 $value = get_post_meta($post_id, $key, true);
4223
4224 // Only include non-empty string values
4225 if (!empty($value) && is_string($value)) {
4226 $result[$key] = $value;
4227 } elseif (!empty($value) && is_array($value)) {
4228 // Handle array values by joining them
4229 $flat_value = $this->mxchat_flatten_meta_array($value);
4230 if (!empty($flat_value)) {
4231 $result[$key] = $flat_value;
4232 }
4233 }
4234 }
4235
4236 return $result;
4237 }
4238
4239 /**
4240 * Flatten array meta values into a readable string
4241 */
4242 private function mxchat_flatten_meta_array($array, $depth = 0) {
4243 if ($depth > 3) {
4244 return ''; // Prevent infinite recursion
4245 }
4246
4247 $parts = array();
4248
4249 foreach ($array as $key => $value) {
4250 if (is_string($value) && !empty($value)) {
4251 $parts[] = $value;
4252 } elseif (is_array($value)) {
4253 $nested = $this->mxchat_flatten_meta_array($value, $depth + 1);
4254 if (!empty($nested)) {
4255 $parts[] = $nested;
4256 }
4257 }
4258 }
4259
4260 return implode(', ', $parts);
4261 }
4262
4263 /**
4264 * Format ACF field values for content extraction
4265 */
4266 public function mxchat_format_acf_field_value($value, $field_name = '', $post_id = 0) {
4267 if (empty($value)) {
4268 return '';
4269 }
4270
4271 // Handle WP_Post objects first (THIS IS THE KEY FIX)
4272 if ($value instanceof WP_Post) {
4273 return $value->post_title ?: '';
4274 }
4275
4276 // Handle other WP objects
4277 if (is_object($value)) {
4278 if (isset($value->post_title)) {
4279 return $value->post_title;
4280 } elseif (isset($value->display_name)) {
4281 return $value->display_name;
4282 } elseif (isset($value->name)) {
4283 return $value->name;
4284 } elseif (method_exists($value, '__toString')) {
4285 try {
4286 return (string) $value;
4287 } catch (Exception $e) {
4288 return '';
4289 }
4290 }
4291 // For any other objects, return empty string
4292 return '';
4293 }
4294
4295 // Handle different ACF field types
4296 if (is_array($value)) {
4297 // Check if it's an image/file field
4298 if (isset($value['url'])) {
4299 // Image field - return alt text, title, or caption
4300 if (!empty($value['alt'])) {
4301 return $value['alt'];
4302 } elseif (!empty($value['title'])) {
4303 return $value['title'];
4304 } elseif (!empty($value['caption'])) {
4305 return $value['caption'];
4306 } else {
4307 return ''; // Don't include just the URL
4308 }
4309 }
4310
4311 // Check if it's a post object or relationship field
4312 if (isset($value['post_title'])) {
4313 return $value['post_title'];
4314 }
4315
4316 // Check if it's a user field
4317 if (isset($value['display_name'])) {
4318 return $value['display_name'];
4319 }
4320
4321 // Check if it's a taxonomy term
4322 if (isset($value['name']) && isset($value['taxonomy'])) {
4323 return $value['name'];
4324 }
4325
4326 // Check if it's a select field with label
4327 if (isset($value['label'])) {
4328 return $value['label'];
4329 }
4330
4331 // Check for repeater field or flexible content
4332 if (is_numeric(key($value))) {
4333 $sub_values = array();
4334 foreach ($value as $sub_item) {
4335 if (is_array($sub_item)) {
4336 // For repeater/flexible content, extract text values
4337 $sub_text = $this->mxchat_extract_text_from_acf_array($sub_item);
4338 if (!empty($sub_text)) {
4339 $sub_values[] = $sub_text;
4340 }
4341 } elseif ($sub_item instanceof WP_Post) {
4342 // Handle WP_Post objects in arrays
4343 $sub_values[] = $sub_item->post_title ?: '';
4344 } else {
4345 $sub_values[] = (string) $sub_item;
4346 }
4347 }
4348 return implode(', ', array_filter($sub_values));
4349 }
4350
4351 // For other arrays, try to extract meaningful text
4352 $text_values = array();
4353 foreach ($value as $key => $val) {
4354 if (is_string($val) && !empty(trim($val))) {
4355 $text_values[] = trim($val);
4356 } elseif ($val instanceof WP_Post) {
4357 // Handle WP_Post objects in associative arrays
4358 $text_values[] = $val->post_title ?: '';
4359 } elseif (is_array($val) && isset($val['post_title'])) {
4360 $text_values[] = $val['post_title'];
4361 } elseif (is_array($val) && isset($val['name'])) {
4362 $text_values[] = $val['name'];
4363 }
4364 }
4365
4366 return implode(', ', array_filter($text_values));
4367 }
4368
4369 // Handle boolean values
4370 if (is_bool($value)) {
4371 return $value ? 'Yes' : 'No';
4372 }
4373
4374 // Handle numeric values
4375 if (is_numeric($value)) {
4376 return (string) $value;
4377 }
4378
4379 // Handle string values
4380 if (is_string($value)) {
4381 return trim($value);
4382 }
4383
4384 // For anything else that we can't handle, return empty string
4385 // This prevents the "Object could not be converted to string" error
4386 return '';
4387 }
4388
4389 /**
4390 * Extract text from complex ACF array structures
4391 */
4392 private function mxchat_extract_text_from_acf_array($array) {
4393 if (!is_array($array)) {
4394 return '';
4395 }
4396
4397 $text_parts = array();
4398
4399 foreach ($array as $key => $value) {
4400 if (is_string($value) && !empty(trim($value))) {
4401 // Skip keys that are likely to be IDs or technical values
4402 if (!is_numeric($value) || strlen($value) > 10) {
4403 $text_parts[] = trim($value);
4404 }
4405 } elseif ($value instanceof WP_Post) {
4406 // Handle WP_Post objects
4407 $text_parts[] = $value->post_title ?: '';
4408 } elseif (is_array($value)) {
4409 if (isset($value['post_title'])) {
4410 $text_parts[] = $value['post_title'];
4411 } elseif (isset($value['name'])) {
4412 $text_parts[] = $value['name'];
4413 } elseif (isset($value['label'])) {
4414 $text_parts[] = $value['label'];
4415 }
4416 } elseif (is_object($value)) {
4417 // Handle other objects safely
4418 if (isset($value->post_title)) {
4419 $text_parts[] = $value->post_title;
4420 } elseif (isset($value->name)) {
4421 $text_parts[] = $value->name;
4422 } elseif (isset($value->display_name)) {
4423 $text_parts[] = $value->display_name;
4424 }
4425 }
4426 }
4427
4428 return implode(', ', array_filter($text_parts));
4429 }
4430
4431 /**
4432 * Handle ACF save - fires after ACF fields are saved
4433 * This ensures ACF field data is available when syncing to knowledge base
4434 */
4435 public function mxchat_handle_acf_save($post_id) {
4436 // Skip if not a valid post
4437 if (!$post_id || $post_id === 'options') {
4438 return;
4439 }
4440
4441 // Skip autosaves and revisions
4442 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
4443 return;
4444 }
4445
4446 $post = get_post($post_id);
4447 if (!$post) {
4448 return;
4449 }
4450
4451 $post_type = $post->post_type;
4452
4453 // Check if sync is enabled for this post type
4454 $should_sync = false;
4455
4456 if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
4457 $should_sync = true;
4458 } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
4459 $should_sync = true;
4460 } else if ($post_type === 'product' && class_exists('WooCommerce')) {
4461 // WooCommerce products - check if WooCommerce integration is enabled
4462 $options = get_option('mxchat_options', array());
4463 if (isset($options['enable_woocommerce_integration']) &&
4464 ($options['enable_woocommerce_integration'] === '1' || $options['enable_woocommerce_integration'] === 'on')) {
4465 $should_sync = true;
4466 }
4467 } else {
4468 // Check custom post types
4469 $option_name = 'mxchat_auto_sync_' . $post_type;
4470 if (get_option($option_name) === '1') {
4471 $should_sync = true;
4472 }
4473 }
4474
4475 if (!$should_sync) {
4476 return;
4477 }
4478
4479 // Only process published posts
4480 if ($post->post_status !== 'publish') {
4481 return;
4482 }
4483
4484 // Check if this post has any ACF fields - if not, no need to re-sync
4485 $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
4486 if (empty($acf_fields)) {
4487 return;
4488 }
4489
4490 // Use a transient to prevent duplicate processing (post_updated may have already run)
4491 $transient_key = 'mxchat_acf_synced_' . $post_id;
4492 if (get_transient($transient_key)) {
4493 return;
4494 }
4495 set_transient($transient_key, true, 60); // Prevent re-processing for 60 seconds
4496
4497 // Re-run the sync with ACF data now available
4498 // We pass $update=true since this is effectively an update with ACF data
4499 $this->mxchat_handle_post_update($post_id, $post, true);
4500 }
4501
4502 public function mxchat_handle_post_update($post_id, $post, $update) {
4503 // Basic validation checks
4504 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
4505 return;
4506 }
4507
4508 $post_type = $post->post_type;
4509
4510 // Check if sync is enabled for this post type
4511 $should_sync = false;
4512
4513 // Check built-in post types first
4514 if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
4515 $should_sync = true;
4516 } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
4517 $should_sync = true;
4518 } else {
4519 // Check custom post types
4520 $option_name = 'mxchat_auto_sync_' . $post_type;
4521 if (get_option($option_name) === '1') {
4522 $should_sync = true;
4523 }
4524 }
4525
4526 if (!$should_sync) {
4527 return;
4528 }
4529
4530 // Check if we have stored the previous status and URL in our transients
4531 $previous_status_key = 'mxchat_prev_status_' . $post_id;
4532 $previous_status = get_transient($previous_status_key);
4533
4534 $previous_url_key = 'mxchat_prev_url_' . $post_id;
4535 $previous_url = get_transient($previous_url_key);
4536
4537 // If the post was previously published but is now not published, remove from knowledge base
4538 if ($previous_status === 'publish' && $post->post_status !== 'publish') {
4539 // Use the stored URL from when it was published, or fall back to current permalink
4540 $source_url = $previous_url ?: get_permalink($post_id);
4541
4542 if ($source_url) {
4543 // Check if Pinecone is enabled
4544 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4545 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4546
4547 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
4548 // Delete from Pinecone
4549 $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
4550 } else {
4551 // Delete from WordPress DB
4552 global $wpdb;
4553 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4554
4555 $result = $wpdb->delete(
4556 $table_name,
4557 array('source_url' => $source_url),
4558 array('%s')
4559 );
4560 }
4561 }
4562
4563 // Clean up the transients and exit early
4564 delete_transient($previous_status_key);
4565 delete_transient($previous_url_key);
4566 return;
4567 }
4568
4569 // Store the current status for next time (if this is an update)
4570 if ($update) {
4571 set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
4572
4573 // If the post is currently published, also store its URL
4574 if ($post->post_status === 'publish') {
4575 $current_url = get_permalink($post_id);
4576 set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
4577 }
4578 }
4579
4580 // Only process currently published content for adding/updating
4581 if ($post->post_status === 'publish') {
4582 // Get the source URL
4583 $source_url = get_permalink($post_id);
4584
4585 // Get content with proper formatting (matching ajax_mxchat_process_selected_content)
4586 $title = get_the_title($post_id);
4587 $content = get_post_field('post_content', $post_id);
4588 $excerpt = get_post_field('post_excerpt', $post_id);
4589
4590 // Remove shortcode tags but preserve content inside them
4591 $content = $this->strip_shortcode_tags_preserve_content($content);
4592 $excerpt = $this->strip_shortcode_tags_preserve_content($excerpt);
4593
4594 // Strip tags but preserve structure (don't use 'the_content' filter as it may re-add shortcodes)
4595 $content = wp_strip_all_tags($content);
4596
4597 // Combine title, short description (if exists), and content
4598 $final_content = $title . "\n\n";
4599
4600 // Add short description if it exists (WooCommerce products use post_excerpt for short description)
4601 if (!empty($excerpt)) {
4602 $final_content .= "Short Description: " . wp_strip_all_tags($excerpt) . "\n\n";
4603 }
4604
4605 $final_content .= $content;
4606
4607 // For WooCommerce products, include pricing and product details
4608 if ($post_type === 'product' && class_exists('WooCommerce')) {
4609 $product = wc_get_product($post_id);
4610
4611 if ($product) {
4612 // Get pricing information
4613 $regular_price = $product->get_regular_price();
4614 $sale_price = $product->get_sale_price();
4615 $price = $product->get_price();
4616 $sku = $product->get_sku();
4617
4618 // Get currency symbol
4619 $currency_symbol = get_woocommerce_currency_symbol();
4620
4621 // Add pricing information
4622 $final_content .= "\n";
4623 if (!empty($regular_price)) {
4624 $final_content .= "Price: " . $currency_symbol . $regular_price . "\n";
4625 } elseif (!empty($price)) {
4626 $final_content .= "Price: " . $currency_symbol . $price . "\n";
4627 }
4628
4629 if (!empty($sale_price) && $sale_price !== $regular_price) {
4630 $final_content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
4631 }
4632
4633 // Handle variable products - show price range
4634 if ($product->is_type('variable')) {
4635 $min_price = $product->get_variation_price('min');
4636 $max_price = $product->get_variation_price('max');
4637 if ($min_price !== $max_price) {
4638 $final_content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
4639 }
4640 }
4641
4642 if (!empty($sku)) {
4643 $final_content .= "SKU: " . $sku . "\n";
4644 }
4645
4646 // Get product categories
4647 $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
4648 if (!empty($categories) && !is_wp_error($categories)) {
4649 $final_content .= "Categories: " . implode(', ', $categories) . "\n";
4650 }
4651 }
4652 }
4653
4654 // For custom post types like job_listing, include additional fields
4655 if ($post_type === 'job_listing') {
4656 // Add job-specific meta if available
4657 $job_location = get_post_meta($post_id, '_job_location', true);
4658 if (!empty($job_location)) {
4659 $final_content .= "\n\nLocation: " . $job_location;
4660 }
4661
4662 // Get job type terms
4663 $job_types = get_the_terms($post_id, 'job_listing_type');
4664 if (!empty($job_types) && !is_wp_error($job_types)) {
4665 $types = array();
4666 foreach ($job_types as $type) {
4667 $types[] = $type->name;
4668 }
4669 $final_content .= "\n\nJob Type: " . implode(', ', $types);
4670 }
4671
4672 // Get company name if available
4673 $company_name = get_post_meta($post_id, '_company_name', true);
4674 if (!empty($company_name)) {
4675 $final_content .= "\n\nCompany: " . $company_name;
4676 }
4677 }
4678
4679 // ADD ACF FIELDS SUPPORT (matches ajax_mxchat_process_selected_content behavior)
4680 $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
4681 if (!empty($acf_fields)) {
4682 $acf_content_parts = array();
4683
4684 foreach ($acf_fields as $field_name => $field_value) {
4685 $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
4686 if (!empty($formatted_value)) {
4687 // Convert field name to readable label
4688 $field_label = ucwords(str_replace(['_', '-'], ' ', $field_name));
4689 $acf_content_parts[] = $field_label . ": " . $formatted_value;
4690 }
4691 }
4692
4693 if (!empty($acf_content_parts)) {
4694 $final_content .= "\n\n" . implode("\n", $acf_content_parts);
4695 }
4696 }
4697
4698 // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
4699 $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
4700 if (!empty($custom_meta)) {
4701 $meta_content_parts = array();
4702
4703 foreach ($custom_meta as $meta_key => $meta_value) {
4704 // Convert meta key to readable label
4705 $meta_label = ucwords(str_replace(array('_', '-'), ' ', $meta_key));
4706 $meta_content_parts[] = $meta_label . ": " . $meta_value;
4707 }
4708
4709 if (!empty($meta_content_parts)) {
4710 $final_content .= "\n\n" . implode("\n", $meta_content_parts);
4711 }
4712 }
4713
4714 // Get API key with proper model detection
4715 $options = get_option('mxchat_options');
4716 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4717
4718 if (strpos($selected_model, 'voyage') === 0) {
4719 $api_key = $options['voyage_api_key'] ?? '';
4720 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
4721 $api_key = $options['gemini_api_key'] ?? '';
4722 } else {
4723 $api_key = $options['api_key'] ?? '';
4724 }
4725
4726 if (empty($api_key)) {
4727 return;
4728 }
4729
4730 // Use the centralized utility function for storage
4731 $result = MxChat_Utils::submit_content_to_db(
4732 $final_content,
4733 $source_url,
4734 $api_key,
4735 md5($source_url) // Vector ID for Pinecone
4736 );
4737
4738 // After successful storage, apply role restriction based on tags
4739 if (!is_wp_error($result)) {
4740 $this->apply_role_restriction_to_post($post_id, $source_url);
4741 }
4742 }
4743
4744 // Clean up the stored previous status if not used above
4745 if ($previous_status !== 'publish' || $post->post_status === 'publish') {
4746 delete_transient($previous_status_key);
4747 delete_transient($previous_url_key);
4748 }
4749 }
4750
4751 /**
4752 * Store the post status and URL before update to detect status transitions
4753 * This runs before the post is actually updated in the database
4754 */
4755 public function mxchat_store_pre_update_status($post_id, $data) {
4756 // Get the current post from database (before update)
4757 $current_post = get_post($post_id);
4758
4759 if ($current_post) {
4760 // Store the current status temporarily
4761 $status_key = 'mxchat_prev_status_' . $post_id;
4762 set_transient($status_key, $current_post->post_status, HOUR_IN_SECONDS);
4763
4764 // If the post is currently published, also store its URL
4765 if ($current_post->post_status === 'publish') {
4766 $url_key = 'mxchat_prev_url_' . $post_id;
4767 $current_url = get_permalink($post_id);
4768 set_transient($url_key, $current_url, HOUR_IN_SECONDS);
4769 }
4770 }
4771 }
4772
4773 public function mxchat_handle_post_delete($post_id) {
4774 // Get post data before it's deleted
4775 $post = get_post($post_id);
4776
4777 // Basic validation
4778 if (!$post || wp_is_post_revision($post_id)) {
4779 return;
4780 }
4781
4782 $post_type = $post->post_type;
4783
4784 // Check if sync is enabled for this post type
4785 $should_sync = false;
4786
4787 // Check built-in post types first
4788 if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
4789 $should_sync = true;
4790 } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
4791 $should_sync = true;
4792 } else {
4793 // Check custom post types
4794 $option_name = 'mxchat_auto_sync_' . $post_type;
4795 if (get_option($option_name) === '1') {
4796 $should_sync = true;
4797 }
4798 }
4799
4800 if (!$should_sync) {
4801 return;
4802 }
4803
4804 // Get the URL before post is deleted
4805 $source_url = get_permalink($post_id);
4806 if (!$source_url) {
4807 //error_log('MXChat: Failed to get permalink for post ' . $post_id);
4808 return;
4809 }
4810
4811 // Use chunk-aware deletion (handles both chunked and non-chunked content)
4812 $delete_result = MxChat_Utils::delete_chunks_for_url($source_url, 'default');
4813
4814 if (is_wp_error($delete_result)) {
4815 //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message());
4816 }
4817 }
4818
4819
4820 /**
4821 * Deletes data from Pinecone using a source URL
4822 */
4823 public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
4824 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
4825 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
4826
4827 if (empty($host) || empty($api_key)) {
4828 //error_log('MXChat: Pinecone deletion failed - missing configuration');
4829 return false;
4830 }
4831
4832 $api_endpoint = "https://{$host}/vectors/delete";
4833 $vector_id = md5($source_url);
4834
4835 $request_body = array(
4836 'ids' => array($vector_id)
4837 );
4838
4839 $response = wp_remote_post($api_endpoint, array(
4840 'headers' => array(
4841 'Api-Key' => $api_key,
4842 'accept' => 'application/json',
4843 'content-type' => 'application/json'
4844 ),
4845 'body' => wp_json_encode($request_body),
4846 'timeout' => 30
4847 ));
4848
4849 if (is_wp_error($response)) {
4850 //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
4851 return false;
4852 }
4853
4854 $response_code = wp_remote_retrieve_response_code($response);
4855 if ($response_code !== 200) {
4856 //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
4857 return false;
4858 }
4859
4860 return true;
4861 }
4862
4863
4864
4865 public function mxchat_handle_product_change($post_id, $post, $update) {
4866 if ($post->post_type !== 'product') {
4867 return;
4868 }
4869
4870 if ($post->post_status === 'publish') {
4871 add_action('shutdown', function() use ($post_id) {
4872 $product = wc_get_product($post_id);
4873 if ($product) {
4874 $this->mxchat_store_product_embedding($product);
4875 }
4876 });
4877 }
4878 }
4879
4880 /**
4881 * Store WooCommerce product embeddings
4882 */
4883 private function mxchat_store_product_embedding($product) {
4884 if (!isset($this->options['enable_woocommerce_integration']) ||
4885 !in_array($this->options['enable_woocommerce_integration'], ['1', 'on'])) {
4886 return;
4887 }
4888
4889 $source_url = get_permalink($product->get_id());
4890 $product_id = $product->get_id();
4891
4892 // Build product content
4893 $title = $product->get_name();
4894 $description = $product->get_description();
4895 $short_description = $product->get_short_description();
4896 $regular_price = $product->get_regular_price();
4897 $sale_price = $product->get_sale_price();
4898 $price = $product->get_price();
4899 $sku = $product->get_sku();
4900
4901 // Get currency symbol
4902 $currency_symbol = get_woocommerce_currency_symbol();
4903
4904 // Format content consistently
4905 $content = $title . "\n\n";
4906
4907 if (!empty($short_description)) {
4908 $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
4909 }
4910
4911 if (!empty($description)) {
4912 $content .= wp_strip_all_tags($description) . "\n\n";
4913 }
4914
4915 // Add pricing information
4916 if (!empty($regular_price)) {
4917 $content .= "Price: " . $currency_symbol . $regular_price . "\n";
4918 } elseif (!empty($price)) {
4919 $content .= "Price: " . $currency_symbol . $price . "\n";
4920 }
4921
4922 if (!empty($sale_price) && $sale_price !== $regular_price) {
4923 $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
4924 }
4925
4926 // Handle variable products - show price range
4927 if ($product->is_type('variable')) {
4928 $min_price = $product->get_variation_price('min');
4929 $max_price = $product->get_variation_price('max');
4930 if ($min_price !== $max_price) {
4931 $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
4932 }
4933 }
4934
4935 if (!empty($sku)) {
4936 $content .= "SKU: " . $sku . "\n";
4937 }
4938
4939 // Get product categories
4940 $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
4941 if (!empty($categories) && !is_wp_error($categories)) {
4942 $content .= "Categories: " . implode(', ', $categories) . "\n";
4943 }
4944
4945 // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
4946 $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
4947 if (!empty($custom_tabs) && is_array($custom_tabs)) {
4948 foreach ($custom_tabs as $tab) {
4949 $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
4950 $tab_content = isset($tab['content']) ? $tab['content'] : '';
4951
4952 if (!empty($tab_title) && !empty($tab_content)) {
4953 $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
4954 }
4955 }
4956 }
4957
4958 // Also check for reusable/saved tabs applied to this product
4959 $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
4960 if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
4961 // Get the saved tabs option
4962 $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
4963 if (!empty($saved_tabs) && is_array($saved_tabs)) {
4964 foreach ($applied_saved_tabs as $saved_tab_id) {
4965 if (isset($saved_tabs[$saved_tab_id])) {
4966 $tab = $saved_tabs[$saved_tab_id];
4967 $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
4968 $tab_content = isset($tab['content']) ? $tab['content'] : '';
4969
4970 if (!empty($tab_title) && !empty($tab_content)) {
4971 $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
4972 }
4973 }
4974 }
4975 }
4976 }
4977
4978 // Get API key with proper model detection
4979 $options = get_option('mxchat_options');
4980 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4981
4982 if (strpos($selected_model, 'voyage') === 0) {
4983 $api_key = $options['voyage_api_key'] ?? '';
4984 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
4985 $api_key = $options['gemini_api_key'] ?? '';
4986 } else {
4987 $api_key = $options['api_key'] ?? '';
4988 }
4989
4990 if (empty($api_key)) {
4991 //error_log('MxChat Auto-sync: No API key configured for embedding model');
4992 return;
4993 }
4994
4995 // Use the centralized utility function for storage
4996 $result = MxChat_Utils::submit_content_to_db(
4997 $content,
4998 $source_url,
4999 $api_key,
5000 md5($source_url) // Vector ID for Pinecone
5001 );
5002
5003 // After successful storage, apply role restriction based on tags
5004 if (!is_wp_error($result)) {
5005 $this->apply_role_restriction_to_post($product_id, $source_url);
5006 }
5007
5008 if (is_wp_error($result)) {
5009 //error_log('MxChat WooCommerce sync failed for product ' . $product_id . ': ' . $result->get_error_message());
5010 }
5011 }
5012
5013 public function mxchat_handle_product_delete($post_id) {
5014 if (get_post_type($post_id) !== 'product') {
5015 return;
5016 }
5017
5018 $source_url = get_permalink($post_id);
5019
5020 // Check if Pinecone is enabled
5021 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5022 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5023
5024 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
5025 // Delete from Pinecone
5026 $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
5027 } else {
5028 // Delete from WordPress DB
5029 global $wpdb;
5030 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5031
5032 $wpdb->delete(
5033 $table_name,
5034 array('source_url' => $source_url),
5035 array('%s')
5036 );
5037 }
5038 }
5039
5040 /**
5041 * Handle individual Pinecone content deletion
5042 */
5043 public function mxchat_handle_pinecone_prompt_delete() {
5044 // Check permissions
5045 if (!current_user_can('manage_options')) {
5046 wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
5047 }
5048
5049 // Verify nonce
5050 if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'mxchat_delete_pinecone_prompt_nonce')) {
5051 wp_die(esc_html__('Security check failed.', 'mxchat'));
5052 }
5053
5054 $vector_id = isset($_GET['vector_id']) ? sanitize_text_field($_GET['vector_id']) : '';
5055
5056 if (empty($vector_id)) {
5057 set_transient('mxchat_admin_notice_error',
5058 esc_html__('Invalid vector ID.', 'mxchat'),
5059 30
5060 );
5061 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5062 exit;
5063 }
5064
5065 // Get Pinecone settings
5066 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5067 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5068
5069 if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5070 set_transient('mxchat_admin_notice_error',
5071 esc_html__('Pinecone is not properly configured.', 'mxchat'),
5072 30
5073 );
5074 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5075 exit;
5076 }
5077
5078 // Delete from Pinecone
5079 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5080 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
5081 $vector_id,
5082 $pinecone_options['mxchat_pinecone_api_key'],
5083 $pinecone_options['mxchat_pinecone_host']
5084 );
5085
5086 if ($result['success']) {
5087 // No cache clearing needed since we removed caching
5088 set_transient('mxchat_admin_notice_success',
5089 esc_html__('Entry deleted successfully from Pinecone.', 'mxchat'),
5090 30
5091 );
5092 } else {
5093 set_transient('mxchat_admin_notice_error',
5094 esc_html__('Failed to delete entry: ', 'mxchat') . $result['message'],
5095 30
5096 );
5097 }
5098
5099 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5100 exit;
5101 }
5102 /**
5103 * Handle individual Pinecone content deletion via AJAX
5104 */
5105 public function ajax_mxchat_delete_pinecone_prompt() {
5106 // Verify nonce and permissions
5107 if (!check_ajax_referer('mxchat_delete_pinecone_prompt_nonce', 'nonce', false)) {
5108 wp_send_json_error('Invalid nonce');
5109 exit;
5110 }
5111
5112 if (!current_user_can('manage_options')) {
5113 wp_send_json_error('Unauthorized access');
5114 exit;
5115 }
5116
5117 $vector_id = isset($_POST['vector_id']) ? sanitize_text_field($_POST['vector_id']) : '';
5118 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
5119
5120 if (empty($vector_id)) {
5121 wp_send_json_error('Missing vector ID');
5122 exit;
5123 }
5124
5125 // Get bot-specific Pinecone settings
5126 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5127 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
5128
5129 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5130
5131 if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5132 wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
5133 exit;
5134 }
5135
5136 // Delete from the correct Pinecone index
5137 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
5138 $vector_id,
5139 $pinecone_options['mxchat_pinecone_api_key'],
5140 $pinecone_options['mxchat_pinecone_host']
5141 );
5142
5143 if ($result['success']) {
5144 // No cache clearing needed since we removed caching
5145 wp_send_json_success(array(
5146 'message' => 'Entry deleted successfully from Pinecone',
5147 'vector_id' => $vector_id,
5148 'bot_id' => $bot_id
5149 ));
5150 } else {
5151 wp_send_json_error('Failed to delete from Pinecone: ' . $result['message']);
5152 }
5153
5154 exit;
5155 }
5156
5157 /**
5158 * Handle deletion of all chunks for a given source URL via AJAX
5159 * Follows the same pattern as ajax_mxchat_delete_pinecone_prompt
5160 */
5161 public function ajax_mxchat_delete_chunks_by_url() {
5162 // Verify nonce and permissions
5163 if (!check_ajax_referer('mxchat_delete_chunks_nonce', 'nonce', false)) {
5164 wp_send_json_error('Invalid nonce');
5165 exit;
5166 }
5167
5168 if (!current_user_can('manage_options')) {
5169 wp_send_json_error('Unauthorized access');
5170 exit;
5171 }
5172
5173 $source_url = isset($_POST['source_url']) ? esc_url_raw($_POST['source_url']) : '';
5174 $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
5175 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
5176
5177 if (empty($source_url)) {
5178 wp_send_json_error('Missing source URL');
5179 exit;
5180 }
5181
5182 // Generate the base vector ID from the source URL (same as how chunks are created)
5183 $base_vector_id = md5($source_url);
5184
5185 if ($data_source === 'pinecone') {
5186 // Get bot-specific Pinecone settings (same as working delete function)
5187 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5188 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
5189
5190 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5191
5192 if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5193 wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
5194 exit;
5195 }
5196
5197 $api_key = $pinecone_options['mxchat_pinecone_api_key'];
5198 $host = $pinecone_options['mxchat_pinecone_host'];
5199 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
5200
5201 // Collect all vector IDs to delete
5202 $vectors_to_delete = array();
5203
5204 // Add the original single-vector ID (for non-chunked content)
5205 $vectors_to_delete[] = $base_vector_id;
5206
5207 // Use Pinecone list API to find all chunk vectors with this prefix
5208 // NOTE: Pinecone List API is a GET request with query parameters, not POST
5209 $prefix = $base_vector_id . '_chunk_';
5210
5211 $query_params = array(
5212 'prefix' => $prefix,
5213 'limit' => 100
5214 );
5215
5216 if (!empty($namespace)) {
5217 $query_params['namespace'] = $namespace;
5218 }
5219
5220 $list_url = "https://{$host}/vectors/list?" . http_build_query($query_params);
5221
5222 $list_response = wp_remote_get($list_url, array(
5223 'headers' => array(
5224 'Api-Key' => $api_key,
5225 'accept' => 'application/json'
5226 ),
5227 'timeout' => 30
5228 ));
5229
5230 if (!is_wp_error($list_response)) {
5231 $list_body_response = wp_remote_retrieve_body($list_response);
5232 $list_data = json_decode($list_body_response, true);
5233 if (!empty($list_data['vectors'])) {
5234 foreach ($list_data['vectors'] as $vector) {
5235 if (isset($vector['id'])) {
5236 $vectors_to_delete[] = $vector['id'];
5237 }
5238 }
5239 }
5240 }
5241
5242 if (empty($vectors_to_delete)) {
5243 wp_send_json_success(array(
5244 'message' => 'No vectors found to delete',
5245 'source_url' => $source_url
5246 ));
5247 exit;
5248 }
5249
5250 // Delete all vectors using the same endpoint as the working function
5251 $delete_url = "https://{$host}/vectors/delete";
5252
5253 $delete_body = array(
5254 'ids' => $vectors_to_delete
5255 );
5256
5257 if (!empty($namespace)) {
5258 $delete_body['namespace'] = $namespace;
5259 }
5260
5261 $delete_response = wp_remote_post($delete_url, array(
5262 'headers' => array(
5263 'Api-Key' => $api_key,
5264 'accept' => 'application/json',
5265 'content-type' => 'application/json'
5266 ),
5267 'body' => wp_json_encode($delete_body),
5268 'timeout' => 30
5269 ));
5270
5271 if (is_wp_error($delete_response)) {
5272 wp_send_json_error('Failed to delete from Pinecone: ' . $delete_response->get_error_message());
5273 exit;
5274 }
5275
5276 $response_code = wp_remote_retrieve_response_code($delete_response);
5277
5278 if ($response_code !== 200) {
5279 wp_send_json_error('Pinecone API error (HTTP ' . $response_code . ')');
5280 exit;
5281 }
5282
5283 wp_send_json_success(array(
5284 'message' => 'All chunks deleted successfully from Pinecone',
5285 'source_url' => $source_url,
5286 'deleted_count' => count($vectors_to_delete)
5287 ));
5288
5289 } else {
5290 // WordPress database deletion
5291 global $wpdb;
5292 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5293
5294 $result = $wpdb->delete(
5295 $table_name,
5296 array('source_url' => $source_url),
5297 array('%s')
5298 );
5299
5300 if ($result === false) {
5301 wp_send_json_error('Failed to delete from database: ' . $wpdb->last_error);
5302 exit;
5303 }
5304
5305 wp_send_json_success(array(
5306 'message' => 'All chunks deleted successfully from database',
5307 'source_url' => $source_url,
5308 'deleted_count' => $result
5309 ));
5310 }
5311
5312 exit;
5313 }
5314
5315 /**
5316 * Handle individual WordPress database content deletion via AJAX
5317 * Mirrors the Pinecone delete handler but for WordPress database entries
5318 */
5319 public function ajax_mxchat_delete_wordpress_prompt() {
5320 // Verify nonce and permissions
5321 if (!check_ajax_referer('mxchat_delete_wordpress_prompt_nonce', 'nonce', false)) {
5322 wp_send_json_error('Invalid nonce');
5323 exit;
5324 }
5325
5326 if (!current_user_can('manage_options')) {
5327 wp_send_json_error('Unauthorized access');
5328 exit;
5329 }
5330
5331 $entry_id = isset($_POST['entry_id']) ? intval($_POST['entry_id']) : 0;
5332
5333 if (empty($entry_id)) {
5334 wp_send_json_error('Missing entry ID');
5335 exit;
5336 }
5337
5338 global $wpdb;
5339 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5340
5341 // Clear cache for this entry
5342 wp_cache_delete('prompt_' . $entry_id, 'mxchat_prompts');
5343
5344 // Delete from database
5345 $result = $wpdb->delete(
5346 $table_name,
5347 array('id' => $entry_id),
5348 array('%d')
5349 );
5350
5351 if ($result !== false) {
5352 wp_send_json_success(array(
5353 'message' => 'Entry deleted successfully',
5354 'entry_id' => $entry_id
5355 ));
5356 } else {
5357 wp_send_json_error('Failed to delete entry from database');
5358 }
5359
5360 exit;
5361 }
5362
5363 /**
5364 * Handle bulk deletion of knowledge entries via AJAX
5365 * Supports both Pinecone and WordPress database entries
5366 */
5367 public function ajax_mxchat_bulk_delete_knowledge() {
5368 // Verify nonce and permissions
5369 if (!check_ajax_referer('mxchat_bulk_delete_knowledge_nonce', 'nonce', false)) {
5370 wp_send_json_error('Invalid nonce');
5371 exit;
5372 }
5373
5374 if (!current_user_can('manage_options')) {
5375 wp_send_json_error('Unauthorized access');
5376 exit;
5377 }
5378
5379 $entries = isset($_POST['entries']) ? $_POST['entries'] : array();
5380 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
5381
5382 if (empty($entries) || !is_array($entries)) {
5383 wp_send_json_error('No entries provided');
5384 exit;
5385 }
5386
5387 $success_ids = array();
5388 $failed_ids = array();
5389 $errors = array();
5390
5391 global $wpdb;
5392 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5393
5394 // Get Pinecone manager for Pinecone deletions
5395 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5396 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
5397 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5398
5399 foreach ($entries as $entry) {
5400 $entry_id = sanitize_text_field($entry['id'] ?? '');
5401 $source = sanitize_text_field($entry['source'] ?? 'wordpress');
5402 $source_url = isset($entry['sourceUrl']) ? esc_url_raw($entry['sourceUrl']) : '';
5403 $is_group = isset($entry['isGroup']) && ($entry['isGroup'] === true || $entry['isGroup'] === 'true');
5404
5405 if (empty($entry_id)) {
5406 continue;
5407 }
5408
5409 try {
5410 if ($source === 'pinecone') {
5411 // Handle Pinecone deletion
5412 if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5413 $failed_ids[] = $entry_id;
5414 $errors[] = "Pinecone not configured for entry: $entry_id";
5415 continue;
5416 }
5417
5418 if ($is_group && !empty($source_url)) {
5419 // Delete all chunks for this URL
5420 $base_vector_id = md5($source_url);
5421 $api_key = $pinecone_options['mxchat_pinecone_api_key'];
5422 $host = $pinecone_options['mxchat_pinecone_host'];
5423
5424 // List all vectors with this prefix
5425 $list_url = 'https://' . $host . '/vectors/list?prefix=' . urlencode($base_vector_id . '_chunk_') . '&limit=100';
5426 $list_response = wp_remote_get($list_url, array(
5427 'headers' => array(
5428 'Api-Key' => $api_key,
5429 'Content-Type' => 'application/json'
5430 ),
5431 'timeout' => 30
5432 ));
5433
5434 $vector_ids = array($base_vector_id); // Include base ID
5435
5436 if (!is_wp_error($list_response)) {
5437 $list_body = json_decode(wp_remote_retrieve_body($list_response), true);
5438 if (isset($list_body['vectors']) && is_array($list_body['vectors'])) {
5439 foreach ($list_body['vectors'] as $vector) {
5440 if (isset($vector['id'])) {
5441 $vector_ids[] = $vector['id'];
5442 }
5443 }
5444 }
5445 }
5446
5447 // Delete all vectors
5448 $delete_result = $pinecone_manager->mxchat_delete_pinecone_batch(
5449 $vector_ids,
5450 $api_key,
5451 $host
5452 );
5453
5454 if ($delete_result['success']) {
5455 $success_ids[] = $entry_id;
5456 } else {
5457 $failed_ids[] = $entry_id;
5458 $errors[] = $delete_result['message'] ?? "Failed to delete group: $entry_id";
5459 }
5460 } else {
5461 // Delete single vector
5462 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
5463 $entry_id,
5464 $pinecone_options['mxchat_pinecone_api_key'],
5465 $pinecone_options['mxchat_pinecone_host']
5466 );
5467
5468 if ($result['success']) {
5469 $success_ids[] = $entry_id;
5470 } else {
5471 $failed_ids[] = $entry_id;
5472 $errors[] = $result['message'] ?? "Failed to delete: $entry_id";
5473 }
5474 }
5475 } else {
5476 // Handle WordPress database deletion
5477 if ($is_group && !empty($source_url)) {
5478 // Delete all entries with this source URL
5479 $result = $wpdb->delete(
5480 $table_name,
5481 array('source_url' => $source_url),
5482 array('%s')
5483 );
5484 } else {
5485 // Delete single entry
5486 wp_cache_delete('prompt_' . $entry_id, 'mxchat_prompts');
5487 $result = $wpdb->delete(
5488 $table_name,
5489 array('id' => intval($entry_id)),
5490 array('%d')
5491 );
5492 }
5493
5494 if ($result !== false) {
5495 $success_ids[] = $entry_id;
5496 } else {
5497 $failed_ids[] = $entry_id;
5498 $errors[] = "Database error for entry: $entry_id";
5499 }
5500 }
5501 } catch (Exception $e) {
5502 $failed_ids[] = $entry_id;
5503 $errors[] = $e->getMessage();
5504 }
5505 }
5506
5507 wp_send_json_success(array(
5508 'success_ids' => $success_ids,
5509 'failed_ids' => $failed_ids,
5510 'errors' => $errors,
5511 'total_processed' => count($success_ids) + count($failed_ids)
5512 ));
5513
5514 exit;
5515 }
5516
5517 /**
5518 * Get hierarchical roles for dropdown
5519 */
5520 public function mxchat_get_role_options() {
5521 return array(
5522 'public' => __('Public (Everyone)', 'mxchat'),
5523 'logged_in' => __('Logged In Users', 'mxchat'),
5524 'subscriber' => __('Subscribers & Above', 'mxchat'),
5525 'contributor' => __('Contributors & Above', 'mxchat'),
5526 'author' => __('Authors & Above', 'mxchat'),
5527 'editor' => __('Editors & Above', 'mxchat'),
5528 'administrator' => __('Administrators Only', 'mxchat')
5529 );
5530 }
5531
5532 /**
5533 * Check if user has access to content based on role restriction
5534 */
5535 public function mxchat_user_has_content_access($role_restriction) {
5536 // Public content is always accessible
5537 if ($role_restriction === 'public' || empty($role_restriction)) {
5538 return true;
5539 }
5540
5541 // Check if user is logged in for logged_in restriction
5542 if ($role_restriction === 'logged_in') {
5543 return is_user_logged_in();
5544 }
5545
5546 // If not logged in, no access to role-restricted content
5547 if (!is_user_logged_in()) {
5548 return false;
5549 }
5550
5551 $user = wp_get_current_user();
5552 $user_roles = $user->roles;
5553
5554 if (empty($user_roles)) {
5555 return false;
5556 }
5557
5558 // Define role hierarchy (higher number = higher access)
5559 $hierarchy = array(
5560 'subscriber' => 1,
5561 'contributor' => 2,
5562 'author' => 3,
5563 'editor' => 4,
5564 'administrator' => 5
5565 );
5566
5567 // Get required level
5568 $required_level = isset($hierarchy[$role_restriction]) ? $hierarchy[$role_restriction] : 0;
5569
5570 // Check if user has required level or higher
5571 foreach ($user_roles as $user_role) {
5572 $user_level = isset($hierarchy[$user_role]) ? $hierarchy[$user_role] : 0;
5573 if ($user_level >= $required_level) {
5574 return true;
5575 }
5576 }
5577
5578 return false;
5579 }
5580
5581 /**
5582 * Handle role restriction updates via AJAX
5583 * Removed cache clearing call since we removed caching
5584 */
5585 public function ajax_mxchat_update_role_restriction() {
5586 // Verify nonce and permissions
5587 if (!check_ajax_referer('mxchat_update_role_nonce', 'nonce', false)) {
5588 wp_send_json_error('Invalid nonce');
5589 exit;
5590 }
5591
5592 if (!current_user_can('manage_options')) {
5593 wp_send_json_error('Unauthorized access');
5594 exit;
5595 }
5596
5597 $entry_id = isset($_POST['entry_id']) ? sanitize_text_field($_POST['entry_id']) : '';
5598 $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
5599 $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
5600
5601 if (empty($entry_id)) {
5602 wp_send_json_error('Invalid entry ID');
5603 exit;
5604 }
5605
5606 // Get knowledge manager instance to validate role restriction
5607 $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
5608 $valid_roles = array_keys($knowledge_manager->mxchat_get_role_options());
5609 if (!in_array($role_restriction, $valid_roles)) {
5610 wp_send_json_error('Invalid role restriction');
5611 exit;
5612 }
5613
5614 global $wpdb;
5615
5616 if ($data_source === 'pinecone') {
5617 // Handle Pinecone role restriction (stored separately in WordPress table)
5618 $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
5619
5620 // Use REPLACE to insert or update the role restriction
5621 $result = $wpdb->replace(
5622 $roles_table,
5623 array(
5624 'vector_id' => $entry_id,
5625 'role_restriction' => $role_restriction,
5626 'updated_at' => current_time('mysql')
5627 ),
5628 array('%s', '%s', '%s')
5629 );
5630
5631 // No cache clearing needed since we removed caching
5632
5633 } else {
5634 // Handle WordPress database role restriction (existing functionality)
5635 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5636
5637 $result = $wpdb->update(
5638 $table_name,
5639 array('role_restriction' => $role_restriction),
5640 array('id' => absint($entry_id)),
5641 array('%s'),
5642 array('%d')
5643 );
5644 }
5645
5646 if ($result === false) {
5647 wp_send_json_error('Database update failed: ' . $wpdb->last_error);
5648 exit;
5649 }
5650
5651 wp_send_json_success(array(
5652 'message' => 'Role restriction updated successfully',
5653 'role_restriction' => $role_restriction,
5654 'data_source' => $data_source,
5655 'entry_id' => $entry_id
5656 ));
5657 exit;
5658 }
5659
5660 // ========================================
5661 // ROLE-BASED CONTENT RESTRICTIONS - NEW FUNCTIONS
5662 // Add these to your MxChat_Knowledge_Manager class
5663 // ========================================
5664
5665 /**
5666 * Initialize role-based content hooks
5667 * Add this call to your __construct() or mxchat_init_hooks() method
5668 */
5669 private function mxchat_init_role_hooks() {
5670 // AJAX handlers for tag-role mappings
5671 add_action('wp_ajax_mxchat_add_tag_role_mapping', array($this, 'ajax_add_tag_role_mapping'));
5672 add_action('wp_ajax_mxchat_delete_tag_role_mapping', array($this, 'ajax_delete_tag_role_mapping'));
5673 add_action('wp_ajax_mxchat_get_tag_role_mappings', array($this, 'ajax_get_tag_role_mappings'));
5674 add_action('wp_ajax_mxchat_bulk_update_tag_roles', array($this, 'ajax_bulk_update_tag_roles'));
5675
5676 // Hook to automatically update role restrictions when tags are added/removed
5677 add_action('set_object_terms', array($this, 'handle_tag_change'), 10, 6);
5678
5679 // Hook to apply role restrictions on auto-sync
5680 add_action('mxchat_content_stored', array($this, 'apply_role_restriction_after_storage'), 10, 2);
5681 }
5682
5683 /**
5684 * Add tag-role mapping via AJAX
5685 */
5686 public function ajax_add_tag_role_mapping() {
5687 // Verify nonce and permissions
5688 check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
5689
5690 if (!current_user_can('manage_options')) {
5691 wp_send_json_error('Unauthorized access');
5692 exit;
5693 }
5694
5695 $tag_slug = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
5696 $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
5697
5698 if (empty($tag_slug)) {
5699 wp_send_json_error('Tag slug is required');
5700 exit;
5701 }
5702
5703 // Validate role restriction
5704 $valid_roles = array_keys($this->mxchat_get_role_options());
5705 if (!in_array($role_restriction, $valid_roles)) {
5706 wp_send_json_error('Invalid role restriction');
5707 exit;
5708 }
5709
5710 // Check if tag exists in WordPress
5711 $term = get_term_by('slug', $tag_slug, 'post_tag');
5712 if (!$term) {
5713 wp_send_json_error('Tag does not exist in WordPress');
5714 exit;
5715 }
5716
5717 // Get existing mappings
5718 $mappings = get_option('mxchat_tag_role_mappings', array());
5719
5720 // Check if mapping already exists
5721 if (isset($mappings[$tag_slug])) {
5722 wp_send_json_error('Mapping for this tag already exists');
5723 exit;
5724 }
5725
5726 // Add new mapping
5727 $mappings[$tag_slug] = $role_restriction;
5728 update_option('mxchat_tag_role_mappings', $mappings);
5729
5730 wp_send_json_success(array(
5731 'message' => 'Tag-role mapping added successfully',
5732 'tag_slug' => $tag_slug,
5733 'role_restriction' => $role_restriction
5734 ));
5735 exit;
5736 }
5737
5738 /**
5739 * Delete tag-role mapping via AJAX
5740 */
5741 public function ajax_delete_tag_role_mapping() {
5742 // Verify nonce and permissions
5743 check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
5744
5745 if (!current_user_can('manage_options')) {
5746 wp_send_json_error('Unauthorized access');
5747 exit;
5748 }
5749
5750 $tag_slug = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
5751
5752 if (empty($tag_slug)) {
5753 wp_send_json_error('Tag slug is required');
5754 exit;
5755 }
5756
5757 // Get existing mappings
5758 $mappings = get_option('mxchat_tag_role_mappings', array());
5759
5760 // Check if mapping exists
5761 if (!isset($mappings[$tag_slug])) {
5762 wp_send_json_error('Mapping does not exist');
5763 exit;
5764 }
5765
5766 // Remove mapping
5767 unset($mappings[$tag_slug]);
5768 update_option('mxchat_tag_role_mappings', $mappings);
5769
5770 wp_send_json_success(array(
5771 'message' => 'Tag-role mapping deleted successfully',
5772 'tag_slug' => $tag_slug
5773 ));
5774 exit;
5775 }
5776
5777 /**
5778 * Get all tag-role mappings via AJAX
5779 */
5780 public function ajax_get_tag_role_mappings() {
5781 // Verify nonce and permissions
5782 check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
5783
5784 if (!current_user_can('manage_options')) {
5785 wp_send_json_error('Unauthorized access');
5786 exit;
5787 }
5788
5789 // Get mappings
5790 $mappings = get_option('mxchat_tag_role_mappings', array());
5791 $role_options = $this->mxchat_get_role_options();
5792
5793 $formatted_mappings = array();
5794
5795 foreach ($mappings as $tag_slug => $role_restriction) {
5796 // Get tag object
5797 $term = get_term_by('slug', $tag_slug, 'post_tag');
5798
5799 // Count posts with this tag
5800 $post_count = 0;
5801 if ($term) {
5802 $post_count = $term->count;
5803 }
5804
5805 $formatted_mappings[] = array(
5806 'tag_slug' => $tag_slug,
5807 'role_restriction' => $role_restriction,
5808 'role_label' => $role_options[$role_restriction] ?? $role_restriction,
5809 'post_count' => $post_count
5810 );
5811 }
5812
5813 wp_send_json_success(array(
5814 'mappings' => $formatted_mappings
5815 ));
5816 exit;
5817 }
5818
5819 /**
5820 * Bulk update role restrictions for all existing content with mapped tags
5821 */
5822 public function ajax_bulk_update_tag_roles() {
5823 // Verify nonce and permissions
5824 check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
5825
5826 if (!current_user_can('manage_options')) {
5827 wp_send_json_error('Unauthorized access');
5828 exit;
5829 }
5830
5831 // Get mappings
5832 $mappings = get_option('mxchat_tag_role_mappings', array());
5833
5834 if (empty($mappings)) {
5835 wp_send_json_error('No tag-role mappings found');
5836 exit;
5837 }
5838
5839 global $wpdb;
5840
5841 // Check if using Pinecone
5842 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5843 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5844
5845 $updated_count = 0;
5846 $details = array();
5847
5848 foreach ($mappings as $tag_slug => $role_restriction) {
5849 // Get all posts with this tag
5850 $posts = get_posts(array(
5851 'tag' => $tag_slug,
5852 'post_type' => 'any',
5853 'posts_per_page' => -1,
5854 'fields' => 'ids',
5855 'post_status' => 'publish'
5856 ));
5857
5858 if (empty($posts)) {
5859 continue;
5860 }
5861
5862 $tag_updated = 0;
5863
5864 foreach ($posts as $post_id) {
5865 $source_url = get_permalink($post_id);
5866 if (!$source_url) {
5867 continue;
5868 }
5869
5870 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
5871 // Update Pinecone role restriction
5872 $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
5873 $vector_id = md5($source_url);
5874
5875 $result = $wpdb->replace(
5876 $roles_table,
5877 array(
5878 'vector_id' => $vector_id,
5879 'role_restriction' => $role_restriction,
5880 'updated_at' => current_time('mysql')
5881 ),
5882 array('%s', '%s', '%s')
5883 );
5884 } else {
5885 // Update WordPress DB
5886 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5887
5888 $result = $wpdb->update(
5889 $table_name,
5890 array('role_restriction' => $role_restriction),
5891 array('source_url' => $source_url),
5892 array('%s'),
5893 array('%s')
5894 );
5895 }
5896
5897 if ($result !== false) {
5898 $tag_updated++;
5899 $updated_count++;
5900 }
5901 }
5902
5903 if ($tag_updated > 0) {
5904 $details[] = sprintf(
5905 'Tag "%s" (%s): %d posts updated',
5906 $tag_slug,
5907 $role_restriction,
5908 $tag_updated
5909 );
5910 }
5911 }
5912
5913 wp_send_json_success(array(
5914 'message' => 'Bulk update completed',
5915 'updated_count' => $updated_count,
5916 'tags_processed' => count($mappings),
5917 'details' => $details
5918 ));
5919 exit;
5920 }
5921
5922 /**
5923 * Handle tag changes on posts (when tags are added or removed)
5924 */
5925 public function handle_tag_change($object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids) {
5926 // Only process post tags
5927 if ($taxonomy !== 'post_tag') {
5928 return;
5929 }
5930
5931 // Get tag-role mappings
5932 $mappings = get_option('mxchat_tag_role_mappings', array());
5933
5934 if (empty($mappings)) {
5935 return;
5936 }
5937
5938 // Get the post's URL
5939 $source_url = get_permalink($object_id);
5940 if (!$source_url) {
5941 return;
5942 }
5943
5944 // Determine the highest role restriction based on tags
5945 $highest_role = 'public';
5946 $role_hierarchy = array(
5947 'public' => 0,
5948 'logged_in' => 1,
5949 'subscriber' => 2,
5950 'contributor' => 3,
5951 'author' => 4,
5952 'editor' => 5,
5953 'administrator' => 6
5954 );
5955
5956 // Get all current tags for the post
5957 $current_tags = wp_get_post_tags($object_id, array('fields' => 'slugs'));
5958
5959 // Find the highest role restriction among the tags
5960 foreach ($current_tags as $tag_slug) {
5961 if (isset($mappings[$tag_slug])) {
5962 $role = $mappings[$tag_slug];
5963 if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
5964 $highest_role = $role;
5965 }
5966 }
5967 }
5968
5969 // Update the role restriction in the database
5970 global $wpdb;
5971
5972 // Check if using Pinecone
5973 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5974 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5975
5976 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
5977 // Update Pinecone role restriction
5978 $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
5979 $vector_id = md5($source_url);
5980
5981 $wpdb->replace(
5982 $roles_table,
5983 array(
5984 'vector_id' => $vector_id,
5985 'role_restriction' => $highest_role,
5986 'updated_at' => current_time('mysql')
5987 ),
5988 array('%s', '%s', '%s')
5989 );
5990 } else {
5991 // Update WordPress DB
5992 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5993
5994 $wpdb->update(
5995 $table_name,
5996 array('role_restriction' => $highest_role),
5997 array('source_url' => $source_url),
5998 array('%s'),
5999 array('%s')
6000 );
6001 }
6002 }
6003
6004 /**
6005 * Apply role restriction after content is stored (for auto-sync)
6006 */
6007 public function apply_role_restriction_after_storage($post_id, $source_url) {
6008 // Get tag-role mappings
6009 $mappings = get_option('mxchat_tag_role_mappings', array());
6010
6011 if (empty($mappings)) {
6012 return;
6013 }
6014
6015 // Get all tags for the post
6016 $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
6017
6018 if (empty($post_tags)) {
6019 return;
6020 }
6021
6022 // Determine the highest role restriction based on tags
6023 $highest_role = 'public';
6024 $role_hierarchy = array(
6025 'public' => 0,
6026 'logged_in' => 1,
6027 'subscriber' => 2,
6028 'contributor' => 3,
6029 'author' => 4,
6030 'editor' => 5,
6031 'administrator' => 6
6032 );
6033
6034 foreach ($post_tags as $tag_slug) {
6035 if (isset($mappings[$tag_slug])) {
6036 $role = $mappings[$tag_slug];
6037 if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
6038 $highest_role = $role;
6039 }
6040 }
6041 }
6042
6043 // If no restricted tags found, return (leave as public)
6044 if ($highest_role === 'public') {
6045 return;
6046 }
6047
6048 // Update the role restriction
6049 global $wpdb;
6050
6051 // Check if using Pinecone
6052 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
6053 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6054
6055 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
6056 // Update Pinecone role restriction
6057 $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6058 $vector_id = md5($source_url);
6059
6060 $wpdb->replace(
6061 $roles_table,
6062 array(
6063 'vector_id' => $vector_id,
6064 'role_restriction' => $highest_role,
6065 'updated_at' => current_time('mysql')
6066 ),
6067 array('%s', '%s', '%s')
6068 );
6069 } else {
6070 // Update WordPress DB
6071 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6072
6073 $wpdb->update(
6074 $table_name,
6075 array('role_restriction' => $highest_role),
6076 array('source_url' => $source_url),
6077 array('%s'),
6078 array('%s')
6079 );
6080 }
6081 }
6082
6083
6084 // ========================================
6085 // HELPER METHODS
6086 // ========================================
6087
6088 /**
6089 * Check if user has required permissions for content processing
6090 */
6091 private function mxchat_check_user_permissions() {
6092 if (!current_user_can('manage_options')) {
6093 wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
6094 }
6095 }
6096
6097 /**
6098 * Validate nonce for security
6099 */
6100 private function mxchat_validate_nonce($nonce_name, $nonce_action) {
6101 if (!isset($_POST[$nonce_name]) || !wp_verify_nonce($_POST[$nonce_name], $nonce_action)) {
6102 wp_die(esc_html__('Security check failed.', 'mxchat'));
6103 }
6104 }
6105
6106 /**
6107 * Get embedding API credentials
6108 */
6109 private function mxchat_get_embedding_credentials() {
6110 $embedding_model = $this->options['embedding_model'] ?? 'text-embedding-ada-002';
6111
6112 if (strpos($embedding_model, 'text-embedding-') !== false) {
6113 return array(
6114 'type' => 'openai',
6115 'api_key' => $this->options['api_key'] ?? ''
6116 );
6117 } elseif (strpos($embedding_model, 'voyage-') !== false) {
6118 return array(
6119 'type' => 'voyage',
6120 'api_key' => $this->options['voyage_api_key'] ?? ''
6121 );
6122 } elseif (strpos($embedding_model, 'gemini-embedding-') !== false) {
6123 return array(
6124 'type' => 'gemini',
6125 'api_key' => $this->options['gemini_api_key'] ?? ''
6126 );
6127 }
6128
6129 return array('type' => 'unknown', 'api_key' => '');
6130 }
6131
6132 /**
6133 * Log processing errors
6134 */
6135 private function mxchat_log_processing_error($operation, $error_message) {
6136 //error_log("MxChat Knowledge Processing {$operation} Error: " . $error_message);
6137 }
6138
6139 /**
6140 * Set admin notice transient
6141 */
6142 private function mxchat_set_admin_notice($type, $message) {
6143 set_transient("mxchat_admin_notice_{$type}", $message, 30);
6144 }
6145
6146 /**
6147 * Get Pinecone manager instance for vector operations
6148 */
6149 private function mxchat_get_pinecone_manager() {
6150 return MxChat_Pinecone_Manager::get_instance();
6151 }
6152
6153
6154 // ========================================
6155 // DATABASE QUEUE TABLE MANAGEMENT
6156 // ========================================
6157
6158 /**
6159 * Create queue table on plugin activation
6160 * Call this from your plugin activation hook
6161 */
6162 public function mxchat_create_queue_table() {
6163 global $wpdb;
6164
6165 $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6166 $charset_collate = $wpdb->get_charset_collate();
6167
6168 $sql = "CREATE TABLE IF NOT EXISTS $table_name (
6169 id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
6170 queue_id varchar(64) NOT NULL,
6171 item_type varchar(20) NOT NULL,
6172 item_data longtext NOT NULL,
6173 status varchar(20) NOT NULL DEFAULT 'pending',
6174 bot_id varchar(50) NOT NULL DEFAULT 'default',
6175 priority int(11) NOT NULL DEFAULT 0,
6176 attempts int(11) NOT NULL DEFAULT 0,
6177 max_attempts int(11) NOT NULL DEFAULT 3,
6178 error_message text DEFAULT NULL,
6179 created_at datetime NOT NULL,
6180 started_at datetime DEFAULT NULL,
6181 completed_at datetime DEFAULT NULL,
6182 PRIMARY KEY (id),
6183 KEY queue_id (queue_id),
6184 KEY status (status),
6185 KEY item_type (item_type),
6186 KEY priority (priority)
6187 ) $charset_collate;";
6188
6189 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
6190 dbDelta($sql);
6191
6192 // Also create a meta table for queue metadata
6193 $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
6194
6195 $meta_sql = "CREATE TABLE IF NOT EXISTS $meta_table (
6196 id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
6197 queue_id varchar(64) NOT NULL,
6198 meta_key varchar(255) NOT NULL,
6199 meta_value longtext,
6200 PRIMARY KEY (id),
6201 KEY queue_id (queue_id),
6202 KEY meta_key (meta_key)
6203 ) $charset_collate;";
6204
6205 dbDelta($meta_sql);
6206 }
6207
6208 /**
6209 * Add items to the processing queue
6210 *
6211 * @param string $queue_id Unique identifier for this queue batch
6212 * @param string $item_type Type of item (url, pdf_page)
6213 * @param array $items Array of items to queue
6214 * @param string $bot_id Bot ID for processing
6215 * @return int Number of items queued
6216 */
6217 private function mxchat_add_to_queue($queue_id, $item_type, $items, $bot_id = 'default') {
6218 global $wpdb;
6219 $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6220
6221 $queued_count = 0;
6222 $priority = 0;
6223
6224 foreach ($items as $item) {
6225 $result = $wpdb->insert(
6226 $table_name,
6227 array(
6228 'queue_id' => $queue_id,
6229 'item_type' => $item_type,
6230 'item_data' => wp_json_encode($item),
6231 'status' => 'pending',
6232 'bot_id' => $bot_id,
6233 'priority' => $priority,
6234 'attempts' => 0,
6235 'max_attempts' => 3,
6236 'created_at' => current_time('mysql')
6237 ),
6238 array('%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%s')
6239 );
6240
6241 if ($result) {
6242 $queued_count++;
6243 }
6244
6245 $priority++; // Process in order
6246 }
6247
6248 return $queued_count;
6249 }
6250
6251 /**
6252 * Store queue metadata (total counts, source URL, etc.)
6253 */
6254 private function mxchat_set_queue_meta($queue_id, $meta_key, $meta_value) {
6255 global $wpdb;
6256 $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
6257
6258 // Check if meta exists
6259 $existing = $wpdb->get_var($wpdb->prepare(
6260 "SELECT id FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
6261 $queue_id,
6262 $meta_key
6263 ));
6264
6265 if ($existing) {
6266 // Update
6267 $wpdb->update(
6268 $meta_table,
6269 array('meta_value' => maybe_serialize($meta_value)),
6270 array('queue_id' => $queue_id, 'meta_key' => $meta_key),
6271 array('%s'),
6272 array('%s', '%s')
6273 );
6274 } else {
6275 // Insert
6276 $wpdb->insert(
6277 $meta_table,
6278 array(
6279 'queue_id' => $queue_id,
6280 'meta_key' => $meta_key,
6281 'meta_value' => maybe_serialize($meta_value)
6282 ),
6283 array('%s', '%s', '%s')
6284 );
6285 }
6286 }
6287
6288 /**
6289 * Get queue metadata
6290 */
6291 private function mxchat_get_queue_meta($queue_id, $meta_key) {
6292 global $wpdb;
6293 $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
6294
6295 $value = $wpdb->get_var($wpdb->prepare(
6296 "SELECT meta_value FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
6297 $queue_id,
6298 $meta_key
6299 ));
6300
6301 return maybe_unserialize($value);
6302 }
6303
6304 // ========================================
6305 // AJAX QUEUE PROCESSING HANDLERS
6306 // ========================================
6307
6308 /**
6309 * AJAX: Get next item from queue to process
6310 */
6311 public function ajax_mxchat_get_next_queue_item() {
6312 // Verify nonce and permissions
6313 check_ajax_referer('mxchat_queue_nonce', 'nonce');
6314
6315 if (!current_user_can('manage_options')) {
6316 wp_send_json_error('Unauthorized access');
6317 }
6318
6319 $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
6320
6321 if (empty($queue_id)) {
6322 wp_send_json_error('Missing queue ID');
6323 }
6324
6325 global $wpdb;
6326 $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6327
6328 // Get next pending item with retry logic for failed items
6329 $next_item = $wpdb->get_row($wpdb->prepare(
6330 "SELECT * FROM $table_name
6331 WHERE queue_id = %s
6332 AND status IN ('pending', 'failed')
6333 AND attempts < max_attempts
6334 ORDER BY priority ASC, id ASC
6335 LIMIT 1",
6336 $queue_id
6337 ));
6338
6339 if (!$next_item) {
6340 // No more items - queue complete
6341 wp_send_json_success(array(
6342 'complete' => true,
6343 'message' => 'Queue processing complete'
6344 ));
6345 }
6346
6347 // Mark item as processing
6348 $wpdb->update(
6349 $table_name,
6350 array(
6351 'status' => 'processing',
6352 'started_at' => current_time('mysql'),
6353 'attempts' => $next_item->attempts + 1
6354 ),
6355 array('id' => $next_item->id),
6356 array('%s', '%s', '%d'),
6357 array('%d')
6358 );
6359
6360 wp_send_json_success(array(
6361 'complete' => false,
6362 'item' => array(
6363 'id' => $next_item->id,
6364 'type' => $next_item->item_type,
6365 'data' => json_decode($next_item->item_data, true),
6366 'bot_id' => $next_item->bot_id,
6367 'attempt' => $next_item->attempts + 1
6368 )
6369 ));
6370 }
6371
6372 /**
6373 * AJAX: Process a single queue item
6374 */
6375 public function ajax_mxchat_process_queue_item() {
6376 // Verify nonce and permissions
6377 check_ajax_referer('mxchat_queue_nonce', 'nonce');
6378
6379 if (!current_user_can('manage_options')) {
6380 wp_send_json_error('Unauthorized access');
6381 }
6382
6383 $item_id = isset($_POST['item_id']) ? absint($_POST['item_id']) : 0;
6384 $item_type = isset($_POST['item_type']) ? sanitize_text_field($_POST['item_type']) : '';
6385 $item_data = isset($_POST['item_data']) ? $_POST['item_data'] : array();
6386 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
6387
6388 if (empty($item_id) || empty($item_type)) {
6389 wp_send_json_error('Missing item data');
6390 }
6391
6392 global $wpdb;
6393 $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6394
6395 // Process based on item type
6396 try {
6397 set_time_limit(60); // Give processing 60 seconds
6398
6399 $result = false;
6400 $error_message = '';
6401
6402 switch ($item_type) {
6403 case 'url':
6404 $result = $this->mxchat_process_queue_url($item_data, $bot_id);
6405 break;
6406
6407 case 'pdf_page':
6408 $result = $this->mxchat_process_queue_pdf_page($item_data, $bot_id);
6409 break;
6410
6411 default:
6412 throw new Exception('Unknown item type: ' . $item_type);
6413 }
6414
6415 if (is_wp_error($result)) {
6416 throw new Exception($result->get_error_message());
6417 }
6418
6419 if ($result === false) {
6420 throw new Exception('Processing returned false - item may be empty or invalid');
6421 }
6422
6423 // Mark as completed
6424 $wpdb->update(
6425 $table_name,
6426 array(
6427 'status' => 'completed',
6428 'completed_at' => current_time('mysql'),
6429 'error_message' => null
6430 ),
6431 array('id' => $item_id),
6432 array('%s', '%s', '%s'),
6433 array('%d')
6434 );
6435
6436 wp_send_json_success(array(
6437 'processed' => true,
6438 'item_id' => $item_id,
6439 'message' => 'Item processed successfully'
6440 ));
6441
6442 } catch (Exception $e) {
6443 $error_message = $e->getMessage();
6444
6445 // Get current attempt count
6446 $item = $wpdb->get_row($wpdb->prepare(
6447 "SELECT attempts, max_attempts FROM $table_name WHERE id = %d",
6448 $item_id
6449 ));
6450
6451 // Check if we've exhausted retries
6452 if ($item && $item->attempts >= $item->max_attempts) {
6453 // Permanently failed
6454 $wpdb->update(
6455 $table_name,
6456 array(
6457 'status' => 'failed',
6458 'error_message' => $error_message
6459 ),
6460 array('id' => $item_id),
6461 array('%s', '%s'),
6462 array('%d')
6463 );
6464
6465 wp_send_json_error(array(
6466 'message' => 'Item failed after maximum attempts: ' . $error_message,
6467 'permanent_failure' => true,
6468 'item_id' => $item_id
6469 ));
6470 } else {
6471 // Mark for retry
6472 $wpdb->update(
6473 $table_name,
6474 array(
6475 'status' => 'failed',
6476 'error_message' => $error_message
6477 ),
6478 array('id' => $item_id),
6479 array('%s', '%s'),
6480 array('%d')
6481 );
6482
6483 wp_send_json_error(array(
6484 'message' => 'Item processing failed, will retry: ' . $error_message,
6485 'can_retry' => true,
6486 'item_id' => $item_id,
6487 'attempts' => $item ? $item->attempts : 0
6488 ));
6489 }
6490 }
6491 }
6492
6493 /**
6494 * Process a URL from the queue
6495 */
6496 private function mxchat_process_queue_url($item_data, $bot_id = 'default') {
6497 $url = isset($item_data['url']) ? $item_data['url'] : '';
6498
6499 if (empty($url)) {
6500 return new WP_Error('invalid_url', 'URL is empty');
6501 }
6502
6503 // Get bot-specific API key early (needed for both paths)
6504 $bot_options = $this->get_bot_options($bot_id);
6505 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
6506 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
6507
6508 if (strpos($selected_model, 'voyage') === 0) {
6509 $api_key = $options['voyage_api_key'] ?? '';
6510 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
6511 $api_key = $options['gemini_api_key'] ?? '';
6512 } else {
6513 $api_key = $options['api_key'] ?? '';
6514 }
6515
6516 if (empty($api_key)) {
6517 return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
6518 }
6519
6520 // Check if this is a WooCommerce product URL and WooCommerce is active
6521 $is_product_url = (strpos($url, '/product/') !== false || strpos($url, '/shop/') !== false);
6522 $content_type = $is_product_url ? 'product' : 'url';
6523
6524 // Try to get WooCommerce product data if it's a product URL
6525 if ($is_product_url && class_exists('WooCommerce')) {
6526 $product_content = $this->mxchat_extract_woocommerce_product_content($url);
6527
6528 if (!empty($product_content)) {
6529 // Successfully extracted WooCommerce product data with pricing
6530 $result = MxChat_Utils::submit_content_to_db(
6531 $product_content,
6532 $url,
6533 $api_key,
6534 null,
6535 $bot_id,
6536 'product'
6537 );
6538 return $result;
6539 }
6540 // If WooCommerce extraction failed, fall through to HTML extraction
6541 }
6542
6543 // Fetch URL content (fallback for non-products or when WooCommerce extraction fails)
6544 $response = wp_remote_get($url, array(
6545 'timeout' => 30,
6546 'redirection' => 5,
6547 'user-agent' => 'MxChat/1.0'
6548 ));
6549
6550 if (is_wp_error($response)) {
6551 return $response;
6552 }
6553
6554 $response_code = wp_remote_retrieve_response_code($response);
6555 if ($response_code !== 200) {
6556 return new WP_Error('http_error', 'HTTP ' . $response_code . ' error');
6557 }
6558
6559 $html = wp_remote_retrieve_body($response);
6560
6561 if (empty($html)) {
6562 return new WP_Error('empty_response', 'Empty response body');
6563 }
6564
6565 // Extract and sanitize content
6566 $content = $this->mxchat_extract_main_content($html);
6567 $sanitized = $this->mxchat_sanitize_content_for_api($content);
6568
6569 if (empty($sanitized)) {
6570 // Not an error - just no content found (maybe a redirect or empty page)
6571 return false;
6572 }
6573
6574 // Submit to database with content_type
6575 $result = MxChat_Utils::submit_content_to_db(
6576 $sanitized,
6577 $url,
6578 $api_key,
6579 null,
6580 $bot_id,
6581 $content_type
6582 );
6583
6584 return $result;
6585 }
6586
6587 /**
6588 * Extract WooCommerce product content including pricing
6589 *
6590 * @param string $url The product URL
6591 * @return string|false Product content with pricing, or false if not found
6592 */
6593 private function mxchat_extract_woocommerce_product_content($url) {
6594 // Try to get product ID from URL
6595 $product_id = url_to_postid($url);
6596
6597 // If url_to_postid fails, try to extract from URL pattern
6598 if (!$product_id) {
6599 $product_slug = '';
6600
6601 // Handle pretty permalinks: /product/product-name/
6602 if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
6603 $product_slug = $matches[1];
6604 }
6605
6606 if (!empty($product_slug)) {
6607 $product_post = get_page_by_path($product_slug, OBJECT, 'product');
6608 if ($product_post) {
6609 $product_id = $product_post->ID;
6610 }
6611 }
6612 }
6613
6614 if (!$product_id) {
6615 return false;
6616 }
6617
6618 // Get WooCommerce product object
6619 $product = wc_get_product($product_id);
6620
6621 if (!$product) {
6622 return false;
6623 }
6624
6625 // Build product content with pricing (similar to mxchat_store_product_embedding)
6626 $title = $product->get_name();
6627 $description = $product->get_description();
6628 $short_description = $product->get_short_description();
6629 $sku = $product->get_sku();
6630
6631 // Get pricing information
6632 $regular_price = $product->get_regular_price();
6633 $sale_price = $product->get_sale_price();
6634 $price = $product->get_price(); // Current active price
6635
6636 // Get currency symbol
6637 $currency_symbol = get_woocommerce_currency_symbol();
6638
6639 // Format content
6640 $content = $title . "\n\n";
6641
6642 if (!empty($short_description)) {
6643 $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
6644 }
6645
6646 if (!empty($description)) {
6647 $content .= wp_strip_all_tags($description) . "\n\n";
6648 }
6649
6650 // Add pricing information
6651 if (!empty($regular_price)) {
6652 $content .= "Price: " . $currency_symbol . $regular_price . "\n";
6653 } elseif (!empty($price)) {
6654 $content .= "Price: " . $currency_symbol . $price . "\n";
6655 }
6656
6657 if (!empty($sale_price) && $sale_price !== $regular_price) {
6658 $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
6659 }
6660
6661 // Handle variable products - show price range
6662 if ($product->is_type('variable')) {
6663 $min_price = $product->get_variation_price('min');
6664 $max_price = $product->get_variation_price('max');
6665 if ($min_price !== $max_price) {
6666 $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
6667 }
6668 }
6669
6670 if (!empty($sku)) {
6671 $content .= "SKU: " . $sku . "\n";
6672 }
6673
6674 // Get product categories
6675 $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
6676 if (!empty($categories) && !is_wp_error($categories)) {
6677 $content .= "Categories: " . implode(', ', $categories) . "\n";
6678 }
6679
6680 // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
6681 $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
6682 if (!empty($custom_tabs) && is_array($custom_tabs)) {
6683 foreach ($custom_tabs as $tab) {
6684 $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
6685 $tab_content = isset($tab['content']) ? $tab['content'] : '';
6686
6687 if (!empty($tab_title) && !empty($tab_content)) {
6688 $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
6689 }
6690 }
6691 }
6692
6693 // Also check for reusable/saved tabs applied to this product
6694 $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
6695 if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
6696 $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
6697 if (!empty($saved_tabs) && is_array($saved_tabs)) {
6698 foreach ($applied_saved_tabs as $saved_tab_id) {
6699 if (isset($saved_tabs[$saved_tab_id])) {
6700 $tab = $saved_tabs[$saved_tab_id];
6701 $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
6702 $tab_content = isset($tab['content']) ? $tab['content'] : '';
6703
6704 if (!empty($tab_title) && !empty($tab_content)) {
6705 $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
6706 }
6707 }
6708 }
6709 }
6710 }
6711
6712 return $this->mxchat_sanitize_content_for_api($content);
6713 }
6714
6715 /**
6716 * Process a PDF page from the queue
6717 */
6718 private function mxchat_process_queue_pdf_page($item_data, $bot_id = 'default') {
6719 $pdf_path = isset($item_data['pdf_path']) ? $item_data['pdf_path'] : '';
6720 $pdf_url = isset($item_data['pdf_url']) ? $item_data['pdf_url'] : '';
6721 $page_number = isset($item_data['page_number']) ? absint($item_data['page_number']) : 0;
6722 $total_pages = isset($item_data['total_pages']) ? absint($item_data['total_pages']) : 0;
6723
6724 if (empty($pdf_path) || !file_exists($pdf_path)) {
6725 return new WP_Error('pdf_not_found', 'PDF file not found: ' . $pdf_path);
6726 }
6727
6728 if ($page_number < 1) {
6729 return new WP_Error('invalid_page', 'Invalid page number');
6730 }
6731
6732 try {
6733 $parser = new \Smalot\PdfParser\Parser();
6734 $pdf = $parser->parseFile($pdf_path);
6735 $pages = $pdf->getPages();
6736
6737 if (!isset($pages[$page_number - 1])) {
6738 return new WP_Error('page_not_found', 'Page ' . $page_number . ' not found in PDF');
6739 }
6740
6741 $text = $pages[$page_number - 1]->getText();
6742
6743 if (empty($text)) {
6744 // Not an error - just an empty page
6745 return false;
6746 }
6747
6748 $sanitized = $this->mxchat_sanitize_content_for_api($text);
6749
6750 if (empty($sanitized)) {
6751 return false;
6752 }
6753
6754 // Create metadata
6755 $metadata = array(
6756 'document_type' => 'pdf',
6757 'total_pages' => $total_pages,
6758 'current_page' => $page_number,
6759 'source_url' => $pdf_url
6760 );
6761
6762 $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized;
6763 $page_url = esc_url($pdf_url . "#page=" . $page_number);
6764
6765 // Get bot-specific API key
6766 $bot_options = $this->get_bot_options($bot_id);
6767 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
6768 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
6769
6770 if (strpos($selected_model, 'voyage') === 0) {
6771 $api_key = $options['voyage_api_key'] ?? '';
6772 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
6773 $api_key = $options['gemini_api_key'] ?? '';
6774 } else {
6775 $api_key = $options['api_key'] ?? '';
6776 }
6777
6778 if (empty($api_key)) {
6779 return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
6780 }
6781
6782 // Submit to database - UPDATED 2.5.6: Added content_type 'pdf'
6783 $result = MxChat_Utils::submit_content_to_db(
6784 $content_with_metadata,
6785 $page_url,
6786 $api_key,
6787 null,
6788 $bot_id,
6789 'pdf'
6790 );
6791
6792 return $result;
6793
6794 } catch (Exception $e) {
6795 return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
6796 }
6797 }
6798
6799 /**
6800 * AJAX: Get queue processing status
6801 */
6802 public function ajax_mxchat_get_queue_status() {
6803 // Verify nonce and permissions
6804 check_ajax_referer('mxchat_queue_nonce', 'nonce');
6805
6806 if (!current_user_can('manage_options')) {
6807 wp_send_json_error('Unauthorized access');
6808 }
6809
6810 $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
6811
6812 if (empty($queue_id)) {
6813 wp_send_json_error('Missing queue ID');
6814 }
6815
6816 global $wpdb;
6817 $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6818
6819 // Get counts by status
6820 $counts = $wpdb->get_results($wpdb->prepare(
6821 "SELECT status, COUNT(*) as count
6822 FROM $table_name
6823 WHERE queue_id = %s
6824 GROUP BY status",
6825 $queue_id
6826 ), OBJECT_K);
6827
6828 $total = 0;
6829 $completed = 0;
6830 $failed = 0;
6831 $processing = 0;
6832 $pending = 0;
6833
6834 foreach ($counts as $status => $data) {
6835 $count = absint($data->count);
6836 $total += $count;
6837
6838 switch ($status) {
6839 case 'completed':
6840 $completed = $count;
6841 break;
6842 case 'failed':
6843 $failed = $count;
6844 break;
6845 case 'processing':
6846 $processing = $count;
6847 break;
6848 case 'pending':
6849 $pending = $count;
6850 break;
6851 }
6852 }
6853
6854 // Calculate percentage
6855 $percentage = $total > 0 ? round((($completed + $failed) / $total) * 100) : 0;
6856
6857 // Get failed items details
6858 $failed_items = array();
6859 if ($failed > 0) {
6860 $failed_items = $wpdb->get_results($wpdb->prepare(
6861 "SELECT item_type, item_data, error_message, attempts
6862 FROM $table_name
6863 WHERE queue_id = %s
6864 AND status = 'failed'
6865 AND attempts >= max_attempts
6866 ORDER BY id DESC
6867 LIMIT 50",
6868 $queue_id
6869 ));
6870 }
6871
6872 // Get queue metadata
6873 $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
6874 $queue_type = $this->mxchat_get_queue_meta($queue_id, 'queue_type');
6875
6876 // Determine if queue is complete
6877 $is_complete = ($pending === 0 && $processing === 0);
6878
6879 wp_send_json_success(array(
6880 'queue_id' => $queue_id,
6881 'queue_type' => $queue_type,
6882 'source_url' => $source_url,
6883 'total' => $total,
6884 'completed' => $completed,
6885 'failed' => $failed,
6886 'processing' => $processing,
6887 'pending' => $pending,
6888 'percentage' => $percentage,
6889 'is_complete' => $is_complete,
6890 'failed_items' => $failed_items,
6891 'status' => $is_complete ? 'complete' : 'processing'
6892 ));
6893 }
6894
6895 /**
6896 * AJAX: Clear completed queue
6897 */
6898 public function ajax_mxchat_clear_queue() {
6899 // Verify nonce and permissions
6900 check_ajax_referer('mxchat_queue_nonce', 'nonce');
6901
6902 if (!current_user_can('manage_options')) {
6903 wp_send_json_error('Unauthorized access');
6904 }
6905
6906 $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
6907
6908 if (empty($queue_id)) {
6909 wp_send_json_error('Missing queue ID');
6910 }
6911
6912 global $wpdb;
6913 $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6914 $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
6915
6916 // Delete queue items
6917 $wpdb->delete(
6918 $table_name,
6919 array('queue_id' => $queue_id),
6920 array('%s')
6921 );
6922
6923 // Delete queue metadata
6924 $wpdb->delete(
6925 $meta_table,
6926 array('queue_id' => $queue_id),
6927 array('%s')
6928 );
6929
6930 wp_send_json_success(array(
6931 'message' => 'Queue cleared successfully'
6932 ));
6933 }
6934
6935 /**
6936 * AJAX: Retry failed items in queue
6937 */
6938 public function ajax_mxchat_retry_failed() {
6939 // Verify nonce and permissions
6940 check_ajax_referer('mxchat_queue_nonce', 'nonce');
6941
6942 if (!current_user_can('manage_options')) {
6943 wp_send_json_error('Unauthorized access');
6944 }
6945
6946 $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
6947
6948 if (empty($queue_id)) {
6949 wp_send_json_error('Missing queue ID');
6950 }
6951
6952 global $wpdb;
6953 $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6954
6955 // Reset failed items to pending and reset attempt count
6956 $updated = $wpdb->update(
6957 $table_name,
6958 array(
6959 'status' => 'pending',
6960 'attempts' => 0,
6961 'error_message' => null
6962 ),
6963 array(
6964 'queue_id' => $queue_id,
6965 'status' => 'failed'
6966 ),
6967 array('%s', '%d', '%s'),
6968 array('%s', '%s')
6969 );
6970
6971 wp_send_json_success(array(
6972 'message' => 'Reset ' . $updated . ' failed items for retry',
6973 'reset_count' => $updated
6974 ));
6975 }
6976
6977
6978 public function ajax_mxchat_mark_queue_complete() {
6979 check_ajax_referer('mxchat_queue_nonce', 'nonce');
6980
6981 if (!current_user_can('manage_options')) {
6982 wp_send_json_error('Unauthorized access');
6983 }
6984
6985 $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
6986
6987 if (empty($queue_id)) {
6988 wp_send_json_error('Missing queue ID');
6989 }
6990
6991 // Clear active queue transients
6992 if (strpos($queue_id, 'sitemap_') === 0) {
6993 delete_transient('mxchat_active_queue_sitemap');
6994 } else if (strpos($queue_id, 'pdf_') === 0) {
6995 delete_transient('mxchat_active_queue_pdf');
6996 }
6997
6998 wp_send_json_success(array('message' => 'Queue marked as complete'));
6999 }
7000
7001
7002 // ========================================
7003 // STATIC ACCESS METHODS
7004 // ========================================
7005
7006 /**
7007 * Get singleton instance
7008 */
7009 public static function get_instance() {
7010 static $instance = null;
7011 if ($instance === null) {
7012 $instance = new self();
7013 }
7014 return $instance;
7015 }
7016 }
7017
7018 // Initialize the Knowledge manager
7019 $mxchat_knowledge_manager = MxChat_Knowledge_Manager::get_instance();