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

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

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