PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.2
MxChat – AI Chatbot & Content Generation for WordPress v3.0.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
← All changes | admin/class-knowledge-manager.php +7018 -7930 3.2.13.0.2 View file →
@@ -1,7931 +1,7019 @@
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_submit_pdf_file', array($this, 'mxchat_handle_pdf_file_submission'));
35 - add_action('admin_post_mxchat_stop_processing', array($this, 'mxchat_stop_processing'));
36 -
37 - // AJAX handlers for real-time processing and status updates
38 - add_action('wp_ajax_mxchat_get_status_updates', array($this, 'mxchat_ajax_get_status_updates'));
39 - add_action('wp_ajax_mxchat_dismiss_completed_status', array($this, 'mxchat_ajax_dismiss_completed_status'));
40 - add_action('wp_ajax_mxchat_get_content_list', array($this, 'ajax_mxchat_get_content_list'));
41 - add_action('wp_ajax_mxchat_process_selected_content', array($this, 'ajax_mxchat_process_selected_content'));
42 - add_action('wp_ajax_mxchat_save_inline_prompt', array($this, 'mxchat_save_inline_prompt'));
43 - add_action('admin_post_mxchat_delete_pinecone_prompt', array($this, 'mxchat_handle_pinecone_prompt_delete'));
44 - add_action('wp_ajax_mxchat_delete_pinecone_prompt', array($this, 'ajax_mxchat_delete_pinecone_prompt'));
45 - add_action('wp_ajax_mxchat_delete_chunks_by_url', array($this, 'ajax_mxchat_delete_chunks_by_url'));
46 - add_action('wp_ajax_mxchat_delete_wordpress_prompt', array($this, 'ajax_mxchat_delete_wordpress_prompt'));
47 - add_action('wp_ajax_mxchat_bulk_delete_knowledge', array($this, 'ajax_mxchat_bulk_delete_knowledge'));
48 - add_action('wp_ajax_mxchat_update_role_restriction', array($this, 'ajax_mxchat_update_role_restriction'));
49 -
50 - // Queue-based processing AJAX handlers
51 - add_action('wp_ajax_mxchat_get_next_queue_item', array($this, 'ajax_mxchat_get_next_queue_item'));
52 - add_action('wp_ajax_mxchat_process_queue_item', array($this, 'ajax_mxchat_process_queue_item'));
53 - add_action('wp_ajax_mxchat_get_queue_status', array($this, 'ajax_mxchat_get_queue_status'));
54 - add_action('wp_ajax_mxchat_clear_queue', array($this, 'ajax_mxchat_clear_queue'));
55 - add_action('wp_ajax_mxchat_retry_failed', array($this, 'ajax_mxchat_retry_failed'));
56 - add_action('wp_ajax_mxchat_get_recent_entries', array($this, 'ajax_mxchat_get_recent_entries'));
57 - add_action('wp_ajax_mxchat_detect_sitemaps', array($this, 'ajax_mxchat_detect_sitemaps'));
58 - add_action('wp_ajax_mxchat_refresh_pinecone_entries', array($this, 'ajax_mxchat_refresh_pinecone_entries'));
59 - add_action('wp_ajax_mxchat_paginate_entries', array($this, 'ajax_mxchat_paginate_entries'));
60 - add_action('wp_ajax_mxchat_get_entry_content', array($this, 'ajax_mxchat_get_entry_content'));
61 - add_action('wp_ajax_mxchat_save_entry_content', array($this, 'ajax_mxchat_save_entry_content'));
62 -
63 - // Hook for content deletion
64 - add_action('mxchat_delete_content', array($this, 'mxchat_delete_from_pinecone_by_url'), 10, 1);
65 -
66 - // WordPress post management hooks
67 - add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
68 - add_action('post_updated', array($this, 'mxchat_handle_post_update'), 10, 3);
69 - add_action('before_delete_post', array($this, 'mxchat_handle_post_delete'));
70 - add_action('wp_trash_post', array($this, 'mxchat_handle_post_delete'));
71 -
72 - // ACF hook - fires AFTER ACF fields are saved, ensuring ACF data is available
73 - // Priority 20 to run after ACF's own save (which runs at priority 10)
74 - add_action('acf/save_post', array($this, 'mxchat_handle_acf_save'), 20);
75 -
76 - add_action('wp_ajax_mxchat_mark_queue_complete', array($this, 'ajax_mxchat_mark_queue_complete'));
77 -
78 - // WooCommerce product hooks (if WooCommerce is active)
79 - if (class_exists('WooCommerce')) {
80 - add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
81 - add_action('save_post_product', array($this, 'mxchat_handle_product_change'), 10, 3);
82 - add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
83 - add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
84 - }
85 -}
86 -
87 - /**
88 - * Get current options (refreshed)
89 - */
90 - private function mxchat_get_options() {
91 - if (empty($this->options)) {
92 - $this->options = get_option('mxchat_options', array());
93 - }
94 - return $this->options;
95 - }
96 -
97 -
98 - // ========================================
99 - // MAIN CONTENT SUBMISSION HANDLERS
100 - // ========================================
101 -
102 -public function mxchat_handle_content_submission() {
103 - // Check if the form was submitted and the user has permission.
104 - if (!isset($_POST['submit_content']) || !current_user_can('manage_options')) {
105 - return;
106 - }
107 -
108 - // Verify the nonce.
109 - $nonce = isset($_POST['mxchat_submit_content_nonce']) ? sanitize_text_field(wp_unslash($_POST['mxchat_submit_content_nonce'])) : '';
110 - if (!wp_verify_nonce($nonce, 'mxchat_submit_content_action')) {
111 - wp_die(esc_html__('Nonce verification failed.', 'mxchat'));
112 - }
113 -
114 - // Sanitize the inputs.
115 - // Use wp_kses_post to allow safe HTML and wp_unslash to remove WordPress added slashes
116 - $article_content = wp_kses_post(wp_unslash($_POST['article_content']));
117 - $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
118 -
119 - // Get bot_id from form submission
120 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
121 -
122 - // Get bot-specific options and API key
123 - $bot_options = $this->get_bot_options($bot_id);
124 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
125 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
126 -
127 - if (strpos($selected_model, 'voyage') === 0) {
128 - $api_key = $options['voyage_api_key'] ?? '';
129 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
130 - $api_key = $options['gemini_api_key'] ?? '';
131 - } else {
132 - $api_key = $options['api_key'] ?? '';
133 - }
134 -
135 - if (empty($api_key)) {
136 - set_transient('mxchat_admin_notice_error',
137 - esc_html__('API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
138 - 30
139 - );
140 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
141 - exit;
142 - }
143 -
144 - // Use centralized utility function with bot_id
145 - $result = MxChat_Utils::submit_content_to_db($article_content, $article_url, $api_key, null, $bot_id);
146 -
147 - if (is_wp_error($result)) {
148 - set_transient('mxchat_admin_notice_error',
149 - esc_html__('Error storing content: ', 'mxchat') . $result->get_error_message(),
150 - 30
151 - );
152 - } else {
153 - set_transient('mxchat_admin_notice_success',
154 - esc_html__('Content successfully submitted!', 'mxchat'),
155 - 30
156 - );
157 - }
158 -
159 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
160 - exit;
161 -}
162 -
163 -public function mxchat_is_pdf_url($url, $response) {
164 - $content_type = wp_remote_retrieve_header($response, 'content-type');
165 - $file_extension = strtolower(pathinfo($url, PATHINFO_EXTENSION));
166 -
167 - // Check Content-Disposition header for .pdf filename (Google Drive sends this)
168 - $disposition = wp_remote_retrieve_header($response, 'content-disposition');
169 - $has_pdf_disposition = ! empty($disposition) && stripos($disposition, '.pdf') !== false;
170 -
171 - return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf' || $has_pdf_disposition;
172 -}
173 -
174 -
175 -public function mxchat_handle_pdf_for_knowledge_base($pdf_url, $response, $bot_id = 'default') {
176 - if (!current_user_can('manage_options')) {
177 - return false;
178 - }
179 -
180 - $pdf_url = esc_url_raw($pdf_url);
181 - $upload_dir = wp_upload_dir();
182 -
183 - if (isset($upload_dir['error']) && $upload_dir['error'] !== false) {
184 - return false;
185 - }
186 -
187 - $pdf_filename = sanitize_file_name('mxchat_kb_' . time() . '.pdf');
188 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
189 -
190 - $response_body = wp_remote_retrieve_body($response);
191 - if (empty($response_body)) {
192 - return false;
193 - }
194 -
195 - if (!wp_mkdir_p(dirname($pdf_path))) {
196 - return false;
197 - }
198 -
199 - try {
200 - file_put_contents($pdf_path, $response_body);
201 -
202 - if (!file_exists($pdf_path)) {
203 - throw new Exception(__('Failed to save PDF file', 'mxchat'));
204 - }
205 -
206 - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
207 -
208 - if ($total_pages === false || $total_pages < 1) {
209 - throw new Exception(__('Invalid PDF: Unable to parse or no pages found', 'mxchat'));
210 - }
211 -
212 - // Create unique queue ID
213 - $queue_id = 'pdf_' . md5($pdf_url . time());
214 -
215 - // Create array of pages to process
216 - $pages = array();
217 - for ($i = 1; $i <= $total_pages; $i++) {
218 - $pages[] = array(
219 - 'pdf_path' => $pdf_path,
220 - 'pdf_url' => $pdf_url,
221 - 'page_number' => $i,
222 - 'total_pages' => $total_pages
223 - );
224 - }
225 -
226 - // Add pages to queue
227 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
228 -
229 - if ($queued_count === 0) {
230 - wp_delete_file($pdf_path);
231 - throw new Exception(__('Failed to add PDF pages to processing queue', 'mxchat'));
232 - }
233 -
234 - // Store queue metadata
235 - $this->mxchat_set_queue_meta($queue_id, 'source_url', $pdf_url);
236 - $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'pdf');
237 - $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_pages);
238 - $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
239 - $this->mxchat_set_queue_meta($queue_id, 'pdf_path', $pdf_path);
240 - $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
241 -
242 - // Store queue ID in transient for status tracking
243 - set_transient('mxchat_active_queue_pdf', $queue_id, DAY_IN_SECONDS);
244 - set_transient('mxchat_last_pdf_url', $pdf_url, DAY_IN_SECONDS);
245 -
246 - return 'queued';
247 -
248 - } catch (Exception $e) {
249 - if (file_exists($pdf_path)) {
250 - wp_delete_file($pdf_path);
251 - }
252 - return $e->getMessage();
253 - }
254 -}
255 -
256 -/**
257 - * Handle direct PDF file upload from the knowledge base page
258 - */
259 -public function mxchat_handle_pdf_file_submission() {
260 - if (!isset($_POST['submit_pdf_file']) || !current_user_can('manage_options')) {
261 - wp_die(esc_html__('Unauthorized access', 'mxchat'));
262 - }
263 -
264 - check_admin_referer('mxchat_submit_pdf_file_action', 'mxchat_submit_pdf_file_nonce');
265 -
266 - $redirect_url = admin_url('admin.php?page=mxchat-prompts');
267 -
268 - // Validate file upload
269 - if (empty($_FILES['pdf_file']) || $_FILES['pdf_file']['error'] !== UPLOAD_ERR_OK) {
270 - $error_code = isset($_FILES['pdf_file']['error']) ? $_FILES['pdf_file']['error'] : UPLOAD_ERR_NO_FILE;
271 - $error_messages = array(
272 - UPLOAD_ERR_INI_SIZE => __('The uploaded file exceeds the server upload_max_filesize limit.', 'mxchat'),
273 - UPLOAD_ERR_FORM_SIZE => __('The uploaded file exceeds the form MAX_FILE_SIZE limit.', 'mxchat'),
274 - UPLOAD_ERR_PARTIAL => __('The file was only partially uploaded.', 'mxchat'),
275 - UPLOAD_ERR_NO_FILE => __('No file was uploaded. Please select a PDF file.', 'mxchat'),
276 - UPLOAD_ERR_NO_TMP_DIR => __('Server missing temporary folder.', 'mxchat'),
277 - UPLOAD_ERR_CANT_WRITE => __('Server failed to write file to disk.', 'mxchat'),
278 - );
279 - $error_msg = isset($error_messages[$error_code]) ? $error_messages[$error_code] : __('Unknown upload error.', 'mxchat');
280 - set_transient('mxchat_admin_notice_error', $error_msg, 30);
281 - wp_safe_redirect(esc_url($redirect_url));
282 - exit;
283 - }
284 -
285 - $file = $_FILES['pdf_file'];
286 -
287 - // Validate MIME type
288 - $finfo = finfo_open(FILEINFO_MIME_TYPE);
289 - $mime_type = finfo_file($finfo, $file['tmp_name']);
290 - finfo_close($finfo);
291 -
292 - if ($mime_type !== 'application/pdf') {
293 - set_transient('mxchat_admin_notice_error',
294 - esc_html__('Invalid file type. Only PDF files are accepted.', 'mxchat'),
295 - 30
296 - );
297 - wp_safe_redirect(esc_url($redirect_url));
298 - exit;
299 - }
300 -
301 - // Validate extension
302 - $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
303 - if ($ext !== 'pdf') {
304 - set_transient('mxchat_admin_notice_error',
305 - esc_html__('Invalid file extension. Only .pdf files are accepted.', 'mxchat'),
306 - 30
307 - );
308 - wp_safe_redirect(esc_url($redirect_url));
309 - exit;
310 - }
311 -
312 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
313 - $original_filename = sanitize_file_name($file['name']);
314 -
315 - $upload_dir = wp_upload_dir();
316 - if (isset($upload_dir['error']) && $upload_dir['error'] !== false) {
317 - set_transient('mxchat_admin_notice_error',
318 - esc_html__('WordPress upload directory is not writable.', 'mxchat'),
319 - 30
320 - );
321 - wp_safe_redirect(esc_url($redirect_url));
322 - exit;
323 - }
324 -
325 - $pdf_filename = sanitize_file_name('mxchat_kb_' . time() . '.pdf');
326 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
327 -
328 - if (!wp_mkdir_p(dirname($pdf_path))) {
329 - set_transient('mxchat_admin_notice_error',
330 - esc_html__('Failed to create upload directory.', 'mxchat'),
331 - 30
332 - );
333 - wp_safe_redirect(esc_url($redirect_url));
334 - exit;
335 - }
336 -
337 - // Move uploaded file
338 - if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
339 - set_transient('mxchat_admin_notice_error',
340 - esc_html__('Failed to save uploaded PDF file.', 'mxchat'),
341 - 30
342 - );
343 - wp_safe_redirect(esc_url($redirect_url));
344 - exit;
345 - }
346 -
347 - try {
348 - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
349 -
350 - if ($total_pages === false || $total_pages < 1) {
351 - throw new Exception(__('Invalid PDF: Unable to parse or no pages found', 'mxchat'));
352 - }
353 -
354 - // Use original filename as the source identifier
355 - $source_label = 'upload://' . $original_filename;
356 -
357 - $queue_id = 'pdf_' . md5($source_label . time());
358 -
359 - $pages = array();
360 - for ($i = 1; $i <= $total_pages; $i++) {
361 - $pages[] = array(
362 - 'pdf_path' => $pdf_path,
363 - 'pdf_url' => $source_label,
364 - 'page_number' => $i,
365 - 'total_pages' => $total_pages,
366 - );
367 - }
368 -
369 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
370 -
371 - if ($queued_count === 0) {
372 - wp_delete_file($pdf_path);
373 - throw new Exception(__('Failed to add PDF pages to processing queue', 'mxchat'));
374 - }
375 -
376 - $this->mxchat_set_queue_meta($queue_id, 'source_url', $source_label);
377 - $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'pdf');
378 - $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_pages);
379 - $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
380 - $this->mxchat_set_queue_meta($queue_id, 'pdf_path', $pdf_path);
381 - $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
382 -
383 - set_transient('mxchat_active_queue_pdf', $queue_id, DAY_IN_SECONDS);
384 - set_transient('mxchat_last_pdf_url', $source_label, DAY_IN_SECONDS);
385 -
386 - set_transient('mxchat_admin_notice_success',
387 - sprintf(
388 - esc_html__('PDF "%s" (%d pages) queued for processing. Processing will start automatically.', 'mxchat'),
389 - esc_html($original_filename),
390 - $total_pages
391 - ),
392 - 30
393 - );
394 -
395 - } catch (Exception $e) {
396 - if (file_exists($pdf_path)) {
397 - wp_delete_file($pdf_path);
398 - }
399 - set_transient('mxchat_admin_notice_error',
400 - esc_html__('Failed to process uploaded PDF: ', 'mxchat') . esc_html($e->getMessage()),
401 - 30
402 - );
403 - }
404 -
405 - wp_safe_redirect(esc_url($redirect_url));
406 - exit;
407 -}
408 -
409 -/**
410 - * Validate PDF and count pages with multiple parser attempts
411 - */
412 -private function mxchat_validate_and_count_pdf_pages($pdf_path) {
413 - // Method 1: Try with Smalot PDF Parser (your current method)
414 - try {
415 - mxchat_load_pdf_parser();
416 - $parser = new \Smalot\PdfParser\Parser();
417 - $pdf = $parser->parseFile($pdf_path);
418 - $pages = $pdf->getPages();
419 - $page_count = count($pages);
420 -
421 - if ($page_count > 0) {
422 - //error_log('PDF parsed successfully with Smalot parser: ' . $page_count . ' pages');
423 - return $page_count;
424 - }
425 - } catch (Exception $e) {
426 - //error_log('Smalot PDF parser failed: ' . $e->getMessage());
427 - }
428 -
429 - // Method 2: Try with pdfinfo command (if available)
430 - if (function_exists('shell_exec') && !$this->mxchat_is_shell_disabled()) {
431 - try {
432 - $command = 'pdfinfo ' . escapeshellarg($pdf_path) . ' 2>&1';
433 - $output = shell_exec($command);
434 -
435 - if ($output && preg_match('/Pages:\s*(\d+)/', $output, $matches)) {
436 - $page_count = intval($matches[1]);
437 - if ($page_count > 0) {
438 - //error_log('PDF parsed successfully with pdfinfo: ' . $page_count . ' pages');
439 - return $page_count;
440 - }
441 - }
442 - } catch (Exception $e) {
443 - //error_log('pdfinfo command failed: ' . $e->getMessage());
444 - }
445 - }
446 -
447 - // Method 3: Try to repair PDF and parse again
448 - try {
449 - $repaired_path = $this->mxchat_attempt_pdf_repair($pdf_path);
450 - if ($repaired_path && $repaired_path !== $pdf_path) {
451 - mxchat_load_pdf_parser();
452 - $parser = new \Smalot\PdfParser\Parser();
453 - $pdf = $parser->parseFile($repaired_path);
454 - $pages = $pdf->getPages();
455 - $page_count = count($pages);
456 -
457 - if ($page_count > 0) {
458 - // Replace original with repaired version
459 - copy($repaired_path, $pdf_path);
460 - unlink($repaired_path);
461 - //error_log('PDF repaired and parsed successfully: ' . $page_count . ' pages');
462 - return $page_count;
463 - }
464 -
465 - // Clean up repaired file if it didn't work
466 - unlink($repaired_path);
467 - }
468 - } catch (Exception $e) {
469 - //error_log('PDF repair attempt failed: ' . $e->getMessage());
470 - }
471 -
472 - // Method 4: Manual PDF structure analysis (basic page count)
473 - try {
474 - $page_count = $this->mxchat_manual_pdf_page_count($pdf_path);
475 - if ($page_count > 0) {
476 - //error_log('PDF page count determined manually: ' . $page_count . ' pages');
477 - return $page_count;
478 - }
479 - } catch (Exception $e) {
480 - //error_log('Manual PDF analysis failed: ' . $e->getMessage());
481 - }
482 -
483 - //error_log('All PDF parsing methods failed for: ' . $pdf_path);
484 - return false;
485 -}
486 -
487 -/**
488 - * Check if shell_exec is disabled
489 - */
490 -private function mxchat_is_shell_disabled() {
491 - $disabled = explode(',', ini_get('disable_functions'));
492 - return in_array('shell_exec', $disabled);
493 -}
494 -
495 -/**
496 - * Attempt to repair PDF using basic methods
497 - */
498 -private function mxchat_attempt_pdf_repair($pdf_path) {
499 - try {
500 - $content = file_get_contents($pdf_path);
501 - if (!$content) {
502 - return false;
503 - }
504 -
505 - // Check if PDF starts with proper header
506 - if (substr($content, 0, 4) !== '%PDF') {
507 - // Try to find PDF header in the content
508 - $header_pos = strpos($content, '%PDF');
509 - if ($header_pos !== false && $header_pos < 1024) {
510 - // Remove junk before PDF header
511 - $content = substr($content, $header_pos);
512 - $repaired_path = $pdf_path . '.repaired';
513 - file_put_contents($repaired_path, $content);
514 - return $repaired_path;
515 - }
516 - }
517 -
518 - // Check for EOF marker
519 - $content = rtrim($content);
520 - if (!preg_match('/%%EOF\s*$/', $content)) {
521 - // Add EOF marker if missing
522 - $content .= "\n%%EOF";
523 - $repaired_path = $pdf_path . '.repaired';
524 - file_put_contents($repaired_path, $content);
525 - return $repaired_path;
526 - }
527 -
528 - } catch (Exception $e) {
529 - //error_log('PDF repair error: ' . $e->getMessage());
530 - }
531 -
532 - return false;
533 -}
534 -
535 -/**
536 - * Manual PDF page counting by analyzing PDF structure
537 - */
538 -private function mxchat_manual_pdf_page_count($pdf_path) {
539 - try {
540 - $content = file_get_contents($pdf_path);
541 - if (!$content) {
542 - return 0;
543 - }
544 -
545 - // Method 1: Count /Type /Page objects
546 - $page_count = preg_match_all('/\/Type\s*\/Page[^s]/', $content);
547 - if ($page_count > 0) {
548 - return $page_count;
549 - }
550 -
551 - // Method 2: Look for /Count in pages object
552 - if (preg_match('/\/Type\s*\/Pages.*?\/Count\s+(\d+)/', $content, $matches)) {
553 - return intval($matches[1]);
554 - }
555 -
556 - // Method 3: Count page references
557 - $page_count = preg_match_all('/\d+\s+0\s+obj\s*<<[^>]*\/Type\s*\/Page/', $content);
558 - if ($page_count > 0) {
559 - return $page_count;
560 - }
561 -
562 - } catch (Exception $e) {
563 - //error_log('Manual PDF analysis error: ' . $e->getMessage());
564 - }
565 -
566 - return 0;
567 -}
568 -
569 -
570 -public function mxchat_save_inline_prompt() {
571 - // DEBUG: Log what we're receiving
572 - //error_log('=== MXCHAT DEBUG ===');
573 - //error_log('POST data: ' . print_r($_POST, true));
574 - //error_log('Nonce from POST: ' . ($_POST['_ajax_nonce'] ?? 'NOT FOUND'));
575 -
576 - // Check for nonce security
577 - check_ajax_referer('mxchat_save_inline_nonce', '_ajax_nonce');
578 -
579 - // If we get here, nonce passed
580 - //error_log('Nonce verification PASSED');
581 -
582 - // Verify permissions
583 - if (!current_user_can('manage_options')) {
584 - wp_send_json_error(esc_html__('Permission denied.', 'mxchat'));
585 - return;
586 - }
587 -
588 - global $wpdb;
589 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
590 -
591 - // Validate and sanitize input data
592 - $prompt_id = isset($_POST['id']) ? absint($_POST['id']) : 0;
593 - $article_content = isset($_POST['article_content']) ? wp_kses_post(wp_unslash($_POST['article_content'])) : '';
594 - $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
595 -
596 - if ($prompt_id > 0 && !empty($article_content)) {
597 - // Re-generate the embedding vector for the updated content
598 - $embedding_vector = $this->mxchat_generate_embedding($article_content);
599 - if (is_array($embedding_vector)) {
600 - // Serialize the embedding vector before storing it
601 - $embedding_vector_serialized = serialize($embedding_vector);
602 - // Update the prompt in the database
603 - $updated = $wpdb->update(
604 - $table_name,
605 - array(
606 - 'article_content' => $article_content,
607 - 'embedding_vector' => $embedding_vector_serialized,
608 - 'source_url' => $article_url,
609 - ),
610 - array('id' => $prompt_id),
611 - array('%s', '%s', '%s'),
612 - array('%d')
613 - );
614 - if ($updated !== false) {
615 - wp_send_json_success();
616 - } else {
617 - MxChat_Admin::mxchat_log_debug('knowledge_error', 'Database update failed when saving knowledge entry');
618 - wp_send_json_error(esc_html__('Database update failed.', 'mxchat'));
619 - }
620 - } else {
621 - MxChat_Admin::mxchat_log_debug('embedding_error', 'Embedding generation failed for knowledge entry');
622 - wp_send_json_error(esc_html__('Embedding generation failed.', 'mxchat'));
623 - }
624 - } else {
625 - wp_send_json_error(esc_html__('Invalid data.', 'mxchat'));
626 - }
627 -}
628 -
629 -
630 -/**
631 - * AJAX: Get full content for editing — reassembles chunks if needed.
632 - * Works for both WordPress DB and Pinecone entries.
633 - */
634 -public function ajax_mxchat_get_entry_content() {
635 - check_ajax_referer('mxchat_edit_entry_nonce', 'nonce');
636 -
637 - if ( ! current_user_can('manage_options') ) {
638 - wp_send_json_error( array( 'message' => 'Permission denied.' ) );
639 - }
640 -
641 - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : '';
642 - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0;
643 - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress';
644 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
645 -
646 - if ( $data_source === 'pinecone' ) {
647 - // Pinecone: fetch vectors by source_url, reassemble chunks
648 - $content = $this->get_pinecone_entry_content( $source_url, $entry_id, $bot_id );
649 - } else {
650 - // WordPress DB
651 - $content = $this->get_wordpress_entry_content( $source_url, $entry_id );
652 - }
653 -
654 - if ( is_wp_error( $content ) ) {
655 - wp_send_json_error( array( 'message' => $content->get_error_message() ) );
656 - }
657 -
658 - wp_send_json_success( $content );
659 -}
660 -
661 -/**
662 - * Get content from WordPress DB — reassembles chunks by source_url.
663 - */
664 -private function get_wordpress_entry_content( $source_url, $entry_id ) {
665 - global $wpdb;
666 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
667 -
668 - // If we have a source_url, check for chunks
669 - if ( ! empty( $source_url ) && strpos( $source_url, 'mxchat://' ) !== 0 ) {
670 - $rows = $wpdb->get_results( $wpdb->prepare(
671 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE source_url = %s ORDER BY id ASC",
672 - $source_url
673 - ) );
674 -
675 - if ( $rows && count( $rows ) > 1 ) {
676 - // Multiple rows = chunked. Reassemble.
677 - $chunks = array();
678 - foreach ( $rows as $row ) {
679 - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content );
680 - $index = isset( $parsed['metadata']['chunk_index'] ) ? intval( $parsed['metadata']['chunk_index'] ) : count( $chunks );
681 - $chunks[ $index ] = $parsed['text'];
682 - }
683 - ksort( $chunks );
684 - return array(
685 - 'content' => implode( "\n\n", $chunks ),
686 - 'source_url' => $source_url,
687 - 'is_chunked' => true,
688 - 'chunk_count' => count( $chunks ),
689 - 'content_type' => $rows[0]->content_type,
690 - );
691 - } elseif ( $rows && count( $rows ) === 1 ) {
692 - $parsed = MxChat_Chunker::parse_stored_chunk( $rows[0]->article_content );
693 - return array(
694 - 'content' => $parsed['text'],
695 - 'source_url' => $source_url,
696 - 'entry_id' => $rows[0]->id,
697 - 'is_chunked' => false,
698 - 'content_type' => $rows[0]->content_type,
699 - );
700 - }
701 - }
702 -
703 - // Fallback: fetch by ID
704 - if ( $entry_id > 0 ) {
705 - $row = $wpdb->get_row( $wpdb->prepare(
706 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE id = %d",
707 - $entry_id
708 - ) );
709 - if ( $row ) {
710 - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content );
711 - return array(
712 - 'content' => $parsed['text'],
713 - 'source_url' => $row->source_url,
714 - 'entry_id' => $row->id,
715 - 'is_chunked' => false,
716 - 'content_type' => $row->content_type,
717 - );
718 - }
719 - }
720 -
721 - return new WP_Error( 'not_found', 'Entry not found.' );
722 -}
723 -
724 -/**
725 - * Get content from Pinecone — fetches vectors by source_url, reassembles chunks.
726 - */
727 -private function get_pinecone_entry_content( $source_url, $entry_id, $bot_id ) {
728 - if ( ! class_exists('MxChat_Pinecone_Manager') ) {
729 - return new WP_Error( 'pinecone_unavailable', 'Pinecone manager not available.' );
730 - }
731 -
732 - // Get Pinecone config
733 - if ( $bot_id === 'default' || ! class_exists('MxChat_Multi_Bot_Manager') ) {
734 - $pinecone_options = get_option('mxchat_pinecone_addon_options');
735 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
736 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
737 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
738 - } else {
739 - $bot_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
740 - $api_key = $bot_config['api_key'] ?? '';
741 - $host = $bot_config['host'] ?? '';
742 - $namespace = $bot_config['namespace'] ?? '';
743 - }
744 -
745 - if ( empty($host) || empty($api_key) ) {
746 - return new WP_Error( 'pinecone_config', 'Pinecone not configured.' );
747 - }
748 -
749 - // List vectors with the source_url prefix
750 - $base_id = md5( $source_url );
751 - $vector_ids = array( $base_id );
752 -
753 - // Find chunk vectors
754 - $list_url = "https://{$host}/vectors/list";
755 - $list_body = array( 'prefix' => $base_id . '_chunk_', 'limit' => 100 );
756 - if ( ! empty($namespace) ) {
757 - $list_body['namespace'] = $namespace;
758 - }
759 -
760 - $list_resp = wp_remote_post( $list_url, array(
761 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
762 - 'body' => wp_json_encode( $list_body ),
763 - 'timeout' => 15,
764 - ) );
765 -
766 - if ( ! is_wp_error($list_resp) ) {
767 - $list_data = json_decode( wp_remote_retrieve_body($list_resp), true );
768 - if ( ! empty($list_data['vectors']) ) {
769 - foreach ( $list_data['vectors'] as $v ) {
770 - $vector_ids[] = $v['id'];
771 - }
772 - }
773 - }
774 -
775 - // Fetch vectors with metadata
776 - $fetch_url = "https://{$host}/vectors/fetch";
777 - $fetch_body = array( 'ids' => $vector_ids );
778 - if ( ! empty($namespace) ) {
779 - $fetch_body['namespace'] = $namespace;
780 - }
781 -
782 - $fetch_resp = wp_remote_post( $fetch_url, array(
783 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
784 - 'body' => wp_json_encode( $fetch_body ),
785 - 'timeout' => 15,
786 - ) );
787 -
788 - if ( is_wp_error($fetch_resp) ) {
789 - return new WP_Error( 'pinecone_fetch', 'Failed to fetch from Pinecone.' );
790 - }
791 -
792 - $fetch_data = json_decode( wp_remote_retrieve_body($fetch_resp), true );
793 - $vectors = $fetch_data['vectors'] ?? array();
794 -
795 - if ( empty($vectors) ) {
796 - return new WP_Error( 'not_found', 'Entry not found in Pinecone.' );
797 - }
798 -
799 - // Reassemble chunks
800 - $chunks = array();
801 - $content_type = 'content';
802 - foreach ( $vectors as $vid => $vector ) {
803 - $meta = $vector['metadata'] ?? array();
804 - $text = $meta['text'] ?? '';
805 - $index = $meta['chunk_index'] ?? 0;
806 - $content_type = $meta['type'] ?? 'content';
807 - $chunks[ intval($index) ] = $text;
808 - }
809 - ksort( $chunks );
810 -
811 - return array(
812 - 'content' => implode( "\n\n", $chunks ),
813 - 'source_url' => $source_url,
814 - 'is_chunked' => count($chunks) > 1,
815 - 'chunk_count' => count($chunks),
816 - 'content_type' => $content_type,
817 - );
818 -}
819 -
820 -/**
821 - * AJAX: Save edited content — re-chunks and re-embeds as needed.
822 - * Works for both WordPress DB and Pinecone entries.
823 - */
824 -public function ajax_mxchat_save_entry_content() {
825 - check_ajax_referer('mxchat_edit_entry_nonce', 'nonce');
826 -
827 - if ( ! current_user_can('manage_options') ) {
828 - wp_send_json_error( array( 'message' => 'Permission denied.' ) );
829 - }
830 -
831 - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : '';
832 - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0;
833 - $content = isset($_POST['content']) ? wp_kses_post( wp_unslash($_POST['content']) ) : '';
834 - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress';
835 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
836 - $content_type = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : 'content';
837 -
838 - if ( empty($content) ) {
839 - wp_send_json_error( array( 'message' => 'Content cannot be empty.' ) );
840 - }
841 -
842 - // Get the embedding API key
843 - $options = get_option('mxchat_options', array());
844 - $api_key = '';
845 -
846 - if ( $bot_id !== 'default' && class_exists('MxChat_Multi_Bot_Manager') ) {
847 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
848 - $api_key = $bot_options['api_key'] ?? '';
849 - }
850 - if ( empty($api_key) ) {
851 - $api_key = $options['api_key'] ?? '';
852 - }
853 -
854 - global $wpdb;
855 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
856 -
857 - // If source_url is empty but we have an entry_id, look it up
858 - if ( empty($source_url) && $entry_id > 0 && $data_source === 'wordpress' ) {
859 - $row = $wpdb->get_row( $wpdb->prepare( "SELECT source_url FROM {$table} WHERE id = %d", $entry_id ) );
860 - if ( $row && ! empty($row->source_url) ) {
861 - $source_url = $row->source_url;
862 - }
863 - }
864 -
865 - // For manual entries (no source_url or mxchat:// prefix), delete the old entry by ID first
866 - // so submit_content_to_db creates a replacement instead of a duplicate
867 - // Also treat legacy mxchat.ai source URLs as manual — old bug assigned the site URL to manual entries
868 - $is_legacy_manual = !empty($source_url) && strpos($source_url, 'mxchat.ai') !== false && strpos($source_url, 'mxchat://') !== 0;
869 - if ( $entry_id > 0 && (empty($source_url) || strpos($source_url, 'mxchat://') === 0 || $is_legacy_manual) ) {
870 - $wpdb->delete( $table, array( 'id' => $entry_id ), array( '%d' ) );
871 - // Clear legacy URL so submit_content_to_db generates a unique mxchat:// identifier
872 - // instead of reusing the shared URL (which would mass-delete other entries with the same URL)
873 - if ( $is_legacy_manual ) {
874 - $source_url = '';
875 - }
876 - }
877 -
878 - // Use the existing submit_content_to_db which handles chunking, Pinecone, and WP DB
879 - $vector_id = ! empty($source_url) ? md5($source_url) : md5('mxchat_manual_' . $entry_id);
880 -
881 - // submit_content_to_db already handles: delete old chunks → re-chunk → re-embed → store
882 - $result = MxChat_Utils::submit_content_to_db( $content, $source_url, $api_key, $vector_id, $bot_id, $content_type );
883 -
884 - if ( is_wp_error($result) ) {
885 - wp_send_json_error( array( 'message' => $result->get_error_message() ) );
886 - }
887 -
888 - wp_send_json_success( array( 'message' => 'Content saved and re-embedded successfully.' ) );
889 -}
890 -
891 -public function mxchat_get_pdf_processing_status($pdf_url) {
892 - $pdf_url = esc_url_raw($pdf_url);
893 - $status = get_transient(sanitize_key('mxchat_pdf_status_' . md5($pdf_url)));
894 -
895 - if (!$status || !is_array($status)) {
896 - return false;
897 - }
898 -
899 - // Check for stalled processing (no updates for 5 minutes)
900 - if ($status['status'] === 'processing' && (time() - absint($status['last_update'])) > 300) {
901 - $status['status'] = 'error';
902 - $status['error'] = __('PDF processing appears to be stalled. No updates for over 5 minutes.', 'mxchat');
903 -
904 - // Save the updated status
905 - set_transient(
906 - sanitize_key('mxchat_pdf_status_' . md5($pdf_url)),
907 - array_map('sanitize_text_field', $status),
908 - DAY_IN_SECONDS
909 - );
910 - }
911 -
912 - $result = array(
913 - 'total_pages' => absint($status['total_pages']),
914 - 'processed_pages' => absint($status['processed_pages']),
915 - 'failed_pages' => absint($status['failed_pages'] ?? 0),
916 - 'percentage' => ($status['total_pages'] > 0)
917 - ? round((absint($status['processed_pages']) / absint($status['total_pages'])) * 100)
918 - : 0,
919 - 'status' => sanitize_text_field($status['status']),
920 - 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
921 - 'failed_pages_list' => isset($status['failed_pages_list']) ? $status['failed_pages_list'] : array(),
922 - 'completion_summary' => isset($status['completion_summary']) ? $status['completion_summary'] : null
923 - );
924 -
925 - // Add error message if present
926 - if (isset($status['error']) && !empty($status['error'])) {
927 - $result['error'] = sanitize_text_field($status['error']);
928 - }
929 -
930 - return $result;
931 -}
932 -
933 -
934 -public function mxchat_handle_sitemap_submission() {
935 - // Check if the form was submitted and verify permissions
936 - if (!isset($_POST['submit_sitemap']) || !current_user_can('manage_options')) {
937 - wp_die(esc_html__('Unauthorized access', 'mxchat'));
938 - }
939 -
940 - // Verify nonce
941 - check_admin_referer('mxchat_submit_sitemap_action', 'mxchat_submit_sitemap_nonce');
942 -
943 - // Validate URL
944 - if (!isset($_POST['sitemap_url']) || empty($_POST['sitemap_url'])) {
945 - set_transient('mxchat_admin_notice_error',
946 - esc_html__('Please provide a valid URL.', 'mxchat'),
947 - 30
948 - );
949 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
950 - exit;
951 - }
952 -
953 - $submitted_url = esc_url_raw($_POST['sitemap_url']);
954 -
955 - // Convert Google Drive sharing URLs to direct download URLs
956 - if ( strpos($submitted_url, 'drive.google.com') !== false ) {
957 - $file_id = '';
958 - if ( preg_match('/[?&]id=([a-zA-Z0-9_-]+)/', $submitted_url, $m) ) {
959 - $file_id = $m[1];
960 - } elseif ( preg_match('#/file/d/([a-zA-Z0-9_-]+)#', $submitted_url, $m) ) {
961 - $file_id = $m[1];
962 - }
963 - if ( ! empty($file_id) ) {
964 - $submitted_url = 'https://drive.google.com/uc?export=download&id=' . $file_id;
965 - }
966 - }
967 -
968 - // Get bot_id from form submission
969 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
970 -
971 - // Get bot-specific options and validate API key
972 - $bot_options = $this->get_bot_options($bot_id);
973 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
974 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
975 -
976 - if (strpos($selected_model, 'voyage') === 0) {
977 - $api_key = $options['voyage_api_key'] ?? '';
978 - $provider_name = 'Voyage AI';
979 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
980 - $api_key = $options['gemini_api_key'] ?? '';
981 - $provider_name = 'Google Gemini';
982 - } else {
983 - $api_key = $options['api_key'] ?? '';
984 - $provider_name = 'OpenAI';
985 - }
986 -
987 - if (empty($api_key)) {
988 - $error_message = sprintf(
989 - esc_html__('%s API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
990 - $provider_name
991 - );
992 - set_transient('mxchat_admin_notice_error', $error_message, 30);
993 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
994 - exit;
995 - }
996 -
997 - // Fetch URL — use browser-like headers so servers with bot protection don't block us
998 - $response = wp_remote_get($submitted_url, array(
999 - 'timeout' => 30,
1000 - 'sslverify' => false,
1001 - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
1002 - 'headers' => array(
1003 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
1004 - 'Accept-Language' => 'en-US,en;q=0.9',
1005 - ),
1006 - ));
1007 -
1008 - if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1009 - $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response);
1010 - set_transient('mxchat_admin_notice_error',
1011 - sprintf(
1012 - esc_html__('Failed to fetch the URL: %s', 'mxchat'),
1013 - esc_html($error_message)
1014 - ),
1015 - 30
1016 - );
1017 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1018 - exit;
1019 - }
1020 -
1021 - $content_type = wp_remote_retrieve_header($response, 'content-type');
1022 - $body_content = wp_remote_retrieve_body($response);
1023 -
1024 - if (empty($body_content)) {
1025 - set_transient('mxchat_admin_notice_error',
1026 - esc_html__('Empty response received from URL.', 'mxchat'),
1027 - 30
1028 - );
1029 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1030 - exit;
1031 - }
1032 -
1033 - // Handle PDF URL
1034 - if ($this->mxchat_is_pdf_url($submitted_url, $response)) {
1035 - $result = $this->mxchat_handle_pdf_for_knowledge_base($submitted_url, $response, $bot_id);
1036 -
1037 - if ($result === 'queued') {
1038 - set_transient('mxchat_admin_notice_success',
1039 - esc_html__('PDF queued for processing. Processing will start automatically.', 'mxchat'),
1040 - 30
1041 - );
1042 - } else {
1043 - set_transient('mxchat_admin_notice_error',
1044 - esc_html__('Failed to queue PDF processing: ', 'mxchat') . esc_html($result),
1045 - 30
1046 - );
1047 - }
1048 -
1049 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1050 - exit;
1051 - }
1052 -
1053 - // Handle Sitemap XML
1054 - if (strpos($content_type, 'xml') !== false || strpos($body_content, '<urlset') !== false) {
1055 - libxml_use_internal_errors(true);
1056 - $xml = simplexml_load_string($body_content);
1057 - $xml_errors = libxml_get_errors();
1058 - libxml_clear_errors();
1059 -
1060 - if ($xml === false || !empty($xml_errors)) {
1061 - set_transient('mxchat_admin_notice_error',
1062 - esc_html__('Invalid sitemap XML. Please provide a valid sitemap.', 'mxchat'),
1063 - 30
1064 - );
1065 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1066 - exit;
1067 - }
1068 -
1069 - $result = $this->mxchat_handle_sitemap_for_knowledge_base($xml, $submitted_url, $bot_id);
1070 -
1071 - if ($result === 'queued') {
1072 - set_transient('mxchat_admin_notice_success',
1073 - esc_html__('Sitemap queued for processing. Processing will start automatically.', 'mxchat'),
1074 - 30
1075 - );
1076 - } else {
1077 - set_transient('mxchat_admin_notice_error',
1078 - esc_html__('Failed to queue sitemap processing. Please check the status below for details.', 'mxchat'),
1079 - 30
1080 - );
1081 - }
1082 -
1083 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1084 - exit;
1085 - }
1086 -
1087 - // Handle Regular URL (single page)
1088 - $page_content = $this->mxchat_extract_main_content($body_content);
1089 - $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
1090 -
1091 - //error_log('[MXCHAT-URL-DEBUG] URL: ' . $submitted_url);
1092 - //error_log('[MXCHAT-URL-DEBUG] Extracted page_content length: ' . strlen($page_content));
1093 - //error_log('[MXCHAT-URL-DEBUG] Sanitized content length: ' . strlen($sanitized_content));
1094 - //error_log('[MXCHAT-URL-DEBUG] Content preview: ' . substr($sanitized_content, 0, 500));
1095 -
1096 - if (empty($sanitized_content)) {
1097 - set_transient('mxchat_admin_notice_error',
1098 - esc_html__('No valid content found on the provided URL.', 'mxchat'),
1099 - 30
1100 - );
1101 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1102 - exit;
1103 - }
1104 -
1105 - // For single URLs, process immediately using submit_content_to_db
1106 - // This handles chunking automatically for large content
1107 - $db_result = MxChat_Utils::submit_content_to_db(
1108 - $sanitized_content,
1109 - $submitted_url,
1110 - $api_key,
1111 - null,
1112 - $bot_id,
1113 - 'url' // content_type
1114 - );
1115 -
1116 - if (is_wp_error($db_result)) {
1117 - $error_message = esc_html__('Failed to store content in database: ', 'mxchat') . esc_html($db_result->get_error_message());
1118 - set_transient('mxchat_admin_notice_error', $error_message, 30);
1119 - } else {
1120 - $success_message = esc_html__('URL content successfully submitted!', 'mxchat');
1121 - set_transient('mxchat_admin_notice_success', $success_message, 30);
1122 - }
1123 -
1124 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1125 - exit;
1126 -}
1127 -
1128 -
1129 -public function mxchat_get_single_url_status() {
1130 - $status = get_transient('mxchat_single_url_status');
1131 - if (!$status) {
1132 - return null;
1133 - }
1134 -
1135 - // Add human-readable time
1136 - if (isset($status['timestamp'])) {
1137 - $status['human_time'] = human_time_diff(strtotime($status['timestamp']), current_time('timestamp')) . ' ' . __('ago', 'mxchat');
1138 - }
1139 -
1140 - return $status;
1141 -}
1142 -
1143 -public function mxchat_handle_sitemap_for_knowledge_base($xml, $sitemap_url, $bot_id = 'default') {
1144 - if (!current_user_can('manage_options')) {
1145 - return false;
1146 - }
1147 -
1148 - try {
1149 - $sitemap_url = esc_url_raw($sitemap_url);
1150 -
1151 - if (!$xml || !is_object($xml)) {
1152 - throw new Exception(__('Invalid XML object provided', 'mxchat'));
1153 - }
1154 -
1155 - // Get bot-specific embedding API for validation
1156 - $bot_options = $this->get_bot_options($bot_id);
1157 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
1158 -
1159 - // Test the embedding API before processing
1160 - $test_phrase = "Test embedding generation for MxChat";
1161 - $test_result = $this->mxchat_generate_embedding($test_phrase, $bot_id);
1162 -
1163 - if (is_string($test_result)) {
1164 - throw new Exception(__('Embedding API validation failed: ', 'mxchat') . $test_result);
1165 - }
1166 -
1167 - if (!is_array($test_result)) {
1168 - throw new Exception(__('Embedding API returned unexpected result type. Please check your configuration.', 'mxchat'));
1169 - }
1170 -
1171 - // Extract URLs from sitemap
1172 - $urls = array();
1173 - foreach ($xml->url as $url_element) {
1174 - $url = esc_url_raw((string)$url_element->loc);
1175 - if ($url) {
1176 - $urls[] = array('url' => $url);
1177 - }
1178 - }
1179 -
1180 - $total_urls = count($urls);
1181 -
1182 - if ($total_urls < 1) {
1183 - throw new Exception(__('No valid URLs found in sitemap', 'mxchat'));
1184 - }
1185 -
1186 - // Create unique queue ID
1187 - $queue_id = 'sitemap_' . md5($sitemap_url . time());
1188 -
1189 - // Add URLs to queue
1190 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'url', $urls, $bot_id);
1191 -
1192 - if ($queued_count === 0) {
1193 - throw new Exception(__('Failed to add URLs to processing queue', 'mxchat'));
1194 - }
1195 -
1196 - // Store queue metadata
1197 - $this->mxchat_set_queue_meta($queue_id, 'source_url', $sitemap_url);
1198 - $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'sitemap');
1199 - $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_urls);
1200 - $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
1201 - $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
1202 -
1203 - // Store queue ID in transient for status tracking
1204 - set_transient('mxchat_active_queue_sitemap', $queue_id, DAY_IN_SECONDS);
1205 - set_transient('mxchat_last_sitemap_url', $sitemap_url, DAY_IN_SECONDS);
1206 -
1207 - return 'queued';
1208 -
1209 - } catch (Exception $e) {
1210 - $error_message = $e->getMessage();
1211 - //error_log(sprintf(esc_html__('Error preparing sitemap for processing: %s', 'mxchat'), esc_html($error_message)));
1212 -
1213 - return $error_message;
1214 - }
1215 -
1216 -}
1217 -
1218 -/**
1219 - * Remove shortcode tags but preserve the content inside them
1220 - * Example: [vc_column]Hello World[/vc_column] becomes "Hello World"
1221 - *
1222 - * @param string $content The content containing shortcodes
1223 - * @return string Content with shortcode tags removed but inner content preserved
1224 - */
1225 -private function strip_shortcode_tags_preserve_content($content) {
1226 - // Single-pass regex removes all shortcode brackets: [tag], [tag attr="val"], [/tag], [tag /]
1227 - // Content between tags is inherently preserved since only brackets are targeted
1228 - $result = preg_replace('/\[\/?\w[\w-]*[^\]]*\]/', '', $content);
1229 - return ($result !== null) ? $result : $content;
1230 -}
1231 -
1232 -public function mxchat_sanitize_content_for_api($content) {
1233 - //error_log('[MXCHAT-SANITIZE] Original content preview: ' . substr($content, 0, 500) . '...');
1234 -
1235 - // Remove shortcode tags but PRESERVE content inside them
1236 - $content = $this->strip_shortcode_tags_preserve_content($content);
1237 -
1238 - // Remove script, style tags, and HTML comments
1239 - $content = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $content);
1240 - $content = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $content);
1241 - $content = preg_replace('/<!--(.|\s)*?-->/', '', $content);
1242 -
1243 - // Remove all HTML tags and decode HTML entities
1244 - $content = wp_strip_all_tags($content);
1245 - $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5);
1246 -
1247 - // Normalize whitespace but preserve paragraph breaks
1248 - // First, normalize line endings to \n
1249 - $content = str_replace(["\r\n", "\r"], "\n", $content);
1250 - // Replace multiple spaces/tabs with single space, but preserve newlines
1251 - $content = preg_replace('/[ \t]+/', ' ', $content);
1252 - // Replace 3+ newlines with 2 newlines (max 2 blank lines)
1253 - $content = preg_replace('/\n{3,}/', "\n\n", $content);
1254 - // Trim each line
1255 - $lines = explode("\n", $content);
1256 - $lines = array_map('trim', $lines);
1257 - $content = implode("\n", $lines);
1258 - // Final trim
1259 - $content = trim($content);
1260 -
1261 - // Remove control characters (which can cause database issues) but preserve \n (0x0A) and \r (0x0D)
1262 - $content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $content);
1263 -
1264 - // Remove NULL bytes which can cause database errors
1265 - $content = str_replace("\0", "", $content);
1266 -
1267 - // Ensure valid UTF-8 encoding
1268 - $content = wp_check_invalid_utf8($content);
1269 -
1270 - // Remove any extremely long strings without spaces (often garbage)
1271 - $content = preg_replace('/\S{300,}/', ' ', $content);
1272 -
1273 - // Replace problematic characters that often cause database issues
1274 - $content = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $content); // Remove emoji and other high Unicode characters
1275 -
1276 - // Replace any remaining potentially problematic characters with spaces
1277 - // BUT preserve newlines by temporarily replacing them
1278 - $content = str_replace("\n", "NEWLINE_PLACEHOLDER", $content);
1279 - $content = preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\p{Sm}]/u', ' ', $content);
1280 - $content = str_replace("NEWLINE_PLACEHOLDER", "\n", $content);
1281 -
1282 - // Limit to reasonable length if needed
1283 - $max_length = 65000; // Just under MySQL TEXT field limit
1284 - if (strlen($content) > $max_length) {
1285 - $content = substr($content, 0, $max_length);
1286 - }
1287 -
1288 - //error_log('[MXCHAT-SANITIZE] Sanitized content preview: ' . substr($content, 0, 500) . '...');
1289 - return $content;
1290 -}
1291 -public function mxchat_extract_main_content($html) {
1292 - if (empty($html)) {
1293 - return '';
1294 - }
1295 - try {
1296 - $dom = new DOMDocument;
1297 - libxml_use_internal_errors(true); // Suppress HTML parsing errors
1298 - @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
1299 - $xpath = new DOMXPath($dom);
1300 -
1301 - // For debugging purposes
1302 - $debugEnabled = true; // Set to true to enable debugging output
1303 - $debug = function($message) use ($debugEnabled) {
1304 - if ($debugEnabled) {
1305 - //error_log('[MXCHAT-EXTRACT-DEBUG] ' . $message);
1306 - }
1307 - };
1308 -
1309 - // Direct targeting for Gerow theme posts
1310 - $post_text = $xpath->query('//div[contains(@class, "post-text")]');
1311 - if ($post_text && $post_text->length > 0) {
1312 - $debug("Found post-text directly");
1313 - $content = '';
1314 - foreach ($post_text as $node) {
1315 - $content .= $dom->saveHTML($node);
1316 - }
1317 - if (!empty($content)) {
1318 - $debug("Returning post-text content");
1319 - return $content;
1320 - }
1321 - }
1322 -
1323 - // Try to get the blog details content which contains the post-text
1324 - $blog_details = $xpath->query('//div[contains(@class, "blog-details-content")]');
1325 - if ($blog_details && $blog_details->length > 0) {
1326 - $debug("Found blog-details-content");
1327 - $content = '';
1328 - foreach ($blog_details as $node) {
1329 - $content .= $dom->saveHTML($node);
1330 - }
1331 - if (!empty($content)) {
1332 - $debug("Returning blog-details-content");
1333 - return $content;
1334 - }
1335 - }
1336 -
1337 - // Try to get the article which contains the blog details
1338 - $article = $xpath->query('//article[contains(@class, "blog-details-wrap")]');
1339 - if ($article && $article->length > 0) {
1340 - $debug("Found article with blog-details-wrap");
1341 - $content = '';
1342 - foreach ($article as $node) {
1343 - $content .= $dom->saveHTML($node);
1344 - }
1345 - if (!empty($content)) {
1346 - $debug("Returning article content");
1347 - return $content;
1348 - }
1349 - }
1350 -
1351 - // Try even broader with the blog-item-wrap
1352 - $blog_item = $xpath->query('//div[contains(@class, "blog-item-wrap")]');
1353 - if ($blog_item && $blog_item->length > 0) {
1354 - $debug("Found blog-item-wrap");
1355 - $content = '';
1356 - foreach ($blog_item as $node) {
1357 - $content .= $dom->saveHTML($node);
1358 - }
1359 - if (!empty($content)) {
1360 - $debug("Returning blog-item-wrap content");
1361 - return $content;
1362 - }
1363 - }
1364 -
1365 - // Specific Gerow theme path
1366 - $gerow_path = $xpath->query('//section[contains(@class, "blog-area")]//div[contains(@class, "post-text")]');
1367 - if ($gerow_path && $gerow_path->length > 0) {
1368 - $debug("Found Gerow theme path to post-text");
1369 - $content = '';
1370 - foreach ($gerow_path as $node) {
1371 - $content .= $dom->saveHTML($node);
1372 - }
1373 - if (!empty($content)) {
1374 - $debug("Returning Gerow post-text content");
1375 - return $content;
1376 - }
1377 - }
1378 -
1379 - // Generic blog post selectors
1380 - $selectors = [
1381 - // Blog post specific selectors
1382 - '//div[contains(@class, "post-text")]',
1383 - '//article[contains(@class, "blog-post-item")]//div[contains(@class, "post-text")]',
1384 - '//div[contains(@class, "blog-details-content")]',
1385 - '//article[contains(@class, "blog-details-wrap")]',
1386 - '//div[contains(@class, "entry-content")]',
1387 - '//div[contains(@class, "blog-content")]',
1388 - '//div[contains(@class, "blog-item-wrap")]',
1389 -
1390 - // More general content selectors
1391 - '//div[contains(@class, "page__content")]',
1392 - '//div[contains(@class, "elementor-widget-container")]',
1393 - '//div[contains(@class, "elementor-text-editor")]',
1394 - '//div[contains(@class, "elementor-widget-text-editor")]',
1395 - '//*[contains(@class, "entry-content")]',
1396 - '//*[contains(@class, "post-content")]',
1397 - '//*[contains(@class, "article-content")]',
1398 - '//*[@id="content"]',
1399 - '//*[@id="main-content"]',
1400 - '//section[contains(@class, "blog-area")]',
1401 - '//article',
1402 - '//main',
1403 - '//div[contains(@class, "content")]'
1404 - ];
1405 -
1406 - // First handle Elementor content - get only leaf widget containers to avoid duplicates
1407 - $debug("Checking for Elementor content");
1408 - // Get widget containers that are direct children of widgets (not nested inside other widget containers)
1409 - $elementor_widgets = $xpath->query('//div[contains(@class, "elementor-widget")]//div[contains(@class, "elementor-widget-container")]');
1410 - if ($elementor_widgets && $elementor_widgets->length > 0) {
1411 - $debug("Found Elementor widgets");
1412 - $seen_content = array(); // Track seen content to avoid duplicates
1413 - $combined_content = '';
1414 - foreach ($elementor_widgets as $widget) {
1415 - $widget_content = $dom->saveHTML($widget);
1416 - if (!empty($widget_content)) {
1417 - // Create a hash of the content to detect duplicates
1418 - $content_hash = md5($widget_content);
1419 - if (!isset($seen_content[$content_hash])) {
1420 - $seen_content[$content_hash] = true;
1421 - $combined_content .= $widget_content;
1422 - }
1423 - }
1424 - }
1425 - if (!empty($combined_content)) {
1426 - $debug("Returning Elementor content");
1427 - return $combined_content;
1428 - }
1429 - }
1430 -
1431 - // Try standard selectors one by one
1432 - foreach ($selectors as $selector) {
1433 - $debug("Trying selector: " . $selector);
1434 - $nodes = $xpath->query($selector);
1435 - if ($nodes && $nodes->length > 0) {
1436 - $debug("Found " . $nodes->length . " matches for selector: " . $selector);
1437 - // Only take the FIRST matching node to avoid duplicate content
1438 - // (pages often have nested or multiple containers with same class)
1439 - $content = $dom->saveHTML($nodes->item(0));
1440 - if (!empty($content)) {
1441 - $debug("Returning content from selector: " . $selector . " (first match only)");
1442 - return $content;
1443 - }
1444 - }
1445 - }
1446 -
1447 - // Manual regex fallback for post-text if DOM methods fail
1448 - $debug("Trying regex fallback");
1449 - if (preg_match('/<div class="post-text">(.*?)<\/div>\s*<\/div>\s*<\/div>/s', $html, $matches)) {
1450 - $debug("Found post-text via regex");
1451 - return '<div class="post-text">' . $matches[1] . '</div>';
1452 - }
1453 -
1454 - // Try to extract the blog section as a whole
1455 - $blog_section = $xpath->query('//section[contains(@class, "blog-area")]');
1456 - if ($blog_section && $blog_section->length > 0) {
1457 - $debug("Found blog-area section");
1458 - $content = '';
1459 - foreach ($blog_section as $node) {
1460 - $content .= $dom->saveHTML($node);
1461 - }
1462 - if (!empty($content)) {
1463 - $debug("Returning blog-area section content");
1464 - return $content;
1465 - }
1466 - }
1467 -
1468 - // Generic container selectors for non-CMS sites (like .asp pages)
1469 - $debug("Trying generic container selectors");
1470 - $generic_selectors = [
1471 - '//div[@id="main"]',
1472 - '//div[@id="wrapper"]',
1473 - '//div[@id="page"]',
1474 - '//div[@id="site-content"]',
1475 - '//div[contains(@class, "main-content")]',
1476 - '//div[contains(@class, "page-content")]',
1477 - '//div[contains(@class, "site-content")]',
1478 - ];
1479 -
1480 - foreach ($generic_selectors as $selector) {
1481 - $debug("Trying generic selector: " . $selector);
1482 - $nodes = $xpath->query($selector);
1483 - if ($nodes && $nodes->length > 0) {
1484 - $content = $dom->saveHTML($nodes->item(0));
1485 - if (!empty($content)) {
1486 - $debug("Returning content from generic selector: " . $selector);
1487 - return $content;
1488 - }
1489 - }
1490 - }
1491 -
1492 - // Paragraph-based content detection - find regions with substantial text
1493 - $debug("Trying paragraph-based content detection");
1494 - $paragraphs = $xpath->query('//p[string-length(normalize-space()) > 50]');
1495 - if ($paragraphs && $paragraphs->length >= 3) {
1496 - $debug("Found " . $paragraphs->length . " substantial paragraphs");
1497 - // Collect all substantial paragraphs and their content
1498 - $paragraph_content = '';
1499 - foreach ($paragraphs as $p) {
1500 - $paragraph_content .= $dom->saveHTML($p) . "\n";
1501 - }
1502 - if (!empty($paragraph_content)) {
1503 - $debug("Returning paragraph-based content");
1504 - return $paragraph_content;
1505 - }
1506 - }
1507 -
1508 - // Improved body fallback - strip nav/header/footer elements first
1509 - $debug("Using improved body fallback");
1510 - $body = $dom->getElementsByTagName('body');
1511 - if ($body->length > 0) {
1512 - // Clone the body to avoid modifying the original DOM
1513 - $body_clone = $body->item(0)->cloneNode(true);
1514 -
1515 - // Remove common non-content elements by tag name
1516 - $remove_tags = ['nav', 'header', 'footer', 'aside', 'script', 'style', 'noscript'];
1517 - foreach ($remove_tags as $tag) {
1518 - $elements = $body_clone->getElementsByTagName($tag);
1519 - // Iterate backwards to safely remove elements
1520 - for ($i = $elements->length - 1; $i >= 0; $i--) {
1521 - $el = $elements->item($i);
1522 - if ($el && $el->parentNode) {
1523 - $el->parentNode->removeChild($el);
1524 - }
1525 - }
1526 - }
1527 -
1528 - // Remove elements with common non-content class names using XPath on the cloned body
1529 - $temp_dom = new DOMDocument();
1530 - @$temp_dom->appendChild($temp_dom->importNode($body_clone, true));
1531 - $temp_xpath = new DOMXPath($temp_dom);
1532 -
1533 - $remove_class_patterns = [
1534 - '//*[contains(@class, "nav")]',
1535 - '//*[contains(@class, "menu")]',
1536 - '//*[contains(@class, "sidebar")]',
1537 - '//*[contains(@class, "footer")]',
1538 - '//*[contains(@class, "header")]',
1539 - '//*[contains(@id, "nav")]',
1540 - '//*[contains(@id, "menu")]',
1541 - '//*[contains(@id, "sidebar")]',
1542 - '//*[contains(@id, "footer")]',
1543 - '//*[contains(@id, "header")]',
1544 - ];
1545 -
1546 - foreach ($remove_class_patterns as $pattern) {
1547 - $elements = $temp_xpath->query($pattern);
1548 - if ($elements) {
1549 - for ($i = $elements->length - 1; $i >= 0; $i--) {
1550 - $el = $elements->item($i);
1551 - if ($el && $el->parentNode) {
1552 - $el->parentNode->removeChild($el);
1553 - }
1554 - }
1555 - }
1556 - }
1557 -
1558 - $cleaned_content = $temp_dom->saveHTML();
1559 - if (!empty($cleaned_content)) {
1560 - $debug("Returning cleaned body content");
1561 - return $cleaned_content;
1562 - }
1563 - }
1564 -
1565 - // Last resort: return the original HTML
1566 - $debug("Returning original HTML");
1567 - return $html;
1568 - } catch (Exception $e) {
1569 - //error_log('[MXCHAT-ERROR] Content extraction failed: ' . $e->getMessage());
1570 - return $html; // Return original HTML if parsing fails
1571 - } finally {
1572 - libxml_clear_errors();
1573 - }
1574 -}
1575 -public function mxchat_get_sitemap_processing_status($sitemap_url) {
1576 - $sitemap_url = esc_url_raw($sitemap_url);
1577 - $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
1578 - $status = get_transient($status_key);
1579 -
1580 - if (!$status || !is_array($status)) {
1581 - return false;
1582 - }
1583 -
1584 - // Auto-complete check: if all URLs are processed but status isn't complete
1585 - if (isset($status['processed_urls']) && isset($status['total_urls']) &&
1586 - $status['processed_urls'] >= $status['total_urls'] &&
1587 - isset($status['status']) && $status['status'] !== 'complete' &&
1588 - $status['status'] !== 'error') {
1589 -
1590 - // Mark as complete
1591 - $status['status'] = 'complete';
1592 - $status['processed_urls'] = $status['total_urls']; // Ensure exact match
1593 -
1594 - // Update the transient with the corrected status
1595 - set_transient($status_key, $status, DAY_IN_SECONDS);
1596 - }
1597 -
1598 - return array(
1599 - 'total_urls' => absint($status['total_urls']),
1600 - 'processed_urls' => absint($status['processed_urls']),
1601 - 'failed_urls' => absint($status['failed_urls'] ?? 0),
1602 - 'percentage' => ($status['total_urls'] > 0)
1603 - ? round((absint($status['processed_urls']) / absint($status['total_urls'])) * 100)
1604 - : 0,
1605 - 'status' => sanitize_text_field($status['status']),
1606 - 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
1607 - 'error' => isset($status['error']) ? sanitize_text_field($status['error']) : '',
1608 - 'last_error' => isset($status['last_error']) ? sanitize_text_field($status['last_error']) : '',
1609 - 'failed_urls_list' => isset($status['failed_urls_list']) ? $status['failed_urls_list'] : array()
1610 - );
1611 -}
1612 -
1613 -public function mxchat_ajax_get_status_updates() {
1614 - try {
1615 - // Verify the request
1616 - check_ajax_referer('mxchat_status_nonce', 'nonce');
1617 -
1618 - // Get active queue IDs
1619 - $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
1620 - $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
1621 -
1622 - $sitemap_status = false;
1623 - $pdf_status = false;
1624 -
1625 - // Get sitemap queue status
1626 - if ($sitemap_queue_id) {
1627 - $sitemap_status = $this->mxchat_get_queue_status_data($sitemap_queue_id, 'sitemap');
1628 - }
1629 -
1630 - // Get PDF queue status
1631 - if ($pdf_queue_id) {
1632 - $pdf_status = $this->mxchat_get_queue_status_data($pdf_queue_id, 'pdf');
1633 - }
1634 -
1635 - $is_active_processing =
1636 - ($sitemap_status && $sitemap_status['status'] === 'processing') ||
1637 - ($pdf_status && $pdf_status['status'] === 'processing');
1638 -
1639 - // Return JSON response with the status data
1640 - wp_send_json(array(
1641 - 'pdf_status' => $pdf_status,
1642 - 'sitemap_status' => $sitemap_status,
1643 - 'is_processing' => $is_active_processing,
1644 - 'sitemap_queue_id' => $sitemap_queue_id,
1645 - 'pdf_queue_id' => $pdf_queue_id
1646 - ));
1647 -
1648 - } catch (Exception $e) {
1649 - //error_log('MxChat Status Update Error: ' . $e->getMessage());
1650 -
1651 - wp_send_json_error(array(
1652 - 'message' => 'Error getting status updates: ' . $e->getMessage(),
1653 - 'status' => 'error'
1654 - ));
1655 - }
1656 -}
1657 -
1658 -/**
1659 - * Helper function to get queue status data
1660 - */
1661 -private function mxchat_get_queue_status_data($queue_id, $type = 'sitemap') {
1662 - global $wpdb;
1663 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
1664 -
1665 - // Get counts by status
1666 - $counts = $wpdb->get_results($wpdb->prepare(
1667 - "SELECT status, COUNT(*) as count
1668 - FROM $table_name
1669 - WHERE queue_id = %s
1670 - GROUP BY status",
1671 - $queue_id
1672 - ), OBJECT_K);
1673 -
1674 - $total = 0;
1675 - $completed = 0;
1676 - $failed = 0;
1677 - $processing = 0;
1678 - $pending = 0;
1679 -
1680 - foreach ($counts as $status => $data) {
1681 - $count = absint($data->count);
1682 - $total += $count;
1683 -
1684 - switch ($status) {
1685 - case 'completed':
1686 - $completed = $count;
1687 - break;
1688 - case 'failed':
1689 - $failed = $count;
1690 - break;
1691 - case 'processing':
1692 - $processing = $count;
1693 - break;
1694 - case 'pending':
1695 - $pending = $count;
1696 - break;
1697 - }
1698 - }
1699 -
1700 - if ($total === 0) {
1701 - return false;
1702 - }
1703 -
1704 - // Calculate percentage
1705 - $percentage = round((($completed + $failed) / $total) * 100);
1706 -
1707 - // Get failed items details (limit to 50)
1708 - $failed_items = array();
1709 - if ($failed > 0) {
1710 - $failed_results = $wpdb->get_results($wpdb->prepare(
1711 - "SELECT item_type, item_data, error_message, attempts, completed_at
1712 - FROM $table_name
1713 - WHERE queue_id = %s
1714 - AND status = 'failed'
1715 - AND attempts >= max_attempts
1716 - ORDER BY id DESC
1717 - LIMIT 50",
1718 - $queue_id
1719 - ));
1720 -
1721 - foreach ($failed_results as $item) {
1722 - $data = json_decode($item->item_data, true);
1723 - $url = $type === 'pdf' ? ($data['pdf_url'] ?? '') : ($data['url'] ?? '');
1724 -
1725 - $failed_items[] = array(
1726 - 'url' => $url,
1727 - 'page' => $type === 'pdf' ? ($data['page_number'] ?? 0) : 0,
1728 - 'error' => $item->error_message,
1729 - 'retries' => $item->attempts,
1730 - 'time' => strtotime($item->completed_at)
1731 - );
1732 - }
1733 - }
1734 -
1735 - // Get queue metadata
1736 - $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
1737 -
1738 - // Determine if queue is complete
1739 - $is_complete = ($pending === 0 && $processing === 0);
1740 -
1741 - // Get last update time
1742 - $last_update = $wpdb->get_var($wpdb->prepare(
1743 - "SELECT MAX(UNIX_TIMESTAMP(COALESCE(completed_at, started_at, created_at)))
1744 - FROM $table_name
1745 - WHERE queue_id = %s",
1746 - $queue_id
1747 - ));
1748 -
1749 - $last_update_text = $last_update ? human_time_diff($last_update, time()) . ' ' . __('ago', 'mxchat') : __('Just now', 'mxchat');
1750 -
1751 - // Format based on type
1752 - if ($type === 'pdf') {
1753 - return array(
1754 - 'total_pages' => $total,
1755 - 'processed_pages' => $completed + $failed,
1756 - 'failed_pages' => $failed,
1757 - 'percentage' => $percentage,
1758 - 'status' => $is_complete ? 'complete' : 'processing',
1759 - 'last_update' => $last_update_text,
1760 - 'failed_pages_list' => $failed_items,
1761 - 'pdf_url' => $source_url,
1762 - 'queue_id' => $queue_id
1763 - );
1764 - } else {
1765 - return array(
1766 - 'total_urls' => $total,
1767 - 'processed_urls' => $completed + $failed,
1768 - 'failed_urls' => $failed,
1769 - 'percentage' => $percentage,
1770 - 'status' => $is_complete ? 'complete' : 'processing',
1771 - 'last_update' => $last_update_text,
1772 - 'failed_urls_list' => $failed_items,
1773 - 'sitemap_url' => $source_url,
1774 - 'queue_id' => $queue_id
1775 - );
1776 - }
1777 -}
1778 -
1779 -/**
1780 - * Public method to get processing status for both sitemap and PDF queues
1781 - * Used by admin pages to display processing status
1782 - *
1783 - * @return array Array with 'sitemap_status', 'pdf_status', and 'is_processing' keys
1784 - */
1785 -public function mxchat_get_processing_statuses() {
1786 - $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
1787 - $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
1788 -
1789 - $sitemap_status = false;
1790 - $pdf_status = false;
1791 -
1792 - if ($sitemap_queue_id) {
1793 - $sitemap_status = $this->mxchat_get_queue_status_data($sitemap_queue_id, 'sitemap');
1794 - }
1795 -
1796 - if ($pdf_queue_id) {
1797 - $pdf_status = $this->mxchat_get_queue_status_data($pdf_queue_id, 'pdf');
1798 - }
1799 -
1800 - $is_processing =
1801 - ($sitemap_status && $sitemap_status['status'] === 'processing') ||
1802 - ($pdf_status && $pdf_status['status'] === 'processing');
1803 -
1804 - return array(
1805 - 'sitemap_status' => $sitemap_status,
1806 - 'pdf_status' => $pdf_status,
1807 - 'is_processing' => $is_processing
1808 - );
1809 -}
1810 -
1811 -/**
1812 - * AJAX handler to get recent knowledge entries for real-time table updates
1813 - * UPDATED: Now supports both WordPress DB and Pinecone data sources
1814 - */
1815 -public function ajax_mxchat_get_recent_entries() {
1816 - check_ajax_referer('mxchat_entries_nonce', 'nonce');
1817 -
1818 - if (!current_user_can('manage_options')) {
1819 - wp_send_json_error(array('message' => 'Unauthorized'));
1820 - return;
1821 - }
1822 -
1823 - global $wpdb;
1824 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1825 -
1826 - // Get parameters
1827 - $last_id = isset($_POST['last_id']) ? absint($_POST['last_id']) : 0;
1828 - $limit = isset($_POST['limit']) ? min(absint($_POST['limit']), 50) : 10;
1829 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
1830 -
1831 - // Check if Pinecone is enabled for this bot
1832 - $pinecone_manager = $this->mxchat_get_pinecone_manager();
1833 - $pinecone_options = $pinecone_manager ? $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id) : array();
1834 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1835 - $has_pinecone_api = !empty($pinecone_options['mxchat_pinecone_api_key']);
1836 -
1837 - if ($use_pinecone && $has_pinecone_api) {
1838 - // PINECONE DATA SOURCE - Get grouped entry count (not raw vector count)
1839 - // Use mxchat_fetch_pinecone_records which returns total_unique_entries
1840 - $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, '', 1, 10, $bot_id, '');
1841 - $total_count = $records['total'] ?? 0;
1842 -
1843 - // For Pinecone, we don't return individual entries during polling
1844 - // (entries are already displayed on page load via mxchat_fetch_pinecone_records)
1845 - // We just return the updated count
1846 - wp_send_json_success(array(
1847 - 'entries' => array(),
1848 - 'total_count' => absint($total_count),
1849 - 'max_id' => $last_id,
1850 - 'data_source' => 'pinecone'
1851 - ));
1852 - return;
1853 - }
1854 -
1855 - // WORDPRESS DB DATA SOURCE
1856 - // Build query to get entries newer than last_id
1857 - $where_clauses = array('1=1');
1858 - $where_values = array();
1859 -
1860 - if ($last_id > 0) {
1861 - $where_clauses[] = 'id > %d';
1862 - $where_values[] = $last_id;
1863 - }
1864 -
1865 - // Note: WordPress DB table doesn't have bot_id column
1866 - // Multi-bot filtering is handled via Pinecone namespaces
1867 -
1868 - $where_sql = implode(' AND ', $where_clauses);
1869 -
1870 - // Get recent entries
1871 - $query = "SELECT id, article_content, source_url, timestamp
1872 - FROM $table_name
1873 - WHERE $where_sql
1874 - ORDER BY id DESC
1875 - LIMIT %d";
1876 -
1877 - $where_values[] = $limit;
1878 -
1879 - $entries = $wpdb->get_results($wpdb->prepare($query, $where_values));
1880 -
1881 - // Get total count of GROUPED entries (by source_url) - matches pagination display
1882 - // Count unique source_urls (excluding mxchat:// internal refs) + count of ungrouped rows
1883 - $total_count = $wpdb->get_var(
1884 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '' AND source_url NOT LIKE 'mxchat://%') +
1885 - (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')"
1886 - );
1887 -
1888 - // Format entries for response
1889 - $formatted_entries = array();
1890 - $preview_length = 150;
1891 - foreach ($entries as $entry) {
1892 - // Parse chunk metadata using the proper chunker method (same as initial page load)
1893 - if (class_exists('MxChat_Chunker')) {
1894 - $chunk_meta = MxChat_Chunker::parse_stored_chunk($entry->article_content);
1895 - $display_content = $chunk_meta['text'];
1896 - $chunk_metadata = $chunk_meta['metadata'];
1897 - } else {
1898 - $display_content = $entry->article_content;
1899 - $chunk_metadata = array();
1900 - }
1901 -
1902 - $content_preview = mb_strlen($display_content) > $preview_length
1903 - ? mb_substr($display_content, 0, $preview_length) . '...'
1904 - : $display_content;
1905 -
1906 - $formatted_entries[] = array(
1907 - 'id' => $entry->id,
1908 - 'preview' => esc_html($content_preview),
1909 - 'full_content' => wp_kses_post(wpautop($display_content)),
1910 - 'content_length' => mb_strlen($display_content),
1911 - 'preview_length' => $preview_length,
1912 - 'source_url' => $entry->source_url,
1913 - 'has_link' => !empty($entry->source_url) && strpos($entry->source_url, 'mxchat://') !== 0,
1914 - 'chunk_metadata' => $chunk_metadata,
1915 - 'bot_id' => $entry->bot_id ?? 'default',
1916 - 'edit_nonce' => wp_create_nonce('mxchat_edit_entry_nonce'),
1917 - 'delete_nonce' => wp_create_nonce('mxchat_delete_prompt_nonce')
1918 - );
1919 - }
1920 -
1921 - wp_send_json_success(array(
1922 - 'entries' => $formatted_entries,
1923 - 'total_count' => absint($total_count),
1924 - 'max_id' => !empty($entries) ? $entries[0]->id : $last_id,
1925 - 'data_source' => 'wordpress'
1926 - ));
1927 -}
1928 -
1929 -/**
1930 - * Get Pinecone total count from stats API
1931 - * Helper function for ajax_mxchat_get_recent_entries
1932 - */
1933 -private function mxchat_get_pinecone_count_from_stats($pinecone_options) {
1934 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1935 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1936 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
1937 -
1938 - if (empty($api_key) || empty($host)) {
1939 - return 0;
1940 - }
1941 -
1942 - try {
1943 - $stats_url = "https://{$host}/describe_index_stats";
1944 -
1945 - $response = wp_remote_post($stats_url, array(
1946 - 'headers' => array(
1947 - 'Api-Key' => $api_key,
1948 - 'Content-Type' => 'application/json'
1949 - ),
1950 - 'body' => '{}',
1951 - 'timeout' => 10
1952 - ));
1953 -
1954 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
1955 - $body = wp_remote_retrieve_body($response);
1956 - $stats_data = json_decode($body, true);
1957 -
1958 - // If namespace is specified, get count from that specific namespace
1959 - if (!empty($namespace) && isset($stats_data['namespaces'][$namespace]['vectorCount'])) {
1960 - return intval($stats_data['namespaces'][$namespace]['vectorCount']);
1961 - }
1962 -
1963 - // If no namespace specified or namespace not found in response, use total
1964 - return intval($stats_data['totalVectorCount'] ?? 0);
1965 - }
1966 -
1967 - return 0;
1968 -
1969 - } catch (Exception $e) {
1970 - return 0;
1971 - }
1972 -}
1973 -
1974 -/**
1975 - * AJAX handler to refresh Pinecone entries table via AJAX
1976 - * Returns the table HTML for updating the UI without a full page reload
1977 - */
1978 -public function ajax_mxchat_refresh_pinecone_entries() {
1979 - check_ajax_referer('mxchat_entries_nonce', 'nonce');
1980 -
1981 - if (!current_user_can('manage_options')) {
1982 - wp_send_json_error(array('message' => 'Unauthorized'));
1983 - return;
1984 - }
1985 -
1986 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
1987 - $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
1988 - $per_page = 25;
1989 - $search_query = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
1990 - $content_type_filter = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : '';
1991 -
1992 - // Get Pinecone manager and options
1993 - $pinecone_manager = $this->mxchat_get_pinecone_manager();
1994 - if (!$pinecone_manager) {
1995 - wp_send_json_error(array('message' => 'Pinecone manager not available'));
1996 - return;
1997 - }
1998 -
1999 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
2000 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2001 - $pinecone_api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2002 -
2003 - if (!$use_pinecone || empty($pinecone_api_key)) {
2004 - wp_send_json_error(array('message' => 'Pinecone not configured'));
2005 - return;
2006 - }
2007 -
2008 - // Fetch records from Pinecone
2009 - $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, $search_query, $page, $per_page, $bot_id, $content_type_filter);
2010 - $prompts = $records['data'] ?? array();
2011 - $total_records = $records['total'] ?? 0;
2012 -
2013 - // Preprocess Pinecone records — set chunk_metadata and display_content
2014 - // (matches admin-knowledge-page.php preprocessing)
2015 - foreach ($prompts as $prompt) {
2016 - if (isset($prompt->chunk_index) && $prompt->chunk_index !== null) {
2017 - $prompt->chunk_metadata = array(
2018 - 'chunk_index' => intval($prompt->chunk_index),
2019 - 'total_chunks' => isset($prompt->total_chunks) ? intval($prompt->total_chunks) : null,
2020 - 'is_chunked' => isset($prompt->is_chunked) ? (bool) $prompt->is_chunked : true,
2021 - 'source_url' => $prompt->source_url ?? ''
2022 - );
2023 - $prompt->display_content = $prompt->article_content;
2024 - } else {
2025 - $prompt->chunk_metadata = array();
2026 - $prompt->display_content = $prompt->article_content ?? '';
2027 - }
2028 - }
2029 -
2030 - // Group prompts by source_url
2031 - $grouped_prompts = array();
2032 - foreach ($prompts as $prompt) {
2033 - $source_url = '';
2034 - if (!empty($prompt->chunk_metadata['source_url'])) {
2035 - $source_url = $prompt->chunk_metadata['source_url'];
2036 - } elseif (!empty($prompt->source_url)) {
2037 - $source_url = $prompt->source_url;
2038 - }
2039 -
2040 - if (!empty($source_url)) {
2041 - if (!isset($grouped_prompts[$source_url])) {
2042 - $grouped_prompts[$source_url] = array();
2043 - }
2044 - $grouped_prompts[$source_url][] = $prompt;
2045 - } else {
2046 - $grouped_prompts['_ungrouped_' . $prompt->id] = array($prompt);
2047 - }
2048 - }
2049 -
2050 - // Sort each group by chunk_index
2051 - foreach ($grouped_prompts as $source_url => &$group) {
2052 - usort($group, function($a, $b) {
2053 - $index_a = isset($a->chunk_metadata['chunk_index']) ? intval($a->chunk_metadata['chunk_index']) : 0;
2054 - $index_b = isset($b->chunk_metadata['chunk_index']) ? intval($b->chunk_metadata['chunk_index']) : 0;
2055 - return $index_a - $index_b;
2056 - });
2057 - }
2058 - unset($group);
2059 -
2060 - // Build HTML for the table rows - must match admin-knowledge-page.php structure exactly
2061 - ob_start();
2062 - $display_index = 0;
2063 - $current_page = $page;
2064 - $data_source = 'pinecone';
2065 - $current_bot_id = $bot_id;
2066 - $preview_length = 150;
2067 -
2068 - if (empty($grouped_prompts)) {
2069 - echo '<tr><td colspan="4" style="padding: 40px; text-align: center; color: var(--mxch-text-muted);">';
2070 - esc_html_e('No knowledge entries found in Pinecone.', 'mxchat');
2071 - echo '</td></tr>';
2072 - } else {
2073 - foreach ($grouped_prompts as $source_url => $group) {
2074 - $chunk_count = count($group);
2075 - $first_prompt = $group[0];
2076 - $display_index++;
2077 -
2078 - if ($chunk_count > 1) {
2079 - // Multiple chunks - show grouped row with expand button
2080 - $group_id = 'group-' . md5($source_url);
2081 - ?>
2082 - <tr id="prompt-<?php echo esc_attr($first_prompt->id); ?>"
2083 - class="mxchat-chunk-group-header"
2084 - data-source="<?php echo esc_attr($data_source); ?>"
2085 - data-group-id="<?php echo esc_attr($group_id); ?>"
2086 - style="border-bottom: 1px solid var(--mxch-card-border); background: rgba(33, 150, 243, 0.02);">
2087 - <td style="padding: 12px 16px; text-align: center;">
2088 - <input type="checkbox"
2089 - class="mxchat-entry-checkbox"
2090 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2091 - data-source="<?php echo esc_attr($data_source); ?>"
2092 - data-source-url="<?php echo esc_attr($source_url); ?>"
2093 - data-is-group="true"
2094 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>">
2095 - </td>
2096 - <td style="padding: 12px 16px; font-size: 13px;">
2097 - <?php echo esc_html($display_index + (($current_page - 1) * $per_page)); ?>
2098 - </td>
2099 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2100 - <div class="mxchat-chunk-group-info">
2101 - <button type="button" class="mxchat-chunk-toggle" data-group-id="<?php echo esc_attr($group_id); ?>">
2102 - <span class="dashicons dashicons-arrow-right-alt2"></span>
2103 - </button>
2104 - <span class="mxchat-chunk-badge"><?php echo esc_html($chunk_count); ?> <?php esc_html_e('chunks', 'mxchat'); ?></span>
2105 - <span class="mxchat-chunk-preview">
2106 - <?php
2107 - $parent_content = isset($first_prompt->display_content) ? $first_prompt->display_content : (isset($first_prompt->article_content) ? $first_prompt->article_content : '');
2108 - $content_preview = mb_substr($parent_content, 0, 100);
2109 - echo esc_html($content_preview . '...');
2110 - ?>
2111 - </span>
2112 - </div>
2113 - </td>
2114 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2115 - <?php if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0 && strpos($source_url, '_ungrouped_') !== 0) : ?>
2116 - <a href="<?php echo esc_url($source_url); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2117 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2118 - <?php esc_html_e('View Source', 'mxchat'); ?>
2119 - </a>
2120 - <?php else : ?>
2121 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
2122 - <?php endif; ?>
2123 - </td>
2124 - <td class="mxchat-actions-cell" style="padding: 12px 16px; white-space: nowrap;">
2125 - <?php if ($data_source !== 'pinecone') : ?>
2126 - <button type="button"
2127 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2128 - data-source-url="<?php echo esc_attr($source_url); ?>"
2129 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2130 - data-data-source="<?php echo esc_attr($data_source); ?>"
2131 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2132 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2133 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2134 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2135 - </button>
2136 - <?php endif; ?>
2137 - <button type="button"
2138 - class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
2139 - data-source-url="<?php echo esc_attr($source_url); ?>"
2140 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
2141 - data-data-source="<?php echo esc_attr($data_source); ?>"
2142 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2143 - data-nonce="<?php echo wp_create_nonce('mxchat_delete_chunks_nonce'); ?>"
2144 - style="color: var(--mxch-error);"
2145 - title="<?php esc_attr_e('Delete all chunks', 'mxchat'); ?>">
2146 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2147 - </button>
2148 - </td>
2149 - </tr>
2150 - <?php
2151 - // Render hidden chunk rows
2152 - foreach ($group as $chunk_index => $chunk) {
2153 - $meta_chunk_index = isset($chunk->chunk_metadata['chunk_index']) ? intval($chunk->chunk_metadata['chunk_index']) : $chunk_index;
2154 - $meta_total_chunks = isset($chunk->chunk_metadata['total_chunks']) ? intval($chunk->chunk_metadata['total_chunks']) : $chunk_count;
2155 - $content = isset($chunk->display_content) ? $chunk->display_content : (isset($chunk->article_content) ? $chunk->article_content : '');
2156 - $content_preview = mb_strlen($content) > $preview_length
2157 - ? mb_substr($content, 0, $preview_length) . '...'
2158 - : $content;
2159 - ?>
2160 - <tr id="prompt-<?php echo esc_attr($chunk->id); ?>"
2161 - class="mxchat-chunk-row <?php echo esc_attr($group_id); ?>"
2162 - data-source="<?php echo esc_attr($data_source); ?>"
2163 - style="display: none; background: #f8f9fa; border-bottom: 1px solid var(--mxch-card-border);">
2164 - <td style="padding: 12px 16px; text-align: center;">
2165 - <!-- Checkbox column placeholder for chunks (managed by group) -->
2166 - </td>
2167 - <td style="padding: 12px 16px 12px 30px; font-size: 13px;">
2168 - <!-- Hidden ID column for chunks -->
2169 - </td>
2170 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2171 - <div class="mxchat-accordion-wrapper">
2172 - <div class="mxchat-content-preview">
2173 - <span class="mxchat-chunk-indicator" style="margin-right: 10px; color: var(--mxch-text-secondary); font-size: 12px;">
2174 - <?php printf(esc_html__('Chunk %d of %d', 'mxchat'), $meta_chunk_index + 1, $meta_total_chunks); ?>
2175 - </span>
2176 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2177 - <?php if (mb_strlen($content) > $preview_length) : ?>
2178 - <button class="mxchat-expand-toggle" type="button">
2179 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2180 - </button>
2181 - <?php endif; ?>
2182 - </div>
2183 - <div class="mxchat-content-full" style="display: none;">
2184 - <div class="content-view">
2185 - <?php
2186 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2187 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2188 - echo wp_kses_post(wpautop($content));
2189 - echo '</div>';
2190 - } else {
2191 - echo wp_kses_post(wpautop($content));
2192 - }
2193 - ?>
2194 - </div>
2195 - </div>
2196 - </div>
2197 - </td>
2198 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2199 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Same as parent', 'mxchat'); ?></span>
2200 - </td>
2201 - <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2202 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Managed by group', 'mxchat'); ?></span>
2203 - </td>
2204 - </tr>
2205 - <?php
2206 - }
2207 - } else {
2208 - // Single entry - display normally with accordion
2209 - $prompt = $first_prompt;
2210 - $content = isset($prompt->display_content) ? $prompt->display_content : (isset($prompt->article_content) ? $prompt->article_content : '');
2211 - $content_preview = mb_strlen($content) > $preview_length
2212 - ? mb_substr($content, 0, $preview_length) . '...'
2213 - : $content;
2214 - ?>
2215 - <tr id="prompt-<?php echo esc_attr($prompt->id); ?>"
2216 - data-source="<?php echo esc_attr($data_source); ?>"
2217 - style="border-bottom: 1px solid var(--mxch-card-border); background: rgba(33, 150, 243, 0.02);">
2218 - <td style="padding: 12px 16px; text-align: center;">
2219 - <input type="checkbox"
2220 - class="mxchat-entry-checkbox"
2221 - data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2222 - data-source="<?php echo esc_attr($data_source); ?>"
2223 - data-source-url="<?php echo esc_attr($prompt->source_url ?? ''); ?>"
2224 - data-is-group="false"
2225 - data-chunk-count="1">
2226 - </td>
2227 - <td style="padding: 12px 16px; font-size: 13px;">
2228 - <?php echo esc_html($display_index + (($current_page - 1) * $per_page)); ?>
2229 - </td>
2230 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2231 - <div class="mxchat-accordion-wrapper">
2232 - <div class="mxchat-content-preview">
2233 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2234 - <?php if (mb_strlen($content) > $preview_length) : ?>
2235 - <button class="mxchat-expand-toggle" type="button">
2236 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2237 - </button>
2238 - <?php endif; ?>
2239 - </div>
2240 - <div class="mxchat-content-full" style="display: none;">
2241 - <div class="content-view">
2242 - <?php
2243 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2244 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2245 - echo wp_kses_post(wpautop($content));
2246 - echo '</div>';
2247 - } else {
2248 - echo wp_kses_post(wpautop($content));
2249 - }
2250 - ?>
2251 - </div>
2252 - </div>
2253 - </div>
2254 - </td>
2255 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2256 - <?php
2257 - $actual_source = $source_url;
2258 - if (strpos($source_url, '_ungrouped_') === 0) {
2259 - $actual_source = $prompt->source_url ?? '';
2260 - }
2261 - if (!empty($actual_source) && strpos($actual_source, 'mxchat://') !== 0) : ?>
2262 - <a href="<?php echo esc_url($actual_source); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2263 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2264 - <?php esc_html_e('View', 'mxchat'); ?>
2265 - </a>
2266 - <?php else : ?>
2267 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
2268 - <?php endif; ?>
2269 - </td>
2270 - <td style="padding: 12px 16px;">
2271 - <button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-ajax" data-vector-id="<?php echo esc_attr($prompt->id); ?>" data-bot-id="<?php echo esc_attr($current_bot_id); ?>" data-nonce="<?php echo wp_create_nonce('mxchat_delete_pinecone_prompt_nonce'); ?>" style="color: var(--mxch-error);">
2272 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2273 - </button>
2274 - </td>
2275 - </tr>
2276 - <?php
2277 - }
2278 - }
2279 - }
2280 - $html = ob_get_clean();
2281 -
2282 - // Generate pagination HTML for Pinecone
2283 - $total_pages = ceil($total_records / $per_page);
2284 - $pagination_html = '';
2285 - if ($total_pages > 1) {
2286 - $pagination_html = '<div class="mxchat-ajax-pagination" data-current-page="' . esc_attr($page) . '" data-total-pages="' . esc_attr($total_pages) . '" data-search="' . esc_attr($search_query) . '" data-content-type="' . esc_attr($content_type_filter) . '">';
2287 -
2288 - // Previous button
2289 - if ($page > 1) {
2290 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page - 1) . '">' . esc_html__('&laquo; Previous', 'mxchat') . '</a> ';
2291 - }
2292 -
2293 - // Page numbers
2294 - $start_page = max(1, $page - 2);
2295 - $end_page = min($total_pages, $page + 2);
2296 -
2297 - if ($start_page > 1) {
2298 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="1">1</a> ';
2299 - if ($start_page > 2) {
2300 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2301 - }
2302 - }
2303 -
2304 - for ($i = $start_page; $i <= $end_page; $i++) {
2305 - if ($i == $page) {
2306 - $pagination_html .= '<span class="mxchat-page-current">' . $i . '</span> ';
2307 - } else {
2308 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $i . '">' . $i . '</a> ';
2309 - }
2310 - }
2311 -
2312 - if ($end_page < $total_pages) {
2313 - if ($end_page < $total_pages - 1) {
2314 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2315 - }
2316 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $total_pages . '">' . $total_pages . '</a> ';
2317 - }
2318 -
2319 - // Next button
2320 - if ($page < $total_pages) {
2321 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page + 1) . '">' . esc_html__('Next &raquo;', 'mxchat') . '</a>';
2322 - }
2323 -
2324 - $pagination_html .= '</div>';
2325 - }
2326 -
2327 - wp_send_json_success(array(
2328 - 'html' => $html,
2329 - 'pagination_html' => $pagination_html,
2330 - 'total_count' => $total_records,
2331 - 'total_pages' => $total_pages,
2332 - 'page' => $page,
2333 - 'per_page' => $per_page,
2334 - 'data_source' => 'pinecone'
2335 - ));
2336 -}
2337 -
2338 -/**
2339 - * AJAX handler for pagination - handles both WordPress DB and Pinecone data sources
2340 - * Returns paginated entries without requiring a full page reload
2341 - */
2342 -public function ajax_mxchat_paginate_entries() {
2343 - check_ajax_referer('mxchat_entries_nonce', 'nonce');
2344 -
2345 - if (!current_user_can('manage_options')) {
2346 - wp_send_json_error(array('message' => 'Unauthorized'));
2347 - return;
2348 - }
2349 -
2350 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
2351 - $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
2352 - $search_query = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
2353 - $content_type_filter = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : '';
2354 - $per_page = 25;
2355 -
2356 - // Check if Pinecone is enabled for this bot
2357 - $pinecone_manager = $this->mxchat_get_pinecone_manager();
2358 - $pinecone_options = $pinecone_manager ? $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id) : array();
2359 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2360 - $has_pinecone_api = !empty($pinecone_options['mxchat_pinecone_api_key']);
2361 -
2362 - if ($use_pinecone && $has_pinecone_api) {
2363 - // Delegate to Pinecone pagination handler (pass search params)
2364 - $_POST['page'] = $page;
2365 - $_POST['search'] = $search_query;
2366 - $_POST['content_type'] = $content_type_filter;
2367 - $this->ajax_mxchat_refresh_pinecone_entries();
2368 - return;
2369 - }
2370 -
2371 - // WordPress DB pagination - MUST match initial page load logic exactly
2372 - global $wpdb;
2373 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2374 - $offset = ($page - 1) * $per_page;
2375 -
2376 - // Build WHERE clause for search and content type filtering
2377 - $where_clauses = array();
2378 - $where_values = array();
2379 -
2380 - if ($search_query) {
2381 - $where_clauses[] = "article_content LIKE %s";
2382 - $where_values[] = '%' . $wpdb->esc_like($search_query) . '%';
2383 - }
2384 -
2385 - if ($content_type_filter) {
2386 - switch ($content_type_filter) {
2387 - case 'manual':
2388 - $where_clauses[] = "(source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')";
2389 - break;
2390 - case 'pdf':
2391 - $where_clauses[] = "source_url LIKE '%.pdf'";
2392 - break;
2393 - case 'url':
2394 - $where_clauses[] = "source_url != '' AND source_url IS NOT NULL AND source_url NOT LIKE 'mxchat://%' AND source_url NOT LIKE '%.pdf'";
2395 - break;
2396 - }
2397 - }
2398 -
2399 - $where_sql = !empty($where_clauses) ? 'WHERE ' . implode(' AND ', $where_clauses) : '';
2400 -
2401 - // Count grouped entries with filters applied
2402 - if (!empty($where_values)) {
2403 - $count_args = array_merge($where_values, $where_values);
2404 - $count_query = $wpdb->prepare(
2405 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$where_sql} AND source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2406 - (SELECT COUNT(*) FROM {$table_name} {$where_sql} AND (source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%'))",
2407 - ...$count_args
2408 - );
2409 - $total_records = $wpdb->get_var($count_query);
2410 - } else if (!empty($where_sql)) {
2411 - // Content type filter only (no search), no prepared values needed
2412 - $total_records = $wpdb->get_var(
2413 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$where_sql} AND source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2414 - (SELECT COUNT(*) FROM {$table_name} {$where_sql} AND (source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%'))"
2415 - );
2416 - } else {
2417 - // No filters
2418 - $total_records = $wpdb->get_var(
2419 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2420 - (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')"
2421 - );
2422 - }
2423 - $total_pages = ceil($total_records / $per_page);
2424 -
2425 - // Step 1: Get unique source_urls for this page (ordered by latest timestamp) with filters
2426 - if (!empty($where_values)) {
2427 - $query_args = array_merge($where_values, array($per_page, $offset));
2428 - $urls_query = $wpdb->prepare(
2429 - "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2430 - {$where_sql}
2431 - GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2432 - ...$query_args
2433 - );
2434 - } else if (!empty($where_sql)) {
2435 - $urls_query = $wpdb->prepare(
2436 - "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2437 - {$where_sql}
2438 - GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2439 - $per_page, $offset
2440 - );
2441 - } else {
2442 - $urls_query = $wpdb->prepare(
2443 - "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2444 - GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2445 - $per_page, $offset
2446 - );
2447 - }
2448 - $page_urls = $wpdb->get_results($urls_query);
2449 -
2450 - // Step 2: Build list of source_urls to fetch
2451 - $url_list = array();
2452 - $url_order_map = array();
2453 - $order_index = 0;
2454 - foreach ($page_urls as $url_row) {
2455 - $url = $url_row->source_url;
2456 - $url_list[] = $url;
2457 - $url_order_map[$url] = $order_index++;
2458 - }
2459 -
2460 - // Step 3: Fetch all rows for these source_urls (with search filter if applicable)
2461 - $prompts = array();
2462 - if (!empty($url_list)) {
2463 - $placeholders = implode(',', array_fill(0, count($url_list), '%s'));
2464 - if ($search_query) {
2465 - // Include search filter in the final fetch
2466 - $prompts_query = $wpdb->prepare(
2467 - "SELECT id, article_content, source_url, timestamp, role_restriction
2468 - FROM {$table_name}
2469 - WHERE source_url IN ($placeholders) AND article_content LIKE %s
2470 - ORDER BY timestamp DESC",
2471 - ...array_merge($url_list, ['%' . $wpdb->esc_like($search_query) . '%'])
2472 - );
2473 - } else {
2474 - $prompts_query = $wpdb->prepare(
2475 - "SELECT id, article_content, source_url, timestamp, role_restriction
2476 - FROM {$table_name}
2477 - WHERE source_url IN ($placeholders)
2478 - ORDER BY timestamp DESC",
2479 - $url_list
2480 - );
2481 - }
2482 - $prompts = $wpdb->get_results($prompts_query);
2483 - }
2484 -
2485 - // Group prompts by source_url for chunk display
2486 - $grouped_prompts = array();
2487 - foreach ($prompts as $prompt) {
2488 - $source_url = $prompt->source_url ?? '';
2489 -
2490 - // Parse chunk metadata using the proper chunker method (same as initial page load)
2491 - if (class_exists('MxChat_Chunker')) {
2492 - $chunk_meta = MxChat_Chunker::parse_stored_chunk($prompt->article_content);
2493 - $prompt->chunk_metadata = $chunk_meta['metadata'];
2494 - $prompt->display_content = $chunk_meta['text'];
2495 - } else {
2496 - $prompt->chunk_metadata = array();
2497 - $prompt->display_content = $prompt->article_content;
2498 - }
2499 -
2500 - if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0) {
2501 - if (!isset($grouped_prompts[$source_url])) {
2502 - $grouped_prompts[$source_url] = array();
2503 - }
2504 - $grouped_prompts[$source_url][] = $prompt;
2505 - } else {
2506 - // Ungrouped entries
2507 - $grouped_prompts['_ungrouped_' . $prompt->id] = array($prompt);
2508 - }
2509 - }
2510 -
2511 - // Sort groups by the original URL order (newest first)
2512 - uksort($grouped_prompts, function($a, $b) use ($url_order_map) {
2513 - $order_a = isset($url_order_map[$a]) ? $url_order_map[$a] : PHP_INT_MAX;
2514 - $order_b = isset($url_order_map[$b]) ? $url_order_map[$b] : PHP_INT_MAX;
2515 - return $order_a - $order_b;
2516 - });
2517 -
2518 - // Sort each group internally by chunk_index
2519 - foreach ($grouped_prompts as $source_url => &$group) {
2520 - usort($group, function($a, $b) {
2521 - $index_a = isset($a->chunk_metadata['chunk_index']) ? intval($a->chunk_metadata['chunk_index']) : 0;
2522 - $index_b = isset($b->chunk_metadata['chunk_index']) ? intval($b->chunk_metadata['chunk_index']) : 0;
2523 - return $index_a - $index_b;
2524 - });
2525 - }
2526 - unset($group);
2527 -
2528 - // Build HTML for the table rows
2529 - ob_start();
2530 - $display_index = 0;
2531 - $current_page = $page;
2532 - $data_source = 'wordpress';
2533 - $current_bot_id = $bot_id;
2534 - $preview_length = 150;
2535 -
2536 - if (empty($grouped_prompts)) {
2537 - echo '<tr><td colspan="5" style="padding: 40px; text-align: center; color: var(--mxch-text-muted);">';
2538 - esc_html_e('No knowledge entries found. Use the Import Options to add content.', 'mxchat');
2539 - echo '</td></tr>';
2540 - } else {
2541 - foreach ($grouped_prompts as $source_url => $group) {
2542 - $chunk_count = count($group);
2543 - $first_prompt = $group[0];
2544 - $display_index++;
2545 -
2546 - if ($chunk_count > 1) {
2547 - // Multiple chunks - show grouped row with expand button
2548 - $group_id = 'group-' . md5($source_url);
2549 - ?>
2550 - <tr id="prompt-<?php echo esc_attr($first_prompt->id); ?>"
2551 - class="mxchat-chunk-group-header"
2552 - data-source="<?php echo esc_attr($data_source); ?>"
2553 - data-group-id="<?php echo esc_attr($group_id); ?>"
2554 - style="border-bottom: 1px solid var(--mxch-card-border);">
2555 - <td style="padding: 12px 16px; text-align: center;">
2556 - <input type="checkbox"
2557 - class="mxchat-entry-checkbox"
2558 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2559 - data-source="<?php echo esc_attr($data_source); ?>"
2560 - data-source-url="<?php echo esc_attr($source_url); ?>"
2561 - data-is-group="true"
2562 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>">
2563 - </td>
2564 - <td style="padding: 12px 16px; font-size: 13px;">
2565 - <?php echo esc_html($first_prompt->id); ?>
2566 - </td>
2567 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2568 - <div class="mxchat-chunk-group-info">
2569 - <button type="button" class="mxchat-chunk-toggle" data-group-id="<?php echo esc_attr($group_id); ?>">
2570 - <span class="dashicons dashicons-arrow-right-alt2"></span>
2571 - </button>
2572 - <span class="mxchat-chunk-badge"><?php echo esc_html($chunk_count); ?> <?php esc_html_e('chunks', 'mxchat'); ?></span>
2573 - <span class="mxchat-chunk-preview">
2574 - <?php
2575 - $parent_content = isset($first_prompt->display_content) ? $first_prompt->display_content : $first_prompt->article_content;
2576 - $content_preview = mb_substr($parent_content, 0, 100);
2577 - echo esc_html($content_preview . '...');
2578 - ?>
2579 - </span>
2580 - </div>
2581 - </td>
2582 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2583 - <?php if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0 && strpos($source_url, '_ungrouped_') !== 0) : ?>
2584 - <a href="<?php echo esc_url($source_url); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2585 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2586 - <?php esc_html_e('View Source', 'mxchat'); ?>
2587 - </a>
2588 - <?php else : ?>
2589 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
2590 - <?php endif; ?>
2591 - </td>
2592 - <td class="mxchat-actions-cell" style="padding: 12px 16px; white-space: nowrap;">
2593 - <?php if ($data_source !== 'pinecone') : ?>
2594 - <button type="button"
2595 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2596 - data-source-url="<?php echo esc_attr($source_url); ?>"
2597 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2598 - data-data-source="<?php echo esc_attr($data_source); ?>"
2599 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2600 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2601 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2602 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2603 - </button>
2604 - <?php endif; ?>
2605 - <button type="button"
2606 - class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
2607 - data-source-url="<?php echo esc_attr($source_url); ?>"
2608 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
2609 - data-data-source="<?php echo esc_attr($data_source); ?>"
2610 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2611 - data-nonce="<?php echo wp_create_nonce('mxchat_delete_chunks_nonce'); ?>"
2612 - style="color: var(--mxch-error);"
2613 - title="<?php esc_attr_e('Delete all chunks', 'mxchat'); ?>">
2614 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2615 - </button>
2616 - </td>
2617 - </tr>
2618 - <?php
2619 - // Render hidden chunk rows
2620 - foreach ($group as $chunk_index => $chunk) {
2621 - $meta_chunk_index = isset($chunk->chunk_metadata['chunk_index']) ? intval($chunk->chunk_metadata['chunk_index']) : $chunk_index;
2622 - $meta_total_chunks = isset($chunk->chunk_metadata['total_chunks']) ? intval($chunk->chunk_metadata['total_chunks']) : $chunk_count;
2623 - $content = isset($chunk->display_content) ? $chunk->display_content : $chunk->article_content;
2624 - $content_preview = mb_strlen($content) > $preview_length
2625 - ? mb_substr($content, 0, $preview_length) . '...'
2626 - : $content;
2627 - ?>
2628 - <tr id="prompt-<?php echo esc_attr($chunk->id); ?>"
2629 - class="mxchat-chunk-row <?php echo esc_attr($group_id); ?>"
2630 - data-source="<?php echo esc_attr($data_source); ?>"
2631 - style="display: none; background: #f8f9fa; border-bottom: 1px solid var(--mxch-card-border);">
2632 - <td style="padding: 12px 16px; text-align: center;">
2633 - <!-- Checkbox column placeholder for chunks (managed by group) -->
2634 - </td>
2635 - <td style="padding: 12px 16px 12px 30px; font-size: 13px;">
2636 - <!-- Hidden ID column for chunks -->
2637 - </td>
2638 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2639 - <div class="mxchat-accordion-wrapper">
2640 - <div class="mxchat-content-preview">
2641 - <span class="mxchat-chunk-indicator" style="margin-right: 10px; color: var(--mxch-text-secondary); font-size: 12px;">
2642 - <?php printf(esc_html__('Chunk %d of %d', 'mxchat'), $meta_chunk_index + 1, $meta_total_chunks); ?>
2643 - </span>
2644 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2645 - <?php if (mb_strlen($content) > $preview_length) : ?>
2646 - <button class="mxchat-expand-toggle" type="button">
2647 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2648 - </button>
2649 - <?php endif; ?>
2650 - </div>
2651 - <div class="mxchat-content-full" style="display: none;">
2652 - <div class="content-view">
2653 - <?php
2654 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2655 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2656 - echo wp_kses_post(wpautop($content));
2657 - echo '</div>';
2658 - } else {
2659 - echo wp_kses_post(wpautop($content));
2660 - }
2661 - ?>
2662 - </div>
2663 - </div>
2664 - </div>
2665 - </td>
2666 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2667 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Same as parent', 'mxchat'); ?></span>
2668 - </td>
2669 - <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2670 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Managed by group', 'mxchat'); ?></span>
2671 - </td>
2672 - </tr>
2673 - <?php
2674 - }
2675 - } else {
2676 - // Single entry - display normally with accordion
2677 - $prompt = $first_prompt;
2678 - $content = isset($prompt->display_content) ? $prompt->display_content : $prompt->article_content;
2679 - $content_preview = mb_strlen($content) > $preview_length
2680 - ? mb_substr($content, 0, $preview_length) . '...'
2681 - : $content;
2682 - ?>
2683 - <tr id="prompt-<?php echo esc_attr($prompt->id); ?>"
2684 - data-source="<?php echo esc_attr($data_source); ?>"
2685 - style="border-bottom: 1px solid var(--mxch-card-border);">
2686 - <td style="padding: 12px 16px; text-align: center;">
2687 - <input type="checkbox"
2688 - class="mxchat-entry-checkbox"
2689 - data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2690 - data-source="<?php echo esc_attr($data_source); ?>"
2691 - data-source-url="<?php echo esc_attr($source_url); ?>"
2692 - data-is-group="false">
2693 - </td>
2694 - <td style="padding: 12px 16px; font-size: 13px;">
2695 - <?php echo esc_html($prompt->id); ?>
2696 - </td>
2697 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2698 - <div class="mxchat-accordion-wrapper">
2699 - <div class="mxchat-content-preview">
2700 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2701 - <?php if (mb_strlen($content) > $preview_length) : ?>
2702 - <button class="mxchat-expand-toggle" type="button">
2703 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2704 - </button>
2705 - <?php endif; ?>
2706 - </div>
2707 - <div class="mxchat-content-full" style="display: none;">
2708 - <div class="content-view">
2709 - <?php
2710 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2711 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2712 - echo wp_kses_post(wpautop($content));
2713 - echo '</div>';
2714 - } else {
2715 - echo wp_kses_post(wpautop($content));
2716 - }
2717 - ?>
2718 - </div>
2719 - </div>
2720 - </div>
2721 - </td>
2722 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2723 - <?php
2724 - $actual_source = $source_url;
2725 - if (strpos($source_url, '_ungrouped_') === 0) {
2726 - $actual_source = $prompt->source_url ?? '';
2727 - }
2728 - if (!empty($actual_source) && strpos($actual_source, 'mxchat://') !== 0) : ?>
2729 - <a href="<?php echo esc_url($actual_source); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2730 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2731 - <?php esc_html_e('View', 'mxchat'); ?>
2732 - </a>
2733 - <?php else : ?>
2734 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
2735 - <?php endif; ?>
2736 - </td>
2737 - <td style="padding: 12px 16px; white-space: nowrap;">
2738 - <button type="button"
2739 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2740 - data-source-url="<?php echo esc_attr($prompt->source_url ?? ''); ?>"
2741 - data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2742 - data-data-source="<?php echo esc_attr($data_source); ?>"
2743 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2744 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2745 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2746 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2747 - </button>
2748 - <button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-wordpress" data-entry-id="<?php echo esc_attr($prompt->id); ?>" data-bot-id="<?php echo esc_attr($current_bot_id); ?>" data-nonce="<?php echo wp_create_nonce('mxchat_delete_wordpress_prompt_nonce'); ?>" style="color: var(--mxch-error);">
2749 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2750 - </button>
2751 - </td>
2752 - </tr>
2753 - <?php
2754 - }
2755 - }
2756 - }
2757 - $html = ob_get_clean();
2758 -
2759 - // Generate pagination HTML (include search/filter data for subsequent pages)
2760 - $pagination_html = '';
2761 - if ($total_pages > 1) {
2762 - $pagination_html = '<div class="mxchat-ajax-pagination" data-current-page="' . esc_attr($page) . '" data-total-pages="' . esc_attr($total_pages) . '" data-search="' . esc_attr($search_query) . '" data-content-type="' . esc_attr($content_type_filter) . '">';
2763 -
2764 - // Previous button
2765 - if ($page > 1) {
2766 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page - 1) . '">' . esc_html__('&laquo; Previous', 'mxchat') . '</a> ';
2767 - }
2768 -
2769 - // Page numbers
2770 - $start_page = max(1, $page - 2);
2771 - $end_page = min($total_pages, $page + 2);
2772 -
2773 - if ($start_page > 1) {
2774 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="1">1</a> ';
2775 - if ($start_page > 2) {
2776 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2777 - }
2778 - }
2779 -
2780 - for ($i = $start_page; $i <= $end_page; $i++) {
2781 - if ($i == $page) {
2782 - $pagination_html .= '<span class="mxchat-page-current">' . $i . '</span> ';
2783 - } else {
2784 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $i . '">' . $i . '</a> ';
2785 - }
2786 - }
2787 -
2788 - if ($end_page < $total_pages) {
2789 - if ($end_page < $total_pages - 1) {
2790 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2791 - }
2792 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $total_pages . '">' . $total_pages . '</a> ';
2793 - }
2794 -
2795 - // Next button
2796 - if ($page < $total_pages) {
2797 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page + 1) . '">' . esc_html__('Next &raquo;', 'mxchat') . '</a>';
2798 - }
2799 -
2800 - $pagination_html .= '</div>';
2801 - }
2802 -
2803 - wp_send_json_success(array(
2804 - 'html' => $html,
2805 - 'pagination_html' => $pagination_html,
2806 - 'total_count' => $total_records,
2807 - 'total_pages' => $total_pages,
2808 - 'page' => $page,
2809 - 'per_page' => $per_page,
2810 - 'data_source' => 'wordpress'
2811 - ));
2812 -}
2813 -
2814 -/**
2815 - * AJAX handler to detect available sitemaps on the site
2816 - * Optimized for speed - only checks primary sitemap indexes first
2817 - */
2818 -public function ajax_mxchat_detect_sitemaps() {
2819 - check_ajax_referer('mxchat_detect_sitemaps_nonce', 'nonce');
2820 -
2821 - if (!current_user_can('manage_options')) {
2822 - wp_send_json_error(array('message' => 'Unauthorized'));
2823 - return;
2824 - }
2825 -
2826 - $site_url = get_site_url();
2827 - $sitemaps = array();
2828 - $found_index = false;
2829 -
2830 - // Only check the main sitemap index files first (much faster)
2831 - // These are the primary entry points that contain sub-sitemaps
2832 - $primary_indexes = array(
2833 - 'sitemap_index.xml' => 'Yoast SEO / Rank Math', // Yoast & Rank Math
2834 - 'wp-sitemap.xml' => 'WordPress Core', // WordPress Core
2835 - 'sitemap.xml' => 'Standard', // Generic/AIOSEO
2836 - );
2837 -
2838 - foreach ($primary_indexes as $path => $source) {
2839 - $url = trailingslashit($site_url) . $path;
2840 -
2841 - $response = wp_remote_head($url, array(
2842 - 'timeout' => 10,
2843 - 'sslverify' => false,
2844 - 'redirection' => 1,
2845 - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
2846 - ));
2847 -
2848 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2849 - // Found a sitemap index - parse it to get sub-sitemaps
2850 - $sub_sitemaps = $this->parse_sitemap_index($url);
2851 - if (!empty($sub_sitemaps)) {
2852 - $sitemaps[] = array(
2853 - 'url' => $url,
2854 - 'type' => 'index',
2855 - 'source' => $source,
2856 - 'sub_sitemaps' => $sub_sitemaps
2857 - );
2858 - $found_index = true;
2859 - // Found a valid index, no need to check others
2860 - break;
2861 - }
2862 - }
2863 - }
2864 -
2865 - // If no sitemap index found, check for standalone sitemaps
2866 - if (!$found_index) {
2867 - $standalone_sitemaps = array(
2868 - 'post-sitemap.xml' => array('type' => 'content', 'source' => 'Yoast SEO'),
2869 - 'page-sitemap.xml' => array('type' => 'content', 'source' => 'Yoast SEO'),
2870 - );
2871 -
2872 - foreach ($standalone_sitemaps as $path => $info) {
2873 - $url = trailingslashit($site_url) . $path;
2874 -
2875 - $response = wp_remote_head($url, array(
2876 - 'timeout' => 2,
2877 - 'sslverify' => false
2878 - ));
2879 -
2880 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2881 - $sitemaps[] = array(
2882 - 'url' => $url,
2883 - 'type' => $info['type'],
2884 - 'source' => $info['source'],
2885 - 'url_count' => 0 // Skip URL count for speed
2886 - );
2887 - }
2888 - }
2889 - }
2890 -
2891 - wp_send_json_success(array(
2892 - 'sitemaps' => $sitemaps,
2893 - 'site_url' => $site_url
2894 - ));
2895 -}
2896 -
2897 -/**
2898 - * Parse a sitemap index to get sub-sitemaps
2899 - * Optimized: doesn't fetch URL count for each sub-sitemap (too slow)
2900 - */
2901 -private function parse_sitemap_index($url) {
2902 - $sub_sitemaps = array();
2903 -
2904 - $response = wp_remote_get($url, array(
2905 - 'timeout' => 30,
2906 - 'sslverify' => false,
2907 - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
2908 - 'headers' => array(
2909 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
2910 - 'Accept-Language' => 'en-US,en;q=0.9',
2911 - ),
2912 - ));
2913 -
2914 - if (is_wp_error($response)) {
2915 - return $sub_sitemaps;
2916 - }
2917 -
2918 - $body = wp_remote_retrieve_body($response);
2919 - if (empty($body)) {
2920 - return $sub_sitemaps;
2921 - }
2922 -
2923 - // Suppress XML errors
2924 - libxml_use_internal_errors(true);
2925 - $xml = simplexml_load_string($body);
2926 - libxml_clear_errors();
2927 -
2928 - if ($xml === false) {
2929 - return $sub_sitemaps;
2930 - }
2931 -
2932 - // Check if it's a sitemap index (contains <sitemap> elements)
2933 - if (isset($xml->sitemap)) {
2934 - foreach ($xml->sitemap as $sitemap) {
2935 - $loc = (string) $sitemap->loc;
2936 - if (!empty($loc)) {
2937 - // Try to determine the type from the URL
2938 - $type = 'content';
2939 - if (strpos($loc, 'category') !== false || strpos($loc, 'tag') !== false || strpos($loc, 'taxonomy') !== false) {
2940 - $type = 'taxonomy';
2941 - } elseif (strpos($loc, 'author') !== false || strpos($loc, 'user') !== false) {
2942 - $type = 'author';
2943 - }
2944 -
2945 - // Skip URL count - too slow to fetch for each sitemap
2946 - $sub_sitemaps[] = array(
2947 - 'url' => $loc,
2948 - 'type' => $type,
2949 - 'url_count' => 0, // Don't fetch - takes too long
2950 - 'name' => basename(parse_url($loc, PHP_URL_PATH))
2951 - );
2952 - }
2953 - }
2954 - }
2955 -
2956 - return $sub_sitemaps;
2957 -}
2958 -
2959 -/**
2960 - * Get URL count from a sitemap
2961 - */
2962 -private function get_sitemap_url_count($url) {
2963 - $response = wp_remote_get($url, array(
2964 - 'timeout' => 30,
2965 - 'sslverify' => false,
2966 - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
2967 - 'headers' => array(
2968 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
2969 - 'Accept-Language' => 'en-US,en;q=0.9',
2970 - ),
2971 - ));
2972 -
2973 - if (is_wp_error($response)) {
2974 - return 0;
2975 - }
2976 -
2977 - $body = wp_remote_retrieve_body($response);
2978 - if (empty($body)) {
2979 - return 0;
2980 - }
2981 -
2982 - // Count <url> or <loc> elements
2983 - $count = preg_match_all('/<url>/i', $body, $matches);
2984 - return $count ?: 0;
2985 -}
2986 -
2987 -/**
2988 - * Get sitemaps declared in robots.txt
2989 - */
2990 -private function get_sitemaps_from_robots($site_url) {
2991 - $sitemaps = array();
2992 - $robots_url = trailingslashit($site_url) . 'robots.txt';
2993 -
2994 - $response = wp_remote_get($robots_url, array(
2995 - 'timeout' => 15,
2996 - 'sslverify' => false,
2997 - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
2998 - ));
2999 -
3000 - if (is_wp_error($response)) {
3001 - return $sitemaps;
3002 - }
3003 -
3004 - $body = wp_remote_retrieve_body($response);
3005 - if (empty($body)) {
3006 - return $sitemaps;
3007 - }
3008 -
3009 - // Find Sitemap: declarations
3010 - if (preg_match_all('/^Sitemap:\s*(.+)$/mi', $body, $matches)) {
3011 - foreach ($matches[1] as $sitemap_url) {
3012 - $sitemap_url = trim($sitemap_url);
3013 - if (filter_var($sitemap_url, FILTER_VALIDATE_URL)) {
3014 - $sitemaps[] = $sitemap_url;
3015 - }
3016 - }
3017 - }
3018 -
3019 - return $sitemaps;
3020 -}
3021 -
3022 -public function mxchat_stop_processing() {
3023 - // Verify permissions
3024 - if (!current_user_can('manage_options')) {
3025 - wp_die(esc_html__('Unauthorized access', 'mxchat'));
3026 - }
3027 -
3028 - // Verify nonce
3029 - check_admin_referer('mxchat_stop_processing_action', 'mxchat_stop_processing_nonce');
3030 -
3031 - global $wpdb;
3032 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
3033 -
3034 - // Get active queue IDs
3035 - $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
3036 - $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
3037 -
3038 - // Delete all pending items from active queues
3039 - if ($sitemap_queue_id) {
3040 - $wpdb->delete(
3041 - $table_name,
3042 - array(
3043 - 'queue_id' => $sitemap_queue_id,
3044 - 'status' => 'pending'
3045 - ),
3046 - array('%s', '%s')
3047 - );
3048 -
3049 - delete_transient('mxchat_active_queue_sitemap');
3050 - delete_transient('mxchat_last_sitemap_url');
3051 - }
3052 -
3053 - if ($pdf_queue_id) {
3054 - // Get PDF path before deleting
3055 - $pdf_path = $this->mxchat_get_queue_meta($pdf_queue_id, 'pdf_path');
3056 -
3057 - $wpdb->delete(
3058 - $table_name,
3059 - array(
3060 - 'queue_id' => $pdf_queue_id,
3061 - 'status' => 'pending'
3062 - ),
3063 - array('%s', '%s')
3064 - );
3065 -
3066 - // Delete PDF file
3067 - if ($pdf_path && file_exists($pdf_path)) {
3068 - wp_delete_file($pdf_path);
3069 - }
3070 -
3071 - delete_transient('mxchat_active_queue_pdf');
3072 - delete_transient('mxchat_last_pdf_url');
3073 - }
3074 -
3075 - // Redirect back with a success message
3076 - set_transient('mxchat_admin_notice_success',
3077 - esc_html__('Processing has been stopped successfully.', 'mxchat'),
3078 - 30
3079 - );
3080 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
3081 - exit;
3082 -}
3083 -
3084 -/**
3085 - * Get content list for processing
3086 - */
3087 -public function ajax_mxchat_get_content_list() {
3088 - // Verify the nonce
3089 - check_ajax_referer('mxchat_content_selector_nonce', 'nonce');
3090 -
3091 - if (!current_user_can('manage_options')) {
3092 - wp_send_json_error(__('Unauthorized access', 'mxchat'));
3093 - }
3094 -
3095 - $page = isset($_GET['page']) ? absint($_GET['page']) : 1;
3096 - $per_page = isset($_GET['per_page']) ? absint($_GET['per_page']) : 100;
3097 - $search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
3098 - $post_type = isset($_GET['post_type']) ? sanitize_text_field($_GET['post_type']) : 'all';
3099 - $post_status = isset($_GET['post_status']) ? sanitize_text_field($_GET['post_status']) : 'publish';
3100 - $processed_filter = isset($_GET['processed_filter']) ? sanitize_text_field($_GET['processed_filter']) : 'all';
3101 -
3102 - // Build query args
3103 - $args = array(
3104 - 'posts_per_page' => $per_page,
3105 - 'paged' => $page,
3106 - 'post_status' => $post_status !== 'all' ? $post_status : array('publish', 'draft', 'pending'),
3107 - 'orderby' => 'date',
3108 - 'order' => 'DESC',
3109 - );
3110 -
3111 - // Handle post types - IMPROVED VERSION
3112 - if ($post_type !== 'all') {
3113 - $args['post_type'] = $post_type;
3114 - } else {
3115 - // Get all available post types that might contain content
3116 - $all_post_types = array();
3117 -
3118 - // First get all public post types
3119 - $public_types = get_post_types(array('public' => true), 'names');
3120 - $all_post_types = array_merge($all_post_types, $public_types);
3121 -
3122 - // Add common forum/community post types
3123 - $forum_types = array('topic', 'reply', 'forum', 'wpforo_topic', 'wpforo_post');
3124 - foreach ($forum_types as $forum_type) {
3125 - if (post_type_exists($forum_type)) {
3126 - $all_post_types[] = $forum_type;
3127 - }
3128 - }
3129 -
3130 - // Add other commonly used post types
3131 - $common_types = array('product', 'job_listing', 'event', 'portfolio');
3132 - foreach ($common_types as $common_type) {
3133 - if (post_type_exists($common_type)) {
3134 - $all_post_types[] = $common_type;
3135 - }
3136 - }
3137 -
3138 - // Remove duplicates and ensure we have at least some post types
3139 - $all_post_types = array_unique($all_post_types);
3140 -
3141 - if (empty($all_post_types)) {
3142 - // Fallback to basic post types
3143 - $all_post_types = array('post', 'page');
3144 - }
3145 -
3146 - $args['post_type'] = $all_post_types;
3147 -
3148 - // Debug logging to see what post types are being queried
3149 - //error_log('MxChat Debug: Querying post types: ' . implode(', ', $all_post_types));
3150 - }
3151 -
3152 - if (!empty($search)) {
3153 - $args['s'] = $search;
3154 - }
3155 -
3156 - // Get processed data from storage
3157 - $processed_data = array();
3158 -
3159 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3160 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3161 -
3162 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3163 - // Get fresh data from Pinecone - no caching
3164 - $processed_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
3165 - } else {
3166 - // WordPress DB checking with better URL matching for all post types
3167 - global $wpdb;
3168 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3169 - $processed_items = $wpdb->get_results("SELECT id, source_url, article_content, timestamp FROM {$table_name}");
3170 -
3171 - // Group items by source_url to count chunks
3172 - $url_chunk_counts = array();
3173 - $url_latest_timestamp = array();
3174 - $url_first_id = array();
3175 -
3176 - if (!empty($processed_items)) {
3177 - foreach ($processed_items as $item) {
3178 - $url = $item->source_url;
3179 - if (empty($url)) continue;
3180 -
3181 - // Count chunks per URL
3182 - if (!isset($url_chunk_counts[$url])) {
3183 - $url_chunk_counts[$url] = 0;
3184 - $url_latest_timestamp[$url] = $item->timestamp;
3185 - $url_first_id[$url] = $item->id;
3186 - }
3187 - $url_chunk_counts[$url]++;
3188 -
3189 - // Track latest timestamp
3190 - if (strtotime($item->timestamp) > strtotime($url_latest_timestamp[$url])) {
3191 - $url_latest_timestamp[$url] = $item->timestamp;
3192 - }
3193 - }
3194 -
3195 - // Now build processed_data with chunk counts
3196 - foreach ($url_chunk_counts as $url => $chunk_count) {
3197 - $post_id = $this->mxchat_url_to_post_id_improved($url);
3198 -
3199 - if ($post_id) {
3200 - $processed_data[$post_id] = array(
3201 - 'db_id' => $url_first_id[$url],
3202 - 'timestamp' => $url_latest_timestamp[$url],
3203 - 'url' => $url,
3204 - 'source' => 'wordpress',
3205 - 'chunk_count' => $chunk_count
3206 - );
3207 - }
3208 - }
3209 - }
3210 - }
3211 -
3212 - // Get processed IDs as a simple array for in_array checks
3213 - $processed_ids = array_keys($processed_data);
3214 -
3215 - // Handle processed/unprocessed filter
3216 - if ($processed_filter === 'processed' && !empty($processed_ids)) {
3217 - $args['post__in'] = $processed_ids;
3218 - } elseif ($processed_filter === 'unprocessed' && !empty($processed_ids)) {
3219 - $args['post__not_in'] = $processed_ids;
3220 - }
3221 -
3222 - // Run the query
3223 - $query = new WP_Query($args);
3224 - $content_items = array();
3225 -
3226 - if ($query->have_posts()) {
3227 - while ($query->have_posts()) {
3228 - $query->the_post();
3229 - $id = get_the_ID();
3230 - $post_date = get_the_date();
3231 - $excerpt = wp_trim_words(get_the_excerpt(), 20, '...');
3232 - $word_count = str_word_count(strip_tags(get_the_content()));
3233 -
3234 - $is_processed = in_array($id, $processed_ids);
3235 - $processed_date = '';
3236 - $db_record_id = 0;
3237 - $data_source = 'none';
3238 -
3239 - if ($is_processed && isset($processed_data[$id])) {
3240 - $item_data = $processed_data[$id];
3241 - $data_source = $item_data['source'];
3242 -
3243 - if ($data_source === 'wordpress' && isset($item_data['timestamp'])) {
3244 - // WordPress DB format
3245 - $timestamp = strtotime($item_data['timestamp']);
3246 - $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
3247 - $db_record_id = $item_data['db_id'];
3248 - } elseif ($data_source === 'pinecone') {
3249 - // Pinecone format
3250 - $processed_date = $item_data['processed_date'];
3251 - $db_record_id = $item_data['db_id'];
3252 - }
3253 - }
3254 -
3255 - // Get chunk count for this item
3256 - $chunk_count = 0;
3257 - if ($is_processed && isset($processed_data[$id]['chunk_count'])) {
3258 - $chunk_count = intval($processed_data[$id]['chunk_count']);
3259 - }
3260 -
3261 - $content_items[] = array(
3262 - 'id' => $id,
3263 - 'title' => get_the_title(),
3264 - 'permalink' => get_permalink(),
3265 - 'date' => $post_date,
3266 - 'type' => get_post_type(),
3267 - 'status' => get_post_status(),
3268 - 'excerpt' => $excerpt,
3269 - 'word_count' => $word_count,
3270 - 'already_processed' => $is_processed,
3271 - 'processed_date' => $processed_date,
3272 - 'db_record_id' => $db_record_id,
3273 - 'data_source' => $data_source,
3274 - 'chunk_count' => $chunk_count
3275 - );
3276 - }
3277 - wp_reset_postdata();
3278 - }
3279 -
3280 - $response = array(
3281 - 'items' => $content_items,
3282 - 'total' => $query->found_posts,
3283 - 'total_pages' => $query->max_num_pages,
3284 - 'current_page' => $page,
3285 - 'processed_count' => count($processed_ids)
3286 - );
3287 -
3288 - wp_send_json_success($response);
3289 - exit;
3290 -}
3291 -
3292 -
3293 -/**
3294 - * This function handles various WooCommerce URL formats and permalink structures
3295 - */
3296 -private function mxchat_url_to_post_id_improved($url) {
3297 - // First try the standard WordPress function
3298 - $post_id = url_to_postid($url);
3299 -
3300 - if ($post_id > 0) {
3301 - return $post_id;
3302 - }
3303 -
3304 - // If that fails, try more aggressive URL matching
3305 - // Remove trailing slashes and query parameters for better matching
3306 - $clean_url = rtrim($url, '/');
3307 - $clean_url = strtok($clean_url, '?'); // Remove query parameters
3308 -
3309 - // Try again with cleaned URL
3310 - $post_id = url_to_postid($clean_url);
3311 - if ($post_id > 0) {
3312 - return $post_id;
3313 - }
3314 -
3315 - // For bbPress forum topics, try extracting slug from URL
3316 - if (strpos($url, '/topic/') !== false || strpos($url, '/forums/') !== false) {
3317 - // Handle bbPress URLs: /forums/topic/topic-name/
3318 - if (preg_match('/\/forums\/topic\/([^\/\?]+)/', $url, $matches)) {
3319 - $topic_slug = $matches[1];
3320 -
3321 - // Look up topic by slug
3322 - $topic = get_page_by_path($topic_slug, OBJECT, 'topic');
3323 - if ($topic) {
3324 - return $topic->ID;
3325 - }
3326 -
3327 - // Alternative method: query by post_name
3328 - global $wpdb;
3329 - $post_id = $wpdb->get_var($wpdb->prepare(
3330 - "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
3331 - $topic_slug
3332 - ));
3333 -
3334 - if ($post_id) {
3335 - return intval($post_id);
3336 - }
3337 - }
3338 -
3339 - // Handle simpler topic URLs: /topic/topic-name/
3340 - if (preg_match('/\/topic\/([^\/\?]+)/', $url, $matches)) {
3341 - $topic_slug = $matches[1];
3342 -
3343 - global $wpdb;
3344 - $post_id = $wpdb->get_var($wpdb->prepare(
3345 - "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
3346 - $topic_slug
3347 - ));
3348 -
3349 - if ($post_id) {
3350 - return intval($post_id);
3351 - }
3352 - }
3353 - }
3354 -
3355 - // For WooCommerce products
3356 - if (strpos($url, '/product/') !== false || strpos($url, 'product=') !== false) {
3357 - // Extract product slug from various URL formats
3358 - $product_slug = '';
3359 -
3360 - // Handle pretty permalinks: /product/product-name/
3361 - if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
3362 - $product_slug = $matches[1];
3363 - }
3364 - // Handle query parameters: ?product=product-name
3365 - elseif (preg_match('/[\?&]product=([^&]+)/', $url, $matches)) {
3366 - $product_slug = $matches[1];
3367 - }
3368 -
3369 - if (!empty($product_slug)) {
3370 - // Look up product by slug
3371 - $product = get_page_by_path($product_slug, OBJECT, 'product');
3372 - if ($product) {
3373 - return $product->ID;
3374 - }
3375 -
3376 - // Alternative method: query by post_name
3377 - global $wpdb;
3378 - $post_id = $wpdb->get_var($wpdb->prepare(
3379 - "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'product' AND post_status = 'publish'",
3380 - $product_slug
3381 - ));
3382 -
3383 - if ($post_id) {
3384 - return intval($post_id);
3385 - }
3386 - }
3387 - }
3388 -
3389 - // Generic approach: try to extract slug and match against all post types
3390 - $parsed_url = wp_parse_url($clean_url);
3391 - $path = $parsed_url['path'] ?? '';
3392 -
3393 - if (!empty($path)) {
3394 - // Get the last part of the path as potential slug
3395 - $path_parts = array_filter(explode('/', trim($path, '/')));
3396 - $potential_slug = end($path_parts);
3397 -
3398 - if (!empty($potential_slug)) {
3399 - global $wpdb;
3400 -
3401 - // Try to find any post with this slug
3402 - $post_id = $wpdb->get_var($wpdb->prepare(
3403 - "SELECT ID FROM {$wpdb->posts}
3404 - WHERE post_name = %s
3405 - AND post_status IN ('publish', 'closed', 'private')
3406 - AND post_type NOT IN ('revision', 'attachment', 'nav_menu_item')
3407 - ORDER BY CASE
3408 - WHEN post_type = 'post' THEN 1
3409 - WHEN post_type = 'page' THEN 2
3410 - WHEN post_type = 'topic' THEN 3
3411 - WHEN post_type = 'product' THEN 4
3412 - ELSE 5
3413 - END
3414 - LIMIT 1",
3415 - $potential_slug
3416 - ));
3417 -
3418 - if ($post_id) {
3419 - return intval($post_id);
3420 - }
3421 - }
3422 - }
3423 -
3424 - // ADDITIONAL: Try direct database lookup by URL variations
3425 - global $wpdb;
3426 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3427 -
3428 - // Try variations of the URL (with/without trailing slash, http/https)
3429 - $url_variations = array(
3430 - $url,
3431 - rtrim($url, '/'),
3432 - $url . '/',
3433 - str_replace('http://', 'https://', $url),
3434 - str_replace('https://', 'http://', $url),
3435 - str_replace('http://', 'https://', rtrim($url, '/')),
3436 - str_replace('https://', 'http://', rtrim($url, '/'))
3437 - );
3438 -
3439 - // Remove duplicates
3440 - $url_variations = array_unique($url_variations);
3441 -
3442 - foreach ($url_variations as $variation) {
3443 - $existing_record = $wpdb->get_row($wpdb->prepare(
3444 - "SELECT id, source_url FROM $table_name WHERE source_url = %s",
3445 - $variation
3446 - ));
3447 -
3448 - if ($existing_record) {
3449 - // Try to get post ID from this stored URL
3450 - $stored_post_id = url_to_postid($existing_record->source_url);
3451 - if ($stored_post_id > 0) {
3452 - return $stored_post_id;
3453 - }
3454 - }
3455 - }
3456 -
3457 - return 0; // No match found
3458 -}
3459 -/**
3460 - * Process selected content via AJAX
3461 - */
3462 -public function ajax_mxchat_process_selected_content() {
3463 - // Basic request validation
3464 - if (!check_ajax_referer('mxchat_content_selector_nonce', 'nonce', false)) {
3465 - wp_send_json_error('Invalid nonce');
3466 - exit;
3467 - }
3468 -
3469 - if (!current_user_can('manage_options')) {
3470 - wp_send_json_error('Unauthorized access');
3471 - exit;
3472 - }
3473 -
3474 - // Get post IDs - safely parse the array
3475 - $post_ids = array();
3476 - if (isset($_POST['post_ids']) && is_array($_POST['post_ids'])) {
3477 - foreach ($_POST['post_ids'] as $id) {
3478 - $post_ids[] = absint($id);
3479 - }
3480 - }
3481 -
3482 - if (empty($post_ids)) {
3483 - wp_send_json_error('No content selected');
3484 - exit;
3485 - }
3486 -
3487 - // Get bot_id from request
3488 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
3489 -
3490 - // Process only ONE post at a time to avoid request size issues
3491 - $post_id = reset($post_ids);
3492 - $post = get_post($post_id);
3493 -
3494 - if (!$post) {
3495 - wp_send_json_error('Post not found');
3496 - exit;
3497 - }
3498 -
3499 - // Allow developers to modify post data before processing into knowledge base
3500 - $post = apply_filters('mxchat_before_process_post', $post, $bot_id);
3501 -
3502 - // Get content including title, short description (for WooCommerce), and main content
3503 - $content = $post->post_title . "\n\n";
3504 -
3505 - // Add short description if it exists (WooCommerce products use post_excerpt for short description)
3506 - if (!empty($post->post_excerpt)) {
3507 - // Remove shortcode tags but preserve content inside them
3508 - $clean_excerpt = $this->strip_shortcode_tags_preserve_content($post->post_excerpt);
3509 - $content .= "Short Description: " . wp_strip_all_tags($clean_excerpt) . "\n\n";
3510 - }
3511 -
3512 - // Add main content - remove shortcode tags but preserve content inside them
3513 - $clean_content = $this->strip_shortcode_tags_preserve_content($post->post_content);
3514 - $content .= wp_strip_all_tags($clean_content);
3515 -
3516 - // ADD WOOCOMMERCE PRODUCT DATA (pricing, stock, categories, custom tabs)
3517 - if (get_post_type($post_id) === 'product' && class_exists('WooCommerce')) {
3518 - $product = wc_get_product($post_id);
3519 -
3520 - if ($product) {
3521 - // Get pricing information
3522 - $regular_price = $product->get_regular_price();
3523 - $sale_price = $product->get_sale_price();
3524 - $price = $product->get_price();
3525 - $sku = $product->get_sku();
3526 -
3527 - // Get currency symbol
3528 - $currency_symbol = get_woocommerce_currency_symbol();
3529 -
3530 - // Add pricing information
3531 - $content .= "\n";
3532 - if (!empty($regular_price)) {
3533 - $content .= "Price: " . $currency_symbol . $regular_price . "\n";
3534 - } elseif (!empty($price)) {
3535 - $content .= "Price: " . $currency_symbol . $price . "\n";
3536 - }
3537 -
3538 - if (!empty($sale_price) && $sale_price !== $regular_price) {
3539 - $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
3540 - }
3541 -
3542 - // Handle variable products - show price range
3543 - if ($product->is_type('variable')) {
3544 - $min_price = $product->get_variation_price('min');
3545 - $max_price = $product->get_variation_price('max');
3546 - if ($min_price !== $max_price) {
3547 - $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
3548 - }
3549 - }
3550 -
3551 - if (!empty($sku)) {
3552 - $content .= "SKU: " . $sku . "\n";
3553 - }
3554 -
3555 - // Get product categories
3556 - $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
3557 - if (!empty($categories) && !is_wp_error($categories)) {
3558 - $content .= "Categories: " . implode(', ', $categories) . "\n";
3559 - }
3560 - }
3561 -
3562 - // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
3563 - $custom_tabs = get_post_meta($post_id, 'yikes_woo_products_tabs', true);
3564 - if (!empty($custom_tabs) && is_array($custom_tabs)) {
3565 - foreach ($custom_tabs as $tab) {
3566 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
3567 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
3568 -
3569 - if (!empty($tab_title) && !empty($tab_content)) {
3570 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
3571 - }
3572 - }
3573 - }
3574 -
3575 - // Also check for reusable/saved tabs applied to this product
3576 - $applied_saved_tabs = get_post_meta($post_id, 'yikes_woo_reusable_products_tabs_applied', true);
3577 - if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
3578 - $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
3579 - if (!empty($saved_tabs) && is_array($saved_tabs)) {
3580 - foreach ($applied_saved_tabs as $saved_tab_id) {
3581 - if (isset($saved_tabs[$saved_tab_id])) {
3582 - $tab = $saved_tabs[$saved_tab_id];
3583 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
3584 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
3585 -
3586 - if (!empty($tab_title) && !empty($tab_content)) {
3587 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
3588 - }
3589 - }
3590 - }
3591 - }
3592 - }
3593 - }
3594 -
3595 - // ADD ACF FIELDS SUPPORT
3596 - $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
3597 - if (!empty($acf_fields)) {
3598 - $acf_content_parts = array();
3599 -
3600 - foreach ($acf_fields as $field_name => $field_value) {
3601 - $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
3602 -
3603 - if (!empty($formatted_value)) {
3604 - $field_label = ucwords(str_replace('_', ' ', $field_name));
3605 - $acf_content_parts[] = $field_label . ": " . $formatted_value;
3606 - }
3607 - }
3608 -
3609 - if (!empty($acf_content_parts)) {
3610 - $content .= "\n\n" . implode("\n", $acf_content_parts);
3611 - }
3612 - }
3613 -
3614 - // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
3615 - $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
3616 - if (!empty($custom_meta)) {
3617 - $meta_content_parts = array();
3618 -
3619 - foreach ($custom_meta as $meta_key => $meta_value) {
3620 - // Convert meta key to readable label
3621 - $meta_label = ucwords(str_replace(array('_', '-'), ' ', $meta_key));
3622 - $meta_content_parts[] = $meta_label . ": " . $meta_value;
3623 - }
3624 -
3625 - if (!empty($meta_content_parts)) {
3626 - $content .= "\n\n" . implode("\n", $meta_content_parts);
3627 - }
3628 - }
3629 -
3630 - // Debug logging for WordPress Import content
3631 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Post ID: ' . $post_id . ' Title: ' . $post->post_title);
3632 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Raw post_content length: ' . strlen($post->post_content));
3633 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Final content length: ' . strlen($content));
3634 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Content preview: ' . substr($content, 0, 300));
3635 -
3636 - // Note: Removed 10,000 char limit - chunking now handles large content properly
3637 -
3638 - // Get bot-specific API key
3639 - $bot_options = $this->get_bot_options($bot_id);
3640 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
3641 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3642 -
3643 - if (strpos($selected_model, 'voyage') === 0) {
3644 - $api_key = $options['voyage_api_key'] ?? '';
3645 - $provider_name = 'Voyage AI';
3646 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3647 - $api_key = $options['gemini_api_key'] ?? '';
3648 - $provider_name = 'Google Gemini';
3649 - } else {
3650 - $api_key = $options['api_key'] ?? '';
3651 - $provider_name = 'OpenAI';
3652 - }
3653 -
3654 - if (empty($api_key)) {
3655 - MxChat_Admin::mxchat_log_debug('api_error', $provider_name . ' API key not configured for knowledge processing');
3656 - wp_send_json_error($provider_name . ' API key not configured');
3657 - exit;
3658 - }
3659 -
3660 - $source_url = get_permalink($post_id);
3661 - $vector_id = md5($source_url); // Vector ID for Pinecone
3662 -
3663 - // Check for existing content in bot-specific storage
3664 - $is_update = false;
3665 -
3666 - // Get bot-specific Pinecone configuration
3667 - $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
3668 - $use_pinecone = !empty($bot_pinecone_config) && ($bot_pinecone_config['use_pinecone'] ?? false);
3669 -
3670 - if ($use_pinecone && !empty($bot_pinecone_config['api_key'])) {
3671 - // Check Pinecone for this bot
3672 - $pinecone_data = $this->mxchat_get_pinecone_processed_content($bot_pinecone_config);
3673 - if (isset($pinecone_data[$post_id])) {
3674 - $is_update = true;
3675 - }
3676 - } else {
3677 - // Check WordPress DB (same as before since it's shared)
3678 - global $wpdb;
3679 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3680 - $existing_record = $wpdb->get_row($wpdb->prepare(
3681 - "SELECT id FROM $table_name WHERE source_url = %s",
3682 - $source_url
3683 - ));
3684 -
3685 - if ($existing_record) {
3686 - $is_update = true;
3687 - }
3688 - }
3689 -
3690 - // UPDATED 2.5.6: Determine content type based on post_type
3691 - $post_type = $post->post_type;
3692 - $content_type = 'content'; // Default fallback
3693 -
3694 - // Map WordPress post types to content types
3695 - switch ($post_type) {
3696 - case 'post':
3697 - $content_type = 'post';
3698 - break;
3699 - case 'page':
3700 - $content_type = 'page';
3701 - break;
3702 - case 'product':
3703 - $content_type = 'product';
3704 - break;
3705 - default:
3706 - // For custom post types, use the post type name
3707 - $content_type = sanitize_key($post_type);
3708 - break;
3709 - }
3710 -
3711 - // Use the centralized utility function with bot_id and content_type
3712 - $result = MxChat_Utils::submit_content_to_db(
3713 - $content,
3714 - $source_url,
3715 - $api_key,
3716 - $vector_id,
3717 - $bot_id,
3718 - $content_type
3719 - );
3720 -
3721 - if (is_wp_error($result)) {
3722 - MxChat_Admin::mxchat_log_debug('storage_error', 'Knowledge storage failed: ' . $result->get_error_message(), array('source_url' => $source_url));
3723 - wp_send_json_error('Storage failed: ' . $result->get_error_message());
3724 - exit;
3725 - }
3726 -
3727 - // Automatically apply role restriction based on tags
3728 - $this->apply_role_restriction_to_post($post_id, $source_url);
3729 -
3730 - $operation_type = $is_update ? 'update' : 'new';
3731 -
3732 - // Count ACF fields for debugging
3733 - $acf_field_count = count($acf_fields);
3734 -
3735 - // Success response with minimal data
3736 - wp_send_json_success(array(
3737 - 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully',
3738 - 'post_id' => $post_id,
3739 - 'title' => $post->post_title,
3740 - 'operation_type' => $operation_type,
3741 - 'vector_id' => $vector_id,
3742 - 'acf_fields_found' => $acf_field_count,
3743 - 'content_preview' => substr($content, 0, 100) . '...',
3744 - 'bot_id' => $bot_id
3745 - ));
3746 - exit;
3747 -}
3748 -
3749 -private function apply_role_restriction_to_post($post_id, $source_url) {
3750 - // Get tag-role mappings
3751 - $mappings = get_option('mxchat_tag_role_mappings', array());
3752 -
3753 - if (empty($mappings)) {
3754 - return; // No mappings, leave as public
3755 - }
3756 -
3757 - // Get all tags for the post
3758 - $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
3759 -
3760 - if (empty($post_tags)) {
3761 - return; // No tags, leave as public
3762 - }
3763 -
3764 - // Determine the highest role restriction based on tags
3765 - $highest_role = 'public';
3766 - $role_hierarchy = array(
3767 - 'public' => 0,
3768 - 'logged_in' => 1,
3769 - 'subscriber' => 2,
3770 - 'contributor' => 3,
3771 - 'author' => 4,
3772 - 'editor' => 5,
3773 - 'administrator' => 6
3774 - );
3775 -
3776 - foreach ($post_tags as $tag_slug) {
3777 - if (isset($mappings[$tag_slug])) {
3778 - $role = $mappings[$tag_slug];
3779 - if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
3780 - $highest_role = $role;
3781 - }
3782 - }
3783 - }
3784 -
3785 - // If no restricted tags found, return (leave as public)
3786 - if ($highest_role === 'public') {
3787 - return;
3788 - }
3789 -
3790 - // Update the role restriction in the database
3791 - global $wpdb;
3792 -
3793 - // Check if using Pinecone
3794 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3795 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3796 -
3797 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3798 - // Update Pinecone role restriction
3799 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
3800 - $vector_id = md5($source_url);
3801 -
3802 - $wpdb->replace(
3803 - $roles_table,
3804 - array(
3805 - 'vector_id' => $vector_id,
3806 - 'role_restriction' => $highest_role,
3807 - 'updated_at' => current_time('mysql')
3808 - ),
3809 - array('%s', '%s', '%s')
3810 - );
3811 - } else {
3812 - // Update WordPress DB
3813 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3814 -
3815 - $wpdb->update(
3816 - $table_name,
3817 - array('role_restriction' => $highest_role),
3818 - array('source_url' => $source_url),
3819 - array('%s'),
3820 - array('%s')
3821 - );
3822 - }
3823 -}
3824 -
3825 -public function mxchat_get_public_post_types() {
3826 - // Get all public post types
3827 - $post_types = get_post_types(array('public' => true), 'objects');
3828 - $post_type_options = array();
3829 -
3830 - foreach ($post_types as $post_type) {
3831 - $post_type_options[$post_type->name] = $post_type->label;
3832 - }
3833 -
3834 - // Also include common forum/community post types that might not be marked as public
3835 - $additional_types = array(
3836 - 'topic' => 'Forum Topics (bbPress)',
3837 - 'reply' => 'Forum Replies (bbPress)',
3838 - 'forum' => 'Forums (bbPress)',
3839 - 'wpforo_topic' => 'wpForo Topics',
3840 - 'wpforo_post' => 'wpForo Posts'
3841 - );
3842 -
3843 - foreach ($additional_types as $type_name => $type_label) {
3844 - if (post_type_exists($type_name) && !isset($post_type_options[$type_name])) {
3845 - $post_type_options[$type_name] = $type_label;
3846 - }
3847 - }
3848 -
3849 - return $post_type_options;
3850 -}
3851 -
3852 -/**
3853 - * Retrieves processed content from Pinecone API
3854 - */
3855 -public function mxchat_get_pinecone_processed_content($pinecone_options) {
3856 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3857 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3858 -
3859 - if (empty($api_key) || empty($host)) {
3860 - return array();
3861 - }
3862 -
3863 - $pinecone_data = array();
3864 -
3865 - try {
3866 - // Always get fresh data from Pinecone
3867 - $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
3868 -
3869 - // Method 2: Final fallback - try stats endpoint (if available)
3870 - if (empty($pinecone_data)) {
3871 - $stats_url = "https://{$host}/describe_index_stats";
3872 -
3873 - $response = wp_remote_post($stats_url, array(
3874 - 'headers' => array(
3875 - 'Api-Key' => $api_key,
3876 - 'Content-Type' => 'application/json'
3877 - ),
3878 - 'body' => json_encode(array()),
3879 - 'timeout' => 30
3880 - ));
3881 -
3882 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
3883 - $body = wp_remote_retrieve_body($response);
3884 - $stats_data = json_decode($body, true);
3885 - }
3886 - }
3887 -
3888 - } catch (Exception $e) {
3889 - // Log error but return fresh data only
3890 - }
3891 -
3892 - return $pinecone_data;
3893 -}
3894 -public function mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
3895 - //error_log('=== DEBUG: Starting mxchat_fetch_pinecone_vectors_by_ids ===');
3896 -
3897 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3898 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3899 -
3900 - if (empty($api_key) || empty($host) || empty($vector_ids)) {
3901 - //error_log('DEBUG: Missing parameters for fetch by IDs');
3902 - return array();
3903 - }
3904 -
3905 - try {
3906 - $fetch_url = "https://{$host}/vectors/fetch";
3907 - //error_log('DEBUG: Fetch URL: ' . $fetch_url);
3908 - //error_log('DEBUG: Fetching ' . count($vector_ids) . ' vector IDs');
3909 -
3910 - // Pinecone fetch API allows fetching specific vectors by ID
3911 - $fetch_data = array(
3912 - 'ids' => array_values($vector_ids)
3913 - );
3914 -
3915 - $response = wp_remote_post($fetch_url, array(
3916 - 'headers' => array(
3917 - 'Api-Key' => $api_key,
3918 - 'Content-Type' => 'application/json'
3919 - ),
3920 - 'body' => json_encode($fetch_data),
3921 - 'timeout' => 30
3922 - ));
3923 -
3924 - if (is_wp_error($response)) {
3925 - //error_log('DEBUG: Fetch by IDs WP error: ' . $response->get_error_message());
3926 - return array();
3927 - }
3928 -
3929 - $response_code = wp_remote_retrieve_response_code($response);
3930 - //error_log('DEBUG: Fetch response code: ' . $response_code);
3931 -
3932 - if ($response_code !== 200) {
3933 - $error_body = wp_remote_retrieve_body($response);
3934 - //error_log('DEBUG: Fetch failed with body: ' . $error_body);
3935 - return array();
3936 - }
3937 -
3938 - $body = wp_remote_retrieve_body($response);
3939 - $data = json_decode($body, true);
3940 -
3941 - //error_log('DEBUG: Fetch response structure: ' . print_r(array_keys($data), true));
3942 -
3943 - if (!isset($data['vectors'])) {
3944 - //error_log('DEBUG: No vectors key in response');
3945 - return array();
3946 - }
3947 -
3948 - $processed_data = array();
3949 -
3950 - foreach ($data['vectors'] as $vector_id => $vector_data) {
3951 - $metadata = $vector_data['metadata'] ?? array();
3952 - $source_url = $metadata['source_url'] ?? '';
3953 -
3954 - if (!empty($source_url)) {
3955 - $post_id = url_to_postid($source_url);
3956 - if ($post_id) {
3957 - $created_at = $metadata['created_at'] ?? '';
3958 - $processed_date = 'Recently';
3959 -
3960 - if (!empty($created_at)) {
3961 - $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
3962 - if ($timestamp) {
3963 - $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
3964 - }
3965 - }
3966 -
3967 - $processed_data[$post_id] = array(
3968 - 'db_id' => $vector_id,
3969 - 'processed_date' => $processed_date,
3970 - 'url' => $source_url,
3971 - 'source' => 'pinecone',
3972 - 'timestamp' => $timestamp ?? current_time('timestamp')
3973 - );
3974 - }
3975 - }
3976 - }
3977 -
3978 - //error_log('DEBUG: Processed ' . count($processed_data) . ' vectors from fetch');
3979 - return $processed_data;
3980 -
3981 - } catch (Exception $e) {
3982 - //error_log('DEBUG: Exception in fetch_pinecone_vectors_by_ids: ' . $e->getMessage());
3983 - return array();
3984 - }
3985 -}
3986 -
3987 -/**
3988 - * Get embedding dimensions based on the selected model.
3989 - */
3990 -private function mxchat_get_embedding_dimensions() {
3991 - $options = get_option('mxchat_options', array());
3992 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3993 -
3994 - $model_dimensions = array(
3995 - 'text-embedding-ada-002' => 1536,
3996 - 'text-embedding-3-small' => 1536,
3997 - 'text-embedding-3-large' => 3072,
3998 - 'voyage-2' => 1024,
3999 - 'voyage-large-2' => 1536,
4000 - 'voyage-3-large' => 2048,
4001 - 'gemini-embedding-001' => 1536,
4002 - );
4003 -
4004 - if (strpos($selected_model, 'voyage-3-large') === 0) {
4005 - $custom_dimensions = $options['voyage_output_dimension'] ?? 2048;
4006 - return intval($custom_dimensions);
4007 - }
4008 -
4009 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4010 - $custom_dimensions = $options['gemini_output_dimension'] ?? 1536;
4011 - return intval($custom_dimensions);
4012 - }
4013 -
4014 - return $model_dimensions[$selected_model] ?? 1536;
4015 -}
4016 -
4017 -/**
4018 - * Scan Pinecone for processed content
4019 - */
4020 -public function mxchat_scan_pinecone_for_processed_content($pinecone_options) {
4021 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
4022 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
4023 -
4024 - if (empty($api_key) || empty($host)) {
4025 - return array();
4026 - }
4027 -
4028 - try {
4029 - // Use multiple random vectors to get better coverage
4030 - $all_matches = array();
4031 - $seen_ids = array();
4032 -
4033 - // Get correct dimensions for the configured embedding model
4034 - $dimensions = $this->mxchat_get_embedding_dimensions();
4035 -
4036 - // Try 3 different random vectors to get better coverage
4037 - for ($i = 0; $i < 3; $i++) {
4038 - $query_url = "https://{$host}/query";
4039 -
4040 - // Generate a random unit vector instead of zeros
4041 - $random_vector = array();
4042 - for ($j = 0; $j < $dimensions; $j++) {
4043 - $random_vector[] = (rand(-1000, 1000) / 1000.0);
4044 - }
4045 -
4046 - // Normalize the vector to unit length
4047 - $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
4048 - if ($magnitude > 0) {
4049 - $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
4050 - }
4051 -
4052 - $query_data = array(
4053 - 'includeMetadata' => true,
4054 - 'includeValues' => false,
4055 - 'topK' => 10000,
4056 - 'vector' => $random_vector
4057 - );
4058 -
4059 - $response = wp_remote_post($query_url, array(
4060 - 'headers' => array(
4061 - 'Api-Key' => $api_key,
4062 - 'Content-Type' => 'application/json'
4063 - ),
4064 - 'body' => json_encode($query_data),
4065 - 'timeout' => 30
4066 - ));
4067 -
4068 - if (is_wp_error($response)) {
4069 - continue;
4070 - }
4071 -
4072 - $response_code = wp_remote_retrieve_response_code($response);
4073 -
4074 - if ($response_code !== 200) {
4075 - continue;
4076 - }
4077 -
4078 - $body = wp_remote_retrieve_body($response);
4079 - $data = json_decode($body, true);
4080 -
4081 - if (isset($data['matches'])) {
4082 - foreach ($data['matches'] as $match) {
4083 - $match_id = $match['id'] ?? '';
4084 - if (!empty($match_id) && !isset($seen_ids[$match_id])) {
4085 - $all_matches[] = $match;
4086 - $seen_ids[$match_id] = true;
4087 - }
4088 - }
4089 - }
4090 - }
4091 -
4092 - // Convert matches to processed data format, grouping by URL to count chunks
4093 - $processed_data = array();
4094 - $url_chunk_counts = array();
4095 -
4096 - foreach ($all_matches as $match) {
4097 - $metadata = $match['metadata'] ?? array();
4098 - $source_url = $metadata['source_url'] ?? '';
4099 - $match_id = $match['id'] ?? '';
4100 -
4101 - if (!empty($source_url) && !empty($match_id)) {
4102 - $post_id = url_to_postid($source_url);
4103 - if ($post_id) {
4104 - // Count chunks per post_id
4105 - if (!isset($url_chunk_counts[$post_id])) {
4106 - $url_chunk_counts[$post_id] = 0;
4107 - }
4108 - $url_chunk_counts[$post_id]++;
4109 -
4110 - $created_at = $metadata['created_at'] ?? '';
4111 - $processed_date = 'Recently';
4112 -
4113 - if (!empty($created_at)) {
4114 - $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
4115 - if ($timestamp) {
4116 - $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
4117 - }
4118 - }
4119 -
4120 - // Only store if not already set, or update with newer timestamp
4121 - if (!isset($processed_data[$post_id]) ||
4122 - ($timestamp ?? 0) > ($processed_data[$post_id]['timestamp'] ?? 0)) {
4123 - $processed_data[$post_id] = array(
4124 - 'db_id' => $match_id,
4125 - 'processed_date' => $processed_date,
4126 - 'url' => $source_url,
4127 - 'source' => 'pinecone',
4128 - 'timestamp' => $timestamp ?? current_time('timestamp')
4129 - );
4130 - }
4131 - }
4132 - }
4133 - }
4134 -
4135 - // Add chunk counts to processed data
4136 - foreach ($url_chunk_counts as $post_id => $chunk_count) {
4137 - if (isset($processed_data[$post_id])) {
4138 - $processed_data[$post_id]['chunk_count'] = $chunk_count;
4139 - }
4140 - }
4141 -
4142 - return $processed_data;
4143 -
4144 - } catch (Exception $e) {
4145 - return array();
4146 - }
4147 -}
4148 -/**
4149 - * Generate embeddings from input text for MXChat with bot support
4150 - */
4151 -private function mxchat_generate_embedding($text, $bot_id = 'default') {
4152 - // Enable detailed logging for debugging
4153 - //error_log('[MXCHAT-EMBED] Starting embedding generation for bot: ' . $bot_id . '. Text length: ' . strlen($text) . ' bytes');
4154 - //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
4155 -
4156 - // Get bot-specific options
4157 - $bot_options = $this->get_bot_options($bot_id);
4158 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
4159 -
4160 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4161 - //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model);
4162 -
4163 - // Determine provider and endpoint
4164 - if (strpos($selected_model, 'voyage') === 0) {
4165 - $api_key = $options['voyage_api_key'] ?? '';
4166 - $endpoint = 'https://api.voyageai.com/v1/embeddings';
4167 - $provider_name = 'Voyage AI';
4168 - //error_log('[MXCHAT-EMBED] Using Voyage AI API for bot ' . $bot_id);
4169 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
4170 - $api_key = $options['gemini_api_key'] ?? '';
4171 - $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
4172 - $provider_name = 'Google Gemini';
4173 - //error_log('[MXCHAT-EMBED] Using Google Gemini API for bot ' . $bot_id);
4174 - } else {
4175 - $api_key = $options['api_key'] ?? '';
4176 - $endpoint = 'https://api.openai.com/v1/embeddings';
4177 - $provider_name = 'OpenAI';
4178 - //error_log('[MXCHAT-EMBED] Using OpenAI API for bot ' . $bot_id);
4179 - }
4180 -
4181 - //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
4182 -
4183 - if (empty($api_key)) {
4184 - $error_message = sprintf('Missing %s API key for bot %s. Please configure your API key in the bot settings.', $provider_name, $bot_id);
4185 - //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
4186 - return $error_message;
4187 - }
4188 -
4189 - // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
4190 - $estimated_tokens = ceil(str_word_count($text) / 0.75);
4191 - //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
4192 -
4193 - if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
4194 - //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
4195 - // Consider truncating text here
4196 - }
4197 -
4198 - // Prepare request body based on provider
4199 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4200 - // Gemini API format
4201 - $request_body = array(
4202 - 'model' => 'models/' . $selected_model,
4203 - 'content' => array(
4204 - 'parts' => array(
4205 - array('text' => $text)
4206 - )
4207 - )
4208 - );
4209 -
4210 - // Set output dimensionality to 1536 for consistency with other models
4211 - $request_body['outputDimensionality'] = 1536;
4212 - } else {
4213 - // OpenAI/Voyage API format
4214 - $request_body = array(
4215 - 'model' => $selected_model,
4216 - 'input' => $text
4217 - );
4218 -
4219 - // Add output_dimension for voyage-3-large model
4220 - if ($selected_model === 'voyage-3-large') {
4221 - $request_body['output_dimension'] = 2048;
4222 - }
4223 - }
4224 -
4225 - //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
4226 -
4227 - // Prepare headers based on provider
4228 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4229 - // Gemini uses API key as query parameter
4230 - $endpoint .= '?key=' . $api_key;
4231 - $headers = array(
4232 - 'Content-Type' => 'application/json'
4233 - );
4234 - } else {
4235 - // OpenAI/Voyage use Bearer token
4236 - $headers = array(
4237 - 'Authorization' => 'Bearer ' . $api_key,
4238 - 'Content-Type' => 'application/json'
4239 - );
4240 - }
4241 -
4242 - // Make API request
4243 - //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
4244 - $response = wp_remote_post($endpoint, array(
4245 - 'body' => wp_json_encode($request_body),
4246 - 'headers' => $headers,
4247 - 'timeout' => 60 // Increased timeout for large inputs
4248 - ));
4249 -
4250 - // Handle wp_remote_post errors
4251 - if (is_wp_error($response)) {
4252 - $error_message = $response->get_error_message();
4253 - //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
4254 - return 'Connection error: ' . $error_message;
4255 - }
4256 -
4257 - // Get and check HTTP response code
4258 - $http_code = wp_remote_retrieve_response_code($response);
4259 - //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
4260 -
4261 - if ($http_code !== 200) {
4262 - $error_body = wp_remote_retrieve_body($response);
4263 - //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
4264 -
4265 - // Try to parse error for more details
4266 - $error_json = json_decode($error_body, true);
4267 - if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
4268 - $error_type = $error_json['error']['type'] ?? 'unknown';
4269 - $error_message = $error_json['error']['message'] ?? 'No message';
4270 - //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
4271 - //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
4272 -
4273 - // Customize error message for common API errors
4274 - if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
4275 - $error_message = sprintf('Invalid %s API key for bot %s. Please check your API key in the bot settings.', $provider_name, $bot_id);
4276 - } elseif ($error_type === 'authentication_error') {
4277 - $error_message = sprintf('%s authentication failed for bot %s. Please verify your API key in the bot settings.', $provider_name, $bot_id);
4278 - }
4279 -
4280 - //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
4281 - return $error_message;
4282 - }
4283 -
4284 - $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding for bot %s", $http_code, $bot_id);
4285 - //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
4286 - return $error_message;
4287 - }
4288 -
4289 - // Parse response body
4290 - $response_body = wp_remote_retrieve_body($response);
4291 - //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
4292 -
4293 - $response_data = json_decode($response_body, true);
4294 -
4295 - if (json_last_error() !== JSON_ERROR_NONE) {
4296 - $error = json_last_error_msg();
4297 - //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
4298 - //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
4299 - return "Failed to parse API response: $error";
4300 - }
4301 -
4302 - // Handle different response formats based on provider
4303 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4304 - // Gemini API response format
4305 - if (isset($response_data['embedding']['values'])) {
4306 - $embedding_dimensions = count($response_data['embedding']['values']);
4307 - //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
4308 -
4309 - // Check if embedding dimensions are as expected (should be 1536)
4310 - if ($embedding_dimensions !== 1536) {
4311 - //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
4312 - }
4313 -
4314 - return $response_data['embedding']['values'];
4315 - } else {
4316 - //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
4317 - //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
4318 -
4319 - if (isset($response_data['error'])) {
4320 - $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
4321 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4322 - return $error_message;
4323 - }
4324 -
4325 - $error_message = "Invalid Gemini API response format: No embedding found";
4326 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4327 - return $error_message;
4328 - }
4329 - } else {
4330 - // OpenAI/Voyage API response format
4331 - if (isset($response_data['data'][0]['embedding'])) {
4332 - $embedding_dimensions = count($response_data['data'][0]['embedding']);
4333 - //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
4334 -
4335 - // Check if embedding dimensions are as expected
4336 - if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
4337 - ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
4338 - //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
4339 - }
4340 -
4341 - return $response_data['data'][0]['embedding'];
4342 - } else {
4343 - //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
4344 - //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
4345 -
4346 - if (isset($response_data['error'])) {
4347 - $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
4348 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4349 - return $error_message;
4350 - }
4351 -
4352 - $error_message = "Invalid API response format: No embedding found";
4353 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4354 - return $error_message;
4355 - }
4356 - }
4357 -}
4358 -
4359 -/**
4360 - * Get bot-specific options for multi-bot functionality
4361 - * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
4362 - */
4363 -private function get_bot_options($bot_id = 'default') {
4364 - //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
4365 -
4366 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
4367 - //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
4368 - return array();
4369 - }
4370 -
4371 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
4372 -
4373 - if (!empty($bot_options)) {
4374 - //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
4375 - if (isset($bot_options['similarity_threshold'])) {
4376 - //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
4377 - }
4378 - }
4379 -
4380 - return is_array($bot_options) ? $bot_options : array();
4381 -}
4382 -
4383 -/**
4384 - * Get bot-specific Pinecone configuration
4385 - * Used in the knowledge retrieval functions
4386 - */
4387 -// Also add debugging to your get_bot_pinecone_config function
4388 -private function get_bot_pinecone_config($bot_id = 'default') {
4389 - //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
4390 -
4391 - // If default bot or multi-bot add-on not active, use default Pinecone config
4392 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
4393 - //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
4394 - $addon_options = get_option('mxchat_pinecone_addon_options', array());
4395 - $config = array(
4396 - 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
4397 - 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
4398 - 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
4399 - 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
4400 - );
4401 - //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
4402 - return $config;
4403 - }
4404 -
4405 - //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
4406 -
4407 - // Hook for multi-bot add-on to provide bot-specific Pinecone config
4408 - $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
4409 -
4410 - if (!empty($bot_pinecone_config)) {
4411 - //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
4412 - //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
4413 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
4414 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
4415 - } else {
4416 - //error_log("MXCHAT DEBUG: Filter returned empty config!");
4417 - }
4418 -
4419 - return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
4420 -}
4421 -
4422 -
4423 -public function mxchat_ajax_dismiss_completed_status() {
4424 - try {
4425 - // Verify the request
4426 - check_ajax_referer('mxchat_status_nonce', 'nonce');
4427 -
4428 - if (!current_user_can('manage_options')) {
4429 - wp_send_json_error('Unauthorized access');
4430 - exit;
4431 - }
4432 -
4433 - $card_type = isset($_POST['card_type']) ? sanitize_text_field($_POST['card_type']) : '';
4434 -
4435 - if ($card_type === 'pdf') {
4436 - // Clear PDF status
4437 - $pdf_url = get_transient('mxchat_last_pdf_url');
4438 - if ($pdf_url) {
4439 - delete_transient('mxchat_pdf_status_' . md5($pdf_url));
4440 - delete_transient('mxchat_last_pdf_url');
4441 - }
4442 - } elseif ($card_type === 'sitemap') {
4443 - // Clear sitemap status
4444 - $sitemap_url = get_transient('mxchat_last_sitemap_url');
4445 - if ($sitemap_url) {
4446 - delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
4447 - delete_transient('mxchat_last_sitemap_url');
4448 - }
4449 - }
4450 -
4451 - wp_send_json_success(array('message' => 'Status dismissed successfully'));
4452 -
4453 - } catch (Exception $e) {
4454 - wp_send_json_error(array('message' => 'Error dismissing status: ' . $e->getMessage()));
4455 - }
4456 -}
4457 -
4458 -/**
4459 - * Render completed status cards on page load
4460 - * This ensures completed processing status persists through page refreshes
4461 - */
4462 -public function mxchat_render_completed_status_cards() {
4463 - $output = '';
4464 -
4465 - // Check for completed PDF status
4466 - $pdf_url = get_transient('mxchat_last_pdf_url');
4467 - if ($pdf_url) {
4468 - $pdf_status = $this->mxchat_get_pdf_processing_status($pdf_url);
4469 - if ($pdf_status && ($pdf_status['status'] === 'complete' || $pdf_status['status'] === 'error')) {
4470 - $output .= $this->mxchat_render_pdf_status_card($pdf_status, $pdf_url);
4471 - }
4472 - }
4473 -
4474 - // Check for completed sitemap status
4475 - $sitemap_url = get_transient('mxchat_last_sitemap_url');
4476 - if ($sitemap_url) {
4477 - $sitemap_status = $this->mxchat_get_sitemap_processing_status($sitemap_url);
4478 - if ($sitemap_status && ($sitemap_status['status'] === 'complete' || $sitemap_status['status'] === 'error')) {
4479 - $output .= $this->mxchat_render_sitemap_status_card($sitemap_status, $sitemap_url);
4480 - }
4481 - }
4482 -
4483 - return $output;
4484 -}
4485 -
4486 -/**
4487 - * Render PDF status card HTML
4488 - */
4489 -private function mxchat_render_pdf_status_card($status, $pdf_url) {
4490 - $html = '<div class="mxchat-status-card" data-card-type="pdf">';
4491 - $html .= '<div class="mxchat-status-header">';
4492 - $html .= '<h4>' . esc_html__('PDF Processing Status', 'mxchat') . '</h4>';
4493 -
4494 - // Add dismiss button for completed status
4495 - if ($status['status'] === 'complete' || $status['status'] === 'error') {
4496 - $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
4497 - }
4498 -
4499 - // Process Batch button for processing status
4500 - if ($status['status'] === 'processing') {
4501 - $html .= '<button type="button" class="mxchat-manual-batch-btn"
4502 - data-process-type="pdf"
4503 - data-url="' . esc_attr($pdf_url) . '">
4504 - ' . esc_html__('Process Batch', 'mxchat') . '</button>';
4505 - }
4506 -
4507 - // Add status badges
4508 - if ($status['status'] === 'error') {
4509 - $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
4510 - } elseif ($status['status'] === 'complete') {
4511 - if ($status['failed_pages'] > 0) {
4512 - $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
4513 - sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_pages']) . '</span>';
4514 - } else {
4515 - $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
4516 - }
4517 - }
4518 -
4519 - $html .= '</div>'; // End header
4520 -
4521 - // Progress bar
4522 - $html .= '<div class="mxchat-progress-bar">';
4523 - $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
4524 - $html .= '</div>';
4525 -
4526 - // Status details
4527 - $html .= '<div class="mxchat-status-details">';
4528 - $html .= '<p>' . sprintf(
4529 - esc_html__('Progress: %d of %d pages (%d%%)', 'mxchat'),
4530 - $status['processed_pages'],
4531 - $status['total_pages'],
4532 - $status['percentage']
4533 - ) . '</p>';
4534 -
4535 - // Show failed pages count if any
4536 - if ($status['failed_pages'] > 0) {
4537 - $html .= '<p><strong>' . esc_html__('Failed pages:', 'mxchat') . '</strong> ' . esc_html($status['failed_pages']) . '</p>';
4538 - }
4539 -
4540 - $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
4541 - $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
4542 -
4543 - // Add completion summary if available AND it's an array
4544 - if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
4545 - $summary = $status['completion_summary'];
4546 - $html .= '<div class="mxchat-completion-summary">';
4547 - $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
4548 - $html .= '<p><strong>' . esc_html__('Total Pages:', 'mxchat') . '</strong> ' . esc_html($summary['total_pages']) . '</p>';
4549 - $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_pages']) . '</p>';
4550 - $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_pages']) . '</p>';
4551 - $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
4552 - $html .= '</div>';
4553 - }
4554 -
4555 - // Add failed pages list if any AND it's an array
4556 - if (isset($status['failed_pages_list']) && is_array($status['failed_pages_list']) && !empty($status['failed_pages_list'])) {
4557 - $html .= $this->mxchat_render_failed_pages_list($status['failed_pages_list']);
4558 - }
4559 -
4560 - // Add error message if any
4561 - if (isset($status['error']) && !empty($status['error'])) {
4562 - $html .= '<div class="mxchat-error-notice">';
4563 - $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
4564 - $html .= '</div>';
4565 - }
4566 -
4567 - $html .= '</div>'; // End details
4568 - $html .= '</div>'; // End card
4569 -
4570 - return $html;
4571 -}
4572 -/**
4573 - * Render sitemap status card HTML
4574 - */
4575 -private function mxchat_render_sitemap_status_card($status, $sitemap_url) {
4576 - $html = '<div class="mxchat-status-card" data-card-type="sitemap">';
4577 - $html .= '<div class="mxchat-status-header">';
4578 - $html .= '<h4>' . esc_html__('Sitemap Processing Status', 'mxchat') . '</h4>';
4579 -
4580 - // Add dismiss button for completed status
4581 - if ($status['status'] === 'complete' || $status['status'] === 'error') {
4582 - $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
4583 - }
4584 -
4585 - // Process Batch button for processing status
4586 - if ($status['status'] === 'processing') {
4587 - $html .= '<button type="button" class="mxchat-manual-batch-btn"
4588 - data-process-type="sitemap"
4589 - data-url="' . esc_attr($sitemap_url) . '">
4590 - ' . esc_html__('Process Batch', 'mxchat') . '</button>';
4591 - }
4592 -
4593 - // Add status badges
4594 - if ($status['status'] === 'error') {
4595 - $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
4596 - } elseif ($status['status'] === 'complete') {
4597 - if ($status['failed_urls'] > 0) {
4598 - $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
4599 - sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_urls']) . '</span>';
4600 - } else {
4601 - $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
4602 - }
4603 - }
4604 -
4605 - $html .= '</div>'; // End header
4606 -
4607 - // Progress bar
4608 - $html .= '<div class="mxchat-progress-bar">';
4609 - $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
4610 - $html .= '</div>';
4611 -
4612 - // Status details
4613 - $html .= '<div class="mxchat-status-details">';
4614 - $html .= '<p>' . sprintf(
4615 - esc_html__('Progress: %d of %d URLs (%d%%)', 'mxchat'),
4616 - $status['processed_urls'],
4617 - $status['total_urls'],
4618 - $status['percentage']
4619 - ) . '</p>';
4620 -
4621 - // Show failed URLs count if any
4622 - if ($status['failed_urls'] > 0) {
4623 - $html .= '<p><strong>' . esc_html__('Failed URLs:', 'mxchat') . '</strong> ' . esc_html($status['failed_urls']) . '</p>';
4624 - }
4625 -
4626 - $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
4627 - $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
4628 -
4629 - // Add completion summary if available AND it's an array
4630 - if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
4631 - $summary = $status['completion_summary'];
4632 - $html .= '<div class="mxchat-completion-summary">';
4633 - $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
4634 - $html .= '<p><strong>' . esc_html__('Total URLs:', 'mxchat') . '</strong> ' . esc_html($summary['total_urls']) . '</p>';
4635 - $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_urls']) . '</p>';
4636 - $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_urls']) . '</p>';
4637 - $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
4638 - $html .= '</div>';
4639 - }
4640 -
4641 - // Add error messages if any (but not the failed URLs list)
4642 - if (!empty($status['error']) || !empty($status['last_error'])) {
4643 - $html .= '<div class="mxchat-error-notice">';
4644 -
4645 - if (!empty($status['error'])) {
4646 - $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
4647 - }
4648 -
4649 - if (!empty($status['last_error'])) {
4650 - $html .= '<p class="last-error">' . esc_html__('Last error:', 'mxchat') . ' ' . esc_html($status['last_error']) . '</p>';
4651 - }
4652 -
4653 - $html .= '</div>';
4654 - }
4655 -
4656 - $html .= '</div>'; // End details
4657 - $html .= '</div>'; // End card
4658 -
4659 - return $html;
4660 -}
4661 -
4662 -
4663 -/**
4664 - * Render failed pages list
4665 - */
4666 -private function mxchat_render_failed_pages_list($failed_pages_list) {
4667 - // Validate that $failed_pages_list is an array and not empty
4668 - if (!is_array($failed_pages_list) || empty($failed_pages_list)) {
4669 - return '';
4670 - }
4671 -
4672 - $html = '<div class="mxchat-error-notice">';
4673 - $html .= '<div class="mxchat-failed-pages-container">';
4674 - $html .= '<h5>' . sprintf(esc_html__('Failed Pages (%d)', 'mxchat'), count($failed_pages_list)) . '</h5>';
4675 - $html .= '<details>';
4676 - $html .= '<summary>' . esc_html__('Show Failed Pages', 'mxchat') . '</summary>';
4677 - $html .= '<div class="mxchat-failed-pages-list">';
4678 -
4679 - // Create table for failed pages
4680 - $html .= '<table class="widefat striped">';
4681 - $html .= '<thead><tr>';
4682 - $html .= '<th>' . esc_html__('Page', 'mxchat') . '</th>';
4683 - $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
4684 - $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
4685 - $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
4686 - $html .= '</tr></thead><tbody>';
4687 -
4688 - // Sort failed pages by most recent
4689 - $sorted_failed_pages = $failed_pages_list;
4690 - usort($sorted_failed_pages, function($a, $b) {
4691 - return ($b['time'] ?? 0) - ($a['time'] ?? 0);
4692 - });
4693 -
4694 - foreach ($sorted_failed_pages as $item) {
4695 - // Ensure $item is an array before accessing its elements
4696 - if (!is_array($item)) {
4697 - continue;
4698 - }
4699 -
4700 - $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
4701 - $html .= '<tr>';
4702 - $html .= '<td>' . esc_html__('Page', 'mxchat') . ' ' . esc_html($item['page'] ?? 'Unknown') . '</td>';
4703 - $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
4704 - $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
4705 - $html .= '<td>' . esc_html($time_ago) . '</td>';
4706 - $html .= '</tr>';
4707 - }
4708 -
4709 - $html .= '</tbody></table>';
4710 - $html .= '</div></details></div></div>';
4711 -
4712 - return $html;
4713 -}
4714 -
4715 -/**
4716 - * Render failed URLs list
4717 - */
4718 -private function mxchat_render_failed_urls_list($failed_urls_list) {
4719 - // Validate that $failed_urls_list is an array and not empty
4720 - if (!is_array($failed_urls_list) || empty($failed_urls_list)) {
4721 - return '';
4722 - }
4723 -
4724 - $html = '<div class="mxchat-failed-urls-container">';
4725 - $html .= '<h5>' . sprintf(esc_html__('Failed URLs (%d)', 'mxchat'), count($failed_urls_list)) . '</h5>';
4726 - $html .= '<details>';
4727 - $html .= '<summary>' . esc_html__('Show Failed URLs', 'mxchat') . '</summary>';
4728 - $html .= '<div class="mxchat-failed-urls-list">';
4729 -
4730 - // Create table for failed URLs
4731 - $html .= '<table class="widefat striped">';
4732 - $html .= '<thead><tr>';
4733 - $html .= '<th>' . esc_html__('URL', 'mxchat') . '</th>';
4734 - $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
4735 - $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
4736 - $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
4737 - $html .= '</tr></thead><tbody>';
4738 -
4739 - // Sort failed URLs by most recent
4740 - $sorted_failed_urls = $failed_urls_list;
4741 - usort($sorted_failed_urls, function($a, $b) {
4742 - return ($b['time'] ?? 0) - ($a['time'] ?? 0);
4743 - });
4744 -
4745 - // Show up to 50 failed URLs
4746 - $display_urls = array_slice($sorted_failed_urls, 0, 50);
4747 -
4748 - foreach ($display_urls as $item) {
4749 - // Ensure $item is an array before accessing its elements
4750 - if (!is_array($item)) {
4751 - continue;
4752 - }
4753 -
4754 - $url = $item['url'] ?? '';
4755 - $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
4756 -
4757 - // Truncate URL for display
4758 - $display_url = strlen($url) > 50 ? substr($url, 0, 47) . '...' : $url;
4759 -
4760 - $html .= '<tr>';
4761 - $html .= '<td style="word-break: break-all;">';
4762 - if (!empty($url)) {
4763 - $html .= '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($display_url) . '</a>';
4764 - } else {
4765 - $html .= esc_html__('Unknown URL', 'mxchat');
4766 - }
4767 - $html .= '</td>';
4768 - $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
4769 - $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
4770 - $html .= '<td>' . esc_html($time_ago) . '</td>';
4771 - $html .= '</tr>';
4772 - }
4773 -
4774 - $html .= '</tbody></table>';
4775 -
4776 - if (count($failed_urls_list) > 50) {
4777 - $html .= '<div class="mxchat-failed-urls-more">+ ' .
4778 - (count($failed_urls_list) - 50) .
4779 - ' ' . esc_html__('more failed URLs not shown', 'mxchat') . '</div>';
4780 - }
4781 -
4782 - $html .= '</div></details></div>';
4783 -
4784 - return $html;
4785 -}
4786 -
4787 -/**
4788 - * Get all ACF fields for a specific post, excluding any fields the user has disabled
4789 - */
4790 -public function mxchat_get_acf_fields_for_post($post_id) {
4791 - if (!function_exists('get_fields')) {
4792 - return array();
4793 - }
4794 -
4795 - $fields = get_fields($post_id);
4796 - if (!$fields || !is_array($fields)) {
4797 - return array();
4798 - }
4799 -
4800 - // Get excluded fields from settings
4801 - $excluded_fields = get_option('mxchat_acf_excluded_fields', array());
4802 - if (!empty($excluded_fields) && is_array($excluded_fields)) {
4803 - foreach ($excluded_fields as $excluded_field) {
4804 - if (isset($fields[$excluded_field])) {
4805 - unset($fields[$excluded_field]);
4806 - }
4807 - }
4808 - }
4809 -
4810 - return $fields;
4811 -}
4812 -
4813 -/**
4814 - * Get all registered ACF field groups and their fields for the settings UI
4815 - */
4816 -public function mxchat_get_all_acf_fields() {
4817 - if (!function_exists('acf_get_field_groups') || !function_exists('acf_get_fields')) {
4818 - return array();
4819 - }
4820 -
4821 - $all_fields = array();
4822 - $field_groups = acf_get_field_groups();
4823 -
4824 - if (!empty($field_groups)) {
4825 - foreach ($field_groups as $group) {
4826 - $group_fields = acf_get_fields($group['key']);
4827 - if (!empty($group_fields)) {
4828 - $all_fields[$group['title']] = array();
4829 - foreach ($group_fields as $field) {
4830 - $all_fields[$group['title']][] = array(
4831 - 'name' => $field['name'],
4832 - 'label' => $field['label'],
4833 - 'type' => $field['type']
4834 - );
4835 - }
4836 - }
4837 - }
4838 - }
4839 -
4840 - return $all_fields;
4841 -}
4842 -
4843 -/**
4844 - * Get whitelisted custom post meta for a given post
4845 - * This allows non-ACF meta fields (like OptionTree, theme meta boxes) to be included in embeddings
4846 - */
4847 -public function mxchat_get_whitelisted_post_meta($post_id) {
4848 - $whitelist = get_option('mxchat_custom_meta_whitelist', '');
4849 -
4850 - if (empty($whitelist)) {
4851 - return array();
4852 - }
4853 -
4854 - // Parse the whitelist - one meta key per line
4855 - $meta_keys = array_filter(array_map('trim', explode("\n", $whitelist)));
4856 -
4857 - if (empty($meta_keys)) {
4858 - return array();
4859 - }
4860 -
4861 - $result = array();
4862 -
4863 - foreach ($meta_keys as $key) {
4864 - // Skip empty keys
4865 - if (empty($key)) {
4866 - continue;
4867 - }
4868 -
4869 - $value = get_post_meta($post_id, $key, true);
4870 -
4871 - // Only include non-empty string values
4872 - if (!empty($value) && is_string($value)) {
4873 - $result[$key] = $value;
4874 - } elseif (!empty($value) && is_array($value)) {
4875 - // Handle array values by joining them
4876 - $flat_value = $this->mxchat_flatten_meta_array($value);
4877 - if (!empty($flat_value)) {
4878 - $result[$key] = $flat_value;
4879 - }
4880 - }
4881 - }
4882 -
4883 - return $result;
4884 -}
4885 -
4886 -/**
4887 - * Flatten array meta values into a readable string
4888 - */
4889 -private function mxchat_flatten_meta_array($array, $depth = 0) {
4890 - if ($depth > 3) {
4891 - return ''; // Prevent infinite recursion
4892 - }
4893 -
4894 - $parts = array();
4895 -
4896 - foreach ($array as $key => $value) {
4897 - if (is_string($value) && !empty($value)) {
4898 - $parts[] = $value;
4899 - } elseif (is_array($value)) {
4900 - $nested = $this->mxchat_flatten_meta_array($value, $depth + 1);
4901 - if (!empty($nested)) {
4902 - $parts[] = $nested;
4903 - }
4904 - }
4905 - }
4906 -
4907 - return implode(', ', $parts);
4908 -}
4909 -
4910 -/**
4911 - * Format ACF field values for content extraction
4912 - */
4913 -public function mxchat_format_acf_field_value($value, $field_name = '', $post_id = 0) {
4914 - if (empty($value)) {
4915 - return '';
4916 - }
4917 -
4918 - // Handle WP_Post objects first (THIS IS THE KEY FIX)
4919 - if ($value instanceof WP_Post) {
4920 - return $value->post_title ?: '';
4921 - }
4922 -
4923 - // Handle other WP objects
4924 - if (is_object($value)) {
4925 - if (isset($value->post_title)) {
4926 - return $value->post_title;
4927 - } elseif (isset($value->display_name)) {
4928 - return $value->display_name;
4929 - } elseif (isset($value->name)) {
4930 - return $value->name;
4931 - } elseif (method_exists($value, '__toString')) {
4932 - try {
4933 - return (string) $value;
4934 - } catch (Exception $e) {
4935 - return '';
4936 - }
4937 - }
4938 - // For any other objects, return empty string
4939 - return '';
4940 - }
4941 -
4942 - // Handle different ACF field types
4943 - if (is_array($value)) {
4944 - // Check if it's an image/file field
4945 - if (isset($value['url'])) {
4946 - // Image field - return alt text, title, or caption
4947 - if (!empty($value['alt'])) {
4948 - return $value['alt'];
4949 - } elseif (!empty($value['title'])) {
4950 - return $value['title'];
4951 - } elseif (!empty($value['caption'])) {
4952 - return $value['caption'];
4953 - } else {
4954 - return ''; // Don't include just the URL
4955 - }
4956 - }
4957 -
4958 - // Check if it's a post object or relationship field
4959 - if (isset($value['post_title'])) {
4960 - return $value['post_title'];
4961 - }
4962 -
4963 - // Check if it's a user field
4964 - if (isset($value['display_name'])) {
4965 - return $value['display_name'];
4966 - }
4967 -
4968 - // Check if it's a taxonomy term
4969 - if (isset($value['name']) && isset($value['taxonomy'])) {
4970 - return $value['name'];
4971 - }
4972 -
4973 - // Check if it's a select field with label
4974 - if (isset($value['label'])) {
4975 - return $value['label'];
4976 - }
4977 -
4978 - // Check for repeater field or flexible content
4979 - if (is_numeric(key($value))) {
4980 - $sub_values = array();
4981 - foreach ($value as $sub_item) {
4982 - if (is_array($sub_item)) {
4983 - // For repeater/flexible content, extract text values
4984 - $sub_text = $this->mxchat_extract_text_from_acf_array($sub_item);
4985 - if (!empty($sub_text)) {
4986 - $sub_values[] = $sub_text;
4987 - }
4988 - } elseif ($sub_item instanceof WP_Post) {
4989 - // Handle WP_Post objects in arrays
4990 - $sub_values[] = $sub_item->post_title ?: '';
4991 - } else {
4992 - $sub_values[] = (string) $sub_item;
4993 - }
4994 - }
4995 - return implode(', ', array_filter($sub_values));
4996 - }
4997 -
4998 - // For other arrays, try to extract meaningful text
4999 - $text_values = array();
5000 - foreach ($value as $key => $val) {
5001 - if (is_string($val) && !empty(trim($val))) {
5002 - $text_values[] = trim($val);
5003 - } elseif ($val instanceof WP_Post) {
5004 - // Handle WP_Post objects in associative arrays
5005 - $text_values[] = $val->post_title ?: '';
5006 - } elseif (is_array($val) && isset($val['post_title'])) {
5007 - $text_values[] = $val['post_title'];
5008 - } elseif (is_array($val) && isset($val['name'])) {
5009 - $text_values[] = $val['name'];
5010 - }
5011 - }
5012 -
5013 - return implode(', ', array_filter($text_values));
5014 - }
5015 -
5016 - // Handle boolean values
5017 - if (is_bool($value)) {
5018 - return $value ? 'Yes' : 'No';
5019 - }
5020 -
5021 - // Handle numeric values
5022 - if (is_numeric($value)) {
5023 - return (string) $value;
5024 - }
5025 -
5026 - // Handle string values
5027 - if (is_string($value)) {
5028 - return trim($value);
5029 - }
5030 -
5031 - // For anything else that we can't handle, return empty string
5032 - // This prevents the "Object could not be converted to string" error
5033 - return '';
5034 -}
5035 -
5036 -/**
5037 - * Extract text from complex ACF array structures
5038 - */
5039 -private function mxchat_extract_text_from_acf_array($array) {
5040 - if (!is_array($array)) {
5041 - return '';
5042 - }
5043 -
5044 - $text_parts = array();
5045 -
5046 - foreach ($array as $key => $value) {
5047 - if (is_string($value) && !empty(trim($value))) {
5048 - // Skip keys that are likely to be IDs or technical values
5049 - if (!is_numeric($value) || strlen($value) > 10) {
5050 - $text_parts[] = trim($value);
5051 - }
5052 - } elseif ($value instanceof WP_Post) {
5053 - // Handle WP_Post objects
5054 - $text_parts[] = $value->post_title ?: '';
5055 - } elseif (is_array($value)) {
5056 - if (isset($value['post_title'])) {
5057 - $text_parts[] = $value['post_title'];
5058 - } elseif (isset($value['name'])) {
5059 - $text_parts[] = $value['name'];
5060 - } elseif (isset($value['label'])) {
5061 - $text_parts[] = $value['label'];
5062 - }
5063 - } elseif (is_object($value)) {
5064 - // Handle other objects safely
5065 - if (isset($value->post_title)) {
5066 - $text_parts[] = $value->post_title;
5067 - } elseif (isset($value->name)) {
5068 - $text_parts[] = $value->name;
5069 - } elseif (isset($value->display_name)) {
5070 - $text_parts[] = $value->display_name;
5071 - }
5072 - }
5073 - }
5074 -
5075 - return implode(', ', array_filter($text_parts));
5076 -}
5077 -
5078 -/**
5079 - * Handle ACF save - fires after ACF fields are saved
5080 - * This ensures ACF field data is available when syncing to knowledge base
5081 - */
5082 -public function mxchat_handle_acf_save($post_id) {
5083 - // Skip if not a valid post
5084 - if (!$post_id || $post_id === 'options') {
5085 - return;
5086 - }
5087 -
5088 - // Skip autosaves and revisions
5089 - if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
5090 - return;
5091 - }
5092 -
5093 - $post = get_post($post_id);
5094 - if (!$post) {
5095 - return;
5096 - }
5097 -
5098 - $post_type = $post->post_type;
5099 -
5100 - // Check if sync is enabled for this post type
5101 - $should_sync = false;
5102 -
5103 - if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
5104 - $should_sync = true;
5105 - } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
5106 - $should_sync = true;
5107 - } else if ($post_type === 'product' && class_exists('WooCommerce')) {
5108 - // WooCommerce products - check if WooCommerce integration is enabled
5109 - $options = get_option('mxchat_options', array());
5110 - if (isset($options['enable_woocommerce_integration']) &&
5111 - ($options['enable_woocommerce_integration'] === '1' || $options['enable_woocommerce_integration'] === 'on')) {
5112 - $should_sync = true;
5113 - }
5114 - } else {
5115 - // Check custom post types
5116 - $option_name = 'mxchat_auto_sync_' . $post_type;
5117 - if (get_option($option_name) === '1') {
5118 - $should_sync = true;
5119 - }
5120 - }
5121 -
5122 - if (!$should_sync) {
5123 - return;
5124 - }
5125 -
5126 - // Only process published posts
5127 - if ($post->post_status !== 'publish') {
5128 - return;
5129 - }
5130 -
5131 - // Check if this post has any ACF fields - if not, no need to re-sync
5132 - $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
5133 - if (empty($acf_fields)) {
5134 - return;
5135 - }
5136 -
5137 - // Use a transient to prevent duplicate processing (post_updated may have already run)
5138 - $transient_key = 'mxchat_acf_synced_' . $post_id;
5139 - if (get_transient($transient_key)) {
5140 - return;
5141 - }
5142 - set_transient($transient_key, true, 60); // Prevent re-processing for 60 seconds
5143 -
5144 - // Re-run the sync with ACF data now available
5145 - // We pass $update=true since this is effectively an update with ACF data
5146 - $this->mxchat_handle_post_update($post_id, $post, true);
5147 -}
5148 -
5149 -public function mxchat_handle_post_update($post_id, $post, $update) {
5150 - // Basic validation checks
5151 - if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
5152 - return;
5153 - }
5154 -
5155 - $post_type = $post->post_type;
5156 -
5157 - // Check if sync is enabled for this post type
5158 - $should_sync = false;
5159 -
5160 - // Check built-in post types first
5161 - if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
5162 - $should_sync = true;
5163 - } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
5164 - $should_sync = true;
5165 - } else {
5166 - // Check custom post types
5167 - $option_name = 'mxchat_auto_sync_' . $post_type;
5168 - if (get_option($option_name) === '1') {
5169 - $should_sync = true;
5170 - }
5171 - }
5172 -
5173 - if (!$should_sync) {
5174 - return;
5175 - }
5176 -
5177 - // Check if we have stored the previous status and URL in our transients
5178 - $previous_status_key = 'mxchat_prev_status_' . $post_id;
5179 - $previous_status = get_transient($previous_status_key);
5180 -
5181 - $previous_url_key = 'mxchat_prev_url_' . $post_id;
5182 - $previous_url = get_transient($previous_url_key);
5183 -
5184 - // If the post was previously published but is now not published, remove from knowledge base
5185 - if ($previous_status === 'publish' && $post->post_status !== 'publish') {
5186 - // Use the stored URL from when it was published, or fall back to current permalink
5187 - $source_url = $previous_url ?: get_permalink($post_id);
5188 -
5189 - if ($source_url) {
5190 - // Check if Pinecone is enabled
5191 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5192 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5193 -
5194 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
5195 - // Delete from Pinecone
5196 - $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
5197 - } else {
5198 - // Delete from WordPress DB
5199 - global $wpdb;
5200 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5201 -
5202 - $result = $wpdb->delete(
5203 - $table_name,
5204 - array('source_url' => $source_url),
5205 - array('%s')
5206 - );
5207 - }
5208 - }
5209 -
5210 - // Clean up the transients and exit early
5211 - delete_transient($previous_status_key);
5212 - delete_transient($previous_url_key);
5213 - return;
5214 - }
5215 -
5216 - // Store the current status for next time (if this is an update)
5217 - if ($update) {
5218 - set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
5219 -
5220 - // If the post is currently published, also store its URL
5221 - if ($post->post_status === 'publish') {
5222 - $current_url = get_permalink($post_id);
5223 - set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
5224 - }
5225 - }
5226 -
5227 - // Only process currently published content for adding/updating
5228 - if ($post->post_status === 'publish') {
5229 - // Get the source URL
5230 - $source_url = get_permalink($post_id);
5231 -
5232 - // Get content with proper formatting (matching ajax_mxchat_process_selected_content)
5233 - $title = get_the_title($post_id);
5234 - $content = get_post_field('post_content', $post_id);
5235 - $excerpt = get_post_field('post_excerpt', $post_id);
5236 -
5237 - // Remove shortcode tags but preserve content inside them
5238 - $content = $this->strip_shortcode_tags_preserve_content($content);
5239 - $excerpt = $this->strip_shortcode_tags_preserve_content($excerpt);
5240 -
5241 - // Strip tags but preserve structure (don't use 'the_content' filter as it may re-add shortcodes)
5242 - $content = wp_strip_all_tags($content);
5243 -
5244 - // Combine title, short description (if exists), and content
5245 - $final_content = $title . "\n\n";
5246 -
5247 - // Add short description if it exists (WooCommerce products use post_excerpt for short description)
5248 - if (!empty($excerpt)) {
5249 - $final_content .= "Short Description: " . wp_strip_all_tags($excerpt) . "\n\n";
5250 - }
5251 -
5252 - $final_content .= $content;
5253 -
5254 - // For WooCommerce products, include pricing and product details
5255 - if ($post_type === 'product' && class_exists('WooCommerce')) {
5256 - $product = wc_get_product($post_id);
5257 -
5258 - if ($product) {
5259 - // Get pricing information
5260 - $regular_price = $product->get_regular_price();
5261 - $sale_price = $product->get_sale_price();
5262 - $price = $product->get_price();
5263 - $sku = $product->get_sku();
5264 -
5265 - // Get currency symbol
5266 - $currency_symbol = get_woocommerce_currency_symbol();
5267 -
5268 - // Add pricing information
5269 - $final_content .= "\n";
5270 - if (!empty($regular_price)) {
5271 - $final_content .= "Price: " . $currency_symbol . $regular_price . "\n";
5272 - } elseif (!empty($price)) {
5273 - $final_content .= "Price: " . $currency_symbol . $price . "\n";
5274 - }
5275 -
5276 - if (!empty($sale_price) && $sale_price !== $regular_price) {
5277 - $final_content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
5278 - }
5279 -
5280 - // Handle variable products - show price range
5281 - if ($product->is_type('variable')) {
5282 - $min_price = $product->get_variation_price('min');
5283 - $max_price = $product->get_variation_price('max');
5284 - if ($min_price !== $max_price) {
5285 - $final_content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
5286 - }
5287 - }
5288 -
5289 - if (!empty($sku)) {
5290 - $final_content .= "SKU: " . $sku . "\n";
5291 - }
5292 -
5293 - // Get product categories
5294 - $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
5295 - if (!empty($categories) && !is_wp_error($categories)) {
5296 - $final_content .= "Categories: " . implode(', ', $categories) . "\n";
5297 - }
5298 - }
5299 - }
5300 -
5301 - // For custom post types like job_listing, include additional fields
5302 - if ($post_type === 'job_listing') {
5303 - // Add job-specific meta if available
5304 - $job_location = get_post_meta($post_id, '_job_location', true);
5305 - if (!empty($job_location)) {
5306 - $final_content .= "\n\nLocation: " . $job_location;
5307 - }
5308 -
5309 - // Get job type terms
5310 - $job_types = get_the_terms($post_id, 'job_listing_type');
5311 - if (!empty($job_types) && !is_wp_error($job_types)) {
5312 - $types = array();
5313 - foreach ($job_types as $type) {
5314 - $types[] = $type->name;
5315 - }
5316 - $final_content .= "\n\nJob Type: " . implode(', ', $types);
5317 - }
5318 -
5319 - // Get company name if available
5320 - $company_name = get_post_meta($post_id, '_company_name', true);
5321 - if (!empty($company_name)) {
5322 - $final_content .= "\n\nCompany: " . $company_name;
5323 - }
5324 - }
5325 -
5326 - // ADD ACF FIELDS SUPPORT (matches ajax_mxchat_process_selected_content behavior)
5327 - $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
5328 - if (!empty($acf_fields)) {
5329 - $acf_content_parts = array();
5330 -
5331 - foreach ($acf_fields as $field_name => $field_value) {
5332 - $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
5333 - if (!empty($formatted_value)) {
5334 - // Convert field name to readable label
5335 - $field_label = ucwords(str_replace(['_', '-'], ' ', $field_name));
5336 - $acf_content_parts[] = $field_label . ": " . $formatted_value;
5337 - }
5338 - }
5339 -
5340 - if (!empty($acf_content_parts)) {
5341 - $final_content .= "\n\n" . implode("\n", $acf_content_parts);
5342 - }
5343 - }
5344 -
5345 - // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
5346 - $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
5347 - if (!empty($custom_meta)) {
5348 - $meta_content_parts = array();
5349 -
5350 - foreach ($custom_meta as $meta_key => $meta_value) {
5351 - // Convert meta key to readable label
5352 - $meta_label = ucwords(str_replace(array('_', '-'), ' ', $meta_key));
5353 - $meta_content_parts[] = $meta_label . ": " . $meta_value;
5354 - }
5355 -
5356 - if (!empty($meta_content_parts)) {
5357 - $final_content .= "\n\n" . implode("\n", $meta_content_parts);
5358 - }
5359 - }
5360 -
5361 - // Get API key with proper model detection
5362 - $options = get_option('mxchat_options');
5363 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
5364 -
5365 - if (strpos($selected_model, 'voyage') === 0) {
5366 - $api_key = $options['voyage_api_key'] ?? '';
5367 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
5368 - $api_key = $options['gemini_api_key'] ?? '';
5369 - } else {
5370 - $api_key = $options['api_key'] ?? '';
5371 - }
5372 -
5373 - if (empty($api_key)) {
5374 - return;
5375 - }
5376 -
5377 - // Use the centralized utility function for storage
5378 - $result = MxChat_Utils::submit_content_to_db(
5379 - $final_content,
5380 - $source_url,
5381 - $api_key,
5382 - md5($source_url) // Vector ID for Pinecone
5383 - );
5384 -
5385 - // After successful storage, apply role restriction based on tags
5386 - if (!is_wp_error($result)) {
5387 - $this->apply_role_restriction_to_post($post_id, $source_url);
5388 - }
5389 - }
5390 -
5391 - // Clean up the stored previous status if not used above
5392 - if ($previous_status !== 'publish' || $post->post_status === 'publish') {
5393 - delete_transient($previous_status_key);
5394 - delete_transient($previous_url_key);
5395 - }
5396 -}
5397 -
5398 -/**
5399 - * Store the post status and URL before update to detect status transitions
5400 - * This runs before the post is actually updated in the database
5401 - */
5402 -public function mxchat_store_pre_update_status($post_id, $data) {
5403 - // Get the current post from database (before update)
5404 - $current_post = get_post($post_id);
5405 -
5406 - if ($current_post) {
5407 - // Store the current status temporarily
5408 - $status_key = 'mxchat_prev_status_' . $post_id;
5409 - set_transient($status_key, $current_post->post_status, HOUR_IN_SECONDS);
5410 -
5411 - // If the post is currently published, also store its URL
5412 - if ($current_post->post_status === 'publish') {
5413 - $url_key = 'mxchat_prev_url_' . $post_id;
5414 - $current_url = get_permalink($post_id);
5415 - set_transient($url_key, $current_url, HOUR_IN_SECONDS);
5416 - }
5417 - }
5418 -}
5419 -
5420 -public function mxchat_handle_post_delete($post_id) {
5421 - // Get post data before it's deleted
5422 - $post = get_post($post_id);
5423 -
5424 - // Basic validation
5425 - if (!$post || wp_is_post_revision($post_id)) {
5426 - return;
5427 - }
5428 -
5429 - $post_type = $post->post_type;
5430 -
5431 - // Check if sync is enabled for this post type
5432 - $should_sync = false;
5433 -
5434 - // Check built-in post types first
5435 - if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
5436 - $should_sync = true;
5437 - } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
5438 - $should_sync = true;
5439 - } else {
5440 - // Check custom post types
5441 - $option_name = 'mxchat_auto_sync_' . $post_type;
5442 - if (get_option($option_name) === '1') {
5443 - $should_sync = true;
5444 - }
5445 - }
5446 -
5447 - if (!$should_sync) {
5448 - return;
5449 - }
5450 -
5451 - // Get the URL before post is deleted
5452 - $source_url = get_permalink($post_id);
5453 - if (!$source_url) {
5454 - //error_log('MXChat: Failed to get permalink for post ' . $post_id);
5455 - return;
5456 - }
5457 -
5458 - // Use chunk-aware deletion (handles both chunked and non-chunked content)
5459 - $delete_result = MxChat_Utils::delete_chunks_for_url($source_url, 'default');
5460 -
5461 - if (is_wp_error($delete_result)) {
5462 - //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message());
5463 - }
5464 -}
5465 -
5466 -
5467 - /**
5468 - * Deletes data from Pinecone using a source URL
5469 - */
5470 - public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
5471 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
5472 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
5473 -
5474 - if (empty($host) || empty($api_key)) {
5475 - //error_log('MXChat: Pinecone deletion failed - missing configuration');
5476 - return false;
5477 - }
5478 -
5479 - $api_endpoint = "https://{$host}/vectors/delete";
5480 - $vector_id = md5($source_url);
5481 -
5482 - $request_body = array(
5483 - 'ids' => array($vector_id)
5484 - );
5485 -
5486 - $response = wp_remote_post($api_endpoint, array(
5487 - 'headers' => array(
5488 - 'Api-Key' => $api_key,
5489 - 'accept' => 'application/json',
5490 - 'content-type' => 'application/json'
5491 - ),
5492 - 'body' => wp_json_encode($request_body),
5493 - 'timeout' => 30
5494 - ));
5495 -
5496 - if (is_wp_error($response)) {
5497 - //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
5498 - return false;
5499 - }
5500 -
5501 - $response_code = wp_remote_retrieve_response_code($response);
5502 - if ($response_code !== 200) {
5503 - //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
5504 - return false;
5505 - }
5506 -
5507 - return true;
5508 - }
5509 -
5510 -
5511 -
5512 -public function mxchat_handle_product_change($post_id, $post, $update) {
5513 - if ($post->post_type !== 'product') {
5514 - return;
5515 - }
5516 -
5517 - if ($post->post_status === 'publish') {
5518 - add_action('shutdown', function() use ($post_id) {
5519 - $product = wc_get_product($post_id);
5520 - if ($product) {
5521 - $this->mxchat_store_product_embedding($product);
5522 - }
5523 - });
5524 - }
5525 -}
5526 -
5527 -/**
5528 - * Store WooCommerce product embeddings
5529 - */
5530 -private function mxchat_store_product_embedding($product) {
5531 - if (!isset($this->options['enable_woocommerce_integration']) ||
5532 - !in_array($this->options['enable_woocommerce_integration'], ['1', 'on'])) {
5533 - return;
5534 - }
5535 -
5536 - $source_url = get_permalink($product->get_id());
5537 - $product_id = $product->get_id();
5538 -
5539 - // Build product content
5540 - $title = $product->get_name();
5541 - $description = $product->get_description();
5542 - $short_description = $product->get_short_description();
5543 - $regular_price = $product->get_regular_price();
5544 - $sale_price = $product->get_sale_price();
5545 - $price = $product->get_price();
5546 - $sku = $product->get_sku();
5547 -
5548 - // Get currency symbol
5549 - $currency_symbol = get_woocommerce_currency_symbol();
5550 -
5551 - // Format content consistently
5552 - $content = $title . "\n\n";
5553 -
5554 - if (!empty($short_description)) {
5555 - $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
5556 - }
5557 -
5558 - if (!empty($description)) {
5559 - $content .= wp_strip_all_tags($description) . "\n\n";
5560 - }
5561 -
5562 - // Add pricing information
5563 - if (!empty($regular_price)) {
5564 - $content .= "Price: " . $currency_symbol . $regular_price . "\n";
5565 - } elseif (!empty($price)) {
5566 - $content .= "Price: " . $currency_symbol . $price . "\n";
5567 - }
5568 -
5569 - if (!empty($sale_price) && $sale_price !== $regular_price) {
5570 - $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
5571 - }
5572 -
5573 - // Handle variable products - show price range
5574 - if ($product->is_type('variable')) {
5575 - $min_price = $product->get_variation_price('min');
5576 - $max_price = $product->get_variation_price('max');
5577 - if ($min_price !== $max_price) {
5578 - $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
5579 - }
5580 - }
5581 -
5582 - if (!empty($sku)) {
5583 - $content .= "SKU: " . $sku . "\n";
5584 - }
5585 -
5586 - // Get product categories
5587 - $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
5588 - if (!empty($categories) && !is_wp_error($categories)) {
5589 - $content .= "Categories: " . implode(', ', $categories) . "\n";
5590 - }
5591 -
5592 - // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
5593 - $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
5594 - if (!empty($custom_tabs) && is_array($custom_tabs)) {
5595 - foreach ($custom_tabs as $tab) {
5596 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
5597 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
5598 -
5599 - if (!empty($tab_title) && !empty($tab_content)) {
5600 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
5601 - }
5602 - }
5603 - }
5604 -
5605 - // Also check for reusable/saved tabs applied to this product
5606 - $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
5607 - if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
5608 - // Get the saved tabs option
5609 - $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
5610 - if (!empty($saved_tabs) && is_array($saved_tabs)) {
5611 - foreach ($applied_saved_tabs as $saved_tab_id) {
5612 - if (isset($saved_tabs[$saved_tab_id])) {
5613 - $tab = $saved_tabs[$saved_tab_id];
5614 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
5615 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
5616 -
5617 - if (!empty($tab_title) && !empty($tab_content)) {
5618 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
5619 - }
5620 - }
5621 - }
5622 - }
5623 - }
5624 -
5625 - // Get API key with proper model detection
5626 - $options = get_option('mxchat_options');
5627 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
5628 -
5629 - if (strpos($selected_model, 'voyage') === 0) {
5630 - $api_key = $options['voyage_api_key'] ?? '';
5631 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
5632 - $api_key = $options['gemini_api_key'] ?? '';
5633 - } else {
5634 - $api_key = $options['api_key'] ?? '';
5635 - }
5636 -
5637 - if (empty($api_key)) {
5638 - //error_log('MxChat Auto-sync: No API key configured for embedding model');
5639 - return;
5640 - }
5641 -
5642 - // Use the centralized utility function for storage
5643 - $result = MxChat_Utils::submit_content_to_db(
5644 - $content,
5645 - $source_url,
5646 - $api_key,
5647 - md5($source_url) // Vector ID for Pinecone
5648 - );
5649 -
5650 - // After successful storage, apply role restriction based on tags
5651 - if (!is_wp_error($result)) {
5652 - $this->apply_role_restriction_to_post($product_id, $source_url);
5653 - }
5654 -
5655 - if (is_wp_error($result)) {
5656 - //error_log('MxChat WooCommerce sync failed for product ' . $product_id . ': ' . $result->get_error_message());
5657 - }
5658 -}
5659 -
5660 -public function mxchat_handle_product_delete($post_id) {
5661 - if (get_post_type($post_id) !== 'product') {
5662 - return;
5663 - }
5664 -
5665 - $source_url = get_permalink($post_id);
5666 -
5667 - // Check if Pinecone is enabled
5668 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5669 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5670 -
5671 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
5672 - // Delete from Pinecone
5673 - $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
5674 - } else {
5675 - // Delete from WordPress DB
5676 - global $wpdb;
5677 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5678 -
5679 - $wpdb->delete(
5680 - $table_name,
5681 - array('source_url' => $source_url),
5682 - array('%s')
5683 - );
5684 - }
5685 -}
5686 -
5687 -/**
5688 - * Handle individual Pinecone content deletion
5689 - */
5690 -public function mxchat_handle_pinecone_prompt_delete() {
5691 - // Check permissions
5692 - if (!current_user_can('manage_options')) {
5693 - wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
5694 - }
5695 -
5696 - // Verify nonce
5697 - if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'mxchat_delete_pinecone_prompt_nonce')) {
5698 - wp_die(esc_html__('Security check failed.', 'mxchat'));
5699 - }
5700 -
5701 - $vector_id = isset($_GET['vector_id']) ? sanitize_text_field($_GET['vector_id']) : '';
5702 -
5703 - if (empty($vector_id)) {
5704 - set_transient('mxchat_admin_notice_error',
5705 - esc_html__('Invalid vector ID.', 'mxchat'),
5706 - 30
5707 - );
5708 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5709 - exit;
5710 - }
5711 -
5712 - // Get Pinecone settings
5713 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5714 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5715 -
5716 - if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5717 - set_transient('mxchat_admin_notice_error',
5718 - esc_html__('Pinecone is not properly configured.', 'mxchat'),
5719 - 30
5720 - );
5721 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5722 - exit;
5723 - }
5724 -
5725 - // Delete from Pinecone
5726 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5727 - $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
5728 - $vector_id,
5729 - $pinecone_options['mxchat_pinecone_api_key'],
5730 - $pinecone_options['mxchat_pinecone_host']
5731 - );
5732 -
5733 - if ($result['success']) {
5734 - // No cache clearing needed since we removed caching
5735 - set_transient('mxchat_admin_notice_success',
5736 - esc_html__('Entry deleted successfully from Pinecone.', 'mxchat'),
5737 - 30
5738 - );
5739 - } else {
5740 - set_transient('mxchat_admin_notice_error',
5741 - esc_html__('Failed to delete entry: ', 'mxchat') . $result['message'],
5742 - 30
5743 - );
5744 - }
5745 -
5746 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5747 - exit;
5748 -}
5749 -/**
5750 - * Handle individual Pinecone content deletion via AJAX
5751 - */
5752 -public function ajax_mxchat_delete_pinecone_prompt() {
5753 - // Verify nonce and permissions
5754 - if (!check_ajax_referer('mxchat_delete_pinecone_prompt_nonce', 'nonce', false)) {
5755 - wp_send_json_error('Invalid nonce');
5756 - exit;
5757 - }
5758 -
5759 - if (!current_user_can('manage_options')) {
5760 - wp_send_json_error('Unauthorized access');
5761 - exit;
5762 - }
5763 -
5764 - $vector_id = isset($_POST['vector_id']) ? sanitize_text_field($_POST['vector_id']) : '';
5765 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
5766 -
5767 - if (empty($vector_id)) {
5768 - wp_send_json_error('Missing vector ID');
5769 - exit;
5770 - }
5771 -
5772 - // Get bot-specific Pinecone settings
5773 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5774 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
5775 -
5776 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5777 -
5778 - if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5779 - wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
5780 - exit;
5781 - }
5782 -
5783 - // Delete from the correct Pinecone index
5784 - $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
5785 - $vector_id,
5786 - $pinecone_options['mxchat_pinecone_api_key'],
5787 - $pinecone_options['mxchat_pinecone_host']
5788 - );
5789 -
5790 - if ($result['success']) {
5791 - // No cache clearing needed since we removed caching
5792 - wp_send_json_success(array(
5793 - 'message' => 'Entry deleted successfully from Pinecone',
5794 - 'vector_id' => $vector_id,
5795 - 'bot_id' => $bot_id
5796 - ));
5797 - } else {
5798 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Failed to delete from Pinecone: ' . $result['message'], array('vector_id' => $vector_id, 'bot_id' => $bot_id));
5799 - wp_send_json_error('Failed to delete from Pinecone: ' . $result['message']);
5800 - }
5801 -
5802 - exit;
5803 -}
5804 -
5805 -/**
5806 - * Handle deletion of all chunks for a given source URL via AJAX
5807 - * Follows the same pattern as ajax_mxchat_delete_pinecone_prompt
5808 - */
5809 -public function ajax_mxchat_delete_chunks_by_url() {
5810 - // Verify nonce and permissions
5811 - if (!check_ajax_referer('mxchat_delete_chunks_nonce', 'nonce', false)) {
5812 - wp_send_json_error('Invalid nonce');
5813 - exit;
5814 - }
5815 -
5816 - if (!current_user_can('manage_options')) {
5817 - wp_send_json_error('Unauthorized access');
5818 - exit;
5819 - }
5820 -
5821 - $source_url = isset($_POST['source_url']) ? esc_url_raw($_POST['source_url']) : '';
5822 - $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
5823 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
5824 -
5825 - if (empty($source_url)) {
5826 - wp_send_json_error('Missing source URL');
5827 - exit;
5828 - }
5829 -
5830 - // Generate the base vector ID from the source URL (same as how chunks are created)
5831 - $base_vector_id = md5($source_url);
5832 -
5833 - if ($data_source === 'pinecone') {
5834 - // Get bot-specific Pinecone settings (same as working delete function)
5835 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5836 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
5837 -
5838 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5839 -
5840 - if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5841 - wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
5842 - exit;
5843 - }
5844 -
5845 - $api_key = $pinecone_options['mxchat_pinecone_api_key'];
5846 - $host = $pinecone_options['mxchat_pinecone_host'];
5847 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
5848 -
5849 - // Collect all vector IDs to delete
5850 - $vectors_to_delete = array();
5851 -
5852 - // Add the original single-vector ID (for non-chunked content)
5853 - $vectors_to_delete[] = $base_vector_id;
5854 -
5855 - // Use Pinecone list API to find all chunk vectors with this prefix
5856 - // NOTE: Pinecone List API is a GET request with query parameters, not POST
5857 - $prefix = $base_vector_id . '_chunk_';
5858 -
5859 - $query_params = array(
5860 - 'prefix' => $prefix,
5861 - 'limit' => 100
5862 - );
5863 -
5864 - if (!empty($namespace)) {
5865 - $query_params['namespace'] = $namespace;
5866 - }
5867 -
5868 - $list_url = "https://{$host}/vectors/list?" . http_build_query($query_params);
5869 -
5870 - $list_response = wp_remote_get($list_url, array(
5871 - 'headers' => array(
5872 - 'Api-Key' => $api_key,
5873 - 'accept' => 'application/json'
5874 - ),
5875 - 'timeout' => 30
5876 - ));
5877 -
5878 - if (!is_wp_error($list_response)) {
5879 - $list_body_response = wp_remote_retrieve_body($list_response);
5880 - $list_data = json_decode($list_body_response, true);
5881 - if (!empty($list_data['vectors'])) {
5882 - foreach ($list_data['vectors'] as $vector) {
5883 - if (isset($vector['id'])) {
5884 - $vectors_to_delete[] = $vector['id'];
5885 - }
5886 - }
5887 - }
5888 - }
5889 -
5890 - if (empty($vectors_to_delete)) {
5891 - wp_send_json_success(array(
5892 - 'message' => 'No vectors found to delete',
5893 - 'source_url' => $source_url
5894 - ));
5895 - exit;
5896 - }
5897 -
5898 - // Delete all vectors using the same endpoint as the working function
5899 - $delete_url = "https://{$host}/vectors/delete";
5900 -
5901 - $delete_body = array(
5902 - 'ids' => $vectors_to_delete
5903 - );
5904 -
5905 - if (!empty($namespace)) {
5906 - $delete_body['namespace'] = $namespace;
5907 - }
5908 -
5909 - $delete_response = wp_remote_post($delete_url, array(
5910 - 'headers' => array(
5911 - 'Api-Key' => $api_key,
5912 - 'accept' => 'application/json',
5913 - 'content-type' => 'application/json'
5914 - ),
5915 - 'body' => wp_json_encode($delete_body),
5916 - 'timeout' => 30
5917 - ));
5918 -
5919 - if (is_wp_error($delete_response)) {
5920 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Failed to delete chunks from Pinecone: ' . $delete_response->get_error_message(), array('source_url' => $source_url));
5921 - wp_send_json_error('Failed to delete from Pinecone: ' . $delete_response->get_error_message());
5922 - exit;
5923 - }
5924 -
5925 - $response_code = wp_remote_retrieve_response_code($delete_response);
5926 -
5927 - if ($response_code !== 200) {
5928 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Pinecone API error (HTTP ' . $response_code . ')', array('source_url' => $source_url));
5929 - wp_send_json_error('Pinecone API error (HTTP ' . $response_code . ')');
5930 - exit;
5931 - }
5932 -
5933 - wp_send_json_success(array(
5934 - 'message' => 'All chunks deleted successfully from Pinecone',
5935 - 'source_url' => $source_url,
5936 - 'deleted_count' => count($vectors_to_delete)
5937 - ));
5938 -
5939 - } else {
5940 - // WordPress database deletion
5941 - global $wpdb;
5942 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5943 -
5944 - $result = $wpdb->delete(
5945 - $table_name,
5946 - array('source_url' => $source_url),
5947 - array('%s')
5948 - );
5949 -
5950 - if ($result === false) {
5951 - MxChat_Admin::mxchat_log_debug('database_error', 'Failed to delete from database: ' . $wpdb->last_error, array('source_url' => $source_url));
5952 - wp_send_json_error('Failed to delete from database: ' . $wpdb->last_error);
5953 - exit;
5954 - }
5955 -
5956 - wp_send_json_success(array(
5957 - 'message' => 'All chunks deleted successfully from database',
5958 - 'source_url' => $source_url,
5959 - 'deleted_count' => $result
5960 - ));
5961 - }
5962 -
5963 - exit;
5964 -}
5965 -
5966 -/**
5967 - * Handle individual WordPress database content deletion via AJAX
5968 - * Mirrors the Pinecone delete handler but for WordPress database entries
5969 - */
5970 -public function ajax_mxchat_delete_wordpress_prompt() {
5971 - // Verify nonce and permissions
5972 - if (!check_ajax_referer('mxchat_delete_wordpress_prompt_nonce', 'nonce', false)) {
5973 - wp_send_json_error('Invalid nonce');
5974 - exit;
5975 - }
5976 -
5977 - if (!current_user_can('manage_options')) {
5978 - wp_send_json_error('Unauthorized access');
5979 - exit;
5980 - }
5981 -
5982 - $entry_id = isset($_POST['entry_id']) ? intval($_POST['entry_id']) : 0;
5983 -
5984 - if (empty($entry_id)) {
5985 - wp_send_json_error('Missing entry ID');
5986 - exit;
5987 - }
5988 -
5989 - global $wpdb;
5990 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5991 -
5992 - // Clear cache for this entry
5993 - wp_cache_delete('prompt_' . $entry_id, 'mxchat_prompts');
5994 -
5995 - // Delete from database
5996 - $result = $wpdb->delete(
5997 - $table_name,
5998 - array('id' => $entry_id),
5999 - array('%d')
6000 - );
6001 -
6002 - if ($result !== false) {
6003 - wp_send_json_success(array(
6004 - 'message' => 'Entry deleted successfully',
6005 - 'entry_id' => $entry_id
6006 - ));
6007 - } else {
6008 - wp_send_json_error('Failed to delete entry from database');
6009 - }
6010 -
6011 - exit;
6012 -}
6013 -
6014 -/**
6015 - * Handle bulk deletion of knowledge entries via AJAX
6016 - * Supports both Pinecone and WordPress database entries
6017 - */
6018 -public function ajax_mxchat_bulk_delete_knowledge() {
6019 - // Verify nonce and permissions
6020 - if (!check_ajax_referer('mxchat_bulk_delete_knowledge_nonce', 'nonce', false)) {
6021 - wp_send_json_error('Invalid nonce');
6022 - exit;
6023 - }
6024 -
6025 - if (!current_user_can('manage_options')) {
6026 - wp_send_json_error('Unauthorized access');
6027 - exit;
6028 - }
6029 -
6030 - $entries = isset($_POST['entries']) ? $_POST['entries'] : array();
6031 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
6032 -
6033 - if (empty($entries) || !is_array($entries)) {
6034 - wp_send_json_error('No entries provided');
6035 - exit;
6036 - }
6037 -
6038 - // Extend execution time — bulk Pinecone operations can take a while
6039 - if (function_exists('set_time_limit')) {
6040 - set_time_limit(120);
6041 - }
6042 -
6043 - $success_ids = array();
6044 - $failed_ids = array();
6045 - $errors = array();
6046 -
6047 - global $wpdb;
6048 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6049 -
6050 - // Get Pinecone manager for Pinecone deletions
6051 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
6052 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
6053 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6054 -
6055 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
6056 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
6057 -
6058 - // =============================================
6059 - // PHASE 1: Collect all Pinecone vector IDs
6060 - // and separate WordPress entries
6061 - // =============================================
6062 - $pinecone_entry_ids = array(); // entry IDs that are pinecone-sourced
6063 - $wordpress_entries = array(); // entries for WordPress DB deletion
6064 - $all_vector_ids = array(); // all pinecone vector IDs to delete in one batch
6065 -
6066 - foreach ($entries as $entry) {
6067 - $entry_id = sanitize_text_field($entry['id'] ?? '');
6068 - $source = sanitize_text_field($entry['source'] ?? 'wordpress');
6069 - $source_url = isset($entry['sourceUrl']) ? esc_url_raw($entry['sourceUrl']) : '';
6070 - $is_group = isset($entry['isGroup']) && ($entry['isGroup'] === true || $entry['isGroup'] === 'true');
6071 -
6072 - if (empty($entry_id)) {
6073 - continue;
6074 - }
6075 -
6076 - if ($source === 'pinecone') {
6077 - if (!$use_pinecone || empty($api_key)) {
6078 - $failed_ids[] = $entry_id;
6079 - $errors[] = "Pinecone not configured for entry: $entry_id";
6080 - continue;
6081 - }
6082 -
6083 - $pinecone_entry_ids[] = $entry_id;
6084 -
6085 - if ($is_group && !empty($source_url)) {
6086 - // Grouped/chunked entry: collect base ID + chunk IDs via List API
6087 - $base_vector_id = md5($source_url);
6088 - $all_vector_ids[] = $base_vector_id;
6089 -
6090 - $list_url = 'https://' . $host . '/vectors/list?prefix=' . urlencode($base_vector_id . '_chunk_') . '&limit=100';
6091 - $list_response = wp_remote_get($list_url, array(
6092 - 'headers' => array(
6093 - 'Api-Key' => $api_key,
6094 - 'accept' => 'application/json'
6095 - ),
6096 - 'timeout' => 30
6097 - ));
6098 -
6099 - if (!is_wp_error($list_response)) {
6100 - $list_body = json_decode(wp_remote_retrieve_body($list_response), true);
6101 - if (!empty($list_body['vectors']) && is_array($list_body['vectors'])) {
6102 - foreach ($list_body['vectors'] as $vector) {
6103 - if (isset($vector['id'])) {
6104 - $all_vector_ids[] = $vector['id'];
6105 - }
6106 - }
6107 - }
6108 - }
6109 - } else {
6110 - // Single entry: the entry_id IS the vector ID
6111 - $all_vector_ids[] = $entry_id;
6112 - }
6113 - } else {
6114 - $wordpress_entries[] = $entry;
6115 - }
6116 - }
6117 -
6118 - // =============================================
6119 - // PHASE 2: Single batch delete to Pinecone
6120 - // =============================================
6121 - if (!empty($all_vector_ids)) {
6122 - $all_vector_ids = array_values(array_unique($all_vector_ids));
6123 - $pinecone_success = true;
6124 - $batches = array_chunk($all_vector_ids, 100);
6125 -
6126 - foreach ($batches as $batch) {
6127 - $delete_response = wp_remote_post("https://{$host}/vectors/delete", array(
6128 - 'headers' => array(
6129 - 'Api-Key' => $api_key,
6130 - 'accept' => 'application/json',
6131 - 'content-type' => 'application/json'
6132 - ),
6133 - 'body' => wp_json_encode(array('ids' => $batch)),
6134 - 'timeout' => 60
6135 - ));
6136 -
6137 - if (is_wp_error($delete_response)) {
6138 - $pinecone_success = false;
6139 - $errors[] = 'Pinecone batch deletion failed: ' . $delete_response->get_error_message();
6140 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Bulk delete batch failed: ' . $delete_response->get_error_message());
6141 - } else {
6142 - $response_code = wp_remote_retrieve_response_code($delete_response);
6143 - if ($response_code !== 200) {
6144 - $pinecone_success = false;
6145 - $response_body = wp_remote_retrieve_body($delete_response);
6146 - $errors[] = "Pinecone API error (HTTP $response_code)";
6147 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Bulk delete batch failed (HTTP ' . $response_code . ')', array('response' => substr($response_body, 0, 200)));
6148 - }
6149 - }
6150 - }
6151 -
6152 - // Mark all pinecone entries based on batch result
6153 - foreach ($pinecone_entry_ids as $eid) {
6154 - if ($pinecone_success) {
6155 - $success_ids[] = $eid;
6156 - } else {
6157 - $failed_ids[] = $eid;
6158 - }
6159 - }
6160 - }
6161 -
6162 - // =============================================
6163 - // PHASE 3: WordPress database deletions
6164 - // =============================================
6165 - foreach ($wordpress_entries as $entry) {
6166 - $entry_id = sanitize_text_field($entry['id'] ?? '');
6167 - $source_url = isset($entry['sourceUrl']) ? esc_url_raw($entry['sourceUrl']) : '';
6168 - $is_group = isset($entry['isGroup']) && ($entry['isGroup'] === true || $entry['isGroup'] === 'true');
6169 -
6170 - if (empty($entry_id)) {
6171 - continue;
6172 - }
6173 -
6174 - try {
6175 - if ($is_group && !empty($source_url)) {
6176 - $result = $wpdb->delete(
6177 - $table_name,
6178 - array('source_url' => $source_url),
6179 - array('%s')
6180 - );
6181 - } else {
6182 - wp_cache_delete('prompt_' . $entry_id, 'mxchat_prompts');
6183 - $result = $wpdb->delete(
6184 - $table_name,
6185 - array('id' => intval($entry_id)),
6186 - array('%d')
6187 - );
6188 - }
6189 -
6190 - if ($result !== false) {
6191 - $success_ids[] = $entry_id;
6192 - } else {
6193 - $failed_ids[] = $entry_id;
6194 - $errors[] = "Database error for entry: $entry_id";
6195 - }
6196 - } catch (Exception $e) {
6197 - $failed_ids[] = $entry_id;
6198 - $errors[] = $e->getMessage();
6199 - }
6200 - }
6201 -
6202 - wp_send_json_success(array(
6203 - 'success_ids' => $success_ids,
6204 - 'failed_ids' => $failed_ids,
6205 - 'errors' => $errors,
6206 - 'total_processed' => count($success_ids) + count($failed_ids)
6207 - ));
6208 -
6209 - exit;
6210 -}
6211 -
6212 -/**
6213 - * Get hierarchical roles for dropdown
6214 - */
6215 -public function mxchat_get_role_options() {
6216 - return array(
6217 - 'public' => __('Public (Everyone)', 'mxchat'),
6218 - 'logged_in' => __('Logged In Users', 'mxchat'),
6219 - 'subscriber' => __('Subscribers & Above', 'mxchat'),
6220 - 'contributor' => __('Contributors & Above', 'mxchat'),
6221 - 'author' => __('Authors & Above', 'mxchat'),
6222 - 'editor' => __('Editors & Above', 'mxchat'),
6223 - 'administrator' => __('Administrators Only', 'mxchat')
6224 - );
6225 -}
6226 -
6227 -/**
6228 - * Check if user has access to content based on role restriction
6229 - */
6230 -public function mxchat_user_has_content_access($role_restriction) {
6231 - // Public content is always accessible
6232 - if ($role_restriction === 'public' || empty($role_restriction)) {
6233 - return true;
6234 - }
6235 -
6236 - // Check if user is logged in for logged_in restriction
6237 - if ($role_restriction === 'logged_in') {
6238 - return is_user_logged_in();
6239 - }
6240 -
6241 - // If not logged in, no access to role-restricted content
6242 - if (!is_user_logged_in()) {
6243 - return false;
6244 - }
6245 -
6246 - $user = wp_get_current_user();
6247 - $user_roles = $user->roles;
6248 -
6249 - if (empty($user_roles)) {
6250 - return false;
6251 - }
6252 -
6253 - // Define role hierarchy (higher number = higher access)
6254 - $hierarchy = array(
6255 - 'subscriber' => 1,
6256 - 'contributor' => 2,
6257 - 'author' => 3,
6258 - 'editor' => 4,
6259 - 'administrator' => 5
6260 - );
6261 -
6262 - // Get required level
6263 - $required_level = isset($hierarchy[$role_restriction]) ? $hierarchy[$role_restriction] : 0;
6264 -
6265 - // Check if user has required level or higher
6266 - foreach ($user_roles as $user_role) {
6267 - $user_level = isset($hierarchy[$user_role]) ? $hierarchy[$user_role] : 0;
6268 - if ($user_level >= $required_level) {
6269 - return true;
6270 - }
6271 - }
6272 -
6273 - return false;
6274 -}
6275 -
6276 -/**
6277 - * Handle role restriction updates via AJAX
6278 - * Removed cache clearing call since we removed caching
6279 - */
6280 -public function ajax_mxchat_update_role_restriction() {
6281 - // Verify nonce and permissions
6282 - if (!check_ajax_referer('mxchat_update_role_nonce', 'nonce', false)) {
6283 - wp_send_json_error('Invalid nonce');
6284 - exit;
6285 - }
6286 -
6287 - if (!current_user_can('manage_options')) {
6288 - wp_send_json_error('Unauthorized access');
6289 - exit;
6290 - }
6291 -
6292 - $entry_id = isset($_POST['entry_id']) ? sanitize_text_field($_POST['entry_id']) : '';
6293 - $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
6294 - $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
6295 -
6296 - if (empty($entry_id)) {
6297 - wp_send_json_error('Invalid entry ID');
6298 - exit;
6299 - }
6300 -
6301 - // Get knowledge manager instance to validate role restriction
6302 - $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
6303 - $valid_roles = array_keys($knowledge_manager->mxchat_get_role_options());
6304 - if (!in_array($role_restriction, $valid_roles)) {
6305 - wp_send_json_error('Invalid role restriction');
6306 - exit;
6307 - }
6308 -
6309 - global $wpdb;
6310 -
6311 - if ($data_source === 'pinecone') {
6312 - // Handle Pinecone role restriction (stored separately in WordPress table)
6313 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6314 -
6315 - // Use REPLACE to insert or update the role restriction
6316 - $result = $wpdb->replace(
6317 - $roles_table,
6318 - array(
6319 - 'vector_id' => $entry_id,
6320 - 'role_restriction' => $role_restriction,
6321 - 'updated_at' => current_time('mysql')
6322 - ),
6323 - array('%s', '%s', '%s')
6324 - );
6325 -
6326 - // No cache clearing needed since we removed caching
6327 -
6328 - } else {
6329 - // Handle WordPress database role restriction (existing functionality)
6330 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6331 -
6332 - $result = $wpdb->update(
6333 - $table_name,
6334 - array('role_restriction' => $role_restriction),
6335 - array('id' => absint($entry_id)),
6336 - array('%s'),
6337 - array('%d')
6338 - );
6339 - }
6340 -
6341 - if ($result === false) {
6342 - wp_send_json_error('Database update failed: ' . $wpdb->last_error);
6343 - exit;
6344 - }
6345 -
6346 - wp_send_json_success(array(
6347 - 'message' => 'Role restriction updated successfully',
6348 - 'role_restriction' => $role_restriction,
6349 - 'data_source' => $data_source,
6350 - 'entry_id' => $entry_id
6351 - ));
6352 - exit;
6353 -}
6354 -
6355 -// ========================================
6356 -// ROLE-BASED CONTENT RESTRICTIONS - NEW FUNCTIONS
6357 -// Add these to your MxChat_Knowledge_Manager class
6358 -// ========================================
6359 -
6360 -/**
6361 - * Initialize role-based content hooks
6362 - * Add this call to your __construct() or mxchat_init_hooks() method
6363 - */
6364 -private function mxchat_init_role_hooks() {
6365 - // AJAX handlers for tag-role mappings
6366 - add_action('wp_ajax_mxchat_add_tag_role_mapping', array($this, 'ajax_add_tag_role_mapping'));
6367 - add_action('wp_ajax_mxchat_delete_tag_role_mapping', array($this, 'ajax_delete_tag_role_mapping'));
6368 - add_action('wp_ajax_mxchat_get_tag_role_mappings', array($this, 'ajax_get_tag_role_mappings'));
6369 - add_action('wp_ajax_mxchat_bulk_update_tag_roles', array($this, 'ajax_bulk_update_tag_roles'));
6370 -
6371 - // Hook to automatically update role restrictions when tags are added/removed
6372 - add_action('set_object_terms', array($this, 'handle_tag_change'), 10, 6);
6373 -
6374 - // Hook to apply role restrictions on auto-sync
6375 - add_action('mxchat_content_stored', array($this, 'apply_role_restriction_after_storage'), 10, 2);
6376 -}
6377 -
6378 -/**
6379 - * Add tag-role mapping via AJAX
6380 - */
6381 -public function ajax_add_tag_role_mapping() {
6382 - // Verify nonce and permissions
6383 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6384 -
6385 - if (!current_user_can('manage_options')) {
6386 - wp_send_json_error('Unauthorized access');
6387 - exit;
6388 - }
6389 -
6390 - $tag_slug = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
6391 - $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
6392 -
6393 - if (empty($tag_slug)) {
6394 - wp_send_json_error('Tag slug is required');
6395 - exit;
6396 - }
6397 -
6398 - // Validate role restriction
6399 - $valid_roles = array_keys($this->mxchat_get_role_options());
6400 - if (!in_array($role_restriction, $valid_roles)) {
6401 - wp_send_json_error('Invalid role restriction');
6402 - exit;
6403 - }
6404 -
6405 - // Check if tag exists in WordPress
6406 - $term = get_term_by('slug', $tag_slug, 'post_tag');
6407 - if (!$term) {
6408 - wp_send_json_error('Tag does not exist in WordPress');
6409 - exit;
6410 - }
6411 -
6412 - // Get existing mappings
6413 - $mappings = get_option('mxchat_tag_role_mappings', array());
6414 -
6415 - // Check if mapping already exists
6416 - if (isset($mappings[$tag_slug])) {
6417 - wp_send_json_error('Mapping for this tag already exists');
6418 - exit;
6419 - }
6420 -
6421 - // Add new mapping
6422 - $mappings[$tag_slug] = $role_restriction;
6423 - update_option('mxchat_tag_role_mappings', $mappings);
6424 -
6425 - wp_send_json_success(array(
6426 - 'message' => 'Tag-role mapping added successfully',
6427 - 'tag_slug' => $tag_slug,
6428 - 'role_restriction' => $role_restriction
6429 - ));
6430 - exit;
6431 -}
6432 -
6433 -/**
6434 - * Delete tag-role mapping via AJAX
6435 - */
6436 -public function ajax_delete_tag_role_mapping() {
6437 - // Verify nonce and permissions
6438 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6439 -
6440 - if (!current_user_can('manage_options')) {
6441 - wp_send_json_error('Unauthorized access');
6442 - exit;
6443 - }
6444 -
6445 - $tag_slug = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
6446 -
6447 - if (empty($tag_slug)) {
6448 - wp_send_json_error('Tag slug is required');
6449 - exit;
6450 - }
6451 -
6452 - // Get existing mappings
6453 - $mappings = get_option('mxchat_tag_role_mappings', array());
6454 -
6455 - // Check if mapping exists
6456 - if (!isset($mappings[$tag_slug])) {
6457 - wp_send_json_error('Mapping does not exist');
6458 - exit;
6459 - }
6460 -
6461 - // Remove mapping
6462 - unset($mappings[$tag_slug]);
6463 - update_option('mxchat_tag_role_mappings', $mappings);
6464 -
6465 - wp_send_json_success(array(
6466 - 'message' => 'Tag-role mapping deleted successfully',
6467 - 'tag_slug' => $tag_slug
6468 - ));
6469 - exit;
6470 -}
6471 -
6472 -/**
6473 - * Get all tag-role mappings via AJAX
6474 - */
6475 -public function ajax_get_tag_role_mappings() {
6476 - // Verify nonce and permissions
6477 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6478 -
6479 - if (!current_user_can('manage_options')) {
6480 - wp_send_json_error('Unauthorized access');
6481 - exit;
6482 - }
6483 -
6484 - // Get mappings
6485 - $mappings = get_option('mxchat_tag_role_mappings', array());
6486 - $role_options = $this->mxchat_get_role_options();
6487 -
6488 - $formatted_mappings = array();
6489 -
6490 - foreach ($mappings as $tag_slug => $role_restriction) {
6491 - // Get tag object
6492 - $term = get_term_by('slug', $tag_slug, 'post_tag');
6493 -
6494 - // Count posts with this tag
6495 - $post_count = 0;
6496 - if ($term) {
6497 - $post_count = $term->count;
6498 - }
6499 -
6500 - $formatted_mappings[] = array(
6501 - 'tag_slug' => $tag_slug,
6502 - 'role_restriction' => $role_restriction,
6503 - 'role_label' => $role_options[$role_restriction] ?? $role_restriction,
6504 - 'post_count' => $post_count
6505 - );
6506 - }
6507 -
6508 - wp_send_json_success(array(
6509 - 'mappings' => $formatted_mappings
6510 - ));
6511 - exit;
6512 -}
6513 -
6514 -/**
6515 - * Bulk update role restrictions for all existing content with mapped tags
6516 - */
6517 -public function ajax_bulk_update_tag_roles() {
6518 - // Verify nonce and permissions
6519 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6520 -
6521 - if (!current_user_can('manage_options')) {
6522 - wp_send_json_error('Unauthorized access');
6523 - exit;
6524 - }
6525 -
6526 - // Get mappings
6527 - $mappings = get_option('mxchat_tag_role_mappings', array());
6528 -
6529 - if (empty($mappings)) {
6530 - wp_send_json_error('No tag-role mappings found');
6531 - exit;
6532 - }
6533 -
6534 - global $wpdb;
6535 -
6536 - // Check if using Pinecone
6537 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
6538 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6539 -
6540 - $updated_count = 0;
6541 - $details = array();
6542 -
6543 - foreach ($mappings as $tag_slug => $role_restriction) {
6544 - // Get all posts with this tag
6545 - $posts = get_posts(array(
6546 - 'tag' => $tag_slug,
6547 - 'post_type' => 'any',
6548 - 'posts_per_page' => -1,
6549 - 'fields' => 'ids',
6550 - 'post_status' => 'publish'
6551 - ));
6552 -
6553 - if (empty($posts)) {
6554 - continue;
6555 - }
6556 -
6557 - $tag_updated = 0;
6558 -
6559 - foreach ($posts as $post_id) {
6560 - $source_url = get_permalink($post_id);
6561 - if (!$source_url) {
6562 - continue;
6563 - }
6564 -
6565 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
6566 - // Update Pinecone role restriction
6567 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6568 - $vector_id = md5($source_url);
6569 -
6570 - $result = $wpdb->replace(
6571 - $roles_table,
6572 - array(
6573 - 'vector_id' => $vector_id,
6574 - 'role_restriction' => $role_restriction,
6575 - 'updated_at' => current_time('mysql')
6576 - ),
6577 - array('%s', '%s', '%s')
6578 - );
6579 - } else {
6580 - // Update WordPress DB
6581 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6582 -
6583 - $result = $wpdb->update(
6584 - $table_name,
6585 - array('role_restriction' => $role_restriction),
6586 - array('source_url' => $source_url),
6587 - array('%s'),
6588 - array('%s')
6589 - );
6590 - }
6591 -
6592 - if ($result !== false) {
6593 - $tag_updated++;
6594 - $updated_count++;
6595 - }
6596 - }
6597 -
6598 - if ($tag_updated > 0) {
6599 - $details[] = sprintf(
6600 - 'Tag "%s" (%s): %d posts updated',
6601 - $tag_slug,
6602 - $role_restriction,
6603 - $tag_updated
6604 - );
6605 - }
6606 - }
6607 -
6608 - wp_send_json_success(array(
6609 - 'message' => 'Bulk update completed',
6610 - 'updated_count' => $updated_count,
6611 - 'tags_processed' => count($mappings),
6612 - 'details' => $details
6613 - ));
6614 - exit;
6615 -}
6616 -
6617 -/**
6618 - * Handle tag changes on posts (when tags are added or removed)
6619 - */
6620 -public function handle_tag_change($object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids) {
6621 - // Only process post tags
6622 - if ($taxonomy !== 'post_tag') {
6623 - return;
6624 - }
6625 -
6626 - // Get tag-role mappings
6627 - $mappings = get_option('mxchat_tag_role_mappings', array());
6628 -
6629 - if (empty($mappings)) {
6630 - return;
6631 - }
6632 -
6633 - // Get the post's URL
6634 - $source_url = get_permalink($object_id);
6635 - if (!$source_url) {
6636 - return;
6637 - }
6638 -
6639 - // Determine the highest role restriction based on tags
6640 - $highest_role = 'public';
6641 - $role_hierarchy = array(
6642 - 'public' => 0,
6643 - 'logged_in' => 1,
6644 - 'subscriber' => 2,
6645 - 'contributor' => 3,
6646 - 'author' => 4,
6647 - 'editor' => 5,
6648 - 'administrator' => 6
6649 - );
6650 -
6651 - // Get all current tags for the post
6652 - $current_tags = wp_get_post_tags($object_id, array('fields' => 'slugs'));
6653 -
6654 - // Find the highest role restriction among the tags
6655 - foreach ($current_tags as $tag_slug) {
6656 - if (isset($mappings[$tag_slug])) {
6657 - $role = $mappings[$tag_slug];
6658 - if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
6659 - $highest_role = $role;
6660 - }
6661 - }
6662 - }
6663 -
6664 - // Update the role restriction in the database
6665 - global $wpdb;
6666 -
6667 - // Check if using Pinecone
6668 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
6669 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6670 -
6671 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
6672 - // Update Pinecone role restriction
6673 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6674 - $vector_id = md5($source_url);
6675 -
6676 - $wpdb->replace(
6677 - $roles_table,
6678 - array(
6679 - 'vector_id' => $vector_id,
6680 - 'role_restriction' => $highest_role,
6681 - 'updated_at' => current_time('mysql')
6682 - ),
6683 - array('%s', '%s', '%s')
6684 - );
6685 - } else {
6686 - // Update WordPress DB
6687 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6688 -
6689 - $wpdb->update(
6690 - $table_name,
6691 - array('role_restriction' => $highest_role),
6692 - array('source_url' => $source_url),
6693 - array('%s'),
6694 - array('%s')
6695 - );
6696 - }
6697 -}
6698 -
6699 -/**
6700 - * Apply role restriction after content is stored (for auto-sync)
6701 - */
6702 -public function apply_role_restriction_after_storage($post_id, $source_url) {
6703 - // Get tag-role mappings
6704 - $mappings = get_option('mxchat_tag_role_mappings', array());
6705 -
6706 - if (empty($mappings)) {
6707 - return;
6708 - }
6709 -
6710 - // Get all tags for the post
6711 - $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
6712 -
6713 - if (empty($post_tags)) {
6714 - return;
6715 - }
6716 -
6717 - // Determine the highest role restriction based on tags
6718 - $highest_role = 'public';
6719 - $role_hierarchy = array(
6720 - 'public' => 0,
6721 - 'logged_in' => 1,
6722 - 'subscriber' => 2,
6723 - 'contributor' => 3,
6724 - 'author' => 4,
6725 - 'editor' => 5,
6726 - 'administrator' => 6
6727 - );
6728 -
6729 - foreach ($post_tags as $tag_slug) {
6730 - if (isset($mappings[$tag_slug])) {
6731 - $role = $mappings[$tag_slug];
6732 - if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
6733 - $highest_role = $role;
6734 - }
6735 - }
6736 - }
6737 -
6738 - // If no restricted tags found, return (leave as public)
6739 - if ($highest_role === 'public') {
6740 - return;
6741 - }
6742 -
6743 - // Update the role restriction
6744 - global $wpdb;
6745 -
6746 - // Check if using Pinecone
6747 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
6748 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6749 -
6750 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
6751 - // Update Pinecone role restriction
6752 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6753 - $vector_id = md5($source_url);
6754 -
6755 - $wpdb->replace(
6756 - $roles_table,
6757 - array(
6758 - 'vector_id' => $vector_id,
6759 - 'role_restriction' => $highest_role,
6760 - 'updated_at' => current_time('mysql')
6761 - ),
6762 - array('%s', '%s', '%s')
6763 - );
6764 - } else {
6765 - // Update WordPress DB
6766 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6767 -
6768 - $wpdb->update(
6769 - $table_name,
6770 - array('role_restriction' => $highest_role),
6771 - array('source_url' => $source_url),
6772 - array('%s'),
6773 - array('%s')
6774 - );
6775 - }
6776 -}
6777 -
6778 -
6779 - // ========================================
6780 - // HELPER METHODS
6781 - // ========================================
6782 -
6783 - /**
6784 - * Check if user has required permissions for content processing
6785 - */
6786 - private function mxchat_check_user_permissions() {
6787 - if (!current_user_can('manage_options')) {
6788 - wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
6789 - }
6790 - }
6791 -
6792 - /**
6793 - * Validate nonce for security
6794 - */
6795 - private function mxchat_validate_nonce($nonce_name, $nonce_action) {
6796 - if (!isset($_POST[$nonce_name]) || !wp_verify_nonce($_POST[$nonce_name], $nonce_action)) {
6797 - wp_die(esc_html__('Security check failed.', 'mxchat'));
6798 - }
6799 - }
6800 -
6801 - /**
6802 - * Get embedding API credentials
6803 - */
6804 - private function mxchat_get_embedding_credentials() {
6805 - $embedding_model = $this->options['embedding_model'] ?? 'text-embedding-ada-002';
6806 -
6807 - if (strpos($embedding_model, 'text-embedding-') !== false) {
6808 - return array(
6809 - 'type' => 'openai',
6810 - 'api_key' => $this->options['api_key'] ?? ''
6811 - );
6812 - } elseif (strpos($embedding_model, 'voyage-') !== false) {
6813 - return array(
6814 - 'type' => 'voyage',
6815 - 'api_key' => $this->options['voyage_api_key'] ?? ''
6816 - );
6817 - } elseif (strpos($embedding_model, 'gemini-embedding-') !== false) {
6818 - return array(
6819 - 'type' => 'gemini',
6820 - 'api_key' => $this->options['gemini_api_key'] ?? ''
6821 - );
6822 - }
6823 -
6824 - return array('type' => 'unknown', 'api_key' => '');
6825 - }
6826 -
6827 - /**
6828 - * Log processing errors
6829 - */
6830 - private function mxchat_log_processing_error($operation, $error_message) {
6831 - //error_log("MxChat Knowledge Processing {$operation} Error: " . $error_message);
6832 - }
6833 -
6834 - /**
6835 - * Set admin notice transient
6836 - */
6837 - private function mxchat_set_admin_notice($type, $message) {
6838 - set_transient("mxchat_admin_notice_{$type}", $message, 30);
6839 - }
6840 -
6841 - /**
6842 - * Get Pinecone manager instance for vector operations
6843 - */
6844 - private function mxchat_get_pinecone_manager() {
6845 - return MxChat_Pinecone_Manager::get_instance();
6846 - }
6847 -
6848 -
6849 - // ========================================
6850 -// DATABASE QUEUE TABLE MANAGEMENT
6851 -// ========================================
6852 -
6853 -/**
6854 - * Create queue table on plugin activation
6855 - * Call this from your plugin activation hook
6856 - */
6857 -public function mxchat_create_queue_table() {
6858 - global $wpdb;
6859 -
6860 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6861 - $charset_collate = $wpdb->get_charset_collate();
6862 -
6863 - $sql = "CREATE TABLE IF NOT EXISTS $table_name (
6864 - id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
6865 - queue_id varchar(64) NOT NULL,
6866 - item_type varchar(20) NOT NULL,
6867 - item_data longtext NOT NULL,
6868 - status varchar(20) NOT NULL DEFAULT 'pending',
6869 - bot_id varchar(50) NOT NULL DEFAULT 'default',
6870 - priority int(11) NOT NULL DEFAULT 0,
6871 - attempts int(11) NOT NULL DEFAULT 0,
6872 - max_attempts int(11) NOT NULL DEFAULT 3,
6873 - error_message text DEFAULT NULL,
6874 - created_at datetime NOT NULL,
6875 - started_at datetime DEFAULT NULL,
6876 - completed_at datetime DEFAULT NULL,
6877 - PRIMARY KEY (id),
6878 - KEY queue_id (queue_id),
6879 - KEY status (status),
6880 - KEY item_type (item_type),
6881 - KEY priority (priority)
6882 - ) $charset_collate;";
6883 -
6884 - require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
6885 - dbDelta($sql);
6886 -
6887 - // Also create a meta table for queue metadata
6888 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
6889 -
6890 - $meta_sql = "CREATE TABLE IF NOT EXISTS $meta_table (
6891 - id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
6892 - queue_id varchar(64) NOT NULL,
6893 - meta_key varchar(255) NOT NULL,
6894 - meta_value longtext,
6895 - PRIMARY KEY (id),
6896 - KEY queue_id (queue_id),
6897 - KEY meta_key (meta_key)
6898 - ) $charset_collate;";
6899 -
6900 - dbDelta($meta_sql);
6901 -}
6902 -
6903 -/**
6904 - * Add items to the processing queue
6905 - *
6906 - * @param string $queue_id Unique identifier for this queue batch
6907 - * @param string $item_type Type of item (url, pdf_page)
6908 - * @param array $items Array of items to queue
6909 - * @param string $bot_id Bot ID for processing
6910 - * @return int Number of items queued
6911 - */
6912 -private function mxchat_add_to_queue($queue_id, $item_type, $items, $bot_id = 'default') {
6913 - global $wpdb;
6914 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6915 -
6916 - $queued_count = 0;
6917 - $priority = 0;
6918 -
6919 - foreach ($items as $item) {
6920 - $result = $wpdb->insert(
6921 - $table_name,
6922 - array(
6923 - 'queue_id' => $queue_id,
6924 - 'item_type' => $item_type,
6925 - 'item_data' => wp_json_encode($item),
6926 - 'status' => 'pending',
6927 - 'bot_id' => $bot_id,
6928 - 'priority' => $priority,
6929 - 'attempts' => 0,
6930 - 'max_attempts' => 3,
6931 - 'created_at' => current_time('mysql')
6932 - ),
6933 - array('%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%s')
6934 - );
6935 -
6936 - if ($result) {
6937 - $queued_count++;
6938 - }
6939 -
6940 - $priority++; // Process in order
6941 - }
6942 -
6943 - return $queued_count;
6944 -}
6945 -
6946 -/**
6947 - * Store queue metadata (total counts, source URL, etc.)
6948 - */
6949 -private function mxchat_set_queue_meta($queue_id, $meta_key, $meta_value) {
6950 - global $wpdb;
6951 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
6952 -
6953 - // Check if meta exists
6954 - $existing = $wpdb->get_var($wpdb->prepare(
6955 - "SELECT id FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
6956 - $queue_id,
6957 - $meta_key
6958 - ));
6959 -
6960 - if ($existing) {
6961 - // Update
6962 - $wpdb->update(
6963 - $meta_table,
6964 - array('meta_value' => maybe_serialize($meta_value)),
6965 - array('queue_id' => $queue_id, 'meta_key' => $meta_key),
6966 - array('%s'),
6967 - array('%s', '%s')
6968 - );
6969 - } else {
6970 - // Insert
6971 - $wpdb->insert(
6972 - $meta_table,
6973 - array(
6974 - 'queue_id' => $queue_id,
6975 - 'meta_key' => $meta_key,
6976 - 'meta_value' => maybe_serialize($meta_value)
6977 - ),
6978 - array('%s', '%s', '%s')
6979 - );
6980 - }
6981 -}
6982 -
6983 -/**
6984 - * Get queue metadata
6985 - */
6986 -private function mxchat_get_queue_meta($queue_id, $meta_key) {
6987 - global $wpdb;
6988 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
6989 -
6990 - $value = $wpdb->get_var($wpdb->prepare(
6991 - "SELECT meta_value FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
6992 - $queue_id,
6993 - $meta_key
6994 - ));
6995 -
6996 - return maybe_unserialize($value);
6997 -}
6998 -
6999 -// ========================================
7000 -// AJAX QUEUE PROCESSING HANDLERS
7001 -// ========================================
7002 -
7003 -/**
7004 - * AJAX: Get next item from queue to process
7005 - */
7006 -public function ajax_mxchat_get_next_queue_item() {
7007 - // Verify nonce and permissions
7008 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7009 -
7010 - if (!current_user_can('manage_options')) {
7011 - wp_send_json_error('Unauthorized access');
7012 - }
7013 -
7014 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
7015 -
7016 - if (empty($queue_id)) {
7017 - wp_send_json_error('Missing queue ID');
7018 - }
7019 -
7020 - global $wpdb;
7021 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7022 -
7023 - // Get next pending item with retry logic for failed items
7024 - $next_item = $wpdb->get_row($wpdb->prepare(
7025 - "SELECT * FROM $table_name
7026 - WHERE queue_id = %s
7027 - AND status IN ('pending', 'failed')
7028 - AND attempts < max_attempts
7029 - ORDER BY priority ASC, id ASC
7030 - LIMIT 1",
7031 - $queue_id
7032 - ));
7033 -
7034 - if (!$next_item) {
7035 - // No more items - queue complete
7036 - wp_send_json_success(array(
7037 - 'complete' => true,
7038 - 'message' => 'Queue processing complete'
7039 - ));
7040 - }
7041 -
7042 - // Mark item as processing
7043 - $wpdb->update(
7044 - $table_name,
7045 - array(
7046 - 'status' => 'processing',
7047 - 'started_at' => current_time('mysql'),
7048 - 'attempts' => $next_item->attempts + 1
7049 - ),
7050 - array('id' => $next_item->id),
7051 - array('%s', '%s', '%d'),
7052 - array('%d')
7053 - );
7054 -
7055 - wp_send_json_success(array(
7056 - 'complete' => false,
7057 - 'item' => array(
7058 - 'id' => $next_item->id,
7059 - 'type' => $next_item->item_type,
7060 - 'data' => json_decode($next_item->item_data, true),
7061 - 'bot_id' => $next_item->bot_id,
7062 - 'attempt' => $next_item->attempts + 1
7063 - )
7064 - ));
7065 -}
7066 -
7067 -/**
7068 - * AJAX: Process a single queue item
7069 - */
7070 -public function ajax_mxchat_process_queue_item() {
7071 - // Verify nonce and permissions
7072 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7073 -
7074 - if (!current_user_can('manage_options')) {
7075 - wp_send_json_error('Unauthorized access');
7076 - }
7077 -
7078 - $item_id = isset($_POST['item_id']) ? absint($_POST['item_id']) : 0;
7079 - $item_type = isset($_POST['item_type']) ? sanitize_text_field($_POST['item_type']) : '';
7080 - $item_data = isset($_POST['item_data']) ? $_POST['item_data'] : array();
7081 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
7082 -
7083 - if (empty($item_id) || empty($item_type)) {
7084 - wp_send_json_error('Missing item data');
7085 - }
7086 -
7087 - global $wpdb;
7088 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7089 -
7090 - // Process based on item type
7091 - try {
7092 - set_time_limit(60); // Give processing 60 seconds
7093 -
7094 - $result = false;
7095 - $error_message = '';
7096 -
7097 - // Read item directly from DB to get queue_id and preserve special chars in item_data
7098 - // (POST round-trip through JS mangles characters like apostrophes in URLs)
7099 - $db_item = $wpdb->get_row($wpdb->prepare(
7100 - "SELECT queue_id, item_data FROM $table_name WHERE id = %d",
7101 - $item_id
7102 - ));
7103 - $item_queue_id = $db_item ? $db_item->queue_id : '';
7104 - if ($db_item && !empty($db_item->item_data)) {
7105 - $db_data = json_decode($db_item->item_data, true);
7106 - if (is_array($db_data)) {
7107 - $item_data = $db_data;
7108 - }
7109 - }
7110 -
7111 - switch ($item_type) {
7112 - case 'url':
7113 - $result = $this->mxchat_process_queue_url($item_data, $bot_id, $item_queue_id);
7114 - break;
7115 -
7116 - case 'pdf_page':
7117 - $result = $this->mxchat_process_queue_pdf_page($item_data, $bot_id);
7118 - break;
7119 -
7120 - default:
7121 - throw new Exception('Unknown item type: ' . $item_type);
7122 - }
7123 -
7124 - if (is_wp_error($result)) {
7125 - $error_code = $result->get_error_code();
7126 - // Content errors (empty page, sanitization) are permanent — retrying won't help
7127 - $permanent_codes = array('empty_page', 'empty_after_sanitization', 'no_api_key', 'page_not_found');
7128 - if (in_array($error_code, $permanent_codes)) {
7129 - // Mark as permanently failed — set attempts = max_attempts so it won't be retried
7130 - $current_item = $wpdb->get_row($wpdb->prepare(
7131 - "SELECT max_attempts FROM $table_name WHERE id = %d", $item_id
7132 - ));
7133 - $wpdb->update(
7134 - $table_name,
7135 - array(
7136 - 'status' => 'failed',
7137 - 'error_message' => $result->get_error_message(),
7138 - 'attempts' => $current_item ? $current_item->max_attempts : 3
7139 - ),
7140 - array('id' => $item_id),
7141 - array('%s', '%s', '%d'),
7142 - array('%d')
7143 - );
7144 - wp_send_json_error(array(
7145 - 'message' => $result->get_error_message(),
7146 - 'permanent_failure' => true,
7147 - 'item_id' => $item_id
7148 - ));
7149 - return;
7150 - }
7151 - throw new Exception($result->get_error_message());
7152 - }
7153 -
7154 - if ($result === false) {
7155 - throw new Exception('Processing returned false - item may be empty or invalid');
7156 - }
7157 -
7158 - // Mark as completed
7159 - $wpdb->update(
7160 - $table_name,
7161 - array(
7162 - 'status' => 'completed',
7163 - 'completed_at' => current_time('mysql'),
7164 - 'error_message' => null
7165 - ),
7166 - array('id' => $item_id),
7167 - array('%s', '%s', '%s'),
7168 - array('%d')
7169 - );
7170 -
7171 - wp_send_json_success(array(
7172 - 'processed' => true,
7173 - 'item_id' => $item_id,
7174 - 'message' => 'Item processed successfully'
7175 - ));
7176 -
7177 - } catch (Exception $e) {
7178 - $error_message = $e->getMessage();
7179 -
7180 - // Get current attempt count
7181 - $item = $wpdb->get_row($wpdb->prepare(
7182 - "SELECT attempts, max_attempts FROM $table_name WHERE id = %d",
7183 - $item_id
7184 - ));
7185 -
7186 - // Check if we've exhausted retries
7187 - if ($item && $item->attempts >= $item->max_attempts) {
7188 - // Permanently failed
7189 - $wpdb->update(
7190 - $table_name,
7191 - array(
7192 - 'status' => 'failed',
7193 - 'error_message' => $error_message
7194 - ),
7195 - array('id' => $item_id),
7196 - array('%s', '%s'),
7197 - array('%d')
7198 - );
7199 -
7200 - wp_send_json_error(array(
7201 - 'message' => 'Item failed after maximum attempts: ' . $error_message,
7202 - 'permanent_failure' => true,
7203 - 'item_id' => $item_id
7204 - ));
7205 - } else {
7206 - // Mark for retry
7207 - $wpdb->update(
7208 - $table_name,
7209 - array(
7210 - 'status' => 'failed',
7211 - 'error_message' => $error_message
7212 - ),
7213 - array('id' => $item_id),
7214 - array('%s', '%s'),
7215 - array('%d')
7216 - );
7217 -
7218 - wp_send_json_error(array(
7219 - 'message' => 'Item processing failed, will retry: ' . $error_message,
7220 - 'can_retry' => true,
7221 - 'item_id' => $item_id,
7222 - 'attempts' => $item ? $item->attempts : 0
7223 - ));
7224 - }
7225 - }
7226 -}
7227 -
7228 -/**
7229 - * Process a URL from the queue
7230 - */
7231 -private function mxchat_process_queue_url($item_data, $bot_id = 'default', $queue_id = '') {
7232 - $url = isset($item_data['url']) ? $item_data['url'] : '';
7233 -
7234 - if (empty($url)) {
7235 - return new WP_Error('invalid_url', 'URL is empty');
7236 - }
7237 -
7238 - // Get bot-specific API key early (needed for both paths)
7239 - $bot_options = $this->get_bot_options($bot_id);
7240 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
7241 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
7242 -
7243 - if (strpos($selected_model, 'voyage') === 0) {
7244 - $api_key = $options['voyage_api_key'] ?? '';
7245 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
7246 - $api_key = $options['gemini_api_key'] ?? '';
7247 - } else {
7248 - $api_key = $options['api_key'] ?? '';
7249 - }
7250 -
7251 - if (empty($api_key)) {
7252 - return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
7253 - }
7254 -
7255 - // Check if this is a WooCommerce product URL and WooCommerce is active
7256 - $is_product_url = (strpos($url, '/product/') !== false || strpos($url, '/shop/') !== false);
7257 - $content_type = $is_product_url ? 'product' : 'url';
7258 -
7259 - // Try to get WooCommerce product data if it's a product URL
7260 - if ($is_product_url && class_exists('WooCommerce')) {
7261 - $product_content = $this->mxchat_extract_woocommerce_product_content($url);
7262 -
7263 - if (!empty($product_content)) {
7264 - // Successfully extracted WooCommerce product data with pricing
7265 - $result = MxChat_Utils::submit_content_to_db(
7266 - $product_content,
7267 - $url,
7268 - $api_key,
7269 - null,
7270 - $bot_id,
7271 - 'product'
7272 - );
7273 - return $result;
7274 - }
7275 - // If WooCommerce extraction failed, fall through to HTML extraction
7276 - }
7277 -
7278 - // Fetch URL content (fallback for non-products or when WooCommerce extraction fails)
7279 - $is_likely_pdf = (strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?: '', PATHINFO_EXTENSION)) === 'pdf');
7280 - $response = wp_remote_get($url, array(
7281 - 'timeout' => $is_likely_pdf ? 120 : 30,
7282 - 'redirection' => 5,
7283 - 'user-agent' => 'MxChat/1.0'
7284 - ));
7285 -
7286 - if (is_wp_error($response)) {
7287 - return $response;
7288 - }
7289 -
7290 - $response_code = wp_remote_retrieve_response_code($response);
7291 - if ($response_code !== 200) {
7292 - return new WP_Error('http_error', 'HTTP ' . $response_code . ' error for: ' . $url);
7293 - }
7294 -
7295 - // Check if URL is a PDF — expand into per-page queue items using the standard PDF pipeline
7296 - if ($this->mxchat_is_pdf_url($url, $response)) {
7297 - return $this->mxchat_expand_pdf_to_queue($url, $response, $bot_id, $queue_id);
7298 - }
7299 -
7300 - $html = wp_remote_retrieve_body($response);
7301 -
7302 - if (empty($html)) {
7303 - return new WP_Error('empty_response', 'Empty response body');
7304 - }
7305 -
7306 - // Extract and sanitize content
7307 - $content = $this->mxchat_extract_main_content($html);
7308 - $sanitized = $this->mxchat_sanitize_content_for_api($content);
7309 -
7310 - if (empty($sanitized)) {
7311 - // Not an error - just no content found (maybe a redirect or empty page)
7312 - return false;
7313 - }
7314 -
7315 - // Submit to database with content_type
7316 - $result = MxChat_Utils::submit_content_to_db(
7317 - $sanitized,
7318 - $url,
7319 - $api_key,
7320 - null,
7321 - $bot_id,
7322 - $content_type
7323 - );
7324 -
7325 - return $result;
7326 -}
7327 -
7328 -/**
7329 - * Expand a PDF URL into per-page queue items using the standard PDF pipeline.
7330 - * Called when a sitemap URL turns out to be a PDF — downloads, parses page count,
7331 - * and adds pdf_page items to the same queue so they process with full progress tracking.
7332 - */
7333 -private function mxchat_expand_pdf_to_queue($pdf_url, $response, $bot_id = 'default', $queue_id = '') {
7334 - set_time_limit(120); // PDFs need extra time for download + parsing
7335 -
7336 - $upload_dir = wp_upload_dir();
7337 - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf');
7338 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
7339 -
7340 - $response_body = wp_remote_retrieve_body($response);
7341 - if (empty($response_body)) {
7342 - return new WP_Error('empty_pdf', 'Empty PDF response for: ' . $pdf_url);
7343 - }
7344 -
7345 - if (!wp_mkdir_p(dirname($pdf_path))) {
7346 - return new WP_Error('dir_error', 'Failed to create upload directory');
7347 - }
7348 -
7349 - file_put_contents($pdf_path, $response_body);
7350 -
7351 - if (!file_exists($pdf_path)) {
7352 - return new WP_Error('save_error', 'Failed to save PDF file');
7353 - }
7354 -
7355 - try {
7356 - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
7357 -
7358 - if ($total_pages === false || $total_pages < 1) {
7359 - wp_delete_file($pdf_path);
7360 - return new WP_Error('no_pages', 'PDF has no pages: ' . $pdf_url);
7361 - }
7362 -
7363 - // Build per-page items identical to mxchat_handle_pdf_for_knowledge_base
7364 - $pages = array();
7365 - for ($i = 1; $i <= $total_pages; $i++) {
7366 - $pages[] = array(
7367 - 'pdf_path' => $pdf_path,
7368 - 'pdf_url' => $pdf_url,
7369 - 'page_number' => $i,
7370 - 'total_pages' => $total_pages
7371 - );
7372 - }
7373 -
7374 - // Add pdf_page items to the SAME queue so the JS picks them up automatically
7375 - if (!empty($queue_id)) {
7376 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
7377 - } else {
7378 - // Fallback: create a new PDF queue (shouldn't happen in sitemap flow)
7379 - $new_queue_id = 'pdf_' . md5($pdf_url . time());
7380 - $queued_count = $this->mxchat_add_to_queue($new_queue_id, 'pdf_page', $pages, $bot_id);
7381 - $this->mxchat_set_queue_meta($new_queue_id, 'source_url', $pdf_url);
7382 - $this->mxchat_set_queue_meta($new_queue_id, 'queue_type', 'pdf');
7383 - $this->mxchat_set_queue_meta($new_queue_id, 'total_items', $total_pages);
7384 - $this->mxchat_set_queue_meta($new_queue_id, 'bot_id', $bot_id);
7385 - $this->mxchat_set_queue_meta($new_queue_id, 'pdf_path', $pdf_path);
7386 - $this->mxchat_set_queue_meta($new_queue_id, 'created_at', current_time('mysql'));
7387 - }
7388 -
7389 - if ($queued_count === 0) {
7390 - wp_delete_file($pdf_path);
7391 - return new WP_Error('queue_error', 'Failed to add PDF pages to queue');
7392 - }
7393 -
7394 - // Return true so the original URL item is marked complete
7395 - // The new pdf_page items will be processed in subsequent batches
7396 - return true;
7397 -
7398 - } catch (Exception $e) {
7399 - if (file_exists($pdf_path)) {
7400 - wp_delete_file($pdf_path);
7401 - }
7402 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
7403 - }
7404 -}
7405 -
7406 -/**
7407 - * Legacy: Process a PDF URL inline during sitemap queue processing.
7408 - * @deprecated Use mxchat_expand_pdf_to_queue instead — kept for reference only.
7409 - */
7410 -private function mxchat_process_pdf_url_inline($pdf_url, $response, $api_key, $bot_id = 'default') {
7411 - set_time_limit(120); // PDFs need more time — downloading + parsing all pages
7412 -
7413 - $upload_dir = wp_upload_dir();
7414 - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf');
7415 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
7416 -
7417 - $response_body = wp_remote_retrieve_body($response);
7418 - if (empty($response_body)) {
7419 - return new WP_Error('empty_pdf', 'Empty PDF response');
7420 - }
7421 -
7422 - if (!wp_mkdir_p(dirname($pdf_path))) {
7423 - return new WP_Error('dir_error', 'Failed to create upload directory');
7424 - }
7425 -
7426 - file_put_contents($pdf_path, $response_body);
7427 -
7428 - if (!file_exists($pdf_path)) {
7429 - return new WP_Error('save_error', 'Failed to save PDF file');
7430 - }
7431 -
7432 - try {
7433 - mxchat_load_pdf_parser();
7434 - $parser = new \Smalot\PdfParser\Parser();
7435 - $pdf = $parser->parseFile($pdf_path);
7436 - $pages = $pdf->getPages();
7437 - $total_pages = count($pages);
7438 -
7439 - if ($total_pages < 1) {
7440 - wp_delete_file($pdf_path);
7441 - return new WP_Error('no_pages', 'PDF has no pages');
7442 - }
7443 -
7444 - $processed = 0;
7445 - $skipped_pages = array();
7446 -
7447 - for ($i = 0; $i < $total_pages; $i++) {
7448 - $page_num = $i + 1;
7449 - $text = $pages[$i]->getText();
7450 - if (empty($text)) {
7451 - $skipped_pages[] = 'Page ' . $page_num . ': No text could be extracted — page may contain only images, links, or non-standard encoding';
7452 - continue;
7453 - }
7454 -
7455 - $sanitized = $this->mxchat_sanitize_content_for_api($text);
7456 - if (empty($sanitized)) {
7457 - $skipped_pages[] = 'Page ' . $page_num . ': Text was extracted but contained only special characters, control codes, or unsupported content';
7458 - continue;
7459 - }
7460 -
7461 - $metadata = array(
7462 - 'document_type' => 'pdf',
7463 - 'total_pages' => $total_pages,
7464 - 'current_page' => $page_num,
7465 - 'source_url' => $pdf_url,
7466 - );
7467 -
7468 - $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized;
7469 - $page_url = esc_url($pdf_url . '#page=' . $page_num);
7470 -
7471 - MxChat_Utils::submit_content_to_db(
7472 - $content_with_metadata,
7473 - $page_url,
7474 - $api_key,
7475 - null,
7476 - $bot_id,
7477 - 'pdf'
7478 - );
7479 -
7480 - $processed++;
7481 - }
7482 -
7483 - // Clean up the temp PDF file
7484 - wp_delete_file($pdf_path);
7485 -
7486 - if (!empty($skipped_pages)) {
7487 - error_log('MxChat PDF: Skipped ' . count($skipped_pages) . ' of ' . $total_pages . ' pages: ' . implode('; ', $skipped_pages));
7488 - }
7489 -
7490 - return $processed > 0 ? true : false;
7491 -
7492 - } catch (Exception $e) {
7493 - if (file_exists($pdf_path)) {
7494 - wp_delete_file($pdf_path);
7495 - }
7496 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
7497 - }
7498 -}
7499 -
7500 -/**
7501 - * Extract WooCommerce product content including pricing
7502 - *
7503 - * @param string $url The product URL
7504 - * @return string|false Product content with pricing, or false if not found
7505 - */
7506 -private function mxchat_extract_woocommerce_product_content($url) {
7507 - // Try to get product ID from URL
7508 - $product_id = url_to_postid($url);
7509 -
7510 - // If url_to_postid fails, try to extract from URL pattern
7511 - if (!$product_id) {
7512 - $product_slug = '';
7513 -
7514 - // Handle pretty permalinks: /product/product-name/
7515 - if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
7516 - $product_slug = $matches[1];
7517 - }
7518 -
7519 - if (!empty($product_slug)) {
7520 - $product_post = get_page_by_path($product_slug, OBJECT, 'product');
7521 - if ($product_post) {
7522 - $product_id = $product_post->ID;
7523 - }
7524 - }
7525 - }
7526 -
7527 - if (!$product_id) {
7528 - return false;
7529 - }
7530 -
7531 - // Get WooCommerce product object
7532 - $product = wc_get_product($product_id);
7533 -
7534 - if (!$product) {
7535 - return false;
7536 - }
7537 -
7538 - // Build product content with pricing (similar to mxchat_store_product_embedding)
7539 - $title = $product->get_name();
7540 - $description = $product->get_description();
7541 - $short_description = $product->get_short_description();
7542 - $sku = $product->get_sku();
7543 -
7544 - // Get pricing information
7545 - $regular_price = $product->get_regular_price();
7546 - $sale_price = $product->get_sale_price();
7547 - $price = $product->get_price(); // Current active price
7548 -
7549 - // Get currency symbol
7550 - $currency_symbol = get_woocommerce_currency_symbol();
7551 -
7552 - // Format content
7553 - $content = $title . "\n\n";
7554 -
7555 - if (!empty($short_description)) {
7556 - $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
7557 - }
7558 -
7559 - if (!empty($description)) {
7560 - $content .= wp_strip_all_tags($description) . "\n\n";
7561 - }
7562 -
7563 - // Add pricing information
7564 - if (!empty($regular_price)) {
7565 - $content .= "Price: " . $currency_symbol . $regular_price . "\n";
7566 - } elseif (!empty($price)) {
7567 - $content .= "Price: " . $currency_symbol . $price . "\n";
7568 - }
7569 -
7570 - if (!empty($sale_price) && $sale_price !== $regular_price) {
7571 - $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
7572 - }
7573 -
7574 - // Handle variable products - show price range
7575 - if ($product->is_type('variable')) {
7576 - $min_price = $product->get_variation_price('min');
7577 - $max_price = $product->get_variation_price('max');
7578 - if ($min_price !== $max_price) {
7579 - $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
7580 - }
7581 - }
7582 -
7583 - if (!empty($sku)) {
7584 - $content .= "SKU: " . $sku . "\n";
7585 - }
7586 -
7587 - // Get product categories
7588 - $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
7589 - if (!empty($categories) && !is_wp_error($categories)) {
7590 - $content .= "Categories: " . implode(', ', $categories) . "\n";
7591 - }
7592 -
7593 - // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
7594 - $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
7595 - if (!empty($custom_tabs) && is_array($custom_tabs)) {
7596 - foreach ($custom_tabs as $tab) {
7597 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
7598 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
7599 -
7600 - if (!empty($tab_title) && !empty($tab_content)) {
7601 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
7602 - }
7603 - }
7604 - }
7605 -
7606 - // Also check for reusable/saved tabs applied to this product
7607 - $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
7608 - if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
7609 - $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
7610 - if (!empty($saved_tabs) && is_array($saved_tabs)) {
7611 - foreach ($applied_saved_tabs as $saved_tab_id) {
7612 - if (isset($saved_tabs[$saved_tab_id])) {
7613 - $tab = $saved_tabs[$saved_tab_id];
7614 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
7615 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
7616 -
7617 - if (!empty($tab_title) && !empty($tab_content)) {
7618 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
7619 - }
7620 - }
7621 - }
7622 - }
7623 - }
7624 -
7625 - return $this->mxchat_sanitize_content_for_api($content);
7626 -}
7627 -
7628 -/**
7629 - * Process a PDF page from the queue
7630 - */
7631 -private function mxchat_process_queue_pdf_page($item_data, $bot_id = 'default') {
7632 - $pdf_path = isset($item_data['pdf_path']) ? $item_data['pdf_path'] : '';
7633 - $pdf_url = isset($item_data['pdf_url']) ? $item_data['pdf_url'] : '';
7634 - $page_number = isset($item_data['page_number']) ? absint($item_data['page_number']) : 0;
7635 - $total_pages = isset($item_data['total_pages']) ? absint($item_data['total_pages']) : 0;
7636 -
7637 - if (empty($pdf_path) || !file_exists($pdf_path)) {
7638 - return new WP_Error('pdf_not_found', 'PDF file not found: ' . $pdf_path);
7639 - }
7640 -
7641 - if ($page_number < 1) {
7642 - return new WP_Error('invalid_page', 'Invalid page number');
7643 - }
7644 -
7645 - try {
7646 - mxchat_load_pdf_parser();
7647 - $parser = new \Smalot\PdfParser\Parser();
7648 - $pdf = $parser->parseFile($pdf_path);
7649 - $pages = $pdf->getPages();
7650 -
7651 - if (!isset($pages[$page_number - 1])) {
7652 - return new WP_Error('page_not_found', 'Page ' . $page_number . ' not found in PDF');
7653 - }
7654 -
7655 - $text = $pages[$page_number - 1]->getText();
7656 -
7657 - if (empty($text)) {
7658 - return new WP_Error('empty_page', 'Page ' . $page_number . ': No text could be extracted — page may contain only images, links, or non-standard encoding');
7659 - }
7660 -
7661 - $sanitized = $this->mxchat_sanitize_content_for_api($text);
7662 -
7663 - if (empty($sanitized)) {
7664 - return new WP_Error('empty_after_sanitization', 'Page ' . $page_number . ': Text was extracted but contained only special characters, control codes, or unsupported content that was removed during cleanup');
7665 - }
7666 -
7667 - // Create metadata
7668 - $metadata = array(
7669 - 'document_type' => 'pdf',
7670 - 'total_pages' => $total_pages,
7671 - 'current_page' => $page_number,
7672 - 'source_url' => $pdf_url
7673 - );
7674 -
7675 - $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized;
7676 - $page_url = esc_url($pdf_url . "#page=" . $page_number);
7677 -
7678 - // Get bot-specific API key
7679 - $bot_options = $this->get_bot_options($bot_id);
7680 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
7681 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
7682 -
7683 - if (strpos($selected_model, 'voyage') === 0) {
7684 - $api_key = $options['voyage_api_key'] ?? '';
7685 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
7686 - $api_key = $options['gemini_api_key'] ?? '';
7687 - } else {
7688 - $api_key = $options['api_key'] ?? '';
7689 - }
7690 -
7691 - if (empty($api_key)) {
7692 - return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
7693 - }
7694 -
7695 - // Submit to database - UPDATED 2.5.6: Added content_type 'pdf'
7696 - $result = MxChat_Utils::submit_content_to_db(
7697 - $content_with_metadata,
7698 - $page_url,
7699 - $api_key,
7700 - null,
7701 - $bot_id,
7702 - 'pdf'
7703 - );
7704 -
7705 - return $result;
7706 -
7707 - } catch (Exception $e) {
7708 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
7709 - }
7710 -}
7711 -
7712 -/**
7713 - * AJAX: Get queue processing status
7714 - */
7715 -public function ajax_mxchat_get_queue_status() {
7716 - // Verify nonce and permissions
7717 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7718 -
7719 - if (!current_user_can('manage_options')) {
7720 - wp_send_json_error('Unauthorized access');
7721 - }
7722 -
7723 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
7724 -
7725 - if (empty($queue_id)) {
7726 - wp_send_json_error('Missing queue ID');
7727 - }
7728 -
7729 - global $wpdb;
7730 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7731 -
7732 - // Get counts by status
7733 - $counts = $wpdb->get_results($wpdb->prepare(
7734 - "SELECT status, COUNT(*) as count
7735 - FROM $table_name
7736 - WHERE queue_id = %s
7737 - GROUP BY status",
7738 - $queue_id
7739 - ), OBJECT_K);
7740 -
7741 - $total = 0;
7742 - $completed = 0;
7743 - $failed = 0;
7744 - $processing = 0;
7745 - $pending = 0;
7746 -
7747 - foreach ($counts as $status => $data) {
7748 - $count = absint($data->count);
7749 - $total += $count;
7750 -
7751 - switch ($status) {
7752 - case 'completed':
7753 - $completed = $count;
7754 - break;
7755 - case 'failed':
7756 - $failed = $count;
7757 - break;
7758 - case 'processing':
7759 - $processing = $count;
7760 - break;
7761 - case 'pending':
7762 - $pending = $count;
7763 - break;
7764 - }
7765 - }
7766 -
7767 - // Calculate percentage
7768 - $percentage = $total > 0 ? round((($completed + $failed) / $total) * 100) : 0;
7769 -
7770 - // Get failed items details (include all failed items, not just those that exhausted retries)
7771 - $failed_items = array();
7772 - if ($failed > 0) {
7773 - $failed_items = $wpdb->get_results($wpdb->prepare(
7774 - "SELECT item_type, item_data, error_message, attempts
7775 - FROM $table_name
7776 - WHERE queue_id = %s
7777 - AND status = 'failed'
7778 - ORDER BY id DESC
7779 - LIMIT 50",
7780 - $queue_id
7781 - ));
7782 - }
7783 -
7784 - // Get queue metadata
7785 - $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
7786 - $queue_type = $this->mxchat_get_queue_meta($queue_id, 'queue_type');
7787 -
7788 - // Determine if queue is complete
7789 - $is_complete = ($pending === 0 && $processing === 0);
7790 -
7791 - wp_send_json_success(array(
7792 - 'queue_id' => $queue_id,
7793 - 'queue_type' => $queue_type,
7794 - 'source_url' => $source_url,
7795 - 'total' => $total,
7796 - 'completed' => $completed,
7797 - 'failed' => $failed,
7798 - 'processing' => $processing,
7799 - 'pending' => $pending,
7800 - 'percentage' => $percentage,
7801 - 'is_complete' => $is_complete,
7802 - 'failed_items' => $failed_items,
7803 - 'status' => $is_complete ? 'complete' : 'processing'
7804 - ));
7805 -}
7806 -
7807 -/**
7808 - * AJAX: Clear completed queue
7809 - */
7810 -public function ajax_mxchat_clear_queue() {
7811 - // Verify nonce and permissions
7812 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7813 -
7814 - if (!current_user_can('manage_options')) {
7815 - wp_send_json_error('Unauthorized access');
7816 - }
7817 -
7818 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
7819 -
7820 - if (empty($queue_id)) {
7821 - wp_send_json_error('Missing queue ID');
7822 - }
7823 -
7824 - global $wpdb;
7825 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7826 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
7827 -
7828 - // Delete queue items
7829 - $wpdb->delete(
7830 - $table_name,
7831 - array('queue_id' => $queue_id),
7832 - array('%s')
7833 - );
7834 -
7835 - // Delete queue metadata
7836 - $wpdb->delete(
7837 - $meta_table,
7838 - array('queue_id' => $queue_id),
7839 - array('%s')
7840 - );
7841 -
7842 - wp_send_json_success(array(
7843 - 'message' => 'Queue cleared successfully'
7844 - ));
7845 -}
7846 -
7847 -/**
7848 - * AJAX: Retry failed items in queue
7849 - */
7850 -public function ajax_mxchat_retry_failed() {
7851 - // Verify nonce and permissions
7852 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7853 -
7854 - if (!current_user_can('manage_options')) {
7855 - wp_send_json_error('Unauthorized access');
7856 - }
7857 -
7858 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
7859 -
7860 - if (empty($queue_id)) {
7861 - wp_send_json_error('Missing queue ID');
7862 - }
7863 -
7864 - global $wpdb;
7865 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7866 -
7867 - // Reset failed items to pending and reset attempt count
7868 - $updated = $wpdb->update(
7869 - $table_name,
7870 - array(
7871 - 'status' => 'pending',
7872 - 'attempts' => 0,
7873 - 'error_message' => null
7874 - ),
7875 - array(
7876 - 'queue_id' => $queue_id,
7877 - 'status' => 'failed'
7878 - ),
7879 - array('%s', '%d', '%s'),
7880 - array('%s', '%s')
7881 - );
7882 -
7883 - wp_send_json_success(array(
7884 - 'message' => 'Reset ' . $updated . ' failed items for retry',
7885 - 'reset_count' => $updated
7886 - ));
7887 -}
7888 -
7889 -
7890 -public function ajax_mxchat_mark_queue_complete() {
7891 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7892 -
7893 - if (!current_user_can('manage_options')) {
7894 - wp_send_json_error('Unauthorized access');
7895 - }
7896 -
7897 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
7898 -
7899 - if (empty($queue_id)) {
7900 - wp_send_json_error('Missing queue ID');
7901 - }
7902 -
7903 - // Clear active queue transients
7904 - if (strpos($queue_id, 'sitemap_') === 0) {
7905 - delete_transient('mxchat_active_queue_sitemap');
7906 - } else if (strpos($queue_id, 'pdf_') === 0) {
7907 - delete_transient('mxchat_active_queue_pdf');
7908 - }
7909 -
7910 - wp_send_json_success(array('message' => 'Queue marked as complete'));
7911 -}
7912 -
7913 -
7914 - // ========================================
7915 - // STATIC ACCESS METHODS
7916 - // ========================================
7917 -
7918 - /**
7919 - * Get singleton instance
7920 - */
7921 - public static function get_instance() {
7922 - static $instance = null;
7923 - if ($instance === null) {
7924 - $instance = new self();
7925 - }
7926 - return $instance;
7927 - }
7928 -}
7929 -
7930 -// Initialize the Knowledge manager
1 +<?php
2 +/**
3 + * File: admin/class-knowledge-manager.php
4 + *
5 + * Handles all knowledge base content processing for MxChat
6 + * Including PDF, sitemap, content processing, and WordPress post management
7 + */
8 +if (!defined('ABSPATH')) {
9 + exit; // Exit if accessed directly
10 +}
11 +
12 +class MxChat_Knowledge_Manager {
13 +
14 + private $options;
15 +
16 + /**
17 + * Constructor - Register hooks for content processing
18 + */
19 +public function __construct() {
20 + $this->options = get_option('mxchat_options', array());
21 + $this->mxchat_init_hooks();
22 +
23 + $this->mxchat_init_role_hooks();
24 +}
25 +
26 +/**
27 + * Initialize WordPress hooks for content processing
28 + *
29 + */
30 +private function mxchat_init_hooks() {
31 + // Admin post handlers for form submissions
32 + add_action('admin_post_mxchat_submit_content', array($this, 'mxchat_handle_content_submission'));
33 + add_action('admin_post_mxchat_submit_sitemap', array($this, 'mxchat_handle_sitemap_submission'));
34 + add_action('admin_post_mxchat_stop_processing', array($this, 'mxchat_stop_processing'));
35 +
36 + // AJAX handlers for real-time processing and status updates
37 + add_action('wp_ajax_mxchat_get_status_updates', array($this, 'mxchat_ajax_get_status_updates'));
38 + add_action('wp_ajax_mxchat_dismiss_completed_status', array($this, 'mxchat_ajax_dismiss_completed_status'));
39 + add_action('wp_ajax_mxchat_get_content_list', array($this, 'ajax_mxchat_get_content_list'));
40 + add_action('wp_ajax_mxchat_process_selected_content', array($this, 'ajax_mxchat_process_selected_content'));
41 + add_action('wp_ajax_mxchat_save_inline_prompt', array($this, 'mxchat_save_inline_prompt'));
42 + add_action('admin_post_mxchat_delete_pinecone_prompt', array($this, 'mxchat_handle_pinecone_prompt_delete'));
43 + add_action('wp_ajax_mxchat_delete_pinecone_prompt', array($this, 'ajax_mxchat_delete_pinecone_prompt'));
44 + add_action('wp_ajax_mxchat_delete_chunks_by_url', array($this, 'ajax_mxchat_delete_chunks_by_url'));
45 + add_action('wp_ajax_mxchat_delete_wordpress_prompt', array($this, 'ajax_mxchat_delete_wordpress_prompt'));
46 + add_action('wp_ajax_mxchat_bulk_delete_knowledge', array($this, 'ajax_mxchat_bulk_delete_knowledge'));
47 + add_action('wp_ajax_mxchat_update_role_restriction', array($this, 'ajax_mxchat_update_role_restriction'));
48 +
49 + // Queue-based processing AJAX handlers
50 + add_action('wp_ajax_mxchat_get_next_queue_item', array($this, 'ajax_mxchat_get_next_queue_item'));
51 + add_action('wp_ajax_mxchat_process_queue_item', array($this, 'ajax_mxchat_process_queue_item'));
52 + add_action('wp_ajax_mxchat_get_queue_status', array($this, 'ajax_mxchat_get_queue_status'));
53 + add_action('wp_ajax_mxchat_clear_queue', array($this, 'ajax_mxchat_clear_queue'));
54 + add_action('wp_ajax_mxchat_retry_failed', array($this, 'ajax_mxchat_retry_failed'));
55 + add_action('wp_ajax_mxchat_get_recent_entries', array($this, 'ajax_mxchat_get_recent_entries'));
56 + add_action('wp_ajax_mxchat_detect_sitemaps', array($this, 'ajax_mxchat_detect_sitemaps'));
57 + add_action('wp_ajax_mxchat_refresh_pinecone_entries', array($this, 'ajax_mxchat_refresh_pinecone_entries'));
58 + add_action('wp_ajax_mxchat_paginate_entries', array($this, 'ajax_mxchat_paginate_entries'));
59 +
60 + // Hook for content deletion
61 + add_action('mxchat_delete_content', array($this, 'mxchat_delete_from_pinecone_by_url'), 10, 1);
62 +
63 + // WordPress post management hooks
64 + add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
65 + add_action('post_updated', array($this, 'mxchat_handle_post_update'), 10, 3);
66 + add_action('before_delete_post', array($this, 'mxchat_handle_post_delete'));
67 + add_action('wp_trash_post', array($this, 'mxchat_handle_post_delete'));
68 +
69 + // ACF hook - fires AFTER ACF fields are saved, ensuring ACF data is available
70 + // Priority 20 to run after ACF's own save (which runs at priority 10)
71 + add_action('acf/save_post', array($this, 'mxchat_handle_acf_save'), 20);
72 +
73 + add_action('wp_ajax_mxchat_mark_queue_complete', array($this, 'ajax_mxchat_mark_queue_complete'));
74 +
75 + // WooCommerce product hooks (if WooCommerce is active)
76 + if (class_exists('WooCommerce')) {
77 + add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
78 + add_action('save_post_product', array($this, 'mxchat_handle_product_change'), 10, 3);
79 + add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
80 + add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
81 + }
82 +}
83 +
84 + /**
85 + * Get current options (refreshed)
86 + */
87 + private function mxchat_get_options() {
88 + if (empty($this->options)) {
89 + $this->options = get_option('mxchat_options', array());
90 + }
91 + return $this->options;
92 + }
93 +
94 +
95 + // ========================================
96 + // MAIN CONTENT SUBMISSION HANDLERS
97 + // ========================================
98 +
99 +public function mxchat_handle_content_submission() {
100 + // Check if the form was submitted and the user has permission.
101 + if (!isset($_POST['submit_content']) || !current_user_can('manage_options')) {
102 + return;
103 + }
104 +
105 + // Verify the nonce.
106 + $nonce = isset($_POST['mxchat_submit_content_nonce']) ? sanitize_text_field(wp_unslash($_POST['mxchat_submit_content_nonce'])) : '';
107 + if (!wp_verify_nonce($nonce, 'mxchat_submit_content_action')) {
108 + wp_die(esc_html__('Nonce verification failed.', 'mxchat'));
109 + }
110 +
111 + // Sanitize the inputs.
112 + // Use wp_kses_post to allow safe HTML and wp_unslash to remove WordPress added slashes
113 + $article_content = wp_kses_post(wp_unslash($_POST['article_content']));
114 + $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
115 +
116 + // Get bot_id from form submission
117 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
118 +
119 + // Get bot-specific options and API key
120 + $bot_options = $this->get_bot_options($bot_id);
121 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
122 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
123 +
124 + if (strpos($selected_model, 'voyage') === 0) {
125 + $api_key = $options['voyage_api_key'] ?? '';
126 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
127 + $api_key = $options['gemini_api_key'] ?? '';
128 + } else {
129 + $api_key = $options['api_key'] ?? '';
130 + }
131 +
132 + if (empty($api_key)) {
133 + set_transient('mxchat_admin_notice_error',
134 + esc_html__('API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
135 + 30
136 + );
137 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
138 + exit;
139 + }
140 +
141 + // Use centralized utility function with bot_id
142 + $result = MxChat_Utils::submit_content_to_db($article_content, $article_url, $api_key, null, $bot_id);
143 +
144 + if (is_wp_error($result)) {
145 + set_transient('mxchat_admin_notice_error',
146 + esc_html__('Error storing content: ', 'mxchat') . $result->get_error_message(),
147 + 30
148 + );
149 + } else {
150 + set_transient('mxchat_admin_notice_success',
151 + esc_html__('Content successfully submitted!', 'mxchat'),
152 + 30
153 + );
154 + }
155 +
156 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
157 + exit;
158 +}
159 +
160 +public function mxchat_is_pdf_url($url, $response) {
161 + $content_type = wp_remote_retrieve_header($response, 'content-type');
162 + $file_extension = strtolower(pathinfo($url, PATHINFO_EXTENSION));
163 +
164 + return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf';
165 +}
166 +
167 +
168 +public function mxchat_handle_pdf_for_knowledge_base($pdf_url, $response, $bot_id = 'default') {
169 + if (!current_user_can('manage_options')) {
170 + return false;
171 + }
172 +
173 + $pdf_url = esc_url_raw($pdf_url);
174 + $upload_dir = wp_upload_dir();
175 +
176 + if (isset($upload_dir['error']) && $upload_dir['error'] !== false) {
177 + return false;
178 + }
179 +
180 + $pdf_filename = sanitize_file_name('mxchat_kb_' . time() . '.pdf');
181 + $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
182 +
183 + $response_body = wp_remote_retrieve_body($response);
184 + if (empty($response_body)) {
185 + return false;
186 + }
187 +
188 + if (!wp_mkdir_p(dirname($pdf_path))) {
189 + return false;
190 + }
191 +
192 + try {
193 + file_put_contents($pdf_path, $response_body);
194 +
195 + if (!file_exists($pdf_path)) {
196 + throw new Exception(__('Failed to save PDF file', 'mxchat'));
197 + }
198 +
199 + $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
200 +
201 + if ($total_pages === false || $total_pages < 1) {
202 + throw new Exception(__('Invalid PDF: Unable to parse or no pages found', 'mxchat'));
203 + }
204 +
205 + // Create unique queue ID
206 + $queue_id = 'pdf_' . md5($pdf_url . time());
207 +
208 + // Create array of pages to process
209 + $pages = array();
210 + for ($i = 1; $i <= $total_pages; $i++) {
211 + $pages[] = array(
212 + 'pdf_path' => $pdf_path,
213 + 'pdf_url' => $pdf_url,
214 + 'page_number' => $i,
215 + 'total_pages' => $total_pages
216 + );
217 + }
218 +
219 + // Add pages to queue
220 + $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
221 +
222 + if ($queued_count === 0) {
223 + wp_delete_file($pdf_path);
224 + throw new Exception(__('Failed to add PDF pages to processing queue', 'mxchat'));
225 + }
226 +
227 + // Store queue metadata
228 + $this->mxchat_set_queue_meta($queue_id, 'source_url', $pdf_url);
229 + $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'pdf');
230 + $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_pages);
231 + $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
232 + $this->mxchat_set_queue_meta($queue_id, 'pdf_path', $pdf_path);
233 + $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
234 +
235 + // Store queue ID in transient for status tracking
236 + set_transient('mxchat_active_queue_pdf', $queue_id, DAY_IN_SECONDS);
237 + set_transient('mxchat_last_pdf_url', $pdf_url, DAY_IN_SECONDS);
238 +
239 + return 'queued';
240 +
241 + } catch (Exception $e) {
242 + if (file_exists($pdf_path)) {
243 + wp_delete_file($pdf_path);
244 + }
245 + return $e->getMessage();
246 + }
247 +}
248 +
249 +/**
250 + * Validate PDF and count pages with multiple parser attempts
251 + */
252 +private function mxchat_validate_and_count_pdf_pages($pdf_path) {
253 + // Method 1: Try with Smalot PDF Parser (your current method)
254 + try {
255 + $parser = new \Smalot\PdfParser\Parser();
256 + $pdf = $parser->parseFile($pdf_path);
257 + $pages = $pdf->getPages();
258 + $page_count = count($pages);
259 +
260 + if ($page_count > 0) {
261 + //error_log('PDF parsed successfully with Smalot parser: ' . $page_count . ' pages');
262 + return $page_count;
263 + }
264 + } catch (Exception $e) {
265 + //error_log('Smalot PDF parser failed: ' . $e->getMessage());
266 + }
267 +
268 + // Method 2: Try with pdfinfo command (if available)
269 + if (function_exists('shell_exec') && !$this->mxchat_is_shell_disabled()) {
270 + try {
271 + $command = 'pdfinfo ' . escapeshellarg($pdf_path) . ' 2>&1';
272 + $output = shell_exec($command);
273 +
274 + if ($output && preg_match('/Pages:\s*(\d+)/', $output, $matches)) {
275 + $page_count = intval($matches[1]);
276 + if ($page_count > 0) {
277 + //error_log('PDF parsed successfully with pdfinfo: ' . $page_count . ' pages');
278 + return $page_count;
279 + }
280 + }
281 + } catch (Exception $e) {
282 + //error_log('pdfinfo command failed: ' . $e->getMessage());
283 + }
284 + }
285 +
286 + // Method 3: Try to repair PDF and parse again
287 + try {
288 + $repaired_path = $this->mxchat_attempt_pdf_repair($pdf_path);
289 + if ($repaired_path && $repaired_path !== $pdf_path) {
290 + $parser = new \Smalot\PdfParser\Parser();
291 + $pdf = $parser->parseFile($repaired_path);
292 + $pages = $pdf->getPages();
293 + $page_count = count($pages);
294 +
295 + if ($page_count > 0) {
296 + // Replace original with repaired version
297 + copy($repaired_path, $pdf_path);
298 + unlink($repaired_path);
299 + //error_log('PDF repaired and parsed successfully: ' . $page_count . ' pages');
300 + return $page_count;
301 + }
302 +
303 + // Clean up repaired file if it didn't work
304 + unlink($repaired_path);
305 + }
306 + } catch (Exception $e) {
307 + //error_log('PDF repair attempt failed: ' . $e->getMessage());
308 + }
309 +
310 + // Method 4: Manual PDF structure analysis (basic page count)
311 + try {
312 + $page_count = $this->mxchat_manual_pdf_page_count($pdf_path);
313 + if ($page_count > 0) {
314 + //error_log('PDF page count determined manually: ' . $page_count . ' pages');
315 + return $page_count;
316 + }
317 + } catch (Exception $e) {
318 + //error_log('Manual PDF analysis failed: ' . $e->getMessage());
319 + }
320 +
321 + //error_log('All PDF parsing methods failed for: ' . $pdf_path);
322 + return false;
323 +}
324 +
325 +/**
326 + * Check if shell_exec is disabled
327 + */
328 +private function mxchat_is_shell_disabled() {
329 + $disabled = explode(',', ini_get('disable_functions'));
330 + return in_array('shell_exec', $disabled);
331 +}
332 +
333 +/**
334 + * Attempt to repair PDF using basic methods
335 + */
336 +private function mxchat_attempt_pdf_repair($pdf_path) {
337 + try {
338 + $content = file_get_contents($pdf_path);
339 + if (!$content) {
340 + return false;
341 + }
342 +
343 + // Check if PDF starts with proper header
344 + if (substr($content, 0, 4) !== '%PDF') {
345 + // Try to find PDF header in the content
346 + $header_pos = strpos($content, '%PDF');
347 + if ($header_pos !== false && $header_pos < 1024) {
348 + // Remove junk before PDF header
349 + $content = substr($content, $header_pos);
350 + $repaired_path = $pdf_path . '.repaired';
351 + file_put_contents($repaired_path, $content);
352 + return $repaired_path;
353 + }
354 + }
355 +
356 + // Check for EOF marker
357 + $content = rtrim($content);
358 + if (!preg_match('/%%EOF\s*$/', $content)) {
359 + // Add EOF marker if missing
360 + $content .= "\n%%EOF";
361 + $repaired_path = $pdf_path . '.repaired';
362 + file_put_contents($repaired_path, $content);
363 + return $repaired_path;
364 + }
365 +
366 + } catch (Exception $e) {
367 + //error_log('PDF repair error: ' . $e->getMessage());
368 + }
369 +
370 + return false;
371 +}
372 +
373 +/**
374 + * Manual PDF page counting by analyzing PDF structure
375 + */
376 +private function mxchat_manual_pdf_page_count($pdf_path) {
377 + try {
378 + $content = file_get_contents($pdf_path);
379 + if (!$content) {
380 + return 0;
381 + }
382 +
383 + // Method 1: Count /Type /Page objects
384 + $page_count = preg_match_all('/\/Type\s*\/Page[^s]/', $content);
385 + if ($page_count > 0) {
386 + return $page_count;
387 + }
388 +
389 + // Method 2: Look for /Count in pages object
390 + if (preg_match('/\/Type\s*\/Pages.*?\/Count\s+(\d+)/', $content, $matches)) {
391 + return intval($matches[1]);
392 + }
393 +
394 + // Method 3: Count page references
395 + $page_count = preg_match_all('/\d+\s+0\s+obj\s*<<[^>]*\/Type\s*\/Page/', $content);
396 + if ($page_count > 0) {
397 + return $page_count;
398 + }
399 +
400 + } catch (Exception $e) {
401 + //error_log('Manual PDF analysis error: ' . $e->getMessage());
402 + }
403 +
404 + return 0;
405 +}
406 +
407 +
408 +public function mxchat_save_inline_prompt() {
409 + // DEBUG: Log what we're receiving
410 + //error_log('=== MXCHAT DEBUG ===');
411 + //error_log('POST data: ' . print_r($_POST, true));
412 + //error_log('Nonce from POST: ' . ($_POST['_ajax_nonce'] ?? 'NOT FOUND'));
413 +
414 + // Check for nonce security
415 + check_ajax_referer('mxchat_save_inline_nonce', '_ajax_nonce');
416 +
417 + // If we get here, nonce passed
418 + //error_log('Nonce verification PASSED');
419 +
420 + // Verify permissions
421 + if (!current_user_can('manage_options')) {
422 + wp_send_json_error(esc_html__('Permission denied.', 'mxchat'));
423 + return;
424 + }
425 +
426 + global $wpdb;
427 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
428 +
429 + // Validate and sanitize input data
430 + $prompt_id = isset($_POST['id']) ? absint($_POST['id']) : 0;
431 + $article_content = isset($_POST['article_content']) ? wp_kses_post(wp_unslash($_POST['article_content'])) : '';
432 + $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
433 +
434 + if ($prompt_id > 0 && !empty($article_content)) {
435 + // Re-generate the embedding vector for the updated content
436 + $embedding_vector = $this->mxchat_generate_embedding($article_content);
437 + if (is_array($embedding_vector)) {
438 + // Serialize the embedding vector before storing it
439 + $embedding_vector_serialized = serialize($embedding_vector);
440 + // Update the prompt in the database
441 + $updated = $wpdb->update(
442 + $table_name,
443 + array(
444 + 'article_content' => $article_content,
445 + 'embedding_vector' => $embedding_vector_serialized,
446 + 'source_url' => $article_url,
447 + ),
448 + array('id' => $prompt_id),
449 + array('%s', '%s', '%s'),
450 + array('%d')
451 + );
452 + if ($updated !== false) {
453 + wp_send_json_success();
454 + } else {
455 + wp_send_json_error(esc_html__('Database update failed.', 'mxchat'));
456 + }
457 + } else {
458 + wp_send_json_error(esc_html__('Embedding generation failed.', 'mxchat'));
459 + }
460 + } else {
461 + wp_send_json_error(esc_html__('Invalid data.', 'mxchat'));
462 + }
463 +}
464 +
465 +
466 +public function mxchat_get_pdf_processing_status($pdf_url) {
467 + $pdf_url = esc_url_raw($pdf_url);
468 + $status = get_transient(sanitize_key('mxchat_pdf_status_' . md5($pdf_url)));
469 +
470 + if (!$status || !is_array($status)) {
471 + return false;
472 + }
473 +
474 + // Check for stalled processing (no updates for 5 minutes)
475 + if ($status['status'] === 'processing' && (time() - absint($status['last_update'])) > 300) {
476 + $status['status'] = 'error';
477 + $status['error'] = __('PDF processing appears to be stalled. No updates for over 5 minutes.', 'mxchat');
478 +
479 + // Save the updated status
480 + set_transient(
481 + sanitize_key('mxchat_pdf_status_' . md5($pdf_url)),
482 + array_map('sanitize_text_field', $status),
483 + DAY_IN_SECONDS
484 + );
485 + }
486 +
487 + $result = array(
488 + 'total_pages' => absint($status['total_pages']),
489 + 'processed_pages' => absint($status['processed_pages']),
490 + 'failed_pages' => absint($status['failed_pages'] ?? 0),
491 + 'percentage' => ($status['total_pages'] > 0)
492 + ? round((absint($status['processed_pages']) / absint($status['total_pages'])) * 100)
493 + : 0,
494 + 'status' => sanitize_text_field($status['status']),
495 + 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
496 + 'failed_pages_list' => isset($status['failed_pages_list']) ? $status['failed_pages_list'] : array(),
497 + 'completion_summary' => isset($status['completion_summary']) ? $status['completion_summary'] : null
498 + );
499 +
500 + // Add error message if present
501 + if (isset($status['error']) && !empty($status['error'])) {
502 + $result['error'] = sanitize_text_field($status['error']);
503 + }
504 +
505 + return $result;
506 +}
507 +
508 +
509 +public function mxchat_handle_sitemap_submission() {
510 + // Check if the form was submitted and verify permissions
511 + if (!isset($_POST['submit_sitemap']) || !current_user_can('manage_options')) {
512 + wp_die(esc_html__('Unauthorized access', 'mxchat'));
513 + }
514 +
515 + // Verify nonce
516 + check_admin_referer('mxchat_submit_sitemap_action', 'mxchat_submit_sitemap_nonce');
517 +
518 + // Validate URL
519 + if (!isset($_POST['sitemap_url']) || empty($_POST['sitemap_url'])) {
520 + set_transient('mxchat_admin_notice_error',
521 + esc_html__('Please provide a valid URL.', 'mxchat'),
522 + 30
523 + );
524 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
525 + exit;
526 + }
527 +
528 + $submitted_url = esc_url_raw($_POST['sitemap_url']);
529 +
530 + // Get bot_id from form submission
531 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
532 +
533 + // Get bot-specific options and validate API key
534 + $bot_options = $this->get_bot_options($bot_id);
535 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
536 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
537 +
538 + if (strpos($selected_model, 'voyage') === 0) {
539 + $api_key = $options['voyage_api_key'] ?? '';
540 + $provider_name = 'Voyage AI';
541 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
542 + $api_key = $options['gemini_api_key'] ?? '';
543 + $provider_name = 'Google Gemini';
544 + } else {
545 + $api_key = $options['api_key'] ?? '';
546 + $provider_name = 'OpenAI';
547 + }
548 +
549 + if (empty($api_key)) {
550 + $error_message = sprintf(
551 + esc_html__('%s API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
552 + $provider_name
553 + );
554 + set_transient('mxchat_admin_notice_error', $error_message, 30);
555 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
556 + exit;
557 + }
558 +
559 + // Fetch URL
560 + $response = wp_remote_get($submitted_url, array('timeout' => 30));
561 +
562 + if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
563 + $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response);
564 + set_transient('mxchat_admin_notice_error',
565 + sprintf(
566 + esc_html__('Failed to fetch the URL: %s', 'mxchat'),
567 + esc_html($error_message)
568 + ),
569 + 30
570 + );
571 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
572 + exit;
573 + }
574 +
575 + $content_type = wp_remote_retrieve_header($response, 'content-type');
576 + $body_content = wp_remote_retrieve_body($response);
577 +
578 + if (empty($body_content)) {
579 + set_transient('mxchat_admin_notice_error',
580 + esc_html__('Empty response received from URL.', 'mxchat'),
581 + 30
582 + );
583 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
584 + exit;
585 + }
586 +
587 + // Handle PDF URL
588 + if ($this->mxchat_is_pdf_url($submitted_url, $response)) {
589 + $result = $this->mxchat_handle_pdf_for_knowledge_base($submitted_url, $response, $bot_id);
590 +
591 + if ($result === 'queued') {
592 + set_transient('mxchat_admin_notice_success',
593 + esc_html__('PDF queued for processing. Processing will start automatically.', 'mxchat'),
594 + 30
595 + );
596 + } else {
597 + set_transient('mxchat_admin_notice_error',
598 + esc_html__('Failed to queue PDF processing: ', 'mxchat') . esc_html($result),
599 + 30
600 + );
601 + }
602 +
603 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
604 + exit;
605 + }
606 +
607 + // Handle Sitemap XML
608 + if (strpos($content_type, 'xml') !== false || strpos($body_content, '<urlset') !== false) {
609 + libxml_use_internal_errors(true);
610 + $xml = simplexml_load_string($body_content);
611 + $xml_errors = libxml_get_errors();
612 + libxml_clear_errors();
613 +
614 + if ($xml === false || !empty($xml_errors)) {
615 + set_transient('mxchat_admin_notice_error',
616 + esc_html__('Invalid sitemap XML. Please provide a valid sitemap.', 'mxchat'),
617 + 30
618 + );
619 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
620 + exit;
621 + }
622 +
623 + $result = $this->mxchat_handle_sitemap_for_knowledge_base($xml, $submitted_url, $bot_id);
624 +
625 + if ($result === 'queued') {
626 + set_transient('mxchat_admin_notice_success',
627 + esc_html__('Sitemap queued for processing. Processing will start automatically.', 'mxchat'),
628 + 30
629 + );
630 + } else {
631 + set_transient('mxchat_admin_notice_error',
632 + esc_html__('Failed to queue sitemap processing. Please check the status below for details.', 'mxchat'),
633 + 30
634 + );
635 + }
636 +
637 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
638 + exit;
639 + }
640 +
641 + // Handle Regular URL (single page)
642 + $page_content = $this->mxchat_extract_main_content($body_content);
643 + $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
644 +
645 + error_log('[MXCHAT-URL-DEBUG] URL: ' . $submitted_url);
646 + error_log('[MXCHAT-URL-DEBUG] Extracted page_content length: ' . strlen($page_content));
647 + error_log('[MXCHAT-URL-DEBUG] Sanitized content length: ' . strlen($sanitized_content));
648 + error_log('[MXCHAT-URL-DEBUG] Content preview: ' . substr($sanitized_content, 0, 500));
649 +
650 + if (empty($sanitized_content)) {
651 + set_transient('mxchat_admin_notice_error',
652 + esc_html__('No valid content found on the provided URL.', 'mxchat'),
653 + 30
654 + );
655 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
656 + exit;
657 + }
658 +
659 + // For single URLs, process immediately using submit_content_to_db
660 + // This handles chunking automatically for large content
661 + $db_result = MxChat_Utils::submit_content_to_db(
662 + $sanitized_content,
663 + $submitted_url,
664 + $api_key,
665 + null,
666 + $bot_id,
667 + 'url' // content_type
668 + );
669 +
670 + if (is_wp_error($db_result)) {
671 + $error_message = esc_html__('Failed to store content in database: ', 'mxchat') . esc_html($db_result->get_error_message());
672 + set_transient('mxchat_admin_notice_error', $error_message, 30);
673 + } else {
674 + $success_message = esc_html__('URL content successfully submitted!', 'mxchat');
675 + set_transient('mxchat_admin_notice_success', $success_message, 30);
676 + }
677 +
678 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
679 + exit;
680 +}
681 +
682 +
683 +public function mxchat_get_single_url_status() {
684 + $status = get_transient('mxchat_single_url_status');
685 + if (!$status) {
686 + return null;
687 + }
688 +
689 + // Add human-readable time
690 + if (isset($status['timestamp'])) {
691 + $status['human_time'] = human_time_diff(strtotime($status['timestamp']), current_time('timestamp')) . ' ' . __('ago', 'mxchat');
692 + }
693 +
694 + return $status;
695 +}
696 +
697 +public function mxchat_handle_sitemap_for_knowledge_base($xml, $sitemap_url, $bot_id = 'default') {
698 + if (!current_user_can('manage_options')) {
699 + return false;
700 + }
701 +
702 + try {
703 + $sitemap_url = esc_url_raw($sitemap_url);
704 +
705 + if (!$xml || !is_object($xml)) {
706 + throw new Exception(__('Invalid XML object provided', 'mxchat'));
707 + }
708 +
709 + // Get bot-specific embedding API for validation
710 + $bot_options = $this->get_bot_options($bot_id);
711 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
712 +
713 + // Test the embedding API before processing
714 + $test_phrase = "Test embedding generation for MxChat";
715 + $test_result = $this->mxchat_generate_embedding($test_phrase, $bot_id);
716 +
717 + if (is_string($test_result)) {
718 + throw new Exception(__('Embedding API validation failed: ', 'mxchat') . $test_result);
719 + }
720 +
721 + if (!is_array($test_result)) {
722 + throw new Exception(__('Embedding API returned unexpected result type. Please check your configuration.', 'mxchat'));
723 + }
724 +
725 + // Extract URLs from sitemap
726 + $urls = array();
727 + foreach ($xml->url as $url_element) {
728 + $url = esc_url_raw((string)$url_element->loc);
729 + if ($url) {
730 + $urls[] = array('url' => $url);
731 + }
732 + }
733 +
734 + $total_urls = count($urls);
735 +
736 + if ($total_urls < 1) {
737 + throw new Exception(__('No valid URLs found in sitemap', 'mxchat'));
738 + }
739 +
740 + // Create unique queue ID
741 + $queue_id = 'sitemap_' . md5($sitemap_url . time());
742 +
743 + // Add URLs to queue
744 + $queued_count = $this->mxchat_add_to_queue($queue_id, 'url', $urls, $bot_id);
745 +
746 + if ($queued_count === 0) {
747 + throw new Exception(__('Failed to add URLs to processing queue', 'mxchat'));
748 + }
749 +
750 + // Store queue metadata
751 + $this->mxchat_set_queue_meta($queue_id, 'source_url', $sitemap_url);
752 + $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'sitemap');
753 + $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_urls);
754 + $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
755 + $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
756 +
757 + // Store queue ID in transient for status tracking
758 + set_transient('mxchat_active_queue_sitemap', $queue_id, DAY_IN_SECONDS);
759 + set_transient('mxchat_last_sitemap_url', $sitemap_url, DAY_IN_SECONDS);
760 +
761 + return 'queued';
762 +
763 + } catch (Exception $e) {
764 + $error_message = $e->getMessage();
765 + error_log(sprintf(esc_html__('Error preparing sitemap for processing: %s', 'mxchat'), esc_html($error_message)));
766 +
767 + return $error_message;
768 + }
769 +
770 +}
771 +
772 +/**
773 + * Remove shortcode tags but preserve the content inside them
774 + * Example: [vc_column]Hello World[/vc_column] becomes "Hello World"
775 + *
776 + * @param string $content The content containing shortcodes
777 + * @return string Content with shortcode tags removed but inner content preserved
778 + */
779 +private function strip_shortcode_tags_preserve_content($content) {
780 + // Handle nested shortcodes by running multiple passes
781 + $prev_content = '';
782 + $max_iterations = 10; // Prevent infinite loops
783 + $iteration = 0;
784 + while ($prev_content !== $content && $iteration < $max_iterations) {
785 + $prev_content = $content;
786 + // Replace paired shortcodes [tag]content[/tag] with just the content
787 + $content = preg_replace('/\[([a-zA-Z0-9_-]+)[^\]]*\](.*?)\[\/\1\]/s', '$2', $content);
788 + $iteration++;
789 + }
790 + // Remove self-closing shortcodes [tag /] or [tag attr="val" /]
791 + $content = preg_replace('/\[[a-zA-Z0-9_-]+[^\]]*\/\]/', '', $content);
792 + // Remove any remaining opening shortcode tags [tag] or [tag attr="val"]
793 + $content = preg_replace('/\[[a-zA-Z0-9_-]+[^\]]*\]/', '', $content);
794 +
795 + return $content;
796 +}
797 +
798 +public function mxchat_sanitize_content_for_api($content) {
799 + //error_log('[MXCHAT-SANITIZE] Original content preview: ' . substr($content, 0, 500) . '...');
800 +
801 + // Remove shortcode tags but PRESERVE content inside them
802 + $content = $this->strip_shortcode_tags_preserve_content($content);
803 +
804 + // Remove script, style tags, and HTML comments
805 + $content = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $content);
806 + $content = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $content);
807 + $content = preg_replace('/<!--(.|\s)*?-->/', '', $content);
808 +
809 + // Remove all HTML tags and decode HTML entities
810 + $content = wp_strip_all_tags($content);
811 + $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5);
812 +
813 + // Normalize whitespace but preserve paragraph breaks
814 + // First, normalize line endings to \n
815 + $content = str_replace(["\r\n", "\r"], "\n", $content);
816 + // Replace multiple spaces/tabs with single space, but preserve newlines
817 + $content = preg_replace('/[ \t]+/', ' ', $content);
818 + // Replace 3+ newlines with 2 newlines (max 2 blank lines)
819 + $content = preg_replace('/\n{3,}/', "\n\n", $content);
820 + // Trim each line
821 + $lines = explode("\n", $content);
822 + $lines = array_map('trim', $lines);
823 + $content = implode("\n", $lines);
824 + // Final trim
825 + $content = trim($content);
826 +
827 + // Remove control characters (which can cause database issues) but preserve \n (0x0A) and \r (0x0D)
828 + $content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $content);
829 +
830 + // Remove NULL bytes which can cause database errors
831 + $content = str_replace("\0", "", $content);
832 +
833 + // Ensure valid UTF-8 encoding
834 + $content = wp_check_invalid_utf8($content);
835 +
836 + // Remove any extremely long strings without spaces (often garbage)
837 + $content = preg_replace('/\S{300,}/', ' ', $content);
838 +
839 + // Replace problematic characters that often cause database issues
840 + $content = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $content); // Remove emoji and other high Unicode characters
841 +
842 + // Replace any remaining potentially problematic characters with spaces
843 + // BUT preserve newlines by temporarily replacing them
844 + $content = str_replace("\n", "NEWLINE_PLACEHOLDER", $content);
845 + $content = preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\p{Sm}]/u', ' ', $content);
846 + $content = str_replace("NEWLINE_PLACEHOLDER", "\n", $content);
847 +
848 + // Limit to reasonable length if needed
849 + $max_length = 65000; // Just under MySQL TEXT field limit
850 + if (strlen($content) > $max_length) {
851 + $content = substr($content, 0, $max_length);
852 + }
853 +
854 + //error_log('[MXCHAT-SANITIZE] Sanitized content preview: ' . substr($content, 0, 500) . '...');
855 + return $content;
856 +}
857 +public function mxchat_extract_main_content($html) {
858 + if (empty($html)) {
859 + return '';
860 + }
861 + try {
862 + $dom = new DOMDocument;
863 + libxml_use_internal_errors(true); // Suppress HTML parsing errors
864 + @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
865 + $xpath = new DOMXPath($dom);
866 +
867 + // For debugging purposes
868 + $debugEnabled = true; // Set to true to enable debugging output
869 + $debug = function($message) use ($debugEnabled) {
870 + if ($debugEnabled) {
871 + error_log('[MXCHAT-EXTRACT-DEBUG] ' . $message);
872 + }
873 + };
874 +
875 + // Direct targeting for Gerow theme posts
876 + $post_text = $xpath->query('//div[contains(@class, "post-text")]');
877 + if ($post_text && $post_text->length > 0) {
878 + $debug("Found post-text directly");
879 + $content = '';
880 + foreach ($post_text as $node) {
881 + $content .= $dom->saveHTML($node);
882 + }
883 + if (!empty($content)) {
884 + $debug("Returning post-text content");
885 + return $content;
886 + }
887 + }
888 +
889 + // Try to get the blog details content which contains the post-text
890 + $blog_details = $xpath->query('//div[contains(@class, "blog-details-content")]');
891 + if ($blog_details && $blog_details->length > 0) {
892 + $debug("Found blog-details-content");
893 + $content = '';
894 + foreach ($blog_details as $node) {
895 + $content .= $dom->saveHTML($node);
896 + }
897 + if (!empty($content)) {
898 + $debug("Returning blog-details-content");
899 + return $content;
900 + }
901 + }
902 +
903 + // Try to get the article which contains the blog details
904 + $article = $xpath->query('//article[contains(@class, "blog-details-wrap")]');
905 + if ($article && $article->length > 0) {
906 + $debug("Found article with blog-details-wrap");
907 + $content = '';
908 + foreach ($article as $node) {
909 + $content .= $dom->saveHTML($node);
910 + }
911 + if (!empty($content)) {
912 + $debug("Returning article content");
913 + return $content;
914 + }
915 + }
916 +
917 + // Try even broader with the blog-item-wrap
918 + $blog_item = $xpath->query('//div[contains(@class, "blog-item-wrap")]');
919 + if ($blog_item && $blog_item->length > 0) {
920 + $debug("Found blog-item-wrap");
921 + $content = '';
922 + foreach ($blog_item as $node) {
923 + $content .= $dom->saveHTML($node);
924 + }
925 + if (!empty($content)) {
926 + $debug("Returning blog-item-wrap content");
927 + return $content;
928 + }
929 + }
930 +
931 + // Specific Gerow theme path
932 + $gerow_path = $xpath->query('//section[contains(@class, "blog-area")]//div[contains(@class, "post-text")]');
933 + if ($gerow_path && $gerow_path->length > 0) {
934 + $debug("Found Gerow theme path to post-text");
935 + $content = '';
936 + foreach ($gerow_path as $node) {
937 + $content .= $dom->saveHTML($node);
938 + }
939 + if (!empty($content)) {
940 + $debug("Returning Gerow post-text content");
941 + return $content;
942 + }
943 + }
944 +
945 + // Generic blog post selectors
946 + $selectors = [
947 + // Blog post specific selectors
948 + '//div[contains(@class, "post-text")]',
949 + '//article[contains(@class, "blog-post-item")]//div[contains(@class, "post-text")]',
950 + '//div[contains(@class, "blog-details-content")]',
951 + '//article[contains(@class, "blog-details-wrap")]',
952 + '//div[contains(@class, "entry-content")]',
953 + '//div[contains(@class, "blog-content")]',
954 + '//div[contains(@class, "blog-item-wrap")]',
955 +
956 + // More general content selectors
957 + '//div[contains(@class, "page__content")]',
958 + '//div[contains(@class, "elementor-widget-container")]',
959 + '//div[contains(@class, "elementor-text-editor")]',
960 + '//div[contains(@class, "elementor-widget-text-editor")]',
961 + '//*[contains(@class, "entry-content")]',
962 + '//*[contains(@class, "post-content")]',
963 + '//*[contains(@class, "article-content")]',
964 + '//*[@id="content"]',
965 + '//*[@id="main-content"]',
966 + '//section[contains(@class, "blog-area")]',
967 + '//article',
968 + '//main',
969 + '//div[contains(@class, "content")]'
970 + ];
971 +
972 + // First handle Elementor content - get only leaf widget containers to avoid duplicates
973 + $debug("Checking for Elementor content");
974 + // Get widget containers that are direct children of widgets (not nested inside other widget containers)
975 + $elementor_widgets = $xpath->query('//div[contains(@class, "elementor-widget")]//div[contains(@class, "elementor-widget-container")]');
976 + if ($elementor_widgets && $elementor_widgets->length > 0) {
977 + $debug("Found Elementor widgets");
978 + $seen_content = array(); // Track seen content to avoid duplicates
979 + $combined_content = '';
980 + foreach ($elementor_widgets as $widget) {
981 + $widget_content = $dom->saveHTML($widget);
982 + if (!empty($widget_content)) {
983 + // Create a hash of the content to detect duplicates
984 + $content_hash = md5($widget_content);
985 + if (!isset($seen_content[$content_hash])) {
986 + $seen_content[$content_hash] = true;
987 + $combined_content .= $widget_content;
988 + }
989 + }
990 + }
991 + if (!empty($combined_content)) {
992 + $debug("Returning Elementor content");
993 + return $combined_content;
994 + }
995 + }
996 +
997 + // Try standard selectors one by one
998 + foreach ($selectors as $selector) {
999 + $debug("Trying selector: " . $selector);
1000 + $nodes = $xpath->query($selector);
1001 + if ($nodes && $nodes->length > 0) {
1002 + $debug("Found " . $nodes->length . " matches for selector: " . $selector);
1003 + // Only take the FIRST matching node to avoid duplicate content
1004 + // (pages often have nested or multiple containers with same class)
1005 + $content = $dom->saveHTML($nodes->item(0));
1006 + if (!empty($content)) {
1007 + $debug("Returning content from selector: " . $selector . " (first match only)");
1008 + return $content;
1009 + }
1010 + }
1011 + }
1012 +
1013 + // Manual regex fallback for post-text if DOM methods fail
1014 + $debug("Trying regex fallback");
1015 + if (preg_match('/<div class="post-text">(.*?)<\/div>\s*<\/div>\s*<\/div>/s', $html, $matches)) {
1016 + $debug("Found post-text via regex");
1017 + return '<div class="post-text">' . $matches[1] . '</div>';
1018 + }
1019 +
1020 + // Try to extract the blog section as a whole
1021 + $blog_section = $xpath->query('//section[contains(@class, "blog-area")]');
1022 + if ($blog_section && $blog_section->length > 0) {
1023 + $debug("Found blog-area section");
1024 + $content = '';
1025 + foreach ($blog_section as $node) {
1026 + $content .= $dom->saveHTML($node);
1027 + }
1028 + if (!empty($content)) {
1029 + $debug("Returning blog-area section content");
1030 + return $content;
1031 + }
1032 + }
1033 +
1034 + // Fallback: Return the body content if no specific selector matches
1035 + $debug("Using body fallback");
1036 + $body = $dom->getElementsByTagName('body');
1037 + if ($body->length > 0) {
1038 + return $dom->saveHTML($body->item(0));
1039 + }
1040 +
1041 + // Last resort: return the original HTML
1042 + $debug("Returning original HTML");
1043 + return $html;
1044 + } catch (Exception $e) {
1045 + //error_log('[MXCHAT-ERROR] Content extraction failed: ' . $e->getMessage());
1046 + return $html; // Return original HTML if parsing fails
1047 + } finally {
1048 + libxml_clear_errors();
1049 + }
1050 +}
1051 +public function mxchat_get_sitemap_processing_status($sitemap_url) {
1052 + $sitemap_url = esc_url_raw($sitemap_url);
1053 + $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
1054 + $status = get_transient($status_key);
1055 +
1056 + if (!$status || !is_array($status)) {
1057 + return false;
1058 + }
1059 +
1060 + // Auto-complete check: if all URLs are processed but status isn't complete
1061 + if (isset($status['processed_urls']) && isset($status['total_urls']) &&
1062 + $status['processed_urls'] >= $status['total_urls'] &&
1063 + isset($status['status']) && $status['status'] !== 'complete' &&
1064 + $status['status'] !== 'error') {
1065 +
1066 + // Mark as complete
1067 + $status['status'] = 'complete';
1068 + $status['processed_urls'] = $status['total_urls']; // Ensure exact match
1069 +
1070 + // Update the transient with the corrected status
1071 + set_transient($status_key, $status, DAY_IN_SECONDS);
1072 + }
1073 +
1074 + return array(
1075 + 'total_urls' => absint($status['total_urls']),
1076 + 'processed_urls' => absint($status['processed_urls']),
1077 + 'failed_urls' => absint($status['failed_urls'] ?? 0),
1078 + 'percentage' => ($status['total_urls'] > 0)
1079 + ? round((absint($status['processed_urls']) / absint($status['total_urls'])) * 100)
1080 + : 0,
1081 + 'status' => sanitize_text_field($status['status']),
1082 + 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
1083 + 'error' => isset($status['error']) ? sanitize_text_field($status['error']) : '',
1084 + 'last_error' => isset($status['last_error']) ? sanitize_text_field($status['last_error']) : '',
1085 + 'failed_urls_list' => isset($status['failed_urls_list']) ? $status['failed_urls_list'] : array()
1086 + );
1087 +}
1088 +
1089 +public function mxchat_ajax_get_status_updates() {
1090 + try {
1091 + // Verify the request
1092 + check_ajax_referer('mxchat_status_nonce', 'nonce');
1093 +
1094 + // Get active queue IDs
1095 + $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
1096 + $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
1097 +
1098 + $sitemap_status = false;
1099 + $pdf_status = false;
1100 +
1101 + // Get sitemap queue status
1102 + if ($sitemap_queue_id) {
1103 + $sitemap_status = $this->mxchat_get_queue_status_data($sitemap_queue_id, 'sitemap');
1104 + }
1105 +
1106 + // Get PDF queue status
1107 + if ($pdf_queue_id) {
1108 + $pdf_status = $this->mxchat_get_queue_status_data($pdf_queue_id, 'pdf');
1109 + }
1110 +
1111 + $is_active_processing =
1112 + ($sitemap_status && $sitemap_status['status'] === 'processing') ||
1113 + ($pdf_status && $pdf_status['status'] === 'processing');
1114 +
1115 + // Return JSON response with the status data
1116 + wp_send_json(array(
1117 + 'pdf_status' => $pdf_status,
1118 + 'sitemap_status' => $sitemap_status,
1119 + 'is_processing' => $is_active_processing,
1120 + 'sitemap_queue_id' => $sitemap_queue_id,
1121 + 'pdf_queue_id' => $pdf_queue_id
1122 + ));
1123 +
1124 + } catch (Exception $e) {
1125 + error_log('MxChat Status Update Error: ' . $e->getMessage());
1126 +
1127 + wp_send_json_error(array(
1128 + 'message' => 'Error getting status updates: ' . $e->getMessage(),
1129 + 'status' => 'error'
1130 + ));
1131 + }
1132 +}
1133 +
1134 +/**
1135 + * Helper function to get queue status data
1136 + */
1137 +private function mxchat_get_queue_status_data($queue_id, $type = 'sitemap') {
1138 + global $wpdb;
1139 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
1140 +
1141 + // Get counts by status
1142 + $counts = $wpdb->get_results($wpdb->prepare(
1143 + "SELECT status, COUNT(*) as count
1144 + FROM $table_name
1145 + WHERE queue_id = %s
1146 + GROUP BY status",
1147 + $queue_id
1148 + ), OBJECT_K);
1149 +
1150 + $total = 0;
1151 + $completed = 0;
1152 + $failed = 0;
1153 + $processing = 0;
1154 + $pending = 0;
1155 +
1156 + foreach ($counts as $status => $data) {
1157 + $count = absint($data->count);
1158 + $total += $count;
1159 +
1160 + switch ($status) {
1161 + case 'completed':
1162 + $completed = $count;
1163 + break;
1164 + case 'failed':
1165 + $failed = $count;
1166 + break;
1167 + case 'processing':
1168 + $processing = $count;
1169 + break;
1170 + case 'pending':
1171 + $pending = $count;
1172 + break;
1173 + }
1174 + }
1175 +
1176 + if ($total === 0) {
1177 + return false;
1178 + }
1179 +
1180 + // Calculate percentage
1181 + $percentage = round((($completed + $failed) / $total) * 100);
1182 +
1183 + // Get failed items details (limit to 50)
1184 + $failed_items = array();
1185 + if ($failed > 0) {
1186 + $failed_results = $wpdb->get_results($wpdb->prepare(
1187 + "SELECT item_type, item_data, error_message, attempts, completed_at
1188 + FROM $table_name
1189 + WHERE queue_id = %s
1190 + AND status = 'failed'
1191 + AND attempts >= max_attempts
1192 + ORDER BY id DESC
1193 + LIMIT 50",
1194 + $queue_id
1195 + ));
1196 +
1197 + foreach ($failed_results as $item) {
1198 + $data = json_decode($item->item_data, true);
1199 + $url = $type === 'pdf' ? ($data['pdf_url'] ?? '') : ($data['url'] ?? '');
1200 +
1201 + $failed_items[] = array(
1202 + 'url' => $url,
1203 + 'page' => $type === 'pdf' ? ($data['page_number'] ?? 0) : 0,
1204 + 'error' => $item->error_message,
1205 + 'retries' => $item->attempts,
1206 + 'time' => strtotime($item->completed_at)
1207 + );
1208 + }
1209 + }
1210 +
1211 + // Get queue metadata
1212 + $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
1213 +
1214 + // Determine if queue is complete
1215 + $is_complete = ($pending === 0 && $processing === 0);
1216 +
1217 + // Get last update time
1218 + $last_update = $wpdb->get_var($wpdb->prepare(
1219 + "SELECT MAX(UNIX_TIMESTAMP(COALESCE(completed_at, started_at, created_at)))
1220 + FROM $table_name
1221 + WHERE queue_id = %s",
1222 + $queue_id
1223 + ));
1224 +
1225 + $last_update_text = $last_update ? human_time_diff($last_update, time()) . ' ' . __('ago', 'mxchat') : __('Just now', 'mxchat');
1226 +
1227 + // Format based on type
1228 + if ($type === 'pdf') {
1229 + return array(
1230 + 'total_pages' => $total,
1231 + 'processed_pages' => $completed + $failed,
1232 + 'failed_pages' => $failed,
1233 + 'percentage' => $percentage,
1234 + 'status' => $is_complete ? 'complete' : 'processing',
1235 + 'last_update' => $last_update_text,
1236 + 'failed_pages_list' => $failed_items,
1237 + 'pdf_url' => $source_url,
1238 + 'queue_id' => $queue_id
1239 + );
1240 + } else {
1241 + return array(
1242 + 'total_urls' => $total,
1243 + 'processed_urls' => $completed + $failed,
1244 + 'failed_urls' => $failed,
1245 + 'percentage' => $percentage,
1246 + 'status' => $is_complete ? 'complete' : 'processing',
1247 + 'last_update' => $last_update_text,
1248 + 'failed_urls_list' => $failed_items,
1249 + 'sitemap_url' => $source_url,
1250 + 'queue_id' => $queue_id
1251 + );
1252 + }
1253 +}
1254 +
1255 +/**
1256 + * Public method to get processing status for both sitemap and PDF queues
1257 + * Used by admin pages to display processing status
1258 + *
1259 + * @return array Array with 'sitemap_status', 'pdf_status', and 'is_processing' keys
1260 + */
1261 +public function mxchat_get_processing_statuses() {
1262 + $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
1263 + $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
1264 +
1265 + $sitemap_status = false;
1266 + $pdf_status = false;
1267 +
1268 + if ($sitemap_queue_id) {
1269 + $sitemap_status = $this->mxchat_get_queue_status_data($sitemap_queue_id, 'sitemap');
1270 + }
1271 +
1272 + if ($pdf_queue_id) {
1273 + $pdf_status = $this->mxchat_get_queue_status_data($pdf_queue_id, 'pdf');
1274 + }
1275 +
1276 + $is_processing =
1277 + ($sitemap_status && $sitemap_status['status'] === 'processing') ||
1278 + ($pdf_status && $pdf_status['status'] === 'processing');
1279 +
1280 + return array(
1281 + 'sitemap_status' => $sitemap_status,
1282 + 'pdf_status' => $pdf_status,
1283 + 'is_processing' => $is_processing
1284 + );
1285 +}
1286 +
1287 +/**
1288 + * AJAX handler to get recent knowledge entries for real-time table updates
1289 + * UPDATED: Now supports both WordPress DB and Pinecone data sources
1290 + */
1291 +public function ajax_mxchat_get_recent_entries() {
1292 + check_ajax_referer('mxchat_entries_nonce', 'nonce');
1293 +
1294 + if (!current_user_can('manage_options')) {
1295 + wp_send_json_error(array('message' => 'Unauthorized'));
1296 + return;
1297 + }
1298 +
1299 + global $wpdb;
1300 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1301 +
1302 + // Get parameters
1303 + $last_id = isset($_POST['last_id']) ? absint($_POST['last_id']) : 0;
1304 + $limit = isset($_POST['limit']) ? min(absint($_POST['limit']), 50) : 10;
1305 + $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
1306 +
1307 + // Check if Pinecone is enabled for this bot
1308 + $pinecone_manager = $this->mxchat_get_pinecone_manager();
1309 + $pinecone_options = $pinecone_manager ? $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id) : array();
1310 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1311 + $has_pinecone_api = !empty($pinecone_options['mxchat_pinecone_api_key']);
1312 +
1313 + if ($use_pinecone && $has_pinecone_api) {
1314 + // PINECONE DATA SOURCE - Get grouped entry count (not raw vector count)
1315 + // Use mxchat_fetch_pinecone_records which returns total_unique_entries
1316 + $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, '', 1, 10, $bot_id, '');
1317 + $total_count = $records['total'] ?? 0;
1318 +
1319 + // For Pinecone, we don't return individual entries during polling
1320 + // (entries are already displayed on page load via mxchat_fetch_pinecone_records)
1321 + // We just return the updated count
1322 + wp_send_json_success(array(
1323 + 'entries' => array(),
1324 + 'total_count' => absint($total_count),
1325 + 'max_id' => $last_id,
1326 + 'data_source' => 'pinecone'
1327 + ));
1328 + return;
1329 + }
1330 +
1331 + // WORDPRESS DB DATA SOURCE
1332 + // Build query to get entries newer than last_id
1333 + $where_clauses = array('1=1');
1334 + $where_values = array();
1335 +
1336 + if ($last_id > 0) {
1337 + $where_clauses[] = 'id > %d';
1338 + $where_values[] = $last_id;
1339 + }
1340 +
1341 + // Note: WordPress DB table doesn't have bot_id column
1342 + // Multi-bot filtering is handled via Pinecone namespaces
1343 +
1344 + $where_sql = implode(' AND ', $where_clauses);
1345 +
1346 + // Get recent entries
1347 + $query = "SELECT id, article_content, source_url, timestamp
1348 + FROM $table_name
1349 + WHERE $where_sql
1350 + ORDER BY id DESC
1351 + LIMIT %d";
1352 +
1353 + $where_values[] = $limit;
1354 +
1355 + $entries = $wpdb->get_results($wpdb->prepare($query, $where_values));
1356 +
1357 + // Get total count of GROUPED entries (by source_url) - matches pagination display
1358 + // Count unique source_urls (excluding mxchat:// internal refs) + count of ungrouped rows
1359 + $total_count = $wpdb->get_var(
1360 + "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '' AND source_url NOT LIKE 'mxchat://%') +
1361 + (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')"
1362 + );
1363 +
1364 + // Format entries for response
1365 + $formatted_entries = array();
1366 + $preview_length = 150;
1367 + foreach ($entries as $entry) {
1368 + // Parse chunk metadata using the proper chunker method (same as initial page load)
1369 + if (class_exists('MxChat_Chunker')) {
1370 + $chunk_meta = MxChat_Chunker::parse_stored_chunk($entry->article_content);
1371 + $display_content = $chunk_meta['text'];
1372 + $chunk_metadata = $chunk_meta['metadata'];
1373 + } else {
1374 + $display_content = $entry->article_content;
1375 + $chunk_metadata = array();
1376 + }
1377 +
1378 + $content_preview = mb_strlen($display_content) > $preview_length
1379 + ? mb_substr($display_content, 0, $preview_length) . '...'
1380 + : $display_content;
1381 +
1382 + $formatted_entries[] = array(
1383 + 'id' => $entry->id,
1384 + 'preview' => esc_html($content_preview),
1385 + 'full_content' => wp_kses_post(wpautop($display_content)),
1386 + 'content_length' => mb_strlen($display_content),
1387 + 'preview_length' => $preview_length,
1388 + 'source_url' => $entry->source_url,
1389 + 'has_link' => !empty($entry->source_url) && strpos($entry->source_url, 'mxchat://') !== 0,
1390 + 'chunk_metadata' => $chunk_metadata,
1391 + 'delete_nonce' => wp_create_nonce('mxchat_delete_prompt_nonce')
1392 + );
1393 + }
1394 +
1395 + wp_send_json_success(array(
1396 + 'entries' => $formatted_entries,
1397 + 'total_count' => absint($total_count),
1398 + 'max_id' => !empty($entries) ? $entries[0]->id : $last_id,
1399 + 'data_source' => 'wordpress'
1400 + ));
1401 +}
1402 +
1403 +/**
1404 + * Get Pinecone total count from stats API
1405 + * Helper function for ajax_mxchat_get_recent_entries
1406 + */
1407 +private function mxchat_get_pinecone_count_from_stats($pinecone_options) {
1408 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1409 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1410 + $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
1411 +
1412 + if (empty($api_key) || empty($host)) {
1413 + return 0;
1414 + }
1415 +
1416 + try {
1417 + $stats_url = "https://{$host}/describe_index_stats";
1418 +
1419 + $response = wp_remote_post($stats_url, array(
1420 + 'headers' => array(
1421 + 'Api-Key' => $api_key,
1422 + 'Content-Type' => 'application/json'
1423 + ),
1424 + 'body' => '{}',
1425 + 'timeout' => 10
1426 + ));
1427 +
1428 + if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
1429 + $body = wp_remote_retrieve_body($response);
1430 + $stats_data = json_decode($body, true);
1431 +
1432 + // If namespace is specified, get count from that specific namespace
1433 + if (!empty($namespace) && isset($stats_data['namespaces'][$namespace]['vectorCount'])) {
1434 + return intval($stats_data['namespaces'][$namespace]['vectorCount']);
1435 + }
1436 +
1437 + // If no namespace specified or namespace not found in response, use total
1438 + return intval($stats_data['totalVectorCount'] ?? 0);
1439 + }
1440 +
1441 + return 0;
1442 +
1443 + } catch (Exception $e) {
1444 + return 0;
1445 + }
1446 +}
1447 +
1448 +/**
1449 + * AJAX handler to refresh Pinecone entries table via AJAX
1450 + * Returns the table HTML for updating the UI without a full page reload
1451 + */
1452 +public function ajax_mxchat_refresh_pinecone_entries() {
1453 + check_ajax_referer('mxchat_entries_nonce', 'nonce');
1454 +
1455 + if (!current_user_can('manage_options')) {
1456 + wp_send_json_error(array('message' => 'Unauthorized'));
1457 + return;
1458 + }
1459 +
1460 + $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
1461 + $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
1462 + $per_page = 10;
1463 +
1464 + // Get Pinecone manager and options
1465 + $pinecone_manager = $this->mxchat_get_pinecone_manager();
1466 + if (!$pinecone_manager) {
1467 + wp_send_json_error(array('message' => 'Pinecone manager not available'));
1468 + return;
1469 + }
1470 +
1471 + $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
1472 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1473 + $pinecone_api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1474 +
1475 + if (!$use_pinecone || empty($pinecone_api_key)) {
1476 + wp_send_json_error(array('message' => 'Pinecone not configured'));
1477 + return;
1478 + }
1479 +
1480 + // Fetch records from Pinecone
1481 + $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, '', $page, $per_page, $bot_id, '');
1482 + $prompts = $records['data'] ?? array();
1483 + $total_records = $records['total'] ?? 0;
1484 +
1485 + // Group prompts by source_url
1486 + $grouped_prompts = array();
1487 + foreach ($prompts as $prompt) {
1488 + $source_url = '';
1489 + if (!empty($prompt->chunk_metadata['source_url'])) {
1490 + $source_url = $prompt->chunk_metadata['source_url'];
1491 + } elseif (!empty($prompt->source_url)) {
1492 + $source_url = $prompt->source_url;
1493 + }
1494 +
1495 + if (!empty($source_url)) {
1496 + if (!isset($grouped_prompts[$source_url])) {
1497 + $grouped_prompts[$source_url] = array();
1498 + }
1499 + $grouped_prompts[$source_url][] = $prompt;
1500 + } else {
1501 + $grouped_prompts['_ungrouped_' . $prompt->id] = array($prompt);
1502 + }
1503 + }
1504 +
1505 + // Sort each group by chunk_index
1506 + foreach ($grouped_prompts as $source_url => &$group) {
1507 + usort($group, function($a, $b) {
1508 + $index_a = isset($a->chunk_metadata['chunk_index']) ? intval($a->chunk_metadata['chunk_index']) : 0;
1509 + $index_b = isset($b->chunk_metadata['chunk_index']) ? intval($b->chunk_metadata['chunk_index']) : 0;
1510 + return $index_a - $index_b;
1511 + });
1512 + }
1513 + unset($group);
1514 +
1515 + // Build HTML for the table rows - must match admin-knowledge-page.php structure exactly
1516 + ob_start();
1517 + $display_index = 0;
1518 + $current_page = $page;
1519 + $data_source = 'pinecone';
1520 + $current_bot_id = $bot_id;
1521 + $preview_length = 150;
1522 +
1523 + if (empty($grouped_prompts)) {
1524 + echo '<tr><td colspan="4" style="padding: 40px; text-align: center; color: var(--mxch-text-muted);">';
1525 + esc_html_e('No knowledge entries found in Pinecone.', 'mxchat');
1526 + echo '</td></tr>';
1527 + } else {
1528 + foreach ($grouped_prompts as $source_url => $group) {
1529 + $chunk_count = count($group);
1530 + $first_prompt = $group[0];
1531 + $display_index++;
1532 +
1533 + if ($chunk_count > 1) {
1534 + // Multiple chunks - show grouped row with expand button
1535 + $group_id = 'group-' . md5($source_url);
1536 + ?>
1537 + <tr id="prompt-<?php echo esc_attr($first_prompt->id); ?>"
1538 + class="mxchat-chunk-group-header"
1539 + data-source="<?php echo esc_attr($data_source); ?>"
1540 + data-group-id="<?php echo esc_attr($group_id); ?>"
1541 + style="border-bottom: 1px solid var(--mxch-card-border); background: rgba(33, 150, 243, 0.02);">
1542 + <td style="padding: 12px 16px; font-size: 13px;">
1543 + <?php echo esc_html($display_index + (($current_page - 1) * $per_page)); ?>
1544 + </td>
1545 + <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
1546 + <div class="mxchat-chunk-group-info">
1547 + <button type="button" class="mxchat-chunk-toggle" data-group-id="<?php echo esc_attr($group_id); ?>">
1548 + <span class="dashicons dashicons-arrow-right-alt2"></span>
1549 + </button>
1550 + <span class="mxchat-chunk-badge"><?php echo esc_html($chunk_count); ?> <?php esc_html_e('chunks', 'mxchat'); ?></span>
1551 + <span class="mxchat-chunk-preview">
1552 + <?php
1553 + $parent_content = isset($first_prompt->display_content) ? $first_prompt->display_content : (isset($first_prompt->article_content) ? $first_prompt->article_content : '');
1554 + $content_preview = mb_substr($parent_content, 0, 100);
1555 + echo esc_html($content_preview . '...');
1556 + ?>
1557 + </span>
1558 + </div>
1559 + </td>
1560 + <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
1561 + <?php if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0 && strpos($source_url, '_ungrouped_') !== 0) : ?>
1562 + <a href="<?php echo esc_url($source_url); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
1563 + <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
1564 + <?php esc_html_e('View Source', 'mxchat'); ?>
1565 + </a>
1566 + <?php else : ?>
1567 + <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
1568 + <?php endif; ?>
1569 + </td>
1570 + <td class="mxchat-actions-cell" style="padding: 12px 16px;">
1571 + <button type="button"
1572 + class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
1573 + data-source-url="<?php echo esc_attr($source_url); ?>"
1574 + data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
1575 + data-data-source="<?php echo esc_attr($data_source); ?>"
1576 + data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
1577 + data-nonce="<?php echo wp_create_nonce('mxchat_delete_chunks_nonce'); ?>"
1578 + style="color: var(--mxch-error);"
1579 + title="<?php esc_attr_e('Delete all chunks', 'mxchat'); ?>">
1580 + <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
1581 + </button>
1582 + </td>
1583 + </tr>
1584 + <?php
1585 + // Render hidden chunk rows
1586 + foreach ($group as $chunk_index => $chunk) {
1587 + $meta_chunk_index = isset($chunk->chunk_metadata['chunk_index']) ? intval($chunk->chunk_metadata['chunk_index']) : $chunk_index;
1588 + $meta_total_chunks = isset($chunk->chunk_metadata['total_chunks']) ? intval($chunk->chunk_metadata['total_chunks']) : $chunk_count;
1589 + $content = isset($chunk->display_content) ? $chunk->display_content : (isset($chunk->article_content) ? $chunk->article_content : '');
1590 + $content_preview = mb_strlen($content) > $preview_length
1591 + ? mb_substr($content, 0, $preview_length) . '...'
1592 + : $content;
1593 + ?>
1594 + <tr id="prompt-<?php echo esc_attr($chunk->id); ?>"
1595 + class="mxchat-chunk-row <?php echo esc_attr($group_id); ?>"
1596 + data-source="<?php echo esc_attr($data_source); ?>"
1597 + style="display: none; background: #f8f9fa; border-bottom: 1px solid var(--mxch-card-border);">
1598 + <td style="padding: 12px 16px; text-align: center;">
1599 + <!-- Checkbox column placeholder for chunks (managed by group) -->
1600 + </td>
1601 + <td style="padding: 12px 16px 12px 30px; font-size: 13px;">
1602 + <!-- Hidden ID column for chunks -->
1603 + </td>
1604 + <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
1605 + <div class="mxchat-accordion-wrapper">
1606 + <div class="mxchat-content-preview">
1607 + <span class="mxchat-chunk-indicator" style="margin-right: 10px; color: var(--mxch-text-secondary); font-size: 12px;">
1608 + <?php printf(esc_html__('Chunk %d of %d', 'mxchat'), $meta_chunk_index + 1, $meta_total_chunks); ?>
1609 + </span>
1610 + <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
1611 + <?php if (mb_strlen($content) > $preview_length) : ?>
1612 + <button class="mxchat-expand-toggle" type="button">
1613 + <span class="dashicons dashicons-arrow-down-alt2"></span>
1614 + </button>
1615 + <?php endif; ?>
1616 + </div>
1617 + <div class="mxchat-content-full" style="display: none;">
1618 + <div class="content-view">
1619 + <?php
1620 + if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
1621 + echo '<div dir="rtl" lang="he" class="rtl-content">';
1622 + echo wp_kses_post(wpautop($content));
1623 + echo '</div>';
1624 + } else {
1625 + echo wp_kses_post(wpautop($content));
1626 + }
1627 + ?>
1628 + </div>
1629 + </div>
1630 + </div>
1631 + </td>
1632 + <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
1633 + <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Same as parent', 'mxchat'); ?></span>
1634 + </td>
1635 + <td class="mxchat-actions-cell" style="padding: 12px 16px;">
1636 + <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Managed by group', 'mxchat'); ?></span>
1637 + </td>
1638 + </tr>
1639 + <?php
1640 + }
1641 + } else {
1642 + // Single entry - display normally with accordion
1643 + $prompt = $first_prompt;
1644 + $content = isset($prompt->display_content) ? $prompt->display_content : (isset($prompt->article_content) ? $prompt->article_content : '');
1645 + $content_preview = mb_strlen($content) > $preview_length
1646 + ? mb_substr($content, 0, $preview_length) . '...'
1647 + : $content;
1648 + ?>
1649 + <tr id="prompt-<?php echo esc_attr($prompt->id); ?>"
1650 + data-source="<?php echo esc_attr($data_source); ?>"
1651 + style="border-bottom: 1px solid var(--mxch-card-border); background: rgba(33, 150, 243, 0.02);">
1652 + <td style="padding: 12px 16px; font-size: 13px;">
1653 + <?php echo esc_html($display_index + (($current_page - 1) * $per_page)); ?>
1654 + </td>
1655 + <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
1656 + <div class="mxchat-accordion-wrapper">
1657 + <div class="mxchat-content-preview">
1658 + <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
1659 + <?php if (mb_strlen($content) > $preview_length) : ?>
1660 + <button class="mxchat-expand-toggle" type="button">
1661 + <span class="dashicons dashicons-arrow-down-alt2"></span>
1662 + </button>
1663 + <?php endif; ?>
1664 + </div>
1665 + <div class="mxchat-content-full" style="display: none;">
1666 + <div class="content-view">
1667 + <?php
1668 + if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
1669 + echo '<div dir="rtl" lang="he" class="rtl-content">';
1670 + echo wp_kses_post(wpautop($content));
1671 + echo '</div>';
1672 + } else {
1673 + echo wp_kses_post(wpautop($content));
1674 + }
1675 + ?>
1676 + </div>
1677 + </div>
1678 + </div>
1679 + </td>
1680 + <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
1681 + <?php
1682 + $actual_source = $source_url;
1683 + if (strpos($source_url, '_ungrouped_') === 0) {
1684 + $actual_source = $prompt->source_url ?? '';
1685 + }
1686 + if (!empty($actual_source) && strpos($actual_source, 'mxchat://') !== 0) : ?>
1687 + <a href="<?php echo esc_url($actual_source); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
1688 + <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
1689 + <?php esc_html_e('View', 'mxchat'); ?>
1690 + </a>
1691 + <?php else : ?>
1692 + <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
1693 + <?php endif; ?>
1694 + </td>
1695 + <td style="padding: 12px 16px;">
1696 + <button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-ajax" data-vector-id="<?php echo esc_attr($prompt->id); ?>" data-bot-id="<?php echo esc_attr($current_bot_id); ?>" data-nonce="<?php echo wp_create_nonce('mxchat_delete_pinecone_prompt_nonce'); ?>" style="color: var(--mxch-error);">
1697 + <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
1698 + </button>
1699 + </td>
1700 + </tr>
1701 + <?php
1702 + }
1703 + }
1704 + }
1705 + $html = ob_get_clean();
1706 +
1707 + // Generate pagination HTML for Pinecone
1708 + $total_pages = ceil($total_records / $per_page);
1709 + $pagination_html = '';
1710 + if ($total_pages > 1) {
1711 + $pagination_html = '<div class="mxchat-ajax-pagination" data-current-page="' . esc_attr($page) . '" data-total-pages="' . esc_attr($total_pages) . '">';
1712 +
1713 + // Previous button
1714 + if ($page > 1) {
1715 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page - 1) . '">' . esc_html__('&laquo; Previous', 'mxchat') . '</a> ';
1716 + }
1717 +
1718 + // Page numbers
1719 + $start_page = max(1, $page - 2);
1720 + $end_page = min($total_pages, $page + 2);
1721 +
1722 + if ($start_page > 1) {
1723 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="1">1</a> ';
1724 + if ($start_page > 2) {
1725 + $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
1726 + }
1727 + }
1728 +
1729 + for ($i = $start_page; $i <= $end_page; $i++) {
1730 + if ($i == $page) {
1731 + $pagination_html .= '<span class="mxchat-page-current">' . $i . '</span> ';
1732 + } else {
1733 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $i . '">' . $i . '</a> ';
1734 + }
1735 + }
1736 +
1737 + if ($end_page < $total_pages) {
1738 + if ($end_page < $total_pages - 1) {
1739 + $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
1740 + }
1741 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $total_pages . '">' . $total_pages . '</a> ';
1742 + }
1743 +
1744 + // Next button
1745 + if ($page < $total_pages) {
1746 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page + 1) . '">' . esc_html__('Next &raquo;', 'mxchat') . '</a>';
1747 + }
1748 +
1749 + $pagination_html .= '</div>';
1750 + }
1751 +
1752 + wp_send_json_success(array(
1753 + 'html' => $html,
1754 + 'pagination_html' => $pagination_html,
1755 + 'total_count' => $total_records,
1756 + 'total_pages' => $total_pages,
1757 + 'page' => $page,
1758 + 'per_page' => $per_page,
1759 + 'data_source' => 'pinecone'
1760 + ));
1761 +}
1762 +
1763 +/**
1764 + * AJAX handler for pagination - handles both WordPress DB and Pinecone data sources
1765 + * Returns paginated entries without requiring a full page reload
1766 + */
1767 +public function ajax_mxchat_paginate_entries() {
1768 + check_ajax_referer('mxchat_entries_nonce', 'nonce');
1769 +
1770 + if (!current_user_can('manage_options')) {
1771 + wp_send_json_error(array('message' => 'Unauthorized'));
1772 + return;
1773 + }
1774 +
1775 + $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
1776 + $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
1777 + $search_query = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
1778 + $content_type_filter = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : '';
1779 + $per_page = 25;
1780 +
1781 + // Check if Pinecone is enabled for this bot
1782 + $pinecone_manager = $this->mxchat_get_pinecone_manager();
1783 + $pinecone_options = $pinecone_manager ? $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id) : array();
1784 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1785 + $has_pinecone_api = !empty($pinecone_options['mxchat_pinecone_api_key']);
1786 +
1787 + if ($use_pinecone && $has_pinecone_api) {
1788 + // Delegate to Pinecone pagination handler (pass search params)
1789 + $_POST['page'] = $page;
1790 + $_POST['search'] = $search_query;
1791 + $_POST['content_type'] = $content_type_filter;
1792 + $this->ajax_mxchat_refresh_pinecone_entries();
1793 + return;
1794 + }
1795 +
1796 + // WordPress DB pagination - MUST match initial page load logic exactly
1797 + global $wpdb;
1798 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1799 + $offset = ($page - 1) * $per_page;
1800 +
1801 + // Build WHERE clause for search and content type filtering
1802 + $where_clauses = array();
1803 + $where_values = array();
1804 +
1805 + if ($search_query) {
1806 + $where_clauses[] = "article_content LIKE %s";
1807 + $where_values[] = '%' . $wpdb->esc_like($search_query) . '%';
1808 + }
1809 +
1810 + if ($content_type_filter) {
1811 + switch ($content_type_filter) {
1812 + case 'manual':
1813 + $where_clauses[] = "(source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')";
1814 + break;
1815 + case 'pdf':
1816 + $where_clauses[] = "source_url LIKE '%.pdf'";
1817 + break;
1818 + case 'url':
1819 + $where_clauses[] = "source_url != '' AND source_url IS NOT NULL AND source_url NOT LIKE 'mxchat://%' AND source_url NOT LIKE '%.pdf'";
1820 + break;
1821 + }
1822 + }
1823 +
1824 + $where_sql = !empty($where_clauses) ? 'WHERE ' . implode(' AND ', $where_clauses) : '';
1825 +
1826 + // Count grouped entries with filters applied
1827 + if (!empty($where_values)) {
1828 + $count_args = array_merge($where_values, $where_values);
1829 + $count_query = $wpdb->prepare(
1830 + "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$where_sql} AND source_url != '' AND source_url NOT LIKE 'mxchat://%') +
1831 + (SELECT COUNT(*) FROM {$table_name} {$where_sql} AND (source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%'))",
1832 + ...$count_args
1833 + );
1834 + $total_records = $wpdb->get_var($count_query);
1835 + } else if (!empty($where_sql)) {
1836 + // Content type filter only (no search), no prepared values needed
1837 + $total_records = $wpdb->get_var(
1838 + "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$where_sql} AND source_url != '' AND source_url NOT LIKE 'mxchat://%') +
1839 + (SELECT COUNT(*) FROM {$table_name} {$where_sql} AND (source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%'))"
1840 + );
1841 + } else {
1842 + // No filters
1843 + $total_records = $wpdb->get_var(
1844 + "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '' AND source_url NOT LIKE 'mxchat://%') +
1845 + (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')"
1846 + );
1847 + }
1848 + $total_pages = ceil($total_records / $per_page);
1849 +
1850 + // Step 1: Get unique source_urls for this page (ordered by latest timestamp) with filters
1851 + if (!empty($where_values)) {
1852 + $query_args = array_merge($where_values, array($per_page, $offset));
1853 + $urls_query = $wpdb->prepare(
1854 + "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
1855 + {$where_sql}
1856 + GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
1857 + ...$query_args
1858 + );
1859 + } else if (!empty($where_sql)) {
1860 + $urls_query = $wpdb->prepare(
1861 + "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
1862 + {$where_sql}
1863 + GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
1864 + $per_page, $offset
1865 + );
1866 + } else {
1867 + $urls_query = $wpdb->prepare(
1868 + "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
1869 + GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
1870 + $per_page, $offset
1871 + );
1872 + }
1873 + $page_urls = $wpdb->get_results($urls_query);
1874 +
1875 + // Step 2: Build list of source_urls to fetch
1876 + $url_list = array();
1877 + $url_order_map = array();
1878 + $order_index = 0;
1879 + foreach ($page_urls as $url_row) {
1880 + $url = $url_row->source_url;
1881 + $url_list[] = $url;
1882 + $url_order_map[$url] = $order_index++;
1883 + }
1884 +
1885 + // Step 3: Fetch all rows for these source_urls (with search filter if applicable)
1886 + $prompts = array();
1887 + if (!empty($url_list)) {
1888 + $placeholders = implode(',', array_fill(0, count($url_list), '%s'));
1889 + if ($search_query) {
1890 + // Include search filter in the final fetch
1891 + $prompts_query = $wpdb->prepare(
1892 + "SELECT id, article_content, source_url, timestamp, role_restriction
1893 + FROM {$table_name}
1894 + WHERE source_url IN ($placeholders) AND article_content LIKE %s
1895 + ORDER BY timestamp DESC",
1896 + ...array_merge($url_list, ['%' . $wpdb->esc_like($search_query) . '%'])
1897 + );
1898 + } else {
1899 + $prompts_query = $wpdb->prepare(
1900 + "SELECT id, article_content, source_url, timestamp, role_restriction
1901 + FROM {$table_name}
1902 + WHERE source_url IN ($placeholders)
1903 + ORDER BY timestamp DESC",
1904 + $url_list
1905 + );
1906 + }
1907 + $prompts = $wpdb->get_results($prompts_query);
1908 + }
1909 +
1910 + // Group prompts by source_url for chunk display
1911 + $grouped_prompts = array();
1912 + foreach ($prompts as $prompt) {
1913 + $source_url = $prompt->source_url ?? '';
1914 +
1915 + // Parse chunk metadata using the proper chunker method (same as initial page load)
1916 + if (class_exists('MxChat_Chunker')) {
1917 + $chunk_meta = MxChat_Chunker::parse_stored_chunk($prompt->article_content);
1918 + $prompt->chunk_metadata = $chunk_meta['metadata'];
1919 + $prompt->display_content = $chunk_meta['text'];
1920 + } else {
1921 + $prompt->chunk_metadata = array();
1922 + $prompt->display_content = $prompt->article_content;
1923 + }
1924 +
1925 + if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0) {
1926 + if (!isset($grouped_prompts[$source_url])) {
1927 + $grouped_prompts[$source_url] = array();
1928 + }
1929 + $grouped_prompts[$source_url][] = $prompt;
1930 + } else {
1931 + // Ungrouped entries
1932 + $grouped_prompts['_ungrouped_' . $prompt->id] = array($prompt);
1933 + }
1934 + }
1935 +
1936 + // Sort groups by the original URL order (newest first)
1937 + uksort($grouped_prompts, function($a, $b) use ($url_order_map) {
1938 + $order_a = isset($url_order_map[$a]) ? $url_order_map[$a] : PHP_INT_MAX;
1939 + $order_b = isset($url_order_map[$b]) ? $url_order_map[$b] : PHP_INT_MAX;
1940 + return $order_a - $order_b;
1941 + });
1942 +
1943 + // Sort each group internally by chunk_index
1944 + foreach ($grouped_prompts as $source_url => &$group) {
1945 + usort($group, function($a, $b) {
1946 + $index_a = isset($a->chunk_metadata['chunk_index']) ? intval($a->chunk_metadata['chunk_index']) : 0;
1947 + $index_b = isset($b->chunk_metadata['chunk_index']) ? intval($b->chunk_metadata['chunk_index']) : 0;
1948 + return $index_a - $index_b;
1949 + });
1950 + }
1951 + unset($group);
1952 +
1953 + // Build HTML for the table rows
1954 + ob_start();
1955 + $display_index = 0;
1956 + $current_page = $page;
1957 + $data_source = 'wordpress';
1958 + $current_bot_id = $bot_id;
1959 + $preview_length = 150;
1960 +
1961 + if (empty($grouped_prompts)) {
1962 + echo '<tr><td colspan="5" style="padding: 40px; text-align: center; color: var(--mxch-text-muted);">';
1963 + esc_html_e('No knowledge entries found. Use the Import Options to add content.', 'mxchat');
1964 + echo '</td></tr>';
1965 + } else {
1966 + foreach ($grouped_prompts as $source_url => $group) {
1967 + $chunk_count = count($group);
1968 + $first_prompt = $group[0];
1969 + $display_index++;
1970 +
1971 + if ($chunk_count > 1) {
1972 + // Multiple chunks - show grouped row with expand button
1973 + $group_id = 'group-' . md5($source_url);
1974 + ?>
1975 + <tr id="prompt-<?php echo esc_attr($first_prompt->id); ?>"
1976 + class="mxchat-chunk-group-header"
1977 + data-source="<?php echo esc_attr($data_source); ?>"
1978 + data-group-id="<?php echo esc_attr($group_id); ?>"
1979 + style="border-bottom: 1px solid var(--mxch-card-border);">
1980 + <td style="padding: 12px 16px; text-align: center;">
1981 + <input type="checkbox"
1982 + class="mxchat-entry-checkbox"
1983 + data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
1984 + data-source="<?php echo esc_attr($data_source); ?>"
1985 + data-source-url="<?php echo esc_attr($source_url); ?>"
1986 + data-is-group="true"
1987 + data-chunk-count="<?php echo esc_attr($chunk_count); ?>">
1988 + </td>
1989 + <td style="padding: 12px 16px; font-size: 13px;">
1990 + <?php echo esc_html($first_prompt->id); ?>
1991 + </td>
1992 + <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
1993 + <div class="mxchat-chunk-group-info">
1994 + <button type="button" class="mxchat-chunk-toggle" data-group-id="<?php echo esc_attr($group_id); ?>">
1995 + <span class="dashicons dashicons-arrow-right-alt2"></span>
1996 + </button>
1997 + <span class="mxchat-chunk-badge"><?php echo esc_html($chunk_count); ?> <?php esc_html_e('chunks', 'mxchat'); ?></span>
1998 + <span class="mxchat-chunk-preview">
1999 + <?php
2000 + $parent_content = isset($first_prompt->display_content) ? $first_prompt->display_content : $first_prompt->article_content;
2001 + $content_preview = mb_substr($parent_content, 0, 100);
2002 + echo esc_html($content_preview . '...');
2003 + ?>
2004 + </span>
2005 + </div>
2006 + </td>
2007 + <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2008 + <?php if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0 && strpos($source_url, '_ungrouped_') !== 0) : ?>
2009 + <a href="<?php echo esc_url($source_url); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2010 + <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2011 + <?php esc_html_e('View Source', 'mxchat'); ?>
2012 + </a>
2013 + <?php else : ?>
2014 + <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
2015 + <?php endif; ?>
2016 + </td>
2017 + <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2018 + <button type="button"
2019 + class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
2020 + data-source-url="<?php echo esc_attr($source_url); ?>"
2021 + data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
2022 + data-data-source="<?php echo esc_attr($data_source); ?>"
2023 + data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2024 + data-nonce="<?php echo wp_create_nonce('mxchat_delete_chunks_nonce'); ?>"
2025 + style="color: var(--mxch-error);"
2026 + title="<?php esc_attr_e('Delete all chunks', 'mxchat'); ?>">
2027 + <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2028 + </button>
2029 + </td>
2030 + </tr>
2031 + <?php
2032 + // Render hidden chunk rows
2033 + foreach ($group as $chunk_index => $chunk) {
2034 + $meta_chunk_index = isset($chunk->chunk_metadata['chunk_index']) ? intval($chunk->chunk_metadata['chunk_index']) : $chunk_index;
2035 + $meta_total_chunks = isset($chunk->chunk_metadata['total_chunks']) ? intval($chunk->chunk_metadata['total_chunks']) : $chunk_count;
2036 + $content = isset($chunk->display_content) ? $chunk->display_content : $chunk->article_content;
2037 + $content_preview = mb_strlen($content) > $preview_length
2038 + ? mb_substr($content, 0, $preview_length) . '...'
2039 + : $content;
2040 + ?>
2041 + <tr id="prompt-<?php echo esc_attr($chunk->id); ?>"
2042 + class="mxchat-chunk-row <?php echo esc_attr($group_id); ?>"
2043 + data-source="<?php echo esc_attr($data_source); ?>"
2044 + style="display: none; background: #f8f9fa; border-bottom: 1px solid var(--mxch-card-border);">
2045 + <td style="padding: 12px 16px; text-align: center;">
2046 + <!-- Checkbox column placeholder for chunks (managed by group) -->
2047 + </td>
2048 + <td style="padding: 12px 16px 12px 30px; font-size: 13px;">
2049 + <!-- Hidden ID column for chunks -->
2050 + </td>
2051 + <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2052 + <div class="mxchat-accordion-wrapper">
2053 + <div class="mxchat-content-preview">
2054 + <span class="mxchat-chunk-indicator" style="margin-right: 10px; color: var(--mxch-text-secondary); font-size: 12px;">
2055 + <?php printf(esc_html__('Chunk %d of %d', 'mxchat'), $meta_chunk_index + 1, $meta_total_chunks); ?>
2056 + </span>
2057 + <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2058 + <?php if (mb_strlen($content) > $preview_length) : ?>
2059 + <button class="mxchat-expand-toggle" type="button">
2060 + <span class="dashicons dashicons-arrow-down-alt2"></span>
2061 + </button>
2062 + <?php endif; ?>
2063 + </div>
2064 + <div class="mxchat-content-full" style="display: none;">
2065 + <div class="content-view">
2066 + <?php
2067 + if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2068 + echo '<div dir="rtl" lang="he" class="rtl-content">';
2069 + echo wp_kses_post(wpautop($content));
2070 + echo '</div>';
2071 + } else {
2072 + echo wp_kses_post(wpautop($content));
2073 + }
2074 + ?>
2075 + </div>
2076 + </div>
2077 + </div>
2078 + </td>
2079 + <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2080 + <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Same as parent', 'mxchat'); ?></span>
2081 + </td>
2082 + <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2083 + <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Managed by group', 'mxchat'); ?></span>
2084 + </td>
2085 + </tr>
2086 + <?php
2087 + }
2088 + } else {
2089 + // Single entry - display normally with accordion
2090 + $prompt = $first_prompt;
2091 + $content = isset($prompt->display_content) ? $prompt->display_content : $prompt->article_content;
2092 + $content_preview = mb_strlen($content) > $preview_length
2093 + ? mb_substr($content, 0, $preview_length) . '...'
2094 + : $content;
2095 + ?>
2096 + <tr id="prompt-<?php echo esc_attr($prompt->id); ?>"
2097 + data-source="<?php echo esc_attr($data_source); ?>"
2098 + style="border-bottom: 1px solid var(--mxch-card-border);">
2099 + <td style="padding: 12px 16px; text-align: center;">
2100 + <input type="checkbox"
2101 + class="mxchat-entry-checkbox"
2102 + data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2103 + data-source="<?php echo esc_attr($data_source); ?>"
2104 + data-source-url="<?php echo esc_attr($source_url); ?>"
2105 + data-is-group="false">
2106 + </td>
2107 + <td style="padding: 12px 16px; font-size: 13px;">
2108 + <?php echo esc_html($prompt->id); ?>
2109 + </td>
2110 + <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2111 + <div class="mxchat-accordion-wrapper">
2112 + <div class="mxchat-content-preview">
2113 + <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2114 + <?php if (mb_strlen($content) > $preview_length) : ?>
2115 + <button class="mxchat-expand-toggle" type="button">
2116 + <span class="dashicons dashicons-arrow-down-alt2"></span>
2117 + </button>
2118 + <?php endif; ?>
2119 + </div>
2120 + <div class="mxchat-content-full" style="display: none;">
2121 + <div class="content-view">
2122 + <?php
2123 + if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2124 + echo '<div dir="rtl" lang="he" class="rtl-content">';
2125 + echo wp_kses_post(wpautop($content));
2126 + echo '</div>';
2127 + } else {
2128 + echo wp_kses_post(wpautop($content));
2129 + }
2130 + ?>
2131 + </div>
2132 + </div>
2133 + </div>
2134 + </td>
2135 + <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2136 + <?php
2137 + $actual_source = $source_url;
2138 + if (strpos($source_url, '_ungrouped_') === 0) {
2139 + $actual_source = $prompt->source_url ?? '';
2140 + }
2141 + if (!empty($actual_source) && strpos($actual_source, 'mxchat://') !== 0) : ?>
2142 + <a href="<?php echo esc_url($actual_source); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2143 + <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2144 + <?php esc_html_e('View', 'mxchat'); ?>
2145 + </a>
2146 + <?php else : ?>
2147 + <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
2148 + <?php endif; ?>
2149 + </td>
2150 + <td style="padding: 12px 16px;">
2151 + <button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-wordpress" data-entry-id="<?php echo esc_attr($prompt->id); ?>" data-bot-id="<?php echo esc_attr($current_bot_id); ?>" data-nonce="<?php echo wp_create_nonce('mxchat_delete_wordpress_prompt_nonce'); ?>" style="color: var(--mxch-error);">
2152 + <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2153 + </button>
2154 + </td>
2155 + </tr>
2156 + <?php
2157 + }
2158 + }
2159 + }
2160 + $html = ob_get_clean();
2161 +
2162 + // Generate pagination HTML (include search/filter data for subsequent pages)
2163 + $pagination_html = '';
2164 + if ($total_pages > 1) {
2165 + $pagination_html = '<div class="mxchat-ajax-pagination" data-current-page="' . esc_attr($page) . '" data-total-pages="' . esc_attr($total_pages) . '" data-search="' . esc_attr($search_query) . '" data-content-type="' . esc_attr($content_type_filter) . '">';
2166 +
2167 + // Previous button
2168 + if ($page > 1) {
2169 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page - 1) . '">' . esc_html__('&laquo; Previous', 'mxchat') . '</a> ';
2170 + }
2171 +
2172 + // Page numbers
2173 + $start_page = max(1, $page - 2);
2174 + $end_page = min($total_pages, $page + 2);
2175 +
2176 + if ($start_page > 1) {
2177 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="1">1</a> ';
2178 + if ($start_page > 2) {
2179 + $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2180 + }
2181 + }
2182 +
2183 + for ($i = $start_page; $i <= $end_page; $i++) {
2184 + if ($i == $page) {
2185 + $pagination_html .= '<span class="mxchat-page-current">' . $i . '</span> ';
2186 + } else {
2187 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $i . '">' . $i . '</a> ';
2188 + }
2189 + }
2190 +
2191 + if ($end_page < $total_pages) {
2192 + if ($end_page < $total_pages - 1) {
2193 + $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2194 + }
2195 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $total_pages . '">' . $total_pages . '</a> ';
2196 + }
2197 +
2198 + // Next button
2199 + if ($page < $total_pages) {
2200 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page + 1) . '">' . esc_html__('Next &raquo;', 'mxchat') . '</a>';
2201 + }
2202 +
2203 + $pagination_html .= '</div>';
2204 + }
2205 +
2206 + wp_send_json_success(array(
2207 + 'html' => $html,
2208 + 'pagination_html' => $pagination_html,
2209 + 'total_count' => $total_records,
2210 + 'total_pages' => $total_pages,
2211 + 'page' => $page,
2212 + 'per_page' => $per_page,
2213 + 'data_source' => 'wordpress'
2214 + ));
2215 +}
2216 +
2217 +/**
2218 + * AJAX handler to detect available sitemaps on the site
2219 + * Optimized for speed - only checks primary sitemap indexes first
2220 + */
2221 +public function ajax_mxchat_detect_sitemaps() {
2222 + check_ajax_referer('mxchat_detect_sitemaps_nonce', 'nonce');
2223 +
2224 + if (!current_user_can('manage_options')) {
2225 + wp_send_json_error(array('message' => 'Unauthorized'));
2226 + return;
2227 + }
2228 +
2229 + $site_url = get_site_url();
2230 + $sitemaps = array();
2231 + $found_index = false;
2232 +
2233 + // Only check the main sitemap index files first (much faster)
2234 + // These are the primary entry points that contain sub-sitemaps
2235 + $primary_indexes = array(
2236 + 'sitemap_index.xml' => 'Yoast SEO / Rank Math', // Yoast & Rank Math
2237 + 'wp-sitemap.xml' => 'WordPress Core', // WordPress Core
2238 + 'sitemap.xml' => 'Standard', // Generic/AIOSEO
2239 + );
2240 +
2241 + foreach ($primary_indexes as $path => $source) {
2242 + $url = trailingslashit($site_url) . $path;
2243 +
2244 + $response = wp_remote_head($url, array(
2245 + 'timeout' => 3, // Short timeout
2246 + 'sslverify' => false,
2247 + 'redirection' => 1
2248 + ));
2249 +
2250 + if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2251 + // Found a sitemap index - parse it to get sub-sitemaps
2252 + $sub_sitemaps = $this->parse_sitemap_index($url);
2253 + if (!empty($sub_sitemaps)) {
2254 + $sitemaps[] = array(
2255 + 'url' => $url,
2256 + 'type' => 'index',
2257 + 'source' => $source,
2258 + 'sub_sitemaps' => $sub_sitemaps
2259 + );
2260 + $found_index = true;
2261 + // Found a valid index, no need to check others
2262 + break;
2263 + }
2264 + }
2265 + }
2266 +
2267 + // If no sitemap index found, check for standalone sitemaps
2268 + if (!$found_index) {
2269 + $standalone_sitemaps = array(
2270 + 'post-sitemap.xml' => array('type' => 'content', 'source' => 'Yoast SEO'),
2271 + 'page-sitemap.xml' => array('type' => 'content', 'source' => 'Yoast SEO'),
2272 + );
2273 +
2274 + foreach ($standalone_sitemaps as $path => $info) {
2275 + $url = trailingslashit($site_url) . $path;
2276 +
2277 + $response = wp_remote_head($url, array(
2278 + 'timeout' => 2,
2279 + 'sslverify' => false
2280 + ));
2281 +
2282 + if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2283 + $sitemaps[] = array(
2284 + 'url' => $url,
2285 + 'type' => $info['type'],
2286 + 'source' => $info['source'],
2287 + 'url_count' => 0 // Skip URL count for speed
2288 + );
2289 + }
2290 + }
2291 + }
2292 +
2293 + wp_send_json_success(array(
2294 + 'sitemaps' => $sitemaps,
2295 + 'site_url' => $site_url
2296 + ));
2297 +}
2298 +
2299 +/**
2300 + * Parse a sitemap index to get sub-sitemaps
2301 + * Optimized: doesn't fetch URL count for each sub-sitemap (too slow)
2302 + */
2303 +private function parse_sitemap_index($url) {
2304 + $sub_sitemaps = array();
2305 +
2306 + $response = wp_remote_get($url, array(
2307 + 'timeout' => 5,
2308 + 'sslverify' => false
2309 + ));
2310 +
2311 + if (is_wp_error($response)) {
2312 + return $sub_sitemaps;
2313 + }
2314 +
2315 + $body = wp_remote_retrieve_body($response);
2316 + if (empty($body)) {
2317 + return $sub_sitemaps;
2318 + }
2319 +
2320 + // Suppress XML errors
2321 + libxml_use_internal_errors(true);
2322 + $xml = simplexml_load_string($body);
2323 + libxml_clear_errors();
2324 +
2325 + if ($xml === false) {
2326 + return $sub_sitemaps;
2327 + }
2328 +
2329 + // Check if it's a sitemap index (contains <sitemap> elements)
2330 + if (isset($xml->sitemap)) {
2331 + foreach ($xml->sitemap as $sitemap) {
2332 + $loc = (string) $sitemap->loc;
2333 + if (!empty($loc)) {
2334 + // Try to determine the type from the URL
2335 + $type = 'content';
2336 + if (strpos($loc, 'category') !== false || strpos($loc, 'tag') !== false || strpos($loc, 'taxonomy') !== false) {
2337 + $type = 'taxonomy';
2338 + } elseif (strpos($loc, 'author') !== false || strpos($loc, 'user') !== false) {
2339 + $type = 'author';
2340 + }
2341 +
2342 + // Skip URL count - too slow to fetch for each sitemap
2343 + $sub_sitemaps[] = array(
2344 + 'url' => $loc,
2345 + 'type' => $type,
2346 + 'url_count' => 0, // Don't fetch - takes too long
2347 + 'name' => basename(parse_url($loc, PHP_URL_PATH))
2348 + );
2349 + }
2350 + }
2351 + }
2352 +
2353 + return $sub_sitemaps;
2354 +}
2355 +
2356 +/**
2357 + * Get URL count from a sitemap
2358 + */
2359 +private function get_sitemap_url_count($url) {
2360 + $response = wp_remote_get($url, array(
2361 + 'timeout' => 10,
2362 + 'sslverify' => false
2363 + ));
2364 +
2365 + if (is_wp_error($response)) {
2366 + return 0;
2367 + }
2368 +
2369 + $body = wp_remote_retrieve_body($response);
2370 + if (empty($body)) {
2371 + return 0;
2372 + }
2373 +
2374 + // Count <url> or <loc> elements
2375 + $count = preg_match_all('/<url>/i', $body, $matches);
2376 + return $count ?: 0;
2377 +}
2378 +
2379 +/**
2380 + * Get sitemaps declared in robots.txt
2381 + */
2382 +private function get_sitemaps_from_robots($site_url) {
2383 + $sitemaps = array();
2384 + $robots_url = trailingslashit($site_url) . 'robots.txt';
2385 +
2386 + $response = wp_remote_get($robots_url, array(
2387 + 'timeout' => 5,
2388 + 'sslverify' => false
2389 + ));
2390 +
2391 + if (is_wp_error($response)) {
2392 + return $sitemaps;
2393 + }
2394 +
2395 + $body = wp_remote_retrieve_body($response);
2396 + if (empty($body)) {
2397 + return $sitemaps;
2398 + }
2399 +
2400 + // Find Sitemap: declarations
2401 + if (preg_match_all('/^Sitemap:\s*(.+)$/mi', $body, $matches)) {
2402 + foreach ($matches[1] as $sitemap_url) {
2403 + $sitemap_url = trim($sitemap_url);
2404 + if (filter_var($sitemap_url, FILTER_VALIDATE_URL)) {
2405 + $sitemaps[] = $sitemap_url;
2406 + }
2407 + }
2408 + }
2409 +
2410 + return $sitemaps;
2411 +}
2412 +
2413 +public function mxchat_stop_processing() {
2414 + // Verify permissions
2415 + if (!current_user_can('manage_options')) {
2416 + wp_die(esc_html__('Unauthorized access', 'mxchat'));
2417 + }
2418 +
2419 + // Verify nonce
2420 + check_admin_referer('mxchat_stop_processing_action', 'mxchat_stop_processing_nonce');
2421 +
2422 + global $wpdb;
2423 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
2424 +
2425 + // Get active queue IDs
2426 + $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
2427 + $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
2428 +
2429 + // Delete all pending items from active queues
2430 + if ($sitemap_queue_id) {
2431 + $wpdb->delete(
2432 + $table_name,
2433 + array(
2434 + 'queue_id' => $sitemap_queue_id,
2435 + 'status' => 'pending'
2436 + ),
2437 + array('%s', '%s')
2438 + );
2439 +
2440 + delete_transient('mxchat_active_queue_sitemap');
2441 + delete_transient('mxchat_last_sitemap_url');
2442 + }
2443 +
2444 + if ($pdf_queue_id) {
2445 + // Get PDF path before deleting
2446 + $pdf_path = $this->mxchat_get_queue_meta($pdf_queue_id, 'pdf_path');
2447 +
2448 + $wpdb->delete(
2449 + $table_name,
2450 + array(
2451 + 'queue_id' => $pdf_queue_id,
2452 + 'status' => 'pending'
2453 + ),
2454 + array('%s', '%s')
2455 + );
2456 +
2457 + // Delete PDF file
2458 + if ($pdf_path && file_exists($pdf_path)) {
2459 + wp_delete_file($pdf_path);
2460 + }
2461 +
2462 + delete_transient('mxchat_active_queue_pdf');
2463 + delete_transient('mxchat_last_pdf_url');
2464 + }
2465 +
2466 + // Redirect back with a success message
2467 + set_transient('mxchat_admin_notice_success',
2468 + esc_html__('Processing has been stopped successfully.', 'mxchat'),
2469 + 30
2470 + );
2471 + wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
2472 + exit;
2473 +}
2474 +
2475 +/**
2476 + * Get content list for processing
2477 + */
2478 +public function ajax_mxchat_get_content_list() {
2479 + // Verify the nonce
2480 + check_ajax_referer('mxchat_content_selector_nonce', 'nonce');
2481 +
2482 + if (!current_user_can('manage_options')) {
2483 + wp_send_json_error(__('Unauthorized access', 'mxchat'));
2484 + }
2485 +
2486 + $page = isset($_GET['page']) ? absint($_GET['page']) : 1;
2487 + $per_page = isset($_GET['per_page']) ? absint($_GET['per_page']) : 100;
2488 + $search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
2489 + $post_type = isset($_GET['post_type']) ? sanitize_text_field($_GET['post_type']) : 'all';
2490 + $post_status = isset($_GET['post_status']) ? sanitize_text_field($_GET['post_status']) : 'publish';
2491 + $processed_filter = isset($_GET['processed_filter']) ? sanitize_text_field($_GET['processed_filter']) : 'all';
2492 +
2493 + // Build query args
2494 + $args = array(
2495 + 'posts_per_page' => $per_page,
2496 + 'paged' => $page,
2497 + 'post_status' => $post_status !== 'all' ? $post_status : array('publish', 'draft', 'pending'),
2498 + 'orderby' => 'date',
2499 + 'order' => 'DESC',
2500 + );
2501 +
2502 + // Handle post types - IMPROVED VERSION
2503 + if ($post_type !== 'all') {
2504 + $args['post_type'] = $post_type;
2505 + } else {
2506 + // Get all available post types that might contain content
2507 + $all_post_types = array();
2508 +
2509 + // First get all public post types
2510 + $public_types = get_post_types(array('public' => true), 'names');
2511 + $all_post_types = array_merge($all_post_types, $public_types);
2512 +
2513 + // Add common forum/community post types
2514 + $forum_types = array('topic', 'reply', 'forum', 'wpforo_topic', 'wpforo_post');
2515 + foreach ($forum_types as $forum_type) {
2516 + if (post_type_exists($forum_type)) {
2517 + $all_post_types[] = $forum_type;
2518 + }
2519 + }
2520 +
2521 + // Add other commonly used post types
2522 + $common_types = array('product', 'job_listing', 'event', 'portfolio');
2523 + foreach ($common_types as $common_type) {
2524 + if (post_type_exists($common_type)) {
2525 + $all_post_types[] = $common_type;
2526 + }
2527 + }
2528 +
2529 + // Remove duplicates and ensure we have at least some post types
2530 + $all_post_types = array_unique($all_post_types);
2531 +
2532 + if (empty($all_post_types)) {
2533 + // Fallback to basic post types
2534 + $all_post_types = array('post', 'page');
2535 + }
2536 +
2537 + $args['post_type'] = $all_post_types;
2538 +
2539 + // Debug logging to see what post types are being queried
2540 + //error_log('MxChat Debug: Querying post types: ' . implode(', ', $all_post_types));
2541 + }
2542 +
2543 + if (!empty($search)) {
2544 + $args['s'] = $search;
2545 + }
2546 +
2547 + // Get processed data from storage
2548 + $processed_data = array();
2549 +
2550 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
2551 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2552 +
2553 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
2554 + // Get fresh data from Pinecone - no caching
2555 + $processed_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
2556 + } else {
2557 + // WordPress DB checking with better URL matching for all post types
2558 + global $wpdb;
2559 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2560 + $processed_items = $wpdb->get_results("SELECT id, source_url, article_content, timestamp FROM {$table_name}");
2561 +
2562 + // Group items by source_url to count chunks
2563 + $url_chunk_counts = array();
2564 + $url_latest_timestamp = array();
2565 + $url_first_id = array();
2566 +
2567 + if (!empty($processed_items)) {
2568 + foreach ($processed_items as $item) {
2569 + $url = $item->source_url;
2570 + if (empty($url)) continue;
2571 +
2572 + // Count chunks per URL
2573 + if (!isset($url_chunk_counts[$url])) {
2574 + $url_chunk_counts[$url] = 0;
2575 + $url_latest_timestamp[$url] = $item->timestamp;
2576 + $url_first_id[$url] = $item->id;
2577 + }
2578 + $url_chunk_counts[$url]++;
2579 +
2580 + // Track latest timestamp
2581 + if (strtotime($item->timestamp) > strtotime($url_latest_timestamp[$url])) {
2582 + $url_latest_timestamp[$url] = $item->timestamp;
2583 + }
2584 + }
2585 +
2586 + // Now build processed_data with chunk counts
2587 + foreach ($url_chunk_counts as $url => $chunk_count) {
2588 + $post_id = $this->mxchat_url_to_post_id_improved($url);
2589 +
2590 + if ($post_id) {
2591 + $processed_data[$post_id] = array(
2592 + 'db_id' => $url_first_id[$url],
2593 + 'timestamp' => $url_latest_timestamp[$url],
2594 + 'url' => $url,
2595 + 'source' => 'wordpress',
2596 + 'chunk_count' => $chunk_count
2597 + );
2598 + }
2599 + }
2600 + }
2601 + }
2602 +
2603 + // Get processed IDs as a simple array for in_array checks
2604 + $processed_ids = array_keys($processed_data);
2605 +
2606 + // Handle processed/unprocessed filter
2607 + if ($processed_filter === 'processed' && !empty($processed_ids)) {
2608 + $args['post__in'] = $processed_ids;
2609 + } elseif ($processed_filter === 'unprocessed' && !empty($processed_ids)) {
2610 + $args['post__not_in'] = $processed_ids;
2611 + }
2612 +
2613 + // Run the query
2614 + $query = new WP_Query($args);
2615 + $content_items = array();
2616 +
2617 + if ($query->have_posts()) {
2618 + while ($query->have_posts()) {
2619 + $query->the_post();
2620 + $id = get_the_ID();
2621 + $post_date = get_the_date();
2622 + $excerpt = wp_trim_words(get_the_excerpt(), 20, '...');
2623 + $word_count = str_word_count(strip_tags(get_the_content()));
2624 +
2625 + $is_processed = in_array($id, $processed_ids);
2626 + $processed_date = '';
2627 + $db_record_id = 0;
2628 + $data_source = 'none';
2629 +
2630 + if ($is_processed && isset($processed_data[$id])) {
2631 + $item_data = $processed_data[$id];
2632 + $data_source = $item_data['source'];
2633 +
2634 + if ($data_source === 'wordpress' && isset($item_data['timestamp'])) {
2635 + // WordPress DB format
2636 + $timestamp = strtotime($item_data['timestamp']);
2637 + $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2638 + $db_record_id = $item_data['db_id'];
2639 + } elseif ($data_source === 'pinecone') {
2640 + // Pinecone format
2641 + $processed_date = $item_data['processed_date'];
2642 + $db_record_id = $item_data['db_id'];
2643 + }
2644 + }
2645 +
2646 + // Get chunk count for this item
2647 + $chunk_count = 0;
2648 + if ($is_processed && isset($processed_data[$id]['chunk_count'])) {
2649 + $chunk_count = intval($processed_data[$id]['chunk_count']);
2650 + }
2651 +
2652 + $content_items[] = array(
2653 + 'id' => $id,
2654 + 'title' => get_the_title(),
2655 + 'permalink' => get_permalink(),
2656 + 'date' => $post_date,
2657 + 'type' => get_post_type(),
2658 + 'status' => get_post_status(),
2659 + 'excerpt' => $excerpt,
2660 + 'word_count' => $word_count,
2661 + 'already_processed' => $is_processed,
2662 + 'processed_date' => $processed_date,
2663 + 'db_record_id' => $db_record_id,
2664 + 'data_source' => $data_source,
2665 + 'chunk_count' => $chunk_count
2666 + );
2667 + }
2668 + wp_reset_postdata();
2669 + }
2670 +
2671 + $response = array(
2672 + 'items' => $content_items,
2673 + 'total' => $query->found_posts,
2674 + 'total_pages' => $query->max_num_pages,
2675 + 'current_page' => $page,
2676 + 'processed_count' => count($processed_ids)
2677 + );
2678 +
2679 + wp_send_json_success($response);
2680 + exit;
2681 +}
2682 +
2683 +
2684 +/**
2685 + * This function handles various WooCommerce URL formats and permalink structures
2686 + */
2687 +private function mxchat_url_to_post_id_improved($url) {
2688 + // First try the standard WordPress function
2689 + $post_id = url_to_postid($url);
2690 +
2691 + if ($post_id > 0) {
2692 + return $post_id;
2693 + }
2694 +
2695 + // If that fails, try more aggressive URL matching
2696 + // Remove trailing slashes and query parameters for better matching
2697 + $clean_url = rtrim($url, '/');
2698 + $clean_url = strtok($clean_url, '?'); // Remove query parameters
2699 +
2700 + // Try again with cleaned URL
2701 + $post_id = url_to_postid($clean_url);
2702 + if ($post_id > 0) {
2703 + return $post_id;
2704 + }
2705 +
2706 + // For bbPress forum topics, try extracting slug from URL
2707 + if (strpos($url, '/topic/') !== false || strpos($url, '/forums/') !== false) {
2708 + // Handle bbPress URLs: /forums/topic/topic-name/
2709 + if (preg_match('/\/forums\/topic\/([^\/\?]+)/', $url, $matches)) {
2710 + $topic_slug = $matches[1];
2711 +
2712 + // Look up topic by slug
2713 + $topic = get_page_by_path($topic_slug, OBJECT, 'topic');
2714 + if ($topic) {
2715 + return $topic->ID;
2716 + }
2717 +
2718 + // Alternative method: query by post_name
2719 + global $wpdb;
2720 + $post_id = $wpdb->get_var($wpdb->prepare(
2721 + "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
2722 + $topic_slug
2723 + ));
2724 +
2725 + if ($post_id) {
2726 + return intval($post_id);
2727 + }
2728 + }
2729 +
2730 + // Handle simpler topic URLs: /topic/topic-name/
2731 + if (preg_match('/\/topic\/([^\/\?]+)/', $url, $matches)) {
2732 + $topic_slug = $matches[1];
2733 +
2734 + global $wpdb;
2735 + $post_id = $wpdb->get_var($wpdb->prepare(
2736 + "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
2737 + $topic_slug
2738 + ));
2739 +
2740 + if ($post_id) {
2741 + return intval($post_id);
2742 + }
2743 + }
2744 + }
2745 +
2746 + // For WooCommerce products
2747 + if (strpos($url, '/product/') !== false || strpos($url, 'product=') !== false) {
2748 + // Extract product slug from various URL formats
2749 + $product_slug = '';
2750 +
2751 + // Handle pretty permalinks: /product/product-name/
2752 + if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
2753 + $product_slug = $matches[1];
2754 + }
2755 + // Handle query parameters: ?product=product-name
2756 + elseif (preg_match('/[\?&]product=([^&]+)/', $url, $matches)) {
2757 + $product_slug = $matches[1];
2758 + }
2759 +
2760 + if (!empty($product_slug)) {
2761 + // Look up product by slug
2762 + $product = get_page_by_path($product_slug, OBJECT, 'product');
2763 + if ($product) {
2764 + return $product->ID;
2765 + }
2766 +
2767 + // Alternative method: query by post_name
2768 + global $wpdb;
2769 + $post_id = $wpdb->get_var($wpdb->prepare(
2770 + "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'product' AND post_status = 'publish'",
2771 + $product_slug
2772 + ));
2773 +
2774 + if ($post_id) {
2775 + return intval($post_id);
2776 + }
2777 + }
2778 + }
2779 +
2780 + // Generic approach: try to extract slug and match against all post types
2781 + $parsed_url = wp_parse_url($clean_url);
2782 + $path = $parsed_url['path'] ?? '';
2783 +
2784 + if (!empty($path)) {
2785 + // Get the last part of the path as potential slug
2786 + $path_parts = array_filter(explode('/', trim($path, '/')));
2787 + $potential_slug = end($path_parts);
2788 +
2789 + if (!empty($potential_slug)) {
2790 + global $wpdb;
2791 +
2792 + // Try to find any post with this slug
2793 + $post_id = $wpdb->get_var($wpdb->prepare(
2794 + "SELECT ID FROM {$wpdb->posts}
2795 + WHERE post_name = %s
2796 + AND post_status IN ('publish', 'closed', 'private')
2797 + AND post_type NOT IN ('revision', 'attachment', 'nav_menu_item')
2798 + ORDER BY CASE
2799 + WHEN post_type = 'post' THEN 1
2800 + WHEN post_type = 'page' THEN 2
2801 + WHEN post_type = 'topic' THEN 3
2802 + WHEN post_type = 'product' THEN 4
2803 + ELSE 5
2804 + END
2805 + LIMIT 1",
2806 + $potential_slug
2807 + ));
2808 +
2809 + if ($post_id) {
2810 + return intval($post_id);
2811 + }
2812 + }
2813 + }
2814 +
2815 + // ADDITIONAL: Try direct database lookup by URL variations
2816 + global $wpdb;
2817 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2818 +
2819 + // Try variations of the URL (with/without trailing slash, http/https)
2820 + $url_variations = array(
2821 + $url,
2822 + rtrim($url, '/'),
2823 + $url . '/',
2824 + str_replace('http://', 'https://', $url),
2825 + str_replace('https://', 'http://', $url),
2826 + str_replace('http://', 'https://', rtrim($url, '/')),
2827 + str_replace('https://', 'http://', rtrim($url, '/'))
2828 + );
2829 +
2830 + // Remove duplicates
2831 + $url_variations = array_unique($url_variations);
2832 +
2833 + foreach ($url_variations as $variation) {
2834 + $existing_record = $wpdb->get_row($wpdb->prepare(
2835 + "SELECT id, source_url FROM $table_name WHERE source_url = %s",
2836 + $variation
2837 + ));
2838 +
2839 + if ($existing_record) {
2840 + // Try to get post ID from this stored URL
2841 + $stored_post_id = url_to_postid($existing_record->source_url);
2842 + if ($stored_post_id > 0) {
2843 + return $stored_post_id;
2844 + }
2845 + }
2846 + }
2847 +
2848 + return 0; // No match found
2849 +}
2850 +/**
2851 + * Process selected content via AJAX
2852 + */
2853 +public function ajax_mxchat_process_selected_content() {
2854 + // Basic request validation
2855 + if (!check_ajax_referer('mxchat_content_selector_nonce', 'nonce', false)) {
2856 + wp_send_json_error('Invalid nonce');
2857 + exit;
2858 + }
2859 +
2860 + if (!current_user_can('manage_options')) {
2861 + wp_send_json_error('Unauthorized access');
2862 + exit;
2863 + }
2864 +
2865 + // Get post IDs - safely parse the array
2866 + $post_ids = array();
2867 + if (isset($_POST['post_ids']) && is_array($_POST['post_ids'])) {
2868 + foreach ($_POST['post_ids'] as $id) {
2869 + $post_ids[] = absint($id);
2870 + }
2871 + }
2872 +
2873 + if (empty($post_ids)) {
2874 + wp_send_json_error('No content selected');
2875 + exit;
2876 + }
2877 +
2878 + // Get bot_id from request
2879 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
2880 +
2881 + // Process only ONE post at a time to avoid request size issues
2882 + $post_id = reset($post_ids);
2883 + $post = get_post($post_id);
2884 +
2885 + if (!$post) {
2886 + wp_send_json_error('Post not found');
2887 + exit;
2888 + }
2889 +
2890 + // Get content including title, short description (for WooCommerce), and main content
2891 + $content = $post->post_title . "\n\n";
2892 +
2893 + // Add short description if it exists (WooCommerce products use post_excerpt for short description)
2894 + if (!empty($post->post_excerpt)) {
2895 + // Remove shortcode tags but preserve content inside them
2896 + $clean_excerpt = $this->strip_shortcode_tags_preserve_content($post->post_excerpt);
2897 + $content .= "Short Description: " . wp_strip_all_tags($clean_excerpt) . "\n\n";
2898 + }
2899 +
2900 + // Add main content - remove shortcode tags but preserve content inside them
2901 + $clean_content = $this->strip_shortcode_tags_preserve_content($post->post_content);
2902 + $content .= wp_strip_all_tags($clean_content);
2903 +
2904 + // ADD WOOCOMMERCE PRODUCT DATA (pricing, stock, categories, custom tabs)
2905 + if (get_post_type($post_id) === 'product' && class_exists('WooCommerce')) {
2906 + $product = wc_get_product($post_id);
2907 +
2908 + if ($product) {
2909 + // Get pricing information
2910 + $regular_price = $product->get_regular_price();
2911 + $sale_price = $product->get_sale_price();
2912 + $price = $product->get_price();
2913 + $sku = $product->get_sku();
2914 +
2915 + // Get currency symbol
2916 + $currency_symbol = get_woocommerce_currency_symbol();
2917 +
2918 + // Add pricing information
2919 + $content .= "\n";
2920 + if (!empty($regular_price)) {
2921 + $content .= "Price: " . $currency_symbol . $regular_price . "\n";
2922 + } elseif (!empty($price)) {
2923 + $content .= "Price: " . $currency_symbol . $price . "\n";
2924 + }
2925 +
2926 + if (!empty($sale_price) && $sale_price !== $regular_price) {
2927 + $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
2928 + }
2929 +
2930 + // Handle variable products - show price range
2931 + if ($product->is_type('variable')) {
2932 + $min_price = $product->get_variation_price('min');
2933 + $max_price = $product->get_variation_price('max');
2934 + if ($min_price !== $max_price) {
2935 + $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
2936 + }
2937 + }
2938 +
2939 + if (!empty($sku)) {
2940 + $content .= "SKU: " . $sku . "\n";
2941 + }
2942 +
2943 + // Get product categories
2944 + $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
2945 + if (!empty($categories) && !is_wp_error($categories)) {
2946 + $content .= "Categories: " . implode(', ', $categories) . "\n";
2947 + }
2948 + }
2949 +
2950 + // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
2951 + $custom_tabs = get_post_meta($post_id, 'yikes_woo_products_tabs', true);
2952 + if (!empty($custom_tabs) && is_array($custom_tabs)) {
2953 + foreach ($custom_tabs as $tab) {
2954 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
2955 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
2956 +
2957 + if (!empty($tab_title) && !empty($tab_content)) {
2958 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
2959 + }
2960 + }
2961 + }
2962 +
2963 + // Also check for reusable/saved tabs applied to this product
2964 + $applied_saved_tabs = get_post_meta($post_id, 'yikes_woo_reusable_products_tabs_applied', true);
2965 + if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
2966 + $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
2967 + if (!empty($saved_tabs) && is_array($saved_tabs)) {
2968 + foreach ($applied_saved_tabs as $saved_tab_id) {
2969 + if (isset($saved_tabs[$saved_tab_id])) {
2970 + $tab = $saved_tabs[$saved_tab_id];
2971 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
2972 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
2973 +
2974 + if (!empty($tab_title) && !empty($tab_content)) {
2975 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
2976 + }
2977 + }
2978 + }
2979 + }
2980 + }
2981 + }
2982 +
2983 + // ADD ACF FIELDS SUPPORT
2984 + $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
2985 + if (!empty($acf_fields)) {
2986 + $acf_content_parts = array();
2987 +
2988 + foreach ($acf_fields as $field_name => $field_value) {
2989 + $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
2990 +
2991 + if (!empty($formatted_value)) {
2992 + $field_label = ucwords(str_replace('_', ' ', $field_name));
2993 + $acf_content_parts[] = $field_label . ": " . $formatted_value;
2994 + }
2995 + }
2996 +
2997 + if (!empty($acf_content_parts)) {
2998 + $content .= "\n\n" . implode("\n", $acf_content_parts);
2999 + }
3000 + }
3001 +
3002 + // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
3003 + $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
3004 + if (!empty($custom_meta)) {
3005 + $meta_content_parts = array();
3006 +
3007 + foreach ($custom_meta as $meta_key => $meta_value) {
3008 + // Convert meta key to readable label
3009 + $meta_label = ucwords(str_replace(array('_', '-'), ' ', $meta_key));
3010 + $meta_content_parts[] = $meta_label . ": " . $meta_value;
3011 + }
3012 +
3013 + if (!empty($meta_content_parts)) {
3014 + $content .= "\n\n" . implode("\n", $meta_content_parts);
3015 + }
3016 + }
3017 +
3018 + // Debug logging for WordPress Import content
3019 + error_log('[MXCHAT-WP-IMPORT-DEBUG] Post ID: ' . $post_id . ' Title: ' . $post->post_title);
3020 + error_log('[MXCHAT-WP-IMPORT-DEBUG] Raw post_content length: ' . strlen($post->post_content));
3021 + error_log('[MXCHAT-WP-IMPORT-DEBUG] Final content length: ' . strlen($content));
3022 + error_log('[MXCHAT-WP-IMPORT-DEBUG] Content preview: ' . substr($content, 0, 300));
3023 +
3024 + // Note: Removed 10,000 char limit - chunking now handles large content properly
3025 +
3026 + // Get bot-specific API key
3027 + $bot_options = $this->get_bot_options($bot_id);
3028 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
3029 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3030 +
3031 + if (strpos($selected_model, 'voyage') === 0) {
3032 + $api_key = $options['voyage_api_key'] ?? '';
3033 + $provider_name = 'Voyage AI';
3034 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3035 + $api_key = $options['gemini_api_key'] ?? '';
3036 + $provider_name = 'Google Gemini';
3037 + } else {
3038 + $api_key = $options['api_key'] ?? '';
3039 + $provider_name = 'OpenAI';
3040 + }
3041 +
3042 + if (empty($api_key)) {
3043 + wp_send_json_error($provider_name . ' API key not configured');
3044 + exit;
3045 + }
3046 +
3047 + $source_url = get_permalink($post_id);
3048 + $vector_id = md5($source_url); // Vector ID for Pinecone
3049 +
3050 + // Check for existing content in bot-specific storage
3051 + $is_update = false;
3052 +
3053 + // Get bot-specific Pinecone configuration
3054 + $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
3055 + $use_pinecone = !empty($bot_pinecone_config) && ($bot_pinecone_config['use_pinecone'] ?? false);
3056 +
3057 + if ($use_pinecone && !empty($bot_pinecone_config['api_key'])) {
3058 + // Check Pinecone for this bot
3059 + $pinecone_data = $this->mxchat_get_pinecone_processed_content($bot_pinecone_config);
3060 + if (isset($pinecone_data[$post_id])) {
3061 + $is_update = true;
3062 + }
3063 + } else {
3064 + // Check WordPress DB (same as before since it's shared)
3065 + global $wpdb;
3066 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3067 + $existing_record = $wpdb->get_row($wpdb->prepare(
3068 + "SELECT id FROM $table_name WHERE source_url = %s",
3069 + $source_url
3070 + ));
3071 +
3072 + if ($existing_record) {
3073 + $is_update = true;
3074 + }
3075 + }
3076 +
3077 + // UPDATED 2.5.6: Determine content type based on post_type
3078 + $post_type = $post->post_type;
3079 + $content_type = 'content'; // Default fallback
3080 +
3081 + // Map WordPress post types to content types
3082 + switch ($post_type) {
3083 + case 'post':
3084 + $content_type = 'post';
3085 + break;
3086 + case 'page':
3087 + $content_type = 'page';
3088 + break;
3089 + case 'product':
3090 + $content_type = 'product';
3091 + break;
3092 + default:
3093 + // For custom post types, use the post type name
3094 + $content_type = sanitize_key($post_type);
3095 + break;
3096 + }
3097 +
3098 + // Use the centralized utility function with bot_id and content_type
3099 + $result = MxChat_Utils::submit_content_to_db(
3100 + $content,
3101 + $source_url,
3102 + $api_key,
3103 + $vector_id,
3104 + $bot_id,
3105 + $content_type
3106 + );
3107 +
3108 + if (is_wp_error($result)) {
3109 + wp_send_json_error('Storage failed: ' . $result->get_error_message());
3110 + exit;
3111 + }
3112 +
3113 + // Automatically apply role restriction based on tags
3114 + $this->apply_role_restriction_to_post($post_id, $source_url);
3115 +
3116 + $operation_type = $is_update ? 'update' : 'new';
3117 +
3118 + // Count ACF fields for debugging
3119 + $acf_field_count = count($acf_fields);
3120 +
3121 + // Success response with minimal data
3122 + wp_send_json_success(array(
3123 + 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully',
3124 + 'post_id' => $post_id,
3125 + 'title' => $post->post_title,
3126 + 'operation_type' => $operation_type,
3127 + 'vector_id' => $vector_id,
3128 + 'acf_fields_found' => $acf_field_count,
3129 + 'content_preview' => substr($content, 0, 100) . '...',
3130 + 'bot_id' => $bot_id
3131 + ));
3132 + exit;
3133 +}
3134 +
3135 +private function apply_role_restriction_to_post($post_id, $source_url) {
3136 + // Get tag-role mappings
3137 + $mappings = get_option('mxchat_tag_role_mappings', array());
3138 +
3139 + if (empty($mappings)) {
3140 + return; // No mappings, leave as public
3141 + }
3142 +
3143 + // Get all tags for the post
3144 + $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
3145 +
3146 + if (empty($post_tags)) {
3147 + return; // No tags, leave as public
3148 + }
3149 +
3150 + // Determine the highest role restriction based on tags
3151 + $highest_role = 'public';
3152 + $role_hierarchy = array(
3153 + 'public' => 0,
3154 + 'logged_in' => 1,
3155 + 'subscriber' => 2,
3156 + 'contributor' => 3,
3157 + 'author' => 4,
3158 + 'editor' => 5,
3159 + 'administrator' => 6
3160 + );
3161 +
3162 + foreach ($post_tags as $tag_slug) {
3163 + if (isset($mappings[$tag_slug])) {
3164 + $role = $mappings[$tag_slug];
3165 + if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
3166 + $highest_role = $role;
3167 + }
3168 + }
3169 + }
3170 +
3171 + // If no restricted tags found, return (leave as public)
3172 + if ($highest_role === 'public') {
3173 + return;
3174 + }
3175 +
3176 + // Update the role restriction in the database
3177 + global $wpdb;
3178 +
3179 + // Check if using Pinecone
3180 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3181 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3182 +
3183 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3184 + // Update Pinecone role restriction
3185 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
3186 + $vector_id = md5($source_url);
3187 +
3188 + $wpdb->replace(
3189 + $roles_table,
3190 + array(
3191 + 'vector_id' => $vector_id,
3192 + 'role_restriction' => $highest_role,
3193 + 'updated_at' => current_time('mysql')
3194 + ),
3195 + array('%s', '%s', '%s')
3196 + );
3197 + } else {
3198 + // Update WordPress DB
3199 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3200 +
3201 + $wpdb->update(
3202 + $table_name,
3203 + array('role_restriction' => $highest_role),
3204 + array('source_url' => $source_url),
3205 + array('%s'),
3206 + array('%s')
3207 + );
3208 + }
3209 +}
3210 +
3211 +public function mxchat_get_public_post_types() {
3212 + // Get all public post types
3213 + $post_types = get_post_types(array('public' => true), 'objects');
3214 + $post_type_options = array();
3215 +
3216 + foreach ($post_types as $post_type) {
3217 + $post_type_options[$post_type->name] = $post_type->label;
3218 + }
3219 +
3220 + // Also include common forum/community post types that might not be marked as public
3221 + $additional_types = array(
3222 + 'topic' => 'Forum Topics (bbPress)',
3223 + 'reply' => 'Forum Replies (bbPress)',
3224 + 'forum' => 'Forums (bbPress)',
3225 + 'wpforo_topic' => 'wpForo Topics',
3226 + 'wpforo_post' => 'wpForo Posts'
3227 + );
3228 +
3229 + foreach ($additional_types as $type_name => $type_label) {
3230 + if (post_type_exists($type_name) && !isset($post_type_options[$type_name])) {
3231 + $post_type_options[$type_name] = $type_label;
3232 + }
3233 + }
3234 +
3235 + return $post_type_options;
3236 +}
3237 +
3238 +/**
3239 + * Retrieves processed content from Pinecone API
3240 + */
3241 +public function mxchat_get_pinecone_processed_content($pinecone_options) {
3242 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3243 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3244 +
3245 + if (empty($api_key) || empty($host)) {
3246 + return array();
3247 + }
3248 +
3249 + $pinecone_data = array();
3250 +
3251 + try {
3252 + // Always get fresh data from Pinecone
3253 + $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
3254 +
3255 + // Method 2: Final fallback - try stats endpoint (if available)
3256 + if (empty($pinecone_data)) {
3257 + $stats_url = "https://{$host}/describe_index_stats";
3258 +
3259 + $response = wp_remote_post($stats_url, array(
3260 + 'headers' => array(
3261 + 'Api-Key' => $api_key,
3262 + 'Content-Type' => 'application/json'
3263 + ),
3264 + 'body' => json_encode(array()),
3265 + 'timeout' => 30
3266 + ));
3267 +
3268 + if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
3269 + $body = wp_remote_retrieve_body($response);
3270 + $stats_data = json_decode($body, true);
3271 + }
3272 + }
3273 +
3274 + } catch (Exception $e) {
3275 + // Log error but return fresh data only
3276 + }
3277 +
3278 + return $pinecone_data;
3279 +}
3280 +public function mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
3281 + //error_log('=== DEBUG: Starting mxchat_fetch_pinecone_vectors_by_ids ===');
3282 +
3283 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3284 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3285 +
3286 + if (empty($api_key) || empty($host) || empty($vector_ids)) {
3287 + //error_log('DEBUG: Missing parameters for fetch by IDs');
3288 + return array();
3289 + }
3290 +
3291 + try {
3292 + $fetch_url = "https://{$host}/vectors/fetch";
3293 + //error_log('DEBUG: Fetch URL: ' . $fetch_url);
3294 + //error_log('DEBUG: Fetching ' . count($vector_ids) . ' vector IDs');
3295 +
3296 + // Pinecone fetch API allows fetching specific vectors by ID
3297 + $fetch_data = array(
3298 + 'ids' => array_values($vector_ids)
3299 + );
3300 +
3301 + $response = wp_remote_post($fetch_url, array(
3302 + 'headers' => array(
3303 + 'Api-Key' => $api_key,
3304 + 'Content-Type' => 'application/json'
3305 + ),
3306 + 'body' => json_encode($fetch_data),
3307 + 'timeout' => 30
3308 + ));
3309 +
3310 + if (is_wp_error($response)) {
3311 + //error_log('DEBUG: Fetch by IDs WP error: ' . $response->get_error_message());
3312 + return array();
3313 + }
3314 +
3315 + $response_code = wp_remote_retrieve_response_code($response);
3316 + //error_log('DEBUG: Fetch response code: ' . $response_code);
3317 +
3318 + if ($response_code !== 200) {
3319 + $error_body = wp_remote_retrieve_body($response);
3320 + //error_log('DEBUG: Fetch failed with body: ' . $error_body);
3321 + return array();
3322 + }
3323 +
3324 + $body = wp_remote_retrieve_body($response);
3325 + $data = json_decode($body, true);
3326 +
3327 + //error_log('DEBUG: Fetch response structure: ' . print_r(array_keys($data), true));
3328 +
3329 + if (!isset($data['vectors'])) {
3330 + //error_log('DEBUG: No vectors key in response');
3331 + return array();
3332 + }
3333 +
3334 + $processed_data = array();
3335 +
3336 + foreach ($data['vectors'] as $vector_id => $vector_data) {
3337 + $metadata = $vector_data['metadata'] ?? array();
3338 + $source_url = $metadata['source_url'] ?? '';
3339 +
3340 + if (!empty($source_url)) {
3341 + $post_id = url_to_postid($source_url);
3342 + if ($post_id) {
3343 + $created_at = $metadata['created_at'] ?? '';
3344 + $processed_date = 'Recently';
3345 +
3346 + if (!empty($created_at)) {
3347 + $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
3348 + if ($timestamp) {
3349 + $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
3350 + }
3351 + }
3352 +
3353 + $processed_data[$post_id] = array(
3354 + 'db_id' => $vector_id,
3355 + 'processed_date' => $processed_date,
3356 + 'url' => $source_url,
3357 + 'source' => 'pinecone',
3358 + 'timestamp' => $timestamp ?? current_time('timestamp')
3359 + );
3360 + }
3361 + }
3362 + }
3363 +
3364 + //error_log('DEBUG: Processed ' . count($processed_data) . ' vectors from fetch');
3365 + return $processed_data;
3366 +
3367 + } catch (Exception $e) {
3368 + //error_log('DEBUG: Exception in fetch_pinecone_vectors_by_ids: ' . $e->getMessage());
3369 + return array();
3370 + }
3371 +}
3372 +
3373 +/**
3374 + * Scan Pinecone for processed content
3375 + */
3376 +public function mxchat_scan_pinecone_for_processed_content($pinecone_options) {
3377 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3378 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3379 +
3380 + if (empty($api_key) || empty($host)) {
3381 + return array();
3382 + }
3383 +
3384 + try {
3385 + // Use multiple random vectors to get better coverage
3386 + $all_matches = array();
3387 + $seen_ids = array();
3388 +
3389 + // Try 3 different random vectors to get better coverage
3390 + for ($i = 0; $i < 3; $i++) {
3391 + $query_url = "https://{$host}/query";
3392 +
3393 + // Generate a random unit vector instead of zeros
3394 + $random_vector = array();
3395 + for ($j = 0; $j < 1536; $j++) {
3396 + $random_vector[] = (rand(-1000, 1000) / 1000.0);
3397 + }
3398 +
3399 + // Normalize the vector to unit length
3400 + $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
3401 + if ($magnitude > 0) {
3402 + $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
3403 + }
3404 +
3405 + $query_data = array(
3406 + 'includeMetadata' => true,
3407 + 'includeValues' => false,
3408 + 'topK' => 10000,
3409 + 'vector' => $random_vector
3410 + );
3411 +
3412 + $response = wp_remote_post($query_url, array(
3413 + 'headers' => array(
3414 + 'Api-Key' => $api_key,
3415 + 'Content-Type' => 'application/json'
3416 + ),
3417 + 'body' => json_encode($query_data),
3418 + 'timeout' => 30
3419 + ));
3420 +
3421 + if (is_wp_error($response)) {
3422 + continue;
3423 + }
3424 +
3425 + $response_code = wp_remote_retrieve_response_code($response);
3426 +
3427 + if ($response_code !== 200) {
3428 + continue;
3429 + }
3430 +
3431 + $body = wp_remote_retrieve_body($response);
3432 + $data = json_decode($body, true);
3433 +
3434 + if (isset($data['matches'])) {
3435 + foreach ($data['matches'] as $match) {
3436 + $match_id = $match['id'] ?? '';
3437 + if (!empty($match_id) && !isset($seen_ids[$match_id])) {
3438 + $all_matches[] = $match;
3439 + $seen_ids[$match_id] = true;
3440 + }
3441 + }
3442 + }
3443 + }
3444 +
3445 + // Convert matches to processed data format, grouping by URL to count chunks
3446 + $processed_data = array();
3447 + $url_chunk_counts = array();
3448 +
3449 + foreach ($all_matches as $match) {
3450 + $metadata = $match['metadata'] ?? array();
3451 + $source_url = $metadata['source_url'] ?? '';
3452 + $match_id = $match['id'] ?? '';
3453 +
3454 + if (!empty($source_url) && !empty($match_id)) {
3455 + $post_id = url_to_postid($source_url);
3456 + if ($post_id) {
3457 + // Count chunks per post_id
3458 + if (!isset($url_chunk_counts[$post_id])) {
3459 + $url_chunk_counts[$post_id] = 0;
3460 + }
3461 + $url_chunk_counts[$post_id]++;
3462 +
3463 + $created_at = $metadata['created_at'] ?? '';
3464 + $processed_date = 'Recently';
3465 +
3466 + if (!empty($created_at)) {
3467 + $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
3468 + if ($timestamp) {
3469 + $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
3470 + }
3471 + }
3472 +
3473 + // Only store if not already set, or update with newer timestamp
3474 + if (!isset($processed_data[$post_id]) ||
3475 + ($timestamp ?? 0) > ($processed_data[$post_id]['timestamp'] ?? 0)) {
3476 + $processed_data[$post_id] = array(
3477 + 'db_id' => $match_id,
3478 + 'processed_date' => $processed_date,
3479 + 'url' => $source_url,
3480 + 'source' => 'pinecone',
3481 + 'timestamp' => $timestamp ?? current_time('timestamp')
3482 + );
3483 + }
3484 + }
3485 + }
3486 + }
3487 +
3488 + // Add chunk counts to processed data
3489 + foreach ($url_chunk_counts as $post_id => $chunk_count) {
3490 + if (isset($processed_data[$post_id])) {
3491 + $processed_data[$post_id]['chunk_count'] = $chunk_count;
3492 + }
3493 + }
3494 +
3495 + return $processed_data;
3496 +
3497 + } catch (Exception $e) {
3498 + return array();
3499 + }
3500 +}
3501 +/**
3502 + * Generate embeddings from input text for MXChat with bot support
3503 + */
3504 +private function mxchat_generate_embedding($text, $bot_id = 'default') {
3505 + // Enable detailed logging for debugging
3506 + //error_log('[MXCHAT-EMBED] Starting embedding generation for bot: ' . $bot_id . '. Text length: ' . strlen($text) . ' bytes');
3507 + //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
3508 +
3509 + // Get bot-specific options
3510 + $bot_options = $this->get_bot_options($bot_id);
3511 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
3512 +
3513 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3514 + //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model);
3515 +
3516 + // Determine provider and endpoint
3517 + if (strpos($selected_model, 'voyage') === 0) {
3518 + $api_key = $options['voyage_api_key'] ?? '';
3519 + $endpoint = 'https://api.voyageai.com/v1/embeddings';
3520 + $provider_name = 'Voyage AI';
3521 + //error_log('[MXCHAT-EMBED] Using Voyage AI API for bot ' . $bot_id);
3522 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3523 + $api_key = $options['gemini_api_key'] ?? '';
3524 + $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
3525 + $provider_name = 'Google Gemini';
3526 + //error_log('[MXCHAT-EMBED] Using Google Gemini API for bot ' . $bot_id);
3527 + } else {
3528 + $api_key = $options['api_key'] ?? '';
3529 + $endpoint = 'https://api.openai.com/v1/embeddings';
3530 + $provider_name = 'OpenAI';
3531 + //error_log('[MXCHAT-EMBED] Using OpenAI API for bot ' . $bot_id);
3532 + }
3533 +
3534 + //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
3535 +
3536 + if (empty($api_key)) {
3537 + $error_message = sprintf('Missing %s API key for bot %s. Please configure your API key in the bot settings.', $provider_name, $bot_id);
3538 + //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
3539 + return $error_message;
3540 + }
3541 +
3542 + // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
3543 + $estimated_tokens = ceil(str_word_count($text) / 0.75);
3544 + //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
3545 +
3546 + if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
3547 + //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
3548 + // Consider truncating text here
3549 + }
3550 +
3551 + // Prepare request body based on provider
3552 + if (strpos($selected_model, 'gemini-embedding') === 0) {
3553 + // Gemini API format
3554 + $request_body = array(
3555 + 'model' => 'models/' . $selected_model,
3556 + 'content' => array(
3557 + 'parts' => array(
3558 + array('text' => $text)
3559 + )
3560 + )
3561 + );
3562 +
3563 + // Set output dimensionality to 1536 for consistency with other models
3564 + $request_body['outputDimensionality'] = 1536;
3565 + } else {
3566 + // OpenAI/Voyage API format
3567 + $request_body = array(
3568 + 'model' => $selected_model,
3569 + 'input' => $text
3570 + );
3571 +
3572 + // Add output_dimension for voyage-3-large model
3573 + if ($selected_model === 'voyage-3-large') {
3574 + $request_body['output_dimension'] = 2048;
3575 + }
3576 + }
3577 +
3578 + //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
3579 +
3580 + // Prepare headers based on provider
3581 + if (strpos($selected_model, 'gemini-embedding') === 0) {
3582 + // Gemini uses API key as query parameter
3583 + $endpoint .= '?key=' . $api_key;
3584 + $headers = array(
3585 + 'Content-Type' => 'application/json'
3586 + );
3587 + } else {
3588 + // OpenAI/Voyage use Bearer token
3589 + $headers = array(
3590 + 'Authorization' => 'Bearer ' . $api_key,
3591 + 'Content-Type' => 'application/json'
3592 + );
3593 + }
3594 +
3595 + // Make API request
3596 + //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
3597 + $response = wp_remote_post($endpoint, array(
3598 + 'body' => wp_json_encode($request_body),
3599 + 'headers' => $headers,
3600 + 'timeout' => 60 // Increased timeout for large inputs
3601 + ));
3602 +
3603 + // Handle wp_remote_post errors
3604 + if (is_wp_error($response)) {
3605 + $error_message = $response->get_error_message();
3606 + //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
3607 + return 'Connection error: ' . $error_message;
3608 + }
3609 +
3610 + // Get and check HTTP response code
3611 + $http_code = wp_remote_retrieve_response_code($response);
3612 + //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
3613 +
3614 + if ($http_code !== 200) {
3615 + $error_body = wp_remote_retrieve_body($response);
3616 + //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
3617 +
3618 + // Try to parse error for more details
3619 + $error_json = json_decode($error_body, true);
3620 + if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
3621 + $error_type = $error_json['error']['type'] ?? 'unknown';
3622 + $error_message = $error_json['error']['message'] ?? 'No message';
3623 + //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
3624 + //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
3625 +
3626 + // Customize error message for common API errors
3627 + if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
3628 + $error_message = sprintf('Invalid %s API key for bot %s. Please check your API key in the bot settings.', $provider_name, $bot_id);
3629 + } elseif ($error_type === 'authentication_error') {
3630 + $error_message = sprintf('%s authentication failed for bot %s. Please verify your API key in the bot settings.', $provider_name, $bot_id);
3631 + }
3632 +
3633 + //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
3634 + return $error_message;
3635 + }
3636 +
3637 + $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding for bot %s", $http_code, $bot_id);
3638 + //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
3639 + return $error_message;
3640 + }
3641 +
3642 + // Parse response body
3643 + $response_body = wp_remote_retrieve_body($response);
3644 + //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
3645 +
3646 + $response_data = json_decode($response_body, true);
3647 +
3648 + if (json_last_error() !== JSON_ERROR_NONE) {
3649 + $error = json_last_error_msg();
3650 + //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
3651 + //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
3652 + return "Failed to parse API response: $error";
3653 + }
3654 +
3655 + // Handle different response formats based on provider
3656 + if (strpos($selected_model, 'gemini-embedding') === 0) {
3657 + // Gemini API response format
3658 + if (isset($response_data['embedding']['values'])) {
3659 + $embedding_dimensions = count($response_data['embedding']['values']);
3660 + //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
3661 +
3662 + // Check if embedding dimensions are as expected (should be 1536)
3663 + if ($embedding_dimensions !== 1536) {
3664 + //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
3665 + }
3666 +
3667 + return $response_data['embedding']['values'];
3668 + } else {
3669 + //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
3670 + //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
3671 +
3672 + if (isset($response_data['error'])) {
3673 + $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
3674 + //error_log('[MXCHAT-EMBED] ' . $error_message);
3675 + return $error_message;
3676 + }
3677 +
3678 + $error_message = "Invalid Gemini API response format: No embedding found";
3679 + //error_log('[MXCHAT-EMBED] ' . $error_message);
3680 + return $error_message;
3681 + }
3682 + } else {
3683 + // OpenAI/Voyage API response format
3684 + if (isset($response_data['data'][0]['embedding'])) {
3685 + $embedding_dimensions = count($response_data['data'][0]['embedding']);
3686 + //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
3687 +
3688 + // Check if embedding dimensions are as expected
3689 + if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
3690 + ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
3691 + //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
3692 + }
3693 +
3694 + return $response_data['data'][0]['embedding'];
3695 + } else {
3696 + //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
3697 + //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
3698 +
3699 + if (isset($response_data['error'])) {
3700 + $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
3701 + //error_log('[MXCHAT-EMBED] ' . $error_message);
3702 + return $error_message;
3703 + }
3704 +
3705 + $error_message = "Invalid API response format: No embedding found";
3706 + //error_log('[MXCHAT-EMBED] ' . $error_message);
3707 + return $error_message;
3708 + }
3709 + }
3710 +}
3711 +
3712 +/**
3713 + * Get bot-specific options for multi-bot functionality
3714 + * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
3715 + */
3716 +private function get_bot_options($bot_id = 'default') {
3717 + //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
3718 +
3719 + if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
3720 + //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
3721 + return array();
3722 + }
3723 +
3724 + $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
3725 +
3726 + if (!empty($bot_options)) {
3727 + //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
3728 + if (isset($bot_options['similarity_threshold'])) {
3729 + //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
3730 + }
3731 + }
3732 +
3733 + return is_array($bot_options) ? $bot_options : array();
3734 +}
3735 +
3736 +/**
3737 + * Get bot-specific Pinecone configuration
3738 + * Used in the knowledge retrieval functions
3739 + */
3740 +// Also add debugging to your get_bot_pinecone_config function
3741 +private function get_bot_pinecone_config($bot_id = 'default') {
3742 + //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
3743 +
3744 + // If default bot or multi-bot add-on not active, use default Pinecone config
3745 + if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
3746 + //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
3747 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
3748 + $config = array(
3749 + 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
3750 + 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
3751 + 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
3752 + 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
3753 + );
3754 + //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
3755 + return $config;
3756 + }
3757 +
3758 + //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
3759 +
3760 + // Hook for multi-bot add-on to provide bot-specific Pinecone config
3761 + $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
3762 +
3763 + if (!empty($bot_pinecone_config)) {
3764 + //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
3765 + //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
3766 + //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
3767 + //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
3768 + } else {
3769 + //error_log("MXCHAT DEBUG: Filter returned empty config!");
3770 + }
3771 +
3772 + return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
3773 +}
3774 +
3775 +
3776 +public function mxchat_ajax_dismiss_completed_status() {
3777 + try {
3778 + // Verify the request
3779 + check_ajax_referer('mxchat_status_nonce', 'nonce');
3780 +
3781 + if (!current_user_can('manage_options')) {
3782 + wp_send_json_error('Unauthorized access');
3783 + exit;
3784 + }
3785 +
3786 + $card_type = isset($_POST['card_type']) ? sanitize_text_field($_POST['card_type']) : '';
3787 +
3788 + if ($card_type === 'pdf') {
3789 + // Clear PDF status
3790 + $pdf_url = get_transient('mxchat_last_pdf_url');
3791 + if ($pdf_url) {
3792 + delete_transient('mxchat_pdf_status_' . md5($pdf_url));
3793 + delete_transient('mxchat_last_pdf_url');
3794 + }
3795 + } elseif ($card_type === 'sitemap') {
3796 + // Clear sitemap status
3797 + $sitemap_url = get_transient('mxchat_last_sitemap_url');
3798 + if ($sitemap_url) {
3799 + delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
3800 + delete_transient('mxchat_last_sitemap_url');
3801 + }
3802 + }
3803 +
3804 + wp_send_json_success(array('message' => 'Status dismissed successfully'));
3805 +
3806 + } catch (Exception $e) {
3807 + wp_send_json_error(array('message' => 'Error dismissing status: ' . $e->getMessage()));
3808 + }
3809 +}
3810 +
3811 +/**
3812 + * Render completed status cards on page load
3813 + * This ensures completed processing status persists through page refreshes
3814 + */
3815 +public function mxchat_render_completed_status_cards() {
3816 + $output = '';
3817 +
3818 + // Check for completed PDF status
3819 + $pdf_url = get_transient('mxchat_last_pdf_url');
3820 + if ($pdf_url) {
3821 + $pdf_status = $this->mxchat_get_pdf_processing_status($pdf_url);
3822 + if ($pdf_status && ($pdf_status['status'] === 'complete' || $pdf_status['status'] === 'error')) {
3823 + $output .= $this->mxchat_render_pdf_status_card($pdf_status, $pdf_url);
3824 + }
3825 + }
3826 +
3827 + // Check for completed sitemap status
3828 + $sitemap_url = get_transient('mxchat_last_sitemap_url');
3829 + if ($sitemap_url) {
3830 + $sitemap_status = $this->mxchat_get_sitemap_processing_status($sitemap_url);
3831 + if ($sitemap_status && ($sitemap_status['status'] === 'complete' || $sitemap_status['status'] === 'error')) {
3832 + $output .= $this->mxchat_render_sitemap_status_card($sitemap_status, $sitemap_url);
3833 + }
3834 + }
3835 +
3836 + return $output;
3837 +}
3838 +
3839 +/**
3840 + * Render PDF status card HTML
3841 + */
3842 +private function mxchat_render_pdf_status_card($status, $pdf_url) {
3843 + $html = '<div class="mxchat-status-card" data-card-type="pdf">';
3844 + $html .= '<div class="mxchat-status-header">';
3845 + $html .= '<h4>' . esc_html__('PDF Processing Status', 'mxchat') . '</h4>';
3846 +
3847 + // Add dismiss button for completed status
3848 + if ($status['status'] === 'complete' || $status['status'] === 'error') {
3849 + $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
3850 + }
3851 +
3852 + // Process Batch button for processing status
3853 + if ($status['status'] === 'processing') {
3854 + $html .= '<button type="button" class="mxchat-manual-batch-btn"
3855 + data-process-type="pdf"
3856 + data-url="' . esc_attr($pdf_url) . '">
3857 + ' . esc_html__('Process Batch', 'mxchat') . '</button>';
3858 + }
3859 +
3860 + // Add status badges
3861 + if ($status['status'] === 'error') {
3862 + $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
3863 + } elseif ($status['status'] === 'complete') {
3864 + if ($status['failed_pages'] > 0) {
3865 + $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
3866 + sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_pages']) . '</span>';
3867 + } else {
3868 + $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
3869 + }
3870 + }
3871 +
3872 + $html .= '</div>'; // End header
3873 +
3874 + // Progress bar
3875 + $html .= '<div class="mxchat-progress-bar">';
3876 + $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
3877 + $html .= '</div>';
3878 +
3879 + // Status details
3880 + $html .= '<div class="mxchat-status-details">';
3881 + $html .= '<p>' . sprintf(
3882 + esc_html__('Progress: %d of %d pages (%d%%)', 'mxchat'),
3883 + $status['processed_pages'],
3884 + $status['total_pages'],
3885 + $status['percentage']
3886 + ) . '</p>';
3887 +
3888 + // Show failed pages count if any
3889 + if ($status['failed_pages'] > 0) {
3890 + $html .= '<p><strong>' . esc_html__('Failed pages:', 'mxchat') . '</strong> ' . esc_html($status['failed_pages']) . '</p>';
3891 + }
3892 +
3893 + $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
3894 + $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
3895 +
3896 + // Add completion summary if available AND it's an array
3897 + if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
3898 + $summary = $status['completion_summary'];
3899 + $html .= '<div class="mxchat-completion-summary">';
3900 + $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
3901 + $html .= '<p><strong>' . esc_html__('Total Pages:', 'mxchat') . '</strong> ' . esc_html($summary['total_pages']) . '</p>';
3902 + $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_pages']) . '</p>';
3903 + $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_pages']) . '</p>';
3904 + $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
3905 + $html .= '</div>';
3906 + }
3907 +
3908 + // Add failed pages list if any AND it's an array
3909 + if (isset($status['failed_pages_list']) && is_array($status['failed_pages_list']) && !empty($status['failed_pages_list'])) {
3910 + $html .= $this->mxchat_render_failed_pages_list($status['failed_pages_list']);
3911 + }
3912 +
3913 + // Add error message if any
3914 + if (isset($status['error']) && !empty($status['error'])) {
3915 + $html .= '<div class="mxchat-error-notice">';
3916 + $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
3917 + $html .= '</div>';
3918 + }
3919 +
3920 + $html .= '</div>'; // End details
3921 + $html .= '</div>'; // End card
3922 +
3923 + return $html;
3924 +}
3925 +/**
3926 + * Render sitemap status card HTML
3927 + */
3928 +private function mxchat_render_sitemap_status_card($status, $sitemap_url) {
3929 + $html = '<div class="mxchat-status-card" data-card-type="sitemap">';
3930 + $html .= '<div class="mxchat-status-header">';
3931 + $html .= '<h4>' . esc_html__('Sitemap Processing Status', 'mxchat') . '</h4>';
3932 +
3933 + // Add dismiss button for completed status
3934 + if ($status['status'] === 'complete' || $status['status'] === 'error') {
3935 + $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
3936 + }
3937 +
3938 + // Process Batch button for processing status
3939 + if ($status['status'] === 'processing') {
3940 + $html .= '<button type="button" class="mxchat-manual-batch-btn"
3941 + data-process-type="sitemap"
3942 + data-url="' . esc_attr($sitemap_url) . '">
3943 + ' . esc_html__('Process Batch', 'mxchat') . '</button>';
3944 + }
3945 +
3946 + // Add status badges
3947 + if ($status['status'] === 'error') {
3948 + $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
3949 + } elseif ($status['status'] === 'complete') {
3950 + if ($status['failed_urls'] > 0) {
3951 + $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
3952 + sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_urls']) . '</span>';
3953 + } else {
3954 + $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
3955 + }
3956 + }
3957 +
3958 + $html .= '</div>'; // End header
3959 +
3960 + // Progress bar
3961 + $html .= '<div class="mxchat-progress-bar">';
3962 + $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
3963 + $html .= '</div>';
3964 +
3965 + // Status details
3966 + $html .= '<div class="mxchat-status-details">';
3967 + $html .= '<p>' . sprintf(
3968 + esc_html__('Progress: %d of %d URLs (%d%%)', 'mxchat'),
3969 + $status['processed_urls'],
3970 + $status['total_urls'],
3971 + $status['percentage']
3972 + ) . '</p>';
3973 +
3974 + // Show failed URLs count if any
3975 + if ($status['failed_urls'] > 0) {
3976 + $html .= '<p><strong>' . esc_html__('Failed URLs:', 'mxchat') . '</strong> ' . esc_html($status['failed_urls']) . '</p>';
3977 + }
3978 +
3979 + $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
3980 + $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
3981 +
3982 + // Add completion summary if available AND it's an array
3983 + if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
3984 + $summary = $status['completion_summary'];
3985 + $html .= '<div class="mxchat-completion-summary">';
3986 + $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
3987 + $html .= '<p><strong>' . esc_html__('Total URLs:', 'mxchat') . '</strong> ' . esc_html($summary['total_urls']) . '</p>';
3988 + $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_urls']) . '</p>';
3989 + $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_urls']) . '</p>';
3990 + $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
3991 + $html .= '</div>';
3992 + }
3993 +
3994 + // Add error messages if any (but not the failed URLs list)
3995 + if (!empty($status['error']) || !empty($status['last_error'])) {
3996 + $html .= '<div class="mxchat-error-notice">';
3997 +
3998 + if (!empty($status['error'])) {
3999 + $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
4000 + }
4001 +
4002 + if (!empty($status['last_error'])) {
4003 + $html .= '<p class="last-error">' . esc_html__('Last error:', 'mxchat') . ' ' . esc_html($status['last_error']) . '</p>';
4004 + }
4005 +
4006 + $html .= '</div>';
4007 + }
4008 +
4009 + $html .= '</div>'; // End details
4010 + $html .= '</div>'; // End card
4011 +
4012 + return $html;
4013 +}
4014 +
4015 +
4016 +/**
4017 + * Render failed pages list
4018 + */
4019 +private function mxchat_render_failed_pages_list($failed_pages_list) {
4020 + // Validate that $failed_pages_list is an array and not empty
4021 + if (!is_array($failed_pages_list) || empty($failed_pages_list)) {
4022 + return '';
4023 + }
4024 +
4025 + $html = '<div class="mxchat-error-notice">';
4026 + $html .= '<div class="mxchat-failed-pages-container">';
4027 + $html .= '<h5>' . sprintf(esc_html__('Failed Pages (%d)', 'mxchat'), count($failed_pages_list)) . '</h5>';
4028 + $html .= '<details>';
4029 + $html .= '<summary>' . esc_html__('Show Failed Pages', 'mxchat') . '</summary>';
4030 + $html .= '<div class="mxchat-failed-pages-list">';
4031 +
4032 + // Create table for failed pages
4033 + $html .= '<table class="widefat striped">';
4034 + $html .= '<thead><tr>';
4035 + $html .= '<th>' . esc_html__('Page', 'mxchat') . '</th>';
4036 + $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
4037 + $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
4038 + $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
4039 + $html .= '</tr></thead><tbody>';
4040 +
4041 + // Sort failed pages by most recent
4042 + $sorted_failed_pages = $failed_pages_list;
4043 + usort($sorted_failed_pages, function($a, $b) {
4044 + return ($b['time'] ?? 0) - ($a['time'] ?? 0);
4045 + });
4046 +
4047 + foreach ($sorted_failed_pages as $item) {
4048 + // Ensure $item is an array before accessing its elements
4049 + if (!is_array($item)) {
4050 + continue;
4051 + }
4052 +
4053 + $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
4054 + $html .= '<tr>';
4055 + $html .= '<td>' . esc_html__('Page', 'mxchat') . ' ' . esc_html($item['page'] ?? 'Unknown') . '</td>';
4056 + $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
4057 + $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
4058 + $html .= '<td>' . esc_html($time_ago) . '</td>';
4059 + $html .= '</tr>';
4060 + }
4061 +
4062 + $html .= '</tbody></table>';
4063 + $html .= '</div></details></div></div>';
4064 +
4065 + return $html;
4066 +}
4067 +
4068 +/**
4069 + * Render failed URLs list
4070 + */
4071 +private function mxchat_render_failed_urls_list($failed_urls_list) {
4072 + // Validate that $failed_urls_list is an array and not empty
4073 + if (!is_array($failed_urls_list) || empty($failed_urls_list)) {
4074 + return '';
4075 + }
4076 +
4077 + $html = '<div class="mxchat-failed-urls-container">';
4078 + $html .= '<h5>' . sprintf(esc_html__('Failed URLs (%d)', 'mxchat'), count($failed_urls_list)) . '</h5>';
4079 + $html .= '<details>';
4080 + $html .= '<summary>' . esc_html__('Show Failed URLs', 'mxchat') . '</summary>';
4081 + $html .= '<div class="mxchat-failed-urls-list">';
4082 +
4083 + // Create table for failed URLs
4084 + $html .= '<table class="widefat striped">';
4085 + $html .= '<thead><tr>';
4086 + $html .= '<th>' . esc_html__('URL', 'mxchat') . '</th>';
4087 + $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
4088 + $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
4089 + $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
4090 + $html .= '</tr></thead><tbody>';
4091 +
4092 + // Sort failed URLs by most recent
4093 + $sorted_failed_urls = $failed_urls_list;
4094 + usort($sorted_failed_urls, function($a, $b) {
4095 + return ($b['time'] ?? 0) - ($a['time'] ?? 0);
4096 + });
4097 +
4098 + // Show up to 50 failed URLs
4099 + $display_urls = array_slice($sorted_failed_urls, 0, 50);
4100 +
4101 + foreach ($display_urls as $item) {
4102 + // Ensure $item is an array before accessing its elements
4103 + if (!is_array($item)) {
4104 + continue;
4105 + }
4106 +
4107 + $url = $item['url'] ?? '';
4108 + $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
4109 +
4110 + // Truncate URL for display
4111 + $display_url = strlen($url) > 50 ? substr($url, 0, 47) . '...' : $url;
4112 +
4113 + $html .= '<tr>';
4114 + $html .= '<td style="word-break: break-all;">';
4115 + if (!empty($url)) {
4116 + $html .= '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($display_url) . '</a>';
4117 + } else {
4118 + $html .= esc_html__('Unknown URL', 'mxchat');
4119 + }
4120 + $html .= '</td>';
4121 + $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
4122 + $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
4123 + $html .= '<td>' . esc_html($time_ago) . '</td>';
4124 + $html .= '</tr>';
4125 + }
4126 +
4127 + $html .= '</tbody></table>';
4128 +
4129 + if (count($failed_urls_list) > 50) {
4130 + $html .= '<div class="mxchat-failed-urls-more">+ ' .
4131 + (count($failed_urls_list) - 50) .
4132 + ' ' . esc_html__('more failed URLs not shown', 'mxchat') . '</div>';
4133 + }
4134 +
4135 + $html .= '</div></details></div>';
4136 +
4137 + return $html;
4138 +}
4139 +
4140 +/**
4141 + * Get all ACF fields for a specific post, excluding any fields the user has disabled
4142 + */
4143 +public function mxchat_get_acf_fields_for_post($post_id) {
4144 + if (!function_exists('get_fields')) {
4145 + return array();
4146 + }
4147 +
4148 + $fields = get_fields($post_id);
4149 + if (!$fields || !is_array($fields)) {
4150 + return array();
4151 + }
4152 +
4153 + // Get excluded fields from settings
4154 + $excluded_fields = get_option('mxchat_acf_excluded_fields', array());
4155 + if (!empty($excluded_fields) && is_array($excluded_fields)) {
4156 + foreach ($excluded_fields as $excluded_field) {
4157 + if (isset($fields[$excluded_field])) {
4158 + unset($fields[$excluded_field]);
4159 + }
4160 + }
4161 + }
4162 +
4163 + return $fields;
4164 +}
4165 +
4166 +/**
4167 + * Get all registered ACF field groups and their fields for the settings UI
4168 + */
4169 +public function mxchat_get_all_acf_fields() {
4170 + if (!function_exists('acf_get_field_groups') || !function_exists('acf_get_fields')) {
4171 + return array();
4172 + }
4173 +
4174 + $all_fields = array();
4175 + $field_groups = acf_get_field_groups();
4176 +
4177 + if (!empty($field_groups)) {
4178 + foreach ($field_groups as $group) {
4179 + $group_fields = acf_get_fields($group['key']);
4180 + if (!empty($group_fields)) {
4181 + $all_fields[$group['title']] = array();
4182 + foreach ($group_fields as $field) {
4183 + $all_fields[$group['title']][] = array(
4184 + 'name' => $field['name'],
4185 + 'label' => $field['label'],
4186 + 'type' => $field['type']
4187 + );
4188 + }
4189 + }
4190 + }
4191 + }
4192 +
4193 + return $all_fields;
4194 +}
4195 +
4196 +/**
4197 + * Get whitelisted custom post meta for a given post
4198 + * This allows non-ACF meta fields (like OptionTree, theme meta boxes) to be included in embeddings
4199 + */
4200 +public function mxchat_get_whitelisted_post_meta($post_id) {
4201 + $whitelist = get_option('mxchat_custom_meta_whitelist', '');
4202 +
4203 + if (empty($whitelist)) {
4204 + return array();
4205 + }
4206 +
4207 + // Parse the whitelist - one meta key per line
4208 + $meta_keys = array_filter(array_map('trim', explode("\n", $whitelist)));
4209 +
4210 + if (empty($meta_keys)) {
4211 + return array();
4212 + }
4213 +
4214 + $result = array();
4215 +
4216 + foreach ($meta_keys as $key) {
4217 + // Skip empty keys
4218 + if (empty($key)) {
4219 + continue;
4220 + }
4221 +
4222 + $value = get_post_meta($post_id, $key, true);
4223 +
4224 + // Only include non-empty string values
4225 + if (!empty($value) && is_string($value)) {
4226 + $result[$key] = $value;
4227 + } elseif (!empty($value) && is_array($value)) {
4228 + // Handle array values by joining them
4229 + $flat_value = $this->mxchat_flatten_meta_array($value);
4230 + if (!empty($flat_value)) {
4231 + $result[$key] = $flat_value;
4232 + }
4233 + }
4234 + }
4235 +
4236 + return $result;
4237 +}
4238 +
4239 +/**
4240 + * Flatten array meta values into a readable string
4241 + */
4242 +private function mxchat_flatten_meta_array($array, $depth = 0) {
4243 + if ($depth > 3) {
4244 + return ''; // Prevent infinite recursion
4245 + }
4246 +
4247 + $parts = array();
4248 +
4249 + foreach ($array as $key => $value) {
4250 + if (is_string($value) && !empty($value)) {
4251 + $parts[] = $value;
4252 + } elseif (is_array($value)) {
4253 + $nested = $this->mxchat_flatten_meta_array($value, $depth + 1);
4254 + if (!empty($nested)) {
4255 + $parts[] = $nested;
4256 + }
4257 + }
4258 + }
4259 +
4260 + return implode(', ', $parts);
4261 +}
4262 +
4263 +/**
4264 + * Format ACF field values for content extraction
4265 + */
4266 +public function mxchat_format_acf_field_value($value, $field_name = '', $post_id = 0) {
4267 + if (empty($value)) {
4268 + return '';
4269 + }
4270 +
4271 + // Handle WP_Post objects first (THIS IS THE KEY FIX)
4272 + if ($value instanceof WP_Post) {
4273 + return $value->post_title ?: '';
4274 + }
4275 +
4276 + // Handle other WP objects
4277 + if (is_object($value)) {
4278 + if (isset($value->post_title)) {
4279 + return $value->post_title;
4280 + } elseif (isset($value->display_name)) {
4281 + return $value->display_name;
4282 + } elseif (isset($value->name)) {
4283 + return $value->name;
4284 + } elseif (method_exists($value, '__toString')) {
4285 + try {
4286 + return (string) $value;
4287 + } catch (Exception $e) {
4288 + return '';
4289 + }
4290 + }
4291 + // For any other objects, return empty string
4292 + return '';
4293 + }
4294 +
4295 + // Handle different ACF field types
4296 + if (is_array($value)) {
4297 + // Check if it's an image/file field
4298 + if (isset($value['url'])) {
4299 + // Image field - return alt text, title, or caption
4300 + if (!empty($value['alt'])) {
4301 + return $value['alt'];
4302 + } elseif (!empty($value['title'])) {
4303 + return $value['title'];
4304 + } elseif (!empty($value['caption'])) {
4305 + return $value['caption'];
4306 + } else {
4307 + return ''; // Don't include just the URL
4308 + }
4309 + }
4310 +
4311 + // Check if it's a post object or relationship field
4312 + if (isset($value['post_title'])) {
4313 + return $value['post_title'];
4314 + }
4315 +
4316 + // Check if it's a user field
4317 + if (isset($value['display_name'])) {
4318 + return $value['display_name'];
4319 + }
4320 +
4321 + // Check if it's a taxonomy term
4322 + if (isset($value['name']) && isset($value['taxonomy'])) {
4323 + return $value['name'];
4324 + }
4325 +
4326 + // Check if it's a select field with label
4327 + if (isset($value['label'])) {
4328 + return $value['label'];
4329 + }
4330 +
4331 + // Check for repeater field or flexible content
4332 + if (is_numeric(key($value))) {
4333 + $sub_values = array();
4334 + foreach ($value as $sub_item) {
4335 + if (is_array($sub_item)) {
4336 + // For repeater/flexible content, extract text values
4337 + $sub_text = $this->mxchat_extract_text_from_acf_array($sub_item);
4338 + if (!empty($sub_text)) {
4339 + $sub_values[] = $sub_text;
4340 + }
4341 + } elseif ($sub_item instanceof WP_Post) {
4342 + // Handle WP_Post objects in arrays
4343 + $sub_values[] = $sub_item->post_title ?: '';
4344 + } else {
4345 + $sub_values[] = (string) $sub_item;
4346 + }
4347 + }
4348 + return implode(', ', array_filter($sub_values));
4349 + }
4350 +
4351 + // For other arrays, try to extract meaningful text
4352 + $text_values = array();
4353 + foreach ($value as $key => $val) {
4354 + if (is_string($val) && !empty(trim($val))) {
4355 + $text_values[] = trim($val);
4356 + } elseif ($val instanceof WP_Post) {
4357 + // Handle WP_Post objects in associative arrays
4358 + $text_values[] = $val->post_title ?: '';
4359 + } elseif (is_array($val) && isset($val['post_title'])) {
4360 + $text_values[] = $val['post_title'];
4361 + } elseif (is_array($val) && isset($val['name'])) {
4362 + $text_values[] = $val['name'];
4363 + }
4364 + }
4365 +
4366 + return implode(', ', array_filter($text_values));
4367 + }
4368 +
4369 + // Handle boolean values
4370 + if (is_bool($value)) {
4371 + return $value ? 'Yes' : 'No';
4372 + }
4373 +
4374 + // Handle numeric values
4375 + if (is_numeric($value)) {
4376 + return (string) $value;
4377 + }
4378 +
4379 + // Handle string values
4380 + if (is_string($value)) {
4381 + return trim($value);
4382 + }
4383 +
4384 + // For anything else that we can't handle, return empty string
4385 + // This prevents the "Object could not be converted to string" error
4386 + return '';
4387 +}
4388 +
4389 +/**
4390 + * Extract text from complex ACF array structures
4391 + */
4392 +private function mxchat_extract_text_from_acf_array($array) {
4393 + if (!is_array($array)) {
4394 + return '';
4395 + }
4396 +
4397 + $text_parts = array();
4398 +
4399 + foreach ($array as $key => $value) {
4400 + if (is_string($value) && !empty(trim($value))) {
4401 + // Skip keys that are likely to be IDs or technical values
4402 + if (!is_numeric($value) || strlen($value) > 10) {
4403 + $text_parts[] = trim($value);
4404 + }
4405 + } elseif ($value instanceof WP_Post) {
4406 + // Handle WP_Post objects
4407 + $text_parts[] = $value->post_title ?: '';
4408 + } elseif (is_array($value)) {
4409 + if (isset($value['post_title'])) {
4410 + $text_parts[] = $value['post_title'];
4411 + } elseif (isset($value['name'])) {
4412 + $text_parts[] = $value['name'];
4413 + } elseif (isset($value['label'])) {
4414 + $text_parts[] = $value['label'];
4415 + }
4416 + } elseif (is_object($value)) {
4417 + // Handle other objects safely
4418 + if (isset($value->post_title)) {
4419 + $text_parts[] = $value->post_title;
4420 + } elseif (isset($value->name)) {
4421 + $text_parts[] = $value->name;
4422 + } elseif (isset($value->display_name)) {
4423 + $text_parts[] = $value->display_name;
4424 + }
4425 + }
4426 + }
4427 +
4428 + return implode(', ', array_filter($text_parts));
4429 +}
4430 +
4431 +/**
4432 + * Handle ACF save - fires after ACF fields are saved
4433 + * This ensures ACF field data is available when syncing to knowledge base
4434 + */
4435 +public function mxchat_handle_acf_save($post_id) {
4436 + // Skip if not a valid post
4437 + if (!$post_id || $post_id === 'options') {
4438 + return;
4439 + }
4440 +
4441 + // Skip autosaves and revisions
4442 + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
4443 + return;
4444 + }
4445 +
4446 + $post = get_post($post_id);
4447 + if (!$post) {
4448 + return;
4449 + }
4450 +
4451 + $post_type = $post->post_type;
4452 +
4453 + // Check if sync is enabled for this post type
4454 + $should_sync = false;
4455 +
4456 + if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
4457 + $should_sync = true;
4458 + } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
4459 + $should_sync = true;
4460 + } else if ($post_type === 'product' && class_exists('WooCommerce')) {
4461 + // WooCommerce products - check if WooCommerce integration is enabled
4462 + $options = get_option('mxchat_options', array());
4463 + if (isset($options['enable_woocommerce_integration']) &&
4464 + ($options['enable_woocommerce_integration'] === '1' || $options['enable_woocommerce_integration'] === 'on')) {
4465 + $should_sync = true;
4466 + }
4467 + } else {
4468 + // Check custom post types
4469 + $option_name = 'mxchat_auto_sync_' . $post_type;
4470 + if (get_option($option_name) === '1') {
4471 + $should_sync = true;
4472 + }
4473 + }
4474 +
4475 + if (!$should_sync) {
4476 + return;
4477 + }
4478 +
4479 + // Only process published posts
4480 + if ($post->post_status !== 'publish') {
4481 + return;
4482 + }
4483 +
4484 + // Check if this post has any ACF fields - if not, no need to re-sync
4485 + $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
4486 + if (empty($acf_fields)) {
4487 + return;
4488 + }
4489 +
4490 + // Use a transient to prevent duplicate processing (post_updated may have already run)
4491 + $transient_key = 'mxchat_acf_synced_' . $post_id;
4492 + if (get_transient($transient_key)) {
4493 + return;
4494 + }
4495 + set_transient($transient_key, true, 60); // Prevent re-processing for 60 seconds
4496 +
4497 + // Re-run the sync with ACF data now available
4498 + // We pass $update=true since this is effectively an update with ACF data
4499 + $this->mxchat_handle_post_update($post_id, $post, true);
4500 +}
4501 +
4502 +public function mxchat_handle_post_update($post_id, $post, $update) {
4503 + // Basic validation checks
4504 + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
4505 + return;
4506 + }
4507 +
4508 + $post_type = $post->post_type;
4509 +
4510 + // Check if sync is enabled for this post type
4511 + $should_sync = false;
4512 +
4513 + // Check built-in post types first
4514 + if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
4515 + $should_sync = true;
4516 + } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
4517 + $should_sync = true;
4518 + } else {
4519 + // Check custom post types
4520 + $option_name = 'mxchat_auto_sync_' . $post_type;
4521 + if (get_option($option_name) === '1') {
4522 + $should_sync = true;
4523 + }
4524 + }
4525 +
4526 + if (!$should_sync) {
4527 + return;
4528 + }
4529 +
4530 + // Check if we have stored the previous status and URL in our transients
4531 + $previous_status_key = 'mxchat_prev_status_' . $post_id;
4532 + $previous_status = get_transient($previous_status_key);
4533 +
4534 + $previous_url_key = 'mxchat_prev_url_' . $post_id;
4535 + $previous_url = get_transient($previous_url_key);
4536 +
4537 + // If the post was previously published but is now not published, remove from knowledge base
4538 + if ($previous_status === 'publish' && $post->post_status !== 'publish') {
4539 + // Use the stored URL from when it was published, or fall back to current permalink
4540 + $source_url = $previous_url ?: get_permalink($post_id);
4541 +
4542 + if ($source_url) {
4543 + // Check if Pinecone is enabled
4544 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4545 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4546 +
4547 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
4548 + // Delete from Pinecone
4549 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
4550 + } else {
4551 + // Delete from WordPress DB
4552 + global $wpdb;
4553 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4554 +
4555 + $result = $wpdb->delete(
4556 + $table_name,
4557 + array('source_url' => $source_url),
4558 + array('%s')
4559 + );
4560 + }
4561 + }
4562 +
4563 + // Clean up the transients and exit early
4564 + delete_transient($previous_status_key);
4565 + delete_transient($previous_url_key);
4566 + return;
4567 + }
4568 +
4569 + // Store the current status for next time (if this is an update)
4570 + if ($update) {
4571 + set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
4572 +
4573 + // If the post is currently published, also store its URL
4574 + if ($post->post_status === 'publish') {
4575 + $current_url = get_permalink($post_id);
4576 + set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
4577 + }
4578 + }
4579 +
4580 + // Only process currently published content for adding/updating
4581 + if ($post->post_status === 'publish') {
4582 + // Get the source URL
4583 + $source_url = get_permalink($post_id);
4584 +
4585 + // Get content with proper formatting (matching ajax_mxchat_process_selected_content)
4586 + $title = get_the_title($post_id);
4587 + $content = get_post_field('post_content', $post_id);
4588 + $excerpt = get_post_field('post_excerpt', $post_id);
4589 +
4590 + // Remove shortcode tags but preserve content inside them
4591 + $content = $this->strip_shortcode_tags_preserve_content($content);
4592 + $excerpt = $this->strip_shortcode_tags_preserve_content($excerpt);
4593 +
4594 + // Strip tags but preserve structure (don't use 'the_content' filter as it may re-add shortcodes)
4595 + $content = wp_strip_all_tags($content);
4596 +
4597 + // Combine title, short description (if exists), and content
4598 + $final_content = $title . "\n\n";
4599 +
4600 + // Add short description if it exists (WooCommerce products use post_excerpt for short description)
4601 + if (!empty($excerpt)) {
4602 + $final_content .= "Short Description: " . wp_strip_all_tags($excerpt) . "\n\n";
4603 + }
4604 +
4605 + $final_content .= $content;
4606 +
4607 + // For WooCommerce products, include pricing and product details
4608 + if ($post_type === 'product' && class_exists('WooCommerce')) {
4609 + $product = wc_get_product($post_id);
4610 +
4611 + if ($product) {
4612 + // Get pricing information
4613 + $regular_price = $product->get_regular_price();
4614 + $sale_price = $product->get_sale_price();
4615 + $price = $product->get_price();
4616 + $sku = $product->get_sku();
4617 +
4618 + // Get currency symbol
4619 + $currency_symbol = get_woocommerce_currency_symbol();
4620 +
4621 + // Add pricing information
4622 + $final_content .= "\n";
4623 + if (!empty($regular_price)) {
4624 + $final_content .= "Price: " . $currency_symbol . $regular_price . "\n";
4625 + } elseif (!empty($price)) {
4626 + $final_content .= "Price: " . $currency_symbol . $price . "\n";
4627 + }
4628 +
4629 + if (!empty($sale_price) && $sale_price !== $regular_price) {
4630 + $final_content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
4631 + }
4632 +
4633 + // Handle variable products - show price range
4634 + if ($product->is_type('variable')) {
4635 + $min_price = $product->get_variation_price('min');
4636 + $max_price = $product->get_variation_price('max');
4637 + if ($min_price !== $max_price) {
4638 + $final_content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
4639 + }
4640 + }
4641 +
4642 + if (!empty($sku)) {
4643 + $final_content .= "SKU: " . $sku . "\n";
4644 + }
4645 +
4646 + // Get product categories
4647 + $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
4648 + if (!empty($categories) && !is_wp_error($categories)) {
4649 + $final_content .= "Categories: " . implode(', ', $categories) . "\n";
4650 + }
4651 + }
4652 + }
4653 +
4654 + // For custom post types like job_listing, include additional fields
4655 + if ($post_type === 'job_listing') {
4656 + // Add job-specific meta if available
4657 + $job_location = get_post_meta($post_id, '_job_location', true);
4658 + if (!empty($job_location)) {
4659 + $final_content .= "\n\nLocation: " . $job_location;
4660 + }
4661 +
4662 + // Get job type terms
4663 + $job_types = get_the_terms($post_id, 'job_listing_type');
4664 + if (!empty($job_types) && !is_wp_error($job_types)) {
4665 + $types = array();
4666 + foreach ($job_types as $type) {
4667 + $types[] = $type->name;
4668 + }
4669 + $final_content .= "\n\nJob Type: " . implode(', ', $types);
4670 + }
4671 +
4672 + // Get company name if available
4673 + $company_name = get_post_meta($post_id, '_company_name', true);
4674 + if (!empty($company_name)) {
4675 + $final_content .= "\n\nCompany: " . $company_name;
4676 + }
4677 + }
4678 +
4679 + // ADD ACF FIELDS SUPPORT (matches ajax_mxchat_process_selected_content behavior)
4680 + $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
4681 + if (!empty($acf_fields)) {
4682 + $acf_content_parts = array();
4683 +
4684 + foreach ($acf_fields as $field_name => $field_value) {
4685 + $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
4686 + if (!empty($formatted_value)) {
4687 + // Convert field name to readable label
4688 + $field_label = ucwords(str_replace(['_', '-'], ' ', $field_name));
4689 + $acf_content_parts[] = $field_label . ": " . $formatted_value;
4690 + }
4691 + }
4692 +
4693 + if (!empty($acf_content_parts)) {
4694 + $final_content .= "\n\n" . implode("\n", $acf_content_parts);
4695 + }
4696 + }
4697 +
4698 + // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
4699 + $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
4700 + if (!empty($custom_meta)) {
4701 + $meta_content_parts = array();
4702 +
4703 + foreach ($custom_meta as $meta_key => $meta_value) {
4704 + // Convert meta key to readable label
4705 + $meta_label = ucwords(str_replace(array('_', '-'), ' ', $meta_key));
4706 + $meta_content_parts[] = $meta_label . ": " . $meta_value;
4707 + }
4708 +
4709 + if (!empty($meta_content_parts)) {
4710 + $final_content .= "\n\n" . implode("\n", $meta_content_parts);
4711 + }
4712 + }
4713 +
4714 + // Get API key with proper model detection
4715 + $options = get_option('mxchat_options');
4716 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4717 +
4718 + if (strpos($selected_model, 'voyage') === 0) {
4719 + $api_key = $options['voyage_api_key'] ?? '';
4720 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
4721 + $api_key = $options['gemini_api_key'] ?? '';
4722 + } else {
4723 + $api_key = $options['api_key'] ?? '';
4724 + }
4725 +
4726 + if (empty($api_key)) {
4727 + return;
4728 + }
4729 +
4730 + // Use the centralized utility function for storage
4731 + $result = MxChat_Utils::submit_content_to_db(
4732 + $final_content,
4733 + $source_url,
4734 + $api_key,
4735 + md5($source_url) // Vector ID for Pinecone
4736 + );
4737 +
4738 + // After successful storage, apply role restriction based on tags
4739 + if (!is_wp_error($result)) {
4740 + $this->apply_role_restriction_to_post($post_id, $source_url);
4741 + }
4742 + }
4743 +
4744 + // Clean up the stored previous status if not used above
4745 + if ($previous_status !== 'publish' || $post->post_status === 'publish') {
4746 + delete_transient($previous_status_key);
4747 + delete_transient($previous_url_key);
4748 + }
4749 +}
4750 +
4751 +/**
4752 + * Store the post status and URL before update to detect status transitions
4753 + * This runs before the post is actually updated in the database
4754 + */
4755 +public function mxchat_store_pre_update_status($post_id, $data) {
4756 + // Get the current post from database (before update)
4757 + $current_post = get_post($post_id);
4758 +
4759 + if ($current_post) {
4760 + // Store the current status temporarily
4761 + $status_key = 'mxchat_prev_status_' . $post_id;
4762 + set_transient($status_key, $current_post->post_status, HOUR_IN_SECONDS);
4763 +
4764 + // If the post is currently published, also store its URL
4765 + if ($current_post->post_status === 'publish') {
4766 + $url_key = 'mxchat_prev_url_' . $post_id;
4767 + $current_url = get_permalink($post_id);
4768 + set_transient($url_key, $current_url, HOUR_IN_SECONDS);
4769 + }
4770 + }
4771 +}
4772 +
4773 +public function mxchat_handle_post_delete($post_id) {
4774 + // Get post data before it's deleted
4775 + $post = get_post($post_id);
4776 +
4777 + // Basic validation
4778 + if (!$post || wp_is_post_revision($post_id)) {
4779 + return;
4780 + }
4781 +
4782 + $post_type = $post->post_type;
4783 +
4784 + // Check if sync is enabled for this post type
4785 + $should_sync = false;
4786 +
4787 + // Check built-in post types first
4788 + if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
4789 + $should_sync = true;
4790 + } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
4791 + $should_sync = true;
4792 + } else {
4793 + // Check custom post types
4794 + $option_name = 'mxchat_auto_sync_' . $post_type;
4795 + if (get_option($option_name) === '1') {
4796 + $should_sync = true;
4797 + }
4798 + }
4799 +
4800 + if (!$should_sync) {
4801 + return;
4802 + }
4803 +
4804 + // Get the URL before post is deleted
4805 + $source_url = get_permalink($post_id);
4806 + if (!$source_url) {
4807 + //error_log('MXChat: Failed to get permalink for post ' . $post_id);
4808 + return;
4809 + }
4810 +
4811 + // Use chunk-aware deletion (handles both chunked and non-chunked content)
4812 + $delete_result = MxChat_Utils::delete_chunks_for_url($source_url, 'default');
4813 +
4814 + if (is_wp_error($delete_result)) {
4815 + //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message());
4816 + }
4817 +}
4818 +
4819 +
4820 + /**
4821 + * Deletes data from Pinecone using a source URL
4822 + */
4823 + public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
4824 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
4825 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
4826 +
4827 + if (empty($host) || empty($api_key)) {
4828 + //error_log('MXChat: Pinecone deletion failed - missing configuration');
4829 + return false;
4830 + }
4831 +
4832 + $api_endpoint = "https://{$host}/vectors/delete";
4833 + $vector_id = md5($source_url);
4834 +
4835 + $request_body = array(
4836 + 'ids' => array($vector_id)
4837 + );
4838 +
4839 + $response = wp_remote_post($api_endpoint, array(
4840 + 'headers' => array(
4841 + 'Api-Key' => $api_key,
4842 + 'accept' => 'application/json',
4843 + 'content-type' => 'application/json'
4844 + ),
4845 + 'body' => wp_json_encode($request_body),
4846 + 'timeout' => 30
4847 + ));
4848 +
4849 + if (is_wp_error($response)) {
4850 + //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
4851 + return false;
4852 + }
4853 +
4854 + $response_code = wp_remote_retrieve_response_code($response);
4855 + if ($response_code !== 200) {
4856 + //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
4857 + return false;
4858 + }
4859 +
4860 + return true;
4861 + }
4862 +
4863 +
4864 +
4865 +public function mxchat_handle_product_change($post_id, $post, $update) {
4866 + if ($post->post_type !== 'product') {
4867 + return;
4868 + }
4869 +
4870 + if ($post->post_status === 'publish') {
4871 + add_action('shutdown', function() use ($post_id) {
4872 + $product = wc_get_product($post_id);
4873 + if ($product) {
4874 + $this->mxchat_store_product_embedding($product);
4875 + }
4876 + });
4877 + }
4878 +}
4879 +
4880 +/**
4881 + * Store WooCommerce product embeddings
4882 + */
4883 +private function mxchat_store_product_embedding($product) {
4884 + if (!isset($this->options['enable_woocommerce_integration']) ||
4885 + !in_array($this->options['enable_woocommerce_integration'], ['1', 'on'])) {
4886 + return;
4887 + }
4888 +
4889 + $source_url = get_permalink($product->get_id());
4890 + $product_id = $product->get_id();
4891 +
4892 + // Build product content
4893 + $title = $product->get_name();
4894 + $description = $product->get_description();
4895 + $short_description = $product->get_short_description();
4896 + $regular_price = $product->get_regular_price();
4897 + $sale_price = $product->get_sale_price();
4898 + $price = $product->get_price();
4899 + $sku = $product->get_sku();
4900 +
4901 + // Get currency symbol
4902 + $currency_symbol = get_woocommerce_currency_symbol();
4903 +
4904 + // Format content consistently
4905 + $content = $title . "\n\n";
4906 +
4907 + if (!empty($short_description)) {
4908 + $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
4909 + }
4910 +
4911 + if (!empty($description)) {
4912 + $content .= wp_strip_all_tags($description) . "\n\n";
4913 + }
4914 +
4915 + // Add pricing information
4916 + if (!empty($regular_price)) {
4917 + $content .= "Price: " . $currency_symbol . $regular_price . "\n";
4918 + } elseif (!empty($price)) {
4919 + $content .= "Price: " . $currency_symbol . $price . "\n";
4920 + }
4921 +
4922 + if (!empty($sale_price) && $sale_price !== $regular_price) {
4923 + $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
4924 + }
4925 +
4926 + // Handle variable products - show price range
4927 + if ($product->is_type('variable')) {
4928 + $min_price = $product->get_variation_price('min');
4929 + $max_price = $product->get_variation_price('max');
4930 + if ($min_price !== $max_price) {
4931 + $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
4932 + }
4933 + }
4934 +
4935 + if (!empty($sku)) {
4936 + $content .= "SKU: " . $sku . "\n";
4937 + }
4938 +
4939 + // Get product categories
4940 + $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
4941 + if (!empty($categories) && !is_wp_error($categories)) {
4942 + $content .= "Categories: " . implode(', ', $categories) . "\n";
4943 + }
4944 +
4945 + // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
4946 + $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
4947 + if (!empty($custom_tabs) && is_array($custom_tabs)) {
4948 + foreach ($custom_tabs as $tab) {
4949 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
4950 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
4951 +
4952 + if (!empty($tab_title) && !empty($tab_content)) {
4953 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
4954 + }
4955 + }
4956 + }
4957 +
4958 + // Also check for reusable/saved tabs applied to this product
4959 + $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
4960 + if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
4961 + // Get the saved tabs option
4962 + $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
4963 + if (!empty($saved_tabs) && is_array($saved_tabs)) {
4964 + foreach ($applied_saved_tabs as $saved_tab_id) {
4965 + if (isset($saved_tabs[$saved_tab_id])) {
4966 + $tab = $saved_tabs[$saved_tab_id];
4967 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
4968 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
4969 +
4970 + if (!empty($tab_title) && !empty($tab_content)) {
4971 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
4972 + }
4973 + }
4974 + }
4975 + }
4976 + }
4977 +
4978 + // Get API key with proper model detection
4979 + $options = get_option('mxchat_options');
4980 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4981 +
4982 + if (strpos($selected_model, 'voyage') === 0) {
4983 + $api_key = $options['voyage_api_key'] ?? '';
4984 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
4985 + $api_key = $options['gemini_api_key'] ?? '';
4986 + } else {
4987 + $api_key = $options['api_key'] ?? '';
4988 + }
4989 +
4990 + if (empty($api_key)) {
4991 + //error_log('MxChat Auto-sync: No API key configured for embedding model');
4992 + return;
4993 + }
4994 +
4995 + // Use the centralized utility function for storage
4996 + $result = MxChat_Utils::submit_content_to_db(
4997 + $content,
4998 + $source_url,
4999 + $api_key,
5000 + md5($source_url) // Vector ID for Pinecone
5001 + );
5002 +
5003 + // After successful storage, apply role restriction based on tags
5004 + if (!is_wp_error($result)) {
5005 + $this->apply_role_restriction_to_post($product_id, $source_url);
5006 + }
5007 +
5008 + if (is_wp_error($result)) {
5009 + //error_log('MxChat WooCommerce sync failed for product ' . $product_id . ': ' . $result->get_error_message());
5010 + }
5011 +}
5012 +
5013 +public function mxchat_handle_product_delete($post_id) {
5014 + if (get_post_type($post_id) !== 'product') {
5015 + return;
5016 + }
5017 +
5018 + $source_url = get_permalink($post_id);
5019 +
5020 + // Check if Pinecone is enabled
5021 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5022 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5023 +
5024 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
5025 + // Delete from Pinecone
5026 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
5027 + } else {
5028 + // Delete from WordPress DB
5029 + global $wpdb;
5030 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5031 +
5032 + $wpdb->delete(
5033 + $table_name,
5034 + array('source_url' => $source_url),
5035 + array('%s')
5036 + );
5037 + }
5038 +}
5039 +
5040 +/**
5041 + * Handle individual Pinecone content deletion
5042 + */
5043 +public function mxchat_handle_pinecone_prompt_delete() {
5044 + // Check permissions
5045 + if (!current_user_can('manage_options')) {
5046 + wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
5047 + }
5048 +
5049 + // Verify nonce
5050 + if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'mxchat_delete_pinecone_prompt_nonce')) {
5051 + wp_die(esc_html__('Security check failed.', 'mxchat'));
5052 + }
5053 +
5054 + $vector_id = isset($_GET['vector_id']) ? sanitize_text_field($_GET['vector_id']) : '';
5055 +
5056 + if (empty($vector_id)) {
5057 + set_transient('mxchat_admin_notice_error',
5058 + esc_html__('Invalid vector ID.', 'mxchat'),
5059 + 30
5060 + );
5061 + wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5062 + exit;
5063 + }
5064 +
5065 + // Get Pinecone settings
5066 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5067 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5068 +
5069 + if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5070 + set_transient('mxchat_admin_notice_error',
5071 + esc_html__('Pinecone is not properly configured.', 'mxchat'),
5072 + 30
5073 + );
5074 + wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5075 + exit;
5076 + }
5077 +
5078 + // Delete from Pinecone
5079 + $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5080 + $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
5081 + $vector_id,
5082 + $pinecone_options['mxchat_pinecone_api_key'],
5083 + $pinecone_options['mxchat_pinecone_host']
5084 + );
5085 +
5086 + if ($result['success']) {
5087 + // No cache clearing needed since we removed caching
5088 + set_transient('mxchat_admin_notice_success',
5089 + esc_html__('Entry deleted successfully from Pinecone.', 'mxchat'),
5090 + 30
5091 + );
5092 + } else {
5093 + set_transient('mxchat_admin_notice_error',
5094 + esc_html__('Failed to delete entry: ', 'mxchat') . $result['message'],
5095 + 30
5096 + );
5097 + }
5098 +
5099 + wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5100 + exit;
5101 +}
5102 +/**
5103 + * Handle individual Pinecone content deletion via AJAX
5104 + */
5105 +public function ajax_mxchat_delete_pinecone_prompt() {
5106 + // Verify nonce and permissions
5107 + if (!check_ajax_referer('mxchat_delete_pinecone_prompt_nonce', 'nonce', false)) {
5108 + wp_send_json_error('Invalid nonce');
5109 + exit;
5110 + }
5111 +
5112 + if (!current_user_can('manage_options')) {
5113 + wp_send_json_error('Unauthorized access');
5114 + exit;
5115 + }
5116 +
5117 + $vector_id = isset($_POST['vector_id']) ? sanitize_text_field($_POST['vector_id']) : '';
5118 + $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
5119 +
5120 + if (empty($vector_id)) {
5121 + wp_send_json_error('Missing vector ID');
5122 + exit;
5123 + }
5124 +
5125 + // Get bot-specific Pinecone settings
5126 + $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5127 + $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
5128 +
5129 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5130 +
5131 + if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5132 + wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
5133 + exit;
5134 + }
5135 +
5136 + // Delete from the correct Pinecone index
5137 + $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
5138 + $vector_id,
5139 + $pinecone_options['mxchat_pinecone_api_key'],
5140 + $pinecone_options['mxchat_pinecone_host']
5141 + );
5142 +
5143 + if ($result['success']) {
5144 + // No cache clearing needed since we removed caching
5145 + wp_send_json_success(array(
5146 + 'message' => 'Entry deleted successfully from Pinecone',
5147 + 'vector_id' => $vector_id,
5148 + 'bot_id' => $bot_id
5149 + ));
5150 + } else {
5151 + wp_send_json_error('Failed to delete from Pinecone: ' . $result['message']);
5152 + }
5153 +
5154 + exit;
5155 +}
5156 +
5157 +/**
5158 + * Handle deletion of all chunks for a given source URL via AJAX
5159 + * Follows the same pattern as ajax_mxchat_delete_pinecone_prompt
5160 + */
5161 +public function ajax_mxchat_delete_chunks_by_url() {
5162 + // Verify nonce and permissions
5163 + if (!check_ajax_referer('mxchat_delete_chunks_nonce', 'nonce', false)) {
5164 + wp_send_json_error('Invalid nonce');
5165 + exit;
5166 + }
5167 +
5168 + if (!current_user_can('manage_options')) {
5169 + wp_send_json_error('Unauthorized access');
5170 + exit;
5171 + }
5172 +
5173 + $source_url = isset($_POST['source_url']) ? esc_url_raw($_POST['source_url']) : '';
5174 + $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
5175 + $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
5176 +
5177 + if (empty($source_url)) {
5178 + wp_send_json_error('Missing source URL');
5179 + exit;
5180 + }
5181 +
5182 + // Generate the base vector ID from the source URL (same as how chunks are created)
5183 + $base_vector_id = md5($source_url);
5184 +
5185 + if ($data_source === 'pinecone') {
5186 + // Get bot-specific Pinecone settings (same as working delete function)
5187 + $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5188 + $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
5189 +
5190 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5191 +
5192 + if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5193 + wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
5194 + exit;
5195 + }
5196 +
5197 + $api_key = $pinecone_options['mxchat_pinecone_api_key'];
5198 + $host = $pinecone_options['mxchat_pinecone_host'];
5199 + $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
5200 +
5201 + // Collect all vector IDs to delete
5202 + $vectors_to_delete = array();
5203 +
5204 + // Add the original single-vector ID (for non-chunked content)
5205 + $vectors_to_delete[] = $base_vector_id;
5206 +
5207 + // Use Pinecone list API to find all chunk vectors with this prefix
5208 + // NOTE: Pinecone List API is a GET request with query parameters, not POST
5209 + $prefix = $base_vector_id . '_chunk_';
5210 +
5211 + $query_params = array(
5212 + 'prefix' => $prefix,
5213 + 'limit' => 100
5214 + );
5215 +
5216 + if (!empty($namespace)) {
5217 + $query_params['namespace'] = $namespace;
5218 + }
5219 +
5220 + $list_url = "https://{$host}/vectors/list?" . http_build_query($query_params);
5221 +
5222 + $list_response = wp_remote_get($list_url, array(
5223 + 'headers' => array(
5224 + 'Api-Key' => $api_key,
5225 + 'accept' => 'application/json'
5226 + ),
5227 + 'timeout' => 30
5228 + ));
5229 +
5230 + if (!is_wp_error($list_response)) {
5231 + $list_body_response = wp_remote_retrieve_body($list_response);
5232 + $list_data = json_decode($list_body_response, true);
5233 + if (!empty($list_data['vectors'])) {
5234 + foreach ($list_data['vectors'] as $vector) {
5235 + if (isset($vector['id'])) {
5236 + $vectors_to_delete[] = $vector['id'];
5237 + }
5238 + }
5239 + }
5240 + }
5241 +
5242 + if (empty($vectors_to_delete)) {
5243 + wp_send_json_success(array(
5244 + 'message' => 'No vectors found to delete',
5245 + 'source_url' => $source_url
5246 + ));
5247 + exit;
5248 + }
5249 +
5250 + // Delete all vectors using the same endpoint as the working function
5251 + $delete_url = "https://{$host}/vectors/delete";
5252 +
5253 + $delete_body = array(
5254 + 'ids' => $vectors_to_delete
5255 + );
5256 +
5257 + if (!empty($namespace)) {
5258 + $delete_body['namespace'] = $namespace;
5259 + }
5260 +
5261 + $delete_response = wp_remote_post($delete_url, array(
5262 + 'headers' => array(
5263 + 'Api-Key' => $api_key,
5264 + 'accept' => 'application/json',
5265 + 'content-type' => 'application/json'
5266 + ),
5267 + 'body' => wp_json_encode($delete_body),
5268 + 'timeout' => 30
5269 + ));
5270 +
5271 + if (is_wp_error($delete_response)) {
5272 + wp_send_json_error('Failed to delete from Pinecone: ' . $delete_response->get_error_message());
5273 + exit;
5274 + }
5275 +
5276 + $response_code = wp_remote_retrieve_response_code($delete_response);
5277 +
5278 + if ($response_code !== 200) {
5279 + wp_send_json_error('Pinecone API error (HTTP ' . $response_code . ')');
5280 + exit;
5281 + }
5282 +
5283 + wp_send_json_success(array(
5284 + 'message' => 'All chunks deleted successfully from Pinecone',
5285 + 'source_url' => $source_url,
5286 + 'deleted_count' => count($vectors_to_delete)
5287 + ));
5288 +
5289 + } else {
5290 + // WordPress database deletion
5291 + global $wpdb;
5292 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5293 +
5294 + $result = $wpdb->delete(
5295 + $table_name,
5296 + array('source_url' => $source_url),
5297 + array('%s')
5298 + );
5299 +
5300 + if ($result === false) {
5301 + wp_send_json_error('Failed to delete from database: ' . $wpdb->last_error);
5302 + exit;
5303 + }
5304 +
5305 + wp_send_json_success(array(
5306 + 'message' => 'All chunks deleted successfully from database',
5307 + 'source_url' => $source_url,
5308 + 'deleted_count' => $result
5309 + ));
5310 + }
5311 +
5312 + exit;
5313 +}
5314 +
5315 +/**
5316 + * Handle individual WordPress database content deletion via AJAX
5317 + * Mirrors the Pinecone delete handler but for WordPress database entries
5318 + */
5319 +public function ajax_mxchat_delete_wordpress_prompt() {
5320 + // Verify nonce and permissions
5321 + if (!check_ajax_referer('mxchat_delete_wordpress_prompt_nonce', 'nonce', false)) {
5322 + wp_send_json_error('Invalid nonce');
5323 + exit;
5324 + }
5325 +
5326 + if (!current_user_can('manage_options')) {
5327 + wp_send_json_error('Unauthorized access');
5328 + exit;
5329 + }
5330 +
5331 + $entry_id = isset($_POST['entry_id']) ? intval($_POST['entry_id']) : 0;
5332 +
5333 + if (empty($entry_id)) {
5334 + wp_send_json_error('Missing entry ID');
5335 + exit;
5336 + }
5337 +
5338 + global $wpdb;
5339 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5340 +
5341 + // Clear cache for this entry
5342 + wp_cache_delete('prompt_' . $entry_id, 'mxchat_prompts');
5343 +
5344 + // Delete from database
5345 + $result = $wpdb->delete(
5346 + $table_name,
5347 + array('id' => $entry_id),
5348 + array('%d')
5349 + );
5350 +
5351 + if ($result !== false) {
5352 + wp_send_json_success(array(
5353 + 'message' => 'Entry deleted successfully',
5354 + 'entry_id' => $entry_id
5355 + ));
5356 + } else {
5357 + wp_send_json_error('Failed to delete entry from database');
5358 + }
5359 +
5360 + exit;
5361 +}
5362 +
5363 +/**
5364 + * Handle bulk deletion of knowledge entries via AJAX
5365 + * Supports both Pinecone and WordPress database entries
5366 + */
5367 +public function ajax_mxchat_bulk_delete_knowledge() {
5368 + // Verify nonce and permissions
5369 + if (!check_ajax_referer('mxchat_bulk_delete_knowledge_nonce', 'nonce', false)) {
5370 + wp_send_json_error('Invalid nonce');
5371 + exit;
5372 + }
5373 +
5374 + if (!current_user_can('manage_options')) {
5375 + wp_send_json_error('Unauthorized access');
5376 + exit;
5377 + }
5378 +
5379 + $entries = isset($_POST['entries']) ? $_POST['entries'] : array();
5380 + $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
5381 +
5382 + if (empty($entries) || !is_array($entries)) {
5383 + wp_send_json_error('No entries provided');
5384 + exit;
5385 + }
5386 +
5387 + $success_ids = array();
5388 + $failed_ids = array();
5389 + $errors = array();
5390 +
5391 + global $wpdb;
5392 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5393 +
5394 + // Get Pinecone manager for Pinecone deletions
5395 + $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5396 + $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
5397 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5398 +
5399 + foreach ($entries as $entry) {
5400 + $entry_id = sanitize_text_field($entry['id'] ?? '');
5401 + $source = sanitize_text_field($entry['source'] ?? 'wordpress');
5402 + $source_url = isset($entry['sourceUrl']) ? esc_url_raw($entry['sourceUrl']) : '';
5403 + $is_group = isset($entry['isGroup']) && ($entry['isGroup'] === true || $entry['isGroup'] === 'true');
5404 +
5405 + if (empty($entry_id)) {
5406 + continue;
5407 + }
5408 +
5409 + try {
5410 + if ($source === 'pinecone') {
5411 + // Handle Pinecone deletion
5412 + if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5413 + $failed_ids[] = $entry_id;
5414 + $errors[] = "Pinecone not configured for entry: $entry_id";
5415 + continue;
5416 + }
5417 +
5418 + if ($is_group && !empty($source_url)) {
5419 + // Delete all chunks for this URL
5420 + $base_vector_id = md5($source_url);
5421 + $api_key = $pinecone_options['mxchat_pinecone_api_key'];
5422 + $host = $pinecone_options['mxchat_pinecone_host'];
5423 +
5424 + // List all vectors with this prefix
5425 + $list_url = 'https://' . $host . '/vectors/list?prefix=' . urlencode($base_vector_id . '_chunk_') . '&limit=100';
5426 + $list_response = wp_remote_get($list_url, array(
5427 + 'headers' => array(
5428 + 'Api-Key' => $api_key,
5429 + 'Content-Type' => 'application/json'
5430 + ),
5431 + 'timeout' => 30
5432 + ));
5433 +
5434 + $vector_ids = array($base_vector_id); // Include base ID
5435 +
5436 + if (!is_wp_error($list_response)) {
5437 + $list_body = json_decode(wp_remote_retrieve_body($list_response), true);
5438 + if (isset($list_body['vectors']) && is_array($list_body['vectors'])) {
5439 + foreach ($list_body['vectors'] as $vector) {
5440 + if (isset($vector['id'])) {
5441 + $vector_ids[] = $vector['id'];
5442 + }
5443 + }
5444 + }
5445 + }
5446 +
5447 + // Delete all vectors
5448 + $delete_result = $pinecone_manager->mxchat_delete_pinecone_batch(
5449 + $vector_ids,
5450 + $api_key,
5451 + $host
5452 + );
5453 +
5454 + if ($delete_result['success']) {
5455 + $success_ids[] = $entry_id;
5456 + } else {
5457 + $failed_ids[] = $entry_id;
5458 + $errors[] = $delete_result['message'] ?? "Failed to delete group: $entry_id";
5459 + }
5460 + } else {
5461 + // Delete single vector
5462 + $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
5463 + $entry_id,
5464 + $pinecone_options['mxchat_pinecone_api_key'],
5465 + $pinecone_options['mxchat_pinecone_host']
5466 + );
5467 +
5468 + if ($result['success']) {
5469 + $success_ids[] = $entry_id;
5470 + } else {
5471 + $failed_ids[] = $entry_id;
5472 + $errors[] = $result['message'] ?? "Failed to delete: $entry_id";
5473 + }
5474 + }
5475 + } else {
5476 + // Handle WordPress database deletion
5477 + if ($is_group && !empty($source_url)) {
5478 + // Delete all entries with this source URL
5479 + $result = $wpdb->delete(
5480 + $table_name,
5481 + array('source_url' => $source_url),
5482 + array('%s')
5483 + );
5484 + } else {
5485 + // Delete single entry
5486 + wp_cache_delete('prompt_' . $entry_id, 'mxchat_prompts');
5487 + $result = $wpdb->delete(
5488 + $table_name,
5489 + array('id' => intval($entry_id)),
5490 + array('%d')
5491 + );
5492 + }
5493 +
5494 + if ($result !== false) {
5495 + $success_ids[] = $entry_id;
5496 + } else {
5497 + $failed_ids[] = $entry_id;
5498 + $errors[] = "Database error for entry: $entry_id";
5499 + }
5500 + }
5501 + } catch (Exception $e) {
5502 + $failed_ids[] = $entry_id;
5503 + $errors[] = $e->getMessage();
5504 + }
5505 + }
5506 +
5507 + wp_send_json_success(array(
5508 + 'success_ids' => $success_ids,
5509 + 'failed_ids' => $failed_ids,
5510 + 'errors' => $errors,
5511 + 'total_processed' => count($success_ids) + count($failed_ids)
5512 + ));
5513 +
5514 + exit;
5515 +}
5516 +
5517 +/**
5518 + * Get hierarchical roles for dropdown
5519 + */
5520 +public function mxchat_get_role_options() {
5521 + return array(
5522 + 'public' => __('Public (Everyone)', 'mxchat'),
5523 + 'logged_in' => __('Logged In Users', 'mxchat'),
5524 + 'subscriber' => __('Subscribers & Above', 'mxchat'),
5525 + 'contributor' => __('Contributors & Above', 'mxchat'),
5526 + 'author' => __('Authors & Above', 'mxchat'),
5527 + 'editor' => __('Editors & Above', 'mxchat'),
5528 + 'administrator' => __('Administrators Only', 'mxchat')
5529 + );
5530 +}
5531 +
5532 +/**
5533 + * Check if user has access to content based on role restriction
5534 + */
5535 +public function mxchat_user_has_content_access($role_restriction) {
5536 + // Public content is always accessible
5537 + if ($role_restriction === 'public' || empty($role_restriction)) {
5538 + return true;
5539 + }
5540 +
5541 + // Check if user is logged in for logged_in restriction
5542 + if ($role_restriction === 'logged_in') {
5543 + return is_user_logged_in();
5544 + }
5545 +
5546 + // If not logged in, no access to role-restricted content
5547 + if (!is_user_logged_in()) {
5548 + return false;
5549 + }
5550 +
5551 + $user = wp_get_current_user();
5552 + $user_roles = $user->roles;
5553 +
5554 + if (empty($user_roles)) {
5555 + return false;
5556 + }
5557 +
5558 + // Define role hierarchy (higher number = higher access)
5559 + $hierarchy = array(
5560 + 'subscriber' => 1,
5561 + 'contributor' => 2,
5562 + 'author' => 3,
5563 + 'editor' => 4,
5564 + 'administrator' => 5
5565 + );
5566 +
5567 + // Get required level
5568 + $required_level = isset($hierarchy[$role_restriction]) ? $hierarchy[$role_restriction] : 0;
5569 +
5570 + // Check if user has required level or higher
5571 + foreach ($user_roles as $user_role) {
5572 + $user_level = isset($hierarchy[$user_role]) ? $hierarchy[$user_role] : 0;
5573 + if ($user_level >= $required_level) {
5574 + return true;
5575 + }
5576 + }
5577 +
5578 + return false;
5579 +}
5580 +
5581 +/**
5582 + * Handle role restriction updates via AJAX
5583 + * Removed cache clearing call since we removed caching
5584 + */
5585 +public function ajax_mxchat_update_role_restriction() {
5586 + // Verify nonce and permissions
5587 + if (!check_ajax_referer('mxchat_update_role_nonce', 'nonce', false)) {
5588 + wp_send_json_error('Invalid nonce');
5589 + exit;
5590 + }
5591 +
5592 + if (!current_user_can('manage_options')) {
5593 + wp_send_json_error('Unauthorized access');
5594 + exit;
5595 + }
5596 +
5597 + $entry_id = isset($_POST['entry_id']) ? sanitize_text_field($_POST['entry_id']) : '';
5598 + $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
5599 + $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
5600 +
5601 + if (empty($entry_id)) {
5602 + wp_send_json_error('Invalid entry ID');
5603 + exit;
5604 + }
5605 +
5606 + // Get knowledge manager instance to validate role restriction
5607 + $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
5608 + $valid_roles = array_keys($knowledge_manager->mxchat_get_role_options());
5609 + if (!in_array($role_restriction, $valid_roles)) {
5610 + wp_send_json_error('Invalid role restriction');
5611 + exit;
5612 + }
5613 +
5614 + global $wpdb;
5615 +
5616 + if ($data_source === 'pinecone') {
5617 + // Handle Pinecone role restriction (stored separately in WordPress table)
5618 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
5619 +
5620 + // Use REPLACE to insert or update the role restriction
5621 + $result = $wpdb->replace(
5622 + $roles_table,
5623 + array(
5624 + 'vector_id' => $entry_id,
5625 + 'role_restriction' => $role_restriction,
5626 + 'updated_at' => current_time('mysql')
5627 + ),
5628 + array('%s', '%s', '%s')
5629 + );
5630 +
5631 + // No cache clearing needed since we removed caching
5632 +
5633 + } else {
5634 + // Handle WordPress database role restriction (existing functionality)
5635 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5636 +
5637 + $result = $wpdb->update(
5638 + $table_name,
5639 + array('role_restriction' => $role_restriction),
5640 + array('id' => absint($entry_id)),
5641 + array('%s'),
5642 + array('%d')
5643 + );
5644 + }
5645 +
5646 + if ($result === false) {
5647 + wp_send_json_error('Database update failed: ' . $wpdb->last_error);
5648 + exit;
5649 + }
5650 +
5651 + wp_send_json_success(array(
5652 + 'message' => 'Role restriction updated successfully',
5653 + 'role_restriction' => $role_restriction,
5654 + 'data_source' => $data_source,
5655 + 'entry_id' => $entry_id
5656 + ));
5657 + exit;
5658 +}
5659 +
5660 +// ========================================
5661 +// ROLE-BASED CONTENT RESTRICTIONS - NEW FUNCTIONS
5662 +// Add these to your MxChat_Knowledge_Manager class
5663 +// ========================================
5664 +
5665 +/**
5666 + * Initialize role-based content hooks
5667 + * Add this call to your __construct() or mxchat_init_hooks() method
5668 + */
5669 +private function mxchat_init_role_hooks() {
5670 + // AJAX handlers for tag-role mappings
5671 + add_action('wp_ajax_mxchat_add_tag_role_mapping', array($this, 'ajax_add_tag_role_mapping'));
5672 + add_action('wp_ajax_mxchat_delete_tag_role_mapping', array($this, 'ajax_delete_tag_role_mapping'));
5673 + add_action('wp_ajax_mxchat_get_tag_role_mappings', array($this, 'ajax_get_tag_role_mappings'));
5674 + add_action('wp_ajax_mxchat_bulk_update_tag_roles', array($this, 'ajax_bulk_update_tag_roles'));
5675 +
5676 + // Hook to automatically update role restrictions when tags are added/removed
5677 + add_action('set_object_terms', array($this, 'handle_tag_change'), 10, 6);
5678 +
5679 + // Hook to apply role restrictions on auto-sync
5680 + add_action('mxchat_content_stored', array($this, 'apply_role_restriction_after_storage'), 10, 2);
5681 +}
5682 +
5683 +/**
5684 + * Add tag-role mapping via AJAX
5685 + */
5686 +public function ajax_add_tag_role_mapping() {
5687 + // Verify nonce and permissions
5688 + check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
5689 +
5690 + if (!current_user_can('manage_options')) {
5691 + wp_send_json_error('Unauthorized access');
5692 + exit;
5693 + }
5694 +
5695 + $tag_slug = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
5696 + $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
5697 +
5698 + if (empty($tag_slug)) {
5699 + wp_send_json_error('Tag slug is required');
5700 + exit;
5701 + }
5702 +
5703 + // Validate role restriction
5704 + $valid_roles = array_keys($this->mxchat_get_role_options());
5705 + if (!in_array($role_restriction, $valid_roles)) {
5706 + wp_send_json_error('Invalid role restriction');
5707 + exit;
5708 + }
5709 +
5710 + // Check if tag exists in WordPress
5711 + $term = get_term_by('slug', $tag_slug, 'post_tag');
5712 + if (!$term) {
5713 + wp_send_json_error('Tag does not exist in WordPress');
5714 + exit;
5715 + }
5716 +
5717 + // Get existing mappings
5718 + $mappings = get_option('mxchat_tag_role_mappings', array());
5719 +
5720 + // Check if mapping already exists
5721 + if (isset($mappings[$tag_slug])) {
5722 + wp_send_json_error('Mapping for this tag already exists');
5723 + exit;
5724 + }
5725 +
5726 + // Add new mapping
5727 + $mappings[$tag_slug] = $role_restriction;
5728 + update_option('mxchat_tag_role_mappings', $mappings);
5729 +
5730 + wp_send_json_success(array(
5731 + 'message' => 'Tag-role mapping added successfully',
5732 + 'tag_slug' => $tag_slug,
5733 + 'role_restriction' => $role_restriction
5734 + ));
5735 + exit;
5736 +}
5737 +
5738 +/**
5739 + * Delete tag-role mapping via AJAX
5740 + */
5741 +public function ajax_delete_tag_role_mapping() {
5742 + // Verify nonce and permissions
5743 + check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
5744 +
5745 + if (!current_user_can('manage_options')) {
5746 + wp_send_json_error('Unauthorized access');
5747 + exit;
5748 + }
5749 +
5750 + $tag_slug = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
5751 +
5752 + if (empty($tag_slug)) {
5753 + wp_send_json_error('Tag slug is required');
5754 + exit;
5755 + }
5756 +
5757 + // Get existing mappings
5758 + $mappings = get_option('mxchat_tag_role_mappings', array());
5759 +
5760 + // Check if mapping exists
5761 + if (!isset($mappings[$tag_slug])) {
5762 + wp_send_json_error('Mapping does not exist');
5763 + exit;
5764 + }
5765 +
5766 + // Remove mapping
5767 + unset($mappings[$tag_slug]);
5768 + update_option('mxchat_tag_role_mappings', $mappings);
5769 +
5770 + wp_send_json_success(array(
5771 + 'message' => 'Tag-role mapping deleted successfully',
5772 + 'tag_slug' => $tag_slug
5773 + ));
5774 + exit;
5775 +}
5776 +
5777 +/**
5778 + * Get all tag-role mappings via AJAX
5779 + */
5780 +public function ajax_get_tag_role_mappings() {
5781 + // Verify nonce and permissions
5782 + check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
5783 +
5784 + if (!current_user_can('manage_options')) {
5785 + wp_send_json_error('Unauthorized access');
5786 + exit;
5787 + }
5788 +
5789 + // Get mappings
5790 + $mappings = get_option('mxchat_tag_role_mappings', array());
5791 + $role_options = $this->mxchat_get_role_options();
5792 +
5793 + $formatted_mappings = array();
5794 +
5795 + foreach ($mappings as $tag_slug => $role_restriction) {
5796 + // Get tag object
5797 + $term = get_term_by('slug', $tag_slug, 'post_tag');
5798 +
5799 + // Count posts with this tag
5800 + $post_count = 0;
5801 + if ($term) {
5802 + $post_count = $term->count;
5803 + }
5804 +
5805 + $formatted_mappings[] = array(
5806 + 'tag_slug' => $tag_slug,
5807 + 'role_restriction' => $role_restriction,
5808 + 'role_label' => $role_options[$role_restriction] ?? $role_restriction,
5809 + 'post_count' => $post_count
5810 + );
5811 + }
5812 +
5813 + wp_send_json_success(array(
5814 + 'mappings' => $formatted_mappings
5815 + ));
5816 + exit;
5817 +}
5818 +
5819 +/**
5820 + * Bulk update role restrictions for all existing content with mapped tags
5821 + */
5822 +public function ajax_bulk_update_tag_roles() {
5823 + // Verify nonce and permissions
5824 + check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
5825 +
5826 + if (!current_user_can('manage_options')) {
5827 + wp_send_json_error('Unauthorized access');
5828 + exit;
5829 + }
5830 +
5831 + // Get mappings
5832 + $mappings = get_option('mxchat_tag_role_mappings', array());
5833 +
5834 + if (empty($mappings)) {
5835 + wp_send_json_error('No tag-role mappings found');
5836 + exit;
5837 + }
5838 +
5839 + global $wpdb;
5840 +
5841 + // Check if using Pinecone
5842 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5843 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5844 +
5845 + $updated_count = 0;
5846 + $details = array();
5847 +
5848 + foreach ($mappings as $tag_slug => $role_restriction) {
5849 + // Get all posts with this tag
5850 + $posts = get_posts(array(
5851 + 'tag' => $tag_slug,
5852 + 'post_type' => 'any',
5853 + 'posts_per_page' => -1,
5854 + 'fields' => 'ids',
5855 + 'post_status' => 'publish'
5856 + ));
5857 +
5858 + if (empty($posts)) {
5859 + continue;
5860 + }
5861 +
5862 + $tag_updated = 0;
5863 +
5864 + foreach ($posts as $post_id) {
5865 + $source_url = get_permalink($post_id);
5866 + if (!$source_url) {
5867 + continue;
5868 + }
5869 +
5870 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
5871 + // Update Pinecone role restriction
5872 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
5873 + $vector_id = md5($source_url);
5874 +
5875 + $result = $wpdb->replace(
5876 + $roles_table,
5877 + array(
5878 + 'vector_id' => $vector_id,
5879 + 'role_restriction' => $role_restriction,
5880 + 'updated_at' => current_time('mysql')
5881 + ),
5882 + array('%s', '%s', '%s')
5883 + );
5884 + } else {
5885 + // Update WordPress DB
5886 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5887 +
5888 + $result = $wpdb->update(
5889 + $table_name,
5890 + array('role_restriction' => $role_restriction),
5891 + array('source_url' => $source_url),
5892 + array('%s'),
5893 + array('%s')
5894 + );
5895 + }
5896 +
5897 + if ($result !== false) {
5898 + $tag_updated++;
5899 + $updated_count++;
5900 + }
5901 + }
5902 +
5903 + if ($tag_updated > 0) {
5904 + $details[] = sprintf(
5905 + 'Tag "%s" (%s): %d posts updated',
5906 + $tag_slug,
5907 + $role_restriction,
5908 + $tag_updated
5909 + );
5910 + }
5911 + }
5912 +
5913 + wp_send_json_success(array(
5914 + 'message' => 'Bulk update completed',
5915 + 'updated_count' => $updated_count,
5916 + 'tags_processed' => count($mappings),
5917 + 'details' => $details
5918 + ));
5919 + exit;
5920 +}
5921 +
5922 +/**
5923 + * Handle tag changes on posts (when tags are added or removed)
5924 + */
5925 +public function handle_tag_change($object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids) {
5926 + // Only process post tags
5927 + if ($taxonomy !== 'post_tag') {
5928 + return;
5929 + }
5930 +
5931 + // Get tag-role mappings
5932 + $mappings = get_option('mxchat_tag_role_mappings', array());
5933 +
5934 + if (empty($mappings)) {
5935 + return;
5936 + }
5937 +
5938 + // Get the post's URL
5939 + $source_url = get_permalink($object_id);
5940 + if (!$source_url) {
5941 + return;
5942 + }
5943 +
5944 + // Determine the highest role restriction based on tags
5945 + $highest_role = 'public';
5946 + $role_hierarchy = array(
5947 + 'public' => 0,
5948 + 'logged_in' => 1,
5949 + 'subscriber' => 2,
5950 + 'contributor' => 3,
5951 + 'author' => 4,
5952 + 'editor' => 5,
5953 + 'administrator' => 6
5954 + );
5955 +
5956 + // Get all current tags for the post
5957 + $current_tags = wp_get_post_tags($object_id, array('fields' => 'slugs'));
5958 +
5959 + // Find the highest role restriction among the tags
5960 + foreach ($current_tags as $tag_slug) {
5961 + if (isset($mappings[$tag_slug])) {
5962 + $role = $mappings[$tag_slug];
5963 + if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
5964 + $highest_role = $role;
5965 + }
5966 + }
5967 + }
5968 +
5969 + // Update the role restriction in the database
5970 + global $wpdb;
5971 +
5972 + // Check if using Pinecone
5973 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5974 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5975 +
5976 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
5977 + // Update Pinecone role restriction
5978 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
5979 + $vector_id = md5($source_url);
5980 +
5981 + $wpdb->replace(
5982 + $roles_table,
5983 + array(
5984 + 'vector_id' => $vector_id,
5985 + 'role_restriction' => $highest_role,
5986 + 'updated_at' => current_time('mysql')
5987 + ),
5988 + array('%s', '%s', '%s')
5989 + );
5990 + } else {
5991 + // Update WordPress DB
5992 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5993 +
5994 + $wpdb->update(
5995 + $table_name,
5996 + array('role_restriction' => $highest_role),
5997 + array('source_url' => $source_url),
5998 + array('%s'),
5999 + array('%s')
6000 + );
6001 + }
6002 +}
6003 +
6004 +/**
6005 + * Apply role restriction after content is stored (for auto-sync)
6006 + */
6007 +public function apply_role_restriction_after_storage($post_id, $source_url) {
6008 + // Get tag-role mappings
6009 + $mappings = get_option('mxchat_tag_role_mappings', array());
6010 +
6011 + if (empty($mappings)) {
6012 + return;
6013 + }
6014 +
6015 + // Get all tags for the post
6016 + $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
6017 +
6018 + if (empty($post_tags)) {
6019 + return;
6020 + }
6021 +
6022 + // Determine the highest role restriction based on tags
6023 + $highest_role = 'public';
6024 + $role_hierarchy = array(
6025 + 'public' => 0,
6026 + 'logged_in' => 1,
6027 + 'subscriber' => 2,
6028 + 'contributor' => 3,
6029 + 'author' => 4,
6030 + 'editor' => 5,
6031 + 'administrator' => 6
6032 + );
6033 +
6034 + foreach ($post_tags as $tag_slug) {
6035 + if (isset($mappings[$tag_slug])) {
6036 + $role = $mappings[$tag_slug];
6037 + if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
6038 + $highest_role = $role;
6039 + }
6040 + }
6041 + }
6042 +
6043 + // If no restricted tags found, return (leave as public)
6044 + if ($highest_role === 'public') {
6045 + return;
6046 + }
6047 +
6048 + // Update the role restriction
6049 + global $wpdb;
6050 +
6051 + // Check if using Pinecone
6052 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
6053 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6054 +
6055 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
6056 + // Update Pinecone role restriction
6057 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6058 + $vector_id = md5($source_url);
6059 +
6060 + $wpdb->replace(
6061 + $roles_table,
6062 + array(
6063 + 'vector_id' => $vector_id,
6064 + 'role_restriction' => $highest_role,
6065 + 'updated_at' => current_time('mysql')
6066 + ),
6067 + array('%s', '%s', '%s')
6068 + );
6069 + } else {
6070 + // Update WordPress DB
6071 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6072 +
6073 + $wpdb->update(
6074 + $table_name,
6075 + array('role_restriction' => $highest_role),
6076 + array('source_url' => $source_url),
6077 + array('%s'),
6078 + array('%s')
6079 + );
6080 + }
6081 +}
6082 +
6083 +
6084 + // ========================================
6085 + // HELPER METHODS
6086 + // ========================================
6087 +
6088 + /**
6089 + * Check if user has required permissions for content processing
6090 + */
6091 + private function mxchat_check_user_permissions() {
6092 + if (!current_user_can('manage_options')) {
6093 + wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
6094 + }
6095 + }
6096 +
6097 + /**
6098 + * Validate nonce for security
6099 + */
6100 + private function mxchat_validate_nonce($nonce_name, $nonce_action) {
6101 + if (!isset($_POST[$nonce_name]) || !wp_verify_nonce($_POST[$nonce_name], $nonce_action)) {
6102 + wp_die(esc_html__('Security check failed.', 'mxchat'));
6103 + }
6104 + }
6105 +
6106 + /**
6107 + * Get embedding API credentials
6108 + */
6109 + private function mxchat_get_embedding_credentials() {
6110 + $embedding_model = $this->options['embedding_model'] ?? 'text-embedding-ada-002';
6111 +
6112 + if (strpos($embedding_model, 'text-embedding-') !== false) {
6113 + return array(
6114 + 'type' => 'openai',
6115 + 'api_key' => $this->options['api_key'] ?? ''
6116 + );
6117 + } elseif (strpos($embedding_model, 'voyage-') !== false) {
6118 + return array(
6119 + 'type' => 'voyage',
6120 + 'api_key' => $this->options['voyage_api_key'] ?? ''
6121 + );
6122 + } elseif (strpos($embedding_model, 'gemini-embedding-') !== false) {
6123 + return array(
6124 + 'type' => 'gemini',
6125 + 'api_key' => $this->options['gemini_api_key'] ?? ''
6126 + );
6127 + }
6128 +
6129 + return array('type' => 'unknown', 'api_key' => '');
6130 + }
6131 +
6132 + /**
6133 + * Log processing errors
6134 + */
6135 + private function mxchat_log_processing_error($operation, $error_message) {
6136 + //error_log("MxChat Knowledge Processing {$operation} Error: " . $error_message);
6137 + }
6138 +
6139 + /**
6140 + * Set admin notice transient
6141 + */
6142 + private function mxchat_set_admin_notice($type, $message) {
6143 + set_transient("mxchat_admin_notice_{$type}", $message, 30);
6144 + }
6145 +
6146 + /**
6147 + * Get Pinecone manager instance for vector operations
6148 + */
6149 + private function mxchat_get_pinecone_manager() {
6150 + return MxChat_Pinecone_Manager::get_instance();
6151 + }
6152 +
6153 +
6154 + // ========================================
6155 +// DATABASE QUEUE TABLE MANAGEMENT
6156 +// ========================================
6157 +
6158 +/**
6159 + * Create queue table on plugin activation
6160 + * Call this from your plugin activation hook
6161 + */
6162 +public function mxchat_create_queue_table() {
6163 + global $wpdb;
6164 +
6165 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6166 + $charset_collate = $wpdb->get_charset_collate();
6167 +
6168 + $sql = "CREATE TABLE IF NOT EXISTS $table_name (
6169 + id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
6170 + queue_id varchar(64) NOT NULL,
6171 + item_type varchar(20) NOT NULL,
6172 + item_data longtext NOT NULL,
6173 + status varchar(20) NOT NULL DEFAULT 'pending',
6174 + bot_id varchar(50) NOT NULL DEFAULT 'default',
6175 + priority int(11) NOT NULL DEFAULT 0,
6176 + attempts int(11) NOT NULL DEFAULT 0,
6177 + max_attempts int(11) NOT NULL DEFAULT 3,
6178 + error_message text DEFAULT NULL,
6179 + created_at datetime NOT NULL,
6180 + started_at datetime DEFAULT NULL,
6181 + completed_at datetime DEFAULT NULL,
6182 + PRIMARY KEY (id),
6183 + KEY queue_id (queue_id),
6184 + KEY status (status),
6185 + KEY item_type (item_type),
6186 + KEY priority (priority)
6187 + ) $charset_collate;";
6188 +
6189 + require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
6190 + dbDelta($sql);
6191 +
6192 + // Also create a meta table for queue metadata
6193 + $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
6194 +
6195 + $meta_sql = "CREATE TABLE IF NOT EXISTS $meta_table (
6196 + id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
6197 + queue_id varchar(64) NOT NULL,
6198 + meta_key varchar(255) NOT NULL,
6199 + meta_value longtext,
6200 + PRIMARY KEY (id),
6201 + KEY queue_id (queue_id),
6202 + KEY meta_key (meta_key)
6203 + ) $charset_collate;";
6204 +
6205 + dbDelta($meta_sql);
6206 +}
6207 +
6208 +/**
6209 + * Add items to the processing queue
6210 + *
6211 + * @param string $queue_id Unique identifier for this queue batch
6212 + * @param string $item_type Type of item (url, pdf_page)
6213 + * @param array $items Array of items to queue
6214 + * @param string $bot_id Bot ID for processing
6215 + * @return int Number of items queued
6216 + */
6217 +private function mxchat_add_to_queue($queue_id, $item_type, $items, $bot_id = 'default') {
6218 + global $wpdb;
6219 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6220 +
6221 + $queued_count = 0;
6222 + $priority = 0;
6223 +
6224 + foreach ($items as $item) {
6225 + $result = $wpdb->insert(
6226 + $table_name,
6227 + array(
6228 + 'queue_id' => $queue_id,
6229 + 'item_type' => $item_type,
6230 + 'item_data' => wp_json_encode($item),
6231 + 'status' => 'pending',
6232 + 'bot_id' => $bot_id,
6233 + 'priority' => $priority,
6234 + 'attempts' => 0,
6235 + 'max_attempts' => 3,
6236 + 'created_at' => current_time('mysql')
6237 + ),
6238 + array('%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%s')
6239 + );
6240 +
6241 + if ($result) {
6242 + $queued_count++;
6243 + }
6244 +
6245 + $priority++; // Process in order
6246 + }
6247 +
6248 + return $queued_count;
6249 +}
6250 +
6251 +/**
6252 + * Store queue metadata (total counts, source URL, etc.)
6253 + */
6254 +private function mxchat_set_queue_meta($queue_id, $meta_key, $meta_value) {
6255 + global $wpdb;
6256 + $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
6257 +
6258 + // Check if meta exists
6259 + $existing = $wpdb->get_var($wpdb->prepare(
6260 + "SELECT id FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
6261 + $queue_id,
6262 + $meta_key
6263 + ));
6264 +
6265 + if ($existing) {
6266 + // Update
6267 + $wpdb->update(
6268 + $meta_table,
6269 + array('meta_value' => maybe_serialize($meta_value)),
6270 + array('queue_id' => $queue_id, 'meta_key' => $meta_key),
6271 + array('%s'),
6272 + array('%s', '%s')
6273 + );
6274 + } else {
6275 + // Insert
6276 + $wpdb->insert(
6277 + $meta_table,
6278 + array(
6279 + 'queue_id' => $queue_id,
6280 + 'meta_key' => $meta_key,
6281 + 'meta_value' => maybe_serialize($meta_value)
6282 + ),
6283 + array('%s', '%s', '%s')
6284 + );
6285 + }
6286 +}
6287 +
6288 +/**
6289 + * Get queue metadata
6290 + */
6291 +private function mxchat_get_queue_meta($queue_id, $meta_key) {
6292 + global $wpdb;
6293 + $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
6294 +
6295 + $value = $wpdb->get_var($wpdb->prepare(
6296 + "SELECT meta_value FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
6297 + $queue_id,
6298 + $meta_key
6299 + ));
6300 +
6301 + return maybe_unserialize($value);
6302 +}
6303 +
6304 +// ========================================
6305 +// AJAX QUEUE PROCESSING HANDLERS
6306 +// ========================================
6307 +
6308 +/**
6309 + * AJAX: Get next item from queue to process
6310 + */
6311 +public function ajax_mxchat_get_next_queue_item() {
6312 + // Verify nonce and permissions
6313 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
6314 +
6315 + if (!current_user_can('manage_options')) {
6316 + wp_send_json_error('Unauthorized access');
6317 + }
6318 +
6319 + $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
6320 +
6321 + if (empty($queue_id)) {
6322 + wp_send_json_error('Missing queue ID');
6323 + }
6324 +
6325 + global $wpdb;
6326 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6327 +
6328 + // Get next pending item with retry logic for failed items
6329 + $next_item = $wpdb->get_row($wpdb->prepare(
6330 + "SELECT * FROM $table_name
6331 + WHERE queue_id = %s
6332 + AND status IN ('pending', 'failed')
6333 + AND attempts < max_attempts
6334 + ORDER BY priority ASC, id ASC
6335 + LIMIT 1",
6336 + $queue_id
6337 + ));
6338 +
6339 + if (!$next_item) {
6340 + // No more items - queue complete
6341 + wp_send_json_success(array(
6342 + 'complete' => true,
6343 + 'message' => 'Queue processing complete'
6344 + ));
6345 + }
6346 +
6347 + // Mark item as processing
6348 + $wpdb->update(
6349 + $table_name,
6350 + array(
6351 + 'status' => 'processing',
6352 + 'started_at' => current_time('mysql'),
6353 + 'attempts' => $next_item->attempts + 1
6354 + ),
6355 + array('id' => $next_item->id),
6356 + array('%s', '%s', '%d'),
6357 + array('%d')
6358 + );
6359 +
6360 + wp_send_json_success(array(
6361 + 'complete' => false,
6362 + 'item' => array(
6363 + 'id' => $next_item->id,
6364 + 'type' => $next_item->item_type,
6365 + 'data' => json_decode($next_item->item_data, true),
6366 + 'bot_id' => $next_item->bot_id,
6367 + 'attempt' => $next_item->attempts + 1
6368 + )
6369 + ));
6370 +}
6371 +
6372 +/**
6373 + * AJAX: Process a single queue item
6374 + */
6375 +public function ajax_mxchat_process_queue_item() {
6376 + // Verify nonce and permissions
6377 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
6378 +
6379 + if (!current_user_can('manage_options')) {
6380 + wp_send_json_error('Unauthorized access');
6381 + }
6382 +
6383 + $item_id = isset($_POST['item_id']) ? absint($_POST['item_id']) : 0;
6384 + $item_type = isset($_POST['item_type']) ? sanitize_text_field($_POST['item_type']) : '';
6385 + $item_data = isset($_POST['item_data']) ? $_POST['item_data'] : array();
6386 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
6387 +
6388 + if (empty($item_id) || empty($item_type)) {
6389 + wp_send_json_error('Missing item data');
6390 + }
6391 +
6392 + global $wpdb;
6393 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6394 +
6395 + // Process based on item type
6396 + try {
6397 + set_time_limit(60); // Give processing 60 seconds
6398 +
6399 + $result = false;
6400 + $error_message = '';
6401 +
6402 + switch ($item_type) {
6403 + case 'url':
6404 + $result = $this->mxchat_process_queue_url($item_data, $bot_id);
6405 + break;
6406 +
6407 + case 'pdf_page':
6408 + $result = $this->mxchat_process_queue_pdf_page($item_data, $bot_id);
6409 + break;
6410 +
6411 + default:
6412 + throw new Exception('Unknown item type: ' . $item_type);
6413 + }
6414 +
6415 + if (is_wp_error($result)) {
6416 + throw new Exception($result->get_error_message());
6417 + }
6418 +
6419 + if ($result === false) {
6420 + throw new Exception('Processing returned false - item may be empty or invalid');
6421 + }
6422 +
6423 + // Mark as completed
6424 + $wpdb->update(
6425 + $table_name,
6426 + array(
6427 + 'status' => 'completed',
6428 + 'completed_at' => current_time('mysql'),
6429 + 'error_message' => null
6430 + ),
6431 + array('id' => $item_id),
6432 + array('%s', '%s', '%s'),
6433 + array('%d')
6434 + );
6435 +
6436 + wp_send_json_success(array(
6437 + 'processed' => true,
6438 + 'item_id' => $item_id,
6439 + 'message' => 'Item processed successfully'
6440 + ));
6441 +
6442 + } catch (Exception $e) {
6443 + $error_message = $e->getMessage();
6444 +
6445 + // Get current attempt count
6446 + $item = $wpdb->get_row($wpdb->prepare(
6447 + "SELECT attempts, max_attempts FROM $table_name WHERE id = %d",
6448 + $item_id
6449 + ));
6450 +
6451 + // Check if we've exhausted retries
6452 + if ($item && $item->attempts >= $item->max_attempts) {
6453 + // Permanently failed
6454 + $wpdb->update(
6455 + $table_name,
6456 + array(
6457 + 'status' => 'failed',
6458 + 'error_message' => $error_message
6459 + ),
6460 + array('id' => $item_id),
6461 + array('%s', '%s'),
6462 + array('%d')
6463 + );
6464 +
6465 + wp_send_json_error(array(
6466 + 'message' => 'Item failed after maximum attempts: ' . $error_message,
6467 + 'permanent_failure' => true,
6468 + 'item_id' => $item_id
6469 + ));
6470 + } else {
6471 + // Mark for retry
6472 + $wpdb->update(
6473 + $table_name,
6474 + array(
6475 + 'status' => 'failed',
6476 + 'error_message' => $error_message
6477 + ),
6478 + array('id' => $item_id),
6479 + array('%s', '%s'),
6480 + array('%d')
6481 + );
6482 +
6483 + wp_send_json_error(array(
6484 + 'message' => 'Item processing failed, will retry: ' . $error_message,
6485 + 'can_retry' => true,
6486 + 'item_id' => $item_id,
6487 + 'attempts' => $item ? $item->attempts : 0
6488 + ));
6489 + }
6490 + }
6491 +}
6492 +
6493 +/**
6494 + * Process a URL from the queue
6495 + */
6496 +private function mxchat_process_queue_url($item_data, $bot_id = 'default') {
6497 + $url = isset($item_data['url']) ? $item_data['url'] : '';
6498 +
6499 + if (empty($url)) {
6500 + return new WP_Error('invalid_url', 'URL is empty');
6501 + }
6502 +
6503 + // Get bot-specific API key early (needed for both paths)
6504 + $bot_options = $this->get_bot_options($bot_id);
6505 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
6506 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
6507 +
6508 + if (strpos($selected_model, 'voyage') === 0) {
6509 + $api_key = $options['voyage_api_key'] ?? '';
6510 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
6511 + $api_key = $options['gemini_api_key'] ?? '';
6512 + } else {
6513 + $api_key = $options['api_key'] ?? '';
6514 + }
6515 +
6516 + if (empty($api_key)) {
6517 + return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
6518 + }
6519 +
6520 + // Check if this is a WooCommerce product URL and WooCommerce is active
6521 + $is_product_url = (strpos($url, '/product/') !== false || strpos($url, '/shop/') !== false);
6522 + $content_type = $is_product_url ? 'product' : 'url';
6523 +
6524 + // Try to get WooCommerce product data if it's a product URL
6525 + if ($is_product_url && class_exists('WooCommerce')) {
6526 + $product_content = $this->mxchat_extract_woocommerce_product_content($url);
6527 +
6528 + if (!empty($product_content)) {
6529 + // Successfully extracted WooCommerce product data with pricing
6530 + $result = MxChat_Utils::submit_content_to_db(
6531 + $product_content,
6532 + $url,
6533 + $api_key,
6534 + null,
6535 + $bot_id,
6536 + 'product'
6537 + );
6538 + return $result;
6539 + }
6540 + // If WooCommerce extraction failed, fall through to HTML extraction
6541 + }
6542 +
6543 + // Fetch URL content (fallback for non-products or when WooCommerce extraction fails)
6544 + $response = wp_remote_get($url, array(
6545 + 'timeout' => 30,
6546 + 'redirection' => 5,
6547 + 'user-agent' => 'MxChat/1.0'
6548 + ));
6549 +
6550 + if (is_wp_error($response)) {
6551 + return $response;
6552 + }
6553 +
6554 + $response_code = wp_remote_retrieve_response_code($response);
6555 + if ($response_code !== 200) {
6556 + return new WP_Error('http_error', 'HTTP ' . $response_code . ' error');
6557 + }
6558 +
6559 + $html = wp_remote_retrieve_body($response);
6560 +
6561 + if (empty($html)) {
6562 + return new WP_Error('empty_response', 'Empty response body');
6563 + }
6564 +
6565 + // Extract and sanitize content
6566 + $content = $this->mxchat_extract_main_content($html);
6567 + $sanitized = $this->mxchat_sanitize_content_for_api($content);
6568 +
6569 + if (empty($sanitized)) {
6570 + // Not an error - just no content found (maybe a redirect or empty page)
6571 + return false;
6572 + }
6573 +
6574 + // Submit to database with content_type
6575 + $result = MxChat_Utils::submit_content_to_db(
6576 + $sanitized,
6577 + $url,
6578 + $api_key,
6579 + null,
6580 + $bot_id,
6581 + $content_type
6582 + );
6583 +
6584 + return $result;
6585 +}
6586 +
6587 +/**
6588 + * Extract WooCommerce product content including pricing
6589 + *
6590 + * @param string $url The product URL
6591 + * @return string|false Product content with pricing, or false if not found
6592 + */
6593 +private function mxchat_extract_woocommerce_product_content($url) {
6594 + // Try to get product ID from URL
6595 + $product_id = url_to_postid($url);
6596 +
6597 + // If url_to_postid fails, try to extract from URL pattern
6598 + if (!$product_id) {
6599 + $product_slug = '';
6600 +
6601 + // Handle pretty permalinks: /product/product-name/
6602 + if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
6603 + $product_slug = $matches[1];
6604 + }
6605 +
6606 + if (!empty($product_slug)) {
6607 + $product_post = get_page_by_path($product_slug, OBJECT, 'product');
6608 + if ($product_post) {
6609 + $product_id = $product_post->ID;
6610 + }
6611 + }
6612 + }
6613 +
6614 + if (!$product_id) {
6615 + return false;
6616 + }
6617 +
6618 + // Get WooCommerce product object
6619 + $product = wc_get_product($product_id);
6620 +
6621 + if (!$product) {
6622 + return false;
6623 + }
6624 +
6625 + // Build product content with pricing (similar to mxchat_store_product_embedding)
6626 + $title = $product->get_name();
6627 + $description = $product->get_description();
6628 + $short_description = $product->get_short_description();
6629 + $sku = $product->get_sku();
6630 +
6631 + // Get pricing information
6632 + $regular_price = $product->get_regular_price();
6633 + $sale_price = $product->get_sale_price();
6634 + $price = $product->get_price(); // Current active price
6635 +
6636 + // Get currency symbol
6637 + $currency_symbol = get_woocommerce_currency_symbol();
6638 +
6639 + // Format content
6640 + $content = $title . "\n\n";
6641 +
6642 + if (!empty($short_description)) {
6643 + $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
6644 + }
6645 +
6646 + if (!empty($description)) {
6647 + $content .= wp_strip_all_tags($description) . "\n\n";
6648 + }
6649 +
6650 + // Add pricing information
6651 + if (!empty($regular_price)) {
6652 + $content .= "Price: " . $currency_symbol . $regular_price . "\n";
6653 + } elseif (!empty($price)) {
6654 + $content .= "Price: " . $currency_symbol . $price . "\n";
6655 + }
6656 +
6657 + if (!empty($sale_price) && $sale_price !== $regular_price) {
6658 + $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
6659 + }
6660 +
6661 + // Handle variable products - show price range
6662 + if ($product->is_type('variable')) {
6663 + $min_price = $product->get_variation_price('min');
6664 + $max_price = $product->get_variation_price('max');
6665 + if ($min_price !== $max_price) {
6666 + $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
6667 + }
6668 + }
6669 +
6670 + if (!empty($sku)) {
6671 + $content .= "SKU: " . $sku . "\n";
6672 + }
6673 +
6674 + // Get product categories
6675 + $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
6676 + if (!empty($categories) && !is_wp_error($categories)) {
6677 + $content .= "Categories: " . implode(', ', $categories) . "\n";
6678 + }
6679 +
6680 + // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
6681 + $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
6682 + if (!empty($custom_tabs) && is_array($custom_tabs)) {
6683 + foreach ($custom_tabs as $tab) {
6684 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
6685 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
6686 +
6687 + if (!empty($tab_title) && !empty($tab_content)) {
6688 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
6689 + }
6690 + }
6691 + }
6692 +
6693 + // Also check for reusable/saved tabs applied to this product
6694 + $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
6695 + if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
6696 + $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
6697 + if (!empty($saved_tabs) && is_array($saved_tabs)) {
6698 + foreach ($applied_saved_tabs as $saved_tab_id) {
6699 + if (isset($saved_tabs[$saved_tab_id])) {
6700 + $tab = $saved_tabs[$saved_tab_id];
6701 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
6702 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
6703 +
6704 + if (!empty($tab_title) && !empty($tab_content)) {
6705 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
6706 + }
6707 + }
6708 + }
6709 + }
6710 + }
6711 +
6712 + return $this->mxchat_sanitize_content_for_api($content);
6713 +}
6714 +
6715 +/**
6716 + * Process a PDF page from the queue
6717 + */
6718 +private function mxchat_process_queue_pdf_page($item_data, $bot_id = 'default') {
6719 + $pdf_path = isset($item_data['pdf_path']) ? $item_data['pdf_path'] : '';
6720 + $pdf_url = isset($item_data['pdf_url']) ? $item_data['pdf_url'] : '';
6721 + $page_number = isset($item_data['page_number']) ? absint($item_data['page_number']) : 0;
6722 + $total_pages = isset($item_data['total_pages']) ? absint($item_data['total_pages']) : 0;
6723 +
6724 + if (empty($pdf_path) || !file_exists($pdf_path)) {
6725 + return new WP_Error('pdf_not_found', 'PDF file not found: ' . $pdf_path);
6726 + }
6727 +
6728 + if ($page_number < 1) {
6729 + return new WP_Error('invalid_page', 'Invalid page number');
6730 + }
6731 +
6732 + try {
6733 + $parser = new \Smalot\PdfParser\Parser();
6734 + $pdf = $parser->parseFile($pdf_path);
6735 + $pages = $pdf->getPages();
6736 +
6737 + if (!isset($pages[$page_number - 1])) {
6738 + return new WP_Error('page_not_found', 'Page ' . $page_number . ' not found in PDF');
6739 + }
6740 +
6741 + $text = $pages[$page_number - 1]->getText();
6742 +
6743 + if (empty($text)) {
6744 + // Not an error - just an empty page
6745 + return false;
6746 + }
6747 +
6748 + $sanitized = $this->mxchat_sanitize_content_for_api($text);
6749 +
6750 + if (empty($sanitized)) {
6751 + return false;
6752 + }
6753 +
6754 + // Create metadata
6755 + $metadata = array(
6756 + 'document_type' => 'pdf',
6757 + 'total_pages' => $total_pages,
6758 + 'current_page' => $page_number,
6759 + 'source_url' => $pdf_url
6760 + );
6761 +
6762 + $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized;
6763 + $page_url = esc_url($pdf_url . "#page=" . $page_number);
6764 +
6765 + // Get bot-specific API key
6766 + $bot_options = $this->get_bot_options($bot_id);
6767 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
6768 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
6769 +
6770 + if (strpos($selected_model, 'voyage') === 0) {
6771 + $api_key = $options['voyage_api_key'] ?? '';
6772 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
6773 + $api_key = $options['gemini_api_key'] ?? '';
6774 + } else {
6775 + $api_key = $options['api_key'] ?? '';
6776 + }
6777 +
6778 + if (empty($api_key)) {
6779 + return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
6780 + }
6781 +
6782 + // Submit to database - UPDATED 2.5.6: Added content_type 'pdf'
6783 + $result = MxChat_Utils::submit_content_to_db(
6784 + $content_with_metadata,
6785 + $page_url,
6786 + $api_key,
6787 + null,
6788 + $bot_id,
6789 + 'pdf'
6790 + );
6791 +
6792 + return $result;
6793 +
6794 + } catch (Exception $e) {
6795 + return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
6796 + }
6797 +}
6798 +
6799 +/**
6800 + * AJAX: Get queue processing status
6801 + */
6802 +public function ajax_mxchat_get_queue_status() {
6803 + // Verify nonce and permissions
6804 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
6805 +
6806 + if (!current_user_can('manage_options')) {
6807 + wp_send_json_error('Unauthorized access');
6808 + }
6809 +
6810 + $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
6811 +
6812 + if (empty($queue_id)) {
6813 + wp_send_json_error('Missing queue ID');
6814 + }
6815 +
6816 + global $wpdb;
6817 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6818 +
6819 + // Get counts by status
6820 + $counts = $wpdb->get_results($wpdb->prepare(
6821 + "SELECT status, COUNT(*) as count
6822 + FROM $table_name
6823 + WHERE queue_id = %s
6824 + GROUP BY status",
6825 + $queue_id
6826 + ), OBJECT_K);
6827 +
6828 + $total = 0;
6829 + $completed = 0;
6830 + $failed = 0;
6831 + $processing = 0;
6832 + $pending = 0;
6833 +
6834 + foreach ($counts as $status => $data) {
6835 + $count = absint($data->count);
6836 + $total += $count;
6837 +
6838 + switch ($status) {
6839 + case 'completed':
6840 + $completed = $count;
6841 + break;
6842 + case 'failed':
6843 + $failed = $count;
6844 + break;
6845 + case 'processing':
6846 + $processing = $count;
6847 + break;
6848 + case 'pending':
6849 + $pending = $count;
6850 + break;
6851 + }
6852 + }
6853 +
6854 + // Calculate percentage
6855 + $percentage = $total > 0 ? round((($completed + $failed) / $total) * 100) : 0;
6856 +
6857 + // Get failed items details
6858 + $failed_items = array();
6859 + if ($failed > 0) {
6860 + $failed_items = $wpdb->get_results($wpdb->prepare(
6861 + "SELECT item_type, item_data, error_message, attempts
6862 + FROM $table_name
6863 + WHERE queue_id = %s
6864 + AND status = 'failed'
6865 + AND attempts >= max_attempts
6866 + ORDER BY id DESC
6867 + LIMIT 50",
6868 + $queue_id
6869 + ));
6870 + }
6871 +
6872 + // Get queue metadata
6873 + $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
6874 + $queue_type = $this->mxchat_get_queue_meta($queue_id, 'queue_type');
6875 +
6876 + // Determine if queue is complete
6877 + $is_complete = ($pending === 0 && $processing === 0);
6878 +
6879 + wp_send_json_success(array(
6880 + 'queue_id' => $queue_id,
6881 + 'queue_type' => $queue_type,
6882 + 'source_url' => $source_url,
6883 + 'total' => $total,
6884 + 'completed' => $completed,
6885 + 'failed' => $failed,
6886 + 'processing' => $processing,
6887 + 'pending' => $pending,
6888 + 'percentage' => $percentage,
6889 + 'is_complete' => $is_complete,
6890 + 'failed_items' => $failed_items,
6891 + 'status' => $is_complete ? 'complete' : 'processing'
6892 + ));
6893 +}
6894 +
6895 +/**
6896 + * AJAX: Clear completed queue
6897 + */
6898 +public function ajax_mxchat_clear_queue() {
6899 + // Verify nonce and permissions
6900 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
6901 +
6902 + if (!current_user_can('manage_options')) {
6903 + wp_send_json_error('Unauthorized access');
6904 + }
6905 +
6906 + $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
6907 +
6908 + if (empty($queue_id)) {
6909 + wp_send_json_error('Missing queue ID');
6910 + }
6911 +
6912 + global $wpdb;
6913 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6914 + $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
6915 +
6916 + // Delete queue items
6917 + $wpdb->delete(
6918 + $table_name,
6919 + array('queue_id' => $queue_id),
6920 + array('%s')
6921 + );
6922 +
6923 + // Delete queue metadata
6924 + $wpdb->delete(
6925 + $meta_table,
6926 + array('queue_id' => $queue_id),
6927 + array('%s')
6928 + );
6929 +
6930 + wp_send_json_success(array(
6931 + 'message' => 'Queue cleared successfully'
6932 + ));
6933 +}
6934 +
6935 +/**
6936 + * AJAX: Retry failed items in queue
6937 + */
6938 +public function ajax_mxchat_retry_failed() {
6939 + // Verify nonce and permissions
6940 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
6941 +
6942 + if (!current_user_can('manage_options')) {
6943 + wp_send_json_error('Unauthorized access');
6944 + }
6945 +
6946 + $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
6947 +
6948 + if (empty($queue_id)) {
6949 + wp_send_json_error('Missing queue ID');
6950 + }
6951 +
6952 + global $wpdb;
6953 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6954 +
6955 + // Reset failed items to pending and reset attempt count
6956 + $updated = $wpdb->update(
6957 + $table_name,
6958 + array(
6959 + 'status' => 'pending',
6960 + 'attempts' => 0,
6961 + 'error_message' => null
6962 + ),
6963 + array(
6964 + 'queue_id' => $queue_id,
6965 + 'status' => 'failed'
6966 + ),
6967 + array('%s', '%d', '%s'),
6968 + array('%s', '%s')
6969 + );
6970 +
6971 + wp_send_json_success(array(
6972 + 'message' => 'Reset ' . $updated . ' failed items for retry',
6973 + 'reset_count' => $updated
6974 + ));
6975 +}
6976 +
6977 +
6978 +public function ajax_mxchat_mark_queue_complete() {
6979 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
6980 +
6981 + if (!current_user_can('manage_options')) {
6982 + wp_send_json_error('Unauthorized access');
6983 + }
6984 +
6985 + $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
6986 +
6987 + if (empty($queue_id)) {
6988 + wp_send_json_error('Missing queue ID');
6989 + }
6990 +
6991 + // Clear active queue transients
6992 + if (strpos($queue_id, 'sitemap_') === 0) {
6993 + delete_transient('mxchat_active_queue_sitemap');
6994 + } else if (strpos($queue_id, 'pdf_') === 0) {
6995 + delete_transient('mxchat_active_queue_pdf');
6996 + }
6997 +
6998 + wp_send_json_success(array('message' => 'Queue marked as complete'));
6999 +}
7000 +
7001 +
7002 + // ========================================
7003 + // STATIC ACCESS METHODS
7004 + // ========================================
7005 +
7006 + /**
7007 + * Get singleton instance
7008 + */
7009 + public static function get_instance() {
7010 + static $instance = null;
7011 + if ($instance === null) {
7012 + $instance = new self();
7013 + }
7014 + return $instance;
7015 + }
7016 +}
7017 +
7018 +// Initialize the Knowledge manager
7931 7019 $mxchat_knowledge_manager = MxChat_Knowledge_Manager::get_instance();