PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.1
MxChat – AI Chatbot & Content Generation for WordPress v3.1.1
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 +7378 -8161 3.2.63.1.1 View file →
@@ -1,8162 +1,7379 @@
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 - // 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 - // Check Content-Disposition header for .pdf filename (Google Drive sends this)
165 - $disposition = wp_remote_retrieve_header($response, 'content-disposition');
166 - $has_pdf_disposition = ! empty($disposition) && stripos($disposition, '.pdf') !== false;
167 -
168 - return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf' || $has_pdf_disposition;
169 -}
170 -
171 -
172 -public function mxchat_handle_pdf_for_knowledge_base($pdf_url, $response, $bot_id = 'default') {
173 - if (!current_user_can('manage_options')) {
174 - return false;
175 - }
176 -
177 - $pdf_url = esc_url_raw($pdf_url);
178 - $upload_dir = wp_upload_dir();
179 -
180 - if (isset($upload_dir['error']) && $upload_dir['error'] !== false) {
181 - return false;
182 - }
183 -
184 - $pdf_filename = sanitize_file_name('mxchat_kb_' . time() . '.pdf');
185 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
186 -
187 - $response_body = wp_remote_retrieve_body($response);
188 - if (empty($response_body)) {
189 - return false;
190 - }
191 -
192 - if (!wp_mkdir_p(dirname($pdf_path))) {
193 - return false;
194 - }
195 -
196 - try {
197 - file_put_contents($pdf_path, $response_body);
198 -
199 - if (!file_exists($pdf_path)) {
200 - throw new Exception(__('Failed to save PDF file', 'mxchat'));
201 - }
202 -
203 - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
204 -
205 - if ($total_pages === false || $total_pages < 1) {
206 - throw new Exception(__('Invalid PDF: Unable to parse or no pages found', 'mxchat'));
207 - }
208 -
209 - // Create unique queue ID
210 - $queue_id = 'pdf_' . md5($pdf_url . time());
211 -
212 - // Create array of pages to process
213 - $pages = array();
214 - for ($i = 1; $i <= $total_pages; $i++) {
215 - $pages[] = array(
216 - 'pdf_path' => $pdf_path,
217 - 'pdf_url' => $pdf_url,
218 - 'page_number' => $i,
219 - 'total_pages' => $total_pages
220 - );
221 - }
222 -
223 - // Add pages to queue
224 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
225 -
226 - if ($queued_count === 0) {
227 - wp_delete_file($pdf_path);
228 - throw new Exception(__('Failed to add PDF pages to processing queue', 'mxchat'));
229 - }
230 -
231 - // Store queue metadata
232 - $this->mxchat_set_queue_meta($queue_id, 'source_url', $pdf_url);
233 - $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'pdf');
234 - $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_pages);
235 - $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
236 - $this->mxchat_set_queue_meta($queue_id, 'pdf_path', $pdf_path);
237 - $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
238 -
239 - // Store queue ID in transient for status tracking
240 - set_transient('mxchat_active_queue_pdf', $queue_id, DAY_IN_SECONDS);
241 - set_transient('mxchat_last_pdf_url', $pdf_url, DAY_IN_SECONDS);
242 -
243 - return 'queued';
244 -
245 - } catch (Exception $e) {
246 - if (file_exists($pdf_path)) {
247 - wp_delete_file($pdf_path);
248 - }
249 - return $e->getMessage();
250 - }
251 -}
252 -
253 -/**
254 - * Handle direct PDF file upload from the knowledge base page
255 - */
256 -public function mxchat_handle_pdf_file_submission() {
257 - if (!isset($_POST['submit_pdf_file']) || !current_user_can('manage_options')) {
258 - wp_die(esc_html__('Unauthorized access', 'mxchat'));
259 - }
260 -
261 - check_admin_referer('mxchat_submit_pdf_file_action', 'mxchat_submit_pdf_file_nonce');
262 -
263 - $redirect_url = admin_url('admin.php?page=mxchat-prompts');
264 -
265 - // Validate file upload
266 - if (empty($_FILES['pdf_file']) || $_FILES['pdf_file']['error'] !== UPLOAD_ERR_OK) {
267 - $error_code = isset($_FILES['pdf_file']['error']) ? $_FILES['pdf_file']['error'] : UPLOAD_ERR_NO_FILE;
268 - $error_messages = array(
269 - UPLOAD_ERR_INI_SIZE => __('The uploaded file exceeds the server upload_max_filesize limit.', 'mxchat'),
270 - UPLOAD_ERR_FORM_SIZE => __('The uploaded file exceeds the form MAX_FILE_SIZE limit.', 'mxchat'),
271 - UPLOAD_ERR_PARTIAL => __('The file was only partially uploaded.', 'mxchat'),
272 - UPLOAD_ERR_NO_FILE => __('No file was uploaded. Please select a PDF file.', 'mxchat'),
273 - UPLOAD_ERR_NO_TMP_DIR => __('Server missing temporary folder.', 'mxchat'),
274 - UPLOAD_ERR_CANT_WRITE => __('Server failed to write file to disk.', 'mxchat'),
275 - );
276 - $error_msg = isset($error_messages[$error_code]) ? $error_messages[$error_code] : __('Unknown upload error.', 'mxchat');
277 - set_transient('mxchat_admin_notice_error', $error_msg, 30);
278 - wp_safe_redirect(esc_url($redirect_url));
279 - exit;
280 - }
281 -
282 - $file = $_FILES['pdf_file'];
283 -
284 - // Validate MIME type
285 - $finfo = finfo_open(FILEINFO_MIME_TYPE);
286 - $mime_type = finfo_file($finfo, $file['tmp_name']);
287 - finfo_close($finfo);
288 -
289 - if ($mime_type !== 'application/pdf') {
290 - set_transient('mxchat_admin_notice_error',
291 - esc_html__('Invalid file type. Only PDF files are accepted.', 'mxchat'),
292 - 30
293 - );
294 - wp_safe_redirect(esc_url($redirect_url));
295 - exit;
296 - }
297 -
298 - // Validate extension
299 - $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
300 - if ($ext !== 'pdf') {
301 - set_transient('mxchat_admin_notice_error',
302 - esc_html__('Invalid file extension. Only .pdf files are accepted.', 'mxchat'),
303 - 30
304 - );
305 - wp_safe_redirect(esc_url($redirect_url));
306 - exit;
307 - }
308 -
309 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
310 - $original_filename = sanitize_file_name($file['name']);
311 -
312 - $upload_dir = wp_upload_dir();
313 - if (isset($upload_dir['error']) && $upload_dir['error'] !== false) {
314 - set_transient('mxchat_admin_notice_error',
315 - esc_html__('WordPress upload directory is not writable.', 'mxchat'),
316 - 30
317 - );
318 - wp_safe_redirect(esc_url($redirect_url));
319 - exit;
320 - }
321 -
322 - $pdf_filename = sanitize_file_name('mxchat_kb_' . time() . '.pdf');
323 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
324 -
325 - if (!wp_mkdir_p(dirname($pdf_path))) {
326 - set_transient('mxchat_admin_notice_error',
327 - esc_html__('Failed to create upload directory.', 'mxchat'),
328 - 30
329 - );
330 - wp_safe_redirect(esc_url($redirect_url));
331 - exit;
332 - }
333 -
334 - // Move uploaded file
335 - if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
336 - set_transient('mxchat_admin_notice_error',
337 - esc_html__('Failed to save uploaded PDF file.', 'mxchat'),
338 - 30
339 - );
340 - wp_safe_redirect(esc_url($redirect_url));
341 - exit;
342 - }
343 -
344 - try {
345 - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
346 -
347 - if ($total_pages === false || $total_pages < 1) {
348 - throw new Exception(__('Invalid PDF: Unable to parse or no pages found', 'mxchat'));
349 - }
350 -
351 - // Use original filename as the source identifier
352 - $source_label = 'upload://' . $original_filename;
353 -
354 - $queue_id = 'pdf_' . md5($source_label . time());
355 -
356 - $pages = array();
357 - for ($i = 1; $i <= $total_pages; $i++) {
358 - $pages[] = array(
359 - 'pdf_path' => $pdf_path,
360 - 'pdf_url' => $source_label,
361 - 'page_number' => $i,
362 - 'total_pages' => $total_pages,
363 - );
364 - }
365 -
366 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
367 -
368 - if ($queued_count === 0) {
369 - wp_delete_file($pdf_path);
370 - throw new Exception(__('Failed to add PDF pages to processing queue', 'mxchat'));
371 - }
372 -
373 - $this->mxchat_set_queue_meta($queue_id, 'source_url', $source_label);
374 - $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'pdf');
375 - $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_pages);
376 - $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
377 - $this->mxchat_set_queue_meta($queue_id, 'pdf_path', $pdf_path);
378 - $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
379 -
380 - set_transient('mxchat_active_queue_pdf', $queue_id, DAY_IN_SECONDS);
381 - set_transient('mxchat_last_pdf_url', $source_label, DAY_IN_SECONDS);
382 -
383 - set_transient('mxchat_admin_notice_success',
384 - sprintf(
385 - esc_html__('PDF "%s" (%d pages) queued for processing. Processing will start automatically.', 'mxchat'),
386 - esc_html($original_filename),
387 - $total_pages
388 - ),
389 - 30
390 - );
391 -
392 - } catch (Exception $e) {
393 - if (file_exists($pdf_path)) {
394 - wp_delete_file($pdf_path);
395 - }
396 - set_transient('mxchat_admin_notice_error',
397 - esc_html__('Failed to process uploaded PDF: ', 'mxchat') . esc_html($e->getMessage()),
398 - 30
399 - );
400 - }
401 -
402 - wp_safe_redirect(esc_url($redirect_url));
403 - exit;
404 -}
405 -
406 -/**
407 - * Validate PDF and count pages with multiple parser attempts
408 - */
409 -private function mxchat_validate_and_count_pdf_pages($pdf_path) {
410 - // Method 1: Try with Smalot PDF Parser (your current method)
411 - try {
412 - mxchat_load_pdf_parser();
413 - $parser = new \Smalot\PdfParser\Parser();
414 - $pdf = $parser->parseFile($pdf_path);
415 - $pages = $pdf->getPages();
416 - $page_count = count($pages);
417 -
418 - if ($page_count > 0) {
419 - //error_log('PDF parsed successfully with Smalot parser: ' . $page_count . ' pages');
420 - return $page_count;
421 - }
422 - } catch (Exception $e) {
423 - //error_log('Smalot PDF parser failed: ' . $e->getMessage());
424 - }
425 -
426 - // Method 2: Try with pdfinfo command (if available)
427 - if (function_exists('shell_exec') && !$this->mxchat_is_shell_disabled()) {
428 - try {
429 - $command = 'pdfinfo ' . escapeshellarg($pdf_path) . ' 2>&1';
430 - $output = shell_exec($command);
431 -
432 - if ($output && preg_match('/Pages:\s*(\d+)/', $output, $matches)) {
433 - $page_count = intval($matches[1]);
434 - if ($page_count > 0) {
435 - //error_log('PDF parsed successfully with pdfinfo: ' . $page_count . ' pages');
436 - return $page_count;
437 - }
438 - }
439 - } catch (Exception $e) {
440 - //error_log('pdfinfo command failed: ' . $e->getMessage());
441 - }
442 - }
443 -
444 - // Method 3: Try to repair PDF and parse again
445 - try {
446 - $repaired_path = $this->mxchat_attempt_pdf_repair($pdf_path);
447 - if ($repaired_path && $repaired_path !== $pdf_path) {
448 - mxchat_load_pdf_parser();
449 - $parser = new \Smalot\PdfParser\Parser();
450 - $pdf = $parser->parseFile($repaired_path);
451 - $pages = $pdf->getPages();
452 - $page_count = count($pages);
453 -
454 - if ($page_count > 0) {
455 - // Replace original with repaired version
456 - copy($repaired_path, $pdf_path);
457 - unlink($repaired_path);
458 - //error_log('PDF repaired and parsed successfully: ' . $page_count . ' pages');
459 - return $page_count;
460 - }
461 -
462 - // Clean up repaired file if it didn't work
463 - unlink($repaired_path);
464 - }
465 - } catch (Exception $e) {
466 - //error_log('PDF repair attempt failed: ' . $e->getMessage());
467 - }
468 -
469 - // Method 4: Manual PDF structure analysis (basic page count)
470 - try {
471 - $page_count = $this->mxchat_manual_pdf_page_count($pdf_path);
472 - if ($page_count > 0) {
473 - //error_log('PDF page count determined manually: ' . $page_count . ' pages');
474 - return $page_count;
475 - }
476 - } catch (Exception $e) {
477 - //error_log('Manual PDF analysis failed: ' . $e->getMessage());
478 - }
479 -
480 - //error_log('All PDF parsing methods failed for: ' . $pdf_path);
481 - return false;
482 -}
483 -
484 -/**
485 - * Check if shell_exec is disabled
486 - */
487 -private function mxchat_is_shell_disabled() {
488 - $disabled = explode(',', ini_get('disable_functions'));
489 - return in_array('shell_exec', $disabled);
490 -}
491 -
492 -/**
493 - * Attempt to repair PDF using basic methods
494 - */
495 -private function mxchat_attempt_pdf_repair($pdf_path) {
496 - try {
497 - $content = file_get_contents($pdf_path);
498 - if (!$content) {
499 - return false;
500 - }
501 -
502 - // Check if PDF starts with proper header
503 - if (substr($content, 0, 4) !== '%PDF') {
504 - // Try to find PDF header in the content
505 - $header_pos = strpos($content, '%PDF');
506 - if ($header_pos !== false && $header_pos < 1024) {
507 - // Remove junk before PDF header
508 - $content = substr($content, $header_pos);
509 - $repaired_path = $pdf_path . '.repaired';
510 - file_put_contents($repaired_path, $content);
511 - return $repaired_path;
512 - }
513 - }
514 -
515 - // Check for EOF marker
516 - $content = rtrim($content);
517 - if (!preg_match('/%%EOF\s*$/', $content)) {
518 - // Add EOF marker if missing
519 - $content .= "\n%%EOF";
520 - $repaired_path = $pdf_path . '.repaired';
521 - file_put_contents($repaired_path, $content);
522 - return $repaired_path;
523 - }
524 -
525 - } catch (Exception $e) {
526 - //error_log('PDF repair error: ' . $e->getMessage());
527 - }
528 -
529 - return false;
530 -}
531 -
532 -/**
533 - * Manual PDF page counting by analyzing PDF structure
534 - */
535 -private function mxchat_manual_pdf_page_count($pdf_path) {
536 - try {
537 - $content = file_get_contents($pdf_path);
538 - if (!$content) {
539 - return 0;
540 - }
541 -
542 - // Method 1: Count /Type /Page objects
543 - $page_count = preg_match_all('/\/Type\s*\/Page[^s]/', $content);
544 - if ($page_count > 0) {
545 - return $page_count;
546 - }
547 -
548 - // Method 2: Look for /Count in pages object
549 - if (preg_match('/\/Type\s*\/Pages.*?\/Count\s+(\d+)/', $content, $matches)) {
550 - return intval($matches[1]);
551 - }
552 -
553 - // Method 3: Count page references
554 - $page_count = preg_match_all('/\d+\s+0\s+obj\s*<<[^>]*\/Type\s*\/Page/', $content);
555 - if ($page_count > 0) {
556 - return $page_count;
557 - }
558 -
559 - } catch (Exception $e) {
560 - //error_log('Manual PDF analysis error: ' . $e->getMessage());
561 - }
562 -
563 - return 0;
564 -}
565 -
566 -
567 -public function mxchat_save_inline_prompt() {
568 - // DEBUG: Log what we're receiving
569 - //error_log('=== MXCHAT DEBUG ===');
570 - //error_log('POST data: ' . print_r($_POST, true));
571 - //error_log('Nonce from POST: ' . ($_POST['_ajax_nonce'] ?? 'NOT FOUND'));
572 -
573 - // Check for nonce security
574 - check_ajax_referer('mxchat_save_inline_nonce', '_ajax_nonce');
575 -
576 - // If we get here, nonce passed
577 - //error_log('Nonce verification PASSED');
578 -
579 - // Verify permissions
580 - if (!current_user_can('manage_options')) {
581 - wp_send_json_error(esc_html__('Permission denied.', 'mxchat'));
582 - return;
583 - }
584 -
585 - global $wpdb;
586 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
587 -
588 - // Validate and sanitize input data
589 - $prompt_id = isset($_POST['id']) ? absint($_POST['id']) : 0;
590 - $article_content = isset($_POST['article_content']) ? wp_kses_post(wp_unslash($_POST['article_content'])) : '';
591 - $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
592 -
593 - if ($prompt_id > 0 && !empty($article_content)) {
594 - // Re-generate the embedding vector for the updated content
595 - $embedding_vector = $this->mxchat_generate_embedding($article_content);
596 - if (is_array($embedding_vector)) {
597 - // Serialize the embedding vector before storing it
598 - $embedding_vector_serialized = serialize($embedding_vector);
599 - // Update the prompt in the database
600 - $updated = $wpdb->update(
601 - $table_name,
602 - array(
603 - 'article_content' => $article_content,
604 - 'embedding_vector' => $embedding_vector_serialized,
605 - 'source_url' => $article_url,
606 - ),
607 - array('id' => $prompt_id),
608 - array('%s', '%s', '%s'),
609 - array('%d')
610 - );
611 - if ($updated !== false) {
612 - wp_send_json_success();
613 - } else {
614 - MxChat_Admin::mxchat_log_debug('knowledge_error', 'Database update failed when saving knowledge entry');
615 - wp_send_json_error(esc_html__('Database update failed.', 'mxchat'));
616 - }
617 - } else {
618 - MxChat_Admin::mxchat_log_debug('embedding_error', 'Embedding generation failed for knowledge entry');
619 - wp_send_json_error(esc_html__('Embedding generation failed.', 'mxchat'));
620 - }
621 - } else {
622 - wp_send_json_error(esc_html__('Invalid data.', 'mxchat'));
623 - }
624 -}
625 -
626 -
627 -/**
628 - * AJAX: Get full content for editing — reassembles chunks if needed.
629 - * Works for both WordPress DB and Pinecone entries.
630 - */
631 -public function ajax_mxchat_get_entry_content() {
632 - check_ajax_referer('mxchat_edit_entry_nonce', 'nonce');
633 -
634 - if ( ! current_user_can('manage_options') ) {
635 - wp_send_json_error( array( 'message' => 'Permission denied.' ) );
636 - }
637 -
638 - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : '';
639 - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0;
640 - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress';
641 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
642 -
643 - if ( $data_source === 'pinecone' ) {
644 - // Pinecone: fetch vectors by source_url, reassemble chunks
645 - $content = $this->get_pinecone_entry_content( $source_url, $entry_id, $bot_id );
646 - } else {
647 - // WordPress DB
648 - $content = $this->get_wordpress_entry_content( $source_url, $entry_id );
649 - }
650 -
651 - if ( is_wp_error( $content ) ) {
652 - wp_send_json_error( array( 'message' => $content->get_error_message() ) );
653 - }
654 -
655 - wp_send_json_success( $content );
656 -}
657 -
658 -/**
659 - * Get content from WordPress DB — reassembles chunks by source_url.
660 - */
661 -private function get_wordpress_entry_content( $source_url, $entry_id ) {
662 - global $wpdb;
663 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
664 -
665 - // If we have a source_url, check for chunks
666 - if ( ! empty( $source_url ) && strpos( $source_url, 'mxchat://' ) !== 0 ) {
667 - $rows = $wpdb->get_results( $wpdb->prepare(
668 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE source_url = %s ORDER BY id ASC",
669 - $source_url
670 - ) );
671 -
672 - if ( $rows && count( $rows ) > 1 ) {
673 - // Multiple rows = chunked. Reassemble.
674 - $chunks = array();
675 - foreach ( $rows as $row ) {
676 - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content );
677 - $index = isset( $parsed['metadata']['chunk_index'] ) ? intval( $parsed['metadata']['chunk_index'] ) : count( $chunks );
678 - $chunks[ $index ] = $parsed['text'];
679 - }
680 - ksort( $chunks );
681 - return array(
682 - 'content' => implode( "\n\n", $chunks ),
683 - 'source_url' => $source_url,
684 - 'is_chunked' => true,
685 - 'chunk_count' => count( $chunks ),
686 - 'content_type' => $rows[0]->content_type,
687 - );
688 - } elseif ( $rows && count( $rows ) === 1 ) {
689 - $parsed = MxChat_Chunker::parse_stored_chunk( $rows[0]->article_content );
690 - return array(
691 - 'content' => $parsed['text'],
692 - 'source_url' => $source_url,
693 - 'entry_id' => $rows[0]->id,
694 - 'is_chunked' => false,
695 - 'content_type' => $rows[0]->content_type,
696 - );
697 - }
698 - }
699 -
700 - // Fallback: fetch by ID
701 - if ( $entry_id > 0 ) {
702 - $row = $wpdb->get_row( $wpdb->prepare(
703 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE id = %d",
704 - $entry_id
705 - ) );
706 - if ( $row ) {
707 - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content );
708 - return array(
709 - 'content' => $parsed['text'],
710 - 'source_url' => $row->source_url,
711 - 'entry_id' => $row->id,
712 - 'is_chunked' => false,
713 - 'content_type' => $row->content_type,
714 - );
715 - }
716 - }
717 -
718 - return new WP_Error( 'not_found', 'Entry not found.' );
719 -}
720 -
721 -/**
722 - * Get content from Pinecone — fetches vectors by source_url, reassembles chunks.
723 - */
724 -private function get_pinecone_entry_content( $source_url, $entry_id, $bot_id ) {
725 - if ( ! class_exists('MxChat_Pinecone_Manager') ) {
726 - return new WP_Error( 'pinecone_unavailable', 'Pinecone manager not available.' );
727 - }
728 -
729 - // Get Pinecone config
730 - if ( $bot_id === 'default' || ! class_exists('MxChat_Multi_Bot_Manager') ) {
731 - $pinecone_options = get_option('mxchat_pinecone_addon_options');
732 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
733 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
734 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
735 - } else {
736 - $bot_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
737 - $api_key = $bot_config['api_key'] ?? '';
738 - $host = $bot_config['host'] ?? '';
739 - $namespace = $bot_config['namespace'] ?? '';
740 - }
741 -
742 - if ( empty($host) || empty($api_key) ) {
743 - return new WP_Error( 'pinecone_config', 'Pinecone not configured.' );
744 - }
745 -
746 - // List vectors with the source_url prefix
747 - $base_id = md5( $source_url );
748 - $vector_ids = array( $base_id );
749 -
750 - // Find chunk vectors
751 - $list_url = "https://{$host}/vectors/list";
752 - $list_body = array( 'prefix' => $base_id . '_chunk_', 'limit' => 100 );
753 - if ( ! empty($namespace) ) {
754 - $list_body['namespace'] = $namespace;
755 - }
756 -
757 - $list_resp = wp_remote_post( $list_url, array(
758 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
759 - 'body' => wp_json_encode( $list_body ),
760 - 'timeout' => 15,
761 - ) );
762 -
763 - if ( ! is_wp_error($list_resp) ) {
764 - $list_data = json_decode( wp_remote_retrieve_body($list_resp), true );
765 - if ( ! empty($list_data['vectors']) ) {
766 - foreach ( $list_data['vectors'] as $v ) {
767 - $vector_ids[] = $v['id'];
768 - }
769 - }
770 - }
771 -
772 - // Fetch vectors with metadata
773 - $fetch_url = "https://{$host}/vectors/fetch";
774 - $fetch_body = array( 'ids' => $vector_ids );
775 - if ( ! empty($namespace) ) {
776 - $fetch_body['namespace'] = $namespace;
777 - }
778 -
779 - $fetch_resp = wp_remote_post( $fetch_url, array(
780 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
781 - 'body' => wp_json_encode( $fetch_body ),
782 - 'timeout' => 15,
783 - ) );
784 -
785 - if ( is_wp_error($fetch_resp) ) {
786 - return new WP_Error( 'pinecone_fetch', 'Failed to fetch from Pinecone.' );
787 - }
788 -
789 - $fetch_data = json_decode( wp_remote_retrieve_body($fetch_resp), true );
790 - $vectors = $fetch_data['vectors'] ?? array();
791 -
792 - if ( empty($vectors) ) {
793 - return new WP_Error( 'not_found', 'Entry not found in Pinecone.' );
794 - }
795 -
796 - // Reassemble chunks
797 - $chunks = array();
798 - $content_type = 'content';
799 - foreach ( $vectors as $vid => $vector ) {
800 - $meta = $vector['metadata'] ?? array();
801 - $text = $meta['text'] ?? '';
802 - $index = $meta['chunk_index'] ?? 0;
803 - $content_type = $meta['type'] ?? 'content';
804 - $chunks[ intval($index) ] = $text;
805 - }
806 - ksort( $chunks );
807 -
808 - return array(
809 - 'content' => implode( "\n\n", $chunks ),
810 - 'source_url' => $source_url,
811 - 'is_chunked' => count($chunks) > 1,
812 - 'chunk_count' => count($chunks),
813 - 'content_type' => $content_type,
814 - );
815 -}
816 -
817 -/**
818 - * AJAX: Save edited content — re-chunks and re-embeds as needed.
819 - * Works for both WordPress DB and Pinecone entries.
820 - */
821 -public function ajax_mxchat_save_entry_content() {
822 - check_ajax_referer('mxchat_edit_entry_nonce', 'nonce');
823 -
824 - if ( ! current_user_can('manage_options') ) {
825 - wp_send_json_error( array( 'message' => 'Permission denied.' ) );
826 - }
827 -
828 - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : '';
829 - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0;
830 - $content = isset($_POST['content']) ? wp_kses_post( wp_unslash($_POST['content']) ) : '';
831 - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress';
832 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
833 - $content_type = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : 'content';
834 -
835 - if ( empty($content) ) {
836 - wp_send_json_error( array( 'message' => 'Content cannot be empty.' ) );
837 - }
838 -
839 - // Get the embedding API key
840 - $options = get_option('mxchat_options', array());
841 - $api_key = '';
842 -
843 - if ( $bot_id !== 'default' && class_exists('MxChat_Multi_Bot_Manager') ) {
844 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
845 - $api_key = $bot_options['api_key'] ?? '';
846 - }
847 - if ( empty($api_key) ) {
848 - $api_key = $options['api_key'] ?? '';
849 - }
850 -
851 - global $wpdb;
852 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
853 -
854 - // If source_url is empty but we have an entry_id, look it up
855 - if ( empty($source_url) && $entry_id > 0 && $data_source === 'wordpress' ) {
856 - $row = $wpdb->get_row( $wpdb->prepare( "SELECT source_url FROM {$table} WHERE id = %d", $entry_id ) );
857 - if ( $row && ! empty($row->source_url) ) {
858 - $source_url = $row->source_url;
859 - }
860 - }
861 -
862 - // For manual entries (no source_url or mxchat:// prefix), delete the old entry by ID first
863 - // so submit_content_to_db creates a replacement instead of a duplicate
864 - // Also treat legacy mxchat.ai source URLs as manual — old bug assigned the site URL to manual entries
865 - $is_legacy_manual = !empty($source_url) && strpos($source_url, 'mxchat.ai') !== false && strpos($source_url, 'mxchat://') !== 0;
866 - if ( $entry_id > 0 && (empty($source_url) || strpos($source_url, 'mxchat://') === 0 || $is_legacy_manual) ) {
867 - $wpdb->delete( $table, array( 'id' => $entry_id ), array( '%d' ) );
868 - // Clear legacy URL so submit_content_to_db generates a unique mxchat:// identifier
869 - // instead of reusing the shared URL (which would mass-delete other entries with the same URL)
870 - if ( $is_legacy_manual ) {
871 - $source_url = '';
872 - }
873 - }
874 -
875 - // Use the existing submit_content_to_db which handles chunking, Pinecone, and WP DB
876 - $vector_id = ! empty($source_url) ? md5($source_url) : md5('mxchat_manual_' . $entry_id);
877 -
878 - // submit_content_to_db already handles: delete old chunks → re-chunk → re-embed → store
879 - $result = MxChat_Utils::submit_content_to_db( $content, $source_url, $api_key, $vector_id, $bot_id, $content_type );
880 -
881 - if ( is_wp_error($result) ) {
882 - wp_send_json_error( array( 'message' => $result->get_error_message() ) );
883 - }
884 -
885 - wp_send_json_success( array( 'message' => 'Content saved and re-embedded successfully.' ) );
886 -}
887 -
888 -public function mxchat_get_pdf_processing_status($pdf_url) {
889 - $pdf_url = esc_url_raw($pdf_url);
890 - $status = get_transient(sanitize_key('mxchat_pdf_status_' . md5($pdf_url)));
891 -
892 - if (!$status || !is_array($status)) {
893 - return false;
894 - }
895 -
896 - // Check for stalled processing (no updates for 5 minutes)
897 - if ($status['status'] === 'processing' && (time() - absint($status['last_update'])) > 300) {
898 - $status['status'] = 'error';
899 - $status['error'] = __('PDF processing appears to be stalled. No updates for over 5 minutes.', 'mxchat');
900 -
901 - // Save the updated status
902 - set_transient(
903 - sanitize_key('mxchat_pdf_status_' . md5($pdf_url)),
904 - array_map('sanitize_text_field', $status),
905 - DAY_IN_SECONDS
906 - );
907 - }
908 -
909 - $result = array(
910 - 'total_pages' => absint($status['total_pages']),
911 - 'processed_pages' => absint($status['processed_pages']),
912 - 'failed_pages' => absint($status['failed_pages'] ?? 0),
913 - 'percentage' => ($status['total_pages'] > 0)
914 - ? round((absint($status['processed_pages']) / absint($status['total_pages'])) * 100)
915 - : 0,
916 - 'status' => sanitize_text_field($status['status']),
917 - 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
918 - 'failed_pages_list' => isset($status['failed_pages_list']) ? $status['failed_pages_list'] : array(),
919 - 'completion_summary' => isset($status['completion_summary']) ? $status['completion_summary'] : null
920 - );
921 -
922 - // Add error message if present
923 - if (isset($status['error']) && !empty($status['error'])) {
924 - $result['error'] = sanitize_text_field($status['error']);
925 - }
926 -
927 - return $result;
928 -}
929 -
930 -
931 -public function mxchat_handle_sitemap_submission() {
932 - // Check if the form was submitted and verify permissions
933 - if (!isset($_POST['submit_sitemap']) || !current_user_can('manage_options')) {
934 - wp_die(esc_html__('Unauthorized access', 'mxchat'));
935 - }
936 -
937 - // Verify nonce
938 - check_admin_referer('mxchat_submit_sitemap_action', 'mxchat_submit_sitemap_nonce');
939 -
940 - // Validate URL
941 - if (!isset($_POST['sitemap_url']) || empty($_POST['sitemap_url'])) {
942 - set_transient('mxchat_admin_notice_error',
943 - esc_html__('Please provide a valid URL.', 'mxchat'),
944 - 30
945 - );
946 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
947 - exit;
948 - }
949 -
950 - $submitted_url = esc_url_raw($_POST['sitemap_url']);
951 -
952 - // Convert Google Drive sharing URLs to direct download URLs
953 - if ( strpos($submitted_url, 'drive.google.com') !== false ) {
954 - $file_id = '';
955 - if ( preg_match('/[?&]id=([a-zA-Z0-9_-]+)/', $submitted_url, $m) ) {
956 - $file_id = $m[1];
957 - } elseif ( preg_match('#/file/d/([a-zA-Z0-9_-]+)#', $submitted_url, $m) ) {
958 - $file_id = $m[1];
959 - }
960 - if ( ! empty($file_id) ) {
961 - $submitted_url = 'https://drive.google.com/uc?export=download&id=' . $file_id;
962 - }
963 - }
964 -
965 - // Get bot_id from form submission
966 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
967 -
968 - // Get bot-specific options and validate API key
969 - $bot_options = $this->get_bot_options($bot_id);
970 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
971 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
972 -
973 - if (strpos($selected_model, 'voyage') === 0) {
974 - $api_key = $options['voyage_api_key'] ?? '';
975 - $provider_name = 'Voyage AI';
976 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
977 - $api_key = $options['gemini_api_key'] ?? '';
978 - $provider_name = 'Google Gemini';
979 - } else {
980 - $api_key = $options['api_key'] ?? '';
981 - $provider_name = 'OpenAI';
982 - }
983 -
984 - if (empty($api_key)) {
985 - $error_message = sprintf(
986 - esc_html__('%s API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
987 - $provider_name
988 - );
989 - set_transient('mxchat_admin_notice_error', $error_message, 30);
990 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
991 - exit;
992 - }
993 -
994 - // Fetch URL — use browser-like headers so servers with bot protection don't block us
995 - $response = wp_remote_get($submitted_url, array(
996 - 'timeout' => 30,
997 - 'sslverify' => false,
998 - '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',
999 - 'headers' => array(
1000 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
1001 - 'Accept-Language' => 'en-US,en;q=0.9',
1002 - ),
1003 - ));
1004 -
1005 - if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1006 - $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response);
1007 - set_transient('mxchat_admin_notice_error',
1008 - sprintf(
1009 - esc_html__('Failed to fetch the URL: %s', 'mxchat'),
1010 - esc_html($error_message)
1011 - ),
1012 - 30
1013 - );
1014 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1015 - exit;
1016 - }
1017 -
1018 - $content_type = wp_remote_retrieve_header($response, 'content-type');
1019 - $body_content = wp_remote_retrieve_body($response);
1020 -
1021 - if (empty($body_content)) {
1022 - set_transient('mxchat_admin_notice_error',
1023 - esc_html__('Empty response received from URL.', 'mxchat'),
1024 - 30
1025 - );
1026 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1027 - exit;
1028 - }
1029 -
1030 - // Handle PDF URL
1031 - if ($this->mxchat_is_pdf_url($submitted_url, $response)) {
1032 - $result = $this->mxchat_handle_pdf_for_knowledge_base($submitted_url, $response, $bot_id);
1033 -
1034 - if ($result === 'queued') {
1035 - set_transient('mxchat_admin_notice_success',
1036 - esc_html__('PDF queued for processing. Processing will start automatically.', 'mxchat'),
1037 - 30
1038 - );
1039 - } else {
1040 - set_transient('mxchat_admin_notice_error',
1041 - esc_html__('Failed to queue PDF processing: ', 'mxchat') . esc_html($result),
1042 - 30
1043 - );
1044 - }
1045 -
1046 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1047 - exit;
1048 - }
1049 -
1050 - // Handle Sitemap XML
1051 - if (strpos($content_type, 'xml') !== false || strpos($body_content, '<urlset') !== false) {
1052 - libxml_use_internal_errors(true);
1053 - $xml = simplexml_load_string($body_content);
1054 - $xml_errors = libxml_get_errors();
1055 - libxml_clear_errors();
1056 -
1057 - if ($xml === false || !empty($xml_errors)) {
1058 - set_transient('mxchat_admin_notice_error',
1059 - esc_html__('Invalid sitemap XML. Please provide a valid sitemap.', 'mxchat'),
1060 - 30
1061 - );
1062 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1063 - exit;
1064 - }
1065 -
1066 - $result = $this->mxchat_handle_sitemap_for_knowledge_base($xml, $submitted_url, $bot_id);
1067 -
1068 - if ($result === 'queued') {
1069 - set_transient('mxchat_admin_notice_success',
1070 - esc_html__('Sitemap queued for processing. Processing will start automatically.', 'mxchat'),
1071 - 30
1072 - );
1073 - } else {
1074 - set_transient('mxchat_admin_notice_error',
1075 - esc_html__('Failed to queue sitemap processing. Please check the status below for details.', 'mxchat'),
1076 - 30
1077 - );
1078 - }
1079 -
1080 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1081 - exit;
1082 - }
1083 -
1084 - // Handle Regular URL (single page)
1085 - $page_content = $this->mxchat_extract_main_content($body_content);
1086 - $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
1087 -
1088 - //error_log('[MXCHAT-URL-DEBUG] URL: ' . $submitted_url);
1089 - //error_log('[MXCHAT-URL-DEBUG] Extracted page_content length: ' . strlen($page_content));
1090 - //error_log('[MXCHAT-URL-DEBUG] Sanitized content length: ' . strlen($sanitized_content));
1091 - //error_log('[MXCHAT-URL-DEBUG] Content preview: ' . substr($sanitized_content, 0, 500));
1092 -
1093 - if (empty($sanitized_content)) {
1094 - set_transient('mxchat_admin_notice_error',
1095 - esc_html__('No valid content found on the provided URL.', 'mxchat'),
1096 - 30
1097 - );
1098 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1099 - exit;
1100 - }
1101 -
1102 - // For single URLs, process immediately using submit_content_to_db
1103 - // This handles chunking automatically for large content
1104 - $db_result = MxChat_Utils::submit_content_to_db(
1105 - $sanitized_content,
1106 - $submitted_url,
1107 - $api_key,
1108 - null,
1109 - $bot_id,
1110 - 'url' // content_type
1111 - );
1112 -
1113 - if (is_wp_error($db_result)) {
1114 - $error_message = esc_html__('Failed to store content in database: ', 'mxchat') . esc_html($db_result->get_error_message());
1115 - set_transient('mxchat_admin_notice_error', $error_message, 30);
1116 - } else {
1117 - $success_message = esc_html__('URL content successfully submitted!', 'mxchat');
1118 - set_transient('mxchat_admin_notice_success', $success_message, 30);
1119 - }
1120 -
1121 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1122 - exit;
1123 -}
1124 -
1125 -
1126 -public function mxchat_get_single_url_status() {
1127 - $status = get_transient('mxchat_single_url_status');
1128 - if (!$status) {
1129 - return null;
1130 - }
1131 -
1132 - // Add human-readable time
1133 - if (isset($status['timestamp'])) {
1134 - $status['human_time'] = human_time_diff(strtotime($status['timestamp']), current_time('timestamp')) . ' ' . __('ago', 'mxchat');
1135 - }
1136 -
1137 - return $status;
1138 -}
1139 -
1140 -public function mxchat_handle_sitemap_for_knowledge_base($xml, $sitemap_url, $bot_id = 'default') {
1141 - if (!current_user_can('manage_options')) {
1142 - return false;
1143 - }
1144 -
1145 - try {
1146 - $sitemap_url = esc_url_raw($sitemap_url);
1147 -
1148 - if (!$xml || !is_object($xml)) {
1149 - throw new Exception(__('Invalid XML object provided', 'mxchat'));
1150 - }
1151 -
1152 - // Get bot-specific embedding API for validation
1153 - $bot_options = $this->get_bot_options($bot_id);
1154 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
1155 -
1156 - // Test the embedding API before processing
1157 - $test_phrase = "Test embedding generation for MxChat";
1158 - $test_result = $this->mxchat_generate_embedding($test_phrase, $bot_id);
1159 -
1160 - if (is_string($test_result)) {
1161 - throw new Exception(__('Embedding API validation failed: ', 'mxchat') . $test_result);
1162 - }
1163 -
1164 - if (!is_array($test_result)) {
1165 - throw new Exception(__('Embedding API returned unexpected result type. Please check your configuration.', 'mxchat'));
1166 - }
1167 -
1168 - // Extract URLs from sitemap
1169 - $urls = array();
1170 - foreach ($xml->url as $url_element) {
1171 - $url = esc_url_raw((string)$url_element->loc);
1172 - if ($url) {
1173 - $urls[] = array('url' => $url);
1174 - }
1175 - }
1176 -
1177 - $total_urls = count($urls);
1178 -
1179 - if ($total_urls < 1) {
1180 - throw new Exception(__('No valid URLs found in sitemap', 'mxchat'));
1181 - }
1182 -
1183 - // Create unique queue ID
1184 - $queue_id = 'sitemap_' . md5($sitemap_url . time());
1185 -
1186 - // Add URLs to queue
1187 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'url', $urls, $bot_id);
1188 -
1189 - if ($queued_count === 0) {
1190 - throw new Exception(__('Failed to add URLs to processing queue', 'mxchat'));
1191 - }
1192 -
1193 - // Store queue metadata
1194 - $this->mxchat_set_queue_meta($queue_id, 'source_url', $sitemap_url);
1195 - $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'sitemap');
1196 - $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_urls);
1197 - $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
1198 - $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
1199 -
1200 - // Store queue ID in transient for status tracking
1201 - set_transient('mxchat_active_queue_sitemap', $queue_id, DAY_IN_SECONDS);
1202 - set_transient('mxchat_last_sitemap_url', $sitemap_url, DAY_IN_SECONDS);
1203 -
1204 - return 'queued';
1205 -
1206 - } catch (Exception $e) {
1207 - $error_message = $e->getMessage();
1208 - //error_log(sprintf(esc_html__('Error preparing sitemap for processing: %s', 'mxchat'), esc_html($error_message)));
1209 -
1210 - return $error_message;
1211 - }
1212 -
1213 -}
1214 -
1215 -/**
1216 - * Remove shortcode tags but preserve the content inside them
1217 - * Example: [vc_column]Hello World[/vc_column] becomes "Hello World"
1218 - *
1219 - * @param string $content The content containing shortcodes
1220 - * @return string Content with shortcode tags removed but inner content preserved
1221 - */
1222 -private function strip_shortcode_tags_preserve_content($content) {
1223 - // Single-pass regex removes all shortcode brackets: [tag], [tag attr="val"], [/tag], [tag /]
1224 - // Content between tags is inherently preserved since only brackets are targeted
1225 - $result = preg_replace('/\[\/?\w[\w-]*[^\]]*\]/', '', $content);
1226 - return ($result !== null) ? $result : $content;
1227 -}
1228 -
1229 -public function mxchat_sanitize_content_for_api($content) {
1230 - //error_log('[MXCHAT-SANITIZE] Original content preview: ' . substr($content, 0, 500) . '...');
1231 -
1232 - // Remove shortcode tags but PRESERVE content inside them
1233 - $content = $this->strip_shortcode_tags_preserve_content($content);
1234 -
1235 - // Remove script, style tags, and HTML comments
1236 - $content = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $content);
1237 - $content = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $content);
1238 - $content = preg_replace('/<!--(.|\s)*?-->/', '', $content);
1239 -
1240 - // Remove all HTML tags and decode HTML entities
1241 - $content = wp_strip_all_tags($content);
1242 - $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5);
1243 -
1244 - // Normalize whitespace but preserve paragraph breaks
1245 - // First, normalize line endings to \n
1246 - $content = str_replace(["\r\n", "\r"], "\n", $content);
1247 - // Replace multiple spaces/tabs with single space, but preserve newlines
1248 - $content = preg_replace('/[ \t]+/', ' ', $content);
1249 - // Replace 3+ newlines with 2 newlines (max 2 blank lines)
1250 - $content = preg_replace('/\n{3,}/', "\n\n", $content);
1251 - // Trim each line
1252 - $lines = explode("\n", $content);
1253 - $lines = array_map('trim', $lines);
1254 - $content = implode("\n", $lines);
1255 - // Final trim
1256 - $content = trim($content);
1257 -
1258 - // Remove control characters (which can cause database issues) but preserve \n (0x0A) and \r (0x0D)
1259 - $content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $content);
1260 -
1261 - // Remove NULL bytes which can cause database errors
1262 - $content = str_replace("\0", "", $content);
1263 -
1264 - // Ensure valid UTF-8 encoding
1265 - $content = wp_check_invalid_utf8($content);
1266 -
1267 - // Remove any extremely long strings without spaces (often garbage)
1268 - $content = preg_replace('/\S{300,}/', ' ', $content);
1269 -
1270 - // Replace problematic characters that often cause database issues
1271 - $content = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $content); // Remove emoji and other high Unicode characters
1272 -
1273 - // Replace any remaining potentially problematic characters with spaces
1274 - // BUT preserve newlines by temporarily replacing them
1275 - $content = str_replace("\n", "NEWLINE_PLACEHOLDER", $content);
1276 - $content = preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\p{Sm}]/u', ' ', $content);
1277 - $content = str_replace("NEWLINE_PLACEHOLDER", "\n", $content);
1278 -
1279 - // Limit to reasonable length if needed
1280 - $max_length = 65000; // Just under MySQL TEXT field limit
1281 - if (strlen($content) > $max_length) {
1282 - $content = substr($content, 0, $max_length);
1283 - }
1284 -
1285 - //error_log('[MXCHAT-SANITIZE] Sanitized content preview: ' . substr($content, 0, 500) . '...');
1286 - return $content;
1287 -}
1288 -public function mxchat_extract_main_content($html) {
1289 - if (empty($html)) {
1290 - return '';
1291 - }
1292 - try {
1293 - $dom = new DOMDocument;
1294 - libxml_use_internal_errors(true); // Suppress HTML parsing errors
1295 - @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
1296 - $xpath = new DOMXPath($dom);
1297 -
1298 - // For debugging purposes
1299 - $debugEnabled = true; // Set to true to enable debugging output
1300 - $debug = function($message) use ($debugEnabled) {
1301 - if ($debugEnabled) {
1302 - //error_log('[MXCHAT-EXTRACT-DEBUG] ' . $message);
1303 - }
1304 - };
1305 -
1306 - // Direct targeting for Gerow theme posts
1307 - $post_text = $xpath->query('//div[contains(@class, "post-text")]');
1308 - if ($post_text && $post_text->length > 0) {
1309 - $debug("Found post-text directly");
1310 - $content = '';
1311 - foreach ($post_text as $node) {
1312 - $content .= $dom->saveHTML($node);
1313 - }
1314 - if (!empty($content)) {
1315 - $debug("Returning post-text content");
1316 - return $content;
1317 - }
1318 - }
1319 -
1320 - // Try to get the blog details content which contains the post-text
1321 - $blog_details = $xpath->query('//div[contains(@class, "blog-details-content")]');
1322 - if ($blog_details && $blog_details->length > 0) {
1323 - $debug("Found blog-details-content");
1324 - $content = '';
1325 - foreach ($blog_details as $node) {
1326 - $content .= $dom->saveHTML($node);
1327 - }
1328 - if (!empty($content)) {
1329 - $debug("Returning blog-details-content");
1330 - return $content;
1331 - }
1332 - }
1333 -
1334 - // Try to get the article which contains the blog details
1335 - $article = $xpath->query('//article[contains(@class, "blog-details-wrap")]');
1336 - if ($article && $article->length > 0) {
1337 - $debug("Found article with blog-details-wrap");
1338 - $content = '';
1339 - foreach ($article as $node) {
1340 - $content .= $dom->saveHTML($node);
1341 - }
1342 - if (!empty($content)) {
1343 - $debug("Returning article content");
1344 - return $content;
1345 - }
1346 - }
1347 -
1348 - // Try even broader with the blog-item-wrap
1349 - $blog_item = $xpath->query('//div[contains(@class, "blog-item-wrap")]');
1350 - if ($blog_item && $blog_item->length > 0) {
1351 - $debug("Found blog-item-wrap");
1352 - $content = '';
1353 - foreach ($blog_item as $node) {
1354 - $content .= $dom->saveHTML($node);
1355 - }
1356 - if (!empty($content)) {
1357 - $debug("Returning blog-item-wrap content");
1358 - return $content;
1359 - }
1360 - }
1361 -
1362 - // Specific Gerow theme path
1363 - $gerow_path = $xpath->query('//section[contains(@class, "blog-area")]//div[contains(@class, "post-text")]');
1364 - if ($gerow_path && $gerow_path->length > 0) {
1365 - $debug("Found Gerow theme path to post-text");
1366 - $content = '';
1367 - foreach ($gerow_path as $node) {
1368 - $content .= $dom->saveHTML($node);
1369 - }
1370 - if (!empty($content)) {
1371 - $debug("Returning Gerow post-text content");
1372 - return $content;
1373 - }
1374 - }
1375 -
1376 - // Generic blog post selectors
1377 - $selectors = [
1378 - // Blog post specific selectors
1379 - '//div[contains(@class, "post-text")]',
1380 - '//article[contains(@class, "blog-post-item")]//div[contains(@class, "post-text")]',
1381 - '//div[contains(@class, "blog-details-content")]',
1382 - '//article[contains(@class, "blog-details-wrap")]',
1383 - '//div[contains(@class, "entry-content")]',
1384 - '//div[contains(@class, "blog-content")]',
1385 - '//div[contains(@class, "blog-item-wrap")]',
1386 -
1387 - // More general content selectors
1388 - '//div[contains(@class, "page__content")]',
1389 - '//div[contains(@class, "elementor-widget-container")]',
1390 - '//div[contains(@class, "elementor-text-editor")]',
1391 - '//div[contains(@class, "elementor-widget-text-editor")]',
1392 - '//*[contains(@class, "entry-content")]',
1393 - '//*[contains(@class, "post-content")]',
1394 - '//*[contains(@class, "article-content")]',
1395 - '//*[@id="content"]',
1396 - '//*[@id="main-content"]',
1397 - '//section[contains(@class, "blog-area")]',
1398 - '//article',
1399 - '//main',
1400 - '//div[contains(@class, "content")]'
1401 - ];
1402 -
1403 - // First handle Elementor content - get only leaf widget containers to avoid duplicates
1404 - $debug("Checking for Elementor content");
1405 - // Get widget containers that are direct children of widgets (not nested inside other widget containers)
1406 - $elementor_widgets = $xpath->query('//div[contains(@class, "elementor-widget")]//div[contains(@class, "elementor-widget-container")]');
1407 - if ($elementor_widgets && $elementor_widgets->length > 0) {
1408 - $debug("Found Elementor widgets");
1409 - $seen_content = array(); // Track seen content to avoid duplicates
1410 - $combined_content = '';
1411 - foreach ($elementor_widgets as $widget) {
1412 - $widget_content = $dom->saveHTML($widget);
1413 - if (!empty($widget_content)) {
1414 - // Create a hash of the content to detect duplicates
1415 - $content_hash = md5($widget_content);
1416 - if (!isset($seen_content[$content_hash])) {
1417 - $seen_content[$content_hash] = true;
1418 - $combined_content .= $widget_content;
1419 - }
1420 - }
1421 - }
1422 - if (!empty($combined_content)) {
1423 - $debug("Returning Elementor content");
1424 - return $combined_content;
1425 - }
1426 - }
1427 -
1428 - // Try standard selectors one by one
1429 - foreach ($selectors as $selector) {
1430 - $debug("Trying selector: " . $selector);
1431 - $nodes = $xpath->query($selector);
1432 - if ($nodes && $nodes->length > 0) {
1433 - $debug("Found " . $nodes->length . " matches for selector: " . $selector);
1434 - // Only take the FIRST matching node to avoid duplicate content
1435 - // (pages often have nested or multiple containers with same class)
1436 - $content = $dom->saveHTML($nodes->item(0));
1437 - if (!empty($content)) {
1438 - $debug("Returning content from selector: " . $selector . " (first match only)");
1439 - return $content;
1440 - }
1441 - }
1442 - }
1443 -
1444 - // Manual regex fallback for post-text if DOM methods fail
1445 - $debug("Trying regex fallback");
1446 - if (preg_match('/<div class="post-text">(.*?)<\/div>\s*<\/div>\s*<\/div>/s', $html, $matches)) {
1447 - $debug("Found post-text via regex");
1448 - return '<div class="post-text">' . $matches[1] . '</div>';
1449 - }
1450 -
1451 - // Try to extract the blog section as a whole
1452 - $blog_section = $xpath->query('//section[contains(@class, "blog-area")]');
1453 - if ($blog_section && $blog_section->length > 0) {
1454 - $debug("Found blog-area section");
1455 - $content = '';
1456 - foreach ($blog_section as $node) {
1457 - $content .= $dom->saveHTML($node);
1458 - }
1459 - if (!empty($content)) {
1460 - $debug("Returning blog-area section content");
1461 - return $content;
1462 - }
1463 - }
1464 -
1465 - // Generic container selectors for non-CMS sites (like .asp pages)
1466 - $debug("Trying generic container selectors");
1467 - $generic_selectors = [
1468 - '//div[@id="main"]',
1469 - '//div[@id="wrapper"]',
1470 - '//div[@id="page"]',
1471 - '//div[@id="site-content"]',
1472 - '//div[contains(@class, "main-content")]',
1473 - '//div[contains(@class, "page-content")]',
1474 - '//div[contains(@class, "site-content")]',
1475 - ];
1476 -
1477 - foreach ($generic_selectors as $selector) {
1478 - $debug("Trying generic selector: " . $selector);
1479 - $nodes = $xpath->query($selector);
1480 - if ($nodes && $nodes->length > 0) {
1481 - $content = $dom->saveHTML($nodes->item(0));
1482 - if (!empty($content)) {
1483 - $debug("Returning content from generic selector: " . $selector);
1484 - return $content;
1485 - }
1486 - }
1487 - }
1488 -
1489 - // Paragraph-based content detection - find regions with substantial text
1490 - $debug("Trying paragraph-based content detection");
1491 - $paragraphs = $xpath->query('//p[string-length(normalize-space()) > 50]');
1492 - if ($paragraphs && $paragraphs->length >= 3) {
1493 - $debug("Found " . $paragraphs->length . " substantial paragraphs");
1494 - // Collect all substantial paragraphs and their content
1495 - $paragraph_content = '';
1496 - foreach ($paragraphs as $p) {
1497 - $paragraph_content .= $dom->saveHTML($p) . "\n";
1498 - }
1499 - if (!empty($paragraph_content)) {
1500 - $debug("Returning paragraph-based content");
1501 - return $paragraph_content;
1502 - }
1503 - }
1504 -
1505 - // Improved body fallback - strip nav/header/footer elements first
1506 - $debug("Using improved body fallback");
1507 - $body = $dom->getElementsByTagName('body');
1508 - if ($body->length > 0) {
1509 - // Clone the body to avoid modifying the original DOM
1510 - $body_clone = $body->item(0)->cloneNode(true);
1511 -
1512 - // Remove common non-content elements by tag name
1513 - $remove_tags = ['nav', 'header', 'footer', 'aside', 'script', 'style', 'noscript'];
1514 - foreach ($remove_tags as $tag) {
1515 - $elements = $body_clone->getElementsByTagName($tag);
1516 - // Iterate backwards to safely remove elements
1517 - for ($i = $elements->length - 1; $i >= 0; $i--) {
1518 - $el = $elements->item($i);
1519 - if ($el && $el->parentNode) {
1520 - $el->parentNode->removeChild($el);
1521 - }
1522 - }
1523 - }
1524 -
1525 - // Remove elements with common non-content class names using XPath on the cloned body
1526 - $temp_dom = new DOMDocument();
1527 - @$temp_dom->appendChild($temp_dom->importNode($body_clone, true));
1528 - $temp_xpath = new DOMXPath($temp_dom);
1529 -
1530 - $remove_class_patterns = [
1531 - '//*[contains(@class, "nav")]',
1532 - '//*[contains(@class, "menu")]',
1533 - '//*[contains(@class, "sidebar")]',
1534 - '//*[contains(@class, "footer")]',
1535 - '//*[contains(@class, "header")]',
1536 - '//*[contains(@id, "nav")]',
1537 - '//*[contains(@id, "menu")]',
1538 - '//*[contains(@id, "sidebar")]',
1539 - '//*[contains(@id, "footer")]',
1540 - '//*[contains(@id, "header")]',
1541 - ];
1542 -
1543 - foreach ($remove_class_patterns as $pattern) {
1544 - $elements = $temp_xpath->query($pattern);
1545 - if ($elements) {
1546 - for ($i = $elements->length - 1; $i >= 0; $i--) {
1547 - $el = $elements->item($i);
1548 - if ($el && $el->parentNode) {
1549 - $el->parentNode->removeChild($el);
1550 - }
1551 - }
1552 - }
1553 - }
1554 -
1555 - $cleaned_content = $temp_dom->saveHTML();
1556 - if (!empty($cleaned_content)) {
1557 - $debug("Returning cleaned body content");
1558 - return $cleaned_content;
1559 - }
1560 - }
1561 -
1562 - // Last resort: return the original HTML
1563 - $debug("Returning original HTML");
1564 - return $html;
1565 - } catch (Exception $e) {
1566 - //error_log('[MXCHAT-ERROR] Content extraction failed: ' . $e->getMessage());
1567 - return $html; // Return original HTML if parsing fails
1568 - } finally {
1569 - libxml_clear_errors();
1570 - }
1571 -}
1572 -public function mxchat_get_sitemap_processing_status($sitemap_url) {
1573 - $sitemap_url = esc_url_raw($sitemap_url);
1574 - $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
1575 - $status = get_transient($status_key);
1576 -
1577 - if (!$status || !is_array($status)) {
1578 - return false;
1579 - }
1580 -
1581 - // Auto-complete check: if all URLs are processed but status isn't complete
1582 - if (isset($status['processed_urls']) && isset($status['total_urls']) &&
1583 - $status['processed_urls'] >= $status['total_urls'] &&
1584 - isset($status['status']) && $status['status'] !== 'complete' &&
1585 - $status['status'] !== 'error') {
1586 -
1587 - // Mark as complete
1588 - $status['status'] = 'complete';
1589 - $status['processed_urls'] = $status['total_urls']; // Ensure exact match
1590 -
1591 - // Update the transient with the corrected status
1592 - set_transient($status_key, $status, DAY_IN_SECONDS);
1593 - }
1594 -
1595 - return array(
1596 - 'total_urls' => absint($status['total_urls']),
1597 - 'processed_urls' => absint($status['processed_urls']),
1598 - 'failed_urls' => absint($status['failed_urls'] ?? 0),
1599 - 'percentage' => ($status['total_urls'] > 0)
1600 - ? round((absint($status['processed_urls']) / absint($status['total_urls'])) * 100)
1601 - : 0,
1602 - 'status' => sanitize_text_field($status['status']),
1603 - 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
1604 - 'error' => isset($status['error']) ? sanitize_text_field($status['error']) : '',
1605 - 'last_error' => isset($status['last_error']) ? sanitize_text_field($status['last_error']) : '',
1606 - 'failed_urls_list' => isset($status['failed_urls_list']) ? $status['failed_urls_list'] : array()
1607 - );
1608 -}
1609 -
1610 -public function mxchat_ajax_get_status_updates() {
1611 - try {
1612 - // Verify the request
1613 - check_ajax_referer('mxchat_status_nonce', 'nonce');
1614 -
1615 - // Get active queue IDs
1616 - $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
1617 - $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
1618 -
1619 - $sitemap_status = false;
1620 - $pdf_status = false;
1621 -
1622 - // Get sitemap queue status
1623 - if ($sitemap_queue_id) {
1624 - $sitemap_status = $this->mxchat_get_queue_status_data($sitemap_queue_id, 'sitemap');
1625 - }
1626 -
1627 - // Get PDF queue status
1628 - if ($pdf_queue_id) {
1629 - $pdf_status = $this->mxchat_get_queue_status_data($pdf_queue_id, 'pdf');
1630 - }
1631 -
1632 - $is_active_processing =
1633 - ($sitemap_status && $sitemap_status['status'] === 'processing') ||
1634 - ($pdf_status && $pdf_status['status'] === 'processing');
1635 -
1636 - // Return JSON response with the status data
1637 - wp_send_json(array(
1638 - 'pdf_status' => $pdf_status,
1639 - 'sitemap_status' => $sitemap_status,
1640 - 'is_processing' => $is_active_processing,
1641 - 'sitemap_queue_id' => $sitemap_queue_id,
1642 - 'pdf_queue_id' => $pdf_queue_id
1643 - ));
1644 -
1645 - } catch (Exception $e) {
1646 - //error_log('MxChat Status Update Error: ' . $e->getMessage());
1647 -
1648 - wp_send_json_error(array(
1649 - 'message' => 'Error getting status updates: ' . $e->getMessage(),
1650 - 'status' => 'error'
1651 - ));
1652 - }
1653 -}
1654 -
1655 -/**
1656 - * Helper function to get queue status data
1657 - */
1658 -private function mxchat_get_queue_status_data($queue_id, $type = 'sitemap') {
1659 - global $wpdb;
1660 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
1661 -
1662 - // Get counts by status
1663 - $counts = $wpdb->get_results($wpdb->prepare(
1664 - "SELECT status, COUNT(*) as count
1665 - FROM $table_name
1666 - WHERE queue_id = %s
1667 - GROUP BY status",
1668 - $queue_id
1669 - ), OBJECT_K);
1670 -
1671 - $total = 0;
1672 - $completed = 0;
1673 - $failed = 0;
1674 - $processing = 0;
1675 - $pending = 0;
1676 -
1677 - foreach ($counts as $status => $data) {
1678 - $count = absint($data->count);
1679 - $total += $count;
1680 -
1681 - switch ($status) {
1682 - case 'completed':
1683 - $completed = $count;
1684 - break;
1685 - case 'failed':
1686 - $failed = $count;
1687 - break;
1688 - case 'processing':
1689 - $processing = $count;
1690 - break;
1691 - case 'pending':
1692 - $pending = $count;
1693 - break;
1694 - }
1695 - }
1696 -
1697 - if ($total === 0) {
1698 - return false;
1699 - }
1700 -
1701 - // Calculate percentage
1702 - $percentage = round((($completed + $failed) / $total) * 100);
1703 -
1704 - // Get failed items details (limit to 50)
1705 - $failed_items = array();
1706 - if ($failed > 0) {
1707 - $failed_results = $wpdb->get_results($wpdb->prepare(
1708 - "SELECT item_type, item_data, error_message, attempts, completed_at
1709 - FROM $table_name
1710 - WHERE queue_id = %s
1711 - AND status = 'failed'
1712 - AND attempts >= max_attempts
1713 - ORDER BY id DESC
1714 - LIMIT 50",
1715 - $queue_id
1716 - ));
1717 -
1718 - foreach ($failed_results as $item) {
1719 - $data = json_decode($item->item_data, true);
1720 - $url = $type === 'pdf' ? ($data['pdf_url'] ?? '') : ($data['url'] ?? '');
1721 -
1722 - $failed_items[] = array(
1723 - 'url' => $url,
1724 - 'page' => $type === 'pdf' ? ($data['page_number'] ?? 0) : 0,
1725 - 'error' => $item->error_message,
1726 - 'retries' => $item->attempts,
1727 - 'time' => strtotime($item->completed_at)
1728 - );
1729 - }
1730 - }
1731 -
1732 - // Get queue metadata
1733 - $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
1734 -
1735 - // Determine if queue is complete
1736 - $is_complete = ($pending === 0 && $processing === 0);
1737 -
1738 - // Get last update time
1739 - $last_update = $wpdb->get_var($wpdb->prepare(
1740 - "SELECT MAX(UNIX_TIMESTAMP(COALESCE(completed_at, started_at, created_at)))
1741 - FROM $table_name
1742 - WHERE queue_id = %s",
1743 - $queue_id
1744 - ));
1745 -
1746 - $last_update_text = $last_update ? human_time_diff($last_update, time()) . ' ' . __('ago', 'mxchat') : __('Just now', 'mxchat');
1747 -
1748 - // Format based on type
1749 - if ($type === 'pdf') {
1750 - return array(
1751 - 'total_pages' => $total,
1752 - 'processed_pages' => $completed + $failed,
1753 - 'failed_pages' => $failed,
1754 - 'percentage' => $percentage,
1755 - 'status' => $is_complete ? 'complete' : 'processing',
1756 - 'last_update' => $last_update_text,
1757 - 'failed_pages_list' => $failed_items,
1758 - 'pdf_url' => $source_url,
1759 - 'queue_id' => $queue_id
1760 - );
1761 - } else {
1762 - return array(
1763 - 'total_urls' => $total,
1764 - 'processed_urls' => $completed + $failed,
1765 - 'failed_urls' => $failed,
1766 - 'percentage' => $percentage,
1767 - 'status' => $is_complete ? 'complete' : 'processing',
1768 - 'last_update' => $last_update_text,
1769 - 'failed_urls_list' => $failed_items,
1770 - 'sitemap_url' => $source_url,
1771 - 'queue_id' => $queue_id
1772 - );
1773 - }
1774 -}
1775 -
1776 -/**
1777 - * Public method to get processing status for both sitemap and PDF queues
1778 - * Used by admin pages to display processing status
1779 - *
1780 - * @return array Array with 'sitemap_status', 'pdf_status', and 'is_processing' keys
1781 - */
1782 -public function mxchat_get_processing_statuses() {
1783 - $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
1784 - $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
1785 -
1786 - $sitemap_status = false;
1787 - $pdf_status = false;
1788 -
1789 - if ($sitemap_queue_id) {
1790 - $sitemap_status = $this->mxchat_get_queue_status_data($sitemap_queue_id, 'sitemap');
1791 - }
1792 -
1793 - if ($pdf_queue_id) {
1794 - $pdf_status = $this->mxchat_get_queue_status_data($pdf_queue_id, 'pdf');
1795 - }
1796 -
1797 - $is_processing =
1798 - ($sitemap_status && $sitemap_status['status'] === 'processing') ||
1799 - ($pdf_status && $pdf_status['status'] === 'processing');
1800 -
1801 - return array(
1802 - 'sitemap_status' => $sitemap_status,
1803 - 'pdf_status' => $pdf_status,
1804 - 'is_processing' => $is_processing
1805 - );
1806 -}
1807 -
1808 -/**
1809 - * AJAX handler to get recent knowledge entries for real-time table updates
1810 - * UPDATED: Now supports both WordPress DB and Pinecone data sources
1811 - */
1812 -public function ajax_mxchat_get_recent_entries() {
1813 - check_ajax_referer('mxchat_entries_nonce', 'nonce');
1814 -
1815 - if (!current_user_can('manage_options')) {
1816 - wp_send_json_error(array('message' => 'Unauthorized'));
1817 - return;
1818 - }
1819 -
1820 - global $wpdb;
1821 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1822 -
1823 - // Get parameters
1824 - $last_id = isset($_POST['last_id']) ? absint($_POST['last_id']) : 0;
1825 - $limit = isset($_POST['limit']) ? min(absint($_POST['limit']), 50) : 10;
1826 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
1827 -
1828 - // Check if Pinecone is enabled for this bot
1829 - $pinecone_manager = $this->mxchat_get_pinecone_manager();
1830 - $pinecone_options = $pinecone_manager ? $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id) : array();
1831 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1832 - $has_pinecone_api = !empty($pinecone_options['mxchat_pinecone_api_key']);
1833 -
1834 - if ($use_pinecone && $has_pinecone_api) {
1835 - // PINECONE DATA SOURCE - Get grouped entry count (not raw vector count)
1836 - // Use mxchat_fetch_pinecone_records which returns total_unique_entries
1837 - $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, '', 1, 10, $bot_id, '');
1838 - $total_count = $records['total'] ?? 0;
1839 -
1840 - // For Pinecone, we don't return individual entries during polling
1841 - // (entries are already displayed on page load via mxchat_fetch_pinecone_records)
1842 - // We just return the updated count
1843 - wp_send_json_success(array(
1844 - 'entries' => array(),
1845 - 'total_count' => absint($total_count),
1846 - 'max_id' => $last_id,
1847 - 'data_source' => 'pinecone'
1848 - ));
1849 - return;
1850 - }
1851 -
1852 - // WORDPRESS DB DATA SOURCE
1853 - // Build query to get entries newer than last_id
1854 - $where_clauses = array('1=1');
1855 - $where_values = array();
1856 -
1857 - if ($last_id > 0) {
1858 - $where_clauses[] = 'id > %d';
1859 - $where_values[] = $last_id;
1860 - }
1861 -
1862 - // Note: WordPress DB table doesn't have bot_id column
1863 - // Multi-bot filtering is handled via Pinecone namespaces
1864 -
1865 - $where_sql = implode(' AND ', $where_clauses);
1866 -
1867 - // Get recent entries
1868 - $query = "SELECT id, article_content, source_url, timestamp
1869 - FROM $table_name
1870 - WHERE $where_sql
1871 - ORDER BY id DESC
1872 - LIMIT %d";
1873 -
1874 - $where_values[] = $limit;
1875 -
1876 - $entries = $wpdb->get_results($wpdb->prepare($query, $where_values));
1877 -
1878 - // Get total count of GROUPED entries (by source_url) - matches pagination display
1879 - // Count unique source_urls (excluding mxchat:// internal refs) + count of ungrouped rows
1880 - $total_count = $wpdb->get_var(
1881 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '' AND source_url NOT LIKE 'mxchat://%') +
1882 - (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')"
1883 - );
1884 -
1885 - // Format entries for response
1886 - $formatted_entries = array();
1887 - $preview_length = 150;
1888 - foreach ($entries as $entry) {
1889 - // Parse chunk metadata using the proper chunker method (same as initial page load)
1890 - if (class_exists('MxChat_Chunker')) {
1891 - $chunk_meta = MxChat_Chunker::parse_stored_chunk($entry->article_content);
1892 - $display_content = $chunk_meta['text'];
1893 - $chunk_metadata = $chunk_meta['metadata'];
1894 - } else {
1895 - $display_content = $entry->article_content;
1896 - $chunk_metadata = array();
1897 - }
1898 -
1899 - $content_preview = mb_strlen($display_content) > $preview_length
1900 - ? mb_substr($display_content, 0, $preview_length) . '...'
1901 - : $display_content;
1902 -
1903 - $formatted_entries[] = array(
1904 - 'id' => $entry->id,
1905 - 'preview' => esc_html($content_preview),
1906 - 'full_content' => wp_kses_post(wpautop($display_content)),
1907 - 'content_length' => mb_strlen($display_content),
1908 - 'preview_length' => $preview_length,
1909 - 'source_url' => $entry->source_url,
1910 - 'has_link' => !empty($entry->source_url) && strpos($entry->source_url, 'mxchat://') !== 0,
1911 - 'chunk_metadata' => $chunk_metadata,
1912 - 'bot_id' => $entry->bot_id ?? 'default',
1913 - 'edit_nonce' => wp_create_nonce('mxchat_edit_entry_nonce'),
1914 - 'delete_nonce' => wp_create_nonce('mxchat_delete_prompt_nonce')
1915 - );
1916 - }
1917 -
1918 - wp_send_json_success(array(
1919 - 'entries' => $formatted_entries,
1920 - 'total_count' => absint($total_count),
1921 - 'max_id' => !empty($entries) ? $entries[0]->id : $last_id,
1922 - 'data_source' => 'wordpress'
1923 - ));
1924 -}
1925 -
1926 -/**
1927 - * Get Pinecone total count from stats API
1928 - * Helper function for ajax_mxchat_get_recent_entries
1929 - */
1930 -private function mxchat_get_pinecone_count_from_stats($pinecone_options) {
1931 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1932 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1933 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
1934 -
1935 - if (empty($api_key) || empty($host)) {
1936 - return 0;
1937 - }
1938 -
1939 - try {
1940 - $stats_url = "https://{$host}/describe_index_stats";
1941 -
1942 - $response = wp_remote_post($stats_url, array(
1943 - 'headers' => array(
1944 - 'Api-Key' => $api_key,
1945 - 'Content-Type' => 'application/json'
1946 - ),
1947 - 'body' => '{}',
1948 - 'timeout' => 10
1949 - ));
1950 -
1951 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
1952 - $body = wp_remote_retrieve_body($response);
1953 - $stats_data = json_decode($body, true);
1954 -
1955 - // If namespace is specified, get count from that specific namespace
1956 - if (!empty($namespace) && isset($stats_data['namespaces'][$namespace]['vectorCount'])) {
1957 - return intval($stats_data['namespaces'][$namespace]['vectorCount']);
1958 - }
1959 -
1960 - // If no namespace specified or namespace not found in response, use total
1961 - return intval($stats_data['totalVectorCount'] ?? 0);
1962 - }
1963 -
1964 - return 0;
1965 -
1966 - } catch (Exception $e) {
1967 - return 0;
1968 - }
1969 -}
1970 -
1971 -/**
1972 - * AJAX handler to refresh Pinecone entries table via AJAX
1973 - * Returns the table HTML for updating the UI without a full page reload
1974 - */
1975 -public function ajax_mxchat_refresh_pinecone_entries() {
1976 - check_ajax_referer('mxchat_entries_nonce', 'nonce');
1977 -
1978 - if (!current_user_can('manage_options')) {
1979 - wp_send_json_error(array('message' => 'Unauthorized'));
1980 - return;
1981 - }
1982 -
1983 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
1984 - $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
1985 - $per_page = 25;
1986 - $search_query = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
1987 - $content_type_filter = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : '';
1988 -
1989 - // Get Pinecone manager and options
1990 - $pinecone_manager = $this->mxchat_get_pinecone_manager();
1991 - if (!$pinecone_manager) {
1992 - wp_send_json_error(array('message' => 'Pinecone manager not available'));
1993 - return;
1994 - }
1995 -
1996 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
1997 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1998 - $pinecone_api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1999 -
2000 - if (!$use_pinecone || empty($pinecone_api_key)) {
2001 - wp_send_json_error(array('message' => 'Pinecone not configured'));
2002 - return;
2003 - }
2004 -
2005 - // Fetch records from Pinecone
2006 - $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, $search_query, $page, $per_page, $bot_id, $content_type_filter);
2007 - $prompts = $records['data'] ?? array();
2008 - $total_records = $records['total'] ?? 0;
2009 -
2010 - // Preprocess Pinecone records — set chunk_metadata and display_content
2011 - // (matches admin-knowledge-page.php preprocessing)
2012 - foreach ($prompts as $prompt) {
2013 - if (isset($prompt->chunk_index) && $prompt->chunk_index !== null) {
2014 - $prompt->chunk_metadata = array(
2015 - 'chunk_index' => intval($prompt->chunk_index),
2016 - 'total_chunks' => isset($prompt->total_chunks) ? intval($prompt->total_chunks) : null,
2017 - 'is_chunked' => isset($prompt->is_chunked) ? (bool) $prompt->is_chunked : true,
2018 - 'source_url' => $prompt->source_url ?? ''
2019 - );
2020 - $prompt->display_content = $prompt->article_content;
2021 - } else {
2022 - $prompt->chunk_metadata = array();
2023 - $prompt->display_content = $prompt->article_content ?? '';
2024 - }
2025 - }
2026 -
2027 - // Group prompts by source_url
2028 - $grouped_prompts = array();
2029 - foreach ($prompts as $prompt) {
2030 - $source_url = '';
2031 - if (!empty($prompt->chunk_metadata['source_url'])) {
2032 - $source_url = $prompt->chunk_metadata['source_url'];
2033 - } elseif (!empty($prompt->source_url)) {
2034 - $source_url = $prompt->source_url;
2035 - }
2036 -
2037 - if (!empty($source_url)) {
2038 - if (!isset($grouped_prompts[$source_url])) {
2039 - $grouped_prompts[$source_url] = array();
2040 - }
2041 - $grouped_prompts[$source_url][] = $prompt;
2042 - } else {
2043 - $grouped_prompts['_ungrouped_' . $prompt->id] = array($prompt);
2044 - }
2045 - }
2046 -
2047 - // Sort each group by chunk_index
2048 - foreach ($grouped_prompts as $source_url => &$group) {
2049 - usort($group, function($a, $b) {
2050 - $index_a = isset($a->chunk_metadata['chunk_index']) ? intval($a->chunk_metadata['chunk_index']) : 0;
2051 - $index_b = isset($b->chunk_metadata['chunk_index']) ? intval($b->chunk_metadata['chunk_index']) : 0;
2052 - return $index_a - $index_b;
2053 - });
2054 - }
2055 - unset($group);
2056 -
2057 - // Build HTML for the table rows - must match admin-knowledge-page.php structure exactly
2058 - ob_start();
2059 - $display_index = 0;
2060 - $current_page = $page;
2061 - $data_source = 'pinecone';
2062 - $current_bot_id = $bot_id;
2063 - $preview_length = 150;
2064 -
2065 - if (empty($grouped_prompts)) {
2066 - echo '<tr><td colspan="4" style="padding: 40px; text-align: center; color: var(--mxch-text-muted);">';
2067 - esc_html_e('No knowledge entries found in Pinecone.', 'mxchat');
2068 - echo '</td></tr>';
2069 - } else {
2070 - foreach ($grouped_prompts as $source_url => $group) {
2071 - $chunk_count = count($group);
2072 - $first_prompt = $group[0];
2073 - $display_index++;
2074 -
2075 - if ($chunk_count > 1) {
2076 - // Multiple chunks - show grouped row with expand button
2077 - $group_id = 'group-' . md5($source_url);
2078 - ?>
2079 - <tr id="prompt-<?php echo esc_attr($first_prompt->id); ?>"
2080 - class="mxchat-chunk-group-header"
2081 - data-source="<?php echo esc_attr($data_source); ?>"
2082 - data-group-id="<?php echo esc_attr($group_id); ?>"
2083 - style="border-bottom: 1px solid var(--mxch-card-border); background: rgba(33, 150, 243, 0.02);">
2084 - <td style="padding: 12px 16px; text-align: center;">
2085 - <input type="checkbox"
2086 - class="mxchat-entry-checkbox"
2087 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2088 - data-source="<?php echo esc_attr($data_source); ?>"
2089 - data-source-url="<?php echo esc_attr($source_url); ?>"
2090 - data-is-group="true"
2091 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>">
2092 - </td>
2093 - <td style="padding: 12px 16px; font-size: 13px;">
2094 - <?php echo esc_html($display_index + (($current_page - 1) * $per_page)); ?>
2095 - </td>
2096 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2097 - <div class="mxchat-chunk-group-info">
2098 - <button type="button" class="mxchat-chunk-toggle" data-group-id="<?php echo esc_attr($group_id); ?>">
2099 - <span class="dashicons dashicons-arrow-right-alt2"></span>
2100 - </button>
2101 - <span class="mxchat-chunk-badge"><?php echo esc_html($chunk_count); ?> <?php esc_html_e('chunks', 'mxchat'); ?></span>
2102 - <span class="mxchat-chunk-preview">
2103 - <?php
2104 - $parent_content = isset($first_prompt->display_content) ? $first_prompt->display_content : (isset($first_prompt->article_content) ? $first_prompt->article_content : '');
2105 - $content_preview = mb_substr($parent_content, 0, 100);
2106 - echo esc_html($content_preview . '...');
2107 - ?>
2108 - </span>
2109 - </div>
2110 - </td>
2111 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2112 - <?php if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0 && strpos($source_url, '_ungrouped_') !== 0) : ?>
2113 - <a href="<?php echo esc_url($source_url); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2114 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2115 - <?php esc_html_e('View Source', 'mxchat'); ?>
2116 - </a>
2117 - <?php else : ?>
2118 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
2119 - <?php endif; ?>
2120 - </td>
2121 - <td class="mxchat-actions-cell" style="padding: 12px 16px; white-space: nowrap;">
2122 - <?php if ($data_source !== 'pinecone') : ?>
2123 - <button type="button"
2124 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2125 - data-source-url="<?php echo esc_attr($source_url); ?>"
2126 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2127 - data-data-source="<?php echo esc_attr($data_source); ?>"
2128 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2129 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2130 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2131 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2132 - </button>
2133 - <?php endif; ?>
2134 - <button type="button"
2135 - class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
2136 - data-source-url="<?php echo esc_attr($source_url); ?>"
2137 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
2138 - data-data-source="<?php echo esc_attr($data_source); ?>"
2139 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2140 - data-nonce="<?php echo wp_create_nonce('mxchat_delete_chunks_nonce'); ?>"
2141 - style="color: var(--mxch-error);"
2142 - title="<?php esc_attr_e('Delete all chunks', 'mxchat'); ?>">
2143 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2144 - </button>
2145 - </td>
2146 - </tr>
2147 - <?php
2148 - // Render hidden chunk rows
2149 - foreach ($group as $chunk_index => $chunk) {
2150 - $meta_chunk_index = isset($chunk->chunk_metadata['chunk_index']) ? intval($chunk->chunk_metadata['chunk_index']) : $chunk_index;
2151 - $meta_total_chunks = isset($chunk->chunk_metadata['total_chunks']) ? intval($chunk->chunk_metadata['total_chunks']) : $chunk_count;
2152 - $content = isset($chunk->display_content) ? $chunk->display_content : (isset($chunk->article_content) ? $chunk->article_content : '');
2153 - $content_preview = mb_strlen($content) > $preview_length
2154 - ? mb_substr($content, 0, $preview_length) . '...'
2155 - : $content;
2156 - ?>
2157 - <tr id="prompt-<?php echo esc_attr($chunk->id); ?>"
2158 - class="mxchat-chunk-row <?php echo esc_attr($group_id); ?>"
2159 - data-source="<?php echo esc_attr($data_source); ?>"
2160 - style="display: none; background: #f8f9fa; border-bottom: 1px solid var(--mxch-card-border);">
2161 - <td style="padding: 12px 16px; text-align: center;">
2162 - <!-- Checkbox column placeholder for chunks (managed by group) -->
2163 - </td>
2164 - <td style="padding: 12px 16px 12px 30px; font-size: 13px;">
2165 - <!-- Hidden ID column for chunks -->
2166 - </td>
2167 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2168 - <div class="mxchat-accordion-wrapper">
2169 - <div class="mxchat-content-preview">
2170 - <span class="mxchat-chunk-indicator" style="margin-right: 10px; color: var(--mxch-text-secondary); font-size: 12px;">
2171 - <?php printf(esc_html__('Chunk %d of %d', 'mxchat'), $meta_chunk_index + 1, $meta_total_chunks); ?>
2172 - </span>
2173 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2174 - <?php if (mb_strlen($content) > $preview_length) : ?>
2175 - <button class="mxchat-expand-toggle" type="button">
2176 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2177 - </button>
2178 - <?php endif; ?>
2179 - </div>
2180 - <div class="mxchat-content-full" style="display: none;">
2181 - <div class="content-view">
2182 - <?php
2183 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2184 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2185 - echo wp_kses_post(wpautop($content));
2186 - echo '</div>';
2187 - } else {
2188 - echo wp_kses_post(wpautop($content));
2189 - }
2190 - ?>
2191 - </div>
2192 - </div>
2193 - </div>
2194 - </td>
2195 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2196 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Same as parent', 'mxchat'); ?></span>
2197 - </td>
2198 - <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2199 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Managed by group', 'mxchat'); ?></span>
2200 - </td>
2201 - </tr>
2202 - <?php
2203 - }
2204 - } else {
2205 - // Single entry - display normally with accordion
2206 - $prompt = $first_prompt;
2207 - $content = isset($prompt->display_content) ? $prompt->display_content : (isset($prompt->article_content) ? $prompt->article_content : '');
2208 - $content_preview = mb_strlen($content) > $preview_length
2209 - ? mb_substr($content, 0, $preview_length) . '...'
2210 - : $content;
2211 - ?>
2212 - <tr id="prompt-<?php echo esc_attr($prompt->id); ?>"
2213 - data-source="<?php echo esc_attr($data_source); ?>"
2214 - style="border-bottom: 1px solid var(--mxch-card-border); background: rgba(33, 150, 243, 0.02);">
2215 - <td style="padding: 12px 16px; text-align: center;">
2216 - <input type="checkbox"
2217 - class="mxchat-entry-checkbox"
2218 - data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2219 - data-source="<?php echo esc_attr($data_source); ?>"
2220 - data-source-url="<?php echo esc_attr($prompt->source_url ?? ''); ?>"
2221 - data-is-group="false"
2222 - data-chunk-count="1">
2223 - </td>
2224 - <td style="padding: 12px 16px; font-size: 13px;">
2225 - <?php echo esc_html($display_index + (($current_page - 1) * $per_page)); ?>
2226 - </td>
2227 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2228 - <div class="mxchat-accordion-wrapper">
2229 - <div class="mxchat-content-preview">
2230 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2231 - <?php if (mb_strlen($content) > $preview_length) : ?>
2232 - <button class="mxchat-expand-toggle" type="button">
2233 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2234 - </button>
2235 - <?php endif; ?>
2236 - </div>
2237 - <div class="mxchat-content-full" style="display: none;">
2238 - <div class="content-view">
2239 - <?php
2240 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2241 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2242 - echo wp_kses_post(wpautop($content));
2243 - echo '</div>';
2244 - } else {
2245 - echo wp_kses_post(wpautop($content));
2246 - }
2247 - ?>
2248 - </div>
2249 - </div>
2250 - </div>
2251 - </td>
2252 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2253 - <?php
2254 - $actual_source = $source_url;
2255 - if (strpos($source_url, '_ungrouped_') === 0) {
2256 - $actual_source = $prompt->source_url ?? '';
2257 - }
2258 - if (!empty($actual_source) && strpos($actual_source, 'mxchat://') !== 0) : ?>
2259 - <a href="<?php echo esc_url($actual_source); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2260 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2261 - <?php esc_html_e('View', 'mxchat'); ?>
2262 - </a>
2263 - <?php else : ?>
2264 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
2265 - <?php endif; ?>
2266 - </td>
2267 - <td style="padding: 12px 16px;">
2268 - <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);">
2269 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2270 - </button>
2271 - </td>
2272 - </tr>
2273 - <?php
2274 - }
2275 - }
2276 - }
2277 - $html = ob_get_clean();
2278 -
2279 - // Generate pagination HTML for Pinecone
2280 - $total_pages = ceil($total_records / $per_page);
2281 - $pagination_html = '';
2282 - if ($total_pages > 1) {
2283 - $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) . '">';
2284 -
2285 - // Previous button
2286 - if ($page > 1) {
2287 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page - 1) . '">' . esc_html__('&laquo; Previous', 'mxchat') . '</a> ';
2288 - }
2289 -
2290 - // Page numbers
2291 - $start_page = max(1, $page - 2);
2292 - $end_page = min($total_pages, $page + 2);
2293 -
2294 - if ($start_page > 1) {
2295 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="1">1</a> ';
2296 - if ($start_page > 2) {
2297 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2298 - }
2299 - }
2300 -
2301 - for ($i = $start_page; $i <= $end_page; $i++) {
2302 - if ($i == $page) {
2303 - $pagination_html .= '<span class="mxchat-page-current">' . $i . '</span> ';
2304 - } else {
2305 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $i . '">' . $i . '</a> ';
2306 - }
2307 - }
2308 -
2309 - if ($end_page < $total_pages) {
2310 - if ($end_page < $total_pages - 1) {
2311 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2312 - }
2313 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $total_pages . '">' . $total_pages . '</a> ';
2314 - }
2315 -
2316 - // Next button
2317 - if ($page < $total_pages) {
2318 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page + 1) . '">' . esc_html__('Next &raquo;', 'mxchat') . '</a>';
2319 - }
2320 -
2321 - $pagination_html .= '</div>';
2322 - }
2323 -
2324 - wp_send_json_success(array(
2325 - 'html' => $html,
2326 - 'pagination_html' => $pagination_html,
2327 - 'total_count' => $total_records,
2328 - 'total_pages' => $total_pages,
2329 - 'page' => $page,
2330 - 'per_page' => $per_page,
2331 - 'data_source' => 'pinecone'
2332 - ));
2333 -}
2334 -
2335 -/**
2336 - * AJAX handler for pagination - handles both WordPress DB and Pinecone data sources
2337 - * Returns paginated entries without requiring a full page reload
2338 - */
2339 -public function ajax_mxchat_paginate_entries() {
2340 - check_ajax_referer('mxchat_entries_nonce', 'nonce');
2341 -
2342 - if (!current_user_can('manage_options')) {
2343 - wp_send_json_error(array('message' => 'Unauthorized'));
2344 - return;
2345 - }
2346 -
2347 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
2348 - $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
2349 - $search_query = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
2350 - $content_type_filter = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : '';
2351 - $per_page = 25;
2352 -
2353 - // Check if Pinecone is enabled for this bot
2354 - $pinecone_manager = $this->mxchat_get_pinecone_manager();
2355 - $pinecone_options = $pinecone_manager ? $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id) : array();
2356 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2357 - $has_pinecone_api = !empty($pinecone_options['mxchat_pinecone_api_key']);
2358 -
2359 - if ($use_pinecone && $has_pinecone_api) {
2360 - // Delegate to Pinecone pagination handler (pass search params)
2361 - $_POST['page'] = $page;
2362 - $_POST['search'] = $search_query;
2363 - $_POST['content_type'] = $content_type_filter;
2364 - $this->ajax_mxchat_refresh_pinecone_entries();
2365 - return;
2366 - }
2367 -
2368 - // WordPress DB pagination - MUST match initial page load logic exactly
2369 - global $wpdb;
2370 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2371 - $offset = ($page - 1) * $per_page;
2372 -
2373 - // Build WHERE clause for search and content type filtering
2374 - $where_clauses = array();
2375 - $where_values = array();
2376 -
2377 - if ($search_query) {
2378 - $where_clauses[] = "article_content LIKE %s";
2379 - $where_values[] = '%' . $wpdb->esc_like($search_query) . '%';
2380 - }
2381 -
2382 - if ($content_type_filter) {
2383 - switch ($content_type_filter) {
2384 - case 'manual':
2385 - $where_clauses[] = "(source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')";
2386 - break;
2387 - case 'pdf':
2388 - $where_clauses[] = "source_url LIKE '%.pdf'";
2389 - break;
2390 - case 'url':
2391 - $where_clauses[] = "source_url != '' AND source_url IS NOT NULL AND source_url NOT LIKE 'mxchat://%' AND source_url NOT LIKE '%.pdf'";
2392 - break;
2393 - }
2394 - }
2395 -
2396 - $where_sql = !empty($where_clauses) ? 'WHERE ' . implode(' AND ', $where_clauses) : '';
2397 -
2398 - // Count grouped entries with filters applied
2399 - if (!empty($where_values)) {
2400 - $count_args = array_merge($where_values, $where_values);
2401 - $count_query = $wpdb->prepare(
2402 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$where_sql} AND source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2403 - (SELECT COUNT(*) FROM {$table_name} {$where_sql} AND (source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%'))",
2404 - ...$count_args
2405 - );
2406 - $total_records = $wpdb->get_var($count_query);
2407 - } else if (!empty($where_sql)) {
2408 - // Content type filter only (no search), no prepared values needed
2409 - $total_records = $wpdb->get_var(
2410 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$where_sql} AND source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2411 - (SELECT COUNT(*) FROM {$table_name} {$where_sql} AND (source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%'))"
2412 - );
2413 - } else {
2414 - // No filters
2415 - $total_records = $wpdb->get_var(
2416 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2417 - (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')"
2418 - );
2419 - }
2420 - $total_pages = ceil($total_records / $per_page);
2421 -
2422 - // Step 1: Get unique source_urls for this page (ordered by latest timestamp) with filters
2423 - if (!empty($where_values)) {
2424 - $query_args = array_merge($where_values, array($per_page, $offset));
2425 - $urls_query = $wpdb->prepare(
2426 - "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2427 - {$where_sql}
2428 - GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2429 - ...$query_args
2430 - );
2431 - } else if (!empty($where_sql)) {
2432 - $urls_query = $wpdb->prepare(
2433 - "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2434 - {$where_sql}
2435 - GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2436 - $per_page, $offset
2437 - );
2438 - } else {
2439 - $urls_query = $wpdb->prepare(
2440 - "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2441 - GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2442 - $per_page, $offset
2443 - );
2444 - }
2445 - $page_urls = $wpdb->get_results($urls_query);
2446 -
2447 - // Step 2: Build list of source_urls to fetch
2448 - $url_list = array();
2449 - $url_order_map = array();
2450 - $order_index = 0;
2451 - foreach ($page_urls as $url_row) {
2452 - $url = $url_row->source_url;
2453 - $url_list[] = $url;
2454 - $url_order_map[$url] = $order_index++;
2455 - }
2456 -
2457 - // Step 3: Fetch all rows for these source_urls (with search filter if applicable)
2458 - $prompts = array();
2459 - if (!empty($url_list)) {
2460 - $placeholders = implode(',', array_fill(0, count($url_list), '%s'));
2461 - if ($search_query) {
2462 - // Include search filter in the final fetch
2463 - $prompts_query = $wpdb->prepare(
2464 - "SELECT id, article_content, source_url, timestamp, role_restriction
2465 - FROM {$table_name}
2466 - WHERE source_url IN ($placeholders) AND article_content LIKE %s
2467 - ORDER BY timestamp DESC",
2468 - ...array_merge($url_list, ['%' . $wpdb->esc_like($search_query) . '%'])
2469 - );
2470 - } else {
2471 - $prompts_query = $wpdb->prepare(
2472 - "SELECT id, article_content, source_url, timestamp, role_restriction
2473 - FROM {$table_name}
2474 - WHERE source_url IN ($placeholders)
2475 - ORDER BY timestamp DESC",
2476 - $url_list
2477 - );
2478 - }
2479 - $prompts = $wpdb->get_results($prompts_query);
2480 - }
2481 -
2482 - // Group prompts by source_url for chunk display
2483 - $grouped_prompts = array();
2484 - foreach ($prompts as $prompt) {
2485 - $source_url = $prompt->source_url ?? '';
2486 -
2487 - // Parse chunk metadata using the proper chunker method (same as initial page load)
2488 - if (class_exists('MxChat_Chunker')) {
2489 - $chunk_meta = MxChat_Chunker::parse_stored_chunk($prompt->article_content);
2490 - $prompt->chunk_metadata = $chunk_meta['metadata'];
2491 - $prompt->display_content = $chunk_meta['text'];
2492 - } else {
2493 - $prompt->chunk_metadata = array();
2494 - $prompt->display_content = $prompt->article_content;
2495 - }
2496 -
2497 - if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0) {
2498 - if (!isset($grouped_prompts[$source_url])) {
2499 - $grouped_prompts[$source_url] = array();
2500 - }
2501 - $grouped_prompts[$source_url][] = $prompt;
2502 - } else {
2503 - // Ungrouped entries
2504 - $grouped_prompts['_ungrouped_' . $prompt->id] = array($prompt);
2505 - }
2506 - }
2507 -
2508 - // Sort groups by the original URL order (newest first)
2509 - uksort($grouped_prompts, function($a, $b) use ($url_order_map) {
2510 - $order_a = isset($url_order_map[$a]) ? $url_order_map[$a] : PHP_INT_MAX;
2511 - $order_b = isset($url_order_map[$b]) ? $url_order_map[$b] : PHP_INT_MAX;
2512 - return $order_a - $order_b;
2513 - });
2514 -
2515 - // Sort each group internally by chunk_index
2516 - foreach ($grouped_prompts as $source_url => &$group) {
2517 - usort($group, function($a, $b) {
2518 - $index_a = isset($a->chunk_metadata['chunk_index']) ? intval($a->chunk_metadata['chunk_index']) : 0;
2519 - $index_b = isset($b->chunk_metadata['chunk_index']) ? intval($b->chunk_metadata['chunk_index']) : 0;
2520 - return $index_a - $index_b;
2521 - });
2522 - }
2523 - unset($group);
2524 -
2525 - // Build HTML for the table rows
2526 - ob_start();
2527 - $display_index = 0;
2528 - $current_page = $page;
2529 - $data_source = 'wordpress';
2530 - $current_bot_id = $bot_id;
2531 - $preview_length = 150;
2532 -
2533 - if (empty($grouped_prompts)) {
2534 - echo '<tr><td colspan="5" style="padding: 40px; text-align: center; color: var(--mxch-text-muted);">';
2535 - esc_html_e('No knowledge entries found. Use the Import Options to add content.', 'mxchat');
2536 - echo '</td></tr>';
2537 - } else {
2538 - foreach ($grouped_prompts as $source_url => $group) {
2539 - $chunk_count = count($group);
2540 - $first_prompt = $group[0];
2541 - $display_index++;
2542 -
2543 - if ($chunk_count > 1) {
2544 - // Multiple chunks - show grouped row with expand button
2545 - $group_id = 'group-' . md5($source_url);
2546 - ?>
2547 - <tr id="prompt-<?php echo esc_attr($first_prompt->id); ?>"
2548 - class="mxchat-chunk-group-header"
2549 - data-source="<?php echo esc_attr($data_source); ?>"
2550 - data-group-id="<?php echo esc_attr($group_id); ?>"
2551 - style="border-bottom: 1px solid var(--mxch-card-border);">
2552 - <td style="padding: 12px 16px; text-align: center;">
2553 - <input type="checkbox"
2554 - class="mxchat-entry-checkbox"
2555 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2556 - data-source="<?php echo esc_attr($data_source); ?>"
2557 - data-source-url="<?php echo esc_attr($source_url); ?>"
2558 - data-is-group="true"
2559 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>">
2560 - </td>
2561 - <td style="padding: 12px 16px; font-size: 13px;">
2562 - <?php echo esc_html($first_prompt->id); ?>
2563 - </td>
2564 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2565 - <div class="mxchat-chunk-group-info">
2566 - <button type="button" class="mxchat-chunk-toggle" data-group-id="<?php echo esc_attr($group_id); ?>">
2567 - <span class="dashicons dashicons-arrow-right-alt2"></span>
2568 - </button>
2569 - <span class="mxchat-chunk-badge"><?php echo esc_html($chunk_count); ?> <?php esc_html_e('chunks', 'mxchat'); ?></span>
2570 - <span class="mxchat-chunk-preview">
2571 - <?php
2572 - $parent_content = isset($first_prompt->display_content) ? $first_prompt->display_content : $first_prompt->article_content;
2573 - $content_preview = mb_substr($parent_content, 0, 100);
2574 - echo esc_html($content_preview . '...');
2575 - ?>
2576 - </span>
2577 - </div>
2578 - </td>
2579 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2580 - <?php if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0 && strpos($source_url, '_ungrouped_') !== 0) : ?>
2581 - <a href="<?php echo esc_url($source_url); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2582 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2583 - <?php esc_html_e('View Source', 'mxchat'); ?>
2584 - </a>
2585 - <?php else : ?>
2586 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
2587 - <?php endif; ?>
2588 - </td>
2589 - <td class="mxchat-actions-cell" style="padding: 12px 16px; white-space: nowrap;">
2590 - <?php if ($data_source !== 'pinecone') : ?>
2591 - <button type="button"
2592 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2593 - data-source-url="<?php echo esc_attr($source_url); ?>"
2594 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2595 - data-data-source="<?php echo esc_attr($data_source); ?>"
2596 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2597 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2598 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2599 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2600 - </button>
2601 - <?php endif; ?>
2602 - <button type="button"
2603 - class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
2604 - data-source-url="<?php echo esc_attr($source_url); ?>"
2605 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
2606 - data-data-source="<?php echo esc_attr($data_source); ?>"
2607 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2608 - data-nonce="<?php echo wp_create_nonce('mxchat_delete_chunks_nonce'); ?>"
2609 - style="color: var(--mxch-error);"
2610 - title="<?php esc_attr_e('Delete all chunks', 'mxchat'); ?>">
2611 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2612 - </button>
2613 - </td>
2614 - </tr>
2615 - <?php
2616 - // Render hidden chunk rows
2617 - foreach ($group as $chunk_index => $chunk) {
2618 - $meta_chunk_index = isset($chunk->chunk_metadata['chunk_index']) ? intval($chunk->chunk_metadata['chunk_index']) : $chunk_index;
2619 - $meta_total_chunks = isset($chunk->chunk_metadata['total_chunks']) ? intval($chunk->chunk_metadata['total_chunks']) : $chunk_count;
2620 - $content = isset($chunk->display_content) ? $chunk->display_content : $chunk->article_content;
2621 - $content_preview = mb_strlen($content) > $preview_length
2622 - ? mb_substr($content, 0, $preview_length) . '...'
2623 - : $content;
2624 - ?>
2625 - <tr id="prompt-<?php echo esc_attr($chunk->id); ?>"
2626 - class="mxchat-chunk-row <?php echo esc_attr($group_id); ?>"
2627 - data-source="<?php echo esc_attr($data_source); ?>"
2628 - style="display: none; background: #f8f9fa; border-bottom: 1px solid var(--mxch-card-border);">
2629 - <td style="padding: 12px 16px; text-align: center;">
2630 - <!-- Checkbox column placeholder for chunks (managed by group) -->
2631 - </td>
2632 - <td style="padding: 12px 16px 12px 30px; font-size: 13px;">
2633 - <!-- Hidden ID column for chunks -->
2634 - </td>
2635 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2636 - <div class="mxchat-accordion-wrapper">
2637 - <div class="mxchat-content-preview">
2638 - <span class="mxchat-chunk-indicator" style="margin-right: 10px; color: var(--mxch-text-secondary); font-size: 12px;">
2639 - <?php printf(esc_html__('Chunk %d of %d', 'mxchat'), $meta_chunk_index + 1, $meta_total_chunks); ?>
2640 - </span>
2641 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2642 - <?php if (mb_strlen($content) > $preview_length) : ?>
2643 - <button class="mxchat-expand-toggle" type="button">
2644 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2645 - </button>
2646 - <?php endif; ?>
2647 - </div>
2648 - <div class="mxchat-content-full" style="display: none;">
2649 - <div class="content-view">
2650 - <?php
2651 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2652 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2653 - echo wp_kses_post(wpautop($content));
2654 - echo '</div>';
2655 - } else {
2656 - echo wp_kses_post(wpautop($content));
2657 - }
2658 - ?>
2659 - </div>
2660 - </div>
2661 - </div>
2662 - </td>
2663 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2664 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Same as parent', 'mxchat'); ?></span>
2665 - </td>
2666 - <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2667 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Managed by group', 'mxchat'); ?></span>
2668 - </td>
2669 - </tr>
2670 - <?php
2671 - }
2672 - } else {
2673 - // Single entry - display normally with accordion
2674 - $prompt = $first_prompt;
2675 - $content = isset($prompt->display_content) ? $prompt->display_content : $prompt->article_content;
2676 - $content_preview = mb_strlen($content) > $preview_length
2677 - ? mb_substr($content, 0, $preview_length) . '...'
2678 - : $content;
2679 - ?>
2680 - <tr id="prompt-<?php echo esc_attr($prompt->id); ?>"
2681 - data-source="<?php echo esc_attr($data_source); ?>"
2682 - style="border-bottom: 1px solid var(--mxch-card-border);">
2683 - <td style="padding: 12px 16px; text-align: center;">
2684 - <input type="checkbox"
2685 - class="mxchat-entry-checkbox"
2686 - data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2687 - data-source="<?php echo esc_attr($data_source); ?>"
2688 - data-source-url="<?php echo esc_attr($source_url); ?>"
2689 - data-is-group="false">
2690 - </td>
2691 - <td style="padding: 12px 16px; font-size: 13px;">
2692 - <?php echo esc_html($prompt->id); ?>
2693 - </td>
2694 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2695 - <div class="mxchat-accordion-wrapper">
2696 - <div class="mxchat-content-preview">
2697 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2698 - <?php if (mb_strlen($content) > $preview_length) : ?>
2699 - <button class="mxchat-expand-toggle" type="button">
2700 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2701 - </button>
2702 - <?php endif; ?>
2703 - </div>
2704 - <div class="mxchat-content-full" style="display: none;">
2705 - <div class="content-view">
2706 - <?php
2707 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2708 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2709 - echo wp_kses_post(wpautop($content));
2710 - echo '</div>';
2711 - } else {
2712 - echo wp_kses_post(wpautop($content));
2713 - }
2714 - ?>
2715 - </div>
2716 - </div>
2717 - </div>
2718 - </td>
2719 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2720 - <?php
2721 - $actual_source = $source_url;
2722 - if (strpos($source_url, '_ungrouped_') === 0) {
2723 - $actual_source = $prompt->source_url ?? '';
2724 - }
2725 - if (!empty($actual_source) && strpos($actual_source, 'mxchat://') !== 0) : ?>
2726 - <a href="<?php echo esc_url($actual_source); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2727 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2728 - <?php esc_html_e('View', 'mxchat'); ?>
2729 - </a>
2730 - <?php else : ?>
2731 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
2732 - <?php endif; ?>
2733 - </td>
2734 - <td style="padding: 12px 16px; white-space: nowrap;">
2735 - <button type="button"
2736 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2737 - data-source-url="<?php echo esc_attr($prompt->source_url ?? ''); ?>"
2738 - data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2739 - data-data-source="<?php echo esc_attr($data_source); ?>"
2740 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2741 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2742 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2743 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2744 - </button>
2745 - <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);">
2746 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2747 - </button>
2748 - </td>
2749 - </tr>
2750 - <?php
2751 - }
2752 - }
2753 - }
2754 - $html = ob_get_clean();
2755 -
2756 - // Generate pagination HTML (include search/filter data for subsequent pages)
2757 - $pagination_html = '';
2758 - if ($total_pages > 1) {
2759 - $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) . '">';
2760 -
2761 - // Previous button
2762 - if ($page > 1) {
2763 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page - 1) . '">' . esc_html__('&laquo; Previous', 'mxchat') . '</a> ';
2764 - }
2765 -
2766 - // Page numbers
2767 - $start_page = max(1, $page - 2);
2768 - $end_page = min($total_pages, $page + 2);
2769 -
2770 - if ($start_page > 1) {
2771 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="1">1</a> ';
2772 - if ($start_page > 2) {
2773 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2774 - }
2775 - }
2776 -
2777 - for ($i = $start_page; $i <= $end_page; $i++) {
2778 - if ($i == $page) {
2779 - $pagination_html .= '<span class="mxchat-page-current">' . $i . '</span> ';
2780 - } else {
2781 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $i . '">' . $i . '</a> ';
2782 - }
2783 - }
2784 -
2785 - if ($end_page < $total_pages) {
2786 - if ($end_page < $total_pages - 1) {
2787 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2788 - }
2789 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $total_pages . '">' . $total_pages . '</a> ';
2790 - }
2791 -
2792 - // Next button
2793 - if ($page < $total_pages) {
2794 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page + 1) . '">' . esc_html__('Next &raquo;', 'mxchat') . '</a>';
2795 - }
2796 -
2797 - $pagination_html .= '</div>';
2798 - }
2799 -
2800 - wp_send_json_success(array(
2801 - 'html' => $html,
2802 - 'pagination_html' => $pagination_html,
2803 - 'total_count' => $total_records,
2804 - 'total_pages' => $total_pages,
2805 - 'page' => $page,
2806 - 'per_page' => $per_page,
2807 - 'data_source' => 'wordpress'
2808 - ));
2809 -}
2810 -
2811 -/**
2812 - * AJAX handler to detect available sitemaps on the site
2813 - * Optimized for speed - only checks primary sitemap indexes first
2814 - */
2815 -public function ajax_mxchat_detect_sitemaps() {
2816 - check_ajax_referer('mxchat_detect_sitemaps_nonce', 'nonce');
2817 -
2818 - if (!current_user_can('manage_options')) {
2819 - wp_send_json_error(array('message' => 'Unauthorized'));
2820 - return;
2821 - }
2822 -
2823 - $site_url = get_site_url();
2824 - $sitemaps = array();
2825 - $found_index = false;
2826 -
2827 - // Only check the main sitemap index files first (much faster)
2828 - // These are the primary entry points that contain sub-sitemaps
2829 - $primary_indexes = array(
2830 - 'sitemap_index.xml' => 'Yoast SEO / Rank Math', // Yoast & Rank Math
2831 - 'wp-sitemap.xml' => 'WordPress Core', // WordPress Core
2832 - 'sitemap.xml' => 'Standard', // Generic/AIOSEO
2833 - );
2834 -
2835 - foreach ($primary_indexes as $path => $source) {
2836 - $url = trailingslashit($site_url) . $path;
2837 -
2838 - $response = wp_remote_head($url, array(
2839 - 'timeout' => 10,
2840 - 'sslverify' => false,
2841 - 'redirection' => 1,
2842 - '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',
2843 - ));
2844 -
2845 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2846 - // Found a sitemap index - parse it to get sub-sitemaps
2847 - $sub_sitemaps = $this->parse_sitemap_index($url);
2848 - if (!empty($sub_sitemaps)) {
2849 - $sitemaps[] = array(
2850 - 'url' => $url,
2851 - 'type' => 'index',
2852 - 'source' => $source,
2853 - 'sub_sitemaps' => $sub_sitemaps
2854 - );
2855 - $found_index = true;
2856 - // Found a valid index, no need to check others
2857 - break;
2858 - }
2859 - }
2860 - }
2861 -
2862 - // If no sitemap index found, check for standalone sitemaps
2863 - if (!$found_index) {
2864 - $standalone_sitemaps = array(
2865 - 'post-sitemap.xml' => array('type' => 'content', 'source' => 'Yoast SEO'),
2866 - 'page-sitemap.xml' => array('type' => 'content', 'source' => 'Yoast SEO'),
2867 - );
2868 -
2869 - foreach ($standalone_sitemaps as $path => $info) {
2870 - $url = trailingslashit($site_url) . $path;
2871 -
2872 - $response = wp_remote_head($url, array(
2873 - 'timeout' => 2,
2874 - 'sslverify' => false
2875 - ));
2876 -
2877 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2878 - $sitemaps[] = array(
2879 - 'url' => $url,
2880 - 'type' => $info['type'],
2881 - 'source' => $info['source'],
2882 - 'url_count' => 0 // Skip URL count for speed
2883 - );
2884 - }
2885 - }
2886 - }
2887 -
2888 - wp_send_json_success(array(
2889 - 'sitemaps' => $sitemaps,
2890 - 'site_url' => $site_url
2891 - ));
2892 -}
2893 -
2894 -/**
2895 - * Parse a sitemap index to get sub-sitemaps
2896 - * Optimized: doesn't fetch URL count for each sub-sitemap (too slow)
2897 - */
2898 -private function parse_sitemap_index($url) {
2899 - $sub_sitemaps = array();
2900 -
2901 - $response = wp_remote_get($url, array(
2902 - 'timeout' => 30,
2903 - 'sslverify' => false,
2904 - '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',
2905 - 'headers' => array(
2906 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
2907 - 'Accept-Language' => 'en-US,en;q=0.9',
2908 - ),
2909 - ));
2910 -
2911 - if (is_wp_error($response)) {
2912 - return $sub_sitemaps;
2913 - }
2914 -
2915 - $body = wp_remote_retrieve_body($response);
2916 - if (empty($body)) {
2917 - return $sub_sitemaps;
2918 - }
2919 -
2920 - // Suppress XML errors
2921 - libxml_use_internal_errors(true);
2922 - $xml = simplexml_load_string($body);
2923 - libxml_clear_errors();
2924 -
2925 - if ($xml === false) {
2926 - return $sub_sitemaps;
2927 - }
2928 -
2929 - // Check if it's a sitemap index (contains <sitemap> elements)
2930 - if (isset($xml->sitemap)) {
2931 - foreach ($xml->sitemap as $sitemap) {
2932 - $loc = (string) $sitemap->loc;
2933 - if (!empty($loc)) {
2934 - // Try to determine the type from the URL
2935 - $type = 'content';
2936 - if (strpos($loc, 'category') !== false || strpos($loc, 'tag') !== false || strpos($loc, 'taxonomy') !== false) {
2937 - $type = 'taxonomy';
2938 - } elseif (strpos($loc, 'author') !== false || strpos($loc, 'user') !== false) {
2939 - $type = 'author';
2940 - }
2941 -
2942 - // Skip URL count - too slow to fetch for each sitemap
2943 - $sub_sitemaps[] = array(
2944 - 'url' => $loc,
2945 - 'type' => $type,
2946 - 'url_count' => 0, // Don't fetch - takes too long
2947 - 'name' => basename(parse_url($loc, PHP_URL_PATH))
2948 - );
2949 - }
2950 - }
2951 - }
2952 -
2953 - return $sub_sitemaps;
2954 -}
2955 -
2956 -/**
2957 - * Get URL count from a sitemap
2958 - */
2959 -private function get_sitemap_url_count($url) {
2960 - $response = wp_remote_get($url, array(
2961 - 'timeout' => 30,
2962 - 'sslverify' => false,
2963 - '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',
2964 - 'headers' => array(
2965 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
2966 - 'Accept-Language' => 'en-US,en;q=0.9',
2967 - ),
2968 - ));
2969 -
2970 - if (is_wp_error($response)) {
2971 - return 0;
2972 - }
2973 -
2974 - $body = wp_remote_retrieve_body($response);
2975 - if (empty($body)) {
2976 - return 0;
2977 - }
2978 -
2979 - // Count <url> or <loc> elements
2980 - $count = preg_match_all('/<url>/i', $body, $matches);
2981 - return $count ?: 0;
2982 -}
2983 -
2984 -/**
2985 - * Get sitemaps declared in robots.txt
2986 - */
2987 -private function get_sitemaps_from_robots($site_url) {
2988 - $sitemaps = array();
2989 - $robots_url = trailingslashit($site_url) . 'robots.txt';
2990 -
2991 - $response = wp_remote_get($robots_url, array(
2992 - 'timeout' => 15,
2993 - 'sslverify' => false,
2994 - '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',
2995 - ));
2996 -
2997 - if (is_wp_error($response)) {
2998 - return $sitemaps;
2999 - }
3000 -
3001 - $body = wp_remote_retrieve_body($response);
3002 - if (empty($body)) {
3003 - return $sitemaps;
3004 - }
3005 -
3006 - // Find Sitemap: declarations
3007 - if (preg_match_all('/^Sitemap:\s*(.+)$/mi', $body, $matches)) {
3008 - foreach ($matches[1] as $sitemap_url) {
3009 - $sitemap_url = trim($sitemap_url);
3010 - if (filter_var($sitemap_url, FILTER_VALIDATE_URL)) {
3011 - $sitemaps[] = $sitemap_url;
3012 - }
3013 - }
3014 - }
3015 -
3016 - return $sitemaps;
3017 -}
3018 -
3019 -public function mxchat_stop_processing() {
3020 - // Verify permissions
3021 - if (!current_user_can('manage_options')) {
3022 - wp_die(esc_html__('Unauthorized access', 'mxchat'));
3023 - }
3024 -
3025 - // Verify nonce
3026 - check_admin_referer('mxchat_stop_processing_action', 'mxchat_stop_processing_nonce');
3027 -
3028 - global $wpdb;
3029 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
3030 -
3031 - // Get active queue IDs
3032 - $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
3033 - $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
3034 -
3035 - // Delete all pending items from active queues
3036 - if ($sitemap_queue_id) {
3037 - $wpdb->delete(
3038 - $table_name,
3039 - array(
3040 - 'queue_id' => $sitemap_queue_id,
3041 - 'status' => 'pending'
3042 - ),
3043 - array('%s', '%s')
3044 - );
3045 -
3046 - delete_transient('mxchat_active_queue_sitemap');
3047 - delete_transient('mxchat_last_sitemap_url');
3048 - }
3049 -
3050 - if ($pdf_queue_id) {
3051 - // Get PDF path before deleting
3052 - $pdf_path = $this->mxchat_get_queue_meta($pdf_queue_id, 'pdf_path');
3053 -
3054 - $wpdb->delete(
3055 - $table_name,
3056 - array(
3057 - 'queue_id' => $pdf_queue_id,
3058 - 'status' => 'pending'
3059 - ),
3060 - array('%s', '%s')
3061 - );
3062 -
3063 - // Delete PDF file
3064 - if ($pdf_path && file_exists($pdf_path)) {
3065 - wp_delete_file($pdf_path);
3066 - }
3067 -
3068 - delete_transient('mxchat_active_queue_pdf');
3069 - delete_transient('mxchat_last_pdf_url');
3070 - }
3071 -
3072 - // Redirect back with a success message
3073 - set_transient('mxchat_admin_notice_success',
3074 - esc_html__('Processing has been stopped successfully.', 'mxchat'),
3075 - 30
3076 - );
3077 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
3078 - exit;
3079 -}
3080 -
3081 -/**
3082 - * Get content list for processing
3083 - */
3084 -public function ajax_mxchat_get_content_list() {
3085 - // Verify the nonce
3086 - check_ajax_referer('mxchat_content_selector_nonce', 'nonce');
3087 -
3088 - if (!current_user_can('manage_options')) {
3089 - wp_send_json_error(__('Unauthorized access', 'mxchat'));
3090 - }
3091 -
3092 - $page = isset($_GET['page']) ? absint($_GET['page']) : 1;
3093 - $per_page = isset($_GET['per_page']) ? absint($_GET['per_page']) : 100;
3094 - $search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
3095 - $post_type = isset($_GET['post_type']) ? sanitize_text_field($_GET['post_type']) : 'all';
3096 - $post_status = isset($_GET['post_status']) ? sanitize_text_field($_GET['post_status']) : 'publish';
3097 - $processed_filter = isset($_GET['processed_filter']) ? sanitize_text_field($_GET['processed_filter']) : 'all';
3098 -
3099 - // Build query args
3100 - $args = array(
3101 - 'posts_per_page' => $per_page,
3102 - 'paged' => $page,
3103 - 'post_status' => $post_status !== 'all' ? $post_status : array('publish', 'draft', 'pending'),
3104 - 'orderby' => 'date',
3105 - 'order' => 'DESC',
3106 - );
3107 -
3108 - // Handle post types - IMPROVED VERSION
3109 - if ($post_type !== 'all') {
3110 - $args['post_type'] = $post_type;
3111 - } else {
3112 - // Get all available post types that might contain content
3113 - $all_post_types = array();
3114 -
3115 - // First get all public post types
3116 - $public_types = get_post_types(array('public' => true), 'names');
3117 - $all_post_types = array_merge($all_post_types, $public_types);
3118 -
3119 - // Add common forum/community post types
3120 - $forum_types = array('topic', 'reply', 'forum', 'wpforo_topic', 'wpforo_post');
3121 - foreach ($forum_types as $forum_type) {
3122 - if (post_type_exists($forum_type)) {
3123 - $all_post_types[] = $forum_type;
3124 - }
3125 - }
3126 -
3127 - // Add other commonly used post types
3128 - $common_types = array('product', 'job_listing', 'event', 'portfolio');
3129 - foreach ($common_types as $common_type) {
3130 - if (post_type_exists($common_type)) {
3131 - $all_post_types[] = $common_type;
3132 - }
3133 - }
3134 -
3135 - // Remove duplicates and ensure we have at least some post types
3136 - $all_post_types = array_unique($all_post_types);
3137 -
3138 - if (empty($all_post_types)) {
3139 - // Fallback to basic post types
3140 - $all_post_types = array('post', 'page');
3141 - }
3142 -
3143 - $args['post_type'] = $all_post_types;
3144 -
3145 - // Debug logging to see what post types are being queried
3146 - //error_log('MxChat Debug: Querying post types: ' . implode(', ', $all_post_types));
3147 - }
3148 -
3149 - if (!empty($search)) {
3150 - $args['s'] = $search;
3151 - }
3152 -
3153 - // Get processed data from storage
3154 - $processed_data = array();
3155 -
3156 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3157 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3158 -
3159 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3160 - // Get fresh data from Pinecone - no caching
3161 - $processed_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
3162 - } else {
3163 - // WordPress DB checking with better URL matching for all post types
3164 - global $wpdb;
3165 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3166 - $processed_items = $wpdb->get_results("SELECT id, source_url, article_content, timestamp FROM {$table_name}");
3167 -
3168 - // Group items by source_url to count chunks
3169 - $url_chunk_counts = array();
3170 - $url_latest_timestamp = array();
3171 - $url_first_id = array();
3172 -
3173 - if (!empty($processed_items)) {
3174 - foreach ($processed_items as $item) {
3175 - $url = $item->source_url;
3176 - if (empty($url)) continue;
3177 -
3178 - // Count chunks per URL
3179 - if (!isset($url_chunk_counts[$url])) {
3180 - $url_chunk_counts[$url] = 0;
3181 - $url_latest_timestamp[$url] = $item->timestamp;
3182 - $url_first_id[$url] = $item->id;
3183 - }
3184 - $url_chunk_counts[$url]++;
3185 -
3186 - // Track latest timestamp
3187 - if (strtotime($item->timestamp) > strtotime($url_latest_timestamp[$url])) {
3188 - $url_latest_timestamp[$url] = $item->timestamp;
3189 - }
3190 - }
3191 -
3192 - // Now build processed_data with chunk counts
3193 - foreach ($url_chunk_counts as $url => $chunk_count) {
3194 - $post_id = $this->mxchat_url_to_post_id_improved($url);
3195 -
3196 - if ($post_id) {
3197 - $processed_data[$post_id] = array(
3198 - 'db_id' => $url_first_id[$url],
3199 - 'timestamp' => $url_latest_timestamp[$url],
3200 - 'url' => $url,
3201 - 'source' => 'wordpress',
3202 - 'chunk_count' => $chunk_count
3203 - );
3204 - }
3205 - }
3206 - }
3207 - }
3208 -
3209 - // Get processed IDs as a simple array for in_array checks
3210 - $processed_ids = array_keys($processed_data);
3211 -
3212 - // Handle processed/unprocessed filter
3213 - if ($processed_filter === 'processed' && !empty($processed_ids)) {
3214 - $args['post__in'] = $processed_ids;
3215 - } elseif ($processed_filter === 'unprocessed' && !empty($processed_ids)) {
3216 - $args['post__not_in'] = $processed_ids;
3217 - }
3218 -
3219 - // Run the query
3220 - $query = new WP_Query($args);
3221 - $content_items = array();
3222 -
3223 - if ($query->have_posts()) {
3224 - while ($query->have_posts()) {
3225 - $query->the_post();
3226 - $id = get_the_ID();
3227 - $post_date = get_the_date();
3228 - $excerpt = wp_trim_words(get_the_excerpt(), 20, '...');
3229 - $word_count = str_word_count(strip_tags(get_the_content()));
3230 -
3231 - $is_processed = in_array($id, $processed_ids);
3232 - $processed_date = '';
3233 - $db_record_id = 0;
3234 - $data_source = 'none';
3235 -
3236 - if ($is_processed && isset($processed_data[$id])) {
3237 - $item_data = $processed_data[$id];
3238 - $data_source = $item_data['source'];
3239 -
3240 - if ($data_source === 'wordpress' && isset($item_data['timestamp'])) {
3241 - // WordPress DB format
3242 - $timestamp = strtotime($item_data['timestamp']);
3243 - $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
3244 - $db_record_id = $item_data['db_id'];
3245 - } elseif ($data_source === 'pinecone') {
3246 - // Pinecone format
3247 - $processed_date = $item_data['processed_date'];
3248 - $db_record_id = $item_data['db_id'];
3249 - }
3250 - }
3251 -
3252 - // Get chunk count for this item
3253 - $chunk_count = 0;
3254 - if ($is_processed && isset($processed_data[$id]['chunk_count'])) {
3255 - $chunk_count = intval($processed_data[$id]['chunk_count']);
3256 - }
3257 -
3258 - $content_items[] = array(
3259 - 'id' => $id,
3260 - 'title' => get_the_title(),
3261 - 'permalink' => get_permalink(),
3262 - 'date' => $post_date,
3263 - 'type' => get_post_type(),
3264 - 'status' => get_post_status(),
3265 - 'excerpt' => $excerpt,
3266 - 'word_count' => $word_count,
3267 - 'already_processed' => $is_processed,
3268 - 'processed_date' => $processed_date,
3269 - 'db_record_id' => $db_record_id,
3270 - 'data_source' => $data_source,
3271 - 'chunk_count' => $chunk_count
3272 - );
3273 - }
3274 - wp_reset_postdata();
3275 - }
3276 -
3277 - $response = array(
3278 - 'items' => $content_items,
3279 - 'total' => $query->found_posts,
3280 - 'total_pages' => $query->max_num_pages,
3281 - 'current_page' => $page,
3282 - 'processed_count' => count($processed_ids)
3283 - );
3284 -
3285 - wp_send_json_success($response);
3286 - exit;
3287 -}
3288 -
3289 -
3290 -/**
3291 - * This function handles various WooCommerce URL formats and permalink structures
3292 - */
3293 -private function mxchat_url_to_post_id_improved($url) {
3294 - // First try the standard WordPress function
3295 - $post_id = url_to_postid($url);
3296 -
3297 - if ($post_id > 0) {
3298 - return $post_id;
3299 - }
3300 -
3301 - // If that fails, try more aggressive URL matching
3302 - // Remove trailing slashes and query parameters for better matching
3303 - $clean_url = rtrim($url, '/');
3304 - $clean_url = strtok($clean_url, '?'); // Remove query parameters
3305 -
3306 - // Try again with cleaned URL
3307 - $post_id = url_to_postid($clean_url);
3308 - if ($post_id > 0) {
3309 - return $post_id;
3310 - }
3311 -
3312 - // For bbPress forum topics, try extracting slug from URL
3313 - if (strpos($url, '/topic/') !== false || strpos($url, '/forums/') !== false) {
3314 - // Handle bbPress URLs: /forums/topic/topic-name/
3315 - if (preg_match('/\/forums\/topic\/([^\/\?]+)/', $url, $matches)) {
3316 - $topic_slug = $matches[1];
3317 -
3318 - // Look up topic by slug
3319 - $topic = get_page_by_path($topic_slug, OBJECT, 'topic');
3320 - if ($topic) {
3321 - return $topic->ID;
3322 - }
3323 -
3324 - // Alternative method: query by post_name
3325 - global $wpdb;
3326 - $post_id = $wpdb->get_var($wpdb->prepare(
3327 - "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
3328 - $topic_slug
3329 - ));
3330 -
3331 - if ($post_id) {
3332 - return intval($post_id);
3333 - }
3334 - }
3335 -
3336 - // Handle simpler topic URLs: /topic/topic-name/
3337 - if (preg_match('/\/topic\/([^\/\?]+)/', $url, $matches)) {
3338 - $topic_slug = $matches[1];
3339 -
3340 - global $wpdb;
3341 - $post_id = $wpdb->get_var($wpdb->prepare(
3342 - "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
3343 - $topic_slug
3344 - ));
3345 -
3346 - if ($post_id) {
3347 - return intval($post_id);
3348 - }
3349 - }
3350 - }
3351 -
3352 - // For WooCommerce products
3353 - if (strpos($url, '/product/') !== false || strpos($url, 'product=') !== false) {
3354 - // Extract product slug from various URL formats
3355 - $product_slug = '';
3356 -
3357 - // Handle pretty permalinks: /product/product-name/
3358 - if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
3359 - $product_slug = $matches[1];
3360 - }
3361 - // Handle query parameters: ?product=product-name
3362 - elseif (preg_match('/[\?&]product=([^&]+)/', $url, $matches)) {
3363 - $product_slug = $matches[1];
3364 - }
3365 -
3366 - if (!empty($product_slug)) {
3367 - // Look up product by slug
3368 - $product = get_page_by_path($product_slug, OBJECT, 'product');
3369 - if ($product) {
3370 - return $product->ID;
3371 - }
3372 -
3373 - // Alternative method: query by post_name
3374 - global $wpdb;
3375 - $post_id = $wpdb->get_var($wpdb->prepare(
3376 - "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'product' AND post_status = 'publish'",
3377 - $product_slug
3378 - ));
3379 -
3380 - if ($post_id) {
3381 - return intval($post_id);
3382 - }
3383 - }
3384 - }
3385 -
3386 - // Generic approach: try to extract slug and match against all post types
3387 - $parsed_url = wp_parse_url($clean_url);
3388 - $path = $parsed_url['path'] ?? '';
3389 -
3390 - if (!empty($path)) {
3391 - // Get the last part of the path as potential slug
3392 - $path_parts = array_filter(explode('/', trim($path, '/')));
3393 - $potential_slug = end($path_parts);
3394 -
3395 - if (!empty($potential_slug)) {
3396 - global $wpdb;
3397 -
3398 - // Try to find any post with this slug
3399 - $post_id = $wpdb->get_var($wpdb->prepare(
3400 - "SELECT ID FROM {$wpdb->posts}
3401 - WHERE post_name = %s
3402 - AND post_status IN ('publish', 'closed', 'private')
3403 - AND post_type NOT IN ('revision', 'attachment', 'nav_menu_item')
3404 - ORDER BY CASE
3405 - WHEN post_type = 'post' THEN 1
3406 - WHEN post_type = 'page' THEN 2
3407 - WHEN post_type = 'topic' THEN 3
3408 - WHEN post_type = 'product' THEN 4
3409 - ELSE 5
3410 - END
3411 - LIMIT 1",
3412 - $potential_slug
3413 - ));
3414 -
3415 - if ($post_id) {
3416 - return intval($post_id);
3417 - }
3418 - }
3419 - }
3420 -
3421 - // ADDITIONAL: Try direct database lookup by URL variations
3422 - global $wpdb;
3423 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3424 -
3425 - // Try variations of the URL (with/without trailing slash, http/https)
3426 - $url_variations = array(
3427 - $url,
3428 - rtrim($url, '/'),
3429 - $url . '/',
3430 - str_replace('http://', 'https://', $url),
3431 - str_replace('https://', 'http://', $url),
3432 - str_replace('http://', 'https://', rtrim($url, '/')),
3433 - str_replace('https://', 'http://', rtrim($url, '/'))
3434 - );
3435 -
3436 - // Remove duplicates
3437 - $url_variations = array_unique($url_variations);
3438 -
3439 - foreach ($url_variations as $variation) {
3440 - $existing_record = $wpdb->get_row($wpdb->prepare(
3441 - "SELECT id, source_url FROM $table_name WHERE source_url = %s",
3442 - $variation
3443 - ));
3444 -
3445 - if ($existing_record) {
3446 - // Try to get post ID from this stored URL
3447 - $stored_post_id = url_to_postid($existing_record->source_url);
3448 - if ($stored_post_id > 0) {
3449 - return $stored_post_id;
3450 - }
3451 - }
3452 - }
3453 -
3454 - return 0; // No match found
3455 -}
3456 -/**
3457 - * Process selected content via AJAX
3458 - */
3459 -public function ajax_mxchat_process_selected_content() {
3460 - // Basic request validation
3461 - if (!check_ajax_referer('mxchat_content_selector_nonce', 'nonce', false)) {
3462 - wp_send_json_error('Invalid nonce');
3463 - exit;
3464 - }
3465 -
3466 - if (!current_user_can('manage_options')) {
3467 - wp_send_json_error('Unauthorized access');
3468 - exit;
3469 - }
3470 -
3471 - // Get post IDs - safely parse the array
3472 - $post_ids = array();
3473 - if (isset($_POST['post_ids']) && is_array($_POST['post_ids'])) {
3474 - foreach ($_POST['post_ids'] as $id) {
3475 - $post_ids[] = absint($id);
3476 - }
3477 - }
3478 -
3479 - if (empty($post_ids)) {
3480 - wp_send_json_error('No content selected');
3481 - exit;
3482 - }
3483 -
3484 - // Get bot_id from request
3485 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
3486 -
3487 - // ACF→PDF extraction is opt-in per import batch. Persist the last-used value so users
3488 - // don't re-check on every batch; the default is OFF for installs that haven't set it.
3489 - $extract_acf_pdfs = !empty($_POST['extract_acf_pdfs']) && $_POST['extract_acf_pdfs'] !== 'false';
3490 - $mxchat_options = get_option('mxchat_options', array());
3491 - if (!is_array($mxchat_options)) {
3492 - $mxchat_options = array();
3493 - }
3494 - $prior_default = !empty($mxchat_options['acf_pdf_extract_default']);
3495 - if ($prior_default !== $extract_acf_pdfs) {
3496 - $mxchat_options['acf_pdf_extract_default'] = $extract_acf_pdfs ? 1 : 0;
3497 - update_option('mxchat_options', $mxchat_options);
3498 - }
3499 -
3500 - // Process only ONE post at a time to avoid request size issues
3501 - $post_id = reset($post_ids);
3502 - $post = get_post($post_id);
3503 -
3504 - if (!$post) {
3505 - wp_send_json_error('Post not found');
3506 - exit;
3507 - }
3508 -
3509 - // Allow developers to modify post data before processing into knowledge base
3510 - $post = apply_filters('mxchat_before_process_post', $post, $bot_id);
3511 -
3512 - // Get content including title, short description (for WooCommerce), and main content
3513 - $content = $post->post_title . "\n\n";
3514 -
3515 - // Add short description if it exists (WooCommerce products use post_excerpt for short description)
3516 - if (!empty($post->post_excerpt)) {
3517 - // Remove shortcode tags but preserve content inside them
3518 - $clean_excerpt = $this->strip_shortcode_tags_preserve_content($post->post_excerpt);
3519 - $content .= "Short Description: " . wp_strip_all_tags($clean_excerpt) . "\n\n";
3520 - }
3521 -
3522 - // Add main content - remove shortcode tags but preserve content inside them
3523 - $clean_content = $this->strip_shortcode_tags_preserve_content($post->post_content);
3524 - $content .= wp_strip_all_tags($clean_content);
3525 -
3526 - // ADD WOOCOMMERCE PRODUCT DATA (pricing, stock, categories, custom tabs)
3527 - if (get_post_type($post_id) === 'product' && class_exists('WooCommerce')) {
3528 - $product = wc_get_product($post_id);
3529 -
3530 - if ($product) {
3531 - // Get pricing information
3532 - $regular_price = $product->get_regular_price();
3533 - $sale_price = $product->get_sale_price();
3534 - $price = $product->get_price();
3535 - $sku = $product->get_sku();
3536 -
3537 - // Get currency symbol
3538 - $currency_symbol = get_woocommerce_currency_symbol();
3539 -
3540 - // Add pricing information
3541 - $content .= "\n";
3542 - if (!empty($regular_price)) {
3543 - $content .= "Price: " . $currency_symbol . $regular_price . "\n";
3544 - } elseif (!empty($price)) {
3545 - $content .= "Price: " . $currency_symbol . $price . "\n";
3546 - }
3547 -
3548 - if (!empty($sale_price) && $sale_price !== $regular_price) {
3549 - $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
3550 - }
3551 -
3552 - // Handle variable products - show price range
3553 - if ($product->is_type('variable')) {
3554 - $min_price = $product->get_variation_price('min');
3555 - $max_price = $product->get_variation_price('max');
3556 - if ($min_price !== $max_price) {
3557 - $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
3558 - }
3559 - }
3560 -
3561 - if (!empty($sku)) {
3562 - $content .= "SKU: " . $sku . "\n";
3563 - }
3564 -
3565 - // Get product categories
3566 - $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
3567 - if (!empty($categories) && !is_wp_error($categories)) {
3568 - $content .= "Categories: " . implode(', ', $categories) . "\n";
3569 - }
3570 - }
3571 -
3572 - // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
3573 - $custom_tabs = get_post_meta($post_id, 'yikes_woo_products_tabs', true);
3574 - if (!empty($custom_tabs) && is_array($custom_tabs)) {
3575 - foreach ($custom_tabs as $tab) {
3576 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
3577 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
3578 -
3579 - if (!empty($tab_title) && !empty($tab_content)) {
3580 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
3581 - }
3582 - }
3583 - }
3584 -
3585 - // Also check for reusable/saved tabs applied to this product
3586 - $applied_saved_tabs = get_post_meta($post_id, 'yikes_woo_reusable_products_tabs_applied', true);
3587 - if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
3588 - $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
3589 - if (!empty($saved_tabs) && is_array($saved_tabs)) {
3590 - foreach ($applied_saved_tabs as $saved_tab_id) {
3591 - if (isset($saved_tabs[$saved_tab_id])) {
3592 - $tab = $saved_tabs[$saved_tab_id];
3593 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
3594 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
3595 -
3596 - if (!empty($tab_title) && !empty($tab_content)) {
3597 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
3598 - }
3599 - }
3600 - }
3601 - }
3602 - }
3603 - }
3604 -
3605 - // ADD ACF FIELDS SUPPORT
3606 - $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
3607 - $pdf_extracted_count = 0;
3608 - if (!empty($acf_fields)) {
3609 - $acf_content_parts = array();
3610 - $pdf_attachment_ids = array();
3611 -
3612 - foreach ($acf_fields as $field_name => $field_value) {
3613 - $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
3614 -
3615 - if (!empty($formatted_value)) {
3616 - $field_label = ucwords(str_replace('_', ' ', $field_name));
3617 - $acf_content_parts[] = $field_label . ": " . $formatted_value;
3618 - }
3619 -
3620 - // Walk this field's value tree for any PDF attachment references and queue them for extraction.
3621 - // Only when the user opted into ACF→PDF extraction for this batch; otherwise the ACF text
3622 - // still lands in the KB but the heavier PDF parsing is skipped.
3623 - if ($extract_acf_pdfs) {
3624 - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($field_value, $pdf_attachment_ids);
3625 - }
3626 - }
3627 -
3628 - if (!empty($acf_content_parts)) {
3629 - $content .= "\n\n" . implode("\n", $acf_content_parts);
3630 - }
3631 -
3632 - // Extract text from each unique PDF found in ACF fields and append as a labeled section
3633 - if ($extract_acf_pdfs && !empty($pdf_attachment_ids)) {
3634 - $pdf_attachment_ids = array_unique(array_filter(array_map('intval', $pdf_attachment_ids)));
3635 - $pdf_sections = array();
3636 - foreach ($pdf_attachment_ids as $att_id) {
3637 - $pdf_text = $this->mxchat_extract_pdf_text_by_attachment_id($att_id);
3638 - if (!empty($pdf_text)) {
3639 - $pdf_title = get_the_title($att_id);
3640 - $pdf_url = wp_get_attachment_url($att_id);
3641 - $header = 'PDF Attachment';
3642 - if (!empty($pdf_title)) {
3643 - $header .= ': ' . $pdf_title;
3644 - }
3645 - if (!empty($pdf_url)) {
3646 - $header .= ' (' . $pdf_url . ')';
3647 - }
3648 - $pdf_sections[] = $header . "\n" . $pdf_text;
3649 - $pdf_extracted_count++;
3650 - }
3651 - }
3652 - if (!empty($pdf_sections)) {
3653 - $content .= "\n\nAttached PDFs:\n" . implode("\n\n", $pdf_sections);
3654 - }
3655 - }
3656 - }
3657 -
3658 - // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
3659 - $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
3660 - if (!empty($custom_meta)) {
3661 - $meta_content_parts = array();
3662 -
3663 - foreach ($custom_meta as $meta_key => $meta_value) {
3664 - // Convert meta key to readable label
3665 - $meta_label = ucwords(str_replace(array('_', '-'), ' ', $meta_key));
3666 - $meta_content_parts[] = $meta_label . ": " . $meta_value;
3667 - }
3668 -
3669 - if (!empty($meta_content_parts)) {
3670 - $content .= "\n\n" . implode("\n", $meta_content_parts);
3671 - }
3672 - }
3673 -
3674 - // Debug logging for WordPress Import content
3675 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Post ID: ' . $post_id . ' Title: ' . $post->post_title);
3676 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Raw post_content length: ' . strlen($post->post_content));
3677 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Final content length: ' . strlen($content));
3678 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Content preview: ' . substr($content, 0, 300));
3679 -
3680 - // Note: Removed 10,000 char limit - chunking now handles large content properly
3681 -
3682 - // Get bot-specific API key
3683 - $bot_options = $this->get_bot_options($bot_id);
3684 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
3685 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3686 -
3687 - if (strpos($selected_model, 'voyage') === 0) {
3688 - $api_key = $options['voyage_api_key'] ?? '';
3689 - $provider_name = 'Voyage AI';
3690 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3691 - $api_key = $options['gemini_api_key'] ?? '';
3692 - $provider_name = 'Google Gemini';
3693 - } else {
3694 - $api_key = $options['api_key'] ?? '';
3695 - $provider_name = 'OpenAI';
3696 - }
3697 -
3698 - if (empty($api_key)) {
3699 - MxChat_Admin::mxchat_log_debug('api_error', $provider_name . ' API key not configured for knowledge processing');
3700 - wp_send_json_error($provider_name . ' API key not configured');
3701 - exit;
3702 - }
3703 -
3704 - $source_url = get_permalink($post_id);
3705 - $vector_id = md5($source_url); // Vector ID for Pinecone
3706 -
3707 - // Check for existing content in bot-specific storage
3708 - $is_update = false;
3709 -
3710 - // Get bot-specific Pinecone configuration
3711 - $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
3712 - $use_pinecone = !empty($bot_pinecone_config) && ($bot_pinecone_config['use_pinecone'] ?? false);
3713 -
3714 - if ($use_pinecone && !empty($bot_pinecone_config['api_key'])) {
3715 - // Check Pinecone for this bot
3716 - $pinecone_data = $this->mxchat_get_pinecone_processed_content($bot_pinecone_config);
3717 - if (isset($pinecone_data[$post_id])) {
3718 - $is_update = true;
3719 - }
3720 - } else {
3721 - // Check WordPress DB (same as before since it's shared)
3722 - global $wpdb;
3723 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3724 - $existing_record = $wpdb->get_row($wpdb->prepare(
3725 - "SELECT id FROM $table_name WHERE source_url = %s",
3726 - $source_url
3727 - ));
3728 -
3729 - if ($existing_record) {
3730 - $is_update = true;
3731 - }
3732 - }
3733 -
3734 - // UPDATED 2.5.6: Determine content type based on post_type
3735 - $post_type = $post->post_type;
3736 - $content_type = 'content'; // Default fallback
3737 -
3738 - // Map WordPress post types to content types
3739 - switch ($post_type) {
3740 - case 'post':
3741 - $content_type = 'post';
3742 - break;
3743 - case 'page':
3744 - $content_type = 'page';
3745 - break;
3746 - case 'product':
3747 - $content_type = 'product';
3748 - break;
3749 - default:
3750 - // For custom post types, use the post type name
3751 - $content_type = sanitize_key($post_type);
3752 - break;
3753 - }
3754 -
3755 - // Use the centralized utility function with bot_id and content_type
3756 - $result = MxChat_Utils::submit_content_to_db(
3757 - $content,
3758 - $source_url,
3759 - $api_key,
3760 - $vector_id,
3761 - $bot_id,
3762 - $content_type
3763 - );
3764 -
3765 - if (is_wp_error($result)) {
3766 - MxChat_Admin::mxchat_log_debug('storage_error', 'Knowledge storage failed: ' . $result->get_error_message(), array('source_url' => $source_url));
3767 - wp_send_json_error('Storage failed: ' . $result->get_error_message());
3768 - exit;
3769 - }
3770 -
3771 - // Automatically apply role restriction based on tags
3772 - $this->apply_role_restriction_to_post($post_id, $source_url);
3773 -
3774 - $operation_type = $is_update ? 'update' : 'new';
3775 -
3776 - // Count ACF fields for debugging
3777 - $acf_field_count = count($acf_fields);
3778 -
3779 - // Success response with minimal data
3780 - wp_send_json_success(array(
3781 - 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully',
3782 - 'post_id' => $post_id,
3783 - 'title' => $post->post_title,
3784 - 'operation_type' => $operation_type,
3785 - 'vector_id' => $vector_id,
3786 - 'acf_fields_found' => $acf_field_count,
3787 - 'pdf_extracted_count' => (int) $pdf_extracted_count,
3788 - 'content_preview' => substr($content, 0, 100) . '...',
3789 - 'bot_id' => $bot_id
3790 - ));
3791 - exit;
3792 -}
3793 -
3794 -private function apply_role_restriction_to_post($post_id, $source_url) {
3795 - // Get tag-role mappings
3796 - $mappings = get_option('mxchat_tag_role_mappings', array());
3797 -
3798 - if (empty($mappings)) {
3799 - return; // No mappings, leave as public
3800 - }
3801 -
3802 - // Get all tags for the post
3803 - $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
3804 -
3805 - if (empty($post_tags)) {
3806 - return; // No tags, leave as public
3807 - }
3808 -
3809 - // Determine the highest role restriction based on tags
3810 - $highest_role = 'public';
3811 - $role_hierarchy = array(
3812 - 'public' => 0,
3813 - 'logged_in' => 1,
3814 - 'subscriber' => 2,
3815 - 'contributor' => 3,
3816 - 'author' => 4,
3817 - 'editor' => 5,
3818 - 'administrator' => 6
3819 - );
3820 -
3821 - foreach ($post_tags as $tag_slug) {
3822 - if (isset($mappings[$tag_slug])) {
3823 - $role = $mappings[$tag_slug];
3824 - if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
3825 - $highest_role = $role;
3826 - }
3827 - }
3828 - }
3829 -
3830 - // If no restricted tags found, return (leave as public)
3831 - if ($highest_role === 'public') {
3832 - return;
3833 - }
3834 -
3835 - // Update the role restriction in the database
3836 - global $wpdb;
3837 -
3838 - // Check if using Pinecone
3839 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3840 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3841 -
3842 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3843 - // Update Pinecone role restriction
3844 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
3845 - $vector_id = md5($source_url);
3846 -
3847 - $wpdb->replace(
3848 - $roles_table,
3849 - array(
3850 - 'vector_id' => $vector_id,
3851 - 'role_restriction' => $highest_role,
3852 - 'updated_at' => current_time('mysql')
3853 - ),
3854 - array('%s', '%s', '%s')
3855 - );
3856 - } else {
3857 - // Update WordPress DB
3858 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3859 -
3860 - $wpdb->update(
3861 - $table_name,
3862 - array('role_restriction' => $highest_role),
3863 - array('source_url' => $source_url),
3864 - array('%s'),
3865 - array('%s')
3866 - );
3867 - }
3868 -}
3869 -
3870 -public function mxchat_get_public_post_types() {
3871 - // Get all public post types
3872 - $post_types = get_post_types(array('public' => true), 'objects');
3873 - $post_type_options = array();
3874 -
3875 - foreach ($post_types as $post_type) {
3876 - $post_type_options[$post_type->name] = $post_type->label;
3877 - }
3878 -
3879 - // Also include common forum/community post types that might not be marked as public
3880 - $additional_types = array(
3881 - 'topic' => 'Forum Topics (bbPress)',
3882 - 'reply' => 'Forum Replies (bbPress)',
3883 - 'forum' => 'Forums (bbPress)',
3884 - 'wpforo_topic' => 'wpForo Topics',
3885 - 'wpforo_post' => 'wpForo Posts'
3886 - );
3887 -
3888 - foreach ($additional_types as $type_name => $type_label) {
3889 - if (post_type_exists($type_name) && !isset($post_type_options[$type_name])) {
3890 - $post_type_options[$type_name] = $type_label;
3891 - }
3892 - }
3893 -
3894 - return $post_type_options;
3895 -}
3896 -
3897 -/**
3898 - * Retrieves processed content from Pinecone API
3899 - */
3900 -public function mxchat_get_pinecone_processed_content($pinecone_options) {
3901 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3902 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3903 -
3904 - if (empty($api_key) || empty($host)) {
3905 - return array();
3906 - }
3907 -
3908 - $pinecone_data = array();
3909 -
3910 - try {
3911 - // Always get fresh data from Pinecone
3912 - $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
3913 -
3914 - // Method 2: Final fallback - try stats endpoint (if available)
3915 - if (empty($pinecone_data)) {
3916 - $stats_url = "https://{$host}/describe_index_stats";
3917 -
3918 - $response = wp_remote_post($stats_url, array(
3919 - 'headers' => array(
3920 - 'Api-Key' => $api_key,
3921 - 'Content-Type' => 'application/json'
3922 - ),
3923 - 'body' => json_encode(array()),
3924 - 'timeout' => 30
3925 - ));
3926 -
3927 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
3928 - $body = wp_remote_retrieve_body($response);
3929 - $stats_data = json_decode($body, true);
3930 - }
3931 - }
3932 -
3933 - } catch (Exception $e) {
3934 - // Log error but return fresh data only
3935 - }
3936 -
3937 - return $pinecone_data;
3938 -}
3939 -public function mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
3940 - //error_log('=== DEBUG: Starting mxchat_fetch_pinecone_vectors_by_ids ===');
3941 -
3942 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3943 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3944 -
3945 - if (empty($api_key) || empty($host) || empty($vector_ids)) {
3946 - //error_log('DEBUG: Missing parameters for fetch by IDs');
3947 - return array();
3948 - }
3949 -
3950 - try {
3951 - $fetch_url = "https://{$host}/vectors/fetch";
3952 - //error_log('DEBUG: Fetch URL: ' . $fetch_url);
3953 - //error_log('DEBUG: Fetching ' . count($vector_ids) . ' vector IDs');
3954 -
3955 - // Pinecone fetch API allows fetching specific vectors by ID
3956 - $fetch_data = array(
3957 - 'ids' => array_values($vector_ids)
3958 - );
3959 -
3960 - $response = wp_remote_post($fetch_url, array(
3961 - 'headers' => array(
3962 - 'Api-Key' => $api_key,
3963 - 'Content-Type' => 'application/json'
3964 - ),
3965 - 'body' => json_encode($fetch_data),
3966 - 'timeout' => 30
3967 - ));
3968 -
3969 - if (is_wp_error($response)) {
3970 - //error_log('DEBUG: Fetch by IDs WP error: ' . $response->get_error_message());
3971 - return array();
3972 - }
3973 -
3974 - $response_code = wp_remote_retrieve_response_code($response);
3975 - //error_log('DEBUG: Fetch response code: ' . $response_code);
3976 -
3977 - if ($response_code !== 200) {
3978 - $error_body = wp_remote_retrieve_body($response);
3979 - //error_log('DEBUG: Fetch failed with body: ' . $error_body);
3980 - return array();
3981 - }
3982 -
3983 - $body = wp_remote_retrieve_body($response);
3984 - $data = json_decode($body, true);
3985 -
3986 - //error_log('DEBUG: Fetch response structure: ' . print_r(array_keys($data), true));
3987 -
3988 - if (!isset($data['vectors'])) {
3989 - //error_log('DEBUG: No vectors key in response');
3990 - return array();
3991 - }
3992 -
3993 - $processed_data = array();
3994 -
3995 - foreach ($data['vectors'] as $vector_id => $vector_data) {
3996 - $metadata = $vector_data['metadata'] ?? array();
3997 - $source_url = $metadata['source_url'] ?? '';
3998 -
3999 - if (!empty($source_url)) {
4000 - $post_id = url_to_postid($source_url);
4001 - if ($post_id) {
4002 - $created_at = $metadata['created_at'] ?? '';
4003 - $processed_date = 'Recently';
4004 -
4005 - if (!empty($created_at)) {
4006 - $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
4007 - if ($timestamp) {
4008 - $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
4009 - }
4010 - }
4011 -
4012 - $processed_data[$post_id] = array(
4013 - 'db_id' => $vector_id,
4014 - 'processed_date' => $processed_date,
4015 - 'url' => $source_url,
4016 - 'source' => 'pinecone',
4017 - 'timestamp' => $timestamp ?? current_time('timestamp')
4018 - );
4019 - }
4020 - }
4021 - }
4022 -
4023 - //error_log('DEBUG: Processed ' . count($processed_data) . ' vectors from fetch');
4024 - return $processed_data;
4025 -
4026 - } catch (Exception $e) {
4027 - //error_log('DEBUG: Exception in fetch_pinecone_vectors_by_ids: ' . $e->getMessage());
4028 - return array();
4029 - }
4030 -}
4031 -
4032 -/**
4033 - * Get embedding dimensions based on the selected model.
4034 - */
4035 -private function mxchat_get_embedding_dimensions() {
4036 - $options = get_option('mxchat_options', array());
4037 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4038 -
4039 - $model_dimensions = array(
4040 - 'text-embedding-ada-002' => 1536,
4041 - 'text-embedding-3-small' => 1536,
4042 - 'text-embedding-3-large' => 3072,
4043 - 'voyage-2' => 1024,
4044 - 'voyage-large-2' => 1536,
4045 - 'voyage-3-large' => 2048,
4046 - 'gemini-embedding-001' => 1536,
4047 - );
4048 -
4049 - if (strpos($selected_model, 'voyage-3-large') === 0) {
4050 - $custom_dimensions = $options['voyage_output_dimension'] ?? 2048;
4051 - return intval($custom_dimensions);
4052 - }
4053 -
4054 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4055 - $custom_dimensions = $options['gemini_output_dimension'] ?? 1536;
4056 - return intval($custom_dimensions);
4057 - }
4058 -
4059 - return $model_dimensions[$selected_model] ?? 1536;
4060 -}
4061 -
4062 -/**
4063 - * Scan Pinecone for processed content
4064 - */
4065 -public function mxchat_scan_pinecone_for_processed_content($pinecone_options) {
4066 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
4067 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
4068 -
4069 - if (empty($api_key) || empty($host)) {
4070 - return array();
4071 - }
4072 -
4073 - try {
4074 - // Use multiple random vectors to get better coverage
4075 - $all_matches = array();
4076 - $seen_ids = array();
4077 -
4078 - // Get correct dimensions for the configured embedding model
4079 - $dimensions = $this->mxchat_get_embedding_dimensions();
4080 -
4081 - // Try 3 different random vectors to get better coverage
4082 - for ($i = 0; $i < 3; $i++) {
4083 - $query_url = "https://{$host}/query";
4084 -
4085 - // Generate a random unit vector instead of zeros
4086 - $random_vector = array();
4087 - for ($j = 0; $j < $dimensions; $j++) {
4088 - $random_vector[] = (rand(-1000, 1000) / 1000.0);
4089 - }
4090 -
4091 - // Normalize the vector to unit length
4092 - $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
4093 - if ($magnitude > 0) {
4094 - $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
4095 - }
4096 -
4097 - $query_data = array(
4098 - 'includeMetadata' => true,
4099 - 'includeValues' => false,
4100 - 'topK' => 10000,
4101 - 'vector' => $random_vector
4102 - );
4103 -
4104 - $response = wp_remote_post($query_url, array(
4105 - 'headers' => array(
4106 - 'Api-Key' => $api_key,
4107 - 'Content-Type' => 'application/json'
4108 - ),
4109 - 'body' => json_encode($query_data),
4110 - 'timeout' => 30
4111 - ));
4112 -
4113 - if (is_wp_error($response)) {
4114 - continue;
4115 - }
4116 -
4117 - $response_code = wp_remote_retrieve_response_code($response);
4118 -
4119 - if ($response_code !== 200) {
4120 - continue;
4121 - }
4122 -
4123 - $body = wp_remote_retrieve_body($response);
4124 - $data = json_decode($body, true);
4125 -
4126 - if (isset($data['matches'])) {
4127 - foreach ($data['matches'] as $match) {
4128 - $match_id = $match['id'] ?? '';
4129 - if (!empty($match_id) && !isset($seen_ids[$match_id])) {
4130 - $all_matches[] = $match;
4131 - $seen_ids[$match_id] = true;
4132 - }
4133 - }
4134 - }
4135 - }
4136 -
4137 - // Convert matches to processed data format, grouping by URL to count chunks
4138 - $processed_data = array();
4139 - $url_chunk_counts = array();
4140 -
4141 - foreach ($all_matches as $match) {
4142 - $metadata = $match['metadata'] ?? array();
4143 - $source_url = $metadata['source_url'] ?? '';
4144 - $match_id = $match['id'] ?? '';
4145 -
4146 - if (!empty($source_url) && !empty($match_id)) {
4147 - $post_id = url_to_postid($source_url);
4148 - if ($post_id) {
4149 - // Count chunks per post_id
4150 - if (!isset($url_chunk_counts[$post_id])) {
4151 - $url_chunk_counts[$post_id] = 0;
4152 - }
4153 - $url_chunk_counts[$post_id]++;
4154 -
4155 - $created_at = $metadata['created_at'] ?? '';
4156 - $processed_date = 'Recently';
4157 -
4158 - if (!empty($created_at)) {
4159 - $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
4160 - if ($timestamp) {
4161 - $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
4162 - }
4163 - }
4164 -
4165 - // Only store if not already set, or update with newer timestamp
4166 - if (!isset($processed_data[$post_id]) ||
4167 - ($timestamp ?? 0) > ($processed_data[$post_id]['timestamp'] ?? 0)) {
4168 - $processed_data[$post_id] = array(
4169 - 'db_id' => $match_id,
4170 - 'processed_date' => $processed_date,
4171 - 'url' => $source_url,
4172 - 'source' => 'pinecone',
4173 - 'timestamp' => $timestamp ?? current_time('timestamp')
4174 - );
4175 - }
4176 - }
4177 - }
4178 - }
4179 -
4180 - // Add chunk counts to processed data
4181 - foreach ($url_chunk_counts as $post_id => $chunk_count) {
4182 - if (isset($processed_data[$post_id])) {
4183 - $processed_data[$post_id]['chunk_count'] = $chunk_count;
4184 - }
4185 - }
4186 -
4187 - return $processed_data;
4188 -
4189 - } catch (Exception $e) {
4190 - return array();
4191 - }
4192 -}
4193 -/**
4194 - * Generate embeddings from input text for MXChat with bot support
4195 - */
4196 -private function mxchat_generate_embedding($text, $bot_id = 'default') {
4197 - // Enable detailed logging for debugging
4198 - //error_log('[MXCHAT-EMBED] Starting embedding generation for bot: ' . $bot_id . '. Text length: ' . strlen($text) . ' bytes');
4199 - //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
4200 -
4201 - // Get bot-specific options
4202 - $bot_options = $this->get_bot_options($bot_id);
4203 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
4204 -
4205 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4206 - //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model);
4207 -
4208 - // Determine provider and endpoint
4209 - if (strpos($selected_model, 'voyage') === 0) {
4210 - $api_key = $options['voyage_api_key'] ?? '';
4211 - $endpoint = 'https://api.voyageai.com/v1/embeddings';
4212 - $provider_name = 'Voyage AI';
4213 - //error_log('[MXCHAT-EMBED] Using Voyage AI API for bot ' . $bot_id);
4214 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
4215 - $api_key = $options['gemini_api_key'] ?? '';
4216 - $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
4217 - $provider_name = 'Google Gemini';
4218 - //error_log('[MXCHAT-EMBED] Using Google Gemini API for bot ' . $bot_id);
4219 - } else {
4220 - $api_key = $options['api_key'] ?? '';
4221 - $endpoint = 'https://api.openai.com/v1/embeddings';
4222 - $provider_name = 'OpenAI';
4223 - //error_log('[MXCHAT-EMBED] Using OpenAI API for bot ' . $bot_id);
4224 - }
4225 -
4226 - //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
4227 -
4228 - if (empty($api_key)) {
4229 - $error_message = sprintf('Missing %s API key for bot %s. Please configure your API key in the bot settings.', $provider_name, $bot_id);
4230 - //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
4231 - return $error_message;
4232 - }
4233 -
4234 - // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
4235 - $estimated_tokens = ceil(str_word_count($text) / 0.75);
4236 - //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
4237 -
4238 - if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
4239 - //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
4240 - // Consider truncating text here
4241 - }
4242 -
4243 - // Prepare request body based on provider
4244 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4245 - // Gemini API format
4246 - $request_body = array(
4247 - 'model' => 'models/' . $selected_model,
4248 - 'content' => array(
4249 - 'parts' => array(
4250 - array('text' => $text)
4251 - )
4252 - )
4253 - );
4254 -
4255 - // Set output dimensionality to 1536 for consistency with other models
4256 - $request_body['outputDimensionality'] = 1536;
4257 - } else {
4258 - // OpenAI/Voyage API format
4259 - $request_body = array(
4260 - 'model' => $selected_model,
4261 - 'input' => $text
4262 - );
4263 -
4264 - // Add output_dimension for voyage-3-large model
4265 - if ($selected_model === 'voyage-3-large') {
4266 - $request_body['output_dimension'] = 2048;
4267 - }
4268 - }
4269 -
4270 - //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
4271 -
4272 - // Prepare headers based on provider
4273 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4274 - // Gemini uses API key as query parameter
4275 - $endpoint .= '?key=' . $api_key;
4276 - $headers = array(
4277 - 'Content-Type' => 'application/json'
4278 - );
4279 - } else {
4280 - // OpenAI/Voyage use Bearer token
4281 - $headers = array(
4282 - 'Authorization' => 'Bearer ' . $api_key,
4283 - 'Content-Type' => 'application/json'
4284 - );
4285 - }
4286 -
4287 - // Make API request
4288 - //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
4289 - $response = wp_remote_post($endpoint, array(
4290 - 'body' => wp_json_encode($request_body),
4291 - 'headers' => $headers,
4292 - 'timeout' => 60 // Increased timeout for large inputs
4293 - ));
4294 -
4295 - // Handle wp_remote_post errors
4296 - if (is_wp_error($response)) {
4297 - $error_message = $response->get_error_message();
4298 - //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
4299 - return 'Connection error: ' . $error_message;
4300 - }
4301 -
4302 - // Get and check HTTP response code
4303 - $http_code = wp_remote_retrieve_response_code($response);
4304 - //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
4305 -
4306 - if ($http_code !== 200) {
4307 - $error_body = wp_remote_retrieve_body($response);
4308 - //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
4309 -
4310 - // Try to parse error for more details
4311 - $error_json = json_decode($error_body, true);
4312 - if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
4313 - $error_type = $error_json['error']['type'] ?? 'unknown';
4314 - $error_message = $error_json['error']['message'] ?? 'No message';
4315 - //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
4316 - //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
4317 -
4318 - // Customize error message for common API errors
4319 - if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
4320 - $error_message = sprintf('Invalid %s API key for bot %s. Please check your API key in the bot settings.', $provider_name, $bot_id);
4321 - } elseif ($error_type === 'authentication_error') {
4322 - $error_message = sprintf('%s authentication failed for bot %s. Please verify your API key in the bot settings.', $provider_name, $bot_id);
4323 - }
4324 -
4325 - //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
4326 - return $error_message;
4327 - }
4328 -
4329 - $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding for bot %s", $http_code, $bot_id);
4330 - //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
4331 - return $error_message;
4332 - }
4333 -
4334 - // Parse response body
4335 - $response_body = wp_remote_retrieve_body($response);
4336 - //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
4337 -
4338 - $response_data = json_decode($response_body, true);
4339 -
4340 - if (json_last_error() !== JSON_ERROR_NONE) {
4341 - $error = json_last_error_msg();
4342 - //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
4343 - //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
4344 - return "Failed to parse API response: $error";
4345 - }
4346 -
4347 - // Handle different response formats based on provider
4348 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4349 - // Gemini API response format
4350 - if (isset($response_data['embedding']['values'])) {
4351 - $embedding_dimensions = count($response_data['embedding']['values']);
4352 - //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
4353 -
4354 - // Check if embedding dimensions are as expected (should be 1536)
4355 - if ($embedding_dimensions !== 1536) {
4356 - //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
4357 - }
4358 -
4359 - return $response_data['embedding']['values'];
4360 - } else {
4361 - //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
4362 - //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
4363 -
4364 - if (isset($response_data['error'])) {
4365 - $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
4366 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4367 - return $error_message;
4368 - }
4369 -
4370 - $error_message = "Invalid Gemini API response format: No embedding found";
4371 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4372 - return $error_message;
4373 - }
4374 - } else {
4375 - // OpenAI/Voyage API response format
4376 - if (isset($response_data['data'][0]['embedding'])) {
4377 - $embedding_dimensions = count($response_data['data'][0]['embedding']);
4378 - //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
4379 -
4380 - // Check if embedding dimensions are as expected
4381 - if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
4382 - ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
4383 - //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
4384 - }
4385 -
4386 - return $response_data['data'][0]['embedding'];
4387 - } else {
4388 - //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
4389 - //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
4390 -
4391 - if (isset($response_data['error'])) {
4392 - $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
4393 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4394 - return $error_message;
4395 - }
4396 -
4397 - $error_message = "Invalid API response format: No embedding found";
4398 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4399 - return $error_message;
4400 - }
4401 - }
4402 -}
4403 -
4404 -/**
4405 - * Get bot-specific options for multi-bot functionality
4406 - * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
4407 - */
4408 -private function get_bot_options($bot_id = 'default') {
4409 - //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
4410 -
4411 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
4412 - //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
4413 - return array();
4414 - }
4415 -
4416 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
4417 -
4418 - if (!empty($bot_options)) {
4419 - //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
4420 - if (isset($bot_options['similarity_threshold'])) {
4421 - //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
4422 - }
4423 - }
4424 -
4425 - return is_array($bot_options) ? $bot_options : array();
4426 -}
4427 -
4428 -/**
4429 - * Get bot-specific Pinecone configuration
4430 - * Used in the knowledge retrieval functions
4431 - */
4432 -// Also add debugging to your get_bot_pinecone_config function
4433 -private function get_bot_pinecone_config($bot_id = 'default') {
4434 - //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
4435 -
4436 - // If default bot or multi-bot add-on not active, use default Pinecone config
4437 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
4438 - //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
4439 - $addon_options = get_option('mxchat_pinecone_addon_options', array());
4440 - $config = array(
4441 - 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
4442 - 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
4443 - 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
4444 - 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
4445 - );
4446 - //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
4447 - return $config;
4448 - }
4449 -
4450 - //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
4451 -
4452 - // Hook for multi-bot add-on to provide bot-specific Pinecone config
4453 - $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
4454 -
4455 - if (!empty($bot_pinecone_config)) {
4456 - //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
4457 - //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
4458 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
4459 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
4460 - } else {
4461 - //error_log("MXCHAT DEBUG: Filter returned empty config!");
4462 - }
4463 -
4464 - return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
4465 -}
4466 -
4467 -
4468 -public function mxchat_ajax_dismiss_completed_status() {
4469 - try {
4470 - // Verify the request
4471 - check_ajax_referer('mxchat_status_nonce', 'nonce');
4472 -
4473 - if (!current_user_can('manage_options')) {
4474 - wp_send_json_error('Unauthorized access');
4475 - exit;
4476 - }
4477 -
4478 - $card_type = isset($_POST['card_type']) ? sanitize_text_field($_POST['card_type']) : '';
4479 -
4480 - if ($card_type === 'pdf') {
4481 - // Clear PDF status
4482 - $pdf_url = get_transient('mxchat_last_pdf_url');
4483 - if ($pdf_url) {
4484 - delete_transient('mxchat_pdf_status_' . md5($pdf_url));
4485 - delete_transient('mxchat_last_pdf_url');
4486 - }
4487 - } elseif ($card_type === 'sitemap') {
4488 - // Clear sitemap status
4489 - $sitemap_url = get_transient('mxchat_last_sitemap_url');
4490 - if ($sitemap_url) {
4491 - delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
4492 - delete_transient('mxchat_last_sitemap_url');
4493 - }
4494 - }
4495 -
4496 - wp_send_json_success(array('message' => 'Status dismissed successfully'));
4497 -
4498 - } catch (Exception $e) {
4499 - wp_send_json_error(array('message' => 'Error dismissing status: ' . $e->getMessage()));
4500 - }
4501 -}
4502 -
4503 -/**
4504 - * Render completed status cards on page load
4505 - * This ensures completed processing status persists through page refreshes
4506 - */
4507 -public function mxchat_render_completed_status_cards() {
4508 - $output = '';
4509 -
4510 - // Check for completed PDF status
4511 - $pdf_url = get_transient('mxchat_last_pdf_url');
4512 - if ($pdf_url) {
4513 - $pdf_status = $this->mxchat_get_pdf_processing_status($pdf_url);
4514 - if ($pdf_status && ($pdf_status['status'] === 'complete' || $pdf_status['status'] === 'error')) {
4515 - $output .= $this->mxchat_render_pdf_status_card($pdf_status, $pdf_url);
4516 - }
4517 - }
4518 -
4519 - // Check for completed sitemap status
4520 - $sitemap_url = get_transient('mxchat_last_sitemap_url');
4521 - if ($sitemap_url) {
4522 - $sitemap_status = $this->mxchat_get_sitemap_processing_status($sitemap_url);
4523 - if ($sitemap_status && ($sitemap_status['status'] === 'complete' || $sitemap_status['status'] === 'error')) {
4524 - $output .= $this->mxchat_render_sitemap_status_card($sitemap_status, $sitemap_url);
4525 - }
4526 - }
4527 -
4528 - return $output;
4529 -}
4530 -
4531 -/**
4532 - * Render PDF status card HTML
4533 - */
4534 -private function mxchat_render_pdf_status_card($status, $pdf_url) {
4535 - $html = '<div class="mxchat-status-card" data-card-type="pdf">';
4536 - $html .= '<div class="mxchat-status-header">';
4537 - $html .= '<h4>' . esc_html__('PDF Processing Status', 'mxchat') . '</h4>';
4538 -
4539 - // Add dismiss button for completed status
4540 - if ($status['status'] === 'complete' || $status['status'] === 'error') {
4541 - $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
4542 - }
4543 -
4544 - // Process Batch button for processing status
4545 - if ($status['status'] === 'processing') {
4546 - $html .= '<button type="button" class="mxchat-manual-batch-btn"
4547 - data-process-type="pdf"
4548 - data-url="' . esc_attr($pdf_url) . '">
4549 - ' . esc_html__('Process Batch', 'mxchat') . '</button>';
4550 - }
4551 -
4552 - // Add status badges
4553 - if ($status['status'] === 'error') {
4554 - $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
4555 - } elseif ($status['status'] === 'complete') {
4556 - if ($status['failed_pages'] > 0) {
4557 - $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
4558 - sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_pages']) . '</span>';
4559 - } else {
4560 - $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
4561 - }
4562 - }
4563 -
4564 - $html .= '</div>'; // End header
4565 -
4566 - // Progress bar
4567 - $html .= '<div class="mxchat-progress-bar">';
4568 - $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
4569 - $html .= '</div>';
4570 -
4571 - // Status details
4572 - $html .= '<div class="mxchat-status-details">';
4573 - $html .= '<p>' . sprintf(
4574 - esc_html__('Progress: %d of %d pages (%d%%)', 'mxchat'),
4575 - $status['processed_pages'],
4576 - $status['total_pages'],
4577 - $status['percentage']
4578 - ) . '</p>';
4579 -
4580 - // Show failed pages count if any
4581 - if ($status['failed_pages'] > 0) {
4582 - $html .= '<p><strong>' . esc_html__('Failed pages:', 'mxchat') . '</strong> ' . esc_html($status['failed_pages']) . '</p>';
4583 - }
4584 -
4585 - $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
4586 - $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
4587 -
4588 - // Add completion summary if available AND it's an array
4589 - if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
4590 - $summary = $status['completion_summary'];
4591 - $html .= '<div class="mxchat-completion-summary">';
4592 - $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
4593 - $html .= '<p><strong>' . esc_html__('Total Pages:', 'mxchat') . '</strong> ' . esc_html($summary['total_pages']) . '</p>';
4594 - $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_pages']) . '</p>';
4595 - $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_pages']) . '</p>';
4596 - $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
4597 - $html .= '</div>';
4598 - }
4599 -
4600 - // Add failed pages list if any AND it's an array
4601 - if (isset($status['failed_pages_list']) && is_array($status['failed_pages_list']) && !empty($status['failed_pages_list'])) {
4602 - $html .= $this->mxchat_render_failed_pages_list($status['failed_pages_list']);
4603 - }
4604 -
4605 - // Add error message if any
4606 - if (isset($status['error']) && !empty($status['error'])) {
4607 - $html .= '<div class="mxchat-error-notice">';
4608 - $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
4609 - $html .= '</div>';
4610 - }
4611 -
4612 - $html .= '</div>'; // End details
4613 - $html .= '</div>'; // End card
4614 -
4615 - return $html;
4616 -}
4617 -/**
4618 - * Render sitemap status card HTML
4619 - */
4620 -private function mxchat_render_sitemap_status_card($status, $sitemap_url) {
4621 - $html = '<div class="mxchat-status-card" data-card-type="sitemap">';
4622 - $html .= '<div class="mxchat-status-header">';
4623 - $html .= '<h4>' . esc_html__('Sitemap Processing Status', 'mxchat') . '</h4>';
4624 -
4625 - // Add dismiss button for completed status
4626 - if ($status['status'] === 'complete' || $status['status'] === 'error') {
4627 - $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
4628 - }
4629 -
4630 - // Process Batch button for processing status
4631 - if ($status['status'] === 'processing') {
4632 - $html .= '<button type="button" class="mxchat-manual-batch-btn"
4633 - data-process-type="sitemap"
4634 - data-url="' . esc_attr($sitemap_url) . '">
4635 - ' . esc_html__('Process Batch', 'mxchat') . '</button>';
4636 - }
4637 -
4638 - // Add status badges
4639 - if ($status['status'] === 'error') {
4640 - $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
4641 - } elseif ($status['status'] === 'complete') {
4642 - if ($status['failed_urls'] > 0) {
4643 - $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
4644 - sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_urls']) . '</span>';
4645 - } else {
4646 - $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
4647 - }
4648 - }
4649 -
4650 - $html .= '</div>'; // End header
4651 -
4652 - // Progress bar
4653 - $html .= '<div class="mxchat-progress-bar">';
4654 - $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
4655 - $html .= '</div>';
4656 -
4657 - // Status details
4658 - $html .= '<div class="mxchat-status-details">';
4659 - $html .= '<p>' . sprintf(
4660 - esc_html__('Progress: %d of %d URLs (%d%%)', 'mxchat'),
4661 - $status['processed_urls'],
4662 - $status['total_urls'],
4663 - $status['percentage']
4664 - ) . '</p>';
4665 -
4666 - // Show failed URLs count if any
4667 - if ($status['failed_urls'] > 0) {
4668 - $html .= '<p><strong>' . esc_html__('Failed URLs:', 'mxchat') . '</strong> ' . esc_html($status['failed_urls']) . '</p>';
4669 - }
4670 -
4671 - $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
4672 - $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
4673 -
4674 - // Add completion summary if available AND it's an array
4675 - if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
4676 - $summary = $status['completion_summary'];
4677 - $html .= '<div class="mxchat-completion-summary">';
4678 - $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
4679 - $html .= '<p><strong>' . esc_html__('Total URLs:', 'mxchat') . '</strong> ' . esc_html($summary['total_urls']) . '</p>';
4680 - $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_urls']) . '</p>';
4681 - $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_urls']) . '</p>';
4682 - $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
4683 - $html .= '</div>';
4684 - }
4685 -
4686 - // Add error messages if any (but not the failed URLs list)
4687 - if (!empty($status['error']) || !empty($status['last_error'])) {
4688 - $html .= '<div class="mxchat-error-notice">';
4689 -
4690 - if (!empty($status['error'])) {
4691 - $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
4692 - }
4693 -
4694 - if (!empty($status['last_error'])) {
4695 - $html .= '<p class="last-error">' . esc_html__('Last error:', 'mxchat') . ' ' . esc_html($status['last_error']) . '</p>';
4696 - }
4697 -
4698 - $html .= '</div>';
4699 - }
4700 -
4701 - $html .= '</div>'; // End details
4702 - $html .= '</div>'; // End card
4703 -
4704 - return $html;
4705 -}
4706 -
4707 -
4708 -/**
4709 - * Render failed pages list
4710 - */
4711 -private function mxchat_render_failed_pages_list($failed_pages_list) {
4712 - // Validate that $failed_pages_list is an array and not empty
4713 - if (!is_array($failed_pages_list) || empty($failed_pages_list)) {
4714 - return '';
4715 - }
4716 -
4717 - $html = '<div class="mxchat-error-notice">';
4718 - $html .= '<div class="mxchat-failed-pages-container">';
4719 - $html .= '<h5>' . sprintf(esc_html__('Failed Pages (%d)', 'mxchat'), count($failed_pages_list)) . '</h5>';
4720 - $html .= '<details>';
4721 - $html .= '<summary>' . esc_html__('Show Failed Pages', 'mxchat') . '</summary>';
4722 - $html .= '<div class="mxchat-failed-pages-list">';
4723 -
4724 - // Create table for failed pages
4725 - $html .= '<table class="widefat striped">';
4726 - $html .= '<thead><tr>';
4727 - $html .= '<th>' . esc_html__('Page', 'mxchat') . '</th>';
4728 - $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
4729 - $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
4730 - $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
4731 - $html .= '</tr></thead><tbody>';
4732 -
4733 - // Sort failed pages by most recent
4734 - $sorted_failed_pages = $failed_pages_list;
4735 - usort($sorted_failed_pages, function($a, $b) {
4736 - return ($b['time'] ?? 0) - ($a['time'] ?? 0);
4737 - });
4738 -
4739 - foreach ($sorted_failed_pages as $item) {
4740 - // Ensure $item is an array before accessing its elements
4741 - if (!is_array($item)) {
4742 - continue;
4743 - }
4744 -
4745 - $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
4746 - $html .= '<tr>';
4747 - $html .= '<td>' . esc_html__('Page', 'mxchat') . ' ' . esc_html($item['page'] ?? 'Unknown') . '</td>';
4748 - $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
4749 - $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
4750 - $html .= '<td>' . esc_html($time_ago) . '</td>';
4751 - $html .= '</tr>';
4752 - }
4753 -
4754 - $html .= '</tbody></table>';
4755 - $html .= '</div></details></div></div>';
4756 -
4757 - return $html;
4758 -}
4759 -
4760 -/**
4761 - * Render failed URLs list
4762 - */
4763 -private function mxchat_render_failed_urls_list($failed_urls_list) {
4764 - // Validate that $failed_urls_list is an array and not empty
4765 - if (!is_array($failed_urls_list) || empty($failed_urls_list)) {
4766 - return '';
4767 - }
4768 -
4769 - $html = '<div class="mxchat-failed-urls-container">';
4770 - $html .= '<h5>' . sprintf(esc_html__('Failed URLs (%d)', 'mxchat'), count($failed_urls_list)) . '</h5>';
4771 - $html .= '<details>';
4772 - $html .= '<summary>' . esc_html__('Show Failed URLs', 'mxchat') . '</summary>';
4773 - $html .= '<div class="mxchat-failed-urls-list">';
4774 -
4775 - // Create table for failed URLs
4776 - $html .= '<table class="widefat striped">';
4777 - $html .= '<thead><tr>';
4778 - $html .= '<th>' . esc_html__('URL', 'mxchat') . '</th>';
4779 - $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
4780 - $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
4781 - $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
4782 - $html .= '</tr></thead><tbody>';
4783 -
4784 - // Sort failed URLs by most recent
4785 - $sorted_failed_urls = $failed_urls_list;
4786 - usort($sorted_failed_urls, function($a, $b) {
4787 - return ($b['time'] ?? 0) - ($a['time'] ?? 0);
4788 - });
4789 -
4790 - // Show up to 50 failed URLs
4791 - $display_urls = array_slice($sorted_failed_urls, 0, 50);
4792 -
4793 - foreach ($display_urls as $item) {
4794 - // Ensure $item is an array before accessing its elements
4795 - if (!is_array($item)) {
4796 - continue;
4797 - }
4798 -
4799 - $url = $item['url'] ?? '';
4800 - $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
4801 -
4802 - // Truncate URL for display
4803 - $display_url = strlen($url) > 50 ? substr($url, 0, 47) . '...' : $url;
4804 -
4805 - $html .= '<tr>';
4806 - $html .= '<td style="word-break: break-all;">';
4807 - if (!empty($url)) {
4808 - $html .= '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($display_url) . '</a>';
4809 - } else {
4810 - $html .= esc_html__('Unknown URL', 'mxchat');
4811 - }
4812 - $html .= '</td>';
4813 - $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
4814 - $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
4815 - $html .= '<td>' . esc_html($time_ago) . '</td>';
4816 - $html .= '</tr>';
4817 - }
4818 -
4819 - $html .= '</tbody></table>';
4820 -
4821 - if (count($failed_urls_list) > 50) {
4822 - $html .= '<div class="mxchat-failed-urls-more">+ ' .
4823 - (count($failed_urls_list) - 50) .
4824 - ' ' . esc_html__('more failed URLs not shown', 'mxchat') . '</div>';
4825 - }
4826 -
4827 - $html .= '</div></details></div>';
4828 -
4829 - return $html;
4830 -}
4831 -
4832 -/**
4833 - * Get all ACF fields for a specific post, excluding any fields the user has disabled
4834 - */
4835 -public function mxchat_get_acf_fields_for_post($post_id) {
4836 - if (!function_exists('get_fields')) {
4837 - return array();
4838 - }
4839 -
4840 - $fields = get_fields($post_id);
4841 - if (!$fields || !is_array($fields)) {
4842 - return array();
4843 - }
4844 -
4845 - // Get excluded fields from settings
4846 - $excluded_fields = get_option('mxchat_acf_excluded_fields', array());
4847 - if (!empty($excluded_fields) && is_array($excluded_fields)) {
4848 - foreach ($excluded_fields as $excluded_field) {
4849 - if (isset($fields[$excluded_field])) {
4850 - unset($fields[$excluded_field]);
4851 - }
4852 - }
4853 - }
4854 -
4855 - return $fields;
4856 -}
4857 -
4858 -/**
4859 - * Get all registered ACF field groups and their fields for the settings UI
4860 - */
4861 -public function mxchat_get_all_acf_fields() {
4862 - if (!function_exists('acf_get_field_groups') || !function_exists('acf_get_fields')) {
4863 - return array();
4864 - }
4865 -
4866 - $all_fields = array();
4867 - $field_groups = acf_get_field_groups();
4868 -
4869 - if (!empty($field_groups)) {
4870 - foreach ($field_groups as $group) {
4871 - $group_fields = acf_get_fields($group['key']);
4872 - if (!empty($group_fields)) {
4873 - $all_fields[$group['title']] = array();
4874 - foreach ($group_fields as $field) {
4875 - $all_fields[$group['title']][] = array(
4876 - 'name' => $field['name'],
4877 - 'label' => $field['label'],
4878 - 'type' => $field['type']
4879 - );
4880 - }
4881 - }
4882 - }
4883 - }
4884 -
4885 - return $all_fields;
4886 -}
4887 -
4888 -/**
4889 - * Get whitelisted custom post meta for a given post
4890 - * This allows non-ACF meta fields (like OptionTree, theme meta boxes) to be included in embeddings
4891 - */
4892 -public function mxchat_get_whitelisted_post_meta($post_id) {
4893 - $whitelist = get_option('mxchat_custom_meta_whitelist', '');
4894 -
4895 - if (empty($whitelist)) {
4896 - return array();
4897 - }
4898 -
4899 - // Parse the whitelist - one meta key per line
4900 - $meta_keys = array_filter(array_map('trim', explode("\n", $whitelist)));
4901 -
4902 - if (empty($meta_keys)) {
4903 - return array();
4904 - }
4905 -
4906 - $result = array();
4907 -
4908 - foreach ($meta_keys as $key) {
4909 - // Skip empty keys
4910 - if (empty($key)) {
4911 - continue;
4912 - }
4913 -
4914 - $value = get_post_meta($post_id, $key, true);
4915 -
4916 - // Only include non-empty string values
4917 - if (!empty($value) && is_string($value)) {
4918 - $result[$key] = $value;
4919 - } elseif (!empty($value) && is_array($value)) {
4920 - // Handle array values by joining them
4921 - $flat_value = $this->mxchat_flatten_meta_array($value);
4922 - if (!empty($flat_value)) {
4923 - $result[$key] = $flat_value;
4924 - }
4925 - }
4926 - }
4927 -
4928 - return $result;
4929 -}
4930 -
4931 -/**
4932 - * Flatten array meta values into a readable string
4933 - */
4934 -private function mxchat_flatten_meta_array($array, $depth = 0) {
4935 - if ($depth > 3) {
4936 - return ''; // Prevent infinite recursion
4937 - }
4938 -
4939 - $parts = array();
4940 -
4941 - foreach ($array as $key => $value) {
4942 - if (is_string($value) && !empty($value)) {
4943 - $parts[] = $value;
4944 - } elseif (is_array($value)) {
4945 - $nested = $this->mxchat_flatten_meta_array($value, $depth + 1);
4946 - if (!empty($nested)) {
4947 - $parts[] = $nested;
4948 - }
4949 - }
4950 - }
4951 -
4952 - return implode(', ', $parts);
4953 -}
4954 -
4955 -/**
4956 - * Format ACF field values for content extraction
4957 - */
4958 -public function mxchat_format_acf_field_value($value, $field_name = '', $post_id = 0) {
4959 - if (empty($value)) {
4960 - return '';
4961 - }
4962 -
4963 - // Handle WP_Post objects first (THIS IS THE KEY FIX)
4964 - if ($value instanceof WP_Post) {
4965 - return $value->post_title ?: '';
4966 - }
4967 -
4968 - // Handle other WP objects
4969 - if (is_object($value)) {
4970 - if (isset($value->post_title)) {
4971 - return $value->post_title;
4972 - } elseif (isset($value->display_name)) {
4973 - return $value->display_name;
4974 - } elseif (isset($value->name)) {
4975 - return $value->name;
4976 - } elseif (method_exists($value, '__toString')) {
4977 - try {
4978 - return (string) $value;
4979 - } catch (Exception $e) {
4980 - return '';
4981 - }
4982 - }
4983 - // For any other objects, return empty string
4984 - return '';
4985 - }
4986 -
4987 - // Handle different ACF field types
4988 - if (is_array($value)) {
4989 - // Check if it's an image/file field
4990 - if (isset($value['url'])) {
4991 - // Image field - return alt text, title, or caption
4992 - if (!empty($value['alt'])) {
4993 - return $value['alt'];
4994 - } elseif (!empty($value['title'])) {
4995 - return $value['title'];
4996 - } elseif (!empty($value['caption'])) {
4997 - return $value['caption'];
4998 - } else {
4999 - return ''; // Don't include just the URL
5000 - }
5001 - }
5002 -
5003 - // Check if it's a post object or relationship field
5004 - if (isset($value['post_title'])) {
5005 - return $value['post_title'];
5006 - }
5007 -
5008 - // Check if it's a user field
5009 - if (isset($value['display_name'])) {
5010 - return $value['display_name'];
5011 - }
5012 -
5013 - // Check if it's a taxonomy term
5014 - if (isset($value['name']) && isset($value['taxonomy'])) {
5015 - return $value['name'];
5016 - }
5017 -
5018 - // Check if it's a select field with label
5019 - if (isset($value['label'])) {
5020 - return $value['label'];
5021 - }
5022 -
5023 - // Check for repeater field or flexible content
5024 - if (is_numeric(key($value))) {
5025 - $sub_values = array();
5026 - foreach ($value as $sub_item) {
5027 - if (is_array($sub_item)) {
5028 - // For repeater/flexible content, extract text values
5029 - $sub_text = $this->mxchat_extract_text_from_acf_array($sub_item);
5030 - if (!empty($sub_text)) {
5031 - $sub_values[] = $sub_text;
5032 - }
5033 - } elseif ($sub_item instanceof WP_Post) {
5034 - // Handle WP_Post objects in arrays
5035 - $sub_values[] = $sub_item->post_title ?: '';
5036 - } else {
5037 - $sub_values[] = (string) $sub_item;
5038 - }
5039 - }
5040 - return implode(', ', array_filter($sub_values));
5041 - }
5042 -
5043 - // For other arrays, try to extract meaningful text
5044 - $text_values = array();
5045 - foreach ($value as $key => $val) {
5046 - if (is_string($val) && !empty(trim($val))) {
5047 - $text_values[] = trim($val);
5048 - } elseif ($val instanceof WP_Post) {
5049 - // Handle WP_Post objects in associative arrays
5050 - $text_values[] = $val->post_title ?: '';
5051 - } elseif (is_array($val) && isset($val['post_title'])) {
5052 - $text_values[] = $val['post_title'];
5053 - } elseif (is_array($val) && isset($val['name'])) {
5054 - $text_values[] = $val['name'];
5055 - }
5056 - }
5057 -
5058 - return implode(', ', array_filter($text_values));
5059 - }
5060 -
5061 - // Handle boolean values
5062 - if (is_bool($value)) {
5063 - return $value ? 'Yes' : 'No';
5064 - }
5065 -
5066 - // Handle numeric values
5067 - if (is_numeric($value)) {
5068 - return (string) $value;
5069 - }
5070 -
5071 - // Handle string values
5072 - if (is_string($value)) {
5073 - return trim($value);
5074 - }
5075 -
5076 - // For anything else that we can't handle, return empty string
5077 - // This prevents the "Object could not be converted to string" error
5078 - return '';
5079 -}
5080 -
5081 -/**
5082 - * Extract text from complex ACF array structures
5083 - */
5084 -private function mxchat_extract_text_from_acf_array($array) {
5085 - if (!is_array($array)) {
5086 - return '';
5087 - }
5088 -
5089 - $text_parts = array();
5090 -
5091 - foreach ($array as $key => $value) {
5092 - if (is_string($value) && !empty(trim($value))) {
5093 - // Skip keys that are likely to be IDs or technical values
5094 - if (!is_numeric($value) || strlen($value) > 10) {
5095 - $text_parts[] = trim($value);
5096 - }
5097 - } elseif ($value instanceof WP_Post) {
5098 - // Handle WP_Post objects
5099 - $text_parts[] = $value->post_title ?: '';
5100 - } elseif (is_array($value)) {
5101 - if (isset($value['post_title'])) {
5102 - $text_parts[] = $value['post_title'];
5103 - } elseif (isset($value['name'])) {
5104 - $text_parts[] = $value['name'];
5105 - } elseif (isset($value['label'])) {
5106 - $text_parts[] = $value['label'];
5107 - }
5108 - } elseif (is_object($value)) {
5109 - // Handle other objects safely
5110 - if (isset($value->post_title)) {
5111 - $text_parts[] = $value->post_title;
5112 - } elseif (isset($value->name)) {
5113 - $text_parts[] = $value->name;
5114 - } elseif (isset($value->display_name)) {
5115 - $text_parts[] = $value->display_name;
5116 - }
5117 - }
5118 - }
5119 -
5120 - return implode(', ', array_filter($text_parts));
5121 -}
5122 -
5123 -/**
5124 - * Walk an ACF field value tree and collect attachment IDs for any value that
5125 - * resolves to a PDF in the WordPress media library. Handles the three shapes
5126 - * ACF returns for File/Image/URL fields (array with ID+url, integer attachment ID,
5127 - * plain URL string), and recurses through repeater/group/flexible content.
5128 - *
5129 - * @param mixed $value The ACF field value (any depth)
5130 - * @param array $out Accumulator (passed by reference) for attachment IDs
5131 - * @param int $depth Recursion guard
5132 - */
5133 -private function mxchat_collect_pdf_attachment_ids_from_acf_value($value, &$out, $depth = 0) {
5134 - if ($depth > 6) {
5135 - return; // prevent runaway recursion on circular/very-deep structures
5136 - }
5137 -
5138 - if (empty($value)) {
5139 - return;
5140 - }
5141 -
5142 - // Array shapes: ACF File/Image return value=array; repeaters/groups are arrays of arrays
5143 - if (is_array($value)) {
5144 - // Direct File/Image-style array (has 'url' and usually 'ID' + 'mime_type')
5145 - $looks_like_attachment = isset($value['url']) || isset($value['ID']) || isset($value['id']);
5146 - if ($looks_like_attachment) {
5147 - $att_id = 0;
5148 - if (!empty($value['ID']) && is_numeric($value['ID'])) {
5149 - $att_id = (int) $value['ID'];
5150 - } elseif (!empty($value['id']) && is_numeric($value['id'])) {
5151 - $att_id = (int) $value['id'];
5152 - } elseif (!empty($value['url']) && is_string($value['url'])) {
5153 - $att_id = (int) attachment_url_to_postid($value['url']);
5154 - }
5155 -
5156 - $is_pdf = false;
5157 - if (!empty($value['mime_type']) && $value['mime_type'] === 'application/pdf') {
5158 - $is_pdf = true;
5159 - } elseif (!empty($value['subtype']) && strtolower((string) $value['subtype']) === 'pdf') {
5160 - $is_pdf = true;
5161 - } elseif (!empty($value['url']) && is_string($value['url']) && $this->mxchat_url_looks_like_pdf($value['url'])) {
5162 - $is_pdf = true;
5163 - } elseif ($att_id && get_post_mime_type($att_id) === 'application/pdf') {
5164 - $is_pdf = true;
5165 - }
5166 -
5167 - if ($is_pdf && $att_id && get_post_mime_type($att_id) === 'application/pdf') {
5168 - $out[] = $att_id;
5169 - }
5170 - // An array node that represents one attachment doesn't contain other
5171 - // attachments inside it — done with this branch.
5172 - return;
5173 - }
5174 -
5175 - // Recurse: repeater rows, flexible-content layouts, groups, etc.
5176 - foreach ($value as $sub) {
5177 - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($sub, $out, $depth + 1);
5178 - }
5179 - return;
5180 - }
5181 -
5182 - // Plain numeric attachment ID (ACF File field set to "Return: ID")
5183 - if (is_numeric($value)) {
5184 - $att_id = (int) $value;
5185 - if ($att_id > 0 && get_post_mime_type($att_id) === 'application/pdf') {
5186 - $out[] = $att_id;
5187 - }
5188 - return;
5189 - }
5190 -
5191 - // Plain string — URL pointing at a PDF (ACF File field set to "Return: URL", or a custom URL/text field)
5192 - if (is_string($value)) {
5193 - $trimmed = trim($value);
5194 - if ($trimmed !== '' && $this->mxchat_url_looks_like_pdf($trimmed)) {
5195 - $att_id = (int) attachment_url_to_postid($trimmed);
5196 - if ($att_id > 0 && get_post_mime_type($att_id) === 'application/pdf') {
5197 - $out[] = $att_id;
5198 - }
5199 - }
5200 - return;
5201 - }
5202 -}
5203 -
5204 -/**
5205 - * Heuristic: does this URL/string look like a PDF reference?
5206 - * Tolerates query strings and fragments (#page=2).
5207 - */
5208 -private function mxchat_url_looks_like_pdf($url) {
5209 - if (!is_string($url) || $url === '') {
5210 - return false;
5211 - }
5212 - // Strip query + fragment before checking extension
5213 - $path = preg_replace('/[?#].*$/', '', $url);
5214 - return (bool) preg_match('/\.pdf$/i', $path);
5215 -}
5216 -
5217 -/**
5218 - * Extract text from a PDF attachment by ID using the bundled Smalot parser.
5219 - * Reads the file directly from disk via get_attached_file (no HTTP fetch).
5220 - * Result is cached on the attachment as post_meta keyed by file mtime so we
5221 - * only parse the same PDF once unless the file changes on disk.
5222 - *
5223 - * @param int $attachment_id
5224 - * @return string Extracted plain text, or '' on failure.
5225 - */
5226 -private function mxchat_extract_pdf_text_by_attachment_id($attachment_id) {
5227 - $attachment_id = (int) $attachment_id;
5228 - if ($attachment_id <= 0) {
5229 - return '';
5230 - }
5231 - if (get_post_mime_type($attachment_id) !== 'application/pdf') {
5232 - return '';
5233 - }
5234 -
5235 - $pdf_path = get_attached_file($attachment_id);
5236 - if (empty($pdf_path) || !file_exists($pdf_path) || !is_readable($pdf_path)) {
5237 - return '';
5238 - }
5239 -
5240 - // Raw-file size cap. Parsing very large PDFs can OOM the request; skip with a log entry
5241 - // and let the rest of the ACF content land in the KB. Filterable for users who need it bigger.
5242 - $default_max_bytes = 25 * 1024 * 1024;
5243 - $max_bytes = (int) apply_filters('mxchat_acf_pdf_max_bytes', $default_max_bytes, $attachment_id, $pdf_path);
5244 - if ($max_bytes > 0) {
5245 - $file_size = @filesize($pdf_path);
5246 - if ($file_size !== false && $file_size > $max_bytes) {
5247 - error_log(sprintf(
5248 - '[mxchat] ACF PDF skipped (over size cap): attachment %d "%s" %d bytes > cap %d',
5249 - $attachment_id,
5250 - basename($pdf_path),
5251 - $file_size,
5252 - $max_bytes
5253 - ));
5254 - return '';
5255 - }
5256 - }
5257 -
5258 - $mtime = @filemtime($pdf_path);
5259 - $cache_meta_key = '_mxchat_acf_pdf_text_v1';
5260 - $cached = get_post_meta($attachment_id, $cache_meta_key, true);
5261 - if (is_array($cached) && isset($cached['mtime'], $cached['text']) && (int) $cached['mtime'] === (int) $mtime) {
5262 - return (string) $cached['text'];
5263 - }
5264 -
5265 - $text = '';
5266 - try {
5267 - if (function_exists('mxchat_load_pdf_parser')) {
5268 - mxchat_load_pdf_parser();
5269 - }
5270 - if (!class_exists('\\Smalot\\PdfParser\\Parser')) {
5271 - return '';
5272 - }
5273 - $parser = new \Smalot\PdfParser\Parser();
5274 - $pdf = $parser->parseFile($pdf_path);
5275 - $pages = $pdf->getPages();
5276 - $page_texts = array();
5277 - foreach ($pages as $page) {
5278 - $page_text = '';
5279 - try {
5280 - $page_text = $page->getText();
5281 - } catch (\Exception $e) {
5282 - $page_text = '';
5283 - }
5284 - if (!empty($page_text)) {
5285 - $page_texts[] = $page_text;
5286 - }
5287 - }
5288 - $text = trim(implode("\n\n", $page_texts));
5289 - } catch (\Exception $e) {
5290 - error_log('[mxchat] ACF PDF extraction failed for attachment ' . $attachment_id . ': ' . $e->getMessage());
5291 - return '';
5292 - } catch (\Throwable $e) {
5293 - error_log('[mxchat] ACF PDF extraction error for attachment ' . $attachment_id . ': ' . $e->getMessage());
5294 - return '';
5295 - }
5296 -
5297 - // Cap per-PDF text to avoid blowing up the embedding payload on enormous PDFs.
5298 - // The chunker downstream will still split this into multiple vectors.
5299 - $max_len = (int) apply_filters('mxchat_acf_pdf_text_max_length', 50000);
5300 - if ($max_len > 0 && strlen($text) > $max_len) {
5301 - $text = substr($text, 0, $max_len);
5302 - }
5303 -
5304 - update_post_meta($attachment_id, $cache_meta_key, array(
5305 - 'mtime' => (int) $mtime,
5306 - 'text' => $text,
5307 - ));
5308 -
5309 - return $text;
5310 -}
5311 -
5312 -/**
5313 - * Handle ACF save - fires after ACF fields are saved
5314 - * This ensures ACF field data is available when syncing to knowledge base
5315 - */
5316 -public function mxchat_handle_acf_save($post_id) {
5317 - // Skip if not a valid post
5318 - if (!$post_id || $post_id === 'options') {
5319 - return;
5320 - }
5321 -
5322 - // Skip autosaves and revisions
5323 - if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
5324 - return;
5325 - }
5326 -
5327 - $post = get_post($post_id);
5328 - if (!$post) {
5329 - return;
5330 - }
5331 -
5332 - $post_type = $post->post_type;
5333 -
5334 - // Check if sync is enabled for this post type
5335 - $should_sync = false;
5336 -
5337 - if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
5338 - $should_sync = true;
5339 - } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
5340 - $should_sync = true;
5341 - } else if ($post_type === 'product' && class_exists('WooCommerce')) {
5342 - // WooCommerce products - check if WooCommerce integration is enabled
5343 - $options = get_option('mxchat_options', array());
5344 - if (isset($options['enable_woocommerce_integration']) &&
5345 - ($options['enable_woocommerce_integration'] === '1' || $options['enable_woocommerce_integration'] === 'on')) {
5346 - $should_sync = true;
5347 - }
5348 - } else {
5349 - // Check custom post types
5350 - $option_name = 'mxchat_auto_sync_' . $post_type;
5351 - if (get_option($option_name) === '1') {
5352 - $should_sync = true;
5353 - }
5354 - }
5355 -
5356 - if (!$should_sync) {
5357 - return;
5358 - }
5359 -
5360 - // Only process published posts
5361 - if ($post->post_status !== 'publish') {
5362 - return;
5363 - }
5364 -
5365 - // Check if this post has any ACF fields - if not, no need to re-sync
5366 - $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
5367 - if (empty($acf_fields)) {
5368 - return;
5369 - }
5370 -
5371 - // Use a transient to prevent duplicate processing (post_updated may have already run)
5372 - $transient_key = 'mxchat_acf_synced_' . $post_id;
5373 - if (get_transient($transient_key)) {
5374 - return;
5375 - }
5376 - set_transient($transient_key, true, 60); // Prevent re-processing for 60 seconds
5377 -
5378 - // Re-run the sync with ACF data now available
5379 - // We pass $update=true since this is effectively an update with ACF data
5380 - $this->mxchat_handle_post_update($post_id, $post, true);
5381 -}
5382 -
5383 -public function mxchat_handle_post_update($post_id, $post, $update) {
5384 - // Basic validation checks
5385 - if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
5386 - return;
5387 - }
5388 -
5389 - $post_type = $post->post_type;
5390 -
5391 - // Check if sync is enabled for this post type
5392 - $should_sync = false;
5393 -
5394 - // Check built-in post types first
5395 - if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
5396 - $should_sync = true;
5397 - } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
5398 - $should_sync = true;
5399 - } else {
5400 - // Check custom post types
5401 - $option_name = 'mxchat_auto_sync_' . $post_type;
5402 - if (get_option($option_name) === '1') {
5403 - $should_sync = true;
5404 - }
5405 - }
5406 -
5407 - if (!$should_sync) {
5408 - return;
5409 - }
5410 -
5411 - // Check if we have stored the previous status and URL in our transients
5412 - $previous_status_key = 'mxchat_prev_status_' . $post_id;
5413 - $previous_status = get_transient($previous_status_key);
5414 -
5415 - $previous_url_key = 'mxchat_prev_url_' . $post_id;
5416 - $previous_url = get_transient($previous_url_key);
5417 -
5418 - // If the post was previously published but is now not published, remove from knowledge base
5419 - if ($previous_status === 'publish' && $post->post_status !== 'publish') {
5420 - // Use the stored URL from when it was published, or fall back to current permalink
5421 - $source_url = $previous_url ?: get_permalink($post_id);
5422 -
5423 - if ($source_url) {
5424 - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks)
5425 - MxChat_Utils::delete_chunks_for_url($source_url, 'default');
5426 - }
5427 -
5428 - // Clean up the transients and exit early
5429 - delete_transient($previous_status_key);
5430 - delete_transient($previous_url_key);
5431 - return;
5432 - }
5433 -
5434 - // Slug/permalink rename while still published: delete the old vectors before upserting new ones.
5435 - // Without this, md5(old_url) vectors (base + chunks) would be orphaned under the stale URL.
5436 - if ($post->post_status === 'publish' && !empty($previous_url)) {
5437 - $current_url = get_permalink($post_id);
5438 - if ($current_url && $current_url !== $previous_url) {
5439 - MxChat_Utils::delete_chunks_for_url($previous_url, 'default');
5440 - }
5441 - }
5442 -
5443 - // Store the current status for next time (if this is an update)
5444 - if ($update) {
5445 - set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
5446 -
5447 - // If the post is currently published, also store its URL
5448 - if ($post->post_status === 'publish') {
5449 - $current_url = get_permalink($post_id);
5450 - set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
5451 - }
5452 - }
5453 -
5454 - // Only process currently published content for adding/updating
5455 - if ($post->post_status === 'publish') {
5456 - // Get the source URL
5457 - $source_url = get_permalink($post_id);
5458 -
5459 - // Get content with proper formatting (matching ajax_mxchat_process_selected_content)
5460 - $title = get_the_title($post_id);
5461 - $content = get_post_field('post_content', $post_id);
5462 - $excerpt = get_post_field('post_excerpt', $post_id);
5463 -
5464 - // Remove shortcode tags but preserve content inside them
5465 - $content = $this->strip_shortcode_tags_preserve_content($content);
5466 - $excerpt = $this->strip_shortcode_tags_preserve_content($excerpt);
5467 -
5468 - // Strip tags but preserve structure (don't use 'the_content' filter as it may re-add shortcodes)
5469 - $content = wp_strip_all_tags($content);
5470 -
5471 - // Combine title, short description (if exists), and content
5472 - $final_content = $title . "\n\n";
5473 -
5474 - // Add short description if it exists (WooCommerce products use post_excerpt for short description)
5475 - if (!empty($excerpt)) {
5476 - $final_content .= "Short Description: " . wp_strip_all_tags($excerpt) . "\n\n";
5477 - }
5478 -
5479 - $final_content .= $content;
5480 -
5481 - // For WooCommerce products, include pricing and product details
5482 - if ($post_type === 'product' && class_exists('WooCommerce')) {
5483 - $product = wc_get_product($post_id);
5484 -
5485 - if ($product) {
5486 - // Get pricing information
5487 - $regular_price = $product->get_regular_price();
5488 - $sale_price = $product->get_sale_price();
5489 - $price = $product->get_price();
5490 - $sku = $product->get_sku();
5491 -
5492 - // Get currency symbol
5493 - $currency_symbol = get_woocommerce_currency_symbol();
5494 -
5495 - // Add pricing information
5496 - $final_content .= "\n";
5497 - if (!empty($regular_price)) {
5498 - $final_content .= "Price: " . $currency_symbol . $regular_price . "\n";
5499 - } elseif (!empty($price)) {
5500 - $final_content .= "Price: " . $currency_symbol . $price . "\n";
5501 - }
5502 -
5503 - if (!empty($sale_price) && $sale_price !== $regular_price) {
5504 - $final_content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
5505 - }
5506 -
5507 - // Handle variable products - show price range
5508 - if ($product->is_type('variable')) {
5509 - $min_price = $product->get_variation_price('min');
5510 - $max_price = $product->get_variation_price('max');
5511 - if ($min_price !== $max_price) {
5512 - $final_content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
5513 - }
5514 - }
5515 -
5516 - if (!empty($sku)) {
5517 - $final_content .= "SKU: " . $sku . "\n";
5518 - }
5519 -
5520 - // Get product categories
5521 - $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
5522 - if (!empty($categories) && !is_wp_error($categories)) {
5523 - $final_content .= "Categories: " . implode(', ', $categories) . "\n";
5524 - }
5525 - }
5526 - }
5527 -
5528 - // For custom post types like job_listing, include additional fields
5529 - if ($post_type === 'job_listing') {
5530 - // Add job-specific meta if available
5531 - $job_location = get_post_meta($post_id, '_job_location', true);
5532 - if (!empty($job_location)) {
5533 - $final_content .= "\n\nLocation: " . $job_location;
5534 - }
5535 -
5536 - // Get job type terms
5537 - $job_types = get_the_terms($post_id, 'job_listing_type');
5538 - if (!empty($job_types) && !is_wp_error($job_types)) {
5539 - $types = array();
5540 - foreach ($job_types as $type) {
5541 - $types[] = $type->name;
5542 - }
5543 - $final_content .= "\n\nJob Type: " . implode(', ', $types);
5544 - }
5545 -
5546 - // Get company name if available
5547 - $company_name = get_post_meta($post_id, '_company_name', true);
5548 - if (!empty($company_name)) {
5549 - $final_content .= "\n\nCompany: " . $company_name;
5550 - }
5551 - }
5552 -
5553 - // ADD ACF FIELDS SUPPORT (matches ajax_mxchat_process_selected_content behavior)
5554 - $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
5555 - if (!empty($acf_fields)) {
5556 - $acf_content_parts = array();
5557 - $pdf_attachment_ids = array();
5558 -
5559 - foreach ($acf_fields as $field_name => $field_value) {
5560 - $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
5561 - if (!empty($formatted_value)) {
5562 - // Convert field name to readable label
5563 - $field_label = ucwords(str_replace(['_', '-'], ' ', $field_name));
5564 - $acf_content_parts[] = $field_label . ": " . $formatted_value;
5565 - }
5566 -
5567 - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($field_value, $pdf_attachment_ids);
5568 - }
5569 -
5570 - if (!empty($acf_content_parts)) {
5571 - $final_content .= "\n\n" . implode("\n", $acf_content_parts);
5572 - }
5573 -
5574 - // Gate the auto-sync PDF-extraction loop behind an opt-in option.
5575 - // Mirrors the per-batch checkbox the manual content selector has; the
5576 - // 25 MB size cap lives in the shared extractor so it applies in both
5577 - // paths regardless. Default OFF — re-parsing every ACF PDF on every
5578 - // editor save is expensive and most sites don't want it.
5579 - $autosync_extract_acf_pdfs = get_option('mxchat_auto_sync_acf_pdfs', '0') === '1';
5580 - if ($autosync_extract_acf_pdfs && !empty($pdf_attachment_ids)) {
5581 - $pdf_attachment_ids = array_unique(array_filter(array_map('intval', $pdf_attachment_ids)));
5582 - $pdf_sections = array();
5583 - foreach ($pdf_attachment_ids as $att_id) {
5584 - $pdf_text = $this->mxchat_extract_pdf_text_by_attachment_id($att_id);
5585 - if (!empty($pdf_text)) {
5586 - $pdf_title = get_the_title($att_id);
5587 - $pdf_url = wp_get_attachment_url($att_id);
5588 - $header = 'PDF Attachment';
5589 - if (!empty($pdf_title)) {
5590 - $header .= ': ' . $pdf_title;
5591 - }
5592 - if (!empty($pdf_url)) {
5593 - $header .= ' (' . $pdf_url . ')';
5594 - }
5595 - $pdf_sections[] = $header . "\n" . $pdf_text;
5596 - }
5597 - }
5598 - if (!empty($pdf_sections)) {
5599 - $final_content .= "\n\nAttached PDFs:\n" . implode("\n\n", $pdf_sections);
5600 - }
5601 - }
5602 - }
5603 -
5604 - // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
5605 - $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
5606 - if (!empty($custom_meta)) {
5607 - $meta_content_parts = array();
5608 -
5609 - foreach ($custom_meta as $meta_key => $meta_value) {
5610 - // Convert meta key to readable label
5611 - $meta_label = ucwords(str_replace(array('_', '-'), ' ', $meta_key));
5612 - $meta_content_parts[] = $meta_label . ": " . $meta_value;
5613 - }
5614 -
5615 - if (!empty($meta_content_parts)) {
5616 - $final_content .= "\n\n" . implode("\n", $meta_content_parts);
5617 - }
5618 - }
5619 -
5620 - // Get API key with proper model detection
5621 - $options = get_option('mxchat_options');
5622 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
5623 -
5624 - if (strpos($selected_model, 'voyage') === 0) {
5625 - $api_key = $options['voyage_api_key'] ?? '';
5626 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
5627 - $api_key = $options['gemini_api_key'] ?? '';
5628 - } else {
5629 - $api_key = $options['api_key'] ?? '';
5630 - }
5631 -
5632 - if (empty($api_key)) {
5633 - return;
5634 - }
5635 -
5636 - // Use the centralized utility function for storage
5637 - $result = MxChat_Utils::submit_content_to_db(
5638 - $final_content,
5639 - $source_url,
5640 - $api_key,
5641 - md5($source_url) // Vector ID for Pinecone
5642 - );
5643 -
5644 - // After successful storage, apply role restriction based on tags
5645 - if (!is_wp_error($result)) {
5646 - $this->apply_role_restriction_to_post($post_id, $source_url);
5647 - }
5648 - }
5649 -
5650 - // Clean up the stored previous status if not used above
5651 - if ($previous_status !== 'publish' || $post->post_status === 'publish') {
5652 - delete_transient($previous_status_key);
5653 - delete_transient($previous_url_key);
5654 - }
5655 -}
5656 -
5657 -/**
5658 - * Store the post status and URL before update to detect status transitions
5659 - * This runs before the post is actually updated in the database
5660 - */
5661 -public function mxchat_store_pre_update_status($post_id, $data) {
5662 - // Get the current post from database (before update)
5663 - $current_post = get_post($post_id);
5664 -
5665 - if ($current_post) {
5666 - // Store the current status temporarily
5667 - $status_key = 'mxchat_prev_status_' . $post_id;
5668 - set_transient($status_key, $current_post->post_status, HOUR_IN_SECONDS);
5669 -
5670 - // If the post is currently published, also store its URL
5671 - if ($current_post->post_status === 'publish') {
5672 - $url_key = 'mxchat_prev_url_' . $post_id;
5673 - $current_url = get_permalink($post_id);
5674 - set_transient($url_key, $current_url, HOUR_IN_SECONDS);
5675 - }
5676 - }
5677 -}
5678 -
5679 -public function mxchat_handle_post_delete($post_id) {
5680 - // Get post data before it's deleted
5681 - $post = get_post($post_id);
5682 -
5683 - // Basic validation
5684 - if (!$post || wp_is_post_revision($post_id)) {
5685 - return;
5686 - }
5687 -
5688 - $post_type = $post->post_type;
5689 -
5690 - // Check if sync is enabled for this post type
5691 - $should_sync = false;
5692 -
5693 - // Check built-in post types first
5694 - if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
5695 - $should_sync = true;
5696 - } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
5697 - $should_sync = true;
5698 - } else {
5699 - // Check custom post types
5700 - $option_name = 'mxchat_auto_sync_' . $post_type;
5701 - if (get_option($option_name) === '1') {
5702 - $should_sync = true;
5703 - }
5704 - }
5705 -
5706 - if (!$should_sync) {
5707 - return;
5708 - }
5709 -
5710 - // Resolve the pre-trash URL. wp_trash_post renames the slug with "__trashed" before firing
5711 - // this hook, so get_permalink() here would return the trashed URL and md5() would miss the
5712 - // real vector IDs stored under the original URL.
5713 - $source_url = $this->mxchat_resolve_pre_trash_url($post_id);
5714 - if (!$source_url) {
5715 - //error_log('MXChat: Failed to resolve source URL for post ' . $post_id);
5716 - return;
5717 - }
5718 -
5719 - // Use chunk-aware deletion (handles both chunked and non-chunked content)
5720 - $delete_result = MxChat_Utils::delete_chunks_for_url($source_url, 'default');
5721 -
5722 - if (is_wp_error($delete_result)) {
5723 - //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message());
5724 - }
5725 -
5726 - delete_transient('mxchat_prev_url_' . $post_id);
5727 - delete_transient('mxchat_prev_status_' . $post_id);
5728 -}
5729 -
5730 -/**
5731 - * Resolve the source URL for a post being trashed/deleted.
5732 - *
5733 - * Why: wp_trash_post appends "__trashed" to the slug before the wp_trash_post action fires, so
5734 - * get_permalink() returns a URL whose md5() won't match the vector IDs stored in Pinecone or
5735 - * the source_url rows in the WP DB. Prefer the URL captured by mxchat_store_pre_update_status
5736 - * (runs on pre_post_update, before the rename); fall back to stripping the __trashed suffix.
5737 - */
5738 -private function mxchat_resolve_pre_trash_url($post_id) {
5739 - $previous_url = get_transient('mxchat_prev_url_' . $post_id);
5740 - if (!empty($previous_url)) {
5741 - return $previous_url;
5742 - }
5743 -
5744 - $current = get_permalink($post_id);
5745 - if (!$current) {
5746 - return '';
5747 - }
5748 - return preg_replace('#__trashed(/?)$#', '$1', $current);
5749 -}
5750 -
5751 -
5752 -
5753 -public function mxchat_handle_product_change($post_id, $post, $update) {
5754 - if ($post->post_type !== 'product') {
5755 - return;
5756 - }
5757 -
5758 - if ($post->post_status === 'publish') {
5759 - add_action('shutdown', function() use ($post_id) {
5760 - $product = wc_get_product($post_id);
5761 - if ($product) {
5762 - $this->mxchat_store_product_embedding($product);
5763 - }
5764 - });
5765 - }
5766 -}
5767 -
5768 -/**
5769 - * Store WooCommerce product embeddings
5770 - */
5771 -private function mxchat_store_product_embedding($product) {
5772 - if (!isset($this->options['enable_woocommerce_integration']) ||
5773 - !in_array($this->options['enable_woocommerce_integration'], ['1', 'on'])) {
5774 - return;
5775 - }
5776 -
5777 - $source_url = get_permalink($product->get_id());
5778 - $product_id = $product->get_id();
5779 -
5780 - // Build product content
5781 - $title = $product->get_name();
5782 - $description = $product->get_description();
5783 - $short_description = $product->get_short_description();
5784 - $regular_price = $product->get_regular_price();
5785 - $sale_price = $product->get_sale_price();
5786 - $price = $product->get_price();
5787 - $sku = $product->get_sku();
5788 -
5789 - // Get currency symbol
5790 - $currency_symbol = get_woocommerce_currency_symbol();
5791 -
5792 - // Format content consistently
5793 - $content = $title . "\n\n";
5794 -
5795 - if (!empty($short_description)) {
5796 - $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
5797 - }
5798 -
5799 - if (!empty($description)) {
5800 - $content .= wp_strip_all_tags($description) . "\n\n";
5801 - }
5802 -
5803 - // Add pricing information
5804 - if (!empty($regular_price)) {
5805 - $content .= "Price: " . $currency_symbol . $regular_price . "\n";
5806 - } elseif (!empty($price)) {
5807 - $content .= "Price: " . $currency_symbol . $price . "\n";
5808 - }
5809 -
5810 - if (!empty($sale_price) && $sale_price !== $regular_price) {
5811 - $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
5812 - }
5813 -
5814 - // Handle variable products - show price range
5815 - if ($product->is_type('variable')) {
5816 - $min_price = $product->get_variation_price('min');
5817 - $max_price = $product->get_variation_price('max');
5818 - if ($min_price !== $max_price) {
5819 - $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
5820 - }
5821 - }
5822 -
5823 - if (!empty($sku)) {
5824 - $content .= "SKU: " . $sku . "\n";
5825 - }
5826 -
5827 - // Get product categories
5828 - $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
5829 - if (!empty($categories) && !is_wp_error($categories)) {
5830 - $content .= "Categories: " . implode(', ', $categories) . "\n";
5831 - }
5832 -
5833 - // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
5834 - $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
5835 - if (!empty($custom_tabs) && is_array($custom_tabs)) {
5836 - foreach ($custom_tabs as $tab) {
5837 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
5838 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
5839 -
5840 - if (!empty($tab_title) && !empty($tab_content)) {
5841 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
5842 - }
5843 - }
5844 - }
5845 -
5846 - // Also check for reusable/saved tabs applied to this product
5847 - $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
5848 - if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
5849 - // Get the saved tabs option
5850 - $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
5851 - if (!empty($saved_tabs) && is_array($saved_tabs)) {
5852 - foreach ($applied_saved_tabs as $saved_tab_id) {
5853 - if (isset($saved_tabs[$saved_tab_id])) {
5854 - $tab = $saved_tabs[$saved_tab_id];
5855 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
5856 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
5857 -
5858 - if (!empty($tab_title) && !empty($tab_content)) {
5859 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
5860 - }
5861 - }
5862 - }
5863 - }
5864 - }
5865 -
5866 - // Get API key with proper model detection
5867 - $options = get_option('mxchat_options');
5868 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
5869 -
5870 - if (strpos($selected_model, 'voyage') === 0) {
5871 - $api_key = $options['voyage_api_key'] ?? '';
5872 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
5873 - $api_key = $options['gemini_api_key'] ?? '';
5874 - } else {
5875 - $api_key = $options['api_key'] ?? '';
5876 - }
5877 -
5878 - if (empty($api_key)) {
5879 - //error_log('MxChat Auto-sync: No API key configured for embedding model');
5880 - return;
5881 - }
5882 -
5883 - // Use the centralized utility function for storage
5884 - $result = MxChat_Utils::submit_content_to_db(
5885 - $content,
5886 - $source_url,
5887 - $api_key,
5888 - md5($source_url) // Vector ID for Pinecone
5889 - );
5890 -
5891 - // After successful storage, apply role restriction based on tags
5892 - if (!is_wp_error($result)) {
5893 - $this->apply_role_restriction_to_post($product_id, $source_url);
5894 - }
5895 -
5896 - if (is_wp_error($result)) {
5897 - //error_log('MxChat WooCommerce sync failed for product ' . $product_id . ': ' . $result->get_error_message());
5898 - }
5899 -}
5900 -
5901 -public function mxchat_handle_product_delete($post_id) {
5902 - if (get_post_type($post_id) !== 'product') {
5903 - return;
5904 - }
5905 -
5906 - $source_url = $this->mxchat_resolve_pre_trash_url($post_id);
5907 - if (!$source_url) {
5908 - return;
5909 - }
5910 -
5911 - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks)
5912 - MxChat_Utils::delete_chunks_for_url($source_url, 'default');
5913 -
5914 - delete_transient('mxchat_prev_url_' . $post_id);
5915 - delete_transient('mxchat_prev_status_' . $post_id);
5916 -}
5917 -
5918 -/**
5919 - * Handle individual Pinecone content deletion
5920 - */
5921 -public function mxchat_handle_pinecone_prompt_delete() {
5922 - // Check permissions
5923 - if (!current_user_can('manage_options')) {
5924 - wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
5925 - }
5926 -
5927 - // Verify nonce
5928 - if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'mxchat_delete_pinecone_prompt_nonce')) {
5929 - wp_die(esc_html__('Security check failed.', 'mxchat'));
5930 - }
5931 -
5932 - $vector_id = isset($_GET['vector_id']) ? sanitize_text_field($_GET['vector_id']) : '';
5933 -
5934 - if (empty($vector_id)) {
5935 - set_transient('mxchat_admin_notice_error',
5936 - esc_html__('Invalid vector ID.', 'mxchat'),
5937 - 30
5938 - );
5939 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5940 - exit;
5941 - }
5942 -
5943 - // Get Pinecone settings
5944 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5945 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5946 -
5947 - if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5948 - set_transient('mxchat_admin_notice_error',
5949 - esc_html__('Pinecone is not properly configured.', 'mxchat'),
5950 - 30
5951 - );
5952 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5953 - exit;
5954 - }
5955 -
5956 - // Delete from Pinecone
5957 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5958 - $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
5959 - $vector_id,
5960 - $pinecone_options['mxchat_pinecone_api_key'],
5961 - $pinecone_options['mxchat_pinecone_host']
5962 - );
5963 -
5964 - if ($result['success']) {
5965 - // No cache clearing needed since we removed caching
5966 - set_transient('mxchat_admin_notice_success',
5967 - esc_html__('Entry deleted successfully from Pinecone.', 'mxchat'),
5968 - 30
5969 - );
5970 - } else {
5971 - set_transient('mxchat_admin_notice_error',
5972 - esc_html__('Failed to delete entry: ', 'mxchat') . $result['message'],
5973 - 30
5974 - );
5975 - }
5976 -
5977 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5978 - exit;
5979 -}
5980 -/**
5981 - * Handle individual Pinecone content deletion via AJAX
5982 - */
5983 -public function ajax_mxchat_delete_pinecone_prompt() {
5984 - // Verify nonce and permissions
5985 - if (!check_ajax_referer('mxchat_delete_pinecone_prompt_nonce', 'nonce', false)) {
5986 - wp_send_json_error('Invalid nonce');
5987 - exit;
5988 - }
5989 -
5990 - if (!current_user_can('manage_options')) {
5991 - wp_send_json_error('Unauthorized access');
5992 - exit;
5993 - }
5994 -
5995 - $vector_id = isset($_POST['vector_id']) ? sanitize_text_field($_POST['vector_id']) : '';
5996 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
5997 -
5998 - if (empty($vector_id)) {
5999 - wp_send_json_error('Missing vector ID');
6000 - exit;
6001 - }
6002 -
6003 - // Get bot-specific Pinecone settings
6004 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
6005 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
6006 -
6007 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6008 -
6009 - if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
6010 - wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
6011 - exit;
6012 - }
6013 -
6014 - // Delete from the correct Pinecone index
6015 - $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
6016 - $vector_id,
6017 - $pinecone_options['mxchat_pinecone_api_key'],
6018 - $pinecone_options['mxchat_pinecone_host']
6019 - );
6020 -
6021 - if ($result['success']) {
6022 - // No cache clearing needed since we removed caching
6023 - wp_send_json_success(array(
6024 - 'message' => 'Entry deleted successfully from Pinecone',
6025 - 'vector_id' => $vector_id,
6026 - 'bot_id' => $bot_id
6027 - ));
6028 - } else {
6029 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Failed to delete from Pinecone: ' . $result['message'], array('vector_id' => $vector_id, 'bot_id' => $bot_id));
6030 - wp_send_json_error('Failed to delete from Pinecone: ' . $result['message']);
6031 - }
6032 -
6033 - exit;
6034 -}
6035 -
6036 -/**
6037 - * Handle deletion of all chunks for a given source URL via AJAX
6038 - * Follows the same pattern as ajax_mxchat_delete_pinecone_prompt
6039 - */
6040 -public function ajax_mxchat_delete_chunks_by_url() {
6041 - // Verify nonce and permissions
6042 - if (!check_ajax_referer('mxchat_delete_chunks_nonce', 'nonce', false)) {
6043 - wp_send_json_error('Invalid nonce');
6044 - exit;
6045 - }
6046 -
6047 - if (!current_user_can('manage_options')) {
6048 - wp_send_json_error('Unauthorized access');
6049 - exit;
6050 - }
6051 -
6052 - $source_url = isset($_POST['source_url']) ? esc_url_raw($_POST['source_url']) : '';
6053 - $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
6054 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
6055 -
6056 - if (empty($source_url)) {
6057 - wp_send_json_error('Missing source URL');
6058 - exit;
6059 - }
6060 -
6061 - // Generate the base vector ID from the source URL (same as how chunks are created)
6062 - $base_vector_id = md5($source_url);
6063 -
6064 - if ($data_source === 'pinecone') {
6065 - // Get bot-specific Pinecone settings (same as working delete function)
6066 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
6067 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
6068 -
6069 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6070 -
6071 - if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
6072 - wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
6073 - exit;
6074 - }
6075 -
6076 - $api_key = $pinecone_options['mxchat_pinecone_api_key'];
6077 - $host = $pinecone_options['mxchat_pinecone_host'];
6078 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
6079 -
6080 - // Collect all vector IDs to delete
6081 - $vectors_to_delete = array();
6082 -
6083 - // Add the original single-vector ID (for non-chunked content)
6084 - $vectors_to_delete[] = $base_vector_id;
6085 -
6086 - // Use Pinecone list API to find all chunk vectors with this prefix
6087 - // NOTE: Pinecone List API is a GET request with query parameters, not POST
6088 - $prefix = $base_vector_id . '_chunk_';
6089 -
6090 - $query_params = array(
6091 - 'prefix' => $prefix,
6092 - 'limit' => 100
6093 - );
6094 -
6095 - if (!empty($namespace)) {
6096 - $query_params['namespace'] = $namespace;
6097 - }
6098 -
6099 - $list_url = "https://{$host}/vectors/list?" . http_build_query($query_params);
6100 -
6101 - $list_response = wp_remote_get($list_url, array(
6102 - 'headers' => array(
6103 - 'Api-Key' => $api_key,
6104 - 'accept' => 'application/json'
6105 - ),
6106 - 'timeout' => 30
6107 - ));
6108 -
6109 - if (!is_wp_error($list_response)) {
6110 - $list_body_response = wp_remote_retrieve_body($list_response);
6111 - $list_data = json_decode($list_body_response, true);
6112 - if (!empty($list_data['vectors'])) {
6113 - foreach ($list_data['vectors'] as $vector) {
6114 - if (isset($vector['id'])) {
6115 - $vectors_to_delete[] = $vector['id'];
6116 - }
6117 - }
6118 - }
6119 - }
6120 -
6121 - if (empty($vectors_to_delete)) {
6122 - wp_send_json_success(array(
6123 - 'message' => 'No vectors found to delete',
6124 - 'source_url' => $source_url
6125 - ));
6126 - exit;
6127 - }
6128 -
6129 - // Delete all vectors using the same endpoint as the working function
6130 - $delete_url = "https://{$host}/vectors/delete";
6131 -
6132 - $delete_body = array(
6133 - 'ids' => $vectors_to_delete
6134 - );
6135 -
6136 - if (!empty($namespace)) {
6137 - $delete_body['namespace'] = $namespace;
6138 - }
6139 -
6140 - $delete_response = wp_remote_post($delete_url, array(
6141 - 'headers' => array(
6142 - 'Api-Key' => $api_key,
6143 - 'accept' => 'application/json',
6144 - 'content-type' => 'application/json'
6145 - ),
6146 - 'body' => wp_json_encode($delete_body),
6147 - 'timeout' => 30
6148 - ));
6149 -
6150 - if (is_wp_error($delete_response)) {
6151 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Failed to delete chunks from Pinecone: ' . $delete_response->get_error_message(), array('source_url' => $source_url));
6152 - wp_send_json_error('Failed to delete from Pinecone: ' . $delete_response->get_error_message());
6153 - exit;
6154 - }
6155 -
6156 - $response_code = wp_remote_retrieve_response_code($delete_response);
6157 -
6158 - if ($response_code !== 200) {
6159 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Pinecone API error (HTTP ' . $response_code . ')', array('source_url' => $source_url));
6160 - wp_send_json_error('Pinecone API error (HTTP ' . $response_code . ')');
6161 - exit;
6162 - }
6163 -
6164 - wp_send_json_success(array(
6165 - 'message' => 'All chunks deleted successfully from Pinecone',
6166 - 'source_url' => $source_url,
6167 - 'deleted_count' => count($vectors_to_delete)
6168 - ));
6169 -
6170 - } else {
6171 - // WordPress database deletion
6172 - global $wpdb;
6173 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6174 -
6175 - $result = $wpdb->delete(
6176 - $table_name,
6177 - array('source_url' => $source_url),
6178 - array('%s')
6179 - );
6180 -
6181 - if ($result === false) {
6182 - MxChat_Admin::mxchat_log_debug('database_error', 'Failed to delete from database: ' . $wpdb->last_error, array('source_url' => $source_url));
6183 - wp_send_json_error('Failed to delete from database: ' . $wpdb->last_error);
6184 - exit;
6185 - }
6186 -
6187 - wp_send_json_success(array(
6188 - 'message' => 'All chunks deleted successfully from database',
6189 - 'source_url' => $source_url,
6190 - 'deleted_count' => $result
6191 - ));
6192 - }
6193 -
6194 - exit;
6195 -}
6196 -
6197 -/**
6198 - * Handle individual WordPress database content deletion via AJAX
6199 - * Mirrors the Pinecone delete handler but for WordPress database entries
6200 - */
6201 -public function ajax_mxchat_delete_wordpress_prompt() {
6202 - // Verify nonce and permissions
6203 - if (!check_ajax_referer('mxchat_delete_wordpress_prompt_nonce', 'nonce', false)) {
6204 - wp_send_json_error('Invalid nonce');
6205 - exit;
6206 - }
6207 -
6208 - if (!current_user_can('manage_options')) {
6209 - wp_send_json_error('Unauthorized access');
6210 - exit;
6211 - }
6212 -
6213 - $entry_id = isset($_POST['entry_id']) ? intval($_POST['entry_id']) : 0;
6214 -
6215 - if (empty($entry_id)) {
6216 - wp_send_json_error('Missing entry ID');
6217 - exit;
6218 - }
6219 -
6220 - global $wpdb;
6221 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6222 -
6223 - // Clear cache for this entry
6224 - wp_cache_delete('prompt_' . $entry_id, 'mxchat_prompts');
6225 -
6226 - // Delete from database
6227 - $result = $wpdb->delete(
6228 - $table_name,
6229 - array('id' => $entry_id),
6230 - array('%d')
6231 - );
6232 -
6233 - if ($result !== false) {
6234 - wp_send_json_success(array(
6235 - 'message' => 'Entry deleted successfully',
6236 - 'entry_id' => $entry_id
6237 - ));
6238 - } else {
6239 - wp_send_json_error('Failed to delete entry from database');
6240 - }
6241 -
6242 - exit;
6243 -}
6244 -
6245 -/**
6246 - * Handle bulk deletion of knowledge entries via AJAX
6247 - * Supports both Pinecone and WordPress database entries
6248 - */
6249 -public function ajax_mxchat_bulk_delete_knowledge() {
6250 - // Verify nonce and permissions
6251 - if (!check_ajax_referer('mxchat_bulk_delete_knowledge_nonce', 'nonce', false)) {
6252 - wp_send_json_error('Invalid nonce');
6253 - exit;
6254 - }
6255 -
6256 - if (!current_user_can('manage_options')) {
6257 - wp_send_json_error('Unauthorized access');
6258 - exit;
6259 - }
6260 -
6261 - $entries = isset($_POST['entries']) ? $_POST['entries'] : array();
6262 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
6263 -
6264 - if (empty($entries) || !is_array($entries)) {
6265 - wp_send_json_error('No entries provided');
6266 - exit;
6267 - }
6268 -
6269 - // Extend execution time — bulk Pinecone operations can take a while
6270 - if (function_exists('set_time_limit')) {
6271 - set_time_limit(120);
6272 - }
6273 -
6274 - $success_ids = array();
6275 - $failed_ids = array();
6276 - $errors = array();
6277 -
6278 - global $wpdb;
6279 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6280 -
6281 - // Get Pinecone manager for Pinecone deletions
6282 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
6283 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
6284 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6285 -
6286 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
6287 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
6288 -
6289 - // =============================================
6290 - // PHASE 1: Collect all Pinecone vector IDs
6291 - // and separate WordPress entries
6292 - // =============================================
6293 - $pinecone_entry_ids = array(); // entry IDs that are pinecone-sourced
6294 - $wordpress_entries = array(); // entries for WordPress DB deletion
6295 - $all_vector_ids = array(); // all pinecone vector IDs to delete in one batch
6296 -
6297 - foreach ($entries as $entry) {
6298 - $entry_id = sanitize_text_field($entry['id'] ?? '');
6299 - $source = sanitize_text_field($entry['source'] ?? 'wordpress');
6300 - $source_url = isset($entry['sourceUrl']) ? esc_url_raw($entry['sourceUrl']) : '';
6301 - $is_group = isset($entry['isGroup']) && ($entry['isGroup'] === true || $entry['isGroup'] === 'true');
6302 -
6303 - if (empty($entry_id)) {
6304 - continue;
6305 - }
6306 -
6307 - if ($source === 'pinecone') {
6308 - if (!$use_pinecone || empty($api_key)) {
6309 - $failed_ids[] = $entry_id;
6310 - $errors[] = "Pinecone not configured for entry: $entry_id";
6311 - continue;
6312 - }
6313 -
6314 - $pinecone_entry_ids[] = $entry_id;
6315 -
6316 - if ($is_group && !empty($source_url)) {
6317 - // Grouped/chunked entry: collect base ID + chunk IDs via List API
6318 - $base_vector_id = md5($source_url);
6319 - $all_vector_ids[] = $base_vector_id;
6320 -
6321 - $list_url = 'https://' . $host . '/vectors/list?prefix=' . urlencode($base_vector_id . '_chunk_') . '&limit=100';
6322 - $list_response = wp_remote_get($list_url, array(
6323 - 'headers' => array(
6324 - 'Api-Key' => $api_key,
6325 - 'accept' => 'application/json'
6326 - ),
6327 - 'timeout' => 30
6328 - ));
6329 -
6330 - if (!is_wp_error($list_response)) {
6331 - $list_body = json_decode(wp_remote_retrieve_body($list_response), true);
6332 - if (!empty($list_body['vectors']) && is_array($list_body['vectors'])) {
6333 - foreach ($list_body['vectors'] as $vector) {
6334 - if (isset($vector['id'])) {
6335 - $all_vector_ids[] = $vector['id'];
6336 - }
6337 - }
6338 - }
6339 - }
6340 - } else {
6341 - // Single entry: the entry_id IS the vector ID
6342 - $all_vector_ids[] = $entry_id;
6343 - }
6344 - } else {
6345 - $wordpress_entries[] = $entry;
6346 - }
6347 - }
6348 -
6349 - // =============================================
6350 - // PHASE 2: Single batch delete to Pinecone
6351 - // =============================================
6352 - if (!empty($all_vector_ids)) {
6353 - $all_vector_ids = array_values(array_unique($all_vector_ids));
6354 - $pinecone_success = true;
6355 - $batches = array_chunk($all_vector_ids, 100);
6356 -
6357 - foreach ($batches as $batch) {
6358 - $delete_response = wp_remote_post("https://{$host}/vectors/delete", array(
6359 - 'headers' => array(
6360 - 'Api-Key' => $api_key,
6361 - 'accept' => 'application/json',
6362 - 'content-type' => 'application/json'
6363 - ),
6364 - 'body' => wp_json_encode(array('ids' => $batch)),
6365 - 'timeout' => 60
6366 - ));
6367 -
6368 - if (is_wp_error($delete_response)) {
6369 - $pinecone_success = false;
6370 - $errors[] = 'Pinecone batch deletion failed: ' . $delete_response->get_error_message();
6371 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Bulk delete batch failed: ' . $delete_response->get_error_message());
6372 - } else {
6373 - $response_code = wp_remote_retrieve_response_code($delete_response);
6374 - if ($response_code !== 200) {
6375 - $pinecone_success = false;
6376 - $response_body = wp_remote_retrieve_body($delete_response);
6377 - $errors[] = "Pinecone API error (HTTP $response_code)";
6378 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Bulk delete batch failed (HTTP ' . $response_code . ')', array('response' => substr($response_body, 0, 200)));
6379 - }
6380 - }
6381 - }
6382 -
6383 - // Mark all pinecone entries based on batch result
6384 - foreach ($pinecone_entry_ids as $eid) {
6385 - if ($pinecone_success) {
6386 - $success_ids[] = $eid;
6387 - } else {
6388 - $failed_ids[] = $eid;
6389 - }
6390 - }
6391 - }
6392 -
6393 - // =============================================
6394 - // PHASE 3: WordPress database deletions
6395 - // =============================================
6396 - foreach ($wordpress_entries as $entry) {
6397 - $entry_id = sanitize_text_field($entry['id'] ?? '');
6398 - $source_url = isset($entry['sourceUrl']) ? esc_url_raw($entry['sourceUrl']) : '';
6399 - $is_group = isset($entry['isGroup']) && ($entry['isGroup'] === true || $entry['isGroup'] === 'true');
6400 -
6401 - if (empty($entry_id)) {
6402 - continue;
6403 - }
6404 -
6405 - try {
6406 - if ($is_group && !empty($source_url)) {
6407 - $result = $wpdb->delete(
6408 - $table_name,
6409 - array('source_url' => $source_url),
6410 - array('%s')
6411 - );
6412 - } else {
6413 - wp_cache_delete('prompt_' . $entry_id, 'mxchat_prompts');
6414 - $result = $wpdb->delete(
6415 - $table_name,
6416 - array('id' => intval($entry_id)),
6417 - array('%d')
6418 - );
6419 - }
6420 -
6421 - if ($result !== false) {
6422 - $success_ids[] = $entry_id;
6423 - } else {
6424 - $failed_ids[] = $entry_id;
6425 - $errors[] = "Database error for entry: $entry_id";
6426 - }
6427 - } catch (Exception $e) {
6428 - $failed_ids[] = $entry_id;
6429 - $errors[] = $e->getMessage();
6430 - }
6431 - }
6432 -
6433 - wp_send_json_success(array(
6434 - 'success_ids' => $success_ids,
6435 - 'failed_ids' => $failed_ids,
6436 - 'errors' => $errors,
6437 - 'total_processed' => count($success_ids) + count($failed_ids)
6438 - ));
6439 -
6440 - exit;
6441 -}
6442 -
6443 -/**
6444 - * Get hierarchical roles for dropdown
6445 - */
6446 -public function mxchat_get_role_options() {
6447 - return array(
6448 - 'public' => __('Public (Everyone)', 'mxchat'),
6449 - 'logged_in' => __('Logged In Users', 'mxchat'),
6450 - 'subscriber' => __('Subscribers & Above', 'mxchat'),
6451 - 'contributor' => __('Contributors & Above', 'mxchat'),
6452 - 'author' => __('Authors & Above', 'mxchat'),
6453 - 'editor' => __('Editors & Above', 'mxchat'),
6454 - 'administrator' => __('Administrators Only', 'mxchat')
6455 - );
6456 -}
6457 -
6458 -/**
6459 - * Check if user has access to content based on role restriction
6460 - */
6461 -public function mxchat_user_has_content_access($role_restriction) {
6462 - // Public content is always accessible
6463 - if ($role_restriction === 'public' || empty($role_restriction)) {
6464 - return true;
6465 - }
6466 -
6467 - // Check if user is logged in for logged_in restriction
6468 - if ($role_restriction === 'logged_in') {
6469 - return is_user_logged_in();
6470 - }
6471 -
6472 - // If not logged in, no access to role-restricted content
6473 - if (!is_user_logged_in()) {
6474 - return false;
6475 - }
6476 -
6477 - $user = wp_get_current_user();
6478 - $user_roles = $user->roles;
6479 -
6480 - if (empty($user_roles)) {
6481 - return false;
6482 - }
6483 -
6484 - // Define role hierarchy (higher number = higher access)
6485 - $hierarchy = array(
6486 - 'subscriber' => 1,
6487 - 'contributor' => 2,
6488 - 'author' => 3,
6489 - 'editor' => 4,
6490 - 'administrator' => 5
6491 - );
6492 -
6493 - // Get required level
6494 - $required_level = isset($hierarchy[$role_restriction]) ? $hierarchy[$role_restriction] : 0;
6495 -
6496 - // Check if user has required level or higher
6497 - foreach ($user_roles as $user_role) {
6498 - $user_level = isset($hierarchy[$user_role]) ? $hierarchy[$user_role] : 0;
6499 - if ($user_level >= $required_level) {
6500 - return true;
6501 - }
6502 - }
6503 -
6504 - return false;
6505 -}
6506 -
6507 -/**
6508 - * Handle role restriction updates via AJAX
6509 - * Removed cache clearing call since we removed caching
6510 - */
6511 -public function ajax_mxchat_update_role_restriction() {
6512 - // Verify nonce and permissions
6513 - if (!check_ajax_referer('mxchat_update_role_nonce', 'nonce', false)) {
6514 - wp_send_json_error('Invalid nonce');
6515 - exit;
6516 - }
6517 -
6518 - if (!current_user_can('manage_options')) {
6519 - wp_send_json_error('Unauthorized access');
6520 - exit;
6521 - }
6522 -
6523 - $entry_id = isset($_POST['entry_id']) ? sanitize_text_field($_POST['entry_id']) : '';
6524 - $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
6525 - $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
6526 -
6527 - if (empty($entry_id)) {
6528 - wp_send_json_error('Invalid entry ID');
6529 - exit;
6530 - }
6531 -
6532 - // Get knowledge manager instance to validate role restriction
6533 - $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
6534 - $valid_roles = array_keys($knowledge_manager->mxchat_get_role_options());
6535 - if (!in_array($role_restriction, $valid_roles)) {
6536 - wp_send_json_error('Invalid role restriction');
6537 - exit;
6538 - }
6539 -
6540 - global $wpdb;
6541 -
6542 - if ($data_source === 'pinecone') {
6543 - // Handle Pinecone role restriction (stored separately in WordPress table)
6544 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6545 -
6546 - // Use REPLACE to insert or update the role restriction
6547 - $result = $wpdb->replace(
6548 - $roles_table,
6549 - array(
6550 - 'vector_id' => $entry_id,
6551 - 'role_restriction' => $role_restriction,
6552 - 'updated_at' => current_time('mysql')
6553 - ),
6554 - array('%s', '%s', '%s')
6555 - );
6556 -
6557 - // No cache clearing needed since we removed caching
6558 -
6559 - } else {
6560 - // Handle WordPress database role restriction (existing functionality)
6561 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6562 -
6563 - $result = $wpdb->update(
6564 - $table_name,
6565 - array('role_restriction' => $role_restriction),
6566 - array('id' => absint($entry_id)),
6567 - array('%s'),
6568 - array('%d')
6569 - );
6570 - }
6571 -
6572 - if ($result === false) {
6573 - wp_send_json_error('Database update failed: ' . $wpdb->last_error);
6574 - exit;
6575 - }
6576 -
6577 - wp_send_json_success(array(
6578 - 'message' => 'Role restriction updated successfully',
6579 - 'role_restriction' => $role_restriction,
6580 - 'data_source' => $data_source,
6581 - 'entry_id' => $entry_id
6582 - ));
6583 - exit;
6584 -}
6585 -
6586 -// ========================================
6587 -// ROLE-BASED CONTENT RESTRICTIONS - NEW FUNCTIONS
6588 -// Add these to your MxChat_Knowledge_Manager class
6589 -// ========================================
6590 -
6591 -/**
6592 - * Initialize role-based content hooks
6593 - * Add this call to your __construct() or mxchat_init_hooks() method
6594 - */
6595 -private function mxchat_init_role_hooks() {
6596 - // AJAX handlers for tag-role mappings
6597 - add_action('wp_ajax_mxchat_add_tag_role_mapping', array($this, 'ajax_add_tag_role_mapping'));
6598 - add_action('wp_ajax_mxchat_delete_tag_role_mapping', array($this, 'ajax_delete_tag_role_mapping'));
6599 - add_action('wp_ajax_mxchat_get_tag_role_mappings', array($this, 'ajax_get_tag_role_mappings'));
6600 - add_action('wp_ajax_mxchat_bulk_update_tag_roles', array($this, 'ajax_bulk_update_tag_roles'));
6601 -
6602 - // Hook to automatically update role restrictions when tags are added/removed
6603 - add_action('set_object_terms', array($this, 'handle_tag_change'), 10, 6);
6604 -
6605 - // Hook to apply role restrictions on auto-sync
6606 - add_action('mxchat_content_stored', array($this, 'apply_role_restriction_after_storage'), 10, 2);
6607 -}
6608 -
6609 -/**
6610 - * Add tag-role mapping via AJAX
6611 - */
6612 -public function ajax_add_tag_role_mapping() {
6613 - // Verify nonce and permissions
6614 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6615 -
6616 - if (!current_user_can('manage_options')) {
6617 - wp_send_json_error('Unauthorized access');
6618 - exit;
6619 - }
6620 -
6621 - $tag_slug = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
6622 - $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
6623 -
6624 - if (empty($tag_slug)) {
6625 - wp_send_json_error('Tag slug is required');
6626 - exit;
6627 - }
6628 -
6629 - // Validate role restriction
6630 - $valid_roles = array_keys($this->mxchat_get_role_options());
6631 - if (!in_array($role_restriction, $valid_roles)) {
6632 - wp_send_json_error('Invalid role restriction');
6633 - exit;
6634 - }
6635 -
6636 - // Check if tag exists in WordPress
6637 - $term = get_term_by('slug', $tag_slug, 'post_tag');
6638 - if (!$term) {
6639 - wp_send_json_error('Tag does not exist in WordPress');
6640 - exit;
6641 - }
6642 -
6643 - // Get existing mappings
6644 - $mappings = get_option('mxchat_tag_role_mappings', array());
6645 -
6646 - // Check if mapping already exists
6647 - if (isset($mappings[$tag_slug])) {
6648 - wp_send_json_error('Mapping for this tag already exists');
6649 - exit;
6650 - }
6651 -
6652 - // Add new mapping
6653 - $mappings[$tag_slug] = $role_restriction;
6654 - update_option('mxchat_tag_role_mappings', $mappings);
6655 -
6656 - wp_send_json_success(array(
6657 - 'message' => 'Tag-role mapping added successfully',
6658 - 'tag_slug' => $tag_slug,
6659 - 'role_restriction' => $role_restriction
6660 - ));
6661 - exit;
6662 -}
6663 -
6664 -/**
6665 - * Delete tag-role mapping via AJAX
6666 - */
6667 -public function ajax_delete_tag_role_mapping() {
6668 - // Verify nonce and permissions
6669 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6670 -
6671 - if (!current_user_can('manage_options')) {
6672 - wp_send_json_error('Unauthorized access');
6673 - exit;
6674 - }
6675 -
6676 - $tag_slug = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
6677 -
6678 - if (empty($tag_slug)) {
6679 - wp_send_json_error('Tag slug is required');
6680 - exit;
6681 - }
6682 -
6683 - // Get existing mappings
6684 - $mappings = get_option('mxchat_tag_role_mappings', array());
6685 -
6686 - // Check if mapping exists
6687 - if (!isset($mappings[$tag_slug])) {
6688 - wp_send_json_error('Mapping does not exist');
6689 - exit;
6690 - }
6691 -
6692 - // Remove mapping
6693 - unset($mappings[$tag_slug]);
6694 - update_option('mxchat_tag_role_mappings', $mappings);
6695 -
6696 - wp_send_json_success(array(
6697 - 'message' => 'Tag-role mapping deleted successfully',
6698 - 'tag_slug' => $tag_slug
6699 - ));
6700 - exit;
6701 -}
6702 -
6703 -/**
6704 - * Get all tag-role mappings via AJAX
6705 - */
6706 -public function ajax_get_tag_role_mappings() {
6707 - // Verify nonce and permissions
6708 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6709 -
6710 - if (!current_user_can('manage_options')) {
6711 - wp_send_json_error('Unauthorized access');
6712 - exit;
6713 - }
6714 -
6715 - // Get mappings
6716 - $mappings = get_option('mxchat_tag_role_mappings', array());
6717 - $role_options = $this->mxchat_get_role_options();
6718 -
6719 - $formatted_mappings = array();
6720 -
6721 - foreach ($mappings as $tag_slug => $role_restriction) {
6722 - // Get tag object
6723 - $term = get_term_by('slug', $tag_slug, 'post_tag');
6724 -
6725 - // Count posts with this tag
6726 - $post_count = 0;
6727 - if ($term) {
6728 - $post_count = $term->count;
6729 - }
6730 -
6731 - $formatted_mappings[] = array(
6732 - 'tag_slug' => $tag_slug,
6733 - 'role_restriction' => $role_restriction,
6734 - 'role_label' => $role_options[$role_restriction] ?? $role_restriction,
6735 - 'post_count' => $post_count
6736 - );
6737 - }
6738 -
6739 - wp_send_json_success(array(
6740 - 'mappings' => $formatted_mappings
6741 - ));
6742 - exit;
6743 -}
6744 -
6745 -/**
6746 - * Bulk update role restrictions for all existing content with mapped tags
6747 - */
6748 -public function ajax_bulk_update_tag_roles() {
6749 - // Verify nonce and permissions
6750 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6751 -
6752 - if (!current_user_can('manage_options')) {
6753 - wp_send_json_error('Unauthorized access');
6754 - exit;
6755 - }
6756 -
6757 - // Get mappings
6758 - $mappings = get_option('mxchat_tag_role_mappings', array());
6759 -
6760 - if (empty($mappings)) {
6761 - wp_send_json_error('No tag-role mappings found');
6762 - exit;
6763 - }
6764 -
6765 - global $wpdb;
6766 -
6767 - // Check if using Pinecone
6768 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
6769 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6770 -
6771 - $updated_count = 0;
6772 - $details = array();
6773 -
6774 - foreach ($mappings as $tag_slug => $role_restriction) {
6775 - // Get all posts with this tag
6776 - $posts = get_posts(array(
6777 - 'tag' => $tag_slug,
6778 - 'post_type' => 'any',
6779 - 'posts_per_page' => -1,
6780 - 'fields' => 'ids',
6781 - 'post_status' => 'publish'
6782 - ));
6783 -
6784 - if (empty($posts)) {
6785 - continue;
6786 - }
6787 -
6788 - $tag_updated = 0;
6789 -
6790 - foreach ($posts as $post_id) {
6791 - $source_url = get_permalink($post_id);
6792 - if (!$source_url) {
6793 - continue;
6794 - }
6795 -
6796 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
6797 - // Update Pinecone role restriction
6798 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6799 - $vector_id = md5($source_url);
6800 -
6801 - $result = $wpdb->replace(
6802 - $roles_table,
6803 - array(
6804 - 'vector_id' => $vector_id,
6805 - 'role_restriction' => $role_restriction,
6806 - 'updated_at' => current_time('mysql')
6807 - ),
6808 - array('%s', '%s', '%s')
6809 - );
6810 - } else {
6811 - // Update WordPress DB
6812 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6813 -
6814 - $result = $wpdb->update(
6815 - $table_name,
6816 - array('role_restriction' => $role_restriction),
6817 - array('source_url' => $source_url),
6818 - array('%s'),
6819 - array('%s')
6820 - );
6821 - }
6822 -
6823 - if ($result !== false) {
6824 - $tag_updated++;
6825 - $updated_count++;
6826 - }
6827 - }
6828 -
6829 - if ($tag_updated > 0) {
6830 - $details[] = sprintf(
6831 - 'Tag "%s" (%s): %d posts updated',
6832 - $tag_slug,
6833 - $role_restriction,
6834 - $tag_updated
6835 - );
6836 - }
6837 - }
6838 -
6839 - wp_send_json_success(array(
6840 - 'message' => 'Bulk update completed',
6841 - 'updated_count' => $updated_count,
6842 - 'tags_processed' => count($mappings),
6843 - 'details' => $details
6844 - ));
6845 - exit;
6846 -}
6847 -
6848 -/**
6849 - * Handle tag changes on posts (when tags are added or removed)
6850 - */
6851 -public function handle_tag_change($object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids) {
6852 - // Only process post tags
6853 - if ($taxonomy !== 'post_tag') {
6854 - return;
6855 - }
6856 -
6857 - // Get tag-role mappings
6858 - $mappings = get_option('mxchat_tag_role_mappings', array());
6859 -
6860 - if (empty($mappings)) {
6861 - return;
6862 - }
6863 -
6864 - // Get the post's URL
6865 - $source_url = get_permalink($object_id);
6866 - if (!$source_url) {
6867 - return;
6868 - }
6869 -
6870 - // Determine the highest role restriction based on tags
6871 - $highest_role = 'public';
6872 - $role_hierarchy = array(
6873 - 'public' => 0,
6874 - 'logged_in' => 1,
6875 - 'subscriber' => 2,
6876 - 'contributor' => 3,
6877 - 'author' => 4,
6878 - 'editor' => 5,
6879 - 'administrator' => 6
6880 - );
6881 -
6882 - // Get all current tags for the post
6883 - $current_tags = wp_get_post_tags($object_id, array('fields' => 'slugs'));
6884 -
6885 - // Find the highest role restriction among the tags
6886 - foreach ($current_tags as $tag_slug) {
6887 - if (isset($mappings[$tag_slug])) {
6888 - $role = $mappings[$tag_slug];
6889 - if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
6890 - $highest_role = $role;
6891 - }
6892 - }
6893 - }
6894 -
6895 - // Update the role restriction in the database
6896 - global $wpdb;
6897 -
6898 - // Check if using Pinecone
6899 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
6900 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6901 -
6902 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
6903 - // Update Pinecone role restriction
6904 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6905 - $vector_id = md5($source_url);
6906 -
6907 - $wpdb->replace(
6908 - $roles_table,
6909 - array(
6910 - 'vector_id' => $vector_id,
6911 - 'role_restriction' => $highest_role,
6912 - 'updated_at' => current_time('mysql')
6913 - ),
6914 - array('%s', '%s', '%s')
6915 - );
6916 - } else {
6917 - // Update WordPress DB
6918 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6919 -
6920 - $wpdb->update(
6921 - $table_name,
6922 - array('role_restriction' => $highest_role),
6923 - array('source_url' => $source_url),
6924 - array('%s'),
6925 - array('%s')
6926 - );
6927 - }
6928 -}
6929 -
6930 -/**
6931 - * Apply role restriction after content is stored (for auto-sync)
6932 - */
6933 -public function apply_role_restriction_after_storage($post_id, $source_url) {
6934 - // Get tag-role mappings
6935 - $mappings = get_option('mxchat_tag_role_mappings', array());
6936 -
6937 - if (empty($mappings)) {
6938 - return;
6939 - }
6940 -
6941 - // Get all tags for the post
6942 - $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
6943 -
6944 - if (empty($post_tags)) {
6945 - return;
6946 - }
6947 -
6948 - // Determine the highest role restriction based on tags
6949 - $highest_role = 'public';
6950 - $role_hierarchy = array(
6951 - 'public' => 0,
6952 - 'logged_in' => 1,
6953 - 'subscriber' => 2,
6954 - 'contributor' => 3,
6955 - 'author' => 4,
6956 - 'editor' => 5,
6957 - 'administrator' => 6
6958 - );
6959 -
6960 - foreach ($post_tags as $tag_slug) {
6961 - if (isset($mappings[$tag_slug])) {
6962 - $role = $mappings[$tag_slug];
6963 - if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
6964 - $highest_role = $role;
6965 - }
6966 - }
6967 - }
6968 -
6969 - // If no restricted tags found, return (leave as public)
6970 - if ($highest_role === 'public') {
6971 - return;
6972 - }
6973 -
6974 - // Update the role restriction
6975 - global $wpdb;
6976 -
6977 - // Check if using Pinecone
6978 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
6979 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6980 -
6981 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
6982 - // Update Pinecone role restriction
6983 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6984 - $vector_id = md5($source_url);
6985 -
6986 - $wpdb->replace(
6987 - $roles_table,
6988 - array(
6989 - 'vector_id' => $vector_id,
6990 - 'role_restriction' => $highest_role,
6991 - 'updated_at' => current_time('mysql')
6992 - ),
6993 - array('%s', '%s', '%s')
6994 - );
6995 - } else {
6996 - // Update WordPress DB
6997 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6998 -
6999 - $wpdb->update(
7000 - $table_name,
7001 - array('role_restriction' => $highest_role),
7002 - array('source_url' => $source_url),
7003 - array('%s'),
7004 - array('%s')
7005 - );
7006 - }
7007 -}
7008 -
7009 -
7010 - // ========================================
7011 - // HELPER METHODS
7012 - // ========================================
7013 -
7014 - /**
7015 - * Check if user has required permissions for content processing
7016 - */
7017 - private function mxchat_check_user_permissions() {
7018 - if (!current_user_can('manage_options')) {
7019 - wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
7020 - }
7021 - }
7022 -
7023 - /**
7024 - * Validate nonce for security
7025 - */
7026 - private function mxchat_validate_nonce($nonce_name, $nonce_action) {
7027 - if (!isset($_POST[$nonce_name]) || !wp_verify_nonce($_POST[$nonce_name], $nonce_action)) {
7028 - wp_die(esc_html__('Security check failed.', 'mxchat'));
7029 - }
7030 - }
7031 -
7032 - /**
7033 - * Get embedding API credentials
7034 - */
7035 - private function mxchat_get_embedding_credentials() {
7036 - $embedding_model = $this->options['embedding_model'] ?? 'text-embedding-ada-002';
7037 -
7038 - if (strpos($embedding_model, 'text-embedding-') !== false) {
7039 - return array(
7040 - 'type' => 'openai',
7041 - 'api_key' => $this->options['api_key'] ?? ''
7042 - );
7043 - } elseif (strpos($embedding_model, 'voyage-') !== false) {
7044 - return array(
7045 - 'type' => 'voyage',
7046 - 'api_key' => $this->options['voyage_api_key'] ?? ''
7047 - );
7048 - } elseif (strpos($embedding_model, 'gemini-embedding-') !== false) {
7049 - return array(
7050 - 'type' => 'gemini',
7051 - 'api_key' => $this->options['gemini_api_key'] ?? ''
7052 - );
7053 - }
7054 -
7055 - return array('type' => 'unknown', 'api_key' => '');
7056 - }
7057 -
7058 - /**
7059 - * Log processing errors
7060 - */
7061 - private function mxchat_log_processing_error($operation, $error_message) {
7062 - //error_log("MxChat Knowledge Processing {$operation} Error: " . $error_message);
7063 - }
7064 -
7065 - /**
7066 - * Set admin notice transient
7067 - */
7068 - private function mxchat_set_admin_notice($type, $message) {
7069 - set_transient("mxchat_admin_notice_{$type}", $message, 30);
7070 - }
7071 -
7072 - /**
7073 - * Get Pinecone manager instance for vector operations
7074 - */
7075 - private function mxchat_get_pinecone_manager() {
7076 - return MxChat_Pinecone_Manager::get_instance();
7077 - }
7078 -
7079 -
7080 - // ========================================
7081 -// DATABASE QUEUE TABLE MANAGEMENT
7082 -// ========================================
7083 -
7084 -/**
7085 - * Create queue table on plugin activation
7086 - * Call this from your plugin activation hook
7087 - */
7088 -public function mxchat_create_queue_table() {
7089 - global $wpdb;
7090 -
7091 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7092 - $charset_collate = $wpdb->get_charset_collate();
7093 -
7094 - $sql = "CREATE TABLE IF NOT EXISTS $table_name (
7095 - id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
7096 - queue_id varchar(64) NOT NULL,
7097 - item_type varchar(20) NOT NULL,
7098 - item_data longtext NOT NULL,
7099 - status varchar(20) NOT NULL DEFAULT 'pending',
7100 - bot_id varchar(50) NOT NULL DEFAULT 'default',
7101 - priority int(11) NOT NULL DEFAULT 0,
7102 - attempts int(11) NOT NULL DEFAULT 0,
7103 - max_attempts int(11) NOT NULL DEFAULT 3,
7104 - error_message text DEFAULT NULL,
7105 - created_at datetime NOT NULL,
7106 - started_at datetime DEFAULT NULL,
7107 - completed_at datetime DEFAULT NULL,
7108 - PRIMARY KEY (id),
7109 - KEY queue_id (queue_id),
7110 - KEY status (status),
7111 - KEY item_type (item_type),
7112 - KEY priority (priority)
7113 - ) $charset_collate;";
7114 -
7115 - require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
7116 - dbDelta($sql);
7117 -
7118 - // Also create a meta table for queue metadata
7119 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
7120 -
7121 - $meta_sql = "CREATE TABLE IF NOT EXISTS $meta_table (
7122 - id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
7123 - queue_id varchar(64) NOT NULL,
7124 - meta_key varchar(255) NOT NULL,
7125 - meta_value longtext,
7126 - PRIMARY KEY (id),
7127 - KEY queue_id (queue_id),
7128 - KEY meta_key (meta_key)
7129 - ) $charset_collate;";
7130 -
7131 - dbDelta($meta_sql);
7132 -}
7133 -
7134 -/**
7135 - * Add items to the processing queue
7136 - *
7137 - * @param string $queue_id Unique identifier for this queue batch
7138 - * @param string $item_type Type of item (url, pdf_page)
7139 - * @param array $items Array of items to queue
7140 - * @param string $bot_id Bot ID for processing
7141 - * @return int Number of items queued
7142 - */
7143 -private function mxchat_add_to_queue($queue_id, $item_type, $items, $bot_id = 'default') {
7144 - global $wpdb;
7145 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7146 -
7147 - $queued_count = 0;
7148 - $priority = 0;
7149 -
7150 - foreach ($items as $item) {
7151 - $result = $wpdb->insert(
7152 - $table_name,
7153 - array(
7154 - 'queue_id' => $queue_id,
7155 - 'item_type' => $item_type,
7156 - 'item_data' => wp_json_encode($item),
7157 - 'status' => 'pending',
7158 - 'bot_id' => $bot_id,
7159 - 'priority' => $priority,
7160 - 'attempts' => 0,
7161 - 'max_attempts' => 3,
7162 - 'created_at' => current_time('mysql')
7163 - ),
7164 - array('%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%s')
7165 - );
7166 -
7167 - if ($result) {
7168 - $queued_count++;
7169 - }
7170 -
7171 - $priority++; // Process in order
7172 - }
7173 -
7174 - return $queued_count;
7175 -}
7176 -
7177 -/**
7178 - * Store queue metadata (total counts, source URL, etc.)
7179 - */
7180 -private function mxchat_set_queue_meta($queue_id, $meta_key, $meta_value) {
7181 - global $wpdb;
7182 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
7183 -
7184 - // Check if meta exists
7185 - $existing = $wpdb->get_var($wpdb->prepare(
7186 - "SELECT id FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
7187 - $queue_id,
7188 - $meta_key
7189 - ));
7190 -
7191 - if ($existing) {
7192 - // Update
7193 - $wpdb->update(
7194 - $meta_table,
7195 - array('meta_value' => maybe_serialize($meta_value)),
7196 - array('queue_id' => $queue_id, 'meta_key' => $meta_key),
7197 - array('%s'),
7198 - array('%s', '%s')
7199 - );
7200 - } else {
7201 - // Insert
7202 - $wpdb->insert(
7203 - $meta_table,
7204 - array(
7205 - 'queue_id' => $queue_id,
7206 - 'meta_key' => $meta_key,
7207 - 'meta_value' => maybe_serialize($meta_value)
7208 - ),
7209 - array('%s', '%s', '%s')
7210 - );
7211 - }
7212 -}
7213 -
7214 -/**
7215 - * Get queue metadata
7216 - */
7217 -private function mxchat_get_queue_meta($queue_id, $meta_key) {
7218 - global $wpdb;
7219 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
7220 -
7221 - $value = $wpdb->get_var($wpdb->prepare(
7222 - "SELECT meta_value FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
7223 - $queue_id,
7224 - $meta_key
7225 - ));
7226 -
7227 - return maybe_unserialize($value);
7228 -}
7229 -
7230 -// ========================================
7231 -// AJAX QUEUE PROCESSING HANDLERS
7232 -// ========================================
7233 -
7234 -/**
7235 - * AJAX: Get next item from queue to process
7236 - */
7237 -public function ajax_mxchat_get_next_queue_item() {
7238 - // Verify nonce and permissions
7239 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7240 -
7241 - if (!current_user_can('manage_options')) {
7242 - wp_send_json_error('Unauthorized access');
7243 - }
7244 -
7245 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
7246 -
7247 - if (empty($queue_id)) {
7248 - wp_send_json_error('Missing queue ID');
7249 - }
7250 -
7251 - global $wpdb;
7252 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7253 -
7254 - // Get next pending item with retry logic for failed items
7255 - $next_item = $wpdb->get_row($wpdb->prepare(
7256 - "SELECT * FROM $table_name
7257 - WHERE queue_id = %s
7258 - AND status IN ('pending', 'failed')
7259 - AND attempts < max_attempts
7260 - ORDER BY priority ASC, id ASC
7261 - LIMIT 1",
7262 - $queue_id
7263 - ));
7264 -
7265 - if (!$next_item) {
7266 - // No more items - queue complete
7267 - wp_send_json_success(array(
7268 - 'complete' => true,
7269 - 'message' => 'Queue processing complete'
7270 - ));
7271 - }
7272 -
7273 - // Mark item as processing
7274 - $wpdb->update(
7275 - $table_name,
7276 - array(
7277 - 'status' => 'processing',
7278 - 'started_at' => current_time('mysql'),
7279 - 'attempts' => $next_item->attempts + 1
7280 - ),
7281 - array('id' => $next_item->id),
7282 - array('%s', '%s', '%d'),
7283 - array('%d')
7284 - );
7285 -
7286 - wp_send_json_success(array(
7287 - 'complete' => false,
7288 - 'item' => array(
7289 - 'id' => $next_item->id,
7290 - 'type' => $next_item->item_type,
7291 - 'data' => json_decode($next_item->item_data, true),
7292 - 'bot_id' => $next_item->bot_id,
7293 - 'attempt' => $next_item->attempts + 1
7294 - )
7295 - ));
7296 -}
7297 -
7298 -/**
7299 - * AJAX: Process a single queue item
7300 - */
7301 -public function ajax_mxchat_process_queue_item() {
7302 - // Verify nonce and permissions
7303 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7304 -
7305 - if (!current_user_can('manage_options')) {
7306 - wp_send_json_error('Unauthorized access');
7307 - }
7308 -
7309 - $item_id = isset($_POST['item_id']) ? absint($_POST['item_id']) : 0;
7310 - $item_type = isset($_POST['item_type']) ? sanitize_text_field($_POST['item_type']) : '';
7311 - $item_data = isset($_POST['item_data']) ? $_POST['item_data'] : array();
7312 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
7313 -
7314 - if (empty($item_id) || empty($item_type)) {
7315 - wp_send_json_error('Missing item data');
7316 - }
7317 -
7318 - global $wpdb;
7319 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7320 -
7321 - // Process based on item type
7322 - try {
7323 - set_time_limit(60); // Give processing 60 seconds
7324 -
7325 - $result = false;
7326 - $error_message = '';
7327 -
7328 - // Read item directly from DB to get queue_id and preserve special chars in item_data
7329 - // (POST round-trip through JS mangles characters like apostrophes in URLs)
7330 - $db_item = $wpdb->get_row($wpdb->prepare(
7331 - "SELECT queue_id, item_data FROM $table_name WHERE id = %d",
7332 - $item_id
7333 - ));
7334 - $item_queue_id = $db_item ? $db_item->queue_id : '';
7335 - if ($db_item && !empty($db_item->item_data)) {
7336 - $db_data = json_decode($db_item->item_data, true);
7337 - if (is_array($db_data)) {
7338 - $item_data = $db_data;
7339 - }
7340 - }
7341 -
7342 - switch ($item_type) {
7343 - case 'url':
7344 - $result = $this->mxchat_process_queue_url($item_data, $bot_id, $item_queue_id);
7345 - break;
7346 -
7347 - case 'pdf_page':
7348 - $result = $this->mxchat_process_queue_pdf_page($item_data, $bot_id);
7349 - break;
7350 -
7351 - default:
7352 - throw new Exception('Unknown item type: ' . $item_type);
7353 - }
7354 -
7355 - if (is_wp_error($result)) {
7356 - $error_code = $result->get_error_code();
7357 - // Content errors (empty page, sanitization) are permanent — retrying won't help
7358 - $permanent_codes = array('empty_page', 'empty_after_sanitization', 'no_api_key', 'page_not_found');
7359 - if (in_array($error_code, $permanent_codes)) {
7360 - // Mark as permanently failed — set attempts = max_attempts so it won't be retried
7361 - $current_item = $wpdb->get_row($wpdb->prepare(
7362 - "SELECT max_attempts FROM $table_name WHERE id = %d", $item_id
7363 - ));
7364 - $wpdb->update(
7365 - $table_name,
7366 - array(
7367 - 'status' => 'failed',
7368 - 'error_message' => $result->get_error_message(),
7369 - 'attempts' => $current_item ? $current_item->max_attempts : 3
7370 - ),
7371 - array('id' => $item_id),
7372 - array('%s', '%s', '%d'),
7373 - array('%d')
7374 - );
7375 - wp_send_json_error(array(
7376 - 'message' => $result->get_error_message(),
7377 - 'permanent_failure' => true,
7378 - 'item_id' => $item_id
7379 - ));
7380 - return;
7381 - }
7382 - throw new Exception($result->get_error_message());
7383 - }
7384 -
7385 - if ($result === false) {
7386 - throw new Exception('Processing returned false - item may be empty or invalid');
7387 - }
7388 -
7389 - // Mark as completed
7390 - $wpdb->update(
7391 - $table_name,
7392 - array(
7393 - 'status' => 'completed',
7394 - 'completed_at' => current_time('mysql'),
7395 - 'error_message' => null
7396 - ),
7397 - array('id' => $item_id),
7398 - array('%s', '%s', '%s'),
7399 - array('%d')
7400 - );
7401 -
7402 - wp_send_json_success(array(
7403 - 'processed' => true,
7404 - 'item_id' => $item_id,
7405 - 'message' => 'Item processed successfully'
7406 - ));
7407 -
7408 - } catch (Exception $e) {
7409 - $error_message = $e->getMessage();
7410 -
7411 - // Get current attempt count
7412 - $item = $wpdb->get_row($wpdb->prepare(
7413 - "SELECT attempts, max_attempts FROM $table_name WHERE id = %d",
7414 - $item_id
7415 - ));
7416 -
7417 - // Check if we've exhausted retries
7418 - if ($item && $item->attempts >= $item->max_attempts) {
7419 - // Permanently failed
7420 - $wpdb->update(
7421 - $table_name,
7422 - array(
7423 - 'status' => 'failed',
7424 - 'error_message' => $error_message
7425 - ),
7426 - array('id' => $item_id),
7427 - array('%s', '%s'),
7428 - array('%d')
7429 - );
7430 -
7431 - wp_send_json_error(array(
7432 - 'message' => 'Item failed after maximum attempts: ' . $error_message,
7433 - 'permanent_failure' => true,
7434 - 'item_id' => $item_id
7435 - ));
7436 - } else {
7437 - // Mark for retry
7438 - $wpdb->update(
7439 - $table_name,
7440 - array(
7441 - 'status' => 'failed',
7442 - 'error_message' => $error_message
7443 - ),
7444 - array('id' => $item_id),
7445 - array('%s', '%s'),
7446 - array('%d')
7447 - );
7448 -
7449 - wp_send_json_error(array(
7450 - 'message' => 'Item processing failed, will retry: ' . $error_message,
7451 - 'can_retry' => true,
7452 - 'item_id' => $item_id,
7453 - 'attempts' => $item ? $item->attempts : 0
7454 - ));
7455 - }
7456 - }
7457 -}
7458 -
7459 -/**
7460 - * Process a URL from the queue
7461 - */
7462 -private function mxchat_process_queue_url($item_data, $bot_id = 'default', $queue_id = '') {
7463 - $url = isset($item_data['url']) ? $item_data['url'] : '';
7464 -
7465 - if (empty($url)) {
7466 - return new WP_Error('invalid_url', 'URL is empty');
7467 - }
7468 -
7469 - // Get bot-specific API key early (needed for both paths)
7470 - $bot_options = $this->get_bot_options($bot_id);
7471 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
7472 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
7473 -
7474 - if (strpos($selected_model, 'voyage') === 0) {
7475 - $api_key = $options['voyage_api_key'] ?? '';
7476 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
7477 - $api_key = $options['gemini_api_key'] ?? '';
7478 - } else {
7479 - $api_key = $options['api_key'] ?? '';
7480 - }
7481 -
7482 - if (empty($api_key)) {
7483 - return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
7484 - }
7485 -
7486 - // Check if this is a WooCommerce product URL and WooCommerce is active
7487 - $is_product_url = (strpos($url, '/product/') !== false || strpos($url, '/shop/') !== false);
7488 - $content_type = $is_product_url ? 'product' : 'url';
7489 -
7490 - // Try to get WooCommerce product data if it's a product URL
7491 - if ($is_product_url && class_exists('WooCommerce')) {
7492 - $product_content = $this->mxchat_extract_woocommerce_product_content($url);
7493 -
7494 - if (!empty($product_content)) {
7495 - // Successfully extracted WooCommerce product data with pricing
7496 - $result = MxChat_Utils::submit_content_to_db(
7497 - $product_content,
7498 - $url,
7499 - $api_key,
7500 - null,
7501 - $bot_id,
7502 - 'product'
7503 - );
7504 - return $result;
7505 - }
7506 - // If WooCommerce extraction failed, fall through to HTML extraction
7507 - }
7508 -
7509 - // Fetch URL content (fallback for non-products or when WooCommerce extraction fails)
7510 - $is_likely_pdf = (strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?: '', PATHINFO_EXTENSION)) === 'pdf');
7511 - $response = wp_remote_get($url, array(
7512 - 'timeout' => $is_likely_pdf ? 120 : 30,
7513 - 'redirection' => 5,
7514 - 'user-agent' => 'MxChat/1.0'
7515 - ));
7516 -
7517 - if (is_wp_error($response)) {
7518 - return $response;
7519 - }
7520 -
7521 - $response_code = wp_remote_retrieve_response_code($response);
7522 - if ($response_code !== 200) {
7523 - return new WP_Error('http_error', 'HTTP ' . $response_code . ' error for: ' . $url);
7524 - }
7525 -
7526 - // Check if URL is a PDF — expand into per-page queue items using the standard PDF pipeline
7527 - if ($this->mxchat_is_pdf_url($url, $response)) {
7528 - return $this->mxchat_expand_pdf_to_queue($url, $response, $bot_id, $queue_id);
7529 - }
7530 -
7531 - $html = wp_remote_retrieve_body($response);
7532 -
7533 - if (empty($html)) {
7534 - return new WP_Error('empty_response', 'Empty response body');
7535 - }
7536 -
7537 - // Extract and sanitize content
7538 - $content = $this->mxchat_extract_main_content($html);
7539 - $sanitized = $this->mxchat_sanitize_content_for_api($content);
7540 -
7541 - if (empty($sanitized)) {
7542 - // Not an error - just no content found (maybe a redirect or empty page)
7543 - return false;
7544 - }
7545 -
7546 - // Submit to database with content_type
7547 - $result = MxChat_Utils::submit_content_to_db(
7548 - $sanitized,
7549 - $url,
7550 - $api_key,
7551 - null,
7552 - $bot_id,
7553 - $content_type
7554 - );
7555 -
7556 - return $result;
7557 -}
7558 -
7559 -/**
7560 - * Expand a PDF URL into per-page queue items using the standard PDF pipeline.
7561 - * Called when a sitemap URL turns out to be a PDF — downloads, parses page count,
7562 - * and adds pdf_page items to the same queue so they process with full progress tracking.
7563 - */
7564 -private function mxchat_expand_pdf_to_queue($pdf_url, $response, $bot_id = 'default', $queue_id = '') {
7565 - set_time_limit(120); // PDFs need extra time for download + parsing
7566 -
7567 - $upload_dir = wp_upload_dir();
7568 - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf');
7569 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
7570 -
7571 - $response_body = wp_remote_retrieve_body($response);
7572 - if (empty($response_body)) {
7573 - return new WP_Error('empty_pdf', 'Empty PDF response for: ' . $pdf_url);
7574 - }
7575 -
7576 - if (!wp_mkdir_p(dirname($pdf_path))) {
7577 - return new WP_Error('dir_error', 'Failed to create upload directory');
7578 - }
7579 -
7580 - file_put_contents($pdf_path, $response_body);
7581 -
7582 - if (!file_exists($pdf_path)) {
7583 - return new WP_Error('save_error', 'Failed to save PDF file');
7584 - }
7585 -
7586 - try {
7587 - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
7588 -
7589 - if ($total_pages === false || $total_pages < 1) {
7590 - wp_delete_file($pdf_path);
7591 - return new WP_Error('no_pages', 'PDF has no pages: ' . $pdf_url);
7592 - }
7593 -
7594 - // Build per-page items identical to mxchat_handle_pdf_for_knowledge_base
7595 - $pages = array();
7596 - for ($i = 1; $i <= $total_pages; $i++) {
7597 - $pages[] = array(
7598 - 'pdf_path' => $pdf_path,
7599 - 'pdf_url' => $pdf_url,
7600 - 'page_number' => $i,
7601 - 'total_pages' => $total_pages
7602 - );
7603 - }
7604 -
7605 - // Add pdf_page items to the SAME queue so the JS picks them up automatically
7606 - if (!empty($queue_id)) {
7607 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
7608 - } else {
7609 - // Fallback: create a new PDF queue (shouldn't happen in sitemap flow)
7610 - $new_queue_id = 'pdf_' . md5($pdf_url . time());
7611 - $queued_count = $this->mxchat_add_to_queue($new_queue_id, 'pdf_page', $pages, $bot_id);
7612 - $this->mxchat_set_queue_meta($new_queue_id, 'source_url', $pdf_url);
7613 - $this->mxchat_set_queue_meta($new_queue_id, 'queue_type', 'pdf');
7614 - $this->mxchat_set_queue_meta($new_queue_id, 'total_items', $total_pages);
7615 - $this->mxchat_set_queue_meta($new_queue_id, 'bot_id', $bot_id);
7616 - $this->mxchat_set_queue_meta($new_queue_id, 'pdf_path', $pdf_path);
7617 - $this->mxchat_set_queue_meta($new_queue_id, 'created_at', current_time('mysql'));
7618 - }
7619 -
7620 - if ($queued_count === 0) {
7621 - wp_delete_file($pdf_path);
7622 - return new WP_Error('queue_error', 'Failed to add PDF pages to queue');
7623 - }
7624 -
7625 - // Return true so the original URL item is marked complete
7626 - // The new pdf_page items will be processed in subsequent batches
7627 - return true;
7628 -
7629 - } catch (Exception $e) {
7630 - if (file_exists($pdf_path)) {
7631 - wp_delete_file($pdf_path);
7632 - }
7633 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
7634 - }
7635 -}
7636 -
7637 -/**
7638 - * Legacy: Process a PDF URL inline during sitemap queue processing.
7639 - * @deprecated Use mxchat_expand_pdf_to_queue instead — kept for reference only.
7640 - */
7641 -private function mxchat_process_pdf_url_inline($pdf_url, $response, $api_key, $bot_id = 'default') {
7642 - set_time_limit(120); // PDFs need more time — downloading + parsing all pages
7643 -
7644 - $upload_dir = wp_upload_dir();
7645 - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf');
7646 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
7647 -
7648 - $response_body = wp_remote_retrieve_body($response);
7649 - if (empty($response_body)) {
7650 - return new WP_Error('empty_pdf', 'Empty PDF response');
7651 - }
7652 -
7653 - if (!wp_mkdir_p(dirname($pdf_path))) {
7654 - return new WP_Error('dir_error', 'Failed to create upload directory');
7655 - }
7656 -
7657 - file_put_contents($pdf_path, $response_body);
7658 -
7659 - if (!file_exists($pdf_path)) {
7660 - return new WP_Error('save_error', 'Failed to save PDF file');
7661 - }
7662 -
7663 - try {
7664 - mxchat_load_pdf_parser();
7665 - $parser = new \Smalot\PdfParser\Parser();
7666 - $pdf = $parser->parseFile($pdf_path);
7667 - $pages = $pdf->getPages();
7668 - $total_pages = count($pages);
7669 -
7670 - if ($total_pages < 1) {
7671 - wp_delete_file($pdf_path);
7672 - return new WP_Error('no_pages', 'PDF has no pages');
7673 - }
7674 -
7675 - $processed = 0;
7676 - $skipped_pages = array();
7677 -
7678 - for ($i = 0; $i < $total_pages; $i++) {
7679 - $page_num = $i + 1;
7680 - $text = $pages[$i]->getText();
7681 - if (empty($text)) {
7682 - $skipped_pages[] = 'Page ' . $page_num . ': No text could be extracted — page may contain only images, links, or non-standard encoding';
7683 - continue;
7684 - }
7685 -
7686 - $sanitized = $this->mxchat_sanitize_content_for_api($text);
7687 - if (empty($sanitized)) {
7688 - $skipped_pages[] = 'Page ' . $page_num . ': Text was extracted but contained only special characters, control codes, or unsupported content';
7689 - continue;
7690 - }
7691 -
7692 - $metadata = array(
7693 - 'document_type' => 'pdf',
7694 - 'total_pages' => $total_pages,
7695 - 'current_page' => $page_num,
7696 - 'source_url' => $pdf_url,
7697 - );
7698 -
7699 - $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized;
7700 - $page_url = esc_url($pdf_url . '#page=' . $page_num);
7701 -
7702 - MxChat_Utils::submit_content_to_db(
7703 - $content_with_metadata,
7704 - $page_url,
7705 - $api_key,
7706 - null,
7707 - $bot_id,
7708 - 'pdf'
7709 - );
7710 -
7711 - $processed++;
7712 - }
7713 -
7714 - // Clean up the temp PDF file
7715 - wp_delete_file($pdf_path);
7716 -
7717 - if (!empty($skipped_pages)) {
7718 - error_log('MxChat PDF: Skipped ' . count($skipped_pages) . ' of ' . $total_pages . ' pages: ' . implode('; ', $skipped_pages));
7719 - }
7720 -
7721 - return $processed > 0 ? true : false;
7722 -
7723 - } catch (Exception $e) {
7724 - if (file_exists($pdf_path)) {
7725 - wp_delete_file($pdf_path);
7726 - }
7727 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
7728 - }
7729 -}
7730 -
7731 -/**
7732 - * Extract WooCommerce product content including pricing
7733 - *
7734 - * @param string $url The product URL
7735 - * @return string|false Product content with pricing, or false if not found
7736 - */
7737 -private function mxchat_extract_woocommerce_product_content($url) {
7738 - // Try to get product ID from URL
7739 - $product_id = url_to_postid($url);
7740 -
7741 - // If url_to_postid fails, try to extract from URL pattern
7742 - if (!$product_id) {
7743 - $product_slug = '';
7744 -
7745 - // Handle pretty permalinks: /product/product-name/
7746 - if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
7747 - $product_slug = $matches[1];
7748 - }
7749 -
7750 - if (!empty($product_slug)) {
7751 - $product_post = get_page_by_path($product_slug, OBJECT, 'product');
7752 - if ($product_post) {
7753 - $product_id = $product_post->ID;
7754 - }
7755 - }
7756 - }
7757 -
7758 - if (!$product_id) {
7759 - return false;
7760 - }
7761 -
7762 - // Get WooCommerce product object
7763 - $product = wc_get_product($product_id);
7764 -
7765 - if (!$product) {
7766 - return false;
7767 - }
7768 -
7769 - // Build product content with pricing (similar to mxchat_store_product_embedding)
7770 - $title = $product->get_name();
7771 - $description = $product->get_description();
7772 - $short_description = $product->get_short_description();
7773 - $sku = $product->get_sku();
7774 -
7775 - // Get pricing information
7776 - $regular_price = $product->get_regular_price();
7777 - $sale_price = $product->get_sale_price();
7778 - $price = $product->get_price(); // Current active price
7779 -
7780 - // Get currency symbol
7781 - $currency_symbol = get_woocommerce_currency_symbol();
7782 -
7783 - // Format content
7784 - $content = $title . "\n\n";
7785 -
7786 - if (!empty($short_description)) {
7787 - $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
7788 - }
7789 -
7790 - if (!empty($description)) {
7791 - $content .= wp_strip_all_tags($description) . "\n\n";
7792 - }
7793 -
7794 - // Add pricing information
7795 - if (!empty($regular_price)) {
7796 - $content .= "Price: " . $currency_symbol . $regular_price . "\n";
7797 - } elseif (!empty($price)) {
7798 - $content .= "Price: " . $currency_symbol . $price . "\n";
7799 - }
7800 -
7801 - if (!empty($sale_price) && $sale_price !== $regular_price) {
7802 - $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
7803 - }
7804 -
7805 - // Handle variable products - show price range
7806 - if ($product->is_type('variable')) {
7807 - $min_price = $product->get_variation_price('min');
7808 - $max_price = $product->get_variation_price('max');
7809 - if ($min_price !== $max_price) {
7810 - $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
7811 - }
7812 - }
7813 -
7814 - if (!empty($sku)) {
7815 - $content .= "SKU: " . $sku . "\n";
7816 - }
7817 -
7818 - // Get product categories
7819 - $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
7820 - if (!empty($categories) && !is_wp_error($categories)) {
7821 - $content .= "Categories: " . implode(', ', $categories) . "\n";
7822 - }
7823 -
7824 - // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
7825 - $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
7826 - if (!empty($custom_tabs) && is_array($custom_tabs)) {
7827 - foreach ($custom_tabs as $tab) {
7828 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
7829 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
7830 -
7831 - if (!empty($tab_title) && !empty($tab_content)) {
7832 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
7833 - }
7834 - }
7835 - }
7836 -
7837 - // Also check for reusable/saved tabs applied to this product
7838 - $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
7839 - if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
7840 - $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
7841 - if (!empty($saved_tabs) && is_array($saved_tabs)) {
7842 - foreach ($applied_saved_tabs as $saved_tab_id) {
7843 - if (isset($saved_tabs[$saved_tab_id])) {
7844 - $tab = $saved_tabs[$saved_tab_id];
7845 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
7846 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
7847 -
7848 - if (!empty($tab_title) && !empty($tab_content)) {
7849 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
7850 - }
7851 - }
7852 - }
7853 - }
7854 - }
7855 -
7856 - return $this->mxchat_sanitize_content_for_api($content);
7857 -}
7858 -
7859 -/**
7860 - * Process a PDF page from the queue
7861 - */
7862 -private function mxchat_process_queue_pdf_page($item_data, $bot_id = 'default') {
7863 - $pdf_path = isset($item_data['pdf_path']) ? $item_data['pdf_path'] : '';
7864 - $pdf_url = isset($item_data['pdf_url']) ? $item_data['pdf_url'] : '';
7865 - $page_number = isset($item_data['page_number']) ? absint($item_data['page_number']) : 0;
7866 - $total_pages = isset($item_data['total_pages']) ? absint($item_data['total_pages']) : 0;
7867 -
7868 - if (empty($pdf_path) || !file_exists($pdf_path)) {
7869 - return new WP_Error('pdf_not_found', 'PDF file not found: ' . $pdf_path);
7870 - }
7871 -
7872 - if ($page_number < 1) {
7873 - return new WP_Error('invalid_page', 'Invalid page number');
7874 - }
7875 -
7876 - try {
7877 - mxchat_load_pdf_parser();
7878 - $parser = new \Smalot\PdfParser\Parser();
7879 - $pdf = $parser->parseFile($pdf_path);
7880 - $pages = $pdf->getPages();
7881 -
7882 - if (!isset($pages[$page_number - 1])) {
7883 - return new WP_Error('page_not_found', 'Page ' . $page_number . ' not found in PDF');
7884 - }
7885 -
7886 - $text = $pages[$page_number - 1]->getText();
7887 -
7888 - if (empty($text)) {
7889 - return new WP_Error('empty_page', 'Page ' . $page_number . ': No text could be extracted — page may contain only images, links, or non-standard encoding');
7890 - }
7891 -
7892 - $sanitized = $this->mxchat_sanitize_content_for_api($text);
7893 -
7894 - if (empty($sanitized)) {
7895 - 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');
7896 - }
7897 -
7898 - // Create metadata
7899 - $metadata = array(
7900 - 'document_type' => 'pdf',
7901 - 'total_pages' => $total_pages,
7902 - 'current_page' => $page_number,
7903 - 'source_url' => $pdf_url
7904 - );
7905 -
7906 - $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized;
7907 - $page_url = esc_url($pdf_url . "#page=" . $page_number);
7908 -
7909 - // Get bot-specific API key
7910 - $bot_options = $this->get_bot_options($bot_id);
7911 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
7912 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
7913 -
7914 - if (strpos($selected_model, 'voyage') === 0) {
7915 - $api_key = $options['voyage_api_key'] ?? '';
7916 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
7917 - $api_key = $options['gemini_api_key'] ?? '';
7918 - } else {
7919 - $api_key = $options['api_key'] ?? '';
7920 - }
7921 -
7922 - if (empty($api_key)) {
7923 - return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
7924 - }
7925 -
7926 - // Submit to database - UPDATED 2.5.6: Added content_type 'pdf'
7927 - $result = MxChat_Utils::submit_content_to_db(
7928 - $content_with_metadata,
7929 - $page_url,
7930 - $api_key,
7931 - null,
7932 - $bot_id,
7933 - 'pdf'
7934 - );
7935 -
7936 - return $result;
7937 -
7938 - } catch (Exception $e) {
7939 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
7940 - }
7941 -}
7942 -
7943 -/**
7944 - * AJAX: Get queue processing status
7945 - */
7946 -public function ajax_mxchat_get_queue_status() {
7947 - // Verify nonce and permissions
7948 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7949 -
7950 - if (!current_user_can('manage_options')) {
7951 - wp_send_json_error('Unauthorized access');
7952 - }
7953 -
7954 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
7955 -
7956 - if (empty($queue_id)) {
7957 - wp_send_json_error('Missing queue ID');
7958 - }
7959 -
7960 - global $wpdb;
7961 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7962 -
7963 - // Get counts by status
7964 - $counts = $wpdb->get_results($wpdb->prepare(
7965 - "SELECT status, COUNT(*) as count
7966 - FROM $table_name
7967 - WHERE queue_id = %s
7968 - GROUP BY status",
7969 - $queue_id
7970 - ), OBJECT_K);
7971 -
7972 - $total = 0;
7973 - $completed = 0;
7974 - $failed = 0;
7975 - $processing = 0;
7976 - $pending = 0;
7977 -
7978 - foreach ($counts as $status => $data) {
7979 - $count = absint($data->count);
7980 - $total += $count;
7981 -
7982 - switch ($status) {
7983 - case 'completed':
7984 - $completed = $count;
7985 - break;
7986 - case 'failed':
7987 - $failed = $count;
7988 - break;
7989 - case 'processing':
7990 - $processing = $count;
7991 - break;
7992 - case 'pending':
7993 - $pending = $count;
7994 - break;
7995 - }
7996 - }
7997 -
7998 - // Calculate percentage
7999 - $percentage = $total > 0 ? round((($completed + $failed) / $total) * 100) : 0;
8000 -
8001 - // Get failed items details (include all failed items, not just those that exhausted retries)
8002 - $failed_items = array();
8003 - if ($failed > 0) {
8004 - $failed_items = $wpdb->get_results($wpdb->prepare(
8005 - "SELECT item_type, item_data, error_message, attempts
8006 - FROM $table_name
8007 - WHERE queue_id = %s
8008 - AND status = 'failed'
8009 - ORDER BY id DESC
8010 - LIMIT 50",
8011 - $queue_id
8012 - ));
8013 - }
8014 -
8015 - // Get queue metadata
8016 - $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
8017 - $queue_type = $this->mxchat_get_queue_meta($queue_id, 'queue_type');
8018 -
8019 - // Determine if queue is complete
8020 - $is_complete = ($pending === 0 && $processing === 0);
8021 -
8022 - wp_send_json_success(array(
8023 - 'queue_id' => $queue_id,
8024 - 'queue_type' => $queue_type,
8025 - 'source_url' => $source_url,
8026 - 'total' => $total,
8027 - 'completed' => $completed,
8028 - 'failed' => $failed,
8029 - 'processing' => $processing,
8030 - 'pending' => $pending,
8031 - 'percentage' => $percentage,
8032 - 'is_complete' => $is_complete,
8033 - 'failed_items' => $failed_items,
8034 - 'status' => $is_complete ? 'complete' : 'processing'
8035 - ));
8036 -}
8037 -
8038 -/**
8039 - * AJAX: Clear completed queue
8040 - */
8041 -public function ajax_mxchat_clear_queue() {
8042 - // Verify nonce and permissions
8043 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
8044 -
8045 - if (!current_user_can('manage_options')) {
8046 - wp_send_json_error('Unauthorized access');
8047 - }
8048 -
8049 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
8050 -
8051 - if (empty($queue_id)) {
8052 - wp_send_json_error('Missing queue ID');
8053 - }
8054 -
8055 - global $wpdb;
8056 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
8057 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
8058 -
8059 - // Delete queue items
8060 - $wpdb->delete(
8061 - $table_name,
8062 - array('queue_id' => $queue_id),
8063 - array('%s')
8064 - );
8065 -
8066 - // Delete queue metadata
8067 - $wpdb->delete(
8068 - $meta_table,
8069 - array('queue_id' => $queue_id),
8070 - array('%s')
8071 - );
8072 -
8073 - wp_send_json_success(array(
8074 - 'message' => 'Queue cleared successfully'
8075 - ));
8076 -}
8077 -
8078 -/**
8079 - * AJAX: Retry failed items in queue
8080 - */
8081 -public function ajax_mxchat_retry_failed() {
8082 - // Verify nonce and permissions
8083 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
8084 -
8085 - if (!current_user_can('manage_options')) {
8086 - wp_send_json_error('Unauthorized access');
8087 - }
8088 -
8089 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
8090 -
8091 - if (empty($queue_id)) {
8092 - wp_send_json_error('Missing queue ID');
8093 - }
8094 -
8095 - global $wpdb;
8096 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
8097 -
8098 - // Reset failed items to pending and reset attempt count
8099 - $updated = $wpdb->update(
8100 - $table_name,
8101 - array(
8102 - 'status' => 'pending',
8103 - 'attempts' => 0,
8104 - 'error_message' => null
8105 - ),
8106 - array(
8107 - 'queue_id' => $queue_id,
8108 - 'status' => 'failed'
8109 - ),
8110 - array('%s', '%d', '%s'),
8111 - array('%s', '%s')
8112 - );
8113 -
8114 - wp_send_json_success(array(
8115 - 'message' => 'Reset ' . $updated . ' failed items for retry',
8116 - 'reset_count' => $updated
8117 - ));
8118 -}
8119 -
8120 -
8121 -public function ajax_mxchat_mark_queue_complete() {
8122 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
8123 -
8124 - if (!current_user_can('manage_options')) {
8125 - wp_send_json_error('Unauthorized access');
8126 - }
8127 -
8128 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
8129 -
8130 - if (empty($queue_id)) {
8131 - wp_send_json_error('Missing queue ID');
8132 - }
8133 -
8134 - // Clear active queue transients
8135 - if (strpos($queue_id, 'sitemap_') === 0) {
8136 - delete_transient('mxchat_active_queue_sitemap');
8137 - } else if (strpos($queue_id, 'pdf_') === 0) {
8138 - delete_transient('mxchat_active_queue_pdf');
8139 - }
8140 -
8141 - wp_send_json_success(array('message' => 'Queue marked as complete'));
8142 -}
8143 -
8144 -
8145 - // ========================================
8146 - // STATIC ACCESS METHODS
8147 - // ========================================
8148 -
8149 - /**
8150 - * Get singleton instance
8151 - */
8152 - public static function get_instance() {
8153 - static $instance = null;
8154 - if ($instance === null) {
8155 - $instance = new self();
8156 - }
8157 - return $instance;
8158 - }
8159 -}
8160 -
8161 -// 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_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 +
61 + // Hook for content deletion
62 + add_action('mxchat_delete_content', array($this, 'mxchat_delete_from_pinecone_by_url'), 10, 1);
63 +
64 + // WordPress post management hooks
65 + add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
66 + add_action('post_updated', array($this, 'mxchat_handle_post_update'), 10, 3);
67 + add_action('before_delete_post', array($this, 'mxchat_handle_post_delete'));
68 + add_action('wp_trash_post', array($this, 'mxchat_handle_post_delete'));
69 +
70 + // ACF hook - fires AFTER ACF fields are saved, ensuring ACF data is available
71 + // Priority 20 to run after ACF's own save (which runs at priority 10)
72 + add_action('acf/save_post', array($this, 'mxchat_handle_acf_save'), 20);
73 +
74 + add_action('wp_ajax_mxchat_mark_queue_complete', array($this, 'ajax_mxchat_mark_queue_complete'));
75 +
76 + // WooCommerce product hooks (if WooCommerce is active)
77 + if (class_exists('WooCommerce')) {
78 + add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
79 + add_action('save_post_product', array($this, 'mxchat_handle_product_change'), 10, 3);
80 + add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
81 + add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
82 + }
83 +}
84 +
85 + /**
86 + * Get current options (refreshed)
87 + */
88 + private function mxchat_get_options() {
89 + if (empty($this->options)) {
90 + $this->options = get_option('mxchat_options', array());
91 + }
92 + return $this->options;
93 + }
94 +
95 +
96 + // ========================================
97 + // MAIN CONTENT SUBMISSION HANDLERS
98 + // ========================================
99 +
100 +public function mxchat_handle_content_submission() {
101 + // Check if the form was submitted and the user has permission.
102 + if (!isset($_POST['submit_content']) || !current_user_can('manage_options')) {
103 + return;
104 + }
105 +
106 + // Verify the nonce.
107 + $nonce = isset($_POST['mxchat_submit_content_nonce']) ? sanitize_text_field(wp_unslash($_POST['mxchat_submit_content_nonce'])) : '';
108 + if (!wp_verify_nonce($nonce, 'mxchat_submit_content_action')) {
109 + wp_die(esc_html__('Nonce verification failed.', 'mxchat'));
110 + }
111 +
112 + // Sanitize the inputs.
113 + // Use wp_kses_post to allow safe HTML and wp_unslash to remove WordPress added slashes
114 + $article_content = wp_kses_post(wp_unslash($_POST['article_content']));
115 + $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
116 +
117 + // Get bot_id from form submission
118 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
119 +
120 + // Get bot-specific options and API key
121 + $bot_options = $this->get_bot_options($bot_id);
122 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
123 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
124 +
125 + if (strpos($selected_model, 'voyage') === 0) {
126 + $api_key = $options['voyage_api_key'] ?? '';
127 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
128 + $api_key = $options['gemini_api_key'] ?? '';
129 + } else {
130 + $api_key = $options['api_key'] ?? '';
131 + }
132 +
133 + if (empty($api_key)) {
134 + set_transient('mxchat_admin_notice_error',
135 + esc_html__('API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
136 + 30
137 + );
138 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
139 + exit;
140 + }
141 +
142 + // Use centralized utility function with bot_id
143 + $result = MxChat_Utils::submit_content_to_db($article_content, $article_url, $api_key, null, $bot_id);
144 +
145 + if (is_wp_error($result)) {
146 + set_transient('mxchat_admin_notice_error',
147 + esc_html__('Error storing content: ', 'mxchat') . $result->get_error_message(),
148 + 30
149 + );
150 + } else {
151 + set_transient('mxchat_admin_notice_success',
152 + esc_html__('Content successfully submitted!', 'mxchat'),
153 + 30
154 + );
155 + }
156 +
157 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
158 + exit;
159 +}
160 +
161 +public function mxchat_is_pdf_url($url, $response) {
162 + $content_type = wp_remote_retrieve_header($response, 'content-type');
163 + $file_extension = strtolower(pathinfo($url, PATHINFO_EXTENSION));
164 +
165 + return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf';
166 +}
167 +
168 +
169 +public function mxchat_handle_pdf_for_knowledge_base($pdf_url, $response, $bot_id = 'default') {
170 + if (!current_user_can('manage_options')) {
171 + return false;
172 + }
173 +
174 + $pdf_url = esc_url_raw($pdf_url);
175 + $upload_dir = wp_upload_dir();
176 +
177 + if (isset($upload_dir['error']) && $upload_dir['error'] !== false) {
178 + return false;
179 + }
180 +
181 + $pdf_filename = sanitize_file_name('mxchat_kb_' . time() . '.pdf');
182 + $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
183 +
184 + $response_body = wp_remote_retrieve_body($response);
185 + if (empty($response_body)) {
186 + return false;
187 + }
188 +
189 + if (!wp_mkdir_p(dirname($pdf_path))) {
190 + return false;
191 + }
192 +
193 + try {
194 + file_put_contents($pdf_path, $response_body);
195 +
196 + if (!file_exists($pdf_path)) {
197 + throw new Exception(__('Failed to save PDF file', 'mxchat'));
198 + }
199 +
200 + $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
201 +
202 + if ($total_pages === false || $total_pages < 1) {
203 + throw new Exception(__('Invalid PDF: Unable to parse or no pages found', 'mxchat'));
204 + }
205 +
206 + // Create unique queue ID
207 + $queue_id = 'pdf_' . md5($pdf_url . time());
208 +
209 + // Create array of pages to process
210 + $pages = array();
211 + for ($i = 1; $i <= $total_pages; $i++) {
212 + $pages[] = array(
213 + 'pdf_path' => $pdf_path,
214 + 'pdf_url' => $pdf_url,
215 + 'page_number' => $i,
216 + 'total_pages' => $total_pages
217 + );
218 + }
219 +
220 + // Add pages to queue
221 + $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
222 +
223 + if ($queued_count === 0) {
224 + wp_delete_file($pdf_path);
225 + throw new Exception(__('Failed to add PDF pages to processing queue', 'mxchat'));
226 + }
227 +
228 + // Store queue metadata
229 + $this->mxchat_set_queue_meta($queue_id, 'source_url', $pdf_url);
230 + $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'pdf');
231 + $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_pages);
232 + $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
233 + $this->mxchat_set_queue_meta($queue_id, 'pdf_path', $pdf_path);
234 + $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
235 +
236 + // Store queue ID in transient for status tracking
237 + set_transient('mxchat_active_queue_pdf', $queue_id, DAY_IN_SECONDS);
238 + set_transient('mxchat_last_pdf_url', $pdf_url, DAY_IN_SECONDS);
239 +
240 + return 'queued';
241 +
242 + } catch (Exception $e) {
243 + if (file_exists($pdf_path)) {
244 + wp_delete_file($pdf_path);
245 + }
246 + return $e->getMessage();
247 + }
248 +}
249 +
250 +/**
251 + * Handle direct PDF file upload from the knowledge base page
252 + */
253 +public function mxchat_handle_pdf_file_submission() {
254 + if (!isset($_POST['submit_pdf_file']) || !current_user_can('manage_options')) {
255 + wp_die(esc_html__('Unauthorized access', 'mxchat'));
256 + }
257 +
258 + check_admin_referer('mxchat_submit_pdf_file_action', 'mxchat_submit_pdf_file_nonce');
259 +
260 + $redirect_url = admin_url('admin.php?page=mxchat-prompts');
261 +
262 + // Validate file upload
263 + if (empty($_FILES['pdf_file']) || $_FILES['pdf_file']['error'] !== UPLOAD_ERR_OK) {
264 + $error_code = isset($_FILES['pdf_file']['error']) ? $_FILES['pdf_file']['error'] : UPLOAD_ERR_NO_FILE;
265 + $error_messages = array(
266 + UPLOAD_ERR_INI_SIZE => __('The uploaded file exceeds the server upload_max_filesize limit.', 'mxchat'),
267 + UPLOAD_ERR_FORM_SIZE => __('The uploaded file exceeds the form MAX_FILE_SIZE limit.', 'mxchat'),
268 + UPLOAD_ERR_PARTIAL => __('The file was only partially uploaded.', 'mxchat'),
269 + UPLOAD_ERR_NO_FILE => __('No file was uploaded. Please select a PDF file.', 'mxchat'),
270 + UPLOAD_ERR_NO_TMP_DIR => __('Server missing temporary folder.', 'mxchat'),
271 + UPLOAD_ERR_CANT_WRITE => __('Server failed to write file to disk.', 'mxchat'),
272 + );
273 + $error_msg = isset($error_messages[$error_code]) ? $error_messages[$error_code] : __('Unknown upload error.', 'mxchat');
274 + set_transient('mxchat_admin_notice_error', $error_msg, 30);
275 + wp_safe_redirect(esc_url($redirect_url));
276 + exit;
277 + }
278 +
279 + $file = $_FILES['pdf_file'];
280 +
281 + // Validate MIME type
282 + $finfo = finfo_open(FILEINFO_MIME_TYPE);
283 + $mime_type = finfo_file($finfo, $file['tmp_name']);
284 + finfo_close($finfo);
285 +
286 + if ($mime_type !== 'application/pdf') {
287 + set_transient('mxchat_admin_notice_error',
288 + esc_html__('Invalid file type. Only PDF files are accepted.', 'mxchat'),
289 + 30
290 + );
291 + wp_safe_redirect(esc_url($redirect_url));
292 + exit;
293 + }
294 +
295 + // Validate extension
296 + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
297 + if ($ext !== 'pdf') {
298 + set_transient('mxchat_admin_notice_error',
299 + esc_html__('Invalid file extension. Only .pdf files are accepted.', 'mxchat'),
300 + 30
301 + );
302 + wp_safe_redirect(esc_url($redirect_url));
303 + exit;
304 + }
305 +
306 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
307 + $original_filename = sanitize_file_name($file['name']);
308 +
309 + $upload_dir = wp_upload_dir();
310 + if (isset($upload_dir['error']) && $upload_dir['error'] !== false) {
311 + set_transient('mxchat_admin_notice_error',
312 + esc_html__('WordPress upload directory is not writable.', 'mxchat'),
313 + 30
314 + );
315 + wp_safe_redirect(esc_url($redirect_url));
316 + exit;
317 + }
318 +
319 + $pdf_filename = sanitize_file_name('mxchat_kb_' . time() . '.pdf');
320 + $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
321 +
322 + if (!wp_mkdir_p(dirname($pdf_path))) {
323 + set_transient('mxchat_admin_notice_error',
324 + esc_html__('Failed to create upload directory.', 'mxchat'),
325 + 30
326 + );
327 + wp_safe_redirect(esc_url($redirect_url));
328 + exit;
329 + }
330 +
331 + // Move uploaded file
332 + if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
333 + set_transient('mxchat_admin_notice_error',
334 + esc_html__('Failed to save uploaded PDF file.', 'mxchat'),
335 + 30
336 + );
337 + wp_safe_redirect(esc_url($redirect_url));
338 + exit;
339 + }
340 +
341 + try {
342 + $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
343 +
344 + if ($total_pages === false || $total_pages < 1) {
345 + throw new Exception(__('Invalid PDF: Unable to parse or no pages found', 'mxchat'));
346 + }
347 +
348 + // Use original filename as the source identifier
349 + $source_label = 'upload://' . $original_filename;
350 +
351 + $queue_id = 'pdf_' . md5($source_label . time());
352 +
353 + $pages = array();
354 + for ($i = 1; $i <= $total_pages; $i++) {
355 + $pages[] = array(
356 + 'pdf_path' => $pdf_path,
357 + 'pdf_url' => $source_label,
358 + 'page_number' => $i,
359 + 'total_pages' => $total_pages,
360 + );
361 + }
362 +
363 + $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
364 +
365 + if ($queued_count === 0) {
366 + wp_delete_file($pdf_path);
367 + throw new Exception(__('Failed to add PDF pages to processing queue', 'mxchat'));
368 + }
369 +
370 + $this->mxchat_set_queue_meta($queue_id, 'source_url', $source_label);
371 + $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'pdf');
372 + $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_pages);
373 + $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
374 + $this->mxchat_set_queue_meta($queue_id, 'pdf_path', $pdf_path);
375 + $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
376 +
377 + set_transient('mxchat_active_queue_pdf', $queue_id, DAY_IN_SECONDS);
378 + set_transient('mxchat_last_pdf_url', $source_label, DAY_IN_SECONDS);
379 +
380 + set_transient('mxchat_admin_notice_success',
381 + sprintf(
382 + esc_html__('PDF "%s" (%d pages) queued for processing. Processing will start automatically.', 'mxchat'),
383 + esc_html($original_filename),
384 + $total_pages
385 + ),
386 + 30
387 + );
388 +
389 + } catch (Exception $e) {
390 + if (file_exists($pdf_path)) {
391 + wp_delete_file($pdf_path);
392 + }
393 + set_transient('mxchat_admin_notice_error',
394 + esc_html__('Failed to process uploaded PDF: ', 'mxchat') . esc_html($e->getMessage()),
395 + 30
396 + );
397 + }
398 +
399 + wp_safe_redirect(esc_url($redirect_url));
400 + exit;
401 +}
402 +
403 +/**
404 + * Validate PDF and count pages with multiple parser attempts
405 + */
406 +private function mxchat_validate_and_count_pdf_pages($pdf_path) {
407 + // Method 1: Try with Smalot PDF Parser (your current method)
408 + try {
409 + mxchat_load_pdf_parser();
410 + $parser = new \Smalot\PdfParser\Parser();
411 + $pdf = $parser->parseFile($pdf_path);
412 + $pages = $pdf->getPages();
413 + $page_count = count($pages);
414 +
415 + if ($page_count > 0) {
416 + //error_log('PDF parsed successfully with Smalot parser: ' . $page_count . ' pages');
417 + return $page_count;
418 + }
419 + } catch (Exception $e) {
420 + //error_log('Smalot PDF parser failed: ' . $e->getMessage());
421 + }
422 +
423 + // Method 2: Try with pdfinfo command (if available)
424 + if (function_exists('shell_exec') && !$this->mxchat_is_shell_disabled()) {
425 + try {
426 + $command = 'pdfinfo ' . escapeshellarg($pdf_path) . ' 2>&1';
427 + $output = shell_exec($command);
428 +
429 + if ($output && preg_match('/Pages:\s*(\d+)/', $output, $matches)) {
430 + $page_count = intval($matches[1]);
431 + if ($page_count > 0) {
432 + //error_log('PDF parsed successfully with pdfinfo: ' . $page_count . ' pages');
433 + return $page_count;
434 + }
435 + }
436 + } catch (Exception $e) {
437 + //error_log('pdfinfo command failed: ' . $e->getMessage());
438 + }
439 + }
440 +
441 + // Method 3: Try to repair PDF and parse again
442 + try {
443 + $repaired_path = $this->mxchat_attempt_pdf_repair($pdf_path);
444 + if ($repaired_path && $repaired_path !== $pdf_path) {
445 + mxchat_load_pdf_parser();
446 + $parser = new \Smalot\PdfParser\Parser();
447 + $pdf = $parser->parseFile($repaired_path);
448 + $pages = $pdf->getPages();
449 + $page_count = count($pages);
450 +
451 + if ($page_count > 0) {
452 + // Replace original with repaired version
453 + copy($repaired_path, $pdf_path);
454 + unlink($repaired_path);
455 + //error_log('PDF repaired and parsed successfully: ' . $page_count . ' pages');
456 + return $page_count;
457 + }
458 +
459 + // Clean up repaired file if it didn't work
460 + unlink($repaired_path);
461 + }
462 + } catch (Exception $e) {
463 + //error_log('PDF repair attempt failed: ' . $e->getMessage());
464 + }
465 +
466 + // Method 4: Manual PDF structure analysis (basic page count)
467 + try {
468 + $page_count = $this->mxchat_manual_pdf_page_count($pdf_path);
469 + if ($page_count > 0) {
470 + //error_log('PDF page count determined manually: ' . $page_count . ' pages');
471 + return $page_count;
472 + }
473 + } catch (Exception $e) {
474 + //error_log('Manual PDF analysis failed: ' . $e->getMessage());
475 + }
476 +
477 + //error_log('All PDF parsing methods failed for: ' . $pdf_path);
478 + return false;
479 +}
480 +
481 +/**
482 + * Check if shell_exec is disabled
483 + */
484 +private function mxchat_is_shell_disabled() {
485 + $disabled = explode(',', ini_get('disable_functions'));
486 + return in_array('shell_exec', $disabled);
487 +}
488 +
489 +/**
490 + * Attempt to repair PDF using basic methods
491 + */
492 +private function mxchat_attempt_pdf_repair($pdf_path) {
493 + try {
494 + $content = file_get_contents($pdf_path);
495 + if (!$content) {
496 + return false;
497 + }
498 +
499 + // Check if PDF starts with proper header
500 + if (substr($content, 0, 4) !== '%PDF') {
501 + // Try to find PDF header in the content
502 + $header_pos = strpos($content, '%PDF');
503 + if ($header_pos !== false && $header_pos < 1024) {
504 + // Remove junk before PDF header
505 + $content = substr($content, $header_pos);
506 + $repaired_path = $pdf_path . '.repaired';
507 + file_put_contents($repaired_path, $content);
508 + return $repaired_path;
509 + }
510 + }
511 +
512 + // Check for EOF marker
513 + $content = rtrim($content);
514 + if (!preg_match('/%%EOF\s*$/', $content)) {
515 + // Add EOF marker if missing
516 + $content .= "\n%%EOF";
517 + $repaired_path = $pdf_path . '.repaired';
518 + file_put_contents($repaired_path, $content);
519 + return $repaired_path;
520 + }
521 +
522 + } catch (Exception $e) {
523 + //error_log('PDF repair error: ' . $e->getMessage());
524 + }
525 +
526 + return false;
527 +}
528 +
529 +/**
530 + * Manual PDF page counting by analyzing PDF structure
531 + */
532 +private function mxchat_manual_pdf_page_count($pdf_path) {
533 + try {
534 + $content = file_get_contents($pdf_path);
535 + if (!$content) {
536 + return 0;
537 + }
538 +
539 + // Method 1: Count /Type /Page objects
540 + $page_count = preg_match_all('/\/Type\s*\/Page[^s]/', $content);
541 + if ($page_count > 0) {
542 + return $page_count;
543 + }
544 +
545 + // Method 2: Look for /Count in pages object
546 + if (preg_match('/\/Type\s*\/Pages.*?\/Count\s+(\d+)/', $content, $matches)) {
547 + return intval($matches[1]);
548 + }
549 +
550 + // Method 3: Count page references
551 + $page_count = preg_match_all('/\d+\s+0\s+obj\s*<<[^>]*\/Type\s*\/Page/', $content);
552 + if ($page_count > 0) {
553 + return $page_count;
554 + }
555 +
556 + } catch (Exception $e) {
557 + //error_log('Manual PDF analysis error: ' . $e->getMessage());
558 + }
559 +
560 + return 0;
561 +}
562 +
563 +
564 +public function mxchat_save_inline_prompt() {
565 + // DEBUG: Log what we're receiving
566 + //error_log('=== MXCHAT DEBUG ===');
567 + //error_log('POST data: ' . print_r($_POST, true));
568 + //error_log('Nonce from POST: ' . ($_POST['_ajax_nonce'] ?? 'NOT FOUND'));
569 +
570 + // Check for nonce security
571 + check_ajax_referer('mxchat_save_inline_nonce', '_ajax_nonce');
572 +
573 + // If we get here, nonce passed
574 + //error_log('Nonce verification PASSED');
575 +
576 + // Verify permissions
577 + if (!current_user_can('manage_options')) {
578 + wp_send_json_error(esc_html__('Permission denied.', 'mxchat'));
579 + return;
580 + }
581 +
582 + global $wpdb;
583 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
584 +
585 + // Validate and sanitize input data
586 + $prompt_id = isset($_POST['id']) ? absint($_POST['id']) : 0;
587 + $article_content = isset($_POST['article_content']) ? wp_kses_post(wp_unslash($_POST['article_content'])) : '';
588 + $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
589 +
590 + if ($prompt_id > 0 && !empty($article_content)) {
591 + // Re-generate the embedding vector for the updated content
592 + $embedding_vector = $this->mxchat_generate_embedding($article_content);
593 + if (is_array($embedding_vector)) {
594 + // Serialize the embedding vector before storing it
595 + $embedding_vector_serialized = serialize($embedding_vector);
596 + // Update the prompt in the database
597 + $updated = $wpdb->update(
598 + $table_name,
599 + array(
600 + 'article_content' => $article_content,
601 + 'embedding_vector' => $embedding_vector_serialized,
602 + 'source_url' => $article_url,
603 + ),
604 + array('id' => $prompt_id),
605 + array('%s', '%s', '%s'),
606 + array('%d')
607 + );
608 + if ($updated !== false) {
609 + wp_send_json_success();
610 + } else {
611 + MxChat_Admin::mxchat_log_debug('knowledge_error', 'Database update failed when saving knowledge entry');
612 + wp_send_json_error(esc_html__('Database update failed.', 'mxchat'));
613 + }
614 + } else {
615 + MxChat_Admin::mxchat_log_debug('embedding_error', 'Embedding generation failed for knowledge entry');
616 + wp_send_json_error(esc_html__('Embedding generation failed.', 'mxchat'));
617 + }
618 + } else {
619 + wp_send_json_error(esc_html__('Invalid data.', 'mxchat'));
620 + }
621 +}
622 +
623 +
624 +public function mxchat_get_pdf_processing_status($pdf_url) {
625 + $pdf_url = esc_url_raw($pdf_url);
626 + $status = get_transient(sanitize_key('mxchat_pdf_status_' . md5($pdf_url)));
627 +
628 + if (!$status || !is_array($status)) {
629 + return false;
630 + }
631 +
632 + // Check for stalled processing (no updates for 5 minutes)
633 + if ($status['status'] === 'processing' && (time() - absint($status['last_update'])) > 300) {
634 + $status['status'] = 'error';
635 + $status['error'] = __('PDF processing appears to be stalled. No updates for over 5 minutes.', 'mxchat');
636 +
637 + // Save the updated status
638 + set_transient(
639 + sanitize_key('mxchat_pdf_status_' . md5($pdf_url)),
640 + array_map('sanitize_text_field', $status),
641 + DAY_IN_SECONDS
642 + );
643 + }
644 +
645 + $result = array(
646 + 'total_pages' => absint($status['total_pages']),
647 + 'processed_pages' => absint($status['processed_pages']),
648 + 'failed_pages' => absint($status['failed_pages'] ?? 0),
649 + 'percentage' => ($status['total_pages'] > 0)
650 + ? round((absint($status['processed_pages']) / absint($status['total_pages'])) * 100)
651 + : 0,
652 + 'status' => sanitize_text_field($status['status']),
653 + 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
654 + 'failed_pages_list' => isset($status['failed_pages_list']) ? $status['failed_pages_list'] : array(),
655 + 'completion_summary' => isset($status['completion_summary']) ? $status['completion_summary'] : null
656 + );
657 +
658 + // Add error message if present
659 + if (isset($status['error']) && !empty($status['error'])) {
660 + $result['error'] = sanitize_text_field($status['error']);
661 + }
662 +
663 + return $result;
664 +}
665 +
666 +
667 +public function mxchat_handle_sitemap_submission() {
668 + // Check if the form was submitted and verify permissions
669 + if (!isset($_POST['submit_sitemap']) || !current_user_can('manage_options')) {
670 + wp_die(esc_html__('Unauthorized access', 'mxchat'));
671 + }
672 +
673 + // Verify nonce
674 + check_admin_referer('mxchat_submit_sitemap_action', 'mxchat_submit_sitemap_nonce');
675 +
676 + // Validate URL
677 + if (!isset($_POST['sitemap_url']) || empty($_POST['sitemap_url'])) {
678 + set_transient('mxchat_admin_notice_error',
679 + esc_html__('Please provide a valid URL.', 'mxchat'),
680 + 30
681 + );
682 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
683 + exit;
684 + }
685 +
686 + $submitted_url = esc_url_raw($_POST['sitemap_url']);
687 +
688 + // Get bot_id from form submission
689 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
690 +
691 + // Get bot-specific options and validate API key
692 + $bot_options = $this->get_bot_options($bot_id);
693 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
694 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
695 +
696 + if (strpos($selected_model, 'voyage') === 0) {
697 + $api_key = $options['voyage_api_key'] ?? '';
698 + $provider_name = 'Voyage AI';
699 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
700 + $api_key = $options['gemini_api_key'] ?? '';
701 + $provider_name = 'Google Gemini';
702 + } else {
703 + $api_key = $options['api_key'] ?? '';
704 + $provider_name = 'OpenAI';
705 + }
706 +
707 + if (empty($api_key)) {
708 + $error_message = sprintf(
709 + esc_html__('%s API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
710 + $provider_name
711 + );
712 + set_transient('mxchat_admin_notice_error', $error_message, 30);
713 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
714 + exit;
715 + }
716 +
717 + // Fetch URL
718 + $response = wp_remote_get($submitted_url, array('timeout' => 30));
719 +
720 + if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
721 + $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response);
722 + set_transient('mxchat_admin_notice_error',
723 + sprintf(
724 + esc_html__('Failed to fetch the URL: %s', 'mxchat'),
725 + esc_html($error_message)
726 + ),
727 + 30
728 + );
729 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
730 + exit;
731 + }
732 +
733 + $content_type = wp_remote_retrieve_header($response, 'content-type');
734 + $body_content = wp_remote_retrieve_body($response);
735 +
736 + if (empty($body_content)) {
737 + set_transient('mxchat_admin_notice_error',
738 + esc_html__('Empty response received from URL.', 'mxchat'),
739 + 30
740 + );
741 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
742 + exit;
743 + }
744 +
745 + // Handle PDF URL
746 + if ($this->mxchat_is_pdf_url($submitted_url, $response)) {
747 + $result = $this->mxchat_handle_pdf_for_knowledge_base($submitted_url, $response, $bot_id);
748 +
749 + if ($result === 'queued') {
750 + set_transient('mxchat_admin_notice_success',
751 + esc_html__('PDF queued for processing. Processing will start automatically.', 'mxchat'),
752 + 30
753 + );
754 + } else {
755 + set_transient('mxchat_admin_notice_error',
756 + esc_html__('Failed to queue PDF processing: ', 'mxchat') . esc_html($result),
757 + 30
758 + );
759 + }
760 +
761 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
762 + exit;
763 + }
764 +
765 + // Handle Sitemap XML
766 + if (strpos($content_type, 'xml') !== false || strpos($body_content, '<urlset') !== false) {
767 + libxml_use_internal_errors(true);
768 + $xml = simplexml_load_string($body_content);
769 + $xml_errors = libxml_get_errors();
770 + libxml_clear_errors();
771 +
772 + if ($xml === false || !empty($xml_errors)) {
773 + set_transient('mxchat_admin_notice_error',
774 + esc_html__('Invalid sitemap XML. Please provide a valid sitemap.', 'mxchat'),
775 + 30
776 + );
777 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
778 + exit;
779 + }
780 +
781 + $result = $this->mxchat_handle_sitemap_for_knowledge_base($xml, $submitted_url, $bot_id);
782 +
783 + if ($result === 'queued') {
784 + set_transient('mxchat_admin_notice_success',
785 + esc_html__('Sitemap queued for processing. Processing will start automatically.', 'mxchat'),
786 + 30
787 + );
788 + } else {
789 + set_transient('mxchat_admin_notice_error',
790 + esc_html__('Failed to queue sitemap processing. Please check the status below for details.', 'mxchat'),
791 + 30
792 + );
793 + }
794 +
795 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
796 + exit;
797 + }
798 +
799 + // Handle Regular URL (single page)
800 + $page_content = $this->mxchat_extract_main_content($body_content);
801 + $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
802 +
803 + error_log('[MXCHAT-URL-DEBUG] URL: ' . $submitted_url);
804 + error_log('[MXCHAT-URL-DEBUG] Extracted page_content length: ' . strlen($page_content));
805 + error_log('[MXCHAT-URL-DEBUG] Sanitized content length: ' . strlen($sanitized_content));
806 + error_log('[MXCHAT-URL-DEBUG] Content preview: ' . substr($sanitized_content, 0, 500));
807 +
808 + if (empty($sanitized_content)) {
809 + set_transient('mxchat_admin_notice_error',
810 + esc_html__('No valid content found on the provided URL.', 'mxchat'),
811 + 30
812 + );
813 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
814 + exit;
815 + }
816 +
817 + // For single URLs, process immediately using submit_content_to_db
818 + // This handles chunking automatically for large content
819 + $db_result = MxChat_Utils::submit_content_to_db(
820 + $sanitized_content,
821 + $submitted_url,
822 + $api_key,
823 + null,
824 + $bot_id,
825 + 'url' // content_type
826 + );
827 +
828 + if (is_wp_error($db_result)) {
829 + $error_message = esc_html__('Failed to store content in database: ', 'mxchat') . esc_html($db_result->get_error_message());
830 + set_transient('mxchat_admin_notice_error', $error_message, 30);
831 + } else {
832 + $success_message = esc_html__('URL content successfully submitted!', 'mxchat');
833 + set_transient('mxchat_admin_notice_success', $success_message, 30);
834 + }
835 +
836 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
837 + exit;
838 +}
839 +
840 +
841 +public function mxchat_get_single_url_status() {
842 + $status = get_transient('mxchat_single_url_status');
843 + if (!$status) {
844 + return null;
845 + }
846 +
847 + // Add human-readable time
848 + if (isset($status['timestamp'])) {
849 + $status['human_time'] = human_time_diff(strtotime($status['timestamp']), current_time('timestamp')) . ' ' . __('ago', 'mxchat');
850 + }
851 +
852 + return $status;
853 +}
854 +
855 +public function mxchat_handle_sitemap_for_knowledge_base($xml, $sitemap_url, $bot_id = 'default') {
856 + if (!current_user_can('manage_options')) {
857 + return false;
858 + }
859 +
860 + try {
861 + $sitemap_url = esc_url_raw($sitemap_url);
862 +
863 + if (!$xml || !is_object($xml)) {
864 + throw new Exception(__('Invalid XML object provided', 'mxchat'));
865 + }
866 +
867 + // Get bot-specific embedding API for validation
868 + $bot_options = $this->get_bot_options($bot_id);
869 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
870 +
871 + // Test the embedding API before processing
872 + $test_phrase = "Test embedding generation for MxChat";
873 + $test_result = $this->mxchat_generate_embedding($test_phrase, $bot_id);
874 +
875 + if (is_string($test_result)) {
876 + throw new Exception(__('Embedding API validation failed: ', 'mxchat') . $test_result);
877 + }
878 +
879 + if (!is_array($test_result)) {
880 + throw new Exception(__('Embedding API returned unexpected result type. Please check your configuration.', 'mxchat'));
881 + }
882 +
883 + // Extract URLs from sitemap
884 + $urls = array();
885 + foreach ($xml->url as $url_element) {
886 + $url = esc_url_raw((string)$url_element->loc);
887 + if ($url) {
888 + $urls[] = array('url' => $url);
889 + }
890 + }
891 +
892 + $total_urls = count($urls);
893 +
894 + if ($total_urls < 1) {
895 + throw new Exception(__('No valid URLs found in sitemap', 'mxchat'));
896 + }
897 +
898 + // Create unique queue ID
899 + $queue_id = 'sitemap_' . md5($sitemap_url . time());
900 +
901 + // Add URLs to queue
902 + $queued_count = $this->mxchat_add_to_queue($queue_id, 'url', $urls, $bot_id);
903 +
904 + if ($queued_count === 0) {
905 + throw new Exception(__('Failed to add URLs to processing queue', 'mxchat'));
906 + }
907 +
908 + // Store queue metadata
909 + $this->mxchat_set_queue_meta($queue_id, 'source_url', $sitemap_url);
910 + $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'sitemap');
911 + $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_urls);
912 + $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
913 + $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
914 +
915 + // Store queue ID in transient for status tracking
916 + set_transient('mxchat_active_queue_sitemap', $queue_id, DAY_IN_SECONDS);
917 + set_transient('mxchat_last_sitemap_url', $sitemap_url, DAY_IN_SECONDS);
918 +
919 + return 'queued';
920 +
921 + } catch (Exception $e) {
922 + $error_message = $e->getMessage();
923 + error_log(sprintf(esc_html__('Error preparing sitemap for processing: %s', 'mxchat'), esc_html($error_message)));
924 +
925 + return $error_message;
926 + }
927 +
928 +}
929 +
930 +/**
931 + * Remove shortcode tags but preserve the content inside them
932 + * Example: [vc_column]Hello World[/vc_column] becomes "Hello World"
933 + *
934 + * @param string $content The content containing shortcodes
935 + * @return string Content with shortcode tags removed but inner content preserved
936 + */
937 +private function strip_shortcode_tags_preserve_content($content) {
938 + // Single-pass regex removes all shortcode brackets: [tag], [tag attr="val"], [/tag], [tag /]
939 + // Content between tags is inherently preserved since only brackets are targeted
940 + $result = preg_replace('/\[\/?\w[\w-]*[^\]]*\]/', '', $content);
941 + return ($result !== null) ? $result : $content;
942 +}
943 +
944 +public function mxchat_sanitize_content_for_api($content) {
945 + //error_log('[MXCHAT-SANITIZE] Original content preview: ' . substr($content, 0, 500) . '...');
946 +
947 + // Remove shortcode tags but PRESERVE content inside them
948 + $content = $this->strip_shortcode_tags_preserve_content($content);
949 +
950 + // Remove script, style tags, and HTML comments
951 + $content = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $content);
952 + $content = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $content);
953 + $content = preg_replace('/<!--(.|\s)*?-->/', '', $content);
954 +
955 + // Remove all HTML tags and decode HTML entities
956 + $content = wp_strip_all_tags($content);
957 + $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5);
958 +
959 + // Normalize whitespace but preserve paragraph breaks
960 + // First, normalize line endings to \n
961 + $content = str_replace(["\r\n", "\r"], "\n", $content);
962 + // Replace multiple spaces/tabs with single space, but preserve newlines
963 + $content = preg_replace('/[ \t]+/', ' ', $content);
964 + // Replace 3+ newlines with 2 newlines (max 2 blank lines)
965 + $content = preg_replace('/\n{3,}/', "\n\n", $content);
966 + // Trim each line
967 + $lines = explode("\n", $content);
968 + $lines = array_map('trim', $lines);
969 + $content = implode("\n", $lines);
970 + // Final trim
971 + $content = trim($content);
972 +
973 + // Remove control characters (which can cause database issues) but preserve \n (0x0A) and \r (0x0D)
974 + $content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $content);
975 +
976 + // Remove NULL bytes which can cause database errors
977 + $content = str_replace("\0", "", $content);
978 +
979 + // Ensure valid UTF-8 encoding
980 + $content = wp_check_invalid_utf8($content);
981 +
982 + // Remove any extremely long strings without spaces (often garbage)
983 + $content = preg_replace('/\S{300,}/', ' ', $content);
984 +
985 + // Replace problematic characters that often cause database issues
986 + $content = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $content); // Remove emoji and other high Unicode characters
987 +
988 + // Replace any remaining potentially problematic characters with spaces
989 + // BUT preserve newlines by temporarily replacing them
990 + $content = str_replace("\n", "NEWLINE_PLACEHOLDER", $content);
991 + $content = preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\p{Sm}]/u', ' ', $content);
992 + $content = str_replace("NEWLINE_PLACEHOLDER", "\n", $content);
993 +
994 + // Limit to reasonable length if needed
995 + $max_length = 65000; // Just under MySQL TEXT field limit
996 + if (strlen($content) > $max_length) {
997 + $content = substr($content, 0, $max_length);
998 + }
999 +
1000 + //error_log('[MXCHAT-SANITIZE] Sanitized content preview: ' . substr($content, 0, 500) . '...');
1001 + return $content;
1002 +}
1003 +public function mxchat_extract_main_content($html) {
1004 + if (empty($html)) {
1005 + return '';
1006 + }
1007 + try {
1008 + $dom = new DOMDocument;
1009 + libxml_use_internal_errors(true); // Suppress HTML parsing errors
1010 + @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
1011 + $xpath = new DOMXPath($dom);
1012 +
1013 + // For debugging purposes
1014 + $debugEnabled = true; // Set to true to enable debugging output
1015 + $debug = function($message) use ($debugEnabled) {
1016 + if ($debugEnabled) {
1017 + error_log('[MXCHAT-EXTRACT-DEBUG] ' . $message);
1018 + }
1019 + };
1020 +
1021 + // Direct targeting for Gerow theme posts
1022 + $post_text = $xpath->query('//div[contains(@class, "post-text")]');
1023 + if ($post_text && $post_text->length > 0) {
1024 + $debug("Found post-text directly");
1025 + $content = '';
1026 + foreach ($post_text as $node) {
1027 + $content .= $dom->saveHTML($node);
1028 + }
1029 + if (!empty($content)) {
1030 + $debug("Returning post-text content");
1031 + return $content;
1032 + }
1033 + }
1034 +
1035 + // Try to get the blog details content which contains the post-text
1036 + $blog_details = $xpath->query('//div[contains(@class, "blog-details-content")]');
1037 + if ($blog_details && $blog_details->length > 0) {
1038 + $debug("Found blog-details-content");
1039 + $content = '';
1040 + foreach ($blog_details as $node) {
1041 + $content .= $dom->saveHTML($node);
1042 + }
1043 + if (!empty($content)) {
1044 + $debug("Returning blog-details-content");
1045 + return $content;
1046 + }
1047 + }
1048 +
1049 + // Try to get the article which contains the blog details
1050 + $article = $xpath->query('//article[contains(@class, "blog-details-wrap")]');
1051 + if ($article && $article->length > 0) {
1052 + $debug("Found article with blog-details-wrap");
1053 + $content = '';
1054 + foreach ($article as $node) {
1055 + $content .= $dom->saveHTML($node);
1056 + }
1057 + if (!empty($content)) {
1058 + $debug("Returning article content");
1059 + return $content;
1060 + }
1061 + }
1062 +
1063 + // Try even broader with the blog-item-wrap
1064 + $blog_item = $xpath->query('//div[contains(@class, "blog-item-wrap")]');
1065 + if ($blog_item && $blog_item->length > 0) {
1066 + $debug("Found blog-item-wrap");
1067 + $content = '';
1068 + foreach ($blog_item as $node) {
1069 + $content .= $dom->saveHTML($node);
1070 + }
1071 + if (!empty($content)) {
1072 + $debug("Returning blog-item-wrap content");
1073 + return $content;
1074 + }
1075 + }
1076 +
1077 + // Specific Gerow theme path
1078 + $gerow_path = $xpath->query('//section[contains(@class, "blog-area")]//div[contains(@class, "post-text")]');
1079 + if ($gerow_path && $gerow_path->length > 0) {
1080 + $debug("Found Gerow theme path to post-text");
1081 + $content = '';
1082 + foreach ($gerow_path as $node) {
1083 + $content .= $dom->saveHTML($node);
1084 + }
1085 + if (!empty($content)) {
1086 + $debug("Returning Gerow post-text content");
1087 + return $content;
1088 + }
1089 + }
1090 +
1091 + // Generic blog post selectors
1092 + $selectors = [
1093 + // Blog post specific selectors
1094 + '//div[contains(@class, "post-text")]',
1095 + '//article[contains(@class, "blog-post-item")]//div[contains(@class, "post-text")]',
1096 + '//div[contains(@class, "blog-details-content")]',
1097 + '//article[contains(@class, "blog-details-wrap")]',
1098 + '//div[contains(@class, "entry-content")]',
1099 + '//div[contains(@class, "blog-content")]',
1100 + '//div[contains(@class, "blog-item-wrap")]',
1101 +
1102 + // More general content selectors
1103 + '//div[contains(@class, "page__content")]',
1104 + '//div[contains(@class, "elementor-widget-container")]',
1105 + '//div[contains(@class, "elementor-text-editor")]',
1106 + '//div[contains(@class, "elementor-widget-text-editor")]',
1107 + '//*[contains(@class, "entry-content")]',
1108 + '//*[contains(@class, "post-content")]',
1109 + '//*[contains(@class, "article-content")]',
1110 + '//*[@id="content"]',
1111 + '//*[@id="main-content"]',
1112 + '//section[contains(@class, "blog-area")]',
1113 + '//article',
1114 + '//main',
1115 + '//div[contains(@class, "content")]'
1116 + ];
1117 +
1118 + // First handle Elementor content - get only leaf widget containers to avoid duplicates
1119 + $debug("Checking for Elementor content");
1120 + // Get widget containers that are direct children of widgets (not nested inside other widget containers)
1121 + $elementor_widgets = $xpath->query('//div[contains(@class, "elementor-widget")]//div[contains(@class, "elementor-widget-container")]');
1122 + if ($elementor_widgets && $elementor_widgets->length > 0) {
1123 + $debug("Found Elementor widgets");
1124 + $seen_content = array(); // Track seen content to avoid duplicates
1125 + $combined_content = '';
1126 + foreach ($elementor_widgets as $widget) {
1127 + $widget_content = $dom->saveHTML($widget);
1128 + if (!empty($widget_content)) {
1129 + // Create a hash of the content to detect duplicates
1130 + $content_hash = md5($widget_content);
1131 + if (!isset($seen_content[$content_hash])) {
1132 + $seen_content[$content_hash] = true;
1133 + $combined_content .= $widget_content;
1134 + }
1135 + }
1136 + }
1137 + if (!empty($combined_content)) {
1138 + $debug("Returning Elementor content");
1139 + return $combined_content;
1140 + }
1141 + }
1142 +
1143 + // Try standard selectors one by one
1144 + foreach ($selectors as $selector) {
1145 + $debug("Trying selector: " . $selector);
1146 + $nodes = $xpath->query($selector);
1147 + if ($nodes && $nodes->length > 0) {
1148 + $debug("Found " . $nodes->length . " matches for selector: " . $selector);
1149 + // Only take the FIRST matching node to avoid duplicate content
1150 + // (pages often have nested or multiple containers with same class)
1151 + $content = $dom->saveHTML($nodes->item(0));
1152 + if (!empty($content)) {
1153 + $debug("Returning content from selector: " . $selector . " (first match only)");
1154 + return $content;
1155 + }
1156 + }
1157 + }
1158 +
1159 + // Manual regex fallback for post-text if DOM methods fail
1160 + $debug("Trying regex fallback");
1161 + if (preg_match('/<div class="post-text">(.*?)<\/div>\s*<\/div>\s*<\/div>/s', $html, $matches)) {
1162 + $debug("Found post-text via regex");
1163 + return '<div class="post-text">' . $matches[1] . '</div>';
1164 + }
1165 +
1166 + // Try to extract the blog section as a whole
1167 + $blog_section = $xpath->query('//section[contains(@class, "blog-area")]');
1168 + if ($blog_section && $blog_section->length > 0) {
1169 + $debug("Found blog-area section");
1170 + $content = '';
1171 + foreach ($blog_section as $node) {
1172 + $content .= $dom->saveHTML($node);
1173 + }
1174 + if (!empty($content)) {
1175 + $debug("Returning blog-area section content");
1176 + return $content;
1177 + }
1178 + }
1179 +
1180 + // Generic container selectors for non-CMS sites (like .asp pages)
1181 + $debug("Trying generic container selectors");
1182 + $generic_selectors = [
1183 + '//div[@id="main"]',
1184 + '//div[@id="wrapper"]',
1185 + '//div[@id="page"]',
1186 + '//div[@id="site-content"]',
1187 + '//div[contains(@class, "main-content")]',
1188 + '//div[contains(@class, "page-content")]',
1189 + '//div[contains(@class, "site-content")]',
1190 + ];
1191 +
1192 + foreach ($generic_selectors as $selector) {
1193 + $debug("Trying generic selector: " . $selector);
1194 + $nodes = $xpath->query($selector);
1195 + if ($nodes && $nodes->length > 0) {
1196 + $content = $dom->saveHTML($nodes->item(0));
1197 + if (!empty($content)) {
1198 + $debug("Returning content from generic selector: " . $selector);
1199 + return $content;
1200 + }
1201 + }
1202 + }
1203 +
1204 + // Paragraph-based content detection - find regions with substantial text
1205 + $debug("Trying paragraph-based content detection");
1206 + $paragraphs = $xpath->query('//p[string-length(normalize-space()) > 50]');
1207 + if ($paragraphs && $paragraphs->length >= 3) {
1208 + $debug("Found " . $paragraphs->length . " substantial paragraphs");
1209 + // Collect all substantial paragraphs and their content
1210 + $paragraph_content = '';
1211 + foreach ($paragraphs as $p) {
1212 + $paragraph_content .= $dom->saveHTML($p) . "\n";
1213 + }
1214 + if (!empty($paragraph_content)) {
1215 + $debug("Returning paragraph-based content");
1216 + return $paragraph_content;
1217 + }
1218 + }
1219 +
1220 + // Improved body fallback - strip nav/header/footer elements first
1221 + $debug("Using improved body fallback");
1222 + $body = $dom->getElementsByTagName('body');
1223 + if ($body->length > 0) {
1224 + // Clone the body to avoid modifying the original DOM
1225 + $body_clone = $body->item(0)->cloneNode(true);
1226 +
1227 + // Remove common non-content elements by tag name
1228 + $remove_tags = ['nav', 'header', 'footer', 'aside', 'script', 'style', 'noscript'];
1229 + foreach ($remove_tags as $tag) {
1230 + $elements = $body_clone->getElementsByTagName($tag);
1231 + // Iterate backwards to safely remove elements
1232 + for ($i = $elements->length - 1; $i >= 0; $i--) {
1233 + $el = $elements->item($i);
1234 + if ($el && $el->parentNode) {
1235 + $el->parentNode->removeChild($el);
1236 + }
1237 + }
1238 + }
1239 +
1240 + // Remove elements with common non-content class names using XPath on the cloned body
1241 + $temp_dom = new DOMDocument();
1242 + @$temp_dom->appendChild($temp_dom->importNode($body_clone, true));
1243 + $temp_xpath = new DOMXPath($temp_dom);
1244 +
1245 + $remove_class_patterns = [
1246 + '//*[contains(@class, "nav")]',
1247 + '//*[contains(@class, "menu")]',
1248 + '//*[contains(@class, "sidebar")]',
1249 + '//*[contains(@class, "footer")]',
1250 + '//*[contains(@class, "header")]',
1251 + '//*[contains(@id, "nav")]',
1252 + '//*[contains(@id, "menu")]',
1253 + '//*[contains(@id, "sidebar")]',
1254 + '//*[contains(@id, "footer")]',
1255 + '//*[contains(@id, "header")]',
1256 + ];
1257 +
1258 + foreach ($remove_class_patterns as $pattern) {
1259 + $elements = $temp_xpath->query($pattern);
1260 + if ($elements) {
1261 + for ($i = $elements->length - 1; $i >= 0; $i--) {
1262 + $el = $elements->item($i);
1263 + if ($el && $el->parentNode) {
1264 + $el->parentNode->removeChild($el);
1265 + }
1266 + }
1267 + }
1268 + }
1269 +
1270 + $cleaned_content = $temp_dom->saveHTML();
1271 + if (!empty($cleaned_content)) {
1272 + $debug("Returning cleaned body content");
1273 + return $cleaned_content;
1274 + }
1275 + }
1276 +
1277 + // Last resort: return the original HTML
1278 + $debug("Returning original HTML");
1279 + return $html;
1280 + } catch (Exception $e) {
1281 + //error_log('[MXCHAT-ERROR] Content extraction failed: ' . $e->getMessage());
1282 + return $html; // Return original HTML if parsing fails
1283 + } finally {
1284 + libxml_clear_errors();
1285 + }
1286 +}
1287 +public function mxchat_get_sitemap_processing_status($sitemap_url) {
1288 + $sitemap_url = esc_url_raw($sitemap_url);
1289 + $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
1290 + $status = get_transient($status_key);
1291 +
1292 + if (!$status || !is_array($status)) {
1293 + return false;
1294 + }
1295 +
1296 + // Auto-complete check: if all URLs are processed but status isn't complete
1297 + if (isset($status['processed_urls']) && isset($status['total_urls']) &&
1298 + $status['processed_urls'] >= $status['total_urls'] &&
1299 + isset($status['status']) && $status['status'] !== 'complete' &&
1300 + $status['status'] !== 'error') {
1301 +
1302 + // Mark as complete
1303 + $status['status'] = 'complete';
1304 + $status['processed_urls'] = $status['total_urls']; // Ensure exact match
1305 +
1306 + // Update the transient with the corrected status
1307 + set_transient($status_key, $status, DAY_IN_SECONDS);
1308 + }
1309 +
1310 + return array(
1311 + 'total_urls' => absint($status['total_urls']),
1312 + 'processed_urls' => absint($status['processed_urls']),
1313 + 'failed_urls' => absint($status['failed_urls'] ?? 0),
1314 + 'percentage' => ($status['total_urls'] > 0)
1315 + ? round((absint($status['processed_urls']) / absint($status['total_urls'])) * 100)
1316 + : 0,
1317 + 'status' => sanitize_text_field($status['status']),
1318 + 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
1319 + 'error' => isset($status['error']) ? sanitize_text_field($status['error']) : '',
1320 + 'last_error' => isset($status['last_error']) ? sanitize_text_field($status['last_error']) : '',
1321 + 'failed_urls_list' => isset($status['failed_urls_list']) ? $status['failed_urls_list'] : array()
1322 + );
1323 +}
1324 +
1325 +public function mxchat_ajax_get_status_updates() {
1326 + try {
1327 + // Verify the request
1328 + check_ajax_referer('mxchat_status_nonce', 'nonce');
1329 +
1330 + // Get active queue IDs
1331 + $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
1332 + $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
1333 +
1334 + $sitemap_status = false;
1335 + $pdf_status = false;
1336 +
1337 + // Get sitemap queue status
1338 + if ($sitemap_queue_id) {
1339 + $sitemap_status = $this->mxchat_get_queue_status_data($sitemap_queue_id, 'sitemap');
1340 + }
1341 +
1342 + // Get PDF queue status
1343 + if ($pdf_queue_id) {
1344 + $pdf_status = $this->mxchat_get_queue_status_data($pdf_queue_id, 'pdf');
1345 + }
1346 +
1347 + $is_active_processing =
1348 + ($sitemap_status && $sitemap_status['status'] === 'processing') ||
1349 + ($pdf_status && $pdf_status['status'] === 'processing');
1350 +
1351 + // Return JSON response with the status data
1352 + wp_send_json(array(
1353 + 'pdf_status' => $pdf_status,
1354 + 'sitemap_status' => $sitemap_status,
1355 + 'is_processing' => $is_active_processing,
1356 + 'sitemap_queue_id' => $sitemap_queue_id,
1357 + 'pdf_queue_id' => $pdf_queue_id
1358 + ));
1359 +
1360 + } catch (Exception $e) {
1361 + error_log('MxChat Status Update Error: ' . $e->getMessage());
1362 +
1363 + wp_send_json_error(array(
1364 + 'message' => 'Error getting status updates: ' . $e->getMessage(),
1365 + 'status' => 'error'
1366 + ));
1367 + }
1368 +}
1369 +
1370 +/**
1371 + * Helper function to get queue status data
1372 + */
1373 +private function mxchat_get_queue_status_data($queue_id, $type = 'sitemap') {
1374 + global $wpdb;
1375 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
1376 +
1377 + // Get counts by status
1378 + $counts = $wpdb->get_results($wpdb->prepare(
1379 + "SELECT status, COUNT(*) as count
1380 + FROM $table_name
1381 + WHERE queue_id = %s
1382 + GROUP BY status",
1383 + $queue_id
1384 + ), OBJECT_K);
1385 +
1386 + $total = 0;
1387 + $completed = 0;
1388 + $failed = 0;
1389 + $processing = 0;
1390 + $pending = 0;
1391 +
1392 + foreach ($counts as $status => $data) {
1393 + $count = absint($data->count);
1394 + $total += $count;
1395 +
1396 + switch ($status) {
1397 + case 'completed':
1398 + $completed = $count;
1399 + break;
1400 + case 'failed':
1401 + $failed = $count;
1402 + break;
1403 + case 'processing':
1404 + $processing = $count;
1405 + break;
1406 + case 'pending':
1407 + $pending = $count;
1408 + break;
1409 + }
1410 + }
1411 +
1412 + if ($total === 0) {
1413 + return false;
1414 + }
1415 +
1416 + // Calculate percentage
1417 + $percentage = round((($completed + $failed) / $total) * 100);
1418 +
1419 + // Get failed items details (limit to 50)
1420 + $failed_items = array();
1421 + if ($failed > 0) {
1422 + $failed_results = $wpdb->get_results($wpdb->prepare(
1423 + "SELECT item_type, item_data, error_message, attempts, completed_at
1424 + FROM $table_name
1425 + WHERE queue_id = %s
1426 + AND status = 'failed'
1427 + AND attempts >= max_attempts
1428 + ORDER BY id DESC
1429 + LIMIT 50",
1430 + $queue_id
1431 + ));
1432 +
1433 + foreach ($failed_results as $item) {
1434 + $data = json_decode($item->item_data, true);
1435 + $url = $type === 'pdf' ? ($data['pdf_url'] ?? '') : ($data['url'] ?? '');
1436 +
1437 + $failed_items[] = array(
1438 + 'url' => $url,
1439 + 'page' => $type === 'pdf' ? ($data['page_number'] ?? 0) : 0,
1440 + 'error' => $item->error_message,
1441 + 'retries' => $item->attempts,
1442 + 'time' => strtotime($item->completed_at)
1443 + );
1444 + }
1445 + }
1446 +
1447 + // Get queue metadata
1448 + $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
1449 +
1450 + // Determine if queue is complete
1451 + $is_complete = ($pending === 0 && $processing === 0);
1452 +
1453 + // Get last update time
1454 + $last_update = $wpdb->get_var($wpdb->prepare(
1455 + "SELECT MAX(UNIX_TIMESTAMP(COALESCE(completed_at, started_at, created_at)))
1456 + FROM $table_name
1457 + WHERE queue_id = %s",
1458 + $queue_id
1459 + ));
1460 +
1461 + $last_update_text = $last_update ? human_time_diff($last_update, time()) . ' ' . __('ago', 'mxchat') : __('Just now', 'mxchat');
1462 +
1463 + // Format based on type
1464 + if ($type === 'pdf') {
1465 + return array(
1466 + 'total_pages' => $total,
1467 + 'processed_pages' => $completed + $failed,
1468 + 'failed_pages' => $failed,
1469 + 'percentage' => $percentage,
1470 + 'status' => $is_complete ? 'complete' : 'processing',
1471 + 'last_update' => $last_update_text,
1472 + 'failed_pages_list' => $failed_items,
1473 + 'pdf_url' => $source_url,
1474 + 'queue_id' => $queue_id
1475 + );
1476 + } else {
1477 + return array(
1478 + 'total_urls' => $total,
1479 + 'processed_urls' => $completed + $failed,
1480 + 'failed_urls' => $failed,
1481 + 'percentage' => $percentage,
1482 + 'status' => $is_complete ? 'complete' : 'processing',
1483 + 'last_update' => $last_update_text,
1484 + 'failed_urls_list' => $failed_items,
1485 + 'sitemap_url' => $source_url,
1486 + 'queue_id' => $queue_id
1487 + );
1488 + }
1489 +}
1490 +
1491 +/**
1492 + * Public method to get processing status for both sitemap and PDF queues
1493 + * Used by admin pages to display processing status
1494 + *
1495 + * @return array Array with 'sitemap_status', 'pdf_status', and 'is_processing' keys
1496 + */
1497 +public function mxchat_get_processing_statuses() {
1498 + $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
1499 + $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
1500 +
1501 + $sitemap_status = false;
1502 + $pdf_status = false;
1503 +
1504 + if ($sitemap_queue_id) {
1505 + $sitemap_status = $this->mxchat_get_queue_status_data($sitemap_queue_id, 'sitemap');
1506 + }
1507 +
1508 + if ($pdf_queue_id) {
1509 + $pdf_status = $this->mxchat_get_queue_status_data($pdf_queue_id, 'pdf');
1510 + }
1511 +
1512 + $is_processing =
1513 + ($sitemap_status && $sitemap_status['status'] === 'processing') ||
1514 + ($pdf_status && $pdf_status['status'] === 'processing');
1515 +
1516 + return array(
1517 + 'sitemap_status' => $sitemap_status,
1518 + 'pdf_status' => $pdf_status,
1519 + 'is_processing' => $is_processing
1520 + );
1521 +}
1522 +
1523 +/**
1524 + * AJAX handler to get recent knowledge entries for real-time table updates
1525 + * UPDATED: Now supports both WordPress DB and Pinecone data sources
1526 + */
1527 +public function ajax_mxchat_get_recent_entries() {
1528 + check_ajax_referer('mxchat_entries_nonce', 'nonce');
1529 +
1530 + if (!current_user_can('manage_options')) {
1531 + wp_send_json_error(array('message' => 'Unauthorized'));
1532 + return;
1533 + }
1534 +
1535 + global $wpdb;
1536 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1537 +
1538 + // Get parameters
1539 + $last_id = isset($_POST['last_id']) ? absint($_POST['last_id']) : 0;
1540 + $limit = isset($_POST['limit']) ? min(absint($_POST['limit']), 50) : 10;
1541 + $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
1542 +
1543 + // Check if Pinecone is enabled for this bot
1544 + $pinecone_manager = $this->mxchat_get_pinecone_manager();
1545 + $pinecone_options = $pinecone_manager ? $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id) : array();
1546 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1547 + $has_pinecone_api = !empty($pinecone_options['mxchat_pinecone_api_key']);
1548 +
1549 + if ($use_pinecone && $has_pinecone_api) {
1550 + // PINECONE DATA SOURCE - Get grouped entry count (not raw vector count)
1551 + // Use mxchat_fetch_pinecone_records which returns total_unique_entries
1552 + $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, '', 1, 10, $bot_id, '');
1553 + $total_count = $records['total'] ?? 0;
1554 +
1555 + // For Pinecone, we don't return individual entries during polling
1556 + // (entries are already displayed on page load via mxchat_fetch_pinecone_records)
1557 + // We just return the updated count
1558 + wp_send_json_success(array(
1559 + 'entries' => array(),
1560 + 'total_count' => absint($total_count),
1561 + 'max_id' => $last_id,
1562 + 'data_source' => 'pinecone'
1563 + ));
1564 + return;
1565 + }
1566 +
1567 + // WORDPRESS DB DATA SOURCE
1568 + // Build query to get entries newer than last_id
1569 + $where_clauses = array('1=1');
1570 + $where_values = array();
1571 +
1572 + if ($last_id > 0) {
1573 + $where_clauses[] = 'id > %d';
1574 + $where_values[] = $last_id;
1575 + }
1576 +
1577 + // Note: WordPress DB table doesn't have bot_id column
1578 + // Multi-bot filtering is handled via Pinecone namespaces
1579 +
1580 + $where_sql = implode(' AND ', $where_clauses);
1581 +
1582 + // Get recent entries
1583 + $query = "SELECT id, article_content, source_url, timestamp
1584 + FROM $table_name
1585 + WHERE $where_sql
1586 + ORDER BY id DESC
1587 + LIMIT %d";
1588 +
1589 + $where_values[] = $limit;
1590 +
1591 + $entries = $wpdb->get_results($wpdb->prepare($query, $where_values));
1592 +
1593 + // Get total count of GROUPED entries (by source_url) - matches pagination display
1594 + // Count unique source_urls (excluding mxchat:// internal refs) + count of ungrouped rows
1595 + $total_count = $wpdb->get_var(
1596 + "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '' AND source_url NOT LIKE 'mxchat://%') +
1597 + (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')"
1598 + );
1599 +
1600 + // Format entries for response
1601 + $formatted_entries = array();
1602 + $preview_length = 150;
1603 + foreach ($entries as $entry) {
1604 + // Parse chunk metadata using the proper chunker method (same as initial page load)
1605 + if (class_exists('MxChat_Chunker')) {
1606 + $chunk_meta = MxChat_Chunker::parse_stored_chunk($entry->article_content);
1607 + $display_content = $chunk_meta['text'];
1608 + $chunk_metadata = $chunk_meta['metadata'];
1609 + } else {
1610 + $display_content = $entry->article_content;
1611 + $chunk_metadata = array();
1612 + }
1613 +
1614 + $content_preview = mb_strlen($display_content) > $preview_length
1615 + ? mb_substr($display_content, 0, $preview_length) . '...'
1616 + : $display_content;
1617 +
1618 + $formatted_entries[] = array(
1619 + 'id' => $entry->id,
1620 + 'preview' => esc_html($content_preview),
1621 + 'full_content' => wp_kses_post(wpautop($display_content)),
1622 + 'content_length' => mb_strlen($display_content),
1623 + 'preview_length' => $preview_length,
1624 + 'source_url' => $entry->source_url,
1625 + 'has_link' => !empty($entry->source_url) && strpos($entry->source_url, 'mxchat://') !== 0,
1626 + 'chunk_metadata' => $chunk_metadata,
1627 + 'delete_nonce' => wp_create_nonce('mxchat_delete_prompt_nonce')
1628 + );
1629 + }
1630 +
1631 + wp_send_json_success(array(
1632 + 'entries' => $formatted_entries,
1633 + 'total_count' => absint($total_count),
1634 + 'max_id' => !empty($entries) ? $entries[0]->id : $last_id,
1635 + 'data_source' => 'wordpress'
1636 + ));
1637 +}
1638 +
1639 +/**
1640 + * Get Pinecone total count from stats API
1641 + * Helper function for ajax_mxchat_get_recent_entries
1642 + */
1643 +private function mxchat_get_pinecone_count_from_stats($pinecone_options) {
1644 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1645 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1646 + $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
1647 +
1648 + if (empty($api_key) || empty($host)) {
1649 + return 0;
1650 + }
1651 +
1652 + try {
1653 + $stats_url = "https://{$host}/describe_index_stats";
1654 +
1655 + $response = wp_remote_post($stats_url, array(
1656 + 'headers' => array(
1657 + 'Api-Key' => $api_key,
1658 + 'Content-Type' => 'application/json'
1659 + ),
1660 + 'body' => '{}',
1661 + 'timeout' => 10
1662 + ));
1663 +
1664 + if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
1665 + $body = wp_remote_retrieve_body($response);
1666 + $stats_data = json_decode($body, true);
1667 +
1668 + // If namespace is specified, get count from that specific namespace
1669 + if (!empty($namespace) && isset($stats_data['namespaces'][$namespace]['vectorCount'])) {
1670 + return intval($stats_data['namespaces'][$namespace]['vectorCount']);
1671 + }
1672 +
1673 + // If no namespace specified or namespace not found in response, use total
1674 + return intval($stats_data['totalVectorCount'] ?? 0);
1675 + }
1676 +
1677 + return 0;
1678 +
1679 + } catch (Exception $e) {
1680 + return 0;
1681 + }
1682 +}
1683 +
1684 +/**
1685 + * AJAX handler to refresh Pinecone entries table via AJAX
1686 + * Returns the table HTML for updating the UI without a full page reload
1687 + */
1688 +public function ajax_mxchat_refresh_pinecone_entries() {
1689 + check_ajax_referer('mxchat_entries_nonce', 'nonce');
1690 +
1691 + if (!current_user_can('manage_options')) {
1692 + wp_send_json_error(array('message' => 'Unauthorized'));
1693 + return;
1694 + }
1695 +
1696 + $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
1697 + $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
1698 + $per_page = 25;
1699 + $search_query = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
1700 + $content_type_filter = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : '';
1701 +
1702 + // Get Pinecone manager and options
1703 + $pinecone_manager = $this->mxchat_get_pinecone_manager();
1704 + if (!$pinecone_manager) {
1705 + wp_send_json_error(array('message' => 'Pinecone manager not available'));
1706 + return;
1707 + }
1708 +
1709 + $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
1710 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1711 + $pinecone_api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1712 +
1713 + if (!$use_pinecone || empty($pinecone_api_key)) {
1714 + wp_send_json_error(array('message' => 'Pinecone not configured'));
1715 + return;
1716 + }
1717 +
1718 + // Fetch records from Pinecone
1719 + $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, $search_query, $page, $per_page, $bot_id, $content_type_filter);
1720 + $prompts = $records['data'] ?? array();
1721 + $total_records = $records['total'] ?? 0;
1722 +
1723 + // Preprocess Pinecone records — set chunk_metadata and display_content
1724 + // (matches admin-knowledge-page.php preprocessing)
1725 + foreach ($prompts as $prompt) {
1726 + if (isset($prompt->chunk_index) && $prompt->chunk_index !== null) {
1727 + $prompt->chunk_metadata = array(
1728 + 'chunk_index' => intval($prompt->chunk_index),
1729 + 'total_chunks' => isset($prompt->total_chunks) ? intval($prompt->total_chunks) : null,
1730 + 'is_chunked' => isset($prompt->is_chunked) ? (bool) $prompt->is_chunked : true,
1731 + 'source_url' => $prompt->source_url ?? ''
1732 + );
1733 + $prompt->display_content = $prompt->article_content;
1734 + } else {
1735 + $prompt->chunk_metadata = array();
1736 + $prompt->display_content = $prompt->article_content ?? '';
1737 + }
1738 + }
1739 +
1740 + // Group prompts by source_url
1741 + $grouped_prompts = array();
1742 + foreach ($prompts as $prompt) {
1743 + $source_url = '';
1744 + if (!empty($prompt->chunk_metadata['source_url'])) {
1745 + $source_url = $prompt->chunk_metadata['source_url'];
1746 + } elseif (!empty($prompt->source_url)) {
1747 + $source_url = $prompt->source_url;
1748 + }
1749 +
1750 + if (!empty($source_url)) {
1751 + if (!isset($grouped_prompts[$source_url])) {
1752 + $grouped_prompts[$source_url] = array();
1753 + }
1754 + $grouped_prompts[$source_url][] = $prompt;
1755 + } else {
1756 + $grouped_prompts['_ungrouped_' . $prompt->id] = array($prompt);
1757 + }
1758 + }
1759 +
1760 + // Sort each group by chunk_index
1761 + foreach ($grouped_prompts as $source_url => &$group) {
1762 + usort($group, function($a, $b) {
1763 + $index_a = isset($a->chunk_metadata['chunk_index']) ? intval($a->chunk_metadata['chunk_index']) : 0;
1764 + $index_b = isset($b->chunk_metadata['chunk_index']) ? intval($b->chunk_metadata['chunk_index']) : 0;
1765 + return $index_a - $index_b;
1766 + });
1767 + }
1768 + unset($group);
1769 +
1770 + // Build HTML for the table rows - must match admin-knowledge-page.php structure exactly
1771 + ob_start();
1772 + $display_index = 0;
1773 + $current_page = $page;
1774 + $data_source = 'pinecone';
1775 + $current_bot_id = $bot_id;
1776 + $preview_length = 150;
1777 +
1778 + if (empty($grouped_prompts)) {
1779 + echo '<tr><td colspan="4" style="padding: 40px; text-align: center; color: var(--mxch-text-muted);">';
1780 + esc_html_e('No knowledge entries found in Pinecone.', 'mxchat');
1781 + echo '</td></tr>';
1782 + } else {
1783 + foreach ($grouped_prompts as $source_url => $group) {
1784 + $chunk_count = count($group);
1785 + $first_prompt = $group[0];
1786 + $display_index++;
1787 +
1788 + if ($chunk_count > 1) {
1789 + // Multiple chunks - show grouped row with expand button
1790 + $group_id = 'group-' . md5($source_url);
1791 + ?>
1792 + <tr id="prompt-<?php echo esc_attr($first_prompt->id); ?>"
1793 + class="mxchat-chunk-group-header"
1794 + data-source="<?php echo esc_attr($data_source); ?>"
1795 + data-group-id="<?php echo esc_attr($group_id); ?>"
1796 + style="border-bottom: 1px solid var(--mxch-card-border); background: rgba(33, 150, 243, 0.02);">
1797 + <td style="padding: 12px 16px; text-align: center;">
1798 + <input type="checkbox"
1799 + class="mxchat-entry-checkbox"
1800 + data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
1801 + data-source="<?php echo esc_attr($data_source); ?>"
1802 + data-source-url="<?php echo esc_attr($source_url); ?>"
1803 + data-is-group="true"
1804 + data-chunk-count="<?php echo esc_attr($chunk_count); ?>">
1805 + </td>
1806 + <td style="padding: 12px 16px; font-size: 13px;">
1807 + <?php echo esc_html($display_index + (($current_page - 1) * $per_page)); ?>
1808 + </td>
1809 + <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
1810 + <div class="mxchat-chunk-group-info">
1811 + <button type="button" class="mxchat-chunk-toggle" data-group-id="<?php echo esc_attr($group_id); ?>">
1812 + <span class="dashicons dashicons-arrow-right-alt2"></span>
1813 + </button>
1814 + <span class="mxchat-chunk-badge"><?php echo esc_html($chunk_count); ?> <?php esc_html_e('chunks', 'mxchat'); ?></span>
1815 + <span class="mxchat-chunk-preview">
1816 + <?php
1817 + $parent_content = isset($first_prompt->display_content) ? $first_prompt->display_content : (isset($first_prompt->article_content) ? $first_prompt->article_content : '');
1818 + $content_preview = mb_substr($parent_content, 0, 100);
1819 + echo esc_html($content_preview . '...');
1820 + ?>
1821 + </span>
1822 + </div>
1823 + </td>
1824 + <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
1825 + <?php if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0 && strpos($source_url, '_ungrouped_') !== 0) : ?>
1826 + <a href="<?php echo esc_url($source_url); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
1827 + <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
1828 + <?php esc_html_e('View Source', 'mxchat'); ?>
1829 + </a>
1830 + <?php else : ?>
1831 + <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
1832 + <?php endif; ?>
1833 + </td>
1834 + <td class="mxchat-actions-cell" style="padding: 12px 16px;">
1835 + <button type="button"
1836 + class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
1837 + data-source-url="<?php echo esc_attr($source_url); ?>"
1838 + data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
1839 + data-data-source="<?php echo esc_attr($data_source); ?>"
1840 + data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
1841 + data-nonce="<?php echo wp_create_nonce('mxchat_delete_chunks_nonce'); ?>"
1842 + style="color: var(--mxch-error);"
1843 + title="<?php esc_attr_e('Delete all chunks', 'mxchat'); ?>">
1844 + <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
1845 + </button>
1846 + </td>
1847 + </tr>
1848 + <?php
1849 + // Render hidden chunk rows
1850 + foreach ($group as $chunk_index => $chunk) {
1851 + $meta_chunk_index = isset($chunk->chunk_metadata['chunk_index']) ? intval($chunk->chunk_metadata['chunk_index']) : $chunk_index;
1852 + $meta_total_chunks = isset($chunk->chunk_metadata['total_chunks']) ? intval($chunk->chunk_metadata['total_chunks']) : $chunk_count;
1853 + $content = isset($chunk->display_content) ? $chunk->display_content : (isset($chunk->article_content) ? $chunk->article_content : '');
1854 + $content_preview = mb_strlen($content) > $preview_length
1855 + ? mb_substr($content, 0, $preview_length) . '...'
1856 + : $content;
1857 + ?>
1858 + <tr id="prompt-<?php echo esc_attr($chunk->id); ?>"
1859 + class="mxchat-chunk-row <?php echo esc_attr($group_id); ?>"
1860 + data-source="<?php echo esc_attr($data_source); ?>"
1861 + style="display: none; background: #f8f9fa; border-bottom: 1px solid var(--mxch-card-border);">
1862 + <td style="padding: 12px 16px; text-align: center;">
1863 + <!-- Checkbox column placeholder for chunks (managed by group) -->
1864 + </td>
1865 + <td style="padding: 12px 16px 12px 30px; font-size: 13px;">
1866 + <!-- Hidden ID column for chunks -->
1867 + </td>
1868 + <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
1869 + <div class="mxchat-accordion-wrapper">
1870 + <div class="mxchat-content-preview">
1871 + <span class="mxchat-chunk-indicator" style="margin-right: 10px; color: var(--mxch-text-secondary); font-size: 12px;">
1872 + <?php printf(esc_html__('Chunk %d of %d', 'mxchat'), $meta_chunk_index + 1, $meta_total_chunks); ?>
1873 + </span>
1874 + <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
1875 + <?php if (mb_strlen($content) > $preview_length) : ?>
1876 + <button class="mxchat-expand-toggle" type="button">
1877 + <span class="dashicons dashicons-arrow-down-alt2"></span>
1878 + </button>
1879 + <?php endif; ?>
1880 + </div>
1881 + <div class="mxchat-content-full" style="display: none;">
1882 + <div class="content-view">
1883 + <?php
1884 + if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
1885 + echo '<div dir="rtl" lang="he" class="rtl-content">';
1886 + echo wp_kses_post(wpautop($content));
1887 + echo '</div>';
1888 + } else {
1889 + echo wp_kses_post(wpautop($content));
1890 + }
1891 + ?>
1892 + </div>
1893 + </div>
1894 + </div>
1895 + </td>
1896 + <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
1897 + <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Same as parent', 'mxchat'); ?></span>
1898 + </td>
1899 + <td class="mxchat-actions-cell" style="padding: 12px 16px;">
1900 + <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Managed by group', 'mxchat'); ?></span>
1901 + </td>
1902 + </tr>
1903 + <?php
1904 + }
1905 + } else {
1906 + // Single entry - display normally with accordion
1907 + $prompt = $first_prompt;
1908 + $content = isset($prompt->display_content) ? $prompt->display_content : (isset($prompt->article_content) ? $prompt->article_content : '');
1909 + $content_preview = mb_strlen($content) > $preview_length
1910 + ? mb_substr($content, 0, $preview_length) . '...'
1911 + : $content;
1912 + ?>
1913 + <tr id="prompt-<?php echo esc_attr($prompt->id); ?>"
1914 + data-source="<?php echo esc_attr($data_source); ?>"
1915 + style="border-bottom: 1px solid var(--mxch-card-border); background: rgba(33, 150, 243, 0.02);">
1916 + <td style="padding: 12px 16px; text-align: center;">
1917 + <input type="checkbox"
1918 + class="mxchat-entry-checkbox"
1919 + data-entry-id="<?php echo esc_attr($prompt->id); ?>"
1920 + data-source="<?php echo esc_attr($data_source); ?>"
1921 + data-source-url="<?php echo esc_attr($prompt->source_url ?? ''); ?>"
1922 + data-is-group="false"
1923 + data-chunk-count="1">
1924 + </td>
1925 + <td style="padding: 12px 16px; font-size: 13px;">
1926 + <?php echo esc_html($display_index + (($current_page - 1) * $per_page)); ?>
1927 + </td>
1928 + <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
1929 + <div class="mxchat-accordion-wrapper">
1930 + <div class="mxchat-content-preview">
1931 + <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
1932 + <?php if (mb_strlen($content) > $preview_length) : ?>
1933 + <button class="mxchat-expand-toggle" type="button">
1934 + <span class="dashicons dashicons-arrow-down-alt2"></span>
1935 + </button>
1936 + <?php endif; ?>
1937 + </div>
1938 + <div class="mxchat-content-full" style="display: none;">
1939 + <div class="content-view">
1940 + <?php
1941 + if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
1942 + echo '<div dir="rtl" lang="he" class="rtl-content">';
1943 + echo wp_kses_post(wpautop($content));
1944 + echo '</div>';
1945 + } else {
1946 + echo wp_kses_post(wpautop($content));
1947 + }
1948 + ?>
1949 + </div>
1950 + </div>
1951 + </div>
1952 + </td>
1953 + <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
1954 + <?php
1955 + $actual_source = $source_url;
1956 + if (strpos($source_url, '_ungrouped_') === 0) {
1957 + $actual_source = $prompt->source_url ?? '';
1958 + }
1959 + if (!empty($actual_source) && strpos($actual_source, 'mxchat://') !== 0) : ?>
1960 + <a href="<?php echo esc_url($actual_source); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
1961 + <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
1962 + <?php esc_html_e('View', 'mxchat'); ?>
1963 + </a>
1964 + <?php else : ?>
1965 + <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
1966 + <?php endif; ?>
1967 + </td>
1968 + <td style="padding: 12px 16px;">
1969 + <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);">
1970 + <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
1971 + </button>
1972 + </td>
1973 + </tr>
1974 + <?php
1975 + }
1976 + }
1977 + }
1978 + $html = ob_get_clean();
1979 +
1980 + // Generate pagination HTML for Pinecone
1981 + $total_pages = ceil($total_records / $per_page);
1982 + $pagination_html = '';
1983 + if ($total_pages > 1) {
1984 + $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) . '">';
1985 +
1986 + // Previous button
1987 + if ($page > 1) {
1988 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page - 1) . '">' . esc_html__('&laquo; Previous', 'mxchat') . '</a> ';
1989 + }
1990 +
1991 + // Page numbers
1992 + $start_page = max(1, $page - 2);
1993 + $end_page = min($total_pages, $page + 2);
1994 +
1995 + if ($start_page > 1) {
1996 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="1">1</a> ';
1997 + if ($start_page > 2) {
1998 + $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
1999 + }
2000 + }
2001 +
2002 + for ($i = $start_page; $i <= $end_page; $i++) {
2003 + if ($i == $page) {
2004 + $pagination_html .= '<span class="mxchat-page-current">' . $i . '</span> ';
2005 + } else {
2006 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $i . '">' . $i . '</a> ';
2007 + }
2008 + }
2009 +
2010 + if ($end_page < $total_pages) {
2011 + if ($end_page < $total_pages - 1) {
2012 + $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2013 + }
2014 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $total_pages . '">' . $total_pages . '</a> ';
2015 + }
2016 +
2017 + // Next button
2018 + if ($page < $total_pages) {
2019 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page + 1) . '">' . esc_html__('Next &raquo;', 'mxchat') . '</a>';
2020 + }
2021 +
2022 + $pagination_html .= '</div>';
2023 + }
2024 +
2025 + wp_send_json_success(array(
2026 + 'html' => $html,
2027 + 'pagination_html' => $pagination_html,
2028 + 'total_count' => $total_records,
2029 + 'total_pages' => $total_pages,
2030 + 'page' => $page,
2031 + 'per_page' => $per_page,
2032 + 'data_source' => 'pinecone'
2033 + ));
2034 +}
2035 +
2036 +/**
2037 + * AJAX handler for pagination - handles both WordPress DB and Pinecone data sources
2038 + * Returns paginated entries without requiring a full page reload
2039 + */
2040 +public function ajax_mxchat_paginate_entries() {
2041 + check_ajax_referer('mxchat_entries_nonce', 'nonce');
2042 +
2043 + if (!current_user_can('manage_options')) {
2044 + wp_send_json_error(array('message' => 'Unauthorized'));
2045 + return;
2046 + }
2047 +
2048 + $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
2049 + $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
2050 + $search_query = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
2051 + $content_type_filter = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : '';
2052 + $per_page = 25;
2053 +
2054 + // Check if Pinecone is enabled for this bot
2055 + $pinecone_manager = $this->mxchat_get_pinecone_manager();
2056 + $pinecone_options = $pinecone_manager ? $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id) : array();
2057 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2058 + $has_pinecone_api = !empty($pinecone_options['mxchat_pinecone_api_key']);
2059 +
2060 + if ($use_pinecone && $has_pinecone_api) {
2061 + // Delegate to Pinecone pagination handler (pass search params)
2062 + $_POST['page'] = $page;
2063 + $_POST['search'] = $search_query;
2064 + $_POST['content_type'] = $content_type_filter;
2065 + $this->ajax_mxchat_refresh_pinecone_entries();
2066 + return;
2067 + }
2068 +
2069 + // WordPress DB pagination - MUST match initial page load logic exactly
2070 + global $wpdb;
2071 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2072 + $offset = ($page - 1) * $per_page;
2073 +
2074 + // Build WHERE clause for search and content type filtering
2075 + $where_clauses = array();
2076 + $where_values = array();
2077 +
2078 + if ($search_query) {
2079 + $where_clauses[] = "article_content LIKE %s";
2080 + $where_values[] = '%' . $wpdb->esc_like($search_query) . '%';
2081 + }
2082 +
2083 + if ($content_type_filter) {
2084 + switch ($content_type_filter) {
2085 + case 'manual':
2086 + $where_clauses[] = "(source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')";
2087 + break;
2088 + case 'pdf':
2089 + $where_clauses[] = "source_url LIKE '%.pdf'";
2090 + break;
2091 + case 'url':
2092 + $where_clauses[] = "source_url != '' AND source_url IS NOT NULL AND source_url NOT LIKE 'mxchat://%' AND source_url NOT LIKE '%.pdf'";
2093 + break;
2094 + }
2095 + }
2096 +
2097 + $where_sql = !empty($where_clauses) ? 'WHERE ' . implode(' AND ', $where_clauses) : '';
2098 +
2099 + // Count grouped entries with filters applied
2100 + if (!empty($where_values)) {
2101 + $count_args = array_merge($where_values, $where_values);
2102 + $count_query = $wpdb->prepare(
2103 + "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$where_sql} AND source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2104 + (SELECT COUNT(*) FROM {$table_name} {$where_sql} AND (source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%'))",
2105 + ...$count_args
2106 + );
2107 + $total_records = $wpdb->get_var($count_query);
2108 + } else if (!empty($where_sql)) {
2109 + // Content type filter only (no search), no prepared values needed
2110 + $total_records = $wpdb->get_var(
2111 + "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$where_sql} AND source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2112 + (SELECT COUNT(*) FROM {$table_name} {$where_sql} AND (source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%'))"
2113 + );
2114 + } else {
2115 + // No filters
2116 + $total_records = $wpdb->get_var(
2117 + "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2118 + (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')"
2119 + );
2120 + }
2121 + $total_pages = ceil($total_records / $per_page);
2122 +
2123 + // Step 1: Get unique source_urls for this page (ordered by latest timestamp) with filters
2124 + if (!empty($where_values)) {
2125 + $query_args = array_merge($where_values, array($per_page, $offset));
2126 + $urls_query = $wpdb->prepare(
2127 + "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2128 + {$where_sql}
2129 + GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2130 + ...$query_args
2131 + );
2132 + } else if (!empty($where_sql)) {
2133 + $urls_query = $wpdb->prepare(
2134 + "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2135 + {$where_sql}
2136 + GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2137 + $per_page, $offset
2138 + );
2139 + } else {
2140 + $urls_query = $wpdb->prepare(
2141 + "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2142 + GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2143 + $per_page, $offset
2144 + );
2145 + }
2146 + $page_urls = $wpdb->get_results($urls_query);
2147 +
2148 + // Step 2: Build list of source_urls to fetch
2149 + $url_list = array();
2150 + $url_order_map = array();
2151 + $order_index = 0;
2152 + foreach ($page_urls as $url_row) {
2153 + $url = $url_row->source_url;
2154 + $url_list[] = $url;
2155 + $url_order_map[$url] = $order_index++;
2156 + }
2157 +
2158 + // Step 3: Fetch all rows for these source_urls (with search filter if applicable)
2159 + $prompts = array();
2160 + if (!empty($url_list)) {
2161 + $placeholders = implode(',', array_fill(0, count($url_list), '%s'));
2162 + if ($search_query) {
2163 + // Include search filter in the final fetch
2164 + $prompts_query = $wpdb->prepare(
2165 + "SELECT id, article_content, source_url, timestamp, role_restriction
2166 + FROM {$table_name}
2167 + WHERE source_url IN ($placeholders) AND article_content LIKE %s
2168 + ORDER BY timestamp DESC",
2169 + ...array_merge($url_list, ['%' . $wpdb->esc_like($search_query) . '%'])
2170 + );
2171 + } else {
2172 + $prompts_query = $wpdb->prepare(
2173 + "SELECT id, article_content, source_url, timestamp, role_restriction
2174 + FROM {$table_name}
2175 + WHERE source_url IN ($placeholders)
2176 + ORDER BY timestamp DESC",
2177 + $url_list
2178 + );
2179 + }
2180 + $prompts = $wpdb->get_results($prompts_query);
2181 + }
2182 +
2183 + // Group prompts by source_url for chunk display
2184 + $grouped_prompts = array();
2185 + foreach ($prompts as $prompt) {
2186 + $source_url = $prompt->source_url ?? '';
2187 +
2188 + // Parse chunk metadata using the proper chunker method (same as initial page load)
2189 + if (class_exists('MxChat_Chunker')) {
2190 + $chunk_meta = MxChat_Chunker::parse_stored_chunk($prompt->article_content);
2191 + $prompt->chunk_metadata = $chunk_meta['metadata'];
2192 + $prompt->display_content = $chunk_meta['text'];
2193 + } else {
2194 + $prompt->chunk_metadata = array();
2195 + $prompt->display_content = $prompt->article_content;
2196 + }
2197 +
2198 + if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0) {
2199 + if (!isset($grouped_prompts[$source_url])) {
2200 + $grouped_prompts[$source_url] = array();
2201 + }
2202 + $grouped_prompts[$source_url][] = $prompt;
2203 + } else {
2204 + // Ungrouped entries
2205 + $grouped_prompts['_ungrouped_' . $prompt->id] = array($prompt);
2206 + }
2207 + }
2208 +
2209 + // Sort groups by the original URL order (newest first)
2210 + uksort($grouped_prompts, function($a, $b) use ($url_order_map) {
2211 + $order_a = isset($url_order_map[$a]) ? $url_order_map[$a] : PHP_INT_MAX;
2212 + $order_b = isset($url_order_map[$b]) ? $url_order_map[$b] : PHP_INT_MAX;
2213 + return $order_a - $order_b;
2214 + });
2215 +
2216 + // Sort each group internally by chunk_index
2217 + foreach ($grouped_prompts as $source_url => &$group) {
2218 + usort($group, function($a, $b) {
2219 + $index_a = isset($a->chunk_metadata['chunk_index']) ? intval($a->chunk_metadata['chunk_index']) : 0;
2220 + $index_b = isset($b->chunk_metadata['chunk_index']) ? intval($b->chunk_metadata['chunk_index']) : 0;
2221 + return $index_a - $index_b;
2222 + });
2223 + }
2224 + unset($group);
2225 +
2226 + // Build HTML for the table rows
2227 + ob_start();
2228 + $display_index = 0;
2229 + $current_page = $page;
2230 + $data_source = 'wordpress';
2231 + $current_bot_id = $bot_id;
2232 + $preview_length = 150;
2233 +
2234 + if (empty($grouped_prompts)) {
2235 + echo '<tr><td colspan="5" style="padding: 40px; text-align: center; color: var(--mxch-text-muted);">';
2236 + esc_html_e('No knowledge entries found. Use the Import Options to add content.', 'mxchat');
2237 + echo '</td></tr>';
2238 + } else {
2239 + foreach ($grouped_prompts as $source_url => $group) {
2240 + $chunk_count = count($group);
2241 + $first_prompt = $group[0];
2242 + $display_index++;
2243 +
2244 + if ($chunk_count > 1) {
2245 + // Multiple chunks - show grouped row with expand button
2246 + $group_id = 'group-' . md5($source_url);
2247 + ?>
2248 + <tr id="prompt-<?php echo esc_attr($first_prompt->id); ?>"
2249 + class="mxchat-chunk-group-header"
2250 + data-source="<?php echo esc_attr($data_source); ?>"
2251 + data-group-id="<?php echo esc_attr($group_id); ?>"
2252 + style="border-bottom: 1px solid var(--mxch-card-border);">
2253 + <td style="padding: 12px 16px; text-align: center;">
2254 + <input type="checkbox"
2255 + class="mxchat-entry-checkbox"
2256 + data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2257 + data-source="<?php echo esc_attr($data_source); ?>"
2258 + data-source-url="<?php echo esc_attr($source_url); ?>"
2259 + data-is-group="true"
2260 + data-chunk-count="<?php echo esc_attr($chunk_count); ?>">
2261 + </td>
2262 + <td style="padding: 12px 16px; font-size: 13px;">
2263 + <?php echo esc_html($first_prompt->id); ?>
2264 + </td>
2265 + <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2266 + <div class="mxchat-chunk-group-info">
2267 + <button type="button" class="mxchat-chunk-toggle" data-group-id="<?php echo esc_attr($group_id); ?>">
2268 + <span class="dashicons dashicons-arrow-right-alt2"></span>
2269 + </button>
2270 + <span class="mxchat-chunk-badge"><?php echo esc_html($chunk_count); ?> <?php esc_html_e('chunks', 'mxchat'); ?></span>
2271 + <span class="mxchat-chunk-preview">
2272 + <?php
2273 + $parent_content = isset($first_prompt->display_content) ? $first_prompt->display_content : $first_prompt->article_content;
2274 + $content_preview = mb_substr($parent_content, 0, 100);
2275 + echo esc_html($content_preview . '...');
2276 + ?>
2277 + </span>
2278 + </div>
2279 + </td>
2280 + <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2281 + <?php if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0 && strpos($source_url, '_ungrouped_') !== 0) : ?>
2282 + <a href="<?php echo esc_url($source_url); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2283 + <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2284 + <?php esc_html_e('View Source', 'mxchat'); ?>
2285 + </a>
2286 + <?php else : ?>
2287 + <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
2288 + <?php endif; ?>
2289 + </td>
2290 + <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2291 + <button type="button"
2292 + class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
2293 + data-source-url="<?php echo esc_attr($source_url); ?>"
2294 + data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
2295 + data-data-source="<?php echo esc_attr($data_source); ?>"
2296 + data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2297 + data-nonce="<?php echo wp_create_nonce('mxchat_delete_chunks_nonce'); ?>"
2298 + style="color: var(--mxch-error);"
2299 + title="<?php esc_attr_e('Delete all chunks', 'mxchat'); ?>">
2300 + <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2301 + </button>
2302 + </td>
2303 + </tr>
2304 + <?php
2305 + // Render hidden chunk rows
2306 + foreach ($group as $chunk_index => $chunk) {
2307 + $meta_chunk_index = isset($chunk->chunk_metadata['chunk_index']) ? intval($chunk->chunk_metadata['chunk_index']) : $chunk_index;
2308 + $meta_total_chunks = isset($chunk->chunk_metadata['total_chunks']) ? intval($chunk->chunk_metadata['total_chunks']) : $chunk_count;
2309 + $content = isset($chunk->display_content) ? $chunk->display_content : $chunk->article_content;
2310 + $content_preview = mb_strlen($content) > $preview_length
2311 + ? mb_substr($content, 0, $preview_length) . '...'
2312 + : $content;
2313 + ?>
2314 + <tr id="prompt-<?php echo esc_attr($chunk->id); ?>"
2315 + class="mxchat-chunk-row <?php echo esc_attr($group_id); ?>"
2316 + data-source="<?php echo esc_attr($data_source); ?>"
2317 + style="display: none; background: #f8f9fa; border-bottom: 1px solid var(--mxch-card-border);">
2318 + <td style="padding: 12px 16px; text-align: center;">
2319 + <!-- Checkbox column placeholder for chunks (managed by group) -->
2320 + </td>
2321 + <td style="padding: 12px 16px 12px 30px; font-size: 13px;">
2322 + <!-- Hidden ID column for chunks -->
2323 + </td>
2324 + <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2325 + <div class="mxchat-accordion-wrapper">
2326 + <div class="mxchat-content-preview">
2327 + <span class="mxchat-chunk-indicator" style="margin-right: 10px; color: var(--mxch-text-secondary); font-size: 12px;">
2328 + <?php printf(esc_html__('Chunk %d of %d', 'mxchat'), $meta_chunk_index + 1, $meta_total_chunks); ?>
2329 + </span>
2330 + <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2331 + <?php if (mb_strlen($content) > $preview_length) : ?>
2332 + <button class="mxchat-expand-toggle" type="button">
2333 + <span class="dashicons dashicons-arrow-down-alt2"></span>
2334 + </button>
2335 + <?php endif; ?>
2336 + </div>
2337 + <div class="mxchat-content-full" style="display: none;">
2338 + <div class="content-view">
2339 + <?php
2340 + if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2341 + echo '<div dir="rtl" lang="he" class="rtl-content">';
2342 + echo wp_kses_post(wpautop($content));
2343 + echo '</div>';
2344 + } else {
2345 + echo wp_kses_post(wpautop($content));
2346 + }
2347 + ?>
2348 + </div>
2349 + </div>
2350 + </div>
2351 + </td>
2352 + <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2353 + <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Same as parent', 'mxchat'); ?></span>
2354 + </td>
2355 + <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2356 + <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Managed by group', 'mxchat'); ?></span>
2357 + </td>
2358 + </tr>
2359 + <?php
2360 + }
2361 + } else {
2362 + // Single entry - display normally with accordion
2363 + $prompt = $first_prompt;
2364 + $content = isset($prompt->display_content) ? $prompt->display_content : $prompt->article_content;
2365 + $content_preview = mb_strlen($content) > $preview_length
2366 + ? mb_substr($content, 0, $preview_length) . '...'
2367 + : $content;
2368 + ?>
2369 + <tr id="prompt-<?php echo esc_attr($prompt->id); ?>"
2370 + data-source="<?php echo esc_attr($data_source); ?>"
2371 + style="border-bottom: 1px solid var(--mxch-card-border);">
2372 + <td style="padding: 12px 16px; text-align: center;">
2373 + <input type="checkbox"
2374 + class="mxchat-entry-checkbox"
2375 + data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2376 + data-source="<?php echo esc_attr($data_source); ?>"
2377 + data-source-url="<?php echo esc_attr($source_url); ?>"
2378 + data-is-group="false">
2379 + </td>
2380 + <td style="padding: 12px 16px; font-size: 13px;">
2381 + <?php echo esc_html($prompt->id); ?>
2382 + </td>
2383 + <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2384 + <div class="mxchat-accordion-wrapper">
2385 + <div class="mxchat-content-preview">
2386 + <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2387 + <?php if (mb_strlen($content) > $preview_length) : ?>
2388 + <button class="mxchat-expand-toggle" type="button">
2389 + <span class="dashicons dashicons-arrow-down-alt2"></span>
2390 + </button>
2391 + <?php endif; ?>
2392 + </div>
2393 + <div class="mxchat-content-full" style="display: none;">
2394 + <div class="content-view">
2395 + <?php
2396 + if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2397 + echo '<div dir="rtl" lang="he" class="rtl-content">';
2398 + echo wp_kses_post(wpautop($content));
2399 + echo '</div>';
2400 + } else {
2401 + echo wp_kses_post(wpautop($content));
2402 + }
2403 + ?>
2404 + </div>
2405 + </div>
2406 + </div>
2407 + </td>
2408 + <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2409 + <?php
2410 + $actual_source = $source_url;
2411 + if (strpos($source_url, '_ungrouped_') === 0) {
2412 + $actual_source = $prompt->source_url ?? '';
2413 + }
2414 + if (!empty($actual_source) && strpos($actual_source, 'mxchat://') !== 0) : ?>
2415 + <a href="<?php echo esc_url($actual_source); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2416 + <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2417 + <?php esc_html_e('View', 'mxchat'); ?>
2418 + </a>
2419 + <?php else : ?>
2420 + <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
2421 + <?php endif; ?>
2422 + </td>
2423 + <td style="padding: 12px 16px;">
2424 + <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);">
2425 + <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2426 + </button>
2427 + </td>
2428 + </tr>
2429 + <?php
2430 + }
2431 + }
2432 + }
2433 + $html = ob_get_clean();
2434 +
2435 + // Generate pagination HTML (include search/filter data for subsequent pages)
2436 + $pagination_html = '';
2437 + if ($total_pages > 1) {
2438 + $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) . '">';
2439 +
2440 + // Previous button
2441 + if ($page > 1) {
2442 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page - 1) . '">' . esc_html__('&laquo; Previous', 'mxchat') . '</a> ';
2443 + }
2444 +
2445 + // Page numbers
2446 + $start_page = max(1, $page - 2);
2447 + $end_page = min($total_pages, $page + 2);
2448 +
2449 + if ($start_page > 1) {
2450 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="1">1</a> ';
2451 + if ($start_page > 2) {
2452 + $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2453 + }
2454 + }
2455 +
2456 + for ($i = $start_page; $i <= $end_page; $i++) {
2457 + if ($i == $page) {
2458 + $pagination_html .= '<span class="mxchat-page-current">' . $i . '</span> ';
2459 + } else {
2460 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $i . '">' . $i . '</a> ';
2461 + }
2462 + }
2463 +
2464 + if ($end_page < $total_pages) {
2465 + if ($end_page < $total_pages - 1) {
2466 + $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2467 + }
2468 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $total_pages . '">' . $total_pages . '</a> ';
2469 + }
2470 +
2471 + // Next button
2472 + if ($page < $total_pages) {
2473 + $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page + 1) . '">' . esc_html__('Next &raquo;', 'mxchat') . '</a>';
2474 + }
2475 +
2476 + $pagination_html .= '</div>';
2477 + }
2478 +
2479 + wp_send_json_success(array(
2480 + 'html' => $html,
2481 + 'pagination_html' => $pagination_html,
2482 + 'total_count' => $total_records,
2483 + 'total_pages' => $total_pages,
2484 + 'page' => $page,
2485 + 'per_page' => $per_page,
2486 + 'data_source' => 'wordpress'
2487 + ));
2488 +}
2489 +
2490 +/**
2491 + * AJAX handler to detect available sitemaps on the site
2492 + * Optimized for speed - only checks primary sitemap indexes first
2493 + */
2494 +public function ajax_mxchat_detect_sitemaps() {
2495 + check_ajax_referer('mxchat_detect_sitemaps_nonce', 'nonce');
2496 +
2497 + if (!current_user_can('manage_options')) {
2498 + wp_send_json_error(array('message' => 'Unauthorized'));
2499 + return;
2500 + }
2501 +
2502 + $site_url = get_site_url();
2503 + $sitemaps = array();
2504 + $found_index = false;
2505 +
2506 + // Only check the main sitemap index files first (much faster)
2507 + // These are the primary entry points that contain sub-sitemaps
2508 + $primary_indexes = array(
2509 + 'sitemap_index.xml' => 'Yoast SEO / Rank Math', // Yoast & Rank Math
2510 + 'wp-sitemap.xml' => 'WordPress Core', // WordPress Core
2511 + 'sitemap.xml' => 'Standard', // Generic/AIOSEO
2512 + );
2513 +
2514 + foreach ($primary_indexes as $path => $source) {
2515 + $url = trailingslashit($site_url) . $path;
2516 +
2517 + $response = wp_remote_head($url, array(
2518 + 'timeout' => 3, // Short timeout
2519 + 'sslverify' => false,
2520 + 'redirection' => 1
2521 + ));
2522 +
2523 + if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2524 + // Found a sitemap index - parse it to get sub-sitemaps
2525 + $sub_sitemaps = $this->parse_sitemap_index($url);
2526 + if (!empty($sub_sitemaps)) {
2527 + $sitemaps[] = array(
2528 + 'url' => $url,
2529 + 'type' => 'index',
2530 + 'source' => $source,
2531 + 'sub_sitemaps' => $sub_sitemaps
2532 + );
2533 + $found_index = true;
2534 + // Found a valid index, no need to check others
2535 + break;
2536 + }
2537 + }
2538 + }
2539 +
2540 + // If no sitemap index found, check for standalone sitemaps
2541 + if (!$found_index) {
2542 + $standalone_sitemaps = array(
2543 + 'post-sitemap.xml' => array('type' => 'content', 'source' => 'Yoast SEO'),
2544 + 'page-sitemap.xml' => array('type' => 'content', 'source' => 'Yoast SEO'),
2545 + );
2546 +
2547 + foreach ($standalone_sitemaps as $path => $info) {
2548 + $url = trailingslashit($site_url) . $path;
2549 +
2550 + $response = wp_remote_head($url, array(
2551 + 'timeout' => 2,
2552 + 'sslverify' => false
2553 + ));
2554 +
2555 + if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2556 + $sitemaps[] = array(
2557 + 'url' => $url,
2558 + 'type' => $info['type'],
2559 + 'source' => $info['source'],
2560 + 'url_count' => 0 // Skip URL count for speed
2561 + );
2562 + }
2563 + }
2564 + }
2565 +
2566 + wp_send_json_success(array(
2567 + 'sitemaps' => $sitemaps,
2568 + 'site_url' => $site_url
2569 + ));
2570 +}
2571 +
2572 +/**
2573 + * Parse a sitemap index to get sub-sitemaps
2574 + * Optimized: doesn't fetch URL count for each sub-sitemap (too slow)
2575 + */
2576 +private function parse_sitemap_index($url) {
2577 + $sub_sitemaps = array();
2578 +
2579 + $response = wp_remote_get($url, array(
2580 + 'timeout' => 5,
2581 + 'sslverify' => false
2582 + ));
2583 +
2584 + if (is_wp_error($response)) {
2585 + return $sub_sitemaps;
2586 + }
2587 +
2588 + $body = wp_remote_retrieve_body($response);
2589 + if (empty($body)) {
2590 + return $sub_sitemaps;
2591 + }
2592 +
2593 + // Suppress XML errors
2594 + libxml_use_internal_errors(true);
2595 + $xml = simplexml_load_string($body);
2596 + libxml_clear_errors();
2597 +
2598 + if ($xml === false) {
2599 + return $sub_sitemaps;
2600 + }
2601 +
2602 + // Check if it's a sitemap index (contains <sitemap> elements)
2603 + if (isset($xml->sitemap)) {
2604 + foreach ($xml->sitemap as $sitemap) {
2605 + $loc = (string) $sitemap->loc;
2606 + if (!empty($loc)) {
2607 + // Try to determine the type from the URL
2608 + $type = 'content';
2609 + if (strpos($loc, 'category') !== false || strpos($loc, 'tag') !== false || strpos($loc, 'taxonomy') !== false) {
2610 + $type = 'taxonomy';
2611 + } elseif (strpos($loc, 'author') !== false || strpos($loc, 'user') !== false) {
2612 + $type = 'author';
2613 + }
2614 +
2615 + // Skip URL count - too slow to fetch for each sitemap
2616 + $sub_sitemaps[] = array(
2617 + 'url' => $loc,
2618 + 'type' => $type,
2619 + 'url_count' => 0, // Don't fetch - takes too long
2620 + 'name' => basename(parse_url($loc, PHP_URL_PATH))
2621 + );
2622 + }
2623 + }
2624 + }
2625 +
2626 + return $sub_sitemaps;
2627 +}
2628 +
2629 +/**
2630 + * Get URL count from a sitemap
2631 + */
2632 +private function get_sitemap_url_count($url) {
2633 + $response = wp_remote_get($url, array(
2634 + 'timeout' => 10,
2635 + 'sslverify' => false
2636 + ));
2637 +
2638 + if (is_wp_error($response)) {
2639 + return 0;
2640 + }
2641 +
2642 + $body = wp_remote_retrieve_body($response);
2643 + if (empty($body)) {
2644 + return 0;
2645 + }
2646 +
2647 + // Count <url> or <loc> elements
2648 + $count = preg_match_all('/<url>/i', $body, $matches);
2649 + return $count ?: 0;
2650 +}
2651 +
2652 +/**
2653 + * Get sitemaps declared in robots.txt
2654 + */
2655 +private function get_sitemaps_from_robots($site_url) {
2656 + $sitemaps = array();
2657 + $robots_url = trailingslashit($site_url) . 'robots.txt';
2658 +
2659 + $response = wp_remote_get($robots_url, array(
2660 + 'timeout' => 5,
2661 + 'sslverify' => false
2662 + ));
2663 +
2664 + if (is_wp_error($response)) {
2665 + return $sitemaps;
2666 + }
2667 +
2668 + $body = wp_remote_retrieve_body($response);
2669 + if (empty($body)) {
2670 + return $sitemaps;
2671 + }
2672 +
2673 + // Find Sitemap: declarations
2674 + if (preg_match_all('/^Sitemap:\s*(.+)$/mi', $body, $matches)) {
2675 + foreach ($matches[1] as $sitemap_url) {
2676 + $sitemap_url = trim($sitemap_url);
2677 + if (filter_var($sitemap_url, FILTER_VALIDATE_URL)) {
2678 + $sitemaps[] = $sitemap_url;
2679 + }
2680 + }
2681 + }
2682 +
2683 + return $sitemaps;
2684 +}
2685 +
2686 +public function mxchat_stop_processing() {
2687 + // Verify permissions
2688 + if (!current_user_can('manage_options')) {
2689 + wp_die(esc_html__('Unauthorized access', 'mxchat'));
2690 + }
2691 +
2692 + // Verify nonce
2693 + check_admin_referer('mxchat_stop_processing_action', 'mxchat_stop_processing_nonce');
2694 +
2695 + global $wpdb;
2696 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
2697 +
2698 + // Get active queue IDs
2699 + $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
2700 + $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
2701 +
2702 + // Delete all pending items from active queues
2703 + if ($sitemap_queue_id) {
2704 + $wpdb->delete(
2705 + $table_name,
2706 + array(
2707 + 'queue_id' => $sitemap_queue_id,
2708 + 'status' => 'pending'
2709 + ),
2710 + array('%s', '%s')
2711 + );
2712 +
2713 + delete_transient('mxchat_active_queue_sitemap');
2714 + delete_transient('mxchat_last_sitemap_url');
2715 + }
2716 +
2717 + if ($pdf_queue_id) {
2718 + // Get PDF path before deleting
2719 + $pdf_path = $this->mxchat_get_queue_meta($pdf_queue_id, 'pdf_path');
2720 +
2721 + $wpdb->delete(
2722 + $table_name,
2723 + array(
2724 + 'queue_id' => $pdf_queue_id,
2725 + 'status' => 'pending'
2726 + ),
2727 + array('%s', '%s')
2728 + );
2729 +
2730 + // Delete PDF file
2731 + if ($pdf_path && file_exists($pdf_path)) {
2732 + wp_delete_file($pdf_path);
2733 + }
2734 +
2735 + delete_transient('mxchat_active_queue_pdf');
2736 + delete_transient('mxchat_last_pdf_url');
2737 + }
2738 +
2739 + // Redirect back with a success message
2740 + set_transient('mxchat_admin_notice_success',
2741 + esc_html__('Processing has been stopped successfully.', 'mxchat'),
2742 + 30
2743 + );
2744 + wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
2745 + exit;
2746 +}
2747 +
2748 +/**
2749 + * Get content list for processing
2750 + */
2751 +public function ajax_mxchat_get_content_list() {
2752 + // Verify the nonce
2753 + check_ajax_referer('mxchat_content_selector_nonce', 'nonce');
2754 +
2755 + if (!current_user_can('manage_options')) {
2756 + wp_send_json_error(__('Unauthorized access', 'mxchat'));
2757 + }
2758 +
2759 + $page = isset($_GET['page']) ? absint($_GET['page']) : 1;
2760 + $per_page = isset($_GET['per_page']) ? absint($_GET['per_page']) : 100;
2761 + $search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
2762 + $post_type = isset($_GET['post_type']) ? sanitize_text_field($_GET['post_type']) : 'all';
2763 + $post_status = isset($_GET['post_status']) ? sanitize_text_field($_GET['post_status']) : 'publish';
2764 + $processed_filter = isset($_GET['processed_filter']) ? sanitize_text_field($_GET['processed_filter']) : 'all';
2765 +
2766 + // Build query args
2767 + $args = array(
2768 + 'posts_per_page' => $per_page,
2769 + 'paged' => $page,
2770 + 'post_status' => $post_status !== 'all' ? $post_status : array('publish', 'draft', 'pending'),
2771 + 'orderby' => 'date',
2772 + 'order' => 'DESC',
2773 + );
2774 +
2775 + // Handle post types - IMPROVED VERSION
2776 + if ($post_type !== 'all') {
2777 + $args['post_type'] = $post_type;
2778 + } else {
2779 + // Get all available post types that might contain content
2780 + $all_post_types = array();
2781 +
2782 + // First get all public post types
2783 + $public_types = get_post_types(array('public' => true), 'names');
2784 + $all_post_types = array_merge($all_post_types, $public_types);
2785 +
2786 + // Add common forum/community post types
2787 + $forum_types = array('topic', 'reply', 'forum', 'wpforo_topic', 'wpforo_post');
2788 + foreach ($forum_types as $forum_type) {
2789 + if (post_type_exists($forum_type)) {
2790 + $all_post_types[] = $forum_type;
2791 + }
2792 + }
2793 +
2794 + // Add other commonly used post types
2795 + $common_types = array('product', 'job_listing', 'event', 'portfolio');
2796 + foreach ($common_types as $common_type) {
2797 + if (post_type_exists($common_type)) {
2798 + $all_post_types[] = $common_type;
2799 + }
2800 + }
2801 +
2802 + // Remove duplicates and ensure we have at least some post types
2803 + $all_post_types = array_unique($all_post_types);
2804 +
2805 + if (empty($all_post_types)) {
2806 + // Fallback to basic post types
2807 + $all_post_types = array('post', 'page');
2808 + }
2809 +
2810 + $args['post_type'] = $all_post_types;
2811 +
2812 + // Debug logging to see what post types are being queried
2813 + //error_log('MxChat Debug: Querying post types: ' . implode(', ', $all_post_types));
2814 + }
2815 +
2816 + if (!empty($search)) {
2817 + $args['s'] = $search;
2818 + }
2819 +
2820 + // Get processed data from storage
2821 + $processed_data = array();
2822 +
2823 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
2824 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2825 +
2826 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
2827 + // Get fresh data from Pinecone - no caching
2828 + $processed_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
2829 + } else {
2830 + // WordPress DB checking with better URL matching for all post types
2831 + global $wpdb;
2832 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2833 + $processed_items = $wpdb->get_results("SELECT id, source_url, article_content, timestamp FROM {$table_name}");
2834 +
2835 + // Group items by source_url to count chunks
2836 + $url_chunk_counts = array();
2837 + $url_latest_timestamp = array();
2838 + $url_first_id = array();
2839 +
2840 + if (!empty($processed_items)) {
2841 + foreach ($processed_items as $item) {
2842 + $url = $item->source_url;
2843 + if (empty($url)) continue;
2844 +
2845 + // Count chunks per URL
2846 + if (!isset($url_chunk_counts[$url])) {
2847 + $url_chunk_counts[$url] = 0;
2848 + $url_latest_timestamp[$url] = $item->timestamp;
2849 + $url_first_id[$url] = $item->id;
2850 + }
2851 + $url_chunk_counts[$url]++;
2852 +
2853 + // Track latest timestamp
2854 + if (strtotime($item->timestamp) > strtotime($url_latest_timestamp[$url])) {
2855 + $url_latest_timestamp[$url] = $item->timestamp;
2856 + }
2857 + }
2858 +
2859 + // Now build processed_data with chunk counts
2860 + foreach ($url_chunk_counts as $url => $chunk_count) {
2861 + $post_id = $this->mxchat_url_to_post_id_improved($url);
2862 +
2863 + if ($post_id) {
2864 + $processed_data[$post_id] = array(
2865 + 'db_id' => $url_first_id[$url],
2866 + 'timestamp' => $url_latest_timestamp[$url],
2867 + 'url' => $url,
2868 + 'source' => 'wordpress',
2869 + 'chunk_count' => $chunk_count
2870 + );
2871 + }
2872 + }
2873 + }
2874 + }
2875 +
2876 + // Get processed IDs as a simple array for in_array checks
2877 + $processed_ids = array_keys($processed_data);
2878 +
2879 + // Handle processed/unprocessed filter
2880 + if ($processed_filter === 'processed' && !empty($processed_ids)) {
2881 + $args['post__in'] = $processed_ids;
2882 + } elseif ($processed_filter === 'unprocessed' && !empty($processed_ids)) {
2883 + $args['post__not_in'] = $processed_ids;
2884 + }
2885 +
2886 + // Run the query
2887 + $query = new WP_Query($args);
2888 + $content_items = array();
2889 +
2890 + if ($query->have_posts()) {
2891 + while ($query->have_posts()) {
2892 + $query->the_post();
2893 + $id = get_the_ID();
2894 + $post_date = get_the_date();
2895 + $excerpt = wp_trim_words(get_the_excerpt(), 20, '...');
2896 + $word_count = str_word_count(strip_tags(get_the_content()));
2897 +
2898 + $is_processed = in_array($id, $processed_ids);
2899 + $processed_date = '';
2900 + $db_record_id = 0;
2901 + $data_source = 'none';
2902 +
2903 + if ($is_processed && isset($processed_data[$id])) {
2904 + $item_data = $processed_data[$id];
2905 + $data_source = $item_data['source'];
2906 +
2907 + if ($data_source === 'wordpress' && isset($item_data['timestamp'])) {
2908 + // WordPress DB format
2909 + $timestamp = strtotime($item_data['timestamp']);
2910 + $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2911 + $db_record_id = $item_data['db_id'];
2912 + } elseif ($data_source === 'pinecone') {
2913 + // Pinecone format
2914 + $processed_date = $item_data['processed_date'];
2915 + $db_record_id = $item_data['db_id'];
2916 + }
2917 + }
2918 +
2919 + // Get chunk count for this item
2920 + $chunk_count = 0;
2921 + if ($is_processed && isset($processed_data[$id]['chunk_count'])) {
2922 + $chunk_count = intval($processed_data[$id]['chunk_count']);
2923 + }
2924 +
2925 + $content_items[] = array(
2926 + 'id' => $id,
2927 + 'title' => get_the_title(),
2928 + 'permalink' => get_permalink(),
2929 + 'date' => $post_date,
2930 + 'type' => get_post_type(),
2931 + 'status' => get_post_status(),
2932 + 'excerpt' => $excerpt,
2933 + 'word_count' => $word_count,
2934 + 'already_processed' => $is_processed,
2935 + 'processed_date' => $processed_date,
2936 + 'db_record_id' => $db_record_id,
2937 + 'data_source' => $data_source,
2938 + 'chunk_count' => $chunk_count
2939 + );
2940 + }
2941 + wp_reset_postdata();
2942 + }
2943 +
2944 + $response = array(
2945 + 'items' => $content_items,
2946 + 'total' => $query->found_posts,
2947 + 'total_pages' => $query->max_num_pages,
2948 + 'current_page' => $page,
2949 + 'processed_count' => count($processed_ids)
2950 + );
2951 +
2952 + wp_send_json_success($response);
2953 + exit;
2954 +}
2955 +
2956 +
2957 +/**
2958 + * This function handles various WooCommerce URL formats and permalink structures
2959 + */
2960 +private function mxchat_url_to_post_id_improved($url) {
2961 + // First try the standard WordPress function
2962 + $post_id = url_to_postid($url);
2963 +
2964 + if ($post_id > 0) {
2965 + return $post_id;
2966 + }
2967 +
2968 + // If that fails, try more aggressive URL matching
2969 + // Remove trailing slashes and query parameters for better matching
2970 + $clean_url = rtrim($url, '/');
2971 + $clean_url = strtok($clean_url, '?'); // Remove query parameters
2972 +
2973 + // Try again with cleaned URL
2974 + $post_id = url_to_postid($clean_url);
2975 + if ($post_id > 0) {
2976 + return $post_id;
2977 + }
2978 +
2979 + // For bbPress forum topics, try extracting slug from URL
2980 + if (strpos($url, '/topic/') !== false || strpos($url, '/forums/') !== false) {
2981 + // Handle bbPress URLs: /forums/topic/topic-name/
2982 + if (preg_match('/\/forums\/topic\/([^\/\?]+)/', $url, $matches)) {
2983 + $topic_slug = $matches[1];
2984 +
2985 + // Look up topic by slug
2986 + $topic = get_page_by_path($topic_slug, OBJECT, 'topic');
2987 + if ($topic) {
2988 + return $topic->ID;
2989 + }
2990 +
2991 + // Alternative method: query by post_name
2992 + global $wpdb;
2993 + $post_id = $wpdb->get_var($wpdb->prepare(
2994 + "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
2995 + $topic_slug
2996 + ));
2997 +
2998 + if ($post_id) {
2999 + return intval($post_id);
3000 + }
3001 + }
3002 +
3003 + // Handle simpler topic URLs: /topic/topic-name/
3004 + if (preg_match('/\/topic\/([^\/\?]+)/', $url, $matches)) {
3005 + $topic_slug = $matches[1];
3006 +
3007 + global $wpdb;
3008 + $post_id = $wpdb->get_var($wpdb->prepare(
3009 + "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
3010 + $topic_slug
3011 + ));
3012 +
3013 + if ($post_id) {
3014 + return intval($post_id);
3015 + }
3016 + }
3017 + }
3018 +
3019 + // For WooCommerce products
3020 + if (strpos($url, '/product/') !== false || strpos($url, 'product=') !== false) {
3021 + // Extract product slug from various URL formats
3022 + $product_slug = '';
3023 +
3024 + // Handle pretty permalinks: /product/product-name/
3025 + if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
3026 + $product_slug = $matches[1];
3027 + }
3028 + // Handle query parameters: ?product=product-name
3029 + elseif (preg_match('/[\?&]product=([^&]+)/', $url, $matches)) {
3030 + $product_slug = $matches[1];
3031 + }
3032 +
3033 + if (!empty($product_slug)) {
3034 + // Look up product by slug
3035 + $product = get_page_by_path($product_slug, OBJECT, 'product');
3036 + if ($product) {
3037 + return $product->ID;
3038 + }
3039 +
3040 + // Alternative method: query by post_name
3041 + global $wpdb;
3042 + $post_id = $wpdb->get_var($wpdb->prepare(
3043 + "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'product' AND post_status = 'publish'",
3044 + $product_slug
3045 + ));
3046 +
3047 + if ($post_id) {
3048 + return intval($post_id);
3049 + }
3050 + }
3051 + }
3052 +
3053 + // Generic approach: try to extract slug and match against all post types
3054 + $parsed_url = wp_parse_url($clean_url);
3055 + $path = $parsed_url['path'] ?? '';
3056 +
3057 + if (!empty($path)) {
3058 + // Get the last part of the path as potential slug
3059 + $path_parts = array_filter(explode('/', trim($path, '/')));
3060 + $potential_slug = end($path_parts);
3061 +
3062 + if (!empty($potential_slug)) {
3063 + global $wpdb;
3064 +
3065 + // Try to find any post with this slug
3066 + $post_id = $wpdb->get_var($wpdb->prepare(
3067 + "SELECT ID FROM {$wpdb->posts}
3068 + WHERE post_name = %s
3069 + AND post_status IN ('publish', 'closed', 'private')
3070 + AND post_type NOT IN ('revision', 'attachment', 'nav_menu_item')
3071 + ORDER BY CASE
3072 + WHEN post_type = 'post' THEN 1
3073 + WHEN post_type = 'page' THEN 2
3074 + WHEN post_type = 'topic' THEN 3
3075 + WHEN post_type = 'product' THEN 4
3076 + ELSE 5
3077 + END
3078 + LIMIT 1",
3079 + $potential_slug
3080 + ));
3081 +
3082 + if ($post_id) {
3083 + return intval($post_id);
3084 + }
3085 + }
3086 + }
3087 +
3088 + // ADDITIONAL: Try direct database lookup by URL variations
3089 + global $wpdb;
3090 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3091 +
3092 + // Try variations of the URL (with/without trailing slash, http/https)
3093 + $url_variations = array(
3094 + $url,
3095 + rtrim($url, '/'),
3096 + $url . '/',
3097 + str_replace('http://', 'https://', $url),
3098 + str_replace('https://', 'http://', $url),
3099 + str_replace('http://', 'https://', rtrim($url, '/')),
3100 + str_replace('https://', 'http://', rtrim($url, '/'))
3101 + );
3102 +
3103 + // Remove duplicates
3104 + $url_variations = array_unique($url_variations);
3105 +
3106 + foreach ($url_variations as $variation) {
3107 + $existing_record = $wpdb->get_row($wpdb->prepare(
3108 + "SELECT id, source_url FROM $table_name WHERE source_url = %s",
3109 + $variation
3110 + ));
3111 +
3112 + if ($existing_record) {
3113 + // Try to get post ID from this stored URL
3114 + $stored_post_id = url_to_postid($existing_record->source_url);
3115 + if ($stored_post_id > 0) {
3116 + return $stored_post_id;
3117 + }
3118 + }
3119 + }
3120 +
3121 + return 0; // No match found
3122 +}
3123 +/**
3124 + * Process selected content via AJAX
3125 + */
3126 +public function ajax_mxchat_process_selected_content() {
3127 + // Basic request validation
3128 + if (!check_ajax_referer('mxchat_content_selector_nonce', 'nonce', false)) {
3129 + wp_send_json_error('Invalid nonce');
3130 + exit;
3131 + }
3132 +
3133 + if (!current_user_can('manage_options')) {
3134 + wp_send_json_error('Unauthorized access');
3135 + exit;
3136 + }
3137 +
3138 + // Get post IDs - safely parse the array
3139 + $post_ids = array();
3140 + if (isset($_POST['post_ids']) && is_array($_POST['post_ids'])) {
3141 + foreach ($_POST['post_ids'] as $id) {
3142 + $post_ids[] = absint($id);
3143 + }
3144 + }
3145 +
3146 + if (empty($post_ids)) {
3147 + wp_send_json_error('No content selected');
3148 + exit;
3149 + }
3150 +
3151 + // Get bot_id from request
3152 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
3153 +
3154 + // Process only ONE post at a time to avoid request size issues
3155 + $post_id = reset($post_ids);
3156 + $post = get_post($post_id);
3157 +
3158 + if (!$post) {
3159 + wp_send_json_error('Post not found');
3160 + exit;
3161 + }
3162 +
3163 + // Allow developers to modify post data before processing into knowledge base
3164 + $post = apply_filters('mxchat_before_process_post', $post, $bot_id);
3165 +
3166 + // Get content including title, short description (for WooCommerce), and main content
3167 + $content = $post->post_title . "\n\n";
3168 +
3169 + // Add short description if it exists (WooCommerce products use post_excerpt for short description)
3170 + if (!empty($post->post_excerpt)) {
3171 + // Remove shortcode tags but preserve content inside them
3172 + $clean_excerpt = $this->strip_shortcode_tags_preserve_content($post->post_excerpt);
3173 + $content .= "Short Description: " . wp_strip_all_tags($clean_excerpt) . "\n\n";
3174 + }
3175 +
3176 + // Add main content - remove shortcode tags but preserve content inside them
3177 + $clean_content = $this->strip_shortcode_tags_preserve_content($post->post_content);
3178 + $content .= wp_strip_all_tags($clean_content);
3179 +
3180 + // ADD WOOCOMMERCE PRODUCT DATA (pricing, stock, categories, custom tabs)
3181 + if (get_post_type($post_id) === 'product' && class_exists('WooCommerce')) {
3182 + $product = wc_get_product($post_id);
3183 +
3184 + if ($product) {
3185 + // Get pricing information
3186 + $regular_price = $product->get_regular_price();
3187 + $sale_price = $product->get_sale_price();
3188 + $price = $product->get_price();
3189 + $sku = $product->get_sku();
3190 +
3191 + // Get currency symbol
3192 + $currency_symbol = get_woocommerce_currency_symbol();
3193 +
3194 + // Add pricing information
3195 + $content .= "\n";
3196 + if (!empty($regular_price)) {
3197 + $content .= "Price: " . $currency_symbol . $regular_price . "\n";
3198 + } elseif (!empty($price)) {
3199 + $content .= "Price: " . $currency_symbol . $price . "\n";
3200 + }
3201 +
3202 + if (!empty($sale_price) && $sale_price !== $regular_price) {
3203 + $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
3204 + }
3205 +
3206 + // Handle variable products - show price range
3207 + if ($product->is_type('variable')) {
3208 + $min_price = $product->get_variation_price('min');
3209 + $max_price = $product->get_variation_price('max');
3210 + if ($min_price !== $max_price) {
3211 + $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
3212 + }
3213 + }
3214 +
3215 + if (!empty($sku)) {
3216 + $content .= "SKU: " . $sku . "\n";
3217 + }
3218 +
3219 + // Get product categories
3220 + $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
3221 + if (!empty($categories) && !is_wp_error($categories)) {
3222 + $content .= "Categories: " . implode(', ', $categories) . "\n";
3223 + }
3224 + }
3225 +
3226 + // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
3227 + $custom_tabs = get_post_meta($post_id, 'yikes_woo_products_tabs', true);
3228 + if (!empty($custom_tabs) && is_array($custom_tabs)) {
3229 + foreach ($custom_tabs as $tab) {
3230 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
3231 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
3232 +
3233 + if (!empty($tab_title) && !empty($tab_content)) {
3234 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
3235 + }
3236 + }
3237 + }
3238 +
3239 + // Also check for reusable/saved tabs applied to this product
3240 + $applied_saved_tabs = get_post_meta($post_id, 'yikes_woo_reusable_products_tabs_applied', true);
3241 + if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
3242 + $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
3243 + if (!empty($saved_tabs) && is_array($saved_tabs)) {
3244 + foreach ($applied_saved_tabs as $saved_tab_id) {
3245 + if (isset($saved_tabs[$saved_tab_id])) {
3246 + $tab = $saved_tabs[$saved_tab_id];
3247 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
3248 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
3249 +
3250 + if (!empty($tab_title) && !empty($tab_content)) {
3251 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
3252 + }
3253 + }
3254 + }
3255 + }
3256 + }
3257 + }
3258 +
3259 + // ADD ACF FIELDS SUPPORT
3260 + $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
3261 + if (!empty($acf_fields)) {
3262 + $acf_content_parts = array();
3263 +
3264 + foreach ($acf_fields as $field_name => $field_value) {
3265 + $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
3266 +
3267 + if (!empty($formatted_value)) {
3268 + $field_label = ucwords(str_replace('_', ' ', $field_name));
3269 + $acf_content_parts[] = $field_label . ": " . $formatted_value;
3270 + }
3271 + }
3272 +
3273 + if (!empty($acf_content_parts)) {
3274 + $content .= "\n\n" . implode("\n", $acf_content_parts);
3275 + }
3276 + }
3277 +
3278 + // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
3279 + $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
3280 + if (!empty($custom_meta)) {
3281 + $meta_content_parts = array();
3282 +
3283 + foreach ($custom_meta as $meta_key => $meta_value) {
3284 + // Convert meta key to readable label
3285 + $meta_label = ucwords(str_replace(array('_', '-'), ' ', $meta_key));
3286 + $meta_content_parts[] = $meta_label . ": " . $meta_value;
3287 + }
3288 +
3289 + if (!empty($meta_content_parts)) {
3290 + $content .= "\n\n" . implode("\n", $meta_content_parts);
3291 + }
3292 + }
3293 +
3294 + // Debug logging for WordPress Import content
3295 + error_log('[MXCHAT-WP-IMPORT-DEBUG] Post ID: ' . $post_id . ' Title: ' . $post->post_title);
3296 + error_log('[MXCHAT-WP-IMPORT-DEBUG] Raw post_content length: ' . strlen($post->post_content));
3297 + error_log('[MXCHAT-WP-IMPORT-DEBUG] Final content length: ' . strlen($content));
3298 + error_log('[MXCHAT-WP-IMPORT-DEBUG] Content preview: ' . substr($content, 0, 300));
3299 +
3300 + // Note: Removed 10,000 char limit - chunking now handles large content properly
3301 +
3302 + // Get bot-specific API key
3303 + $bot_options = $this->get_bot_options($bot_id);
3304 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
3305 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3306 +
3307 + if (strpos($selected_model, 'voyage') === 0) {
3308 + $api_key = $options['voyage_api_key'] ?? '';
3309 + $provider_name = 'Voyage AI';
3310 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3311 + $api_key = $options['gemini_api_key'] ?? '';
3312 + $provider_name = 'Google Gemini';
3313 + } else {
3314 + $api_key = $options['api_key'] ?? '';
3315 + $provider_name = 'OpenAI';
3316 + }
3317 +
3318 + if (empty($api_key)) {
3319 + MxChat_Admin::mxchat_log_debug('api_error', $provider_name . ' API key not configured for knowledge processing');
3320 + wp_send_json_error($provider_name . ' API key not configured');
3321 + exit;
3322 + }
3323 +
3324 + $source_url = get_permalink($post_id);
3325 + $vector_id = md5($source_url); // Vector ID for Pinecone
3326 +
3327 + // Check for existing content in bot-specific storage
3328 + $is_update = false;
3329 +
3330 + // Get bot-specific Pinecone configuration
3331 + $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
3332 + $use_pinecone = !empty($bot_pinecone_config) && ($bot_pinecone_config['use_pinecone'] ?? false);
3333 +
3334 + if ($use_pinecone && !empty($bot_pinecone_config['api_key'])) {
3335 + // Check Pinecone for this bot
3336 + $pinecone_data = $this->mxchat_get_pinecone_processed_content($bot_pinecone_config);
3337 + if (isset($pinecone_data[$post_id])) {
3338 + $is_update = true;
3339 + }
3340 + } else {
3341 + // Check WordPress DB (same as before since it's shared)
3342 + global $wpdb;
3343 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3344 + $existing_record = $wpdb->get_row($wpdb->prepare(
3345 + "SELECT id FROM $table_name WHERE source_url = %s",
3346 + $source_url
3347 + ));
3348 +
3349 + if ($existing_record) {
3350 + $is_update = true;
3351 + }
3352 + }
3353 +
3354 + // UPDATED 2.5.6: Determine content type based on post_type
3355 + $post_type = $post->post_type;
3356 + $content_type = 'content'; // Default fallback
3357 +
3358 + // Map WordPress post types to content types
3359 + switch ($post_type) {
3360 + case 'post':
3361 + $content_type = 'post';
3362 + break;
3363 + case 'page':
3364 + $content_type = 'page';
3365 + break;
3366 + case 'product':
3367 + $content_type = 'product';
3368 + break;
3369 + default:
3370 + // For custom post types, use the post type name
3371 + $content_type = sanitize_key($post_type);
3372 + break;
3373 + }
3374 +
3375 + // Use the centralized utility function with bot_id and content_type
3376 + $result = MxChat_Utils::submit_content_to_db(
3377 + $content,
3378 + $source_url,
3379 + $api_key,
3380 + $vector_id,
3381 + $bot_id,
3382 + $content_type
3383 + );
3384 +
3385 + if (is_wp_error($result)) {
3386 + MxChat_Admin::mxchat_log_debug('storage_error', 'Knowledge storage failed: ' . $result->get_error_message(), array('source_url' => $source_url));
3387 + wp_send_json_error('Storage failed: ' . $result->get_error_message());
3388 + exit;
3389 + }
3390 +
3391 + // Automatically apply role restriction based on tags
3392 + $this->apply_role_restriction_to_post($post_id, $source_url);
3393 +
3394 + $operation_type = $is_update ? 'update' : 'new';
3395 +
3396 + // Count ACF fields for debugging
3397 + $acf_field_count = count($acf_fields);
3398 +
3399 + // Success response with minimal data
3400 + wp_send_json_success(array(
3401 + 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully',
3402 + 'post_id' => $post_id,
3403 + 'title' => $post->post_title,
3404 + 'operation_type' => $operation_type,
3405 + 'vector_id' => $vector_id,
3406 + 'acf_fields_found' => $acf_field_count,
3407 + 'content_preview' => substr($content, 0, 100) . '...',
3408 + 'bot_id' => $bot_id
3409 + ));
3410 + exit;
3411 +}
3412 +
3413 +private function apply_role_restriction_to_post($post_id, $source_url) {
3414 + // Get tag-role mappings
3415 + $mappings = get_option('mxchat_tag_role_mappings', array());
3416 +
3417 + if (empty($mappings)) {
3418 + return; // No mappings, leave as public
3419 + }
3420 +
3421 + // Get all tags for the post
3422 + $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
3423 +
3424 + if (empty($post_tags)) {
3425 + return; // No tags, leave as public
3426 + }
3427 +
3428 + // Determine the highest role restriction based on tags
3429 + $highest_role = 'public';
3430 + $role_hierarchy = array(
3431 + 'public' => 0,
3432 + 'logged_in' => 1,
3433 + 'subscriber' => 2,
3434 + 'contributor' => 3,
3435 + 'author' => 4,
3436 + 'editor' => 5,
3437 + 'administrator' => 6
3438 + );
3439 +
3440 + foreach ($post_tags as $tag_slug) {
3441 + if (isset($mappings[$tag_slug])) {
3442 + $role = $mappings[$tag_slug];
3443 + if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
3444 + $highest_role = $role;
3445 + }
3446 + }
3447 + }
3448 +
3449 + // If no restricted tags found, return (leave as public)
3450 + if ($highest_role === 'public') {
3451 + return;
3452 + }
3453 +
3454 + // Update the role restriction in the database
3455 + global $wpdb;
3456 +
3457 + // Check if using Pinecone
3458 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3459 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3460 +
3461 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3462 + // Update Pinecone role restriction
3463 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
3464 + $vector_id = md5($source_url);
3465 +
3466 + $wpdb->replace(
3467 + $roles_table,
3468 + array(
3469 + 'vector_id' => $vector_id,
3470 + 'role_restriction' => $highest_role,
3471 + 'updated_at' => current_time('mysql')
3472 + ),
3473 + array('%s', '%s', '%s')
3474 + );
3475 + } else {
3476 + // Update WordPress DB
3477 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3478 +
3479 + $wpdb->update(
3480 + $table_name,
3481 + array('role_restriction' => $highest_role),
3482 + array('source_url' => $source_url),
3483 + array('%s'),
3484 + array('%s')
3485 + );
3486 + }
3487 +}
3488 +
3489 +public function mxchat_get_public_post_types() {
3490 + // Get all public post types
3491 + $post_types = get_post_types(array('public' => true), 'objects');
3492 + $post_type_options = array();
3493 +
3494 + foreach ($post_types as $post_type) {
3495 + $post_type_options[$post_type->name] = $post_type->label;
3496 + }
3497 +
3498 + // Also include common forum/community post types that might not be marked as public
3499 + $additional_types = array(
3500 + 'topic' => 'Forum Topics (bbPress)',
3501 + 'reply' => 'Forum Replies (bbPress)',
3502 + 'forum' => 'Forums (bbPress)',
3503 + 'wpforo_topic' => 'wpForo Topics',
3504 + 'wpforo_post' => 'wpForo Posts'
3505 + );
3506 +
3507 + foreach ($additional_types as $type_name => $type_label) {
3508 + if (post_type_exists($type_name) && !isset($post_type_options[$type_name])) {
3509 + $post_type_options[$type_name] = $type_label;
3510 + }
3511 + }
3512 +
3513 + return $post_type_options;
3514 +}
3515 +
3516 +/**
3517 + * Retrieves processed content from Pinecone API
3518 + */
3519 +public function mxchat_get_pinecone_processed_content($pinecone_options) {
3520 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3521 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3522 +
3523 + if (empty($api_key) || empty($host)) {
3524 + return array();
3525 + }
3526 +
3527 + $pinecone_data = array();
3528 +
3529 + try {
3530 + // Always get fresh data from Pinecone
3531 + $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
3532 +
3533 + // Method 2: Final fallback - try stats endpoint (if available)
3534 + if (empty($pinecone_data)) {
3535 + $stats_url = "https://{$host}/describe_index_stats";
3536 +
3537 + $response = wp_remote_post($stats_url, array(
3538 + 'headers' => array(
3539 + 'Api-Key' => $api_key,
3540 + 'Content-Type' => 'application/json'
3541 + ),
3542 + 'body' => json_encode(array()),
3543 + 'timeout' => 30
3544 + ));
3545 +
3546 + if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
3547 + $body = wp_remote_retrieve_body($response);
3548 + $stats_data = json_decode($body, true);
3549 + }
3550 + }
3551 +
3552 + } catch (Exception $e) {
3553 + // Log error but return fresh data only
3554 + }
3555 +
3556 + return $pinecone_data;
3557 +}
3558 +public function mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
3559 + //error_log('=== DEBUG: Starting mxchat_fetch_pinecone_vectors_by_ids ===');
3560 +
3561 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3562 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3563 +
3564 + if (empty($api_key) || empty($host) || empty($vector_ids)) {
3565 + //error_log('DEBUG: Missing parameters for fetch by IDs');
3566 + return array();
3567 + }
3568 +
3569 + try {
3570 + $fetch_url = "https://{$host}/vectors/fetch";
3571 + //error_log('DEBUG: Fetch URL: ' . $fetch_url);
3572 + //error_log('DEBUG: Fetching ' . count($vector_ids) . ' vector IDs');
3573 +
3574 + // Pinecone fetch API allows fetching specific vectors by ID
3575 + $fetch_data = array(
3576 + 'ids' => array_values($vector_ids)
3577 + );
3578 +
3579 + $response = wp_remote_post($fetch_url, array(
3580 + 'headers' => array(
3581 + 'Api-Key' => $api_key,
3582 + 'Content-Type' => 'application/json'
3583 + ),
3584 + 'body' => json_encode($fetch_data),
3585 + 'timeout' => 30
3586 + ));
3587 +
3588 + if (is_wp_error($response)) {
3589 + //error_log('DEBUG: Fetch by IDs WP error: ' . $response->get_error_message());
3590 + return array();
3591 + }
3592 +
3593 + $response_code = wp_remote_retrieve_response_code($response);
3594 + //error_log('DEBUG: Fetch response code: ' . $response_code);
3595 +
3596 + if ($response_code !== 200) {
3597 + $error_body = wp_remote_retrieve_body($response);
3598 + //error_log('DEBUG: Fetch failed with body: ' . $error_body);
3599 + return array();
3600 + }
3601 +
3602 + $body = wp_remote_retrieve_body($response);
3603 + $data = json_decode($body, true);
3604 +
3605 + //error_log('DEBUG: Fetch response structure: ' . print_r(array_keys($data), true));
3606 +
3607 + if (!isset($data['vectors'])) {
3608 + //error_log('DEBUG: No vectors key in response');
3609 + return array();
3610 + }
3611 +
3612 + $processed_data = array();
3613 +
3614 + foreach ($data['vectors'] as $vector_id => $vector_data) {
3615 + $metadata = $vector_data['metadata'] ?? array();
3616 + $source_url = $metadata['source_url'] ?? '';
3617 +
3618 + if (!empty($source_url)) {
3619 + $post_id = url_to_postid($source_url);
3620 + if ($post_id) {
3621 + $created_at = $metadata['created_at'] ?? '';
3622 + $processed_date = 'Recently';
3623 +
3624 + if (!empty($created_at)) {
3625 + $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
3626 + if ($timestamp) {
3627 + $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
3628 + }
3629 + }
3630 +
3631 + $processed_data[$post_id] = array(
3632 + 'db_id' => $vector_id,
3633 + 'processed_date' => $processed_date,
3634 + 'url' => $source_url,
3635 + 'source' => 'pinecone',
3636 + 'timestamp' => $timestamp ?? current_time('timestamp')
3637 + );
3638 + }
3639 + }
3640 + }
3641 +
3642 + //error_log('DEBUG: Processed ' . count($processed_data) . ' vectors from fetch');
3643 + return $processed_data;
3644 +
3645 + } catch (Exception $e) {
3646 + //error_log('DEBUG: Exception in fetch_pinecone_vectors_by_ids: ' . $e->getMessage());
3647 + return array();
3648 + }
3649 +}
3650 +
3651 +/**
3652 + * Get embedding dimensions based on the selected model.
3653 + */
3654 +private function mxchat_get_embedding_dimensions() {
3655 + $options = get_option('mxchat_options', array());
3656 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3657 +
3658 + $model_dimensions = array(
3659 + 'text-embedding-ada-002' => 1536,
3660 + 'text-embedding-3-small' => 1536,
3661 + 'text-embedding-3-large' => 3072,
3662 + 'voyage-2' => 1024,
3663 + 'voyage-large-2' => 1536,
3664 + 'voyage-3-large' => 2048,
3665 + 'gemini-embedding-001' => 1536,
3666 + );
3667 +
3668 + if (strpos($selected_model, 'voyage-3-large') === 0) {
3669 + $custom_dimensions = $options['voyage_output_dimension'] ?? 2048;
3670 + return intval($custom_dimensions);
3671 + }
3672 +
3673 + if (strpos($selected_model, 'gemini-embedding') === 0) {
3674 + $custom_dimensions = $options['gemini_output_dimension'] ?? 1536;
3675 + return intval($custom_dimensions);
3676 + }
3677 +
3678 + return $model_dimensions[$selected_model] ?? 1536;
3679 +}
3680 +
3681 +/**
3682 + * Scan Pinecone for processed content
3683 + */
3684 +public function mxchat_scan_pinecone_for_processed_content($pinecone_options) {
3685 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3686 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3687 +
3688 + if (empty($api_key) || empty($host)) {
3689 + return array();
3690 + }
3691 +
3692 + try {
3693 + // Use multiple random vectors to get better coverage
3694 + $all_matches = array();
3695 + $seen_ids = array();
3696 +
3697 + // Get correct dimensions for the configured embedding model
3698 + $dimensions = $this->mxchat_get_embedding_dimensions();
3699 +
3700 + // Try 3 different random vectors to get better coverage
3701 + for ($i = 0; $i < 3; $i++) {
3702 + $query_url = "https://{$host}/query";
3703 +
3704 + // Generate a random unit vector instead of zeros
3705 + $random_vector = array();
3706 + for ($j = 0; $j < $dimensions; $j++) {
3707 + $random_vector[] = (rand(-1000, 1000) / 1000.0);
3708 + }
3709 +
3710 + // Normalize the vector to unit length
3711 + $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
3712 + if ($magnitude > 0) {
3713 + $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
3714 + }
3715 +
3716 + $query_data = array(
3717 + 'includeMetadata' => true,
3718 + 'includeValues' => false,
3719 + 'topK' => 10000,
3720 + 'vector' => $random_vector
3721 + );
3722 +
3723 + $response = wp_remote_post($query_url, array(
3724 + 'headers' => array(
3725 + 'Api-Key' => $api_key,
3726 + 'Content-Type' => 'application/json'
3727 + ),
3728 + 'body' => json_encode($query_data),
3729 + 'timeout' => 30
3730 + ));
3731 +
3732 + if (is_wp_error($response)) {
3733 + continue;
3734 + }
3735 +
3736 + $response_code = wp_remote_retrieve_response_code($response);
3737 +
3738 + if ($response_code !== 200) {
3739 + continue;
3740 + }
3741 +
3742 + $body = wp_remote_retrieve_body($response);
3743 + $data = json_decode($body, true);
3744 +
3745 + if (isset($data['matches'])) {
3746 + foreach ($data['matches'] as $match) {
3747 + $match_id = $match['id'] ?? '';
3748 + if (!empty($match_id) && !isset($seen_ids[$match_id])) {
3749 + $all_matches[] = $match;
3750 + $seen_ids[$match_id] = true;
3751 + }
3752 + }
3753 + }
3754 + }
3755 +
3756 + // Convert matches to processed data format, grouping by URL to count chunks
3757 + $processed_data = array();
3758 + $url_chunk_counts = array();
3759 +
3760 + foreach ($all_matches as $match) {
3761 + $metadata = $match['metadata'] ?? array();
3762 + $source_url = $metadata['source_url'] ?? '';
3763 + $match_id = $match['id'] ?? '';
3764 +
3765 + if (!empty($source_url) && !empty($match_id)) {
3766 + $post_id = url_to_postid($source_url);
3767 + if ($post_id) {
3768 + // Count chunks per post_id
3769 + if (!isset($url_chunk_counts[$post_id])) {
3770 + $url_chunk_counts[$post_id] = 0;
3771 + }
3772 + $url_chunk_counts[$post_id]++;
3773 +
3774 + $created_at = $metadata['created_at'] ?? '';
3775 + $processed_date = 'Recently';
3776 +
3777 + if (!empty($created_at)) {
3778 + $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
3779 + if ($timestamp) {
3780 + $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
3781 + }
3782 + }
3783 +
3784 + // Only store if not already set, or update with newer timestamp
3785 + if (!isset($processed_data[$post_id]) ||
3786 + ($timestamp ?? 0) > ($processed_data[$post_id]['timestamp'] ?? 0)) {
3787 + $processed_data[$post_id] = array(
3788 + 'db_id' => $match_id,
3789 + 'processed_date' => $processed_date,
3790 + 'url' => $source_url,
3791 + 'source' => 'pinecone',
3792 + 'timestamp' => $timestamp ?? current_time('timestamp')
3793 + );
3794 + }
3795 + }
3796 + }
3797 + }
3798 +
3799 + // Add chunk counts to processed data
3800 + foreach ($url_chunk_counts as $post_id => $chunk_count) {
3801 + if (isset($processed_data[$post_id])) {
3802 + $processed_data[$post_id]['chunk_count'] = $chunk_count;
3803 + }
3804 + }
3805 +
3806 + return $processed_data;
3807 +
3808 + } catch (Exception $e) {
3809 + return array();
3810 + }
3811 +}
3812 +/**
3813 + * Generate embeddings from input text for MXChat with bot support
3814 + */
3815 +private function mxchat_generate_embedding($text, $bot_id = 'default') {
3816 + // Enable detailed logging for debugging
3817 + //error_log('[MXCHAT-EMBED] Starting embedding generation for bot: ' . $bot_id . '. Text length: ' . strlen($text) . ' bytes');
3818 + //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
3819 +
3820 + // Get bot-specific options
3821 + $bot_options = $this->get_bot_options($bot_id);
3822 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
3823 +
3824 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3825 + //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model);
3826 +
3827 + // Determine provider and endpoint
3828 + if (strpos($selected_model, 'voyage') === 0) {
3829 + $api_key = $options['voyage_api_key'] ?? '';
3830 + $endpoint = 'https://api.voyageai.com/v1/embeddings';
3831 + $provider_name = 'Voyage AI';
3832 + //error_log('[MXCHAT-EMBED] Using Voyage AI API for bot ' . $bot_id);
3833 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3834 + $api_key = $options['gemini_api_key'] ?? '';
3835 + $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
3836 + $provider_name = 'Google Gemini';
3837 + //error_log('[MXCHAT-EMBED] Using Google Gemini API for bot ' . $bot_id);
3838 + } else {
3839 + $api_key = $options['api_key'] ?? '';
3840 + $endpoint = 'https://api.openai.com/v1/embeddings';
3841 + $provider_name = 'OpenAI';
3842 + //error_log('[MXCHAT-EMBED] Using OpenAI API for bot ' . $bot_id);
3843 + }
3844 +
3845 + //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
3846 +
3847 + if (empty($api_key)) {
3848 + $error_message = sprintf('Missing %s API key for bot %s. Please configure your API key in the bot settings.', $provider_name, $bot_id);
3849 + //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
3850 + return $error_message;
3851 + }
3852 +
3853 + // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
3854 + $estimated_tokens = ceil(str_word_count($text) / 0.75);
3855 + //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
3856 +
3857 + if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
3858 + //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
3859 + // Consider truncating text here
3860 + }
3861 +
3862 + // Prepare request body based on provider
3863 + if (strpos($selected_model, 'gemini-embedding') === 0) {
3864 + // Gemini API format
3865 + $request_body = array(
3866 + 'model' => 'models/' . $selected_model,
3867 + 'content' => array(
3868 + 'parts' => array(
3869 + array('text' => $text)
3870 + )
3871 + )
3872 + );
3873 +
3874 + // Set output dimensionality to 1536 for consistency with other models
3875 + $request_body['outputDimensionality'] = 1536;
3876 + } else {
3877 + // OpenAI/Voyage API format
3878 + $request_body = array(
3879 + 'model' => $selected_model,
3880 + 'input' => $text
3881 + );
3882 +
3883 + // Add output_dimension for voyage-3-large model
3884 + if ($selected_model === 'voyage-3-large') {
3885 + $request_body['output_dimension'] = 2048;
3886 + }
3887 + }
3888 +
3889 + //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
3890 +
3891 + // Prepare headers based on provider
3892 + if (strpos($selected_model, 'gemini-embedding') === 0) {
3893 + // Gemini uses API key as query parameter
3894 + $endpoint .= '?key=' . $api_key;
3895 + $headers = array(
3896 + 'Content-Type' => 'application/json'
3897 + );
3898 + } else {
3899 + // OpenAI/Voyage use Bearer token
3900 + $headers = array(
3901 + 'Authorization' => 'Bearer ' . $api_key,
3902 + 'Content-Type' => 'application/json'
3903 + );
3904 + }
3905 +
3906 + // Make API request
3907 + //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
3908 + $response = wp_remote_post($endpoint, array(
3909 + 'body' => wp_json_encode($request_body),
3910 + 'headers' => $headers,
3911 + 'timeout' => 60 // Increased timeout for large inputs
3912 + ));
3913 +
3914 + // Handle wp_remote_post errors
3915 + if (is_wp_error($response)) {
3916 + $error_message = $response->get_error_message();
3917 + //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
3918 + return 'Connection error: ' . $error_message;
3919 + }
3920 +
3921 + // Get and check HTTP response code
3922 + $http_code = wp_remote_retrieve_response_code($response);
3923 + //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
3924 +
3925 + if ($http_code !== 200) {
3926 + $error_body = wp_remote_retrieve_body($response);
3927 + //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
3928 +
3929 + // Try to parse error for more details
3930 + $error_json = json_decode($error_body, true);
3931 + if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
3932 + $error_type = $error_json['error']['type'] ?? 'unknown';
3933 + $error_message = $error_json['error']['message'] ?? 'No message';
3934 + //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
3935 + //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
3936 +
3937 + // Customize error message for common API errors
3938 + if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
3939 + $error_message = sprintf('Invalid %s API key for bot %s. Please check your API key in the bot settings.', $provider_name, $bot_id);
3940 + } elseif ($error_type === 'authentication_error') {
3941 + $error_message = sprintf('%s authentication failed for bot %s. Please verify your API key in the bot settings.', $provider_name, $bot_id);
3942 + }
3943 +
3944 + //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
3945 + return $error_message;
3946 + }
3947 +
3948 + $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding for bot %s", $http_code, $bot_id);
3949 + //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
3950 + return $error_message;
3951 + }
3952 +
3953 + // Parse response body
3954 + $response_body = wp_remote_retrieve_body($response);
3955 + //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
3956 +
3957 + $response_data = json_decode($response_body, true);
3958 +
3959 + if (json_last_error() !== JSON_ERROR_NONE) {
3960 + $error = json_last_error_msg();
3961 + //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
3962 + //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
3963 + return "Failed to parse API response: $error";
3964 + }
3965 +
3966 + // Handle different response formats based on provider
3967 + if (strpos($selected_model, 'gemini-embedding') === 0) {
3968 + // Gemini API response format
3969 + if (isset($response_data['embedding']['values'])) {
3970 + $embedding_dimensions = count($response_data['embedding']['values']);
3971 + //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
3972 +
3973 + // Check if embedding dimensions are as expected (should be 1536)
3974 + if ($embedding_dimensions !== 1536) {
3975 + //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
3976 + }
3977 +
3978 + return $response_data['embedding']['values'];
3979 + } else {
3980 + //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
3981 + //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
3982 +
3983 + if (isset($response_data['error'])) {
3984 + $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
3985 + //error_log('[MXCHAT-EMBED] ' . $error_message);
3986 + return $error_message;
3987 + }
3988 +
3989 + $error_message = "Invalid Gemini API response format: No embedding found";
3990 + //error_log('[MXCHAT-EMBED] ' . $error_message);
3991 + return $error_message;
3992 + }
3993 + } else {
3994 + // OpenAI/Voyage API response format
3995 + if (isset($response_data['data'][0]['embedding'])) {
3996 + $embedding_dimensions = count($response_data['data'][0]['embedding']);
3997 + //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
3998 +
3999 + // Check if embedding dimensions are as expected
4000 + if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
4001 + ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
4002 + //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
4003 + }
4004 +
4005 + return $response_data['data'][0]['embedding'];
4006 + } else {
4007 + //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
4008 + //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
4009 +
4010 + if (isset($response_data['error'])) {
4011 + $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
4012 + //error_log('[MXCHAT-EMBED] ' . $error_message);
4013 + return $error_message;
4014 + }
4015 +
4016 + $error_message = "Invalid API response format: No embedding found";
4017 + //error_log('[MXCHAT-EMBED] ' . $error_message);
4018 + return $error_message;
4019 + }
4020 + }
4021 +}
4022 +
4023 +/**
4024 + * Get bot-specific options for multi-bot functionality
4025 + * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
4026 + */
4027 +private function get_bot_options($bot_id = 'default') {
4028 + //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
4029 +
4030 + if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
4031 + //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
4032 + return array();
4033 + }
4034 +
4035 + $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
4036 +
4037 + if (!empty($bot_options)) {
4038 + //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
4039 + if (isset($bot_options['similarity_threshold'])) {
4040 + //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
4041 + }
4042 + }
4043 +
4044 + return is_array($bot_options) ? $bot_options : array();
4045 +}
4046 +
4047 +/**
4048 + * Get bot-specific Pinecone configuration
4049 + * Used in the knowledge retrieval functions
4050 + */
4051 +// Also add debugging to your get_bot_pinecone_config function
4052 +private function get_bot_pinecone_config($bot_id = 'default') {
4053 + //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
4054 +
4055 + // If default bot or multi-bot add-on not active, use default Pinecone config
4056 + if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
4057 + //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
4058 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
4059 + $config = array(
4060 + 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
4061 + 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
4062 + 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
4063 + 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
4064 + );
4065 + //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
4066 + return $config;
4067 + }
4068 +
4069 + //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
4070 +
4071 + // Hook for multi-bot add-on to provide bot-specific Pinecone config
4072 + $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
4073 +
4074 + if (!empty($bot_pinecone_config)) {
4075 + //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
4076 + //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
4077 + //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
4078 + //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
4079 + } else {
4080 + //error_log("MXCHAT DEBUG: Filter returned empty config!");
4081 + }
4082 +
4083 + return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
4084 +}
4085 +
4086 +
4087 +public function mxchat_ajax_dismiss_completed_status() {
4088 + try {
4089 + // Verify the request
4090 + check_ajax_referer('mxchat_status_nonce', 'nonce');
4091 +
4092 + if (!current_user_can('manage_options')) {
4093 + wp_send_json_error('Unauthorized access');
4094 + exit;
4095 + }
4096 +
4097 + $card_type = isset($_POST['card_type']) ? sanitize_text_field($_POST['card_type']) : '';
4098 +
4099 + if ($card_type === 'pdf') {
4100 + // Clear PDF status
4101 + $pdf_url = get_transient('mxchat_last_pdf_url');
4102 + if ($pdf_url) {
4103 + delete_transient('mxchat_pdf_status_' . md5($pdf_url));
4104 + delete_transient('mxchat_last_pdf_url');
4105 + }
4106 + } elseif ($card_type === 'sitemap') {
4107 + // Clear sitemap status
4108 + $sitemap_url = get_transient('mxchat_last_sitemap_url');
4109 + if ($sitemap_url) {
4110 + delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
4111 + delete_transient('mxchat_last_sitemap_url');
4112 + }
4113 + }
4114 +
4115 + wp_send_json_success(array('message' => 'Status dismissed successfully'));
4116 +
4117 + } catch (Exception $e) {
4118 + wp_send_json_error(array('message' => 'Error dismissing status: ' . $e->getMessage()));
4119 + }
4120 +}
4121 +
4122 +/**
4123 + * Render completed status cards on page load
4124 + * This ensures completed processing status persists through page refreshes
4125 + */
4126 +public function mxchat_render_completed_status_cards() {
4127 + $output = '';
4128 +
4129 + // Check for completed PDF status
4130 + $pdf_url = get_transient('mxchat_last_pdf_url');
4131 + if ($pdf_url) {
4132 + $pdf_status = $this->mxchat_get_pdf_processing_status($pdf_url);
4133 + if ($pdf_status && ($pdf_status['status'] === 'complete' || $pdf_status['status'] === 'error')) {
4134 + $output .= $this->mxchat_render_pdf_status_card($pdf_status, $pdf_url);
4135 + }
4136 + }
4137 +
4138 + // Check for completed sitemap status
4139 + $sitemap_url = get_transient('mxchat_last_sitemap_url');
4140 + if ($sitemap_url) {
4141 + $sitemap_status = $this->mxchat_get_sitemap_processing_status($sitemap_url);
4142 + if ($sitemap_status && ($sitemap_status['status'] === 'complete' || $sitemap_status['status'] === 'error')) {
4143 + $output .= $this->mxchat_render_sitemap_status_card($sitemap_status, $sitemap_url);
4144 + }
4145 + }
4146 +
4147 + return $output;
4148 +}
4149 +
4150 +/**
4151 + * Render PDF status card HTML
4152 + */
4153 +private function mxchat_render_pdf_status_card($status, $pdf_url) {
4154 + $html = '<div class="mxchat-status-card" data-card-type="pdf">';
4155 + $html .= '<div class="mxchat-status-header">';
4156 + $html .= '<h4>' . esc_html__('PDF Processing Status', 'mxchat') . '</h4>';
4157 +
4158 + // Add dismiss button for completed status
4159 + if ($status['status'] === 'complete' || $status['status'] === 'error') {
4160 + $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
4161 + }
4162 +
4163 + // Process Batch button for processing status
4164 + if ($status['status'] === 'processing') {
4165 + $html .= '<button type="button" class="mxchat-manual-batch-btn"
4166 + data-process-type="pdf"
4167 + data-url="' . esc_attr($pdf_url) . '">
4168 + ' . esc_html__('Process Batch', 'mxchat') . '</button>';
4169 + }
4170 +
4171 + // Add status badges
4172 + if ($status['status'] === 'error') {
4173 + $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
4174 + } elseif ($status['status'] === 'complete') {
4175 + if ($status['failed_pages'] > 0) {
4176 + $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
4177 + sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_pages']) . '</span>';
4178 + } else {
4179 + $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
4180 + }
4181 + }
4182 +
4183 + $html .= '</div>'; // End header
4184 +
4185 + // Progress bar
4186 + $html .= '<div class="mxchat-progress-bar">';
4187 + $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
4188 + $html .= '</div>';
4189 +
4190 + // Status details
4191 + $html .= '<div class="mxchat-status-details">';
4192 + $html .= '<p>' . sprintf(
4193 + esc_html__('Progress: %d of %d pages (%d%%)', 'mxchat'),
4194 + $status['processed_pages'],
4195 + $status['total_pages'],
4196 + $status['percentage']
4197 + ) . '</p>';
4198 +
4199 + // Show failed pages count if any
4200 + if ($status['failed_pages'] > 0) {
4201 + $html .= '<p><strong>' . esc_html__('Failed pages:', 'mxchat') . '</strong> ' . esc_html($status['failed_pages']) . '</p>';
4202 + }
4203 +
4204 + $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
4205 + $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
4206 +
4207 + // Add completion summary if available AND it's an array
4208 + if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
4209 + $summary = $status['completion_summary'];
4210 + $html .= '<div class="mxchat-completion-summary">';
4211 + $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
4212 + $html .= '<p><strong>' . esc_html__('Total Pages:', 'mxchat') . '</strong> ' . esc_html($summary['total_pages']) . '</p>';
4213 + $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_pages']) . '</p>';
4214 + $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_pages']) . '</p>';
4215 + $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
4216 + $html .= '</div>';
4217 + }
4218 +
4219 + // Add failed pages list if any AND it's an array
4220 + if (isset($status['failed_pages_list']) && is_array($status['failed_pages_list']) && !empty($status['failed_pages_list'])) {
4221 + $html .= $this->mxchat_render_failed_pages_list($status['failed_pages_list']);
4222 + }
4223 +
4224 + // Add error message if any
4225 + if (isset($status['error']) && !empty($status['error'])) {
4226 + $html .= '<div class="mxchat-error-notice">';
4227 + $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
4228 + $html .= '</div>';
4229 + }
4230 +
4231 + $html .= '</div>'; // End details
4232 + $html .= '</div>'; // End card
4233 +
4234 + return $html;
4235 +}
4236 +/**
4237 + * Render sitemap status card HTML
4238 + */
4239 +private function mxchat_render_sitemap_status_card($status, $sitemap_url) {
4240 + $html = '<div class="mxchat-status-card" data-card-type="sitemap">';
4241 + $html .= '<div class="mxchat-status-header">';
4242 + $html .= '<h4>' . esc_html__('Sitemap Processing Status', 'mxchat') . '</h4>';
4243 +
4244 + // Add dismiss button for completed status
4245 + if ($status['status'] === 'complete' || $status['status'] === 'error') {
4246 + $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
4247 + }
4248 +
4249 + // Process Batch button for processing status
4250 + if ($status['status'] === 'processing') {
4251 + $html .= '<button type="button" class="mxchat-manual-batch-btn"
4252 + data-process-type="sitemap"
4253 + data-url="' . esc_attr($sitemap_url) . '">
4254 + ' . esc_html__('Process Batch', 'mxchat') . '</button>';
4255 + }
4256 +
4257 + // Add status badges
4258 + if ($status['status'] === 'error') {
4259 + $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
4260 + } elseif ($status['status'] === 'complete') {
4261 + if ($status['failed_urls'] > 0) {
4262 + $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
4263 + sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_urls']) . '</span>';
4264 + } else {
4265 + $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
4266 + }
4267 + }
4268 +
4269 + $html .= '</div>'; // End header
4270 +
4271 + // Progress bar
4272 + $html .= '<div class="mxchat-progress-bar">';
4273 + $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
4274 + $html .= '</div>';
4275 +
4276 + // Status details
4277 + $html .= '<div class="mxchat-status-details">';
4278 + $html .= '<p>' . sprintf(
4279 + esc_html__('Progress: %d of %d URLs (%d%%)', 'mxchat'),
4280 + $status['processed_urls'],
4281 + $status['total_urls'],
4282 + $status['percentage']
4283 + ) . '</p>';
4284 +
4285 + // Show failed URLs count if any
4286 + if ($status['failed_urls'] > 0) {
4287 + $html .= '<p><strong>' . esc_html__('Failed URLs:', 'mxchat') . '</strong> ' . esc_html($status['failed_urls']) . '</p>';
4288 + }
4289 +
4290 + $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
4291 + $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
4292 +
4293 + // Add completion summary if available AND it's an array
4294 + if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
4295 + $summary = $status['completion_summary'];
4296 + $html .= '<div class="mxchat-completion-summary">';
4297 + $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
4298 + $html .= '<p><strong>' . esc_html__('Total URLs:', 'mxchat') . '</strong> ' . esc_html($summary['total_urls']) . '</p>';
4299 + $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_urls']) . '</p>';
4300 + $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_urls']) . '</p>';
4301 + $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
4302 + $html .= '</div>';
4303 + }
4304 +
4305 + // Add error messages if any (but not the failed URLs list)
4306 + if (!empty($status['error']) || !empty($status['last_error'])) {
4307 + $html .= '<div class="mxchat-error-notice">';
4308 +
4309 + if (!empty($status['error'])) {
4310 + $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
4311 + }
4312 +
4313 + if (!empty($status['last_error'])) {
4314 + $html .= '<p class="last-error">' . esc_html__('Last error:', 'mxchat') . ' ' . esc_html($status['last_error']) . '</p>';
4315 + }
4316 +
4317 + $html .= '</div>';
4318 + }
4319 +
4320 + $html .= '</div>'; // End details
4321 + $html .= '</div>'; // End card
4322 +
4323 + return $html;
4324 +}
4325 +
4326 +
4327 +/**
4328 + * Render failed pages list
4329 + */
4330 +private function mxchat_render_failed_pages_list($failed_pages_list) {
4331 + // Validate that $failed_pages_list is an array and not empty
4332 + if (!is_array($failed_pages_list) || empty($failed_pages_list)) {
4333 + return '';
4334 + }
4335 +
4336 + $html = '<div class="mxchat-error-notice">';
4337 + $html .= '<div class="mxchat-failed-pages-container">';
4338 + $html .= '<h5>' . sprintf(esc_html__('Failed Pages (%d)', 'mxchat'), count($failed_pages_list)) . '</h5>';
4339 + $html .= '<details>';
4340 + $html .= '<summary>' . esc_html__('Show Failed Pages', 'mxchat') . '</summary>';
4341 + $html .= '<div class="mxchat-failed-pages-list">';
4342 +
4343 + // Create table for failed pages
4344 + $html .= '<table class="widefat striped">';
4345 + $html .= '<thead><tr>';
4346 + $html .= '<th>' . esc_html__('Page', 'mxchat') . '</th>';
4347 + $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
4348 + $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
4349 + $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
4350 + $html .= '</tr></thead><tbody>';
4351 +
4352 + // Sort failed pages by most recent
4353 + $sorted_failed_pages = $failed_pages_list;
4354 + usort($sorted_failed_pages, function($a, $b) {
4355 + return ($b['time'] ?? 0) - ($a['time'] ?? 0);
4356 + });
4357 +
4358 + foreach ($sorted_failed_pages as $item) {
4359 + // Ensure $item is an array before accessing its elements
4360 + if (!is_array($item)) {
4361 + continue;
4362 + }
4363 +
4364 + $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
4365 + $html .= '<tr>';
4366 + $html .= '<td>' . esc_html__('Page', 'mxchat') . ' ' . esc_html($item['page'] ?? 'Unknown') . '</td>';
4367 + $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
4368 + $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
4369 + $html .= '<td>' . esc_html($time_ago) . '</td>';
4370 + $html .= '</tr>';
4371 + }
4372 +
4373 + $html .= '</tbody></table>';
4374 + $html .= '</div></details></div></div>';
4375 +
4376 + return $html;
4377 +}
4378 +
4379 +/**
4380 + * Render failed URLs list
4381 + */
4382 +private function mxchat_render_failed_urls_list($failed_urls_list) {
4383 + // Validate that $failed_urls_list is an array and not empty
4384 + if (!is_array($failed_urls_list) || empty($failed_urls_list)) {
4385 + return '';
4386 + }
4387 +
4388 + $html = '<div class="mxchat-failed-urls-container">';
4389 + $html .= '<h5>' . sprintf(esc_html__('Failed URLs (%d)', 'mxchat'), count($failed_urls_list)) . '</h5>';
4390 + $html .= '<details>';
4391 + $html .= '<summary>' . esc_html__('Show Failed URLs', 'mxchat') . '</summary>';
4392 + $html .= '<div class="mxchat-failed-urls-list">';
4393 +
4394 + // Create table for failed URLs
4395 + $html .= '<table class="widefat striped">';
4396 + $html .= '<thead><tr>';
4397 + $html .= '<th>' . esc_html__('URL', 'mxchat') . '</th>';
4398 + $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
4399 + $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
4400 + $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
4401 + $html .= '</tr></thead><tbody>';
4402 +
4403 + // Sort failed URLs by most recent
4404 + $sorted_failed_urls = $failed_urls_list;
4405 + usort($sorted_failed_urls, function($a, $b) {
4406 + return ($b['time'] ?? 0) - ($a['time'] ?? 0);
4407 + });
4408 +
4409 + // Show up to 50 failed URLs
4410 + $display_urls = array_slice($sorted_failed_urls, 0, 50);
4411 +
4412 + foreach ($display_urls as $item) {
4413 + // Ensure $item is an array before accessing its elements
4414 + if (!is_array($item)) {
4415 + continue;
4416 + }
4417 +
4418 + $url = $item['url'] ?? '';
4419 + $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
4420 +
4421 + // Truncate URL for display
4422 + $display_url = strlen($url) > 50 ? substr($url, 0, 47) . '...' : $url;
4423 +
4424 + $html .= '<tr>';
4425 + $html .= '<td style="word-break: break-all;">';
4426 + if (!empty($url)) {
4427 + $html .= '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($display_url) . '</a>';
4428 + } else {
4429 + $html .= esc_html__('Unknown URL', 'mxchat');
4430 + }
4431 + $html .= '</td>';
4432 + $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
4433 + $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
4434 + $html .= '<td>' . esc_html($time_ago) . '</td>';
4435 + $html .= '</tr>';
4436 + }
4437 +
4438 + $html .= '</tbody></table>';
4439 +
4440 + if (count($failed_urls_list) > 50) {
4441 + $html .= '<div class="mxchat-failed-urls-more">+ ' .
4442 + (count($failed_urls_list) - 50) .
4443 + ' ' . esc_html__('more failed URLs not shown', 'mxchat') . '</div>';
4444 + }
4445 +
4446 + $html .= '</div></details></div>';
4447 +
4448 + return $html;
4449 +}
4450 +
4451 +/**
4452 + * Get all ACF fields for a specific post, excluding any fields the user has disabled
4453 + */
4454 +public function mxchat_get_acf_fields_for_post($post_id) {
4455 + if (!function_exists('get_fields')) {
4456 + return array();
4457 + }
4458 +
4459 + $fields = get_fields($post_id);
4460 + if (!$fields || !is_array($fields)) {
4461 + return array();
4462 + }
4463 +
4464 + // Get excluded fields from settings
4465 + $excluded_fields = get_option('mxchat_acf_excluded_fields', array());
4466 + if (!empty($excluded_fields) && is_array($excluded_fields)) {
4467 + foreach ($excluded_fields as $excluded_field) {
4468 + if (isset($fields[$excluded_field])) {
4469 + unset($fields[$excluded_field]);
4470 + }
4471 + }
4472 + }
4473 +
4474 + return $fields;
4475 +}
4476 +
4477 +/**
4478 + * Get all registered ACF field groups and their fields for the settings UI
4479 + */
4480 +public function mxchat_get_all_acf_fields() {
4481 + if (!function_exists('acf_get_field_groups') || !function_exists('acf_get_fields')) {
4482 + return array();
4483 + }
4484 +
4485 + $all_fields = array();
4486 + $field_groups = acf_get_field_groups();
4487 +
4488 + if (!empty($field_groups)) {
4489 + foreach ($field_groups as $group) {
4490 + $group_fields = acf_get_fields($group['key']);
4491 + if (!empty($group_fields)) {
4492 + $all_fields[$group['title']] = array();
4493 + foreach ($group_fields as $field) {
4494 + $all_fields[$group['title']][] = array(
4495 + 'name' => $field['name'],
4496 + 'label' => $field['label'],
4497 + 'type' => $field['type']
4498 + );
4499 + }
4500 + }
4501 + }
4502 + }
4503 +
4504 + return $all_fields;
4505 +}
4506 +
4507 +/**
4508 + * Get whitelisted custom post meta for a given post
4509 + * This allows non-ACF meta fields (like OptionTree, theme meta boxes) to be included in embeddings
4510 + */
4511 +public function mxchat_get_whitelisted_post_meta($post_id) {
4512 + $whitelist = get_option('mxchat_custom_meta_whitelist', '');
4513 +
4514 + if (empty($whitelist)) {
4515 + return array();
4516 + }
4517 +
4518 + // Parse the whitelist - one meta key per line
4519 + $meta_keys = array_filter(array_map('trim', explode("\n", $whitelist)));
4520 +
4521 + if (empty($meta_keys)) {
4522 + return array();
4523 + }
4524 +
4525 + $result = array();
4526 +
4527 + foreach ($meta_keys as $key) {
4528 + // Skip empty keys
4529 + if (empty($key)) {
4530 + continue;
4531 + }
4532 +
4533 + $value = get_post_meta($post_id, $key, true);
4534 +
4535 + // Only include non-empty string values
4536 + if (!empty($value) && is_string($value)) {
4537 + $result[$key] = $value;
4538 + } elseif (!empty($value) && is_array($value)) {
4539 + // Handle array values by joining them
4540 + $flat_value = $this->mxchat_flatten_meta_array($value);
4541 + if (!empty($flat_value)) {
4542 + $result[$key] = $flat_value;
4543 + }
4544 + }
4545 + }
4546 +
4547 + return $result;
4548 +}
4549 +
4550 +/**
4551 + * Flatten array meta values into a readable string
4552 + */
4553 +private function mxchat_flatten_meta_array($array, $depth = 0) {
4554 + if ($depth > 3) {
4555 + return ''; // Prevent infinite recursion
4556 + }
4557 +
4558 + $parts = array();
4559 +
4560 + foreach ($array as $key => $value) {
4561 + if (is_string($value) && !empty($value)) {
4562 + $parts[] = $value;
4563 + } elseif (is_array($value)) {
4564 + $nested = $this->mxchat_flatten_meta_array($value, $depth + 1);
4565 + if (!empty($nested)) {
4566 + $parts[] = $nested;
4567 + }
4568 + }
4569 + }
4570 +
4571 + return implode(', ', $parts);
4572 +}
4573 +
4574 +/**
4575 + * Format ACF field values for content extraction
4576 + */
4577 +public function mxchat_format_acf_field_value($value, $field_name = '', $post_id = 0) {
4578 + if (empty($value)) {
4579 + return '';
4580 + }
4581 +
4582 + // Handle WP_Post objects first (THIS IS THE KEY FIX)
4583 + if ($value instanceof WP_Post) {
4584 + return $value->post_title ?: '';
4585 + }
4586 +
4587 + // Handle other WP objects
4588 + if (is_object($value)) {
4589 + if (isset($value->post_title)) {
4590 + return $value->post_title;
4591 + } elseif (isset($value->display_name)) {
4592 + return $value->display_name;
4593 + } elseif (isset($value->name)) {
4594 + return $value->name;
4595 + } elseif (method_exists($value, '__toString')) {
4596 + try {
4597 + return (string) $value;
4598 + } catch (Exception $e) {
4599 + return '';
4600 + }
4601 + }
4602 + // For any other objects, return empty string
4603 + return '';
4604 + }
4605 +
4606 + // Handle different ACF field types
4607 + if (is_array($value)) {
4608 + // Check if it's an image/file field
4609 + if (isset($value['url'])) {
4610 + // Image field - return alt text, title, or caption
4611 + if (!empty($value['alt'])) {
4612 + return $value['alt'];
4613 + } elseif (!empty($value['title'])) {
4614 + return $value['title'];
4615 + } elseif (!empty($value['caption'])) {
4616 + return $value['caption'];
4617 + } else {
4618 + return ''; // Don't include just the URL
4619 + }
4620 + }
4621 +
4622 + // Check if it's a post object or relationship field
4623 + if (isset($value['post_title'])) {
4624 + return $value['post_title'];
4625 + }
4626 +
4627 + // Check if it's a user field
4628 + if (isset($value['display_name'])) {
4629 + return $value['display_name'];
4630 + }
4631 +
4632 + // Check if it's a taxonomy term
4633 + if (isset($value['name']) && isset($value['taxonomy'])) {
4634 + return $value['name'];
4635 + }
4636 +
4637 + // Check if it's a select field with label
4638 + if (isset($value['label'])) {
4639 + return $value['label'];
4640 + }
4641 +
4642 + // Check for repeater field or flexible content
4643 + if (is_numeric(key($value))) {
4644 + $sub_values = array();
4645 + foreach ($value as $sub_item) {
4646 + if (is_array($sub_item)) {
4647 + // For repeater/flexible content, extract text values
4648 + $sub_text = $this->mxchat_extract_text_from_acf_array($sub_item);
4649 + if (!empty($sub_text)) {
4650 + $sub_values[] = $sub_text;
4651 + }
4652 + } elseif ($sub_item instanceof WP_Post) {
4653 + // Handle WP_Post objects in arrays
4654 + $sub_values[] = $sub_item->post_title ?: '';
4655 + } else {
4656 + $sub_values[] = (string) $sub_item;
4657 + }
4658 + }
4659 + return implode(', ', array_filter($sub_values));
4660 + }
4661 +
4662 + // For other arrays, try to extract meaningful text
4663 + $text_values = array();
4664 + foreach ($value as $key => $val) {
4665 + if (is_string($val) && !empty(trim($val))) {
4666 + $text_values[] = trim($val);
4667 + } elseif ($val instanceof WP_Post) {
4668 + // Handle WP_Post objects in associative arrays
4669 + $text_values[] = $val->post_title ?: '';
4670 + } elseif (is_array($val) && isset($val['post_title'])) {
4671 + $text_values[] = $val['post_title'];
4672 + } elseif (is_array($val) && isset($val['name'])) {
4673 + $text_values[] = $val['name'];
4674 + }
4675 + }
4676 +
4677 + return implode(', ', array_filter($text_values));
4678 + }
4679 +
4680 + // Handle boolean values
4681 + if (is_bool($value)) {
4682 + return $value ? 'Yes' : 'No';
4683 + }
4684 +
4685 + // Handle numeric values
4686 + if (is_numeric($value)) {
4687 + return (string) $value;
4688 + }
4689 +
4690 + // Handle string values
4691 + if (is_string($value)) {
4692 + return trim($value);
4693 + }
4694 +
4695 + // For anything else that we can't handle, return empty string
4696 + // This prevents the "Object could not be converted to string" error
4697 + return '';
4698 +}
4699 +
4700 +/**
4701 + * Extract text from complex ACF array structures
4702 + */
4703 +private function mxchat_extract_text_from_acf_array($array) {
4704 + if (!is_array($array)) {
4705 + return '';
4706 + }
4707 +
4708 + $text_parts = array();
4709 +
4710 + foreach ($array as $key => $value) {
4711 + if (is_string($value) && !empty(trim($value))) {
4712 + // Skip keys that are likely to be IDs or technical values
4713 + if (!is_numeric($value) || strlen($value) > 10) {
4714 + $text_parts[] = trim($value);
4715 + }
4716 + } elseif ($value instanceof WP_Post) {
4717 + // Handle WP_Post objects
4718 + $text_parts[] = $value->post_title ?: '';
4719 + } elseif (is_array($value)) {
4720 + if (isset($value['post_title'])) {
4721 + $text_parts[] = $value['post_title'];
4722 + } elseif (isset($value['name'])) {
4723 + $text_parts[] = $value['name'];
4724 + } elseif (isset($value['label'])) {
4725 + $text_parts[] = $value['label'];
4726 + }
4727 + } elseif (is_object($value)) {
4728 + // Handle other objects safely
4729 + if (isset($value->post_title)) {
4730 + $text_parts[] = $value->post_title;
4731 + } elseif (isset($value->name)) {
4732 + $text_parts[] = $value->name;
4733 + } elseif (isset($value->display_name)) {
4734 + $text_parts[] = $value->display_name;
4735 + }
4736 + }
4737 + }
4738 +
4739 + return implode(', ', array_filter($text_parts));
4740 +}
4741 +
4742 +/**
4743 + * Handle ACF save - fires after ACF fields are saved
4744 + * This ensures ACF field data is available when syncing to knowledge base
4745 + */
4746 +public function mxchat_handle_acf_save($post_id) {
4747 + // Skip if not a valid post
4748 + if (!$post_id || $post_id === 'options') {
4749 + return;
4750 + }
4751 +
4752 + // Skip autosaves and revisions
4753 + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
4754 + return;
4755 + }
4756 +
4757 + $post = get_post($post_id);
4758 + if (!$post) {
4759 + return;
4760 + }
4761 +
4762 + $post_type = $post->post_type;
4763 +
4764 + // Check if sync is enabled for this post type
4765 + $should_sync = false;
4766 +
4767 + if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
4768 + $should_sync = true;
4769 + } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
4770 + $should_sync = true;
4771 + } else if ($post_type === 'product' && class_exists('WooCommerce')) {
4772 + // WooCommerce products - check if WooCommerce integration is enabled
4773 + $options = get_option('mxchat_options', array());
4774 + if (isset($options['enable_woocommerce_integration']) &&
4775 + ($options['enable_woocommerce_integration'] === '1' || $options['enable_woocommerce_integration'] === 'on')) {
4776 + $should_sync = true;
4777 + }
4778 + } else {
4779 + // Check custom post types
4780 + $option_name = 'mxchat_auto_sync_' . $post_type;
4781 + if (get_option($option_name) === '1') {
4782 + $should_sync = true;
4783 + }
4784 + }
4785 +
4786 + if (!$should_sync) {
4787 + return;
4788 + }
4789 +
4790 + // Only process published posts
4791 + if ($post->post_status !== 'publish') {
4792 + return;
4793 + }
4794 +
4795 + // Check if this post has any ACF fields - if not, no need to re-sync
4796 + $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
4797 + if (empty($acf_fields)) {
4798 + return;
4799 + }
4800 +
4801 + // Use a transient to prevent duplicate processing (post_updated may have already run)
4802 + $transient_key = 'mxchat_acf_synced_' . $post_id;
4803 + if (get_transient($transient_key)) {
4804 + return;
4805 + }
4806 + set_transient($transient_key, true, 60); // Prevent re-processing for 60 seconds
4807 +
4808 + // Re-run the sync with ACF data now available
4809 + // We pass $update=true since this is effectively an update with ACF data
4810 + $this->mxchat_handle_post_update($post_id, $post, true);
4811 +}
4812 +
4813 +public function mxchat_handle_post_update($post_id, $post, $update) {
4814 + // Basic validation checks
4815 + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
4816 + return;
4817 + }
4818 +
4819 + $post_type = $post->post_type;
4820 +
4821 + // Check if sync is enabled for this post type
4822 + $should_sync = false;
4823 +
4824 + // Check built-in post types first
4825 + if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
4826 + $should_sync = true;
4827 + } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
4828 + $should_sync = true;
4829 + } else {
4830 + // Check custom post types
4831 + $option_name = 'mxchat_auto_sync_' . $post_type;
4832 + if (get_option($option_name) === '1') {
4833 + $should_sync = true;
4834 + }
4835 + }
4836 +
4837 + if (!$should_sync) {
4838 + return;
4839 + }
4840 +
4841 + // Check if we have stored the previous status and URL in our transients
4842 + $previous_status_key = 'mxchat_prev_status_' . $post_id;
4843 + $previous_status = get_transient($previous_status_key);
4844 +
4845 + $previous_url_key = 'mxchat_prev_url_' . $post_id;
4846 + $previous_url = get_transient($previous_url_key);
4847 +
4848 + // If the post was previously published but is now not published, remove from knowledge base
4849 + if ($previous_status === 'publish' && $post->post_status !== 'publish') {
4850 + // Use the stored URL from when it was published, or fall back to current permalink
4851 + $source_url = $previous_url ?: get_permalink($post_id);
4852 +
4853 + if ($source_url) {
4854 + // Check if Pinecone is enabled
4855 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4856 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4857 +
4858 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
4859 + // Delete from Pinecone
4860 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
4861 + } else {
4862 + // Delete from WordPress DB
4863 + global $wpdb;
4864 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4865 +
4866 + $result = $wpdb->delete(
4867 + $table_name,
4868 + array('source_url' => $source_url),
4869 + array('%s')
4870 + );
4871 + }
4872 + }
4873 +
4874 + // Clean up the transients and exit early
4875 + delete_transient($previous_status_key);
4876 + delete_transient($previous_url_key);
4877 + return;
4878 + }
4879 +
4880 + // Store the current status for next time (if this is an update)
4881 + if ($update) {
4882 + set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
4883 +
4884 + // If the post is currently published, also store its URL
4885 + if ($post->post_status === 'publish') {
4886 + $current_url = get_permalink($post_id);
4887 + set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
4888 + }
4889 + }
4890 +
4891 + // Only process currently published content for adding/updating
4892 + if ($post->post_status === 'publish') {
4893 + // Get the source URL
4894 + $source_url = get_permalink($post_id);
4895 +
4896 + // Get content with proper formatting (matching ajax_mxchat_process_selected_content)
4897 + $title = get_the_title($post_id);
4898 + $content = get_post_field('post_content', $post_id);
4899 + $excerpt = get_post_field('post_excerpt', $post_id);
4900 +
4901 + // Remove shortcode tags but preserve content inside them
4902 + $content = $this->strip_shortcode_tags_preserve_content($content);
4903 + $excerpt = $this->strip_shortcode_tags_preserve_content($excerpt);
4904 +
4905 + // Strip tags but preserve structure (don't use 'the_content' filter as it may re-add shortcodes)
4906 + $content = wp_strip_all_tags($content);
4907 +
4908 + // Combine title, short description (if exists), and content
4909 + $final_content = $title . "\n\n";
4910 +
4911 + // Add short description if it exists (WooCommerce products use post_excerpt for short description)
4912 + if (!empty($excerpt)) {
4913 + $final_content .= "Short Description: " . wp_strip_all_tags($excerpt) . "\n\n";
4914 + }
4915 +
4916 + $final_content .= $content;
4917 +
4918 + // For WooCommerce products, include pricing and product details
4919 + if ($post_type === 'product' && class_exists('WooCommerce')) {
4920 + $product = wc_get_product($post_id);
4921 +
4922 + if ($product) {
4923 + // Get pricing information
4924 + $regular_price = $product->get_regular_price();
4925 + $sale_price = $product->get_sale_price();
4926 + $price = $product->get_price();
4927 + $sku = $product->get_sku();
4928 +
4929 + // Get currency symbol
4930 + $currency_symbol = get_woocommerce_currency_symbol();
4931 +
4932 + // Add pricing information
4933 + $final_content .= "\n";
4934 + if (!empty($regular_price)) {
4935 + $final_content .= "Price: " . $currency_symbol . $regular_price . "\n";
4936 + } elseif (!empty($price)) {
4937 + $final_content .= "Price: " . $currency_symbol . $price . "\n";
4938 + }
4939 +
4940 + if (!empty($sale_price) && $sale_price !== $regular_price) {
4941 + $final_content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
4942 + }
4943 +
4944 + // Handle variable products - show price range
4945 + if ($product->is_type('variable')) {
4946 + $min_price = $product->get_variation_price('min');
4947 + $max_price = $product->get_variation_price('max');
4948 + if ($min_price !== $max_price) {
4949 + $final_content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
4950 + }
4951 + }
4952 +
4953 + if (!empty($sku)) {
4954 + $final_content .= "SKU: " . $sku . "\n";
4955 + }
4956 +
4957 + // Get product categories
4958 + $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
4959 + if (!empty($categories) && !is_wp_error($categories)) {
4960 + $final_content .= "Categories: " . implode(', ', $categories) . "\n";
4961 + }
4962 + }
4963 + }
4964 +
4965 + // For custom post types like job_listing, include additional fields
4966 + if ($post_type === 'job_listing') {
4967 + // Add job-specific meta if available
4968 + $job_location = get_post_meta($post_id, '_job_location', true);
4969 + if (!empty($job_location)) {
4970 + $final_content .= "\n\nLocation: " . $job_location;
4971 + }
4972 +
4973 + // Get job type terms
4974 + $job_types = get_the_terms($post_id, 'job_listing_type');
4975 + if (!empty($job_types) && !is_wp_error($job_types)) {
4976 + $types = array();
4977 + foreach ($job_types as $type) {
4978 + $types[] = $type->name;
4979 + }
4980 + $final_content .= "\n\nJob Type: " . implode(', ', $types);
4981 + }
4982 +
4983 + // Get company name if available
4984 + $company_name = get_post_meta($post_id, '_company_name', true);
4985 + if (!empty($company_name)) {
4986 + $final_content .= "\n\nCompany: " . $company_name;
4987 + }
4988 + }
4989 +
4990 + // ADD ACF FIELDS SUPPORT (matches ajax_mxchat_process_selected_content behavior)
4991 + $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
4992 + if (!empty($acf_fields)) {
4993 + $acf_content_parts = array();
4994 +
4995 + foreach ($acf_fields as $field_name => $field_value) {
4996 + $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
4997 + if (!empty($formatted_value)) {
4998 + // Convert field name to readable label
4999 + $field_label = ucwords(str_replace(['_', '-'], ' ', $field_name));
5000 + $acf_content_parts[] = $field_label . ": " . $formatted_value;
5001 + }
5002 + }
5003 +
5004 + if (!empty($acf_content_parts)) {
5005 + $final_content .= "\n\n" . implode("\n", $acf_content_parts);
5006 + }
5007 + }
5008 +
5009 + // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
5010 + $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
5011 + if (!empty($custom_meta)) {
5012 + $meta_content_parts = array();
5013 +
5014 + foreach ($custom_meta as $meta_key => $meta_value) {
5015 + // Convert meta key to readable label
5016 + $meta_label = ucwords(str_replace(array('_', '-'), ' ', $meta_key));
5017 + $meta_content_parts[] = $meta_label . ": " . $meta_value;
5018 + }
5019 +
5020 + if (!empty($meta_content_parts)) {
5021 + $final_content .= "\n\n" . implode("\n", $meta_content_parts);
5022 + }
5023 + }
5024 +
5025 + // Get API key with proper model detection
5026 + $options = get_option('mxchat_options');
5027 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
5028 +
5029 + if (strpos($selected_model, 'voyage') === 0) {
5030 + $api_key = $options['voyage_api_key'] ?? '';
5031 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
5032 + $api_key = $options['gemini_api_key'] ?? '';
5033 + } else {
5034 + $api_key = $options['api_key'] ?? '';
5035 + }
5036 +
5037 + if (empty($api_key)) {
5038 + return;
5039 + }
5040 +
5041 + // Use the centralized utility function for storage
5042 + $result = MxChat_Utils::submit_content_to_db(
5043 + $final_content,
5044 + $source_url,
5045 + $api_key,
5046 + md5($source_url) // Vector ID for Pinecone
5047 + );
5048 +
5049 + // After successful storage, apply role restriction based on tags
5050 + if (!is_wp_error($result)) {
5051 + $this->apply_role_restriction_to_post($post_id, $source_url);
5052 + }
5053 + }
5054 +
5055 + // Clean up the stored previous status if not used above
5056 + if ($previous_status !== 'publish' || $post->post_status === 'publish') {
5057 + delete_transient($previous_status_key);
5058 + delete_transient($previous_url_key);
5059 + }
5060 +}
5061 +
5062 +/**
5063 + * Store the post status and URL before update to detect status transitions
5064 + * This runs before the post is actually updated in the database
5065 + */
5066 +public function mxchat_store_pre_update_status($post_id, $data) {
5067 + // Get the current post from database (before update)
5068 + $current_post = get_post($post_id);
5069 +
5070 + if ($current_post) {
5071 + // Store the current status temporarily
5072 + $status_key = 'mxchat_prev_status_' . $post_id;
5073 + set_transient($status_key, $current_post->post_status, HOUR_IN_SECONDS);
5074 +
5075 + // If the post is currently published, also store its URL
5076 + if ($current_post->post_status === 'publish') {
5077 + $url_key = 'mxchat_prev_url_' . $post_id;
5078 + $current_url = get_permalink($post_id);
5079 + set_transient($url_key, $current_url, HOUR_IN_SECONDS);
5080 + }
5081 + }
5082 +}
5083 +
5084 +public function mxchat_handle_post_delete($post_id) {
5085 + // Get post data before it's deleted
5086 + $post = get_post($post_id);
5087 +
5088 + // Basic validation
5089 + if (!$post || wp_is_post_revision($post_id)) {
5090 + return;
5091 + }
5092 +
5093 + $post_type = $post->post_type;
5094 +
5095 + // Check if sync is enabled for this post type
5096 + $should_sync = false;
5097 +
5098 + // Check built-in post types first
5099 + if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
5100 + $should_sync = true;
5101 + } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
5102 + $should_sync = true;
5103 + } else {
5104 + // Check custom post types
5105 + $option_name = 'mxchat_auto_sync_' . $post_type;
5106 + if (get_option($option_name) === '1') {
5107 + $should_sync = true;
5108 + }
5109 + }
5110 +
5111 + if (!$should_sync) {
5112 + return;
5113 + }
5114 +
5115 + // Get the URL before post is deleted
5116 + $source_url = get_permalink($post_id);
5117 + if (!$source_url) {
5118 + //error_log('MXChat: Failed to get permalink for post ' . $post_id);
5119 + return;
5120 + }
5121 +
5122 + // Use chunk-aware deletion (handles both chunked and non-chunked content)
5123 + $delete_result = MxChat_Utils::delete_chunks_for_url($source_url, 'default');
5124 +
5125 + if (is_wp_error($delete_result)) {
5126 + //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message());
5127 + }
5128 +}
5129 +
5130 +
5131 + /**
5132 + * Deletes data from Pinecone using a source URL
5133 + */
5134 + public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
5135 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
5136 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
5137 +
5138 + if (empty($host) || empty($api_key)) {
5139 + //error_log('MXChat: Pinecone deletion failed - missing configuration');
5140 + return false;
5141 + }
5142 +
5143 + $api_endpoint = "https://{$host}/vectors/delete";
5144 + $vector_id = md5($source_url);
5145 +
5146 + $request_body = array(
5147 + 'ids' => array($vector_id)
5148 + );
5149 +
5150 + $response = wp_remote_post($api_endpoint, array(
5151 + 'headers' => array(
5152 + 'Api-Key' => $api_key,
5153 + 'accept' => 'application/json',
5154 + 'content-type' => 'application/json'
5155 + ),
5156 + 'body' => wp_json_encode($request_body),
5157 + 'timeout' => 30
5158 + ));
5159 +
5160 + if (is_wp_error($response)) {
5161 + //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
5162 + return false;
5163 + }
5164 +
5165 + $response_code = wp_remote_retrieve_response_code($response);
5166 + if ($response_code !== 200) {
5167 + //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
5168 + return false;
5169 + }
5170 +
5171 + return true;
5172 + }
5173 +
5174 +
5175 +
5176 +public function mxchat_handle_product_change($post_id, $post, $update) {
5177 + if ($post->post_type !== 'product') {
5178 + return;
5179 + }
5180 +
5181 + if ($post->post_status === 'publish') {
5182 + add_action('shutdown', function() use ($post_id) {
5183 + $product = wc_get_product($post_id);
5184 + if ($product) {
5185 + $this->mxchat_store_product_embedding($product);
5186 + }
5187 + });
5188 + }
5189 +}
5190 +
5191 +/**
5192 + * Store WooCommerce product embeddings
5193 + */
5194 +private function mxchat_store_product_embedding($product) {
5195 + if (!isset($this->options['enable_woocommerce_integration']) ||
5196 + !in_array($this->options['enable_woocommerce_integration'], ['1', 'on'])) {
5197 + return;
5198 + }
5199 +
5200 + $source_url = get_permalink($product->get_id());
5201 + $product_id = $product->get_id();
5202 +
5203 + // Build product content
5204 + $title = $product->get_name();
5205 + $description = $product->get_description();
5206 + $short_description = $product->get_short_description();
5207 + $regular_price = $product->get_regular_price();
5208 + $sale_price = $product->get_sale_price();
5209 + $price = $product->get_price();
5210 + $sku = $product->get_sku();
5211 +
5212 + // Get currency symbol
5213 + $currency_symbol = get_woocommerce_currency_symbol();
5214 +
5215 + // Format content consistently
5216 + $content = $title . "\n\n";
5217 +
5218 + if (!empty($short_description)) {
5219 + $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
5220 + }
5221 +
5222 + if (!empty($description)) {
5223 + $content .= wp_strip_all_tags($description) . "\n\n";
5224 + }
5225 +
5226 + // Add pricing information
5227 + if (!empty($regular_price)) {
5228 + $content .= "Price: " . $currency_symbol . $regular_price . "\n";
5229 + } elseif (!empty($price)) {
5230 + $content .= "Price: " . $currency_symbol . $price . "\n";
5231 + }
5232 +
5233 + if (!empty($sale_price) && $sale_price !== $regular_price) {
5234 + $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
5235 + }
5236 +
5237 + // Handle variable products - show price range
5238 + if ($product->is_type('variable')) {
5239 + $min_price = $product->get_variation_price('min');
5240 + $max_price = $product->get_variation_price('max');
5241 + if ($min_price !== $max_price) {
5242 + $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
5243 + }
5244 + }
5245 +
5246 + if (!empty($sku)) {
5247 + $content .= "SKU: " . $sku . "\n";
5248 + }
5249 +
5250 + // Get product categories
5251 + $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
5252 + if (!empty($categories) && !is_wp_error($categories)) {
5253 + $content .= "Categories: " . implode(', ', $categories) . "\n";
5254 + }
5255 +
5256 + // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
5257 + $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
5258 + if (!empty($custom_tabs) && is_array($custom_tabs)) {
5259 + foreach ($custom_tabs as $tab) {
5260 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
5261 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
5262 +
5263 + if (!empty($tab_title) && !empty($tab_content)) {
5264 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
5265 + }
5266 + }
5267 + }
5268 +
5269 + // Also check for reusable/saved tabs applied to this product
5270 + $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
5271 + if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
5272 + // Get the saved tabs option
5273 + $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
5274 + if (!empty($saved_tabs) && is_array($saved_tabs)) {
5275 + foreach ($applied_saved_tabs as $saved_tab_id) {
5276 + if (isset($saved_tabs[$saved_tab_id])) {
5277 + $tab = $saved_tabs[$saved_tab_id];
5278 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
5279 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
5280 +
5281 + if (!empty($tab_title) && !empty($tab_content)) {
5282 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
5283 + }
5284 + }
5285 + }
5286 + }
5287 + }
5288 +
5289 + // Get API key with proper model detection
5290 + $options = get_option('mxchat_options');
5291 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
5292 +
5293 + if (strpos($selected_model, 'voyage') === 0) {
5294 + $api_key = $options['voyage_api_key'] ?? '';
5295 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
5296 + $api_key = $options['gemini_api_key'] ?? '';
5297 + } else {
5298 + $api_key = $options['api_key'] ?? '';
5299 + }
5300 +
5301 + if (empty($api_key)) {
5302 + //error_log('MxChat Auto-sync: No API key configured for embedding model');
5303 + return;
5304 + }
5305 +
5306 + // Use the centralized utility function for storage
5307 + $result = MxChat_Utils::submit_content_to_db(
5308 + $content,
5309 + $source_url,
5310 + $api_key,
5311 + md5($source_url) // Vector ID for Pinecone
5312 + );
5313 +
5314 + // After successful storage, apply role restriction based on tags
5315 + if (!is_wp_error($result)) {
5316 + $this->apply_role_restriction_to_post($product_id, $source_url);
5317 + }
5318 +
5319 + if (is_wp_error($result)) {
5320 + //error_log('MxChat WooCommerce sync failed for product ' . $product_id . ': ' . $result->get_error_message());
5321 + }
5322 +}
5323 +
5324 +public function mxchat_handle_product_delete($post_id) {
5325 + if (get_post_type($post_id) !== 'product') {
5326 + return;
5327 + }
5328 +
5329 + $source_url = get_permalink($post_id);
5330 +
5331 + // Check if Pinecone is enabled
5332 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5333 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5334 +
5335 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
5336 + // Delete from Pinecone
5337 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
5338 + } else {
5339 + // Delete from WordPress DB
5340 + global $wpdb;
5341 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5342 +
5343 + $wpdb->delete(
5344 + $table_name,
5345 + array('source_url' => $source_url),
5346 + array('%s')
5347 + );
5348 + }
5349 +}
5350 +
5351 +/**
5352 + * Handle individual Pinecone content deletion
5353 + */
5354 +public function mxchat_handle_pinecone_prompt_delete() {
5355 + // Check permissions
5356 + if (!current_user_can('manage_options')) {
5357 + wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
5358 + }
5359 +
5360 + // Verify nonce
5361 + if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'mxchat_delete_pinecone_prompt_nonce')) {
5362 + wp_die(esc_html__('Security check failed.', 'mxchat'));
5363 + }
5364 +
5365 + $vector_id = isset($_GET['vector_id']) ? sanitize_text_field($_GET['vector_id']) : '';
5366 +
5367 + if (empty($vector_id)) {
5368 + set_transient('mxchat_admin_notice_error',
5369 + esc_html__('Invalid vector ID.', 'mxchat'),
5370 + 30
5371 + );
5372 + wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5373 + exit;
5374 + }
5375 +
5376 + // Get Pinecone settings
5377 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5378 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5379 +
5380 + if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5381 + set_transient('mxchat_admin_notice_error',
5382 + esc_html__('Pinecone is not properly configured.', 'mxchat'),
5383 + 30
5384 + );
5385 + wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5386 + exit;
5387 + }
5388 +
5389 + // Delete from Pinecone
5390 + $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5391 + $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
5392 + $vector_id,
5393 + $pinecone_options['mxchat_pinecone_api_key'],
5394 + $pinecone_options['mxchat_pinecone_host']
5395 + );
5396 +
5397 + if ($result['success']) {
5398 + // No cache clearing needed since we removed caching
5399 + set_transient('mxchat_admin_notice_success',
5400 + esc_html__('Entry deleted successfully from Pinecone.', 'mxchat'),
5401 + 30
5402 + );
5403 + } else {
5404 + set_transient('mxchat_admin_notice_error',
5405 + esc_html__('Failed to delete entry: ', 'mxchat') . $result['message'],
5406 + 30
5407 + );
5408 + }
5409 +
5410 + wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5411 + exit;
5412 +}
5413 +/**
5414 + * Handle individual Pinecone content deletion via AJAX
5415 + */
5416 +public function ajax_mxchat_delete_pinecone_prompt() {
5417 + // Verify nonce and permissions
5418 + if (!check_ajax_referer('mxchat_delete_pinecone_prompt_nonce', 'nonce', false)) {
5419 + wp_send_json_error('Invalid nonce');
5420 + exit;
5421 + }
5422 +
5423 + if (!current_user_can('manage_options')) {
5424 + wp_send_json_error('Unauthorized access');
5425 + exit;
5426 + }
5427 +
5428 + $vector_id = isset($_POST['vector_id']) ? sanitize_text_field($_POST['vector_id']) : '';
5429 + $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
5430 +
5431 + if (empty($vector_id)) {
5432 + wp_send_json_error('Missing vector ID');
5433 + exit;
5434 + }
5435 +
5436 + // Get bot-specific Pinecone settings
5437 + $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5438 + $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
5439 +
5440 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5441 +
5442 + if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5443 + wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
5444 + exit;
5445 + }
5446 +
5447 + // Delete from the correct Pinecone index
5448 + $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
5449 + $vector_id,
5450 + $pinecone_options['mxchat_pinecone_api_key'],
5451 + $pinecone_options['mxchat_pinecone_host']
5452 + );
5453 +
5454 + if ($result['success']) {
5455 + // No cache clearing needed since we removed caching
5456 + wp_send_json_success(array(
5457 + 'message' => 'Entry deleted successfully from Pinecone',
5458 + 'vector_id' => $vector_id,
5459 + 'bot_id' => $bot_id
5460 + ));
5461 + } else {
5462 + MxChat_Admin::mxchat_log_debug('pinecone_error', 'Failed to delete from Pinecone: ' . $result['message'], array('vector_id' => $vector_id, 'bot_id' => $bot_id));
5463 + wp_send_json_error('Failed to delete from Pinecone: ' . $result['message']);
5464 + }
5465 +
5466 + exit;
5467 +}
5468 +
5469 +/**
5470 + * Handle deletion of all chunks for a given source URL via AJAX
5471 + * Follows the same pattern as ajax_mxchat_delete_pinecone_prompt
5472 + */
5473 +public function ajax_mxchat_delete_chunks_by_url() {
5474 + // Verify nonce and permissions
5475 + if (!check_ajax_referer('mxchat_delete_chunks_nonce', 'nonce', false)) {
5476 + wp_send_json_error('Invalid nonce');
5477 + exit;
5478 + }
5479 +
5480 + if (!current_user_can('manage_options')) {
5481 + wp_send_json_error('Unauthorized access');
5482 + exit;
5483 + }
5484 +
5485 + $source_url = isset($_POST['source_url']) ? esc_url_raw($_POST['source_url']) : '';
5486 + $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
5487 + $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
5488 +
5489 + if (empty($source_url)) {
5490 + wp_send_json_error('Missing source URL');
5491 + exit;
5492 + }
5493 +
5494 + // Generate the base vector ID from the source URL (same as how chunks are created)
5495 + $base_vector_id = md5($source_url);
5496 +
5497 + if ($data_source === 'pinecone') {
5498 + // Get bot-specific Pinecone settings (same as working delete function)
5499 + $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5500 + $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
5501 +
5502 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5503 +
5504 + if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5505 + wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
5506 + exit;
5507 + }
5508 +
5509 + $api_key = $pinecone_options['mxchat_pinecone_api_key'];
5510 + $host = $pinecone_options['mxchat_pinecone_host'];
5511 + $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
5512 +
5513 + // Collect all vector IDs to delete
5514 + $vectors_to_delete = array();
5515 +
5516 + // Add the original single-vector ID (for non-chunked content)
5517 + $vectors_to_delete[] = $base_vector_id;
5518 +
5519 + // Use Pinecone list API to find all chunk vectors with this prefix
5520 + // NOTE: Pinecone List API is a GET request with query parameters, not POST
5521 + $prefix = $base_vector_id . '_chunk_';
5522 +
5523 + $query_params = array(
5524 + 'prefix' => $prefix,
5525 + 'limit' => 100
5526 + );
5527 +
5528 + if (!empty($namespace)) {
5529 + $query_params['namespace'] = $namespace;
5530 + }
5531 +
5532 + $list_url = "https://{$host}/vectors/list?" . http_build_query($query_params);
5533 +
5534 + $list_response = wp_remote_get($list_url, array(
5535 + 'headers' => array(
5536 + 'Api-Key' => $api_key,
5537 + 'accept' => 'application/json'
5538 + ),
5539 + 'timeout' => 30
5540 + ));
5541 +
5542 + if (!is_wp_error($list_response)) {
5543 + $list_body_response = wp_remote_retrieve_body($list_response);
5544 + $list_data = json_decode($list_body_response, true);
5545 + if (!empty($list_data['vectors'])) {
5546 + foreach ($list_data['vectors'] as $vector) {
5547 + if (isset($vector['id'])) {
5548 + $vectors_to_delete[] = $vector['id'];
5549 + }
5550 + }
5551 + }
5552 + }
5553 +
5554 + if (empty($vectors_to_delete)) {
5555 + wp_send_json_success(array(
5556 + 'message' => 'No vectors found to delete',
5557 + 'source_url' => $source_url
5558 + ));
5559 + exit;
5560 + }
5561 +
5562 + // Delete all vectors using the same endpoint as the working function
5563 + $delete_url = "https://{$host}/vectors/delete";
5564 +
5565 + $delete_body = array(
5566 + 'ids' => $vectors_to_delete
5567 + );
5568 +
5569 + if (!empty($namespace)) {
5570 + $delete_body['namespace'] = $namespace;
5571 + }
5572 +
5573 + $delete_response = wp_remote_post($delete_url, array(
5574 + 'headers' => array(
5575 + 'Api-Key' => $api_key,
5576 + 'accept' => 'application/json',
5577 + 'content-type' => 'application/json'
5578 + ),
5579 + 'body' => wp_json_encode($delete_body),
5580 + 'timeout' => 30
5581 + ));
5582 +
5583 + if (is_wp_error($delete_response)) {
5584 + MxChat_Admin::mxchat_log_debug('pinecone_error', 'Failed to delete chunks from Pinecone: ' . $delete_response->get_error_message(), array('source_url' => $source_url));
5585 + wp_send_json_error('Failed to delete from Pinecone: ' . $delete_response->get_error_message());
5586 + exit;
5587 + }
5588 +
5589 + $response_code = wp_remote_retrieve_response_code($delete_response);
5590 +
5591 + if ($response_code !== 200) {
5592 + MxChat_Admin::mxchat_log_debug('pinecone_error', 'Pinecone API error (HTTP ' . $response_code . ')', array('source_url' => $source_url));
5593 + wp_send_json_error('Pinecone API error (HTTP ' . $response_code . ')');
5594 + exit;
5595 + }
5596 +
5597 + wp_send_json_success(array(
5598 + 'message' => 'All chunks deleted successfully from Pinecone',
5599 + 'source_url' => $source_url,
5600 + 'deleted_count' => count($vectors_to_delete)
5601 + ));
5602 +
5603 + } else {
5604 + // WordPress database deletion
5605 + global $wpdb;
5606 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5607 +
5608 + $result = $wpdb->delete(
5609 + $table_name,
5610 + array('source_url' => $source_url),
5611 + array('%s')
5612 + );
5613 +
5614 + if ($result === false) {
5615 + MxChat_Admin::mxchat_log_debug('database_error', 'Failed to delete from database: ' . $wpdb->last_error, array('source_url' => $source_url));
5616 + wp_send_json_error('Failed to delete from database: ' . $wpdb->last_error);
5617 + exit;
5618 + }
5619 +
5620 + wp_send_json_success(array(
5621 + 'message' => 'All chunks deleted successfully from database',
5622 + 'source_url' => $source_url,
5623 + 'deleted_count' => $result
5624 + ));
5625 + }
5626 +
5627 + exit;
5628 +}
5629 +
5630 +/**
5631 + * Handle individual WordPress database content deletion via AJAX
5632 + * Mirrors the Pinecone delete handler but for WordPress database entries
5633 + */
5634 +public function ajax_mxchat_delete_wordpress_prompt() {
5635 + // Verify nonce and permissions
5636 + if (!check_ajax_referer('mxchat_delete_wordpress_prompt_nonce', 'nonce', false)) {
5637 + wp_send_json_error('Invalid nonce');
5638 + exit;
5639 + }
5640 +
5641 + if (!current_user_can('manage_options')) {
5642 + wp_send_json_error('Unauthorized access');
5643 + exit;
5644 + }
5645 +
5646 + $entry_id = isset($_POST['entry_id']) ? intval($_POST['entry_id']) : 0;
5647 +
5648 + if (empty($entry_id)) {
5649 + wp_send_json_error('Missing entry ID');
5650 + exit;
5651 + }
5652 +
5653 + global $wpdb;
5654 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5655 +
5656 + // Clear cache for this entry
5657 + wp_cache_delete('prompt_' . $entry_id, 'mxchat_prompts');
5658 +
5659 + // Delete from database
5660 + $result = $wpdb->delete(
5661 + $table_name,
5662 + array('id' => $entry_id),
5663 + array('%d')
5664 + );
5665 +
5666 + if ($result !== false) {
5667 + wp_send_json_success(array(
5668 + 'message' => 'Entry deleted successfully',
5669 + 'entry_id' => $entry_id
5670 + ));
5671 + } else {
5672 + wp_send_json_error('Failed to delete entry from database');
5673 + }
5674 +
5675 + exit;
5676 +}
5677 +
5678 +/**
5679 + * Handle bulk deletion of knowledge entries via AJAX
5680 + * Supports both Pinecone and WordPress database entries
5681 + */
5682 +public function ajax_mxchat_bulk_delete_knowledge() {
5683 + // Verify nonce and permissions
5684 + if (!check_ajax_referer('mxchat_bulk_delete_knowledge_nonce', 'nonce', false)) {
5685 + wp_send_json_error('Invalid nonce');
5686 + exit;
5687 + }
5688 +
5689 + if (!current_user_can('manage_options')) {
5690 + wp_send_json_error('Unauthorized access');
5691 + exit;
5692 + }
5693 +
5694 + $entries = isset($_POST['entries']) ? $_POST['entries'] : array();
5695 + $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
5696 +
5697 + if (empty($entries) || !is_array($entries)) {
5698 + wp_send_json_error('No entries provided');
5699 + exit;
5700 + }
5701 +
5702 + // Extend execution time — bulk Pinecone operations can take a while
5703 + if (function_exists('set_time_limit')) {
5704 + set_time_limit(120);
5705 + }
5706 +
5707 + $success_ids = array();
5708 + $failed_ids = array();
5709 + $errors = array();
5710 +
5711 + global $wpdb;
5712 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5713 +
5714 + // Get Pinecone manager for Pinecone deletions
5715 + $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5716 + $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
5717 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5718 +
5719 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
5720 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
5721 +
5722 + // =============================================
5723 + // PHASE 1: Collect all Pinecone vector IDs
5724 + // and separate WordPress entries
5725 + // =============================================
5726 + $pinecone_entry_ids = array(); // entry IDs that are pinecone-sourced
5727 + $wordpress_entries = array(); // entries for WordPress DB deletion
5728 + $all_vector_ids = array(); // all pinecone vector IDs to delete in one batch
5729 +
5730 + foreach ($entries as $entry) {
5731 + $entry_id = sanitize_text_field($entry['id'] ?? '');
5732 + $source = sanitize_text_field($entry['source'] ?? 'wordpress');
5733 + $source_url = isset($entry['sourceUrl']) ? esc_url_raw($entry['sourceUrl']) : '';
5734 + $is_group = isset($entry['isGroup']) && ($entry['isGroup'] === true || $entry['isGroup'] === 'true');
5735 +
5736 + if (empty($entry_id)) {
5737 + continue;
5738 + }
5739 +
5740 + if ($source === 'pinecone') {
5741 + if (!$use_pinecone || empty($api_key)) {
5742 + $failed_ids[] = $entry_id;
5743 + $errors[] = "Pinecone not configured for entry: $entry_id";
5744 + continue;
5745 + }
5746 +
5747 + $pinecone_entry_ids[] = $entry_id;
5748 +
5749 + if ($is_group && !empty($source_url)) {
5750 + // Grouped/chunked entry: collect base ID + chunk IDs via List API
5751 + $base_vector_id = md5($source_url);
5752 + $all_vector_ids[] = $base_vector_id;
5753 +
5754 + $list_url = 'https://' . $host . '/vectors/list?prefix=' . urlencode($base_vector_id . '_chunk_') . '&limit=100';
5755 + $list_response = wp_remote_get($list_url, array(
5756 + 'headers' => array(
5757 + 'Api-Key' => $api_key,
5758 + 'accept' => 'application/json'
5759 + ),
5760 + 'timeout' => 30
5761 + ));
5762 +
5763 + if (!is_wp_error($list_response)) {
5764 + $list_body = json_decode(wp_remote_retrieve_body($list_response), true);
5765 + if (!empty($list_body['vectors']) && is_array($list_body['vectors'])) {
5766 + foreach ($list_body['vectors'] as $vector) {
5767 + if (isset($vector['id'])) {
5768 + $all_vector_ids[] = $vector['id'];
5769 + }
5770 + }
5771 + }
5772 + }
5773 + } else {
5774 + // Single entry: the entry_id IS the vector ID
5775 + $all_vector_ids[] = $entry_id;
5776 + }
5777 + } else {
5778 + $wordpress_entries[] = $entry;
5779 + }
5780 + }
5781 +
5782 + // =============================================
5783 + // PHASE 2: Single batch delete to Pinecone
5784 + // =============================================
5785 + if (!empty($all_vector_ids)) {
5786 + $all_vector_ids = array_values(array_unique($all_vector_ids));
5787 + $pinecone_success = true;
5788 + $batches = array_chunk($all_vector_ids, 100);
5789 +
5790 + foreach ($batches as $batch) {
5791 + $delete_response = wp_remote_post("https://{$host}/vectors/delete", array(
5792 + 'headers' => array(
5793 + 'Api-Key' => $api_key,
5794 + 'accept' => 'application/json',
5795 + 'content-type' => 'application/json'
5796 + ),
5797 + 'body' => wp_json_encode(array('ids' => $batch)),
5798 + 'timeout' => 60
5799 + ));
5800 +
5801 + if (is_wp_error($delete_response)) {
5802 + $pinecone_success = false;
5803 + $errors[] = 'Pinecone batch deletion failed: ' . $delete_response->get_error_message();
5804 + MxChat_Admin::mxchat_log_debug('pinecone_error', 'Bulk delete batch failed: ' . $delete_response->get_error_message());
5805 + } else {
5806 + $response_code = wp_remote_retrieve_response_code($delete_response);
5807 + if ($response_code !== 200) {
5808 + $pinecone_success = false;
5809 + $response_body = wp_remote_retrieve_body($delete_response);
5810 + $errors[] = "Pinecone API error (HTTP $response_code)";
5811 + MxChat_Admin::mxchat_log_debug('pinecone_error', 'Bulk delete batch failed (HTTP ' . $response_code . ')', array('response' => substr($response_body, 0, 200)));
5812 + }
5813 + }
5814 + }
5815 +
5816 + // Mark all pinecone entries based on batch result
5817 + foreach ($pinecone_entry_ids as $eid) {
5818 + if ($pinecone_success) {
5819 + $success_ids[] = $eid;
5820 + } else {
5821 + $failed_ids[] = $eid;
5822 + }
5823 + }
5824 + }
5825 +
5826 + // =============================================
5827 + // PHASE 3: WordPress database deletions
5828 + // =============================================
5829 + foreach ($wordpress_entries as $entry) {
5830 + $entry_id = sanitize_text_field($entry['id'] ?? '');
5831 + $source_url = isset($entry['sourceUrl']) ? esc_url_raw($entry['sourceUrl']) : '';
5832 + $is_group = isset($entry['isGroup']) && ($entry['isGroup'] === true || $entry['isGroup'] === 'true');
5833 +
5834 + if (empty($entry_id)) {
5835 + continue;
5836 + }
5837 +
5838 + try {
5839 + if ($is_group && !empty($source_url)) {
5840 + $result = $wpdb->delete(
5841 + $table_name,
5842 + array('source_url' => $source_url),
5843 + array('%s')
5844 + );
5845 + } else {
5846 + wp_cache_delete('prompt_' . $entry_id, 'mxchat_prompts');
5847 + $result = $wpdb->delete(
5848 + $table_name,
5849 + array('id' => intval($entry_id)),
5850 + array('%d')
5851 + );
5852 + }
5853 +
5854 + if ($result !== false) {
5855 + $success_ids[] = $entry_id;
5856 + } else {
5857 + $failed_ids[] = $entry_id;
5858 + $errors[] = "Database error for entry: $entry_id";
5859 + }
5860 + } catch (Exception $e) {
5861 + $failed_ids[] = $entry_id;
5862 + $errors[] = $e->getMessage();
5863 + }
5864 + }
5865 +
5866 + wp_send_json_success(array(
5867 + 'success_ids' => $success_ids,
5868 + 'failed_ids' => $failed_ids,
5869 + 'errors' => $errors,
5870 + 'total_processed' => count($success_ids) + count($failed_ids)
5871 + ));
5872 +
5873 + exit;
5874 +}
5875 +
5876 +/**
5877 + * Get hierarchical roles for dropdown
5878 + */
5879 +public function mxchat_get_role_options() {
5880 + return array(
5881 + 'public' => __('Public (Everyone)', 'mxchat'),
5882 + 'logged_in' => __('Logged In Users', 'mxchat'),
5883 + 'subscriber' => __('Subscribers & Above', 'mxchat'),
5884 + 'contributor' => __('Contributors & Above', 'mxchat'),
5885 + 'author' => __('Authors & Above', 'mxchat'),
5886 + 'editor' => __('Editors & Above', 'mxchat'),
5887 + 'administrator' => __('Administrators Only', 'mxchat')
5888 + );
5889 +}
5890 +
5891 +/**
5892 + * Check if user has access to content based on role restriction
5893 + */
5894 +public function mxchat_user_has_content_access($role_restriction) {
5895 + // Public content is always accessible
5896 + if ($role_restriction === 'public' || empty($role_restriction)) {
5897 + return true;
5898 + }
5899 +
5900 + // Check if user is logged in for logged_in restriction
5901 + if ($role_restriction === 'logged_in') {
5902 + return is_user_logged_in();
5903 + }
5904 +
5905 + // If not logged in, no access to role-restricted content
5906 + if (!is_user_logged_in()) {
5907 + return false;
5908 + }
5909 +
5910 + $user = wp_get_current_user();
5911 + $user_roles = $user->roles;
5912 +
5913 + if (empty($user_roles)) {
5914 + return false;
5915 + }
5916 +
5917 + // Define role hierarchy (higher number = higher access)
5918 + $hierarchy = array(
5919 + 'subscriber' => 1,
5920 + 'contributor' => 2,
5921 + 'author' => 3,
5922 + 'editor' => 4,
5923 + 'administrator' => 5
5924 + );
5925 +
5926 + // Get required level
5927 + $required_level = isset($hierarchy[$role_restriction]) ? $hierarchy[$role_restriction] : 0;
5928 +
5929 + // Check if user has required level or higher
5930 + foreach ($user_roles as $user_role) {
5931 + $user_level = isset($hierarchy[$user_role]) ? $hierarchy[$user_role] : 0;
5932 + if ($user_level >= $required_level) {
5933 + return true;
5934 + }
5935 + }
5936 +
5937 + return false;
5938 +}
5939 +
5940 +/**
5941 + * Handle role restriction updates via AJAX
5942 + * Removed cache clearing call since we removed caching
5943 + */
5944 +public function ajax_mxchat_update_role_restriction() {
5945 + // Verify nonce and permissions
5946 + if (!check_ajax_referer('mxchat_update_role_nonce', 'nonce', false)) {
5947 + wp_send_json_error('Invalid nonce');
5948 + exit;
5949 + }
5950 +
5951 + if (!current_user_can('manage_options')) {
5952 + wp_send_json_error('Unauthorized access');
5953 + exit;
5954 + }
5955 +
5956 + $entry_id = isset($_POST['entry_id']) ? sanitize_text_field($_POST['entry_id']) : '';
5957 + $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
5958 + $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
5959 +
5960 + if (empty($entry_id)) {
5961 + wp_send_json_error('Invalid entry ID');
5962 + exit;
5963 + }
5964 +
5965 + // Get knowledge manager instance to validate role restriction
5966 + $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
5967 + $valid_roles = array_keys($knowledge_manager->mxchat_get_role_options());
5968 + if (!in_array($role_restriction, $valid_roles)) {
5969 + wp_send_json_error('Invalid role restriction');
5970 + exit;
5971 + }
5972 +
5973 + global $wpdb;
5974 +
5975 + if ($data_source === 'pinecone') {
5976 + // Handle Pinecone role restriction (stored separately in WordPress table)
5977 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
5978 +
5979 + // Use REPLACE to insert or update the role restriction
5980 + $result = $wpdb->replace(
5981 + $roles_table,
5982 + array(
5983 + 'vector_id' => $entry_id,
5984 + 'role_restriction' => $role_restriction,
5985 + 'updated_at' => current_time('mysql')
5986 + ),
5987 + array('%s', '%s', '%s')
5988 + );
5989 +
5990 + // No cache clearing needed since we removed caching
5991 +
5992 + } else {
5993 + // Handle WordPress database role restriction (existing functionality)
5994 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5995 +
5996 + $result = $wpdb->update(
5997 + $table_name,
5998 + array('role_restriction' => $role_restriction),
5999 + array('id' => absint($entry_id)),
6000 + array('%s'),
6001 + array('%d')
6002 + );
6003 + }
6004 +
6005 + if ($result === false) {
6006 + wp_send_json_error('Database update failed: ' . $wpdb->last_error);
6007 + exit;
6008 + }
6009 +
6010 + wp_send_json_success(array(
6011 + 'message' => 'Role restriction updated successfully',
6012 + 'role_restriction' => $role_restriction,
6013 + 'data_source' => $data_source,
6014 + 'entry_id' => $entry_id
6015 + ));
6016 + exit;
6017 +}
6018 +
6019 +// ========================================
6020 +// ROLE-BASED CONTENT RESTRICTIONS - NEW FUNCTIONS
6021 +// Add these to your MxChat_Knowledge_Manager class
6022 +// ========================================
6023 +
6024 +/**
6025 + * Initialize role-based content hooks
6026 + * Add this call to your __construct() or mxchat_init_hooks() method
6027 + */
6028 +private function mxchat_init_role_hooks() {
6029 + // AJAX handlers for tag-role mappings
6030 + add_action('wp_ajax_mxchat_add_tag_role_mapping', array($this, 'ajax_add_tag_role_mapping'));
6031 + add_action('wp_ajax_mxchat_delete_tag_role_mapping', array($this, 'ajax_delete_tag_role_mapping'));
6032 + add_action('wp_ajax_mxchat_get_tag_role_mappings', array($this, 'ajax_get_tag_role_mappings'));
6033 + add_action('wp_ajax_mxchat_bulk_update_tag_roles', array($this, 'ajax_bulk_update_tag_roles'));
6034 +
6035 + // Hook to automatically update role restrictions when tags are added/removed
6036 + add_action('set_object_terms', array($this, 'handle_tag_change'), 10, 6);
6037 +
6038 + // Hook to apply role restrictions on auto-sync
6039 + add_action('mxchat_content_stored', array($this, 'apply_role_restriction_after_storage'), 10, 2);
6040 +}
6041 +
6042 +/**
6043 + * Add tag-role mapping via AJAX
6044 + */
6045 +public function ajax_add_tag_role_mapping() {
6046 + // Verify nonce and permissions
6047 + check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6048 +
6049 + if (!current_user_can('manage_options')) {
6050 + wp_send_json_error('Unauthorized access');
6051 + exit;
6052 + }
6053 +
6054 + $tag_slug = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
6055 + $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
6056 +
6057 + if (empty($tag_slug)) {
6058 + wp_send_json_error('Tag slug is required');
6059 + exit;
6060 + }
6061 +
6062 + // Validate role restriction
6063 + $valid_roles = array_keys($this->mxchat_get_role_options());
6064 + if (!in_array($role_restriction, $valid_roles)) {
6065 + wp_send_json_error('Invalid role restriction');
6066 + exit;
6067 + }
6068 +
6069 + // Check if tag exists in WordPress
6070 + $term = get_term_by('slug', $tag_slug, 'post_tag');
6071 + if (!$term) {
6072 + wp_send_json_error('Tag does not exist in WordPress');
6073 + exit;
6074 + }
6075 +
6076 + // Get existing mappings
6077 + $mappings = get_option('mxchat_tag_role_mappings', array());
6078 +
6079 + // Check if mapping already exists
6080 + if (isset($mappings[$tag_slug])) {
6081 + wp_send_json_error('Mapping for this tag already exists');
6082 + exit;
6083 + }
6084 +
6085 + // Add new mapping
6086 + $mappings[$tag_slug] = $role_restriction;
6087 + update_option('mxchat_tag_role_mappings', $mappings);
6088 +
6089 + wp_send_json_success(array(
6090 + 'message' => 'Tag-role mapping added successfully',
6091 + 'tag_slug' => $tag_slug,
6092 + 'role_restriction' => $role_restriction
6093 + ));
6094 + exit;
6095 +}
6096 +
6097 +/**
6098 + * Delete tag-role mapping via AJAX
6099 + */
6100 +public function ajax_delete_tag_role_mapping() {
6101 + // Verify nonce and permissions
6102 + check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6103 +
6104 + if (!current_user_can('manage_options')) {
6105 + wp_send_json_error('Unauthorized access');
6106 + exit;
6107 + }
6108 +
6109 + $tag_slug = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
6110 +
6111 + if (empty($tag_slug)) {
6112 + wp_send_json_error('Tag slug is required');
6113 + exit;
6114 + }
6115 +
6116 + // Get existing mappings
6117 + $mappings = get_option('mxchat_tag_role_mappings', array());
6118 +
6119 + // Check if mapping exists
6120 + if (!isset($mappings[$tag_slug])) {
6121 + wp_send_json_error('Mapping does not exist');
6122 + exit;
6123 + }
6124 +
6125 + // Remove mapping
6126 + unset($mappings[$tag_slug]);
6127 + update_option('mxchat_tag_role_mappings', $mappings);
6128 +
6129 + wp_send_json_success(array(
6130 + 'message' => 'Tag-role mapping deleted successfully',
6131 + 'tag_slug' => $tag_slug
6132 + ));
6133 + exit;
6134 +}
6135 +
6136 +/**
6137 + * Get all tag-role mappings via AJAX
6138 + */
6139 +public function ajax_get_tag_role_mappings() {
6140 + // Verify nonce and permissions
6141 + check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6142 +
6143 + if (!current_user_can('manage_options')) {
6144 + wp_send_json_error('Unauthorized access');
6145 + exit;
6146 + }
6147 +
6148 + // Get mappings
6149 + $mappings = get_option('mxchat_tag_role_mappings', array());
6150 + $role_options = $this->mxchat_get_role_options();
6151 +
6152 + $formatted_mappings = array();
6153 +
6154 + foreach ($mappings as $tag_slug => $role_restriction) {
6155 + // Get tag object
6156 + $term = get_term_by('slug', $tag_slug, 'post_tag');
6157 +
6158 + // Count posts with this tag
6159 + $post_count = 0;
6160 + if ($term) {
6161 + $post_count = $term->count;
6162 + }
6163 +
6164 + $formatted_mappings[] = array(
6165 + 'tag_slug' => $tag_slug,
6166 + 'role_restriction' => $role_restriction,
6167 + 'role_label' => $role_options[$role_restriction] ?? $role_restriction,
6168 + 'post_count' => $post_count
6169 + );
6170 + }
6171 +
6172 + wp_send_json_success(array(
6173 + 'mappings' => $formatted_mappings
6174 + ));
6175 + exit;
6176 +}
6177 +
6178 +/**
6179 + * Bulk update role restrictions for all existing content with mapped tags
6180 + */
6181 +public function ajax_bulk_update_tag_roles() {
6182 + // Verify nonce and permissions
6183 + check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6184 +
6185 + if (!current_user_can('manage_options')) {
6186 + wp_send_json_error('Unauthorized access');
6187 + exit;
6188 + }
6189 +
6190 + // Get mappings
6191 + $mappings = get_option('mxchat_tag_role_mappings', array());
6192 +
6193 + if (empty($mappings)) {
6194 + wp_send_json_error('No tag-role mappings found');
6195 + exit;
6196 + }
6197 +
6198 + global $wpdb;
6199 +
6200 + // Check if using Pinecone
6201 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
6202 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6203 +
6204 + $updated_count = 0;
6205 + $details = array();
6206 +
6207 + foreach ($mappings as $tag_slug => $role_restriction) {
6208 + // Get all posts with this tag
6209 + $posts = get_posts(array(
6210 + 'tag' => $tag_slug,
6211 + 'post_type' => 'any',
6212 + 'posts_per_page' => -1,
6213 + 'fields' => 'ids',
6214 + 'post_status' => 'publish'
6215 + ));
6216 +
6217 + if (empty($posts)) {
6218 + continue;
6219 + }
6220 +
6221 + $tag_updated = 0;
6222 +
6223 + foreach ($posts as $post_id) {
6224 + $source_url = get_permalink($post_id);
6225 + if (!$source_url) {
6226 + continue;
6227 + }
6228 +
6229 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
6230 + // Update Pinecone role restriction
6231 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6232 + $vector_id = md5($source_url);
6233 +
6234 + $result = $wpdb->replace(
6235 + $roles_table,
6236 + array(
6237 + 'vector_id' => $vector_id,
6238 + 'role_restriction' => $role_restriction,
6239 + 'updated_at' => current_time('mysql')
6240 + ),
6241 + array('%s', '%s', '%s')
6242 + );
6243 + } else {
6244 + // Update WordPress DB
6245 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6246 +
6247 + $result = $wpdb->update(
6248 + $table_name,
6249 + array('role_restriction' => $role_restriction),
6250 + array('source_url' => $source_url),
6251 + array('%s'),
6252 + array('%s')
6253 + );
6254 + }
6255 +
6256 + if ($result !== false) {
6257 + $tag_updated++;
6258 + $updated_count++;
6259 + }
6260 + }
6261 +
6262 + if ($tag_updated > 0) {
6263 + $details[] = sprintf(
6264 + 'Tag "%s" (%s): %d posts updated',
6265 + $tag_slug,
6266 + $role_restriction,
6267 + $tag_updated
6268 + );
6269 + }
6270 + }
6271 +
6272 + wp_send_json_success(array(
6273 + 'message' => 'Bulk update completed',
6274 + 'updated_count' => $updated_count,
6275 + 'tags_processed' => count($mappings),
6276 + 'details' => $details
6277 + ));
6278 + exit;
6279 +}
6280 +
6281 +/**
6282 + * Handle tag changes on posts (when tags are added or removed)
6283 + */
6284 +public function handle_tag_change($object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids) {
6285 + // Only process post tags
6286 + if ($taxonomy !== 'post_tag') {
6287 + return;
6288 + }
6289 +
6290 + // Get tag-role mappings
6291 + $mappings = get_option('mxchat_tag_role_mappings', array());
6292 +
6293 + if (empty($mappings)) {
6294 + return;
6295 + }
6296 +
6297 + // Get the post's URL
6298 + $source_url = get_permalink($object_id);
6299 + if (!$source_url) {
6300 + return;
6301 + }
6302 +
6303 + // Determine the highest role restriction based on tags
6304 + $highest_role = 'public';
6305 + $role_hierarchy = array(
6306 + 'public' => 0,
6307 + 'logged_in' => 1,
6308 + 'subscriber' => 2,
6309 + 'contributor' => 3,
6310 + 'author' => 4,
6311 + 'editor' => 5,
6312 + 'administrator' => 6
6313 + );
6314 +
6315 + // Get all current tags for the post
6316 + $current_tags = wp_get_post_tags($object_id, array('fields' => 'slugs'));
6317 +
6318 + // Find the highest role restriction among the tags
6319 + foreach ($current_tags as $tag_slug) {
6320 + if (isset($mappings[$tag_slug])) {
6321 + $role = $mappings[$tag_slug];
6322 + if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
6323 + $highest_role = $role;
6324 + }
6325 + }
6326 + }
6327 +
6328 + // Update the role restriction in the database
6329 + global $wpdb;
6330 +
6331 + // Check if using Pinecone
6332 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
6333 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6334 +
6335 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
6336 + // Update Pinecone role restriction
6337 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6338 + $vector_id = md5($source_url);
6339 +
6340 + $wpdb->replace(
6341 + $roles_table,
6342 + array(
6343 + 'vector_id' => $vector_id,
6344 + 'role_restriction' => $highest_role,
6345 + 'updated_at' => current_time('mysql')
6346 + ),
6347 + array('%s', '%s', '%s')
6348 + );
6349 + } else {
6350 + // Update WordPress DB
6351 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6352 +
6353 + $wpdb->update(
6354 + $table_name,
6355 + array('role_restriction' => $highest_role),
6356 + array('source_url' => $source_url),
6357 + array('%s'),
6358 + array('%s')
6359 + );
6360 + }
6361 +}
6362 +
6363 +/**
6364 + * Apply role restriction after content is stored (for auto-sync)
6365 + */
6366 +public function apply_role_restriction_after_storage($post_id, $source_url) {
6367 + // Get tag-role mappings
6368 + $mappings = get_option('mxchat_tag_role_mappings', array());
6369 +
6370 + if (empty($mappings)) {
6371 + return;
6372 + }
6373 +
6374 + // Get all tags for the post
6375 + $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
6376 +
6377 + if (empty($post_tags)) {
6378 + return;
6379 + }
6380 +
6381 + // Determine the highest role restriction based on tags
6382 + $highest_role = 'public';
6383 + $role_hierarchy = array(
6384 + 'public' => 0,
6385 + 'logged_in' => 1,
6386 + 'subscriber' => 2,
6387 + 'contributor' => 3,
6388 + 'author' => 4,
6389 + 'editor' => 5,
6390 + 'administrator' => 6
6391 + );
6392 +
6393 + foreach ($post_tags as $tag_slug) {
6394 + if (isset($mappings[$tag_slug])) {
6395 + $role = $mappings[$tag_slug];
6396 + if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
6397 + $highest_role = $role;
6398 + }
6399 + }
6400 + }
6401 +
6402 + // If no restricted tags found, return (leave as public)
6403 + if ($highest_role === 'public') {
6404 + return;
6405 + }
6406 +
6407 + // Update the role restriction
6408 + global $wpdb;
6409 +
6410 + // Check if using Pinecone
6411 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
6412 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6413 +
6414 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
6415 + // Update Pinecone role restriction
6416 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6417 + $vector_id = md5($source_url);
6418 +
6419 + $wpdb->replace(
6420 + $roles_table,
6421 + array(
6422 + 'vector_id' => $vector_id,
6423 + 'role_restriction' => $highest_role,
6424 + 'updated_at' => current_time('mysql')
6425 + ),
6426 + array('%s', '%s', '%s')
6427 + );
6428 + } else {
6429 + // Update WordPress DB
6430 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6431 +
6432 + $wpdb->update(
6433 + $table_name,
6434 + array('role_restriction' => $highest_role),
6435 + array('source_url' => $source_url),
6436 + array('%s'),
6437 + array('%s')
6438 + );
6439 + }
6440 +}
6441 +
6442 +
6443 + // ========================================
6444 + // HELPER METHODS
6445 + // ========================================
6446 +
6447 + /**
6448 + * Check if user has required permissions for content processing
6449 + */
6450 + private function mxchat_check_user_permissions() {
6451 + if (!current_user_can('manage_options')) {
6452 + wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
6453 + }
6454 + }
6455 +
6456 + /**
6457 + * Validate nonce for security
6458 + */
6459 + private function mxchat_validate_nonce($nonce_name, $nonce_action) {
6460 + if (!isset($_POST[$nonce_name]) || !wp_verify_nonce($_POST[$nonce_name], $nonce_action)) {
6461 + wp_die(esc_html__('Security check failed.', 'mxchat'));
6462 + }
6463 + }
6464 +
6465 + /**
6466 + * Get embedding API credentials
6467 + */
6468 + private function mxchat_get_embedding_credentials() {
6469 + $embedding_model = $this->options['embedding_model'] ?? 'text-embedding-ada-002';
6470 +
6471 + if (strpos($embedding_model, 'text-embedding-') !== false) {
6472 + return array(
6473 + 'type' => 'openai',
6474 + 'api_key' => $this->options['api_key'] ?? ''
6475 + );
6476 + } elseif (strpos($embedding_model, 'voyage-') !== false) {
6477 + return array(
6478 + 'type' => 'voyage',
6479 + 'api_key' => $this->options['voyage_api_key'] ?? ''
6480 + );
6481 + } elseif (strpos($embedding_model, 'gemini-embedding-') !== false) {
6482 + return array(
6483 + 'type' => 'gemini',
6484 + 'api_key' => $this->options['gemini_api_key'] ?? ''
6485 + );
6486 + }
6487 +
6488 + return array('type' => 'unknown', 'api_key' => '');
6489 + }
6490 +
6491 + /**
6492 + * Log processing errors
6493 + */
6494 + private function mxchat_log_processing_error($operation, $error_message) {
6495 + //error_log("MxChat Knowledge Processing {$operation} Error: " . $error_message);
6496 + }
6497 +
6498 + /**
6499 + * Set admin notice transient
6500 + */
6501 + private function mxchat_set_admin_notice($type, $message) {
6502 + set_transient("mxchat_admin_notice_{$type}", $message, 30);
6503 + }
6504 +
6505 + /**
6506 + * Get Pinecone manager instance for vector operations
6507 + */
6508 + private function mxchat_get_pinecone_manager() {
6509 + return MxChat_Pinecone_Manager::get_instance();
6510 + }
6511 +
6512 +
6513 + // ========================================
6514 +// DATABASE QUEUE TABLE MANAGEMENT
6515 +// ========================================
6516 +
6517 +/**
6518 + * Create queue table on plugin activation
6519 + * Call this from your plugin activation hook
6520 + */
6521 +public function mxchat_create_queue_table() {
6522 + global $wpdb;
6523 +
6524 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6525 + $charset_collate = $wpdb->get_charset_collate();
6526 +
6527 + $sql = "CREATE TABLE IF NOT EXISTS $table_name (
6528 + id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
6529 + queue_id varchar(64) NOT NULL,
6530 + item_type varchar(20) NOT NULL,
6531 + item_data longtext NOT NULL,
6532 + status varchar(20) NOT NULL DEFAULT 'pending',
6533 + bot_id varchar(50) NOT NULL DEFAULT 'default',
6534 + priority int(11) NOT NULL DEFAULT 0,
6535 + attempts int(11) NOT NULL DEFAULT 0,
6536 + max_attempts int(11) NOT NULL DEFAULT 3,
6537 + error_message text DEFAULT NULL,
6538 + created_at datetime NOT NULL,
6539 + started_at datetime DEFAULT NULL,
6540 + completed_at datetime DEFAULT NULL,
6541 + PRIMARY KEY (id),
6542 + KEY queue_id (queue_id),
6543 + KEY status (status),
6544 + KEY item_type (item_type),
6545 + KEY priority (priority)
6546 + ) $charset_collate;";
6547 +
6548 + require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
6549 + dbDelta($sql);
6550 +
6551 + // Also create a meta table for queue metadata
6552 + $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
6553 +
6554 + $meta_sql = "CREATE TABLE IF NOT EXISTS $meta_table (
6555 + id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
6556 + queue_id varchar(64) NOT NULL,
6557 + meta_key varchar(255) NOT NULL,
6558 + meta_value longtext,
6559 + PRIMARY KEY (id),
6560 + KEY queue_id (queue_id),
6561 + KEY meta_key (meta_key)
6562 + ) $charset_collate;";
6563 +
6564 + dbDelta($meta_sql);
6565 +}
6566 +
6567 +/**
6568 + * Add items to the processing queue
6569 + *
6570 + * @param string $queue_id Unique identifier for this queue batch
6571 + * @param string $item_type Type of item (url, pdf_page)
6572 + * @param array $items Array of items to queue
6573 + * @param string $bot_id Bot ID for processing
6574 + * @return int Number of items queued
6575 + */
6576 +private function mxchat_add_to_queue($queue_id, $item_type, $items, $bot_id = 'default') {
6577 + global $wpdb;
6578 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6579 +
6580 + $queued_count = 0;
6581 + $priority = 0;
6582 +
6583 + foreach ($items as $item) {
6584 + $result = $wpdb->insert(
6585 + $table_name,
6586 + array(
6587 + 'queue_id' => $queue_id,
6588 + 'item_type' => $item_type,
6589 + 'item_data' => wp_json_encode($item),
6590 + 'status' => 'pending',
6591 + 'bot_id' => $bot_id,
6592 + 'priority' => $priority,
6593 + 'attempts' => 0,
6594 + 'max_attempts' => 3,
6595 + 'created_at' => current_time('mysql')
6596 + ),
6597 + array('%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%s')
6598 + );
6599 +
6600 + if ($result) {
6601 + $queued_count++;
6602 + }
6603 +
6604 + $priority++; // Process in order
6605 + }
6606 +
6607 + return $queued_count;
6608 +}
6609 +
6610 +/**
6611 + * Store queue metadata (total counts, source URL, etc.)
6612 + */
6613 +private function mxchat_set_queue_meta($queue_id, $meta_key, $meta_value) {
6614 + global $wpdb;
6615 + $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
6616 +
6617 + // Check if meta exists
6618 + $existing = $wpdb->get_var($wpdb->prepare(
6619 + "SELECT id FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
6620 + $queue_id,
6621 + $meta_key
6622 + ));
6623 +
6624 + if ($existing) {
6625 + // Update
6626 + $wpdb->update(
6627 + $meta_table,
6628 + array('meta_value' => maybe_serialize($meta_value)),
6629 + array('queue_id' => $queue_id, 'meta_key' => $meta_key),
6630 + array('%s'),
6631 + array('%s', '%s')
6632 + );
6633 + } else {
6634 + // Insert
6635 + $wpdb->insert(
6636 + $meta_table,
6637 + array(
6638 + 'queue_id' => $queue_id,
6639 + 'meta_key' => $meta_key,
6640 + 'meta_value' => maybe_serialize($meta_value)
6641 + ),
6642 + array('%s', '%s', '%s')
6643 + );
6644 + }
6645 +}
6646 +
6647 +/**
6648 + * Get queue metadata
6649 + */
6650 +private function mxchat_get_queue_meta($queue_id, $meta_key) {
6651 + global $wpdb;
6652 + $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
6653 +
6654 + $value = $wpdb->get_var($wpdb->prepare(
6655 + "SELECT meta_value FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
6656 + $queue_id,
6657 + $meta_key
6658 + ));
6659 +
6660 + return maybe_unserialize($value);
6661 +}
6662 +
6663 +// ========================================
6664 +// AJAX QUEUE PROCESSING HANDLERS
6665 +// ========================================
6666 +
6667 +/**
6668 + * AJAX: Get next item from queue to process
6669 + */
6670 +public function ajax_mxchat_get_next_queue_item() {
6671 + // Verify nonce and permissions
6672 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
6673 +
6674 + if (!current_user_can('manage_options')) {
6675 + wp_send_json_error('Unauthorized access');
6676 + }
6677 +
6678 + $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
6679 +
6680 + if (empty($queue_id)) {
6681 + wp_send_json_error('Missing queue ID');
6682 + }
6683 +
6684 + global $wpdb;
6685 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6686 +
6687 + // Get next pending item with retry logic for failed items
6688 + $next_item = $wpdb->get_row($wpdb->prepare(
6689 + "SELECT * FROM $table_name
6690 + WHERE queue_id = %s
6691 + AND status IN ('pending', 'failed')
6692 + AND attempts < max_attempts
6693 + ORDER BY priority ASC, id ASC
6694 + LIMIT 1",
6695 + $queue_id
6696 + ));
6697 +
6698 + if (!$next_item) {
6699 + // No more items - queue complete
6700 + wp_send_json_success(array(
6701 + 'complete' => true,
6702 + 'message' => 'Queue processing complete'
6703 + ));
6704 + }
6705 +
6706 + // Mark item as processing
6707 + $wpdb->update(
6708 + $table_name,
6709 + array(
6710 + 'status' => 'processing',
6711 + 'started_at' => current_time('mysql'),
6712 + 'attempts' => $next_item->attempts + 1
6713 + ),
6714 + array('id' => $next_item->id),
6715 + array('%s', '%s', '%d'),
6716 + array('%d')
6717 + );
6718 +
6719 + wp_send_json_success(array(
6720 + 'complete' => false,
6721 + 'item' => array(
6722 + 'id' => $next_item->id,
6723 + 'type' => $next_item->item_type,
6724 + 'data' => json_decode($next_item->item_data, true),
6725 + 'bot_id' => $next_item->bot_id,
6726 + 'attempt' => $next_item->attempts + 1
6727 + )
6728 + ));
6729 +}
6730 +
6731 +/**
6732 + * AJAX: Process a single queue item
6733 + */
6734 +public function ajax_mxchat_process_queue_item() {
6735 + // Verify nonce and permissions
6736 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
6737 +
6738 + if (!current_user_can('manage_options')) {
6739 + wp_send_json_error('Unauthorized access');
6740 + }
6741 +
6742 + $item_id = isset($_POST['item_id']) ? absint($_POST['item_id']) : 0;
6743 + $item_type = isset($_POST['item_type']) ? sanitize_text_field($_POST['item_type']) : '';
6744 + $item_data = isset($_POST['item_data']) ? $_POST['item_data'] : array();
6745 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
6746 +
6747 + if (empty($item_id) || empty($item_type)) {
6748 + wp_send_json_error('Missing item data');
6749 + }
6750 +
6751 + global $wpdb;
6752 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6753 +
6754 + // Process based on item type
6755 + try {
6756 + set_time_limit(60); // Give processing 60 seconds
6757 +
6758 + $result = false;
6759 + $error_message = '';
6760 +
6761 + switch ($item_type) {
6762 + case 'url':
6763 + $result = $this->mxchat_process_queue_url($item_data, $bot_id);
6764 + break;
6765 +
6766 + case 'pdf_page':
6767 + $result = $this->mxchat_process_queue_pdf_page($item_data, $bot_id);
6768 + break;
6769 +
6770 + default:
6771 + throw new Exception('Unknown item type: ' . $item_type);
6772 + }
6773 +
6774 + if (is_wp_error($result)) {
6775 + throw new Exception($result->get_error_message());
6776 + }
6777 +
6778 + if ($result === false) {
6779 + throw new Exception('Processing returned false - item may be empty or invalid');
6780 + }
6781 +
6782 + // Mark as completed
6783 + $wpdb->update(
6784 + $table_name,
6785 + array(
6786 + 'status' => 'completed',
6787 + 'completed_at' => current_time('mysql'),
6788 + 'error_message' => null
6789 + ),
6790 + array('id' => $item_id),
6791 + array('%s', '%s', '%s'),
6792 + array('%d')
6793 + );
6794 +
6795 + wp_send_json_success(array(
6796 + 'processed' => true,
6797 + 'item_id' => $item_id,
6798 + 'message' => 'Item processed successfully'
6799 + ));
6800 +
6801 + } catch (Exception $e) {
6802 + $error_message = $e->getMessage();
6803 +
6804 + // Get current attempt count
6805 + $item = $wpdb->get_row($wpdb->prepare(
6806 + "SELECT attempts, max_attempts FROM $table_name WHERE id = %d",
6807 + $item_id
6808 + ));
6809 +
6810 + // Check if we've exhausted retries
6811 + if ($item && $item->attempts >= $item->max_attempts) {
6812 + // Permanently failed
6813 + $wpdb->update(
6814 + $table_name,
6815 + array(
6816 + 'status' => 'failed',
6817 + 'error_message' => $error_message
6818 + ),
6819 + array('id' => $item_id),
6820 + array('%s', '%s'),
6821 + array('%d')
6822 + );
6823 +
6824 + wp_send_json_error(array(
6825 + 'message' => 'Item failed after maximum attempts: ' . $error_message,
6826 + 'permanent_failure' => true,
6827 + 'item_id' => $item_id
6828 + ));
6829 + } else {
6830 + // Mark for retry
6831 + $wpdb->update(
6832 + $table_name,
6833 + array(
6834 + 'status' => 'failed',
6835 + 'error_message' => $error_message
6836 + ),
6837 + array('id' => $item_id),
6838 + array('%s', '%s'),
6839 + array('%d')
6840 + );
6841 +
6842 + wp_send_json_error(array(
6843 + 'message' => 'Item processing failed, will retry: ' . $error_message,
6844 + 'can_retry' => true,
6845 + 'item_id' => $item_id,
6846 + 'attempts' => $item ? $item->attempts : 0
6847 + ));
6848 + }
6849 + }
6850 +}
6851 +
6852 +/**
6853 + * Process a URL from the queue
6854 + */
6855 +private function mxchat_process_queue_url($item_data, $bot_id = 'default') {
6856 + $url = isset($item_data['url']) ? $item_data['url'] : '';
6857 +
6858 + if (empty($url)) {
6859 + return new WP_Error('invalid_url', 'URL is empty');
6860 + }
6861 +
6862 + // Get bot-specific API key early (needed for both paths)
6863 + $bot_options = $this->get_bot_options($bot_id);
6864 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
6865 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
6866 +
6867 + if (strpos($selected_model, 'voyage') === 0) {
6868 + $api_key = $options['voyage_api_key'] ?? '';
6869 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
6870 + $api_key = $options['gemini_api_key'] ?? '';
6871 + } else {
6872 + $api_key = $options['api_key'] ?? '';
6873 + }
6874 +
6875 + if (empty($api_key)) {
6876 + return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
6877 + }
6878 +
6879 + // Check if this is a WooCommerce product URL and WooCommerce is active
6880 + $is_product_url = (strpos($url, '/product/') !== false || strpos($url, '/shop/') !== false);
6881 + $content_type = $is_product_url ? 'product' : 'url';
6882 +
6883 + // Try to get WooCommerce product data if it's a product URL
6884 + if ($is_product_url && class_exists('WooCommerce')) {
6885 + $product_content = $this->mxchat_extract_woocommerce_product_content($url);
6886 +
6887 + if (!empty($product_content)) {
6888 + // Successfully extracted WooCommerce product data with pricing
6889 + $result = MxChat_Utils::submit_content_to_db(
6890 + $product_content,
6891 + $url,
6892 + $api_key,
6893 + null,
6894 + $bot_id,
6895 + 'product'
6896 + );
6897 + return $result;
6898 + }
6899 + // If WooCommerce extraction failed, fall through to HTML extraction
6900 + }
6901 +
6902 + // Fetch URL content (fallback for non-products or when WooCommerce extraction fails)
6903 + $response = wp_remote_get($url, array(
6904 + 'timeout' => 30,
6905 + 'redirection' => 5,
6906 + 'user-agent' => 'MxChat/1.0'
6907 + ));
6908 +
6909 + if (is_wp_error($response)) {
6910 + return $response;
6911 + }
6912 +
6913 + $response_code = wp_remote_retrieve_response_code($response);
6914 + if ($response_code !== 200) {
6915 + return new WP_Error('http_error', 'HTTP ' . $response_code . ' error');
6916 + }
6917 +
6918 + $html = wp_remote_retrieve_body($response);
6919 +
6920 + if (empty($html)) {
6921 + return new WP_Error('empty_response', 'Empty response body');
6922 + }
6923 +
6924 + // Extract and sanitize content
6925 + $content = $this->mxchat_extract_main_content($html);
6926 + $sanitized = $this->mxchat_sanitize_content_for_api($content);
6927 +
6928 + if (empty($sanitized)) {
6929 + // Not an error - just no content found (maybe a redirect or empty page)
6930 + return false;
6931 + }
6932 +
6933 + // Submit to database with content_type
6934 + $result = MxChat_Utils::submit_content_to_db(
6935 + $sanitized,
6936 + $url,
6937 + $api_key,
6938 + null,
6939 + $bot_id,
6940 + $content_type
6941 + );
6942 +
6943 + return $result;
6944 +}
6945 +
6946 +/**
6947 + * Extract WooCommerce product content including pricing
6948 + *
6949 + * @param string $url The product URL
6950 + * @return string|false Product content with pricing, or false if not found
6951 + */
6952 +private function mxchat_extract_woocommerce_product_content($url) {
6953 + // Try to get product ID from URL
6954 + $product_id = url_to_postid($url);
6955 +
6956 + // If url_to_postid fails, try to extract from URL pattern
6957 + if (!$product_id) {
6958 + $product_slug = '';
6959 +
6960 + // Handle pretty permalinks: /product/product-name/
6961 + if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
6962 + $product_slug = $matches[1];
6963 + }
6964 +
6965 + if (!empty($product_slug)) {
6966 + $product_post = get_page_by_path($product_slug, OBJECT, 'product');
6967 + if ($product_post) {
6968 + $product_id = $product_post->ID;
6969 + }
6970 + }
6971 + }
6972 +
6973 + if (!$product_id) {
6974 + return false;
6975 + }
6976 +
6977 + // Get WooCommerce product object
6978 + $product = wc_get_product($product_id);
6979 +
6980 + if (!$product) {
6981 + return false;
6982 + }
6983 +
6984 + // Build product content with pricing (similar to mxchat_store_product_embedding)
6985 + $title = $product->get_name();
6986 + $description = $product->get_description();
6987 + $short_description = $product->get_short_description();
6988 + $sku = $product->get_sku();
6989 +
6990 + // Get pricing information
6991 + $regular_price = $product->get_regular_price();
6992 + $sale_price = $product->get_sale_price();
6993 + $price = $product->get_price(); // Current active price
6994 +
6995 + // Get currency symbol
6996 + $currency_symbol = get_woocommerce_currency_symbol();
6997 +
6998 + // Format content
6999 + $content = $title . "\n\n";
7000 +
7001 + if (!empty($short_description)) {
7002 + $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
7003 + }
7004 +
7005 + if (!empty($description)) {
7006 + $content .= wp_strip_all_tags($description) . "\n\n";
7007 + }
7008 +
7009 + // Add pricing information
7010 + if (!empty($regular_price)) {
7011 + $content .= "Price: " . $currency_symbol . $regular_price . "\n";
7012 + } elseif (!empty($price)) {
7013 + $content .= "Price: " . $currency_symbol . $price . "\n";
7014 + }
7015 +
7016 + if (!empty($sale_price) && $sale_price !== $regular_price) {
7017 + $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
7018 + }
7019 +
7020 + // Handle variable products - show price range
7021 + if ($product->is_type('variable')) {
7022 + $min_price = $product->get_variation_price('min');
7023 + $max_price = $product->get_variation_price('max');
7024 + if ($min_price !== $max_price) {
7025 + $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
7026 + }
7027 + }
7028 +
7029 + if (!empty($sku)) {
7030 + $content .= "SKU: " . $sku . "\n";
7031 + }
7032 +
7033 + // Get product categories
7034 + $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
7035 + if (!empty($categories) && !is_wp_error($categories)) {
7036 + $content .= "Categories: " . implode(', ', $categories) . "\n";
7037 + }
7038 +
7039 + // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
7040 + $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
7041 + if (!empty($custom_tabs) && is_array($custom_tabs)) {
7042 + foreach ($custom_tabs as $tab) {
7043 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
7044 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
7045 +
7046 + if (!empty($tab_title) && !empty($tab_content)) {
7047 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
7048 + }
7049 + }
7050 + }
7051 +
7052 + // Also check for reusable/saved tabs applied to this product
7053 + $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
7054 + if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
7055 + $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
7056 + if (!empty($saved_tabs) && is_array($saved_tabs)) {
7057 + foreach ($applied_saved_tabs as $saved_tab_id) {
7058 + if (isset($saved_tabs[$saved_tab_id])) {
7059 + $tab = $saved_tabs[$saved_tab_id];
7060 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
7061 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
7062 +
7063 + if (!empty($tab_title) && !empty($tab_content)) {
7064 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
7065 + }
7066 + }
7067 + }
7068 + }
7069 + }
7070 +
7071 + return $this->mxchat_sanitize_content_for_api($content);
7072 +}
7073 +
7074 +/**
7075 + * Process a PDF page from the queue
7076 + */
7077 +private function mxchat_process_queue_pdf_page($item_data, $bot_id = 'default') {
7078 + $pdf_path = isset($item_data['pdf_path']) ? $item_data['pdf_path'] : '';
7079 + $pdf_url = isset($item_data['pdf_url']) ? $item_data['pdf_url'] : '';
7080 + $page_number = isset($item_data['page_number']) ? absint($item_data['page_number']) : 0;
7081 + $total_pages = isset($item_data['total_pages']) ? absint($item_data['total_pages']) : 0;
7082 +
7083 + if (empty($pdf_path) || !file_exists($pdf_path)) {
7084 + return new WP_Error('pdf_not_found', 'PDF file not found: ' . $pdf_path);
7085 + }
7086 +
7087 + if ($page_number < 1) {
7088 + return new WP_Error('invalid_page', 'Invalid page number');
7089 + }
7090 +
7091 + try {
7092 + mxchat_load_pdf_parser();
7093 + $parser = new \Smalot\PdfParser\Parser();
7094 + $pdf = $parser->parseFile($pdf_path);
7095 + $pages = $pdf->getPages();
7096 +
7097 + if (!isset($pages[$page_number - 1])) {
7098 + return new WP_Error('page_not_found', 'Page ' . $page_number . ' not found in PDF');
7099 + }
7100 +
7101 + $text = $pages[$page_number - 1]->getText();
7102 +
7103 + if (empty($text)) {
7104 + // Not an error - just an empty page
7105 + return false;
7106 + }
7107 +
7108 + $sanitized = $this->mxchat_sanitize_content_for_api($text);
7109 +
7110 + if (empty($sanitized)) {
7111 + return false;
7112 + }
7113 +
7114 + // Create metadata
7115 + $metadata = array(
7116 + 'document_type' => 'pdf',
7117 + 'total_pages' => $total_pages,
7118 + 'current_page' => $page_number,
7119 + 'source_url' => $pdf_url
7120 + );
7121 +
7122 + $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized;
7123 + $page_url = esc_url($pdf_url . "#page=" . $page_number);
7124 +
7125 + // Get bot-specific API key
7126 + $bot_options = $this->get_bot_options($bot_id);
7127 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
7128 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
7129 +
7130 + if (strpos($selected_model, 'voyage') === 0) {
7131 + $api_key = $options['voyage_api_key'] ?? '';
7132 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
7133 + $api_key = $options['gemini_api_key'] ?? '';
7134 + } else {
7135 + $api_key = $options['api_key'] ?? '';
7136 + }
7137 +
7138 + if (empty($api_key)) {
7139 + return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
7140 + }
7141 +
7142 + // Submit to database - UPDATED 2.5.6: Added content_type 'pdf'
7143 + $result = MxChat_Utils::submit_content_to_db(
7144 + $content_with_metadata,
7145 + $page_url,
7146 + $api_key,
7147 + null,
7148 + $bot_id,
7149 + 'pdf'
7150 + );
7151 +
7152 + return $result;
7153 +
7154 + } catch (Exception $e) {
7155 + return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
7156 + }
7157 +}
7158 +
7159 +/**
7160 + * AJAX: Get queue processing status
7161 + */
7162 +public function ajax_mxchat_get_queue_status() {
7163 + // Verify nonce and permissions
7164 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
7165 +
7166 + if (!current_user_can('manage_options')) {
7167 + wp_send_json_error('Unauthorized access');
7168 + }
7169 +
7170 + $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
7171 +
7172 + if (empty($queue_id)) {
7173 + wp_send_json_error('Missing queue ID');
7174 + }
7175 +
7176 + global $wpdb;
7177 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7178 +
7179 + // Get counts by status
7180 + $counts = $wpdb->get_results($wpdb->prepare(
7181 + "SELECT status, COUNT(*) as count
7182 + FROM $table_name
7183 + WHERE queue_id = %s
7184 + GROUP BY status",
7185 + $queue_id
7186 + ), OBJECT_K);
7187 +
7188 + $total = 0;
7189 + $completed = 0;
7190 + $failed = 0;
7191 + $processing = 0;
7192 + $pending = 0;
7193 +
7194 + foreach ($counts as $status => $data) {
7195 + $count = absint($data->count);
7196 + $total += $count;
7197 +
7198 + switch ($status) {
7199 + case 'completed':
7200 + $completed = $count;
7201 + break;
7202 + case 'failed':
7203 + $failed = $count;
7204 + break;
7205 + case 'processing':
7206 + $processing = $count;
7207 + break;
7208 + case 'pending':
7209 + $pending = $count;
7210 + break;
7211 + }
7212 + }
7213 +
7214 + // Calculate percentage
7215 + $percentage = $total > 0 ? round((($completed + $failed) / $total) * 100) : 0;
7216 +
7217 + // Get failed items details
7218 + $failed_items = array();
7219 + if ($failed > 0) {
7220 + $failed_items = $wpdb->get_results($wpdb->prepare(
7221 + "SELECT item_type, item_data, error_message, attempts
7222 + FROM $table_name
7223 + WHERE queue_id = %s
7224 + AND status = 'failed'
7225 + AND attempts >= max_attempts
7226 + ORDER BY id DESC
7227 + LIMIT 50",
7228 + $queue_id
7229 + ));
7230 + }
7231 +
7232 + // Get queue metadata
7233 + $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
7234 + $queue_type = $this->mxchat_get_queue_meta($queue_id, 'queue_type');
7235 +
7236 + // Determine if queue is complete
7237 + $is_complete = ($pending === 0 && $processing === 0);
7238 +
7239 + wp_send_json_success(array(
7240 + 'queue_id' => $queue_id,
7241 + 'queue_type' => $queue_type,
7242 + 'source_url' => $source_url,
7243 + 'total' => $total,
7244 + 'completed' => $completed,
7245 + 'failed' => $failed,
7246 + 'processing' => $processing,
7247 + 'pending' => $pending,
7248 + 'percentage' => $percentage,
7249 + 'is_complete' => $is_complete,
7250 + 'failed_items' => $failed_items,
7251 + 'status' => $is_complete ? 'complete' : 'processing'
7252 + ));
7253 +}
7254 +
7255 +/**
7256 + * AJAX: Clear completed queue
7257 + */
7258 +public function ajax_mxchat_clear_queue() {
7259 + // Verify nonce and permissions
7260 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
7261 +
7262 + if (!current_user_can('manage_options')) {
7263 + wp_send_json_error('Unauthorized access');
7264 + }
7265 +
7266 + $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
7267 +
7268 + if (empty($queue_id)) {
7269 + wp_send_json_error('Missing queue ID');
7270 + }
7271 +
7272 + global $wpdb;
7273 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7274 + $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
7275 +
7276 + // Delete queue items
7277 + $wpdb->delete(
7278 + $table_name,
7279 + array('queue_id' => $queue_id),
7280 + array('%s')
7281 + );
7282 +
7283 + // Delete queue metadata
7284 + $wpdb->delete(
7285 + $meta_table,
7286 + array('queue_id' => $queue_id),
7287 + array('%s')
7288 + );
7289 +
7290 + wp_send_json_success(array(
7291 + 'message' => 'Queue cleared successfully'
7292 + ));
7293 +}
7294 +
7295 +/**
7296 + * AJAX: Retry failed items in queue
7297 + */
7298 +public function ajax_mxchat_retry_failed() {
7299 + // Verify nonce and permissions
7300 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
7301 +
7302 + if (!current_user_can('manage_options')) {
7303 + wp_send_json_error('Unauthorized access');
7304 + }
7305 +
7306 + $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
7307 +
7308 + if (empty($queue_id)) {
7309 + wp_send_json_error('Missing queue ID');
7310 + }
7311 +
7312 + global $wpdb;
7313 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7314 +
7315 + // Reset failed items to pending and reset attempt count
7316 + $updated = $wpdb->update(
7317 + $table_name,
7318 + array(
7319 + 'status' => 'pending',
7320 + 'attempts' => 0,
7321 + 'error_message' => null
7322 + ),
7323 + array(
7324 + 'queue_id' => $queue_id,
7325 + 'status' => 'failed'
7326 + ),
7327 + array('%s', '%d', '%s'),
7328 + array('%s', '%s')
7329 + );
7330 +
7331 + wp_send_json_success(array(
7332 + 'message' => 'Reset ' . $updated . ' failed items for retry',
7333 + 'reset_count' => $updated
7334 + ));
7335 +}
7336 +
7337 +
7338 +public function ajax_mxchat_mark_queue_complete() {
7339 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
7340 +
7341 + if (!current_user_can('manage_options')) {
7342 + wp_send_json_error('Unauthorized access');
7343 + }
7344 +
7345 + $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
7346 +
7347 + if (empty($queue_id)) {
7348 + wp_send_json_error('Missing queue ID');
7349 + }
7350 +
7351 + // Clear active queue transients
7352 + if (strpos($queue_id, 'sitemap_') === 0) {
7353 + delete_transient('mxchat_active_queue_sitemap');
7354 + } else if (strpos($queue_id, 'pdf_') === 0) {
7355 + delete_transient('mxchat_active_queue_pdf');
7356 + }
7357 +
7358 + wp_send_json_success(array('message' => 'Queue marked as complete'));
7359 +}
7360 +
7361 +
7362 + // ========================================
7363 + // STATIC ACCESS METHODS
7364 + // ========================================
7365 +
7366 + /**
7367 + * Get singleton instance
7368 + */
7369 + public static function get_instance() {
7370 + static $instance = null;
7371 + if ($instance === null) {
7372 + $instance = new self();
7373 + }
7374 + return $instance;
7375 + }
7376 +}
7377 +
7378 +// Initialize the Knowledge manager
8162 7379 $mxchat_knowledge_manager = MxChat_Knowledge_Manager::get_instance();