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

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

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