PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.6.0
MxChat – AI Chatbot & Content Generation for WordPress v2.6.0
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 +5241 -7893 3.2.32.6.0 View file →
@@ -1,7894 +1,5242 @@
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 - 'embedding_model' => MxChat_Utils::get_active_embedding_model(),
607 - ),
608 - array('id' => $prompt_id),
609 - array('%s', '%s', '%s', '%s'),
610 - array('%d')
611 - );
612 - if ($updated !== false) {
613 - wp_send_json_success();
614 - } else {
615 - MxChat_Admin::mxchat_log_debug('knowledge_error', 'Database update failed when saving knowledge entry');
616 - wp_send_json_error(esc_html__('Database update failed.', 'mxchat'));
617 - }
618 - } else {
619 - MxChat_Admin::mxchat_log_debug('embedding_error', 'Embedding generation failed for knowledge entry');
620 - wp_send_json_error(esc_html__('Embedding generation failed.', 'mxchat'));
621 - }
622 - } else {
623 - wp_send_json_error(esc_html__('Invalid data.', 'mxchat'));
624 - }
625 -}
626 -
627 -
628 -/**
629 - * AJAX: Get full content for editing — reassembles chunks if needed.
630 - * Works for both WordPress DB and Pinecone entries.
631 - */
632 -public function ajax_mxchat_get_entry_content() {
633 - check_ajax_referer('mxchat_edit_entry_nonce', 'nonce');
634 -
635 - if ( ! current_user_can('manage_options') ) {
636 - wp_send_json_error( array( 'message' => 'Permission denied.' ) );
637 - }
638 -
639 - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : '';
640 - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0;
641 - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress';
642 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
643 -
644 - if ( $data_source === 'pinecone' ) {
645 - // Pinecone: fetch vectors by source_url, reassemble chunks
646 - $content = $this->get_pinecone_entry_content( $source_url, $entry_id, $bot_id );
647 - } else {
648 - // WordPress DB
649 - $content = $this->get_wordpress_entry_content( $source_url, $entry_id );
650 - }
651 -
652 - if ( is_wp_error( $content ) ) {
653 - wp_send_json_error( array( 'message' => $content->get_error_message() ) );
654 - }
655 -
656 - wp_send_json_success( $content );
657 -}
658 -
659 -/**
660 - * Get content from WordPress DB — reassembles chunks by source_url.
661 - */
662 -private function get_wordpress_entry_content( $source_url, $entry_id ) {
663 - global $wpdb;
664 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
665 -
666 - // If we have a source_url, check for chunks
667 - if ( ! empty( $source_url ) && strpos( $source_url, 'mxchat://' ) !== 0 ) {
668 - $rows = $wpdb->get_results( $wpdb->prepare(
669 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE source_url = %s ORDER BY id ASC",
670 - $source_url
671 - ) );
672 -
673 - if ( $rows && count( $rows ) > 1 ) {
674 - // Multiple rows = chunked. Reassemble.
675 - $chunks = array();
676 - foreach ( $rows as $row ) {
677 - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content );
678 - $index = isset( $parsed['metadata']['chunk_index'] ) ? intval( $parsed['metadata']['chunk_index'] ) : count( $chunks );
679 - $chunks[ $index ] = $parsed['text'];
680 - }
681 - ksort( $chunks );
682 - return array(
683 - 'content' => implode( "\n\n", $chunks ),
684 - 'source_url' => $source_url,
685 - 'is_chunked' => true,
686 - 'chunk_count' => count( $chunks ),
687 - 'content_type' => $rows[0]->content_type,
688 - );
689 - } elseif ( $rows && count( $rows ) === 1 ) {
690 - $parsed = MxChat_Chunker::parse_stored_chunk( $rows[0]->article_content );
691 - return array(
692 - 'content' => $parsed['text'],
693 - 'source_url' => $source_url,
694 - 'entry_id' => $rows[0]->id,
695 - 'is_chunked' => false,
696 - 'content_type' => $rows[0]->content_type,
697 - );
698 - }
699 - }
700 -
701 - // Fallback: fetch by ID
702 - if ( $entry_id > 0 ) {
703 - $row = $wpdb->get_row( $wpdb->prepare(
704 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE id = %d",
705 - $entry_id
706 - ) );
707 - if ( $row ) {
708 - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content );
709 - return array(
710 - 'content' => $parsed['text'],
711 - 'source_url' => $row->source_url,
712 - 'entry_id' => $row->id,
713 - 'is_chunked' => false,
714 - 'content_type' => $row->content_type,
715 - );
716 - }
717 - }
718 -
719 - return new WP_Error( 'not_found', 'Entry not found.' );
720 -}
721 -
722 -/**
723 - * Get content from Pinecone — fetches vectors by source_url, reassembles chunks.
724 - */
725 -private function get_pinecone_entry_content( $source_url, $entry_id, $bot_id ) {
726 - if ( ! class_exists('MxChat_Pinecone_Manager') ) {
727 - return new WP_Error( 'pinecone_unavailable', 'Pinecone manager not available.' );
728 - }
729 -
730 - // Get Pinecone config
731 - if ( $bot_id === 'default' || ! class_exists('MxChat_Multi_Bot_Manager') ) {
732 - $pinecone_options = get_option('mxchat_pinecone_addon_options');
733 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
734 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
735 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
736 - } else {
737 - $bot_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
738 - $api_key = $bot_config['api_key'] ?? '';
739 - $host = $bot_config['host'] ?? '';
740 - $namespace = $bot_config['namespace'] ?? '';
741 - }
742 -
743 - if ( empty($host) || empty($api_key) ) {
744 - return new WP_Error( 'pinecone_config', 'Pinecone not configured.' );
745 - }
746 -
747 - // List vectors with the source_url prefix
748 - $base_id = md5( $source_url );
749 - $vector_ids = array( $base_id );
750 -
751 - // Find chunk vectors
752 - $list_url = "https://{$host}/vectors/list";
753 - $list_body = array( 'prefix' => $base_id . '_chunk_', 'limit' => 100 );
754 - if ( ! empty($namespace) ) {
755 - $list_body['namespace'] = $namespace;
756 - }
757 -
758 - $list_resp = wp_remote_post( $list_url, array(
759 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
760 - 'body' => wp_json_encode( $list_body ),
761 - 'timeout' => 15,
762 - ) );
763 -
764 - if ( ! is_wp_error($list_resp) ) {
765 - $list_data = json_decode( wp_remote_retrieve_body($list_resp), true );
766 - if ( ! empty($list_data['vectors']) ) {
767 - foreach ( $list_data['vectors'] as $v ) {
768 - $vector_ids[] = $v['id'];
769 - }
770 - }
771 - }
772 -
773 - // Fetch vectors with metadata
774 - $fetch_url = "https://{$host}/vectors/fetch";
775 - $fetch_body = array( 'ids' => $vector_ids );
776 - if ( ! empty($namespace) ) {
777 - $fetch_body['namespace'] = $namespace;
778 - }
779 -
780 - $fetch_resp = wp_remote_post( $fetch_url, array(
781 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
782 - 'body' => wp_json_encode( $fetch_body ),
783 - 'timeout' => 15,
784 - ) );
785 -
786 - if ( is_wp_error($fetch_resp) ) {
787 - return new WP_Error( 'pinecone_fetch', 'Failed to fetch from Pinecone.' );
788 - }
789 -
790 - $fetch_data = json_decode( wp_remote_retrieve_body($fetch_resp), true );
791 - $vectors = $fetch_data['vectors'] ?? array();
792 -
793 - if ( empty($vectors) ) {
794 - return new WP_Error( 'not_found', 'Entry not found in Pinecone.' );
795 - }
796 -
797 - // Reassemble chunks
798 - $chunks = array();
799 - $content_type = 'content';
800 - foreach ( $vectors as $vid => $vector ) {
801 - $meta = $vector['metadata'] ?? array();
802 - $text = $meta['text'] ?? '';
803 - $index = $meta['chunk_index'] ?? 0;
804 - $content_type = $meta['type'] ?? 'content';
805 - $chunks[ intval($index) ] = $text;
806 - }
807 - ksort( $chunks );
808 -
809 - return array(
810 - 'content' => implode( "\n\n", $chunks ),
811 - 'source_url' => $source_url,
812 - 'is_chunked' => count($chunks) > 1,
813 - 'chunk_count' => count($chunks),
814 - 'content_type' => $content_type,
815 - );
816 -}
817 -
818 -/**
819 - * AJAX: Save edited content — re-chunks and re-embeds as needed.
820 - * Works for both WordPress DB and Pinecone entries.
821 - */
822 -public function ajax_mxchat_save_entry_content() {
823 - check_ajax_referer('mxchat_edit_entry_nonce', 'nonce');
824 -
825 - if ( ! current_user_can('manage_options') ) {
826 - wp_send_json_error( array( 'message' => 'Permission denied.' ) );
827 - }
828 -
829 - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : '';
830 - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0;
831 - $content = isset($_POST['content']) ? wp_kses_post( wp_unslash($_POST['content']) ) : '';
832 - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress';
833 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
834 - $content_type = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : 'content';
835 -
836 - if ( empty($content) ) {
837 - wp_send_json_error( array( 'message' => 'Content cannot be empty.' ) );
838 - }
839 -
840 - // Get the embedding API key
841 - $options = get_option('mxchat_options', array());
842 - $api_key = '';
843 -
844 - if ( $bot_id !== 'default' && class_exists('MxChat_Multi_Bot_Manager') ) {
845 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
846 - $api_key = $bot_options['api_key'] ?? '';
847 - }
848 - if ( empty($api_key) ) {
849 - $api_key = $options['api_key'] ?? '';
850 - }
851 -
852 - global $wpdb;
853 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
854 -
855 - // If source_url is empty but we have an entry_id, look it up
856 - if ( empty($source_url) && $entry_id > 0 && $data_source === 'wordpress' ) {
857 - $row = $wpdb->get_row( $wpdb->prepare( "SELECT source_url FROM {$table} WHERE id = %d", $entry_id ) );
858 - if ( $row && ! empty($row->source_url) ) {
859 - $source_url = $row->source_url;
860 - }
861 - }
862 -
863 - // For manual entries (no source_url or mxchat:// prefix), delete the old entry by ID first
864 - // so submit_content_to_db creates a replacement instead of a duplicate
865 - // Also treat legacy mxchat.ai source URLs as manual — old bug assigned the site URL to manual entries
866 - $is_legacy_manual = !empty($source_url) && strpos($source_url, 'mxchat.ai') !== false && strpos($source_url, 'mxchat://') !== 0;
867 - if ( $entry_id > 0 && (empty($source_url) || strpos($source_url, 'mxchat://') === 0 || $is_legacy_manual) ) {
868 - $wpdb->delete( $table, array( 'id' => $entry_id ), array( '%d' ) );
869 - // Clear legacy URL so submit_content_to_db generates a unique mxchat:// identifier
870 - // instead of reusing the shared URL (which would mass-delete other entries with the same URL)
871 - if ( $is_legacy_manual ) {
872 - $source_url = '';
873 - }
874 - }
875 -
876 - // Use the existing submit_content_to_db which handles chunking, Pinecone, and WP DB
877 - $vector_id = ! empty($source_url) ? md5($source_url) : md5('mxchat_manual_' . $entry_id);
878 -
879 - // submit_content_to_db already handles: delete old chunks → re-chunk → re-embed → store
880 - $result = MxChat_Utils::submit_content_to_db( $content, $source_url, $api_key, $vector_id, $bot_id, $content_type );
881 -
882 - if ( is_wp_error($result) ) {
883 - wp_send_json_error( array( 'message' => $result->get_error_message() ) );
884 - }
885 -
886 - wp_send_json_success( array( 'message' => 'Content saved and re-embedded successfully.' ) );
887 -}
888 -
889 -public function mxchat_get_pdf_processing_status($pdf_url) {
890 - $pdf_url = esc_url_raw($pdf_url);
891 - $status = get_transient(sanitize_key('mxchat_pdf_status_' . md5($pdf_url)));
892 -
893 - if (!$status || !is_array($status)) {
894 - return false;
895 - }
896 -
897 - // Check for stalled processing (no updates for 5 minutes)
898 - if ($status['status'] === 'processing' && (time() - absint($status['last_update'])) > 300) {
899 - $status['status'] = 'error';
900 - $status['error'] = __('PDF processing appears to be stalled. No updates for over 5 minutes.', 'mxchat');
901 -
902 - // Save the updated status
903 - set_transient(
904 - sanitize_key('mxchat_pdf_status_' . md5($pdf_url)),
905 - array_map('sanitize_text_field', $status),
906 - DAY_IN_SECONDS
907 - );
908 - }
909 -
910 - $result = array(
911 - 'total_pages' => absint($status['total_pages']),
912 - 'processed_pages' => absint($status['processed_pages']),
913 - 'failed_pages' => absint($status['failed_pages'] ?? 0),
914 - 'percentage' => ($status['total_pages'] > 0)
915 - ? round((absint($status['processed_pages']) / absint($status['total_pages'])) * 100)
916 - : 0,
917 - 'status' => sanitize_text_field($status['status']),
918 - 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
919 - 'failed_pages_list' => isset($status['failed_pages_list']) ? $status['failed_pages_list'] : array(),
920 - 'completion_summary' => isset($status['completion_summary']) ? $status['completion_summary'] : null
921 - );
922 -
923 - // Add error message if present
924 - if (isset($status['error']) && !empty($status['error'])) {
925 - $result['error'] = sanitize_text_field($status['error']);
926 - }
927 -
928 - return $result;
929 -}
930 -
931 -
932 -public function mxchat_handle_sitemap_submission() {
933 - // Check if the form was submitted and verify permissions
934 - if (!isset($_POST['submit_sitemap']) || !current_user_can('manage_options')) {
935 - wp_die(esc_html__('Unauthorized access', 'mxchat'));
936 - }
937 -
938 - // Verify nonce
939 - check_admin_referer('mxchat_submit_sitemap_action', 'mxchat_submit_sitemap_nonce');
940 -
941 - // Validate URL
942 - if (!isset($_POST['sitemap_url']) || empty($_POST['sitemap_url'])) {
943 - set_transient('mxchat_admin_notice_error',
944 - esc_html__('Please provide a valid URL.', 'mxchat'),
945 - 30
946 - );
947 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
948 - exit;
949 - }
950 -
951 - $submitted_url = esc_url_raw($_POST['sitemap_url']);
952 -
953 - // Convert Google Drive sharing URLs to direct download URLs
954 - if ( strpos($submitted_url, 'drive.google.com') !== false ) {
955 - $file_id = '';
956 - if ( preg_match('/[?&]id=([a-zA-Z0-9_-]+)/', $submitted_url, $m) ) {
957 - $file_id = $m[1];
958 - } elseif ( preg_match('#/file/d/([a-zA-Z0-9_-]+)#', $submitted_url, $m) ) {
959 - $file_id = $m[1];
960 - }
961 - if ( ! empty($file_id) ) {
962 - $submitted_url = 'https://drive.google.com/uc?export=download&id=' . $file_id;
963 - }
964 - }
965 -
966 - // Get bot_id from form submission
967 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
968 -
969 - // Get bot-specific options and validate API key
970 - $bot_options = $this->get_bot_options($bot_id);
971 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
972 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
973 -
974 - if (strpos($selected_model, 'voyage') === 0) {
975 - $api_key = $options['voyage_api_key'] ?? '';
976 - $provider_name = 'Voyage AI';
977 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
978 - $api_key = $options['gemini_api_key'] ?? '';
979 - $provider_name = 'Google Gemini';
980 - } else {
981 - $api_key = $options['api_key'] ?? '';
982 - $provider_name = 'OpenAI';
983 - }
984 -
985 - if (empty($api_key)) {
986 - $error_message = sprintf(
987 - esc_html__('%s API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
988 - $provider_name
989 - );
990 - set_transient('mxchat_admin_notice_error', $error_message, 30);
991 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
992 - exit;
993 - }
994 -
995 - // Fetch URL — use browser-like headers so servers with bot protection don't block us
996 - $response = wp_remote_get($submitted_url, array(
997 - 'timeout' => 30,
998 - 'sslverify' => false,
999 - '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',
1000 - 'headers' => array(
1001 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
1002 - 'Accept-Language' => 'en-US,en;q=0.9',
1003 - ),
1004 - ));
1005 -
1006 - if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1007 - $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response);
1008 - set_transient('mxchat_admin_notice_error',
1009 - sprintf(
1010 - esc_html__('Failed to fetch the URL: %s', 'mxchat'),
1011 - esc_html($error_message)
1012 - ),
1013 - 30
1014 - );
1015 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1016 - exit;
1017 - }
1018 -
1019 - $content_type = wp_remote_retrieve_header($response, 'content-type');
1020 - $body_content = wp_remote_retrieve_body($response);
1021 -
1022 - if (empty($body_content)) {
1023 - set_transient('mxchat_admin_notice_error',
1024 - esc_html__('Empty response received from URL.', 'mxchat'),
1025 - 30
1026 - );
1027 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1028 - exit;
1029 - }
1030 -
1031 - // Handle PDF URL
1032 - if ($this->mxchat_is_pdf_url($submitted_url, $response)) {
1033 - $result = $this->mxchat_handle_pdf_for_knowledge_base($submitted_url, $response, $bot_id);
1034 -
1035 - if ($result === 'queued') {
1036 - set_transient('mxchat_admin_notice_success',
1037 - esc_html__('PDF queued for processing. Processing will start automatically.', 'mxchat'),
1038 - 30
1039 - );
1040 - } else {
1041 - set_transient('mxchat_admin_notice_error',
1042 - esc_html__('Failed to queue PDF processing: ', 'mxchat') . esc_html($result),
1043 - 30
1044 - );
1045 - }
1046 -
1047 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1048 - exit;
1049 - }
1050 -
1051 - // Handle Sitemap XML
1052 - if (strpos($content_type, 'xml') !== false || strpos($body_content, '<urlset') !== false) {
1053 - libxml_use_internal_errors(true);
1054 - $xml = simplexml_load_string($body_content);
1055 - $xml_errors = libxml_get_errors();
1056 - libxml_clear_errors();
1057 -
1058 - if ($xml === false || !empty($xml_errors)) {
1059 - set_transient('mxchat_admin_notice_error',
1060 - esc_html__('Invalid sitemap XML. Please provide a valid sitemap.', 'mxchat'),
1061 - 30
1062 - );
1063 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1064 - exit;
1065 - }
1066 -
1067 - $result = $this->mxchat_handle_sitemap_for_knowledge_base($xml, $submitted_url, $bot_id);
1068 -
1069 - if ($result === 'queued') {
1070 - set_transient('mxchat_admin_notice_success',
1071 - esc_html__('Sitemap queued for processing. Processing will start automatically.', 'mxchat'),
1072 - 30
1073 - );
1074 - } else {
1075 - set_transient('mxchat_admin_notice_error',
1076 - esc_html__('Failed to queue sitemap processing. Please check the status below for details.', 'mxchat'),
1077 - 30
1078 - );
1079 - }
1080 -
1081 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1082 - exit;
1083 - }
1084 -
1085 - // Handle Regular URL (single page)
1086 - $page_content = $this->mxchat_extract_main_content($body_content);
1087 - $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
1088 -
1089 - //error_log('[MXCHAT-URL-DEBUG] URL: ' . $submitted_url);
1090 - //error_log('[MXCHAT-URL-DEBUG] Extracted page_content length: ' . strlen($page_content));
1091 - //error_log('[MXCHAT-URL-DEBUG] Sanitized content length: ' . strlen($sanitized_content));
1092 - //error_log('[MXCHAT-URL-DEBUG] Content preview: ' . substr($sanitized_content, 0, 500));
1093 -
1094 - if (empty($sanitized_content)) {
1095 - set_transient('mxchat_admin_notice_error',
1096 - esc_html__('No valid content found on the provided URL.', 'mxchat'),
1097 - 30
1098 - );
1099 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1100 - exit;
1101 - }
1102 -
1103 - // For single URLs, process immediately using submit_content_to_db
1104 - // This handles chunking automatically for large content
1105 - $db_result = MxChat_Utils::submit_content_to_db(
1106 - $sanitized_content,
1107 - $submitted_url,
1108 - $api_key,
1109 - null,
1110 - $bot_id,
1111 - 'url' // content_type
1112 - );
1113 -
1114 - if (is_wp_error($db_result)) {
1115 - $error_message = esc_html__('Failed to store content in database: ', 'mxchat') . esc_html($db_result->get_error_message());
1116 - set_transient('mxchat_admin_notice_error', $error_message, 30);
1117 - } else {
1118 - $success_message = esc_html__('URL content successfully submitted!', 'mxchat');
1119 - set_transient('mxchat_admin_notice_success', $success_message, 30);
1120 - }
1121 -
1122 - wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1123 - exit;
1124 -}
1125 -
1126 -
1127 -public function mxchat_get_single_url_status() {
1128 - $status = get_transient('mxchat_single_url_status');
1129 - if (!$status) {
1130 - return null;
1131 - }
1132 -
1133 - // Add human-readable time
1134 - if (isset($status['timestamp'])) {
1135 - $status['human_time'] = human_time_diff(strtotime($status['timestamp']), current_time('timestamp')) . ' ' . __('ago', 'mxchat');
1136 - }
1137 -
1138 - return $status;
1139 -}
1140 -
1141 -public function mxchat_handle_sitemap_for_knowledge_base($xml, $sitemap_url, $bot_id = 'default') {
1142 - if (!current_user_can('manage_options')) {
1143 - return false;
1144 - }
1145 -
1146 - try {
1147 - $sitemap_url = esc_url_raw($sitemap_url);
1148 -
1149 - if (!$xml || !is_object($xml)) {
1150 - throw new Exception(__('Invalid XML object provided', 'mxchat'));
1151 - }
1152 -
1153 - // Get bot-specific embedding API for validation
1154 - $bot_options = $this->get_bot_options($bot_id);
1155 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
1156 -
1157 - // Test the embedding API before processing
1158 - $test_phrase = "Test embedding generation for MxChat";
1159 - $test_result = $this->mxchat_generate_embedding($test_phrase, $bot_id);
1160 -
1161 - if (is_string($test_result)) {
1162 - throw new Exception(__('Embedding API validation failed: ', 'mxchat') . $test_result);
1163 - }
1164 -
1165 - if (!is_array($test_result)) {
1166 - throw new Exception(__('Embedding API returned unexpected result type. Please check your configuration.', 'mxchat'));
1167 - }
1168 -
1169 - // Extract URLs from sitemap
1170 - $urls = array();
1171 - foreach ($xml->url as $url_element) {
1172 - $url = esc_url_raw((string)$url_element->loc);
1173 - if ($url) {
1174 - $urls[] = array('url' => $url);
1175 - }
1176 - }
1177 -
1178 - $total_urls = count($urls);
1179 -
1180 - if ($total_urls < 1) {
1181 - throw new Exception(__('No valid URLs found in sitemap', 'mxchat'));
1182 - }
1183 -
1184 - // Create unique queue ID
1185 - $queue_id = 'sitemap_' . md5($sitemap_url . time());
1186 -
1187 - // Add URLs to queue
1188 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'url', $urls, $bot_id);
1189 -
1190 - if ($queued_count === 0) {
1191 - throw new Exception(__('Failed to add URLs to processing queue', 'mxchat'));
1192 - }
1193 -
1194 - // Store queue metadata
1195 - $this->mxchat_set_queue_meta($queue_id, 'source_url', $sitemap_url);
1196 - $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'sitemap');
1197 - $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_urls);
1198 - $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
1199 - $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
1200 -
1201 - // Store queue ID in transient for status tracking
1202 - set_transient('mxchat_active_queue_sitemap', $queue_id, DAY_IN_SECONDS);
1203 - set_transient('mxchat_last_sitemap_url', $sitemap_url, DAY_IN_SECONDS);
1204 -
1205 - return 'queued';
1206 -
1207 - } catch (Exception $e) {
1208 - $error_message = $e->getMessage();
1209 - //error_log(sprintf(esc_html__('Error preparing sitemap for processing: %s', 'mxchat'), esc_html($error_message)));
1210 -
1211 - return $error_message;
1212 - }
1213 -
1214 -}
1215 -
1216 -/**
1217 - * Remove shortcode tags but preserve the content inside them
1218 - * Example: [vc_column]Hello World[/vc_column] becomes "Hello World"
1219 - *
1220 - * @param string $content The content containing shortcodes
1221 - * @return string Content with shortcode tags removed but inner content preserved
1222 - */
1223 -private function strip_shortcode_tags_preserve_content($content) {
1224 - // Single-pass regex removes all shortcode brackets: [tag], [tag attr="val"], [/tag], [tag /]
1225 - // Content between tags is inherently preserved since only brackets are targeted
1226 - $result = preg_replace('/\[\/?\w[\w-]*[^\]]*\]/', '', $content);
1227 - return ($result !== null) ? $result : $content;
1228 -}
1229 -
1230 -public function mxchat_sanitize_content_for_api($content) {
1231 - //error_log('[MXCHAT-SANITIZE] Original content preview: ' . substr($content, 0, 500) . '...');
1232 -
1233 - // Remove shortcode tags but PRESERVE content inside them
1234 - $content = $this->strip_shortcode_tags_preserve_content($content);
1235 -
1236 - // Remove script, style tags, and HTML comments
1237 - $content = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $content);
1238 - $content = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $content);
1239 - $content = preg_replace('/<!--(.|\s)*?-->/', '', $content);
1240 -
1241 - // Remove all HTML tags and decode HTML entities
1242 - $content = wp_strip_all_tags($content);
1243 - $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5);
1244 -
1245 - // Normalize whitespace but preserve paragraph breaks
1246 - // First, normalize line endings to \n
1247 - $content = str_replace(["\r\n", "\r"], "\n", $content);
1248 - // Replace multiple spaces/tabs with single space, but preserve newlines
1249 - $content = preg_replace('/[ \t]+/', ' ', $content);
1250 - // Replace 3+ newlines with 2 newlines (max 2 blank lines)
1251 - $content = preg_replace('/\n{3,}/', "\n\n", $content);
1252 - // Trim each line
1253 - $lines = explode("\n", $content);
1254 - $lines = array_map('trim', $lines);
1255 - $content = implode("\n", $lines);
1256 - // Final trim
1257 - $content = trim($content);
1258 -
1259 - // Remove control characters (which can cause database issues) but preserve \n (0x0A) and \r (0x0D)
1260 - $content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $content);
1261 -
1262 - // Remove NULL bytes which can cause database errors
1263 - $content = str_replace("\0", "", $content);
1264 -
1265 - // Ensure valid UTF-8 encoding
1266 - $content = wp_check_invalid_utf8($content);
1267 -
1268 - // Remove any extremely long strings without spaces (often garbage)
1269 - $content = preg_replace('/\S{300,}/', ' ', $content);
1270 -
1271 - // Replace problematic characters that often cause database issues
1272 - $content = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $content); // Remove emoji and other high Unicode characters
1273 -
1274 - // Replace any remaining potentially problematic characters with spaces
1275 - // BUT preserve newlines by temporarily replacing them
1276 - $content = str_replace("\n", "NEWLINE_PLACEHOLDER", $content);
1277 - $content = preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\p{Sm}]/u', ' ', $content);
1278 - $content = str_replace("NEWLINE_PLACEHOLDER", "\n", $content);
1279 -
1280 - // Limit to reasonable length if needed
1281 - $max_length = 65000; // Just under MySQL TEXT field limit
1282 - if (strlen($content) > $max_length) {
1283 - $content = substr($content, 0, $max_length);
1284 - }
1285 -
1286 - //error_log('[MXCHAT-SANITIZE] Sanitized content preview: ' . substr($content, 0, 500) . '...');
1287 - return $content;
1288 -}
1289 -public function mxchat_extract_main_content($html) {
1290 - if (empty($html)) {
1291 - return '';
1292 - }
1293 - try {
1294 - $dom = new DOMDocument;
1295 - libxml_use_internal_errors(true); // Suppress HTML parsing errors
1296 - @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
1297 - $xpath = new DOMXPath($dom);
1298 -
1299 - // For debugging purposes
1300 - $debugEnabled = true; // Set to true to enable debugging output
1301 - $debug = function($message) use ($debugEnabled) {
1302 - if ($debugEnabled) {
1303 - //error_log('[MXCHAT-EXTRACT-DEBUG] ' . $message);
1304 - }
1305 - };
1306 -
1307 - // Direct targeting for Gerow theme posts
1308 - $post_text = $xpath->query('//div[contains(@class, "post-text")]');
1309 - if ($post_text && $post_text->length > 0) {
1310 - $debug("Found post-text directly");
1311 - $content = '';
1312 - foreach ($post_text as $node) {
1313 - $content .= $dom->saveHTML($node);
1314 - }
1315 - if (!empty($content)) {
1316 - $debug("Returning post-text content");
1317 - return $content;
1318 - }
1319 - }
1320 -
1321 - // Try to get the blog details content which contains the post-text
1322 - $blog_details = $xpath->query('//div[contains(@class, "blog-details-content")]');
1323 - if ($blog_details && $blog_details->length > 0) {
1324 - $debug("Found blog-details-content");
1325 - $content = '';
1326 - foreach ($blog_details as $node) {
1327 - $content .= $dom->saveHTML($node);
1328 - }
1329 - if (!empty($content)) {
1330 - $debug("Returning blog-details-content");
1331 - return $content;
1332 - }
1333 - }
1334 -
1335 - // Try to get the article which contains the blog details
1336 - $article = $xpath->query('//article[contains(@class, "blog-details-wrap")]');
1337 - if ($article && $article->length > 0) {
1338 - $debug("Found article with blog-details-wrap");
1339 - $content = '';
1340 - foreach ($article as $node) {
1341 - $content .= $dom->saveHTML($node);
1342 - }
1343 - if (!empty($content)) {
1344 - $debug("Returning article content");
1345 - return $content;
1346 - }
1347 - }
1348 -
1349 - // Try even broader with the blog-item-wrap
1350 - $blog_item = $xpath->query('//div[contains(@class, "blog-item-wrap")]');
1351 - if ($blog_item && $blog_item->length > 0) {
1352 - $debug("Found blog-item-wrap");
1353 - $content = '';
1354 - foreach ($blog_item as $node) {
1355 - $content .= $dom->saveHTML($node);
1356 - }
1357 - if (!empty($content)) {
1358 - $debug("Returning blog-item-wrap content");
1359 - return $content;
1360 - }
1361 - }
1362 -
1363 - // Specific Gerow theme path
1364 - $gerow_path = $xpath->query('//section[contains(@class, "blog-area")]//div[contains(@class, "post-text")]');
1365 - if ($gerow_path && $gerow_path->length > 0) {
1366 - $debug("Found Gerow theme path to post-text");
1367 - $content = '';
1368 - foreach ($gerow_path as $node) {
1369 - $content .= $dom->saveHTML($node);
1370 - }
1371 - if (!empty($content)) {
1372 - $debug("Returning Gerow post-text content");
1373 - return $content;
1374 - }
1375 - }
1376 -
1377 - // Generic blog post selectors
1378 - $selectors = [
1379 - // Blog post specific selectors
1380 - '//div[contains(@class, "post-text")]',
1381 - '//article[contains(@class, "blog-post-item")]//div[contains(@class, "post-text")]',
1382 - '//div[contains(@class, "blog-details-content")]',
1383 - '//article[contains(@class, "blog-details-wrap")]',
1384 - '//div[contains(@class, "entry-content")]',
1385 - '//div[contains(@class, "blog-content")]',
1386 - '//div[contains(@class, "blog-item-wrap")]',
1387 -
1388 - // More general content selectors
1389 - '//div[contains(@class, "page__content")]',
1390 - '//div[contains(@class, "elementor-widget-container")]',
1391 - '//div[contains(@class, "elementor-text-editor")]',
1392 - '//div[contains(@class, "elementor-widget-text-editor")]',
1393 - '//*[contains(@class, "entry-content")]',
1394 - '//*[contains(@class, "post-content")]',
1395 - '//*[contains(@class, "article-content")]',
1396 - '//*[@id="content"]',
1397 - '//*[@id="main-content"]',
1398 - '//section[contains(@class, "blog-area")]',
1399 - '//article',
1400 - '//main',
1401 - '//div[contains(@class, "content")]'
1402 - ];
1403 -
1404 - // First handle Elementor content - get only leaf widget containers to avoid duplicates
1405 - $debug("Checking for Elementor content");
1406 - // Get widget containers that are direct children of widgets (not nested inside other widget containers)
1407 - $elementor_widgets = $xpath->query('//div[contains(@class, "elementor-widget")]//div[contains(@class, "elementor-widget-container")]');
1408 - if ($elementor_widgets && $elementor_widgets->length > 0) {
1409 - $debug("Found Elementor widgets");
1410 - $seen_content = array(); // Track seen content to avoid duplicates
1411 - $combined_content = '';
1412 - foreach ($elementor_widgets as $widget) {
1413 - $widget_content = $dom->saveHTML($widget);
1414 - if (!empty($widget_content)) {
1415 - // Create a hash of the content to detect duplicates
1416 - $content_hash = md5($widget_content);
1417 - if (!isset($seen_content[$content_hash])) {
1418 - $seen_content[$content_hash] = true;
1419 - $combined_content .= $widget_content;
1420 - }
1421 - }
1422 - }
1423 - if (!empty($combined_content)) {
1424 - $debug("Returning Elementor content");
1425 - return $combined_content;
1426 - }
1427 - }
1428 -
1429 - // Try standard selectors one by one
1430 - foreach ($selectors as $selector) {
1431 - $debug("Trying selector: " . $selector);
1432 - $nodes = $xpath->query($selector);
1433 - if ($nodes && $nodes->length > 0) {
1434 - $debug("Found " . $nodes->length . " matches for selector: " . $selector);
1435 - // Only take the FIRST matching node to avoid duplicate content
1436 - // (pages often have nested or multiple containers with same class)
1437 - $content = $dom->saveHTML($nodes->item(0));
1438 - if (!empty($content)) {
1439 - $debug("Returning content from selector: " . $selector . " (first match only)");
1440 - return $content;
1441 - }
1442 - }
1443 - }
1444 -
1445 - // Manual regex fallback for post-text if DOM methods fail
1446 - $debug("Trying regex fallback");
1447 - if (preg_match('/<div class="post-text">(.*?)<\/div>\s*<\/div>\s*<\/div>/s', $html, $matches)) {
1448 - $debug("Found post-text via regex");
1449 - return '<div class="post-text">' . $matches[1] . '</div>';
1450 - }
1451 -
1452 - // Try to extract the blog section as a whole
1453 - $blog_section = $xpath->query('//section[contains(@class, "blog-area")]');
1454 - if ($blog_section && $blog_section->length > 0) {
1455 - $debug("Found blog-area section");
1456 - $content = '';
1457 - foreach ($blog_section as $node) {
1458 - $content .= $dom->saveHTML($node);
1459 - }
1460 - if (!empty($content)) {
1461 - $debug("Returning blog-area section content");
1462 - return $content;
1463 - }
1464 - }
1465 -
1466 - // Generic container selectors for non-CMS sites (like .asp pages)
1467 - $debug("Trying generic container selectors");
1468 - $generic_selectors = [
1469 - '//div[@id="main"]',
1470 - '//div[@id="wrapper"]',
1471 - '//div[@id="page"]',
1472 - '//div[@id="site-content"]',
1473 - '//div[contains(@class, "main-content")]',
1474 - '//div[contains(@class, "page-content")]',
1475 - '//div[contains(@class, "site-content")]',
1476 - ];
1477 -
1478 - foreach ($generic_selectors as $selector) {
1479 - $debug("Trying generic selector: " . $selector);
1480 - $nodes = $xpath->query($selector);
1481 - if ($nodes && $nodes->length > 0) {
1482 - $content = $dom->saveHTML($nodes->item(0));
1483 - if (!empty($content)) {
1484 - $debug("Returning content from generic selector: " . $selector);
1485 - return $content;
1486 - }
1487 - }
1488 - }
1489 -
1490 - // Paragraph-based content detection - find regions with substantial text
1491 - $debug("Trying paragraph-based content detection");
1492 - $paragraphs = $xpath->query('//p[string-length(normalize-space()) > 50]');
1493 - if ($paragraphs && $paragraphs->length >= 3) {
1494 - $debug("Found " . $paragraphs->length . " substantial paragraphs");
1495 - // Collect all substantial paragraphs and their content
1496 - $paragraph_content = '';
1497 - foreach ($paragraphs as $p) {
1498 - $paragraph_content .= $dom->saveHTML($p) . "\n";
1499 - }
1500 - if (!empty($paragraph_content)) {
1501 - $debug("Returning paragraph-based content");
1502 - return $paragraph_content;
1503 - }
1504 - }
1505 -
1506 - // Improved body fallback - strip nav/header/footer elements first
1507 - $debug("Using improved body fallback");
1508 - $body = $dom->getElementsByTagName('body');
1509 - if ($body->length > 0) {
1510 - // Clone the body to avoid modifying the original DOM
1511 - $body_clone = $body->item(0)->cloneNode(true);
1512 -
1513 - // Remove common non-content elements by tag name
1514 - $remove_tags = ['nav', 'header', 'footer', 'aside', 'script', 'style', 'noscript'];
1515 - foreach ($remove_tags as $tag) {
1516 - $elements = $body_clone->getElementsByTagName($tag);
1517 - // Iterate backwards to safely remove elements
1518 - for ($i = $elements->length - 1; $i >= 0; $i--) {
1519 - $el = $elements->item($i);
1520 - if ($el && $el->parentNode) {
1521 - $el->parentNode->removeChild($el);
1522 - }
1523 - }
1524 - }
1525 -
1526 - // Remove elements with common non-content class names using XPath on the cloned body
1527 - $temp_dom = new DOMDocument();
1528 - @$temp_dom->appendChild($temp_dom->importNode($body_clone, true));
1529 - $temp_xpath = new DOMXPath($temp_dom);
1530 -
1531 - $remove_class_patterns = [
1532 - '//*[contains(@class, "nav")]',
1533 - '//*[contains(@class, "menu")]',
1534 - '//*[contains(@class, "sidebar")]',
1535 - '//*[contains(@class, "footer")]',
1536 - '//*[contains(@class, "header")]',
1537 - '//*[contains(@id, "nav")]',
1538 - '//*[contains(@id, "menu")]',
1539 - '//*[contains(@id, "sidebar")]',
1540 - '//*[contains(@id, "footer")]',
1541 - '//*[contains(@id, "header")]',
1542 - ];
1543 -
1544 - foreach ($remove_class_patterns as $pattern) {
1545 - $elements = $temp_xpath->query($pattern);
1546 - if ($elements) {
1547 - for ($i = $elements->length - 1; $i >= 0; $i--) {
1548 - $el = $elements->item($i);
1549 - if ($el && $el->parentNode) {
1550 - $el->parentNode->removeChild($el);
1551 - }
1552 - }
1553 - }
1554 - }
1555 -
1556 - $cleaned_content = $temp_dom->saveHTML();
1557 - if (!empty($cleaned_content)) {
1558 - $debug("Returning cleaned body content");
1559 - return $cleaned_content;
1560 - }
1561 - }
1562 -
1563 - // Last resort: return the original HTML
1564 - $debug("Returning original HTML");
1565 - return $html;
1566 - } catch (Exception $e) {
1567 - //error_log('[MXCHAT-ERROR] Content extraction failed: ' . $e->getMessage());
1568 - return $html; // Return original HTML if parsing fails
1569 - } finally {
1570 - libxml_clear_errors();
1571 - }
1572 -}
1573 -public function mxchat_get_sitemap_processing_status($sitemap_url) {
1574 - $sitemap_url = esc_url_raw($sitemap_url);
1575 - $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
1576 - $status = get_transient($status_key);
1577 -
1578 - if (!$status || !is_array($status)) {
1579 - return false;
1580 - }
1581 -
1582 - // Auto-complete check: if all URLs are processed but status isn't complete
1583 - if (isset($status['processed_urls']) && isset($status['total_urls']) &&
1584 - $status['processed_urls'] >= $status['total_urls'] &&
1585 - isset($status['status']) && $status['status'] !== 'complete' &&
1586 - $status['status'] !== 'error') {
1587 -
1588 - // Mark as complete
1589 - $status['status'] = 'complete';
1590 - $status['processed_urls'] = $status['total_urls']; // Ensure exact match
1591 -
1592 - // Update the transient with the corrected status
1593 - set_transient($status_key, $status, DAY_IN_SECONDS);
1594 - }
1595 -
1596 - return array(
1597 - 'total_urls' => absint($status['total_urls']),
1598 - 'processed_urls' => absint($status['processed_urls']),
1599 - 'failed_urls' => absint($status['failed_urls'] ?? 0),
1600 - 'percentage' => ($status['total_urls'] > 0)
1601 - ? round((absint($status['processed_urls']) / absint($status['total_urls'])) * 100)
1602 - : 0,
1603 - 'status' => sanitize_text_field($status['status']),
1604 - 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
1605 - 'error' => isset($status['error']) ? sanitize_text_field($status['error']) : '',
1606 - 'last_error' => isset($status['last_error']) ? sanitize_text_field($status['last_error']) : '',
1607 - 'failed_urls_list' => isset($status['failed_urls_list']) ? $status['failed_urls_list'] : array()
1608 - );
1609 -}
1610 -
1611 -public function mxchat_ajax_get_status_updates() {
1612 - try {
1613 - // Verify the request
1614 - check_ajax_referer('mxchat_status_nonce', 'nonce');
1615 -
1616 - // Get active queue IDs
1617 - $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
1618 - $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
1619 -
1620 - $sitemap_status = false;
1621 - $pdf_status = false;
1622 -
1623 - // Get sitemap queue status
1624 - if ($sitemap_queue_id) {
1625 - $sitemap_status = $this->mxchat_get_queue_status_data($sitemap_queue_id, 'sitemap');
1626 - }
1627 -
1628 - // Get PDF queue status
1629 - if ($pdf_queue_id) {
1630 - $pdf_status = $this->mxchat_get_queue_status_data($pdf_queue_id, 'pdf');
1631 - }
1632 -
1633 - $is_active_processing =
1634 - ($sitemap_status && $sitemap_status['status'] === 'processing') ||
1635 - ($pdf_status && $pdf_status['status'] === 'processing');
1636 -
1637 - // Return JSON response with the status data
1638 - wp_send_json(array(
1639 - 'pdf_status' => $pdf_status,
1640 - 'sitemap_status' => $sitemap_status,
1641 - 'is_processing' => $is_active_processing,
1642 - 'sitemap_queue_id' => $sitemap_queue_id,
1643 - 'pdf_queue_id' => $pdf_queue_id
1644 - ));
1645 -
1646 - } catch (Exception $e) {
1647 - //error_log('MxChat Status Update Error: ' . $e->getMessage());
1648 -
1649 - wp_send_json_error(array(
1650 - 'message' => 'Error getting status updates: ' . $e->getMessage(),
1651 - 'status' => 'error'
1652 - ));
1653 - }
1654 -}
1655 -
1656 -/**
1657 - * Helper function to get queue status data
1658 - */
1659 -private function mxchat_get_queue_status_data($queue_id, $type = 'sitemap') {
1660 - global $wpdb;
1661 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
1662 -
1663 - // Get counts by status
1664 - $counts = $wpdb->get_results($wpdb->prepare(
1665 - "SELECT status, COUNT(*) as count
1666 - FROM $table_name
1667 - WHERE queue_id = %s
1668 - GROUP BY status",
1669 - $queue_id
1670 - ), OBJECT_K);
1671 -
1672 - $total = 0;
1673 - $completed = 0;
1674 - $failed = 0;
1675 - $processing = 0;
1676 - $pending = 0;
1677 -
1678 - foreach ($counts as $status => $data) {
1679 - $count = absint($data->count);
1680 - $total += $count;
1681 -
1682 - switch ($status) {
1683 - case 'completed':
1684 - $completed = $count;
1685 - break;
1686 - case 'failed':
1687 - $failed = $count;
1688 - break;
1689 - case 'processing':
1690 - $processing = $count;
1691 - break;
1692 - case 'pending':
1693 - $pending = $count;
1694 - break;
1695 - }
1696 - }
1697 -
1698 - if ($total === 0) {
1699 - return false;
1700 - }
1701 -
1702 - // Calculate percentage
1703 - $percentage = round((($completed + $failed) / $total) * 100);
1704 -
1705 - // Get failed items details (limit to 50)
1706 - $failed_items = array();
1707 - if ($failed > 0) {
1708 - $failed_results = $wpdb->get_results($wpdb->prepare(
1709 - "SELECT item_type, item_data, error_message, attempts, completed_at
1710 - FROM $table_name
1711 - WHERE queue_id = %s
1712 - AND status = 'failed'
1713 - AND attempts >= max_attempts
1714 - ORDER BY id DESC
1715 - LIMIT 50",
1716 - $queue_id
1717 - ));
1718 -
1719 - foreach ($failed_results as $item) {
1720 - $data = json_decode($item->item_data, true);
1721 - $url = $type === 'pdf' ? ($data['pdf_url'] ?? '') : ($data['url'] ?? '');
1722 -
1723 - $failed_items[] = array(
1724 - 'url' => $url,
1725 - 'page' => $type === 'pdf' ? ($data['page_number'] ?? 0) : 0,
1726 - 'error' => $item->error_message,
1727 - 'retries' => $item->attempts,
1728 - 'time' => strtotime($item->completed_at)
1729 - );
1730 - }
1731 - }
1732 -
1733 - // Get queue metadata
1734 - $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
1735 -
1736 - // Determine if queue is complete
1737 - $is_complete = ($pending === 0 && $processing === 0);
1738 -
1739 - // Get last update time
1740 - $last_update = $wpdb->get_var($wpdb->prepare(
1741 - "SELECT MAX(UNIX_TIMESTAMP(COALESCE(completed_at, started_at, created_at)))
1742 - FROM $table_name
1743 - WHERE queue_id = %s",
1744 - $queue_id
1745 - ));
1746 -
1747 - $last_update_text = $last_update ? human_time_diff($last_update, time()) . ' ' . __('ago', 'mxchat') : __('Just now', 'mxchat');
1748 -
1749 - // Format based on type
1750 - if ($type === 'pdf') {
1751 - return array(
1752 - 'total_pages' => $total,
1753 - 'processed_pages' => $completed + $failed,
1754 - 'failed_pages' => $failed,
1755 - 'percentage' => $percentage,
1756 - 'status' => $is_complete ? 'complete' : 'processing',
1757 - 'last_update' => $last_update_text,
1758 - 'failed_pages_list' => $failed_items,
1759 - 'pdf_url' => $source_url,
1760 - 'queue_id' => $queue_id
1761 - );
1762 - } else {
1763 - return array(
1764 - 'total_urls' => $total,
1765 - 'processed_urls' => $completed + $failed,
1766 - 'failed_urls' => $failed,
1767 - 'percentage' => $percentage,
1768 - 'status' => $is_complete ? 'complete' : 'processing',
1769 - 'last_update' => $last_update_text,
1770 - 'failed_urls_list' => $failed_items,
1771 - 'sitemap_url' => $source_url,
1772 - 'queue_id' => $queue_id
1773 - );
1774 - }
1775 -}
1776 -
1777 -/**
1778 - * Public method to get processing status for both sitemap and PDF queues
1779 - * Used by admin pages to display processing status
1780 - *
1781 - * @return array Array with 'sitemap_status', 'pdf_status', and 'is_processing' keys
1782 - */
1783 -public function mxchat_get_processing_statuses() {
1784 - $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
1785 - $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
1786 -
1787 - $sitemap_status = false;
1788 - $pdf_status = false;
1789 -
1790 - if ($sitemap_queue_id) {
1791 - $sitemap_status = $this->mxchat_get_queue_status_data($sitemap_queue_id, 'sitemap');
1792 - }
1793 -
1794 - if ($pdf_queue_id) {
1795 - $pdf_status = $this->mxchat_get_queue_status_data($pdf_queue_id, 'pdf');
1796 - }
1797 -
1798 - $is_processing =
1799 - ($sitemap_status && $sitemap_status['status'] === 'processing') ||
1800 - ($pdf_status && $pdf_status['status'] === 'processing');
1801 -
1802 - return array(
1803 - 'sitemap_status' => $sitemap_status,
1804 - 'pdf_status' => $pdf_status,
1805 - 'is_processing' => $is_processing
1806 - );
1807 -}
1808 -
1809 -/**
1810 - * AJAX handler to get recent knowledge entries for real-time table updates
1811 - * UPDATED: Now supports both WordPress DB and Pinecone data sources
1812 - */
1813 -public function ajax_mxchat_get_recent_entries() {
1814 - check_ajax_referer('mxchat_entries_nonce', 'nonce');
1815 -
1816 - if (!current_user_can('manage_options')) {
1817 - wp_send_json_error(array('message' => 'Unauthorized'));
1818 - return;
1819 - }
1820 -
1821 - global $wpdb;
1822 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1823 -
1824 - // Get parameters
1825 - $last_id = isset($_POST['last_id']) ? absint($_POST['last_id']) : 0;
1826 - $limit = isset($_POST['limit']) ? min(absint($_POST['limit']), 50) : 10;
1827 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
1828 -
1829 - // Check if Pinecone is enabled for this bot
1830 - $pinecone_manager = $this->mxchat_get_pinecone_manager();
1831 - $pinecone_options = $pinecone_manager ? $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id) : array();
1832 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1833 - $has_pinecone_api = !empty($pinecone_options['mxchat_pinecone_api_key']);
1834 -
1835 - if ($use_pinecone && $has_pinecone_api) {
1836 - // PINECONE DATA SOURCE - Get grouped entry count (not raw vector count)
1837 - // Use mxchat_fetch_pinecone_records which returns total_unique_entries
1838 - $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, '', 1, 10, $bot_id, '');
1839 - $total_count = $records['total'] ?? 0;
1840 -
1841 - // For Pinecone, we don't return individual entries during polling
1842 - // (entries are already displayed on page load via mxchat_fetch_pinecone_records)
1843 - // We just return the updated count
1844 - wp_send_json_success(array(
1845 - 'entries' => array(),
1846 - 'total_count' => absint($total_count),
1847 - 'max_id' => $last_id,
1848 - 'data_source' => 'pinecone'
1849 - ));
1850 - return;
1851 - }
1852 -
1853 - // WORDPRESS DB DATA SOURCE
1854 - // Build query to get entries newer than last_id
1855 - $where_clauses = array('1=1');
1856 - $where_values = array();
1857 -
1858 - if ($last_id > 0) {
1859 - $where_clauses[] = 'id > %d';
1860 - $where_values[] = $last_id;
1861 - }
1862 -
1863 - // Note: WordPress DB table doesn't have bot_id column
1864 - // Multi-bot filtering is handled via Pinecone namespaces
1865 -
1866 - $where_sql = implode(' AND ', $where_clauses);
1867 -
1868 - // Get recent entries
1869 - $query = "SELECT id, article_content, source_url, timestamp
1870 - FROM $table_name
1871 - WHERE $where_sql
1872 - ORDER BY id DESC
1873 - LIMIT %d";
1874 -
1875 - $where_values[] = $limit;
1876 -
1877 - $entries = $wpdb->get_results($wpdb->prepare($query, $where_values));
1878 -
1879 - // Get total count of GROUPED entries (by source_url) - matches pagination display
1880 - // Count unique source_urls (excluding mxchat:// internal refs) + count of ungrouped rows
1881 - $total_count = $wpdb->get_var(
1882 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '' AND source_url NOT LIKE 'mxchat://%') +
1883 - (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')"
1884 - );
1885 -
1886 - // Format entries for response
1887 - $formatted_entries = array();
1888 - $preview_length = 150;
1889 - foreach ($entries as $entry) {
1890 - // Parse chunk metadata using the proper chunker method (same as initial page load)
1891 - if (class_exists('MxChat_Chunker')) {
1892 - $chunk_meta = MxChat_Chunker::parse_stored_chunk($entry->article_content);
1893 - $display_content = $chunk_meta['text'];
1894 - $chunk_metadata = $chunk_meta['metadata'];
1895 - } else {
1896 - $display_content = $entry->article_content;
1897 - $chunk_metadata = array();
1898 - }
1899 -
1900 - $content_preview = mb_strlen($display_content) > $preview_length
1901 - ? mb_substr($display_content, 0, $preview_length) . '...'
1902 - : $display_content;
1903 -
1904 - $formatted_entries[] = array(
1905 - 'id' => $entry->id,
1906 - 'preview' => esc_html($content_preview),
1907 - 'full_content' => wp_kses_post(wpautop($display_content)),
1908 - 'content_length' => mb_strlen($display_content),
1909 - 'preview_length' => $preview_length,
1910 - 'source_url' => $entry->source_url,
1911 - 'has_link' => !empty($entry->source_url) && strpos($entry->source_url, 'mxchat://') !== 0,
1912 - 'chunk_metadata' => $chunk_metadata,
1913 - 'bot_id' => $entry->bot_id ?? 'default',
1914 - 'edit_nonce' => wp_create_nonce('mxchat_edit_entry_nonce'),
1915 - 'delete_nonce' => wp_create_nonce('mxchat_delete_prompt_nonce')
1916 - );
1917 - }
1918 -
1919 - wp_send_json_success(array(
1920 - 'entries' => $formatted_entries,
1921 - 'total_count' => absint($total_count),
1922 - 'max_id' => !empty($entries) ? $entries[0]->id : $last_id,
1923 - 'data_source' => 'wordpress'
1924 - ));
1925 -}
1926 -
1927 -/**
1928 - * Get Pinecone total count from stats API
1929 - * Helper function for ajax_mxchat_get_recent_entries
1930 - */
1931 -private function mxchat_get_pinecone_count_from_stats($pinecone_options) {
1932 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1933 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1934 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
1935 -
1936 - if (empty($api_key) || empty($host)) {
1937 - return 0;
1938 - }
1939 -
1940 - try {
1941 - $stats_url = "https://{$host}/describe_index_stats";
1942 -
1943 - $response = wp_remote_post($stats_url, array(
1944 - 'headers' => array(
1945 - 'Api-Key' => $api_key,
1946 - 'Content-Type' => 'application/json'
1947 - ),
1948 - 'body' => '{}',
1949 - 'timeout' => 10
1950 - ));
1951 -
1952 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
1953 - $body = wp_remote_retrieve_body($response);
1954 - $stats_data = json_decode($body, true);
1955 -
1956 - // If namespace is specified, get count from that specific namespace
1957 - if (!empty($namespace) && isset($stats_data['namespaces'][$namespace]['vectorCount'])) {
1958 - return intval($stats_data['namespaces'][$namespace]['vectorCount']);
1959 - }
1960 -
1961 - // If no namespace specified or namespace not found in response, use total
1962 - return intval($stats_data['totalVectorCount'] ?? 0);
1963 - }
1964 -
1965 - return 0;
1966 -
1967 - } catch (Exception $e) {
1968 - return 0;
1969 - }
1970 -}
1971 -
1972 -/**
1973 - * AJAX handler to refresh Pinecone entries table via AJAX
1974 - * Returns the table HTML for updating the UI without a full page reload
1975 - */
1976 -public function ajax_mxchat_refresh_pinecone_entries() {
1977 - check_ajax_referer('mxchat_entries_nonce', 'nonce');
1978 -
1979 - if (!current_user_can('manage_options')) {
1980 - wp_send_json_error(array('message' => 'Unauthorized'));
1981 - return;
1982 - }
1983 -
1984 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
1985 - $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
1986 - $per_page = 25;
1987 - $search_query = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
1988 - $content_type_filter = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : '';
1989 -
1990 - // Get Pinecone manager and options
1991 - $pinecone_manager = $this->mxchat_get_pinecone_manager();
1992 - if (!$pinecone_manager) {
1993 - wp_send_json_error(array('message' => 'Pinecone manager not available'));
1994 - return;
1995 - }
1996 -
1997 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
1998 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1999 - $pinecone_api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2000 -
2001 - if (!$use_pinecone || empty($pinecone_api_key)) {
2002 - wp_send_json_error(array('message' => 'Pinecone not configured'));
2003 - return;
2004 - }
2005 -
2006 - // Fetch records from Pinecone
2007 - $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, $search_query, $page, $per_page, $bot_id, $content_type_filter);
2008 - $prompts = $records['data'] ?? array();
2009 - $total_records = $records['total'] ?? 0;
2010 -
2011 - // Preprocess Pinecone records — set chunk_metadata and display_content
2012 - // (matches admin-knowledge-page.php preprocessing)
2013 - foreach ($prompts as $prompt) {
2014 - if (isset($prompt->chunk_index) && $prompt->chunk_index !== null) {
2015 - $prompt->chunk_metadata = array(
2016 - 'chunk_index' => intval($prompt->chunk_index),
2017 - 'total_chunks' => isset($prompt->total_chunks) ? intval($prompt->total_chunks) : null,
2018 - 'is_chunked' => isset($prompt->is_chunked) ? (bool) $prompt->is_chunked : true,
2019 - 'source_url' => $prompt->source_url ?? ''
2020 - );
2021 - $prompt->display_content = $prompt->article_content;
2022 - } else {
2023 - $prompt->chunk_metadata = array();
2024 - $prompt->display_content = $prompt->article_content ?? '';
2025 - }
2026 - }
2027 -
2028 - // Group prompts by source_url
2029 - $grouped_prompts = array();
2030 - foreach ($prompts as $prompt) {
2031 - $source_url = '';
2032 - if (!empty($prompt->chunk_metadata['source_url'])) {
2033 - $source_url = $prompt->chunk_metadata['source_url'];
2034 - } elseif (!empty($prompt->source_url)) {
2035 - $source_url = $prompt->source_url;
2036 - }
2037 -
2038 - if (!empty($source_url)) {
2039 - if (!isset($grouped_prompts[$source_url])) {
2040 - $grouped_prompts[$source_url] = array();
2041 - }
2042 - $grouped_prompts[$source_url][] = $prompt;
2043 - } else {
2044 - $grouped_prompts['_ungrouped_' . $prompt->id] = array($prompt);
2045 - }
2046 - }
2047 -
2048 - // Sort each group by chunk_index
2049 - foreach ($grouped_prompts as $source_url => &$group) {
2050 - usort($group, function($a, $b) {
2051 - $index_a = isset($a->chunk_metadata['chunk_index']) ? intval($a->chunk_metadata['chunk_index']) : 0;
2052 - $index_b = isset($b->chunk_metadata['chunk_index']) ? intval($b->chunk_metadata['chunk_index']) : 0;
2053 - return $index_a - $index_b;
2054 - });
2055 - }
2056 - unset($group);
2057 -
2058 - // Build HTML for the table rows - must match admin-knowledge-page.php structure exactly
2059 - ob_start();
2060 - $display_index = 0;
2061 - $current_page = $page;
2062 - $data_source = 'pinecone';
2063 - $current_bot_id = $bot_id;
2064 - $preview_length = 150;
2065 -
2066 - if (empty($grouped_prompts)) {
2067 - echo '<tr><td colspan="4" style="padding: 40px; text-align: center; color: var(--mxch-text-muted);">';
2068 - esc_html_e('No knowledge entries found in Pinecone.', 'mxchat');
2069 - echo '</td></tr>';
2070 - } else {
2071 - foreach ($grouped_prompts as $source_url => $group) {
2072 - $chunk_count = count($group);
2073 - $first_prompt = $group[0];
2074 - $display_index++;
2075 -
2076 - if ($chunk_count > 1) {
2077 - // Multiple chunks - show grouped row with expand button
2078 - $group_id = 'group-' . md5($source_url);
2079 - ?>
2080 - <tr id="prompt-<?php echo esc_attr($first_prompt->id); ?>"
2081 - class="mxchat-chunk-group-header"
2082 - data-source="<?php echo esc_attr($data_source); ?>"
2083 - data-group-id="<?php echo esc_attr($group_id); ?>"
2084 - style="border-bottom: 1px solid var(--mxch-card-border); background: rgba(33, 150, 243, 0.02);">
2085 - <td style="padding: 12px 16px; text-align: center;">
2086 - <input type="checkbox"
2087 - class="mxchat-entry-checkbox"
2088 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2089 - data-source="<?php echo esc_attr($data_source); ?>"
2090 - data-source-url="<?php echo esc_attr($source_url); ?>"
2091 - data-is-group="true"
2092 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>">
2093 - </td>
2094 - <td style="padding: 12px 16px; font-size: 13px;">
2095 - <?php echo esc_html($display_index + (($current_page - 1) * $per_page)); ?>
2096 - </td>
2097 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2098 - <div class="mxchat-chunk-group-info">
2099 - <button type="button" class="mxchat-chunk-toggle" data-group-id="<?php echo esc_attr($group_id); ?>">
2100 - <span class="dashicons dashicons-arrow-right-alt2"></span>
2101 - </button>
2102 - <span class="mxchat-chunk-badge"><?php echo esc_html($chunk_count); ?> <?php esc_html_e('chunks', 'mxchat'); ?></span>
2103 - <span class="mxchat-chunk-preview">
2104 - <?php
2105 - $parent_content = isset($first_prompt->display_content) ? $first_prompt->display_content : (isset($first_prompt->article_content) ? $first_prompt->article_content : '');
2106 - $content_preview = mb_substr($parent_content, 0, 100);
2107 - echo esc_html($content_preview . '...');
2108 - ?>
2109 - </span>
2110 - </div>
2111 - </td>
2112 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2113 - <?php if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0 && strpos($source_url, '_ungrouped_') !== 0) : ?>
2114 - <a href="<?php echo esc_url($source_url); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2115 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2116 - <?php esc_html_e('View Source', 'mxchat'); ?>
2117 - </a>
2118 - <?php else : ?>
2119 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
2120 - <?php endif; ?>
2121 - </td>
2122 - <td class="mxchat-actions-cell" style="padding: 12px 16px; white-space: nowrap;">
2123 - <?php if ($data_source !== 'pinecone') : ?>
2124 - <button type="button"
2125 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2126 - data-source-url="<?php echo esc_attr($source_url); ?>"
2127 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2128 - data-data-source="<?php echo esc_attr($data_source); ?>"
2129 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2130 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2131 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2132 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2133 - </button>
2134 - <?php endif; ?>
2135 - <button type="button"
2136 - class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
2137 - data-source-url="<?php echo esc_attr($source_url); ?>"
2138 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
2139 - data-data-source="<?php echo esc_attr($data_source); ?>"
2140 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2141 - data-nonce="<?php echo wp_create_nonce('mxchat_delete_chunks_nonce'); ?>"
2142 - style="color: var(--mxch-error);"
2143 - title="<?php esc_attr_e('Delete all chunks', 'mxchat'); ?>">
2144 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2145 - </button>
2146 - </td>
2147 - </tr>
2148 - <?php
2149 - // Render hidden chunk rows
2150 - foreach ($group as $chunk_index => $chunk) {
2151 - $meta_chunk_index = isset($chunk->chunk_metadata['chunk_index']) ? intval($chunk->chunk_metadata['chunk_index']) : $chunk_index;
2152 - $meta_total_chunks = isset($chunk->chunk_metadata['total_chunks']) ? intval($chunk->chunk_metadata['total_chunks']) : $chunk_count;
2153 - $content = isset($chunk->display_content) ? $chunk->display_content : (isset($chunk->article_content) ? $chunk->article_content : '');
2154 - $content_preview = mb_strlen($content) > $preview_length
2155 - ? mb_substr($content, 0, $preview_length) . '...'
2156 - : $content;
2157 - ?>
2158 - <tr id="prompt-<?php echo esc_attr($chunk->id); ?>"
2159 - class="mxchat-chunk-row <?php echo esc_attr($group_id); ?>"
2160 - data-source="<?php echo esc_attr($data_source); ?>"
2161 - style="display: none; background: #f8f9fa; border-bottom: 1px solid var(--mxch-card-border);">
2162 - <td style="padding: 12px 16px; text-align: center;">
2163 - <!-- Checkbox column placeholder for chunks (managed by group) -->
2164 - </td>
2165 - <td style="padding: 12px 16px 12px 30px; font-size: 13px;">
2166 - <!-- Hidden ID column for chunks -->
2167 - </td>
2168 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2169 - <div class="mxchat-accordion-wrapper">
2170 - <div class="mxchat-content-preview">
2171 - <span class="mxchat-chunk-indicator" style="margin-right: 10px; color: var(--mxch-text-secondary); font-size: 12px;">
2172 - <?php printf(esc_html__('Chunk %d of %d', 'mxchat'), $meta_chunk_index + 1, $meta_total_chunks); ?>
2173 - </span>
2174 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2175 - <?php if (mb_strlen($content) > $preview_length) : ?>
2176 - <button class="mxchat-expand-toggle" type="button">
2177 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2178 - </button>
2179 - <?php endif; ?>
2180 - </div>
2181 - <div class="mxchat-content-full" style="display: none;">
2182 - <div class="content-view">
2183 - <?php
2184 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2185 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2186 - echo wp_kses_post(wpautop($content));
2187 - echo '</div>';
2188 - } else {
2189 - echo wp_kses_post(wpautop($content));
2190 - }
2191 - ?>
2192 - </div>
2193 - </div>
2194 - </div>
2195 - </td>
2196 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2197 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Same as parent', 'mxchat'); ?></span>
2198 - </td>
2199 - <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2200 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Managed by group', 'mxchat'); ?></span>
2201 - </td>
2202 - </tr>
2203 - <?php
2204 - }
2205 - } else {
2206 - // Single entry - display normally with accordion
2207 - $prompt = $first_prompt;
2208 - $content = isset($prompt->display_content) ? $prompt->display_content : (isset($prompt->article_content) ? $prompt->article_content : '');
2209 - $content_preview = mb_strlen($content) > $preview_length
2210 - ? mb_substr($content, 0, $preview_length) . '...'
2211 - : $content;
2212 - ?>
2213 - <tr id="prompt-<?php echo esc_attr($prompt->id); ?>"
2214 - data-source="<?php echo esc_attr($data_source); ?>"
2215 - style="border-bottom: 1px solid var(--mxch-card-border); background: rgba(33, 150, 243, 0.02);">
2216 - <td style="padding: 12px 16px; text-align: center;">
2217 - <input type="checkbox"
2218 - class="mxchat-entry-checkbox"
2219 - data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2220 - data-source="<?php echo esc_attr($data_source); ?>"
2221 - data-source-url="<?php echo esc_attr($prompt->source_url ?? ''); ?>"
2222 - data-is-group="false"
2223 - data-chunk-count="1">
2224 - </td>
2225 - <td style="padding: 12px 16px; font-size: 13px;">
2226 - <?php echo esc_html($display_index + (($current_page - 1) * $per_page)); ?>
2227 - </td>
2228 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2229 - <div class="mxchat-accordion-wrapper">
2230 - <div class="mxchat-content-preview">
2231 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2232 - <?php if (mb_strlen($content) > $preview_length) : ?>
2233 - <button class="mxchat-expand-toggle" type="button">
2234 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2235 - </button>
2236 - <?php endif; ?>
2237 - </div>
2238 - <div class="mxchat-content-full" style="display: none;">
2239 - <div class="content-view">
2240 - <?php
2241 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2242 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2243 - echo wp_kses_post(wpautop($content));
2244 - echo '</div>';
2245 - } else {
2246 - echo wp_kses_post(wpautop($content));
2247 - }
2248 - ?>
2249 - </div>
2250 - </div>
2251 - </div>
2252 - </td>
2253 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2254 - <?php
2255 - $actual_source = $source_url;
2256 - if (strpos($source_url, '_ungrouped_') === 0) {
2257 - $actual_source = $prompt->source_url ?? '';
2258 - }
2259 - if (!empty($actual_source) && strpos($actual_source, 'mxchat://') !== 0) : ?>
2260 - <a href="<?php echo esc_url($actual_source); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2261 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2262 - <?php esc_html_e('View', 'mxchat'); ?>
2263 - </a>
2264 - <?php else : ?>
2265 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
2266 - <?php endif; ?>
2267 - </td>
2268 - <td style="padding: 12px 16px;">
2269 - <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);">
2270 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2271 - </button>
2272 - </td>
2273 - </tr>
2274 - <?php
2275 - }
2276 - }
2277 - }
2278 - $html = ob_get_clean();
2279 -
2280 - // Generate pagination HTML for Pinecone
2281 - $total_pages = ceil($total_records / $per_page);
2282 - $pagination_html = '';
2283 - if ($total_pages > 1) {
2284 - $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) . '">';
2285 -
2286 - // Previous button
2287 - if ($page > 1) {
2288 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page - 1) . '">' . esc_html__('&laquo; Previous', 'mxchat') . '</a> ';
2289 - }
2290 -
2291 - // Page numbers
2292 - $start_page = max(1, $page - 2);
2293 - $end_page = min($total_pages, $page + 2);
2294 -
2295 - if ($start_page > 1) {
2296 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="1">1</a> ';
2297 - if ($start_page > 2) {
2298 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2299 - }
2300 - }
2301 -
2302 - for ($i = $start_page; $i <= $end_page; $i++) {
2303 - if ($i == $page) {
2304 - $pagination_html .= '<span class="mxchat-page-current">' . $i . '</span> ';
2305 - } else {
2306 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $i . '">' . $i . '</a> ';
2307 - }
2308 - }
2309 -
2310 - if ($end_page < $total_pages) {
2311 - if ($end_page < $total_pages - 1) {
2312 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2313 - }
2314 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $total_pages . '">' . $total_pages . '</a> ';
2315 - }
2316 -
2317 - // Next button
2318 - if ($page < $total_pages) {
2319 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page + 1) . '">' . esc_html__('Next &raquo;', 'mxchat') . '</a>';
2320 - }
2321 -
2322 - $pagination_html .= '</div>';
2323 - }
2324 -
2325 - wp_send_json_success(array(
2326 - 'html' => $html,
2327 - 'pagination_html' => $pagination_html,
2328 - 'total_count' => $total_records,
2329 - 'total_pages' => $total_pages,
2330 - 'page' => $page,
2331 - 'per_page' => $per_page,
2332 - 'data_source' => 'pinecone'
2333 - ));
2334 -}
2335 -
2336 -/**
2337 - * AJAX handler for pagination - handles both WordPress DB and Pinecone data sources
2338 - * Returns paginated entries without requiring a full page reload
2339 - */
2340 -public function ajax_mxchat_paginate_entries() {
2341 - check_ajax_referer('mxchat_entries_nonce', 'nonce');
2342 -
2343 - if (!current_user_can('manage_options')) {
2344 - wp_send_json_error(array('message' => 'Unauthorized'));
2345 - return;
2346 - }
2347 -
2348 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
2349 - $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
2350 - $search_query = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
2351 - $content_type_filter = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : '';
2352 - $per_page = 25;
2353 -
2354 - // Check if Pinecone is enabled for this bot
2355 - $pinecone_manager = $this->mxchat_get_pinecone_manager();
2356 - $pinecone_options = $pinecone_manager ? $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id) : array();
2357 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2358 - $has_pinecone_api = !empty($pinecone_options['mxchat_pinecone_api_key']);
2359 -
2360 - if ($use_pinecone && $has_pinecone_api) {
2361 - // Delegate to Pinecone pagination handler (pass search params)
2362 - $_POST['page'] = $page;
2363 - $_POST['search'] = $search_query;
2364 - $_POST['content_type'] = $content_type_filter;
2365 - $this->ajax_mxchat_refresh_pinecone_entries();
2366 - return;
2367 - }
2368 -
2369 - // WordPress DB pagination - MUST match initial page load logic exactly
2370 - global $wpdb;
2371 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2372 - $offset = ($page - 1) * $per_page;
2373 -
2374 - // Build WHERE clause for search and content type filtering
2375 - $where_clauses = array();
2376 - $where_values = array();
2377 -
2378 - if ($search_query) {
2379 - $where_clauses[] = "article_content LIKE %s";
2380 - $where_values[] = '%' . $wpdb->esc_like($search_query) . '%';
2381 - }
2382 -
2383 - if ($content_type_filter) {
2384 - switch ($content_type_filter) {
2385 - case 'manual':
2386 - $where_clauses[] = "(source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')";
2387 - break;
2388 - case 'pdf':
2389 - $where_clauses[] = "source_url LIKE '%.pdf'";
2390 - break;
2391 - case 'url':
2392 - $where_clauses[] = "source_url != '' AND source_url IS NOT NULL AND source_url NOT LIKE 'mxchat://%' AND source_url NOT LIKE '%.pdf'";
2393 - break;
2394 - }
2395 - }
2396 -
2397 - $where_sql = !empty($where_clauses) ? 'WHERE ' . implode(' AND ', $where_clauses) : '';
2398 -
2399 - // Count grouped entries with filters applied
2400 - if (!empty($where_values)) {
2401 - $count_args = array_merge($where_values, $where_values);
2402 - $count_query = $wpdb->prepare(
2403 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$where_sql} AND source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2404 - (SELECT COUNT(*) FROM {$table_name} {$where_sql} AND (source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%'))",
2405 - ...$count_args
2406 - );
2407 - $total_records = $wpdb->get_var($count_query);
2408 - } else if (!empty($where_sql)) {
2409 - // Content type filter only (no search), no prepared values needed
2410 - $total_records = $wpdb->get_var(
2411 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$where_sql} AND source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2412 - (SELECT COUNT(*) FROM {$table_name} {$where_sql} AND (source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%'))"
2413 - );
2414 - } else {
2415 - // No filters
2416 - $total_records = $wpdb->get_var(
2417 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2418 - (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')"
2419 - );
2420 - }
2421 - $total_pages = ceil($total_records / $per_page);
2422 -
2423 - // Step 1: Get unique source_urls for this page (ordered by latest timestamp) with filters
2424 - if (!empty($where_values)) {
2425 - $query_args = array_merge($where_values, array($per_page, $offset));
2426 - $urls_query = $wpdb->prepare(
2427 - "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2428 - {$where_sql}
2429 - GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2430 - ...$query_args
2431 - );
2432 - } else if (!empty($where_sql)) {
2433 - $urls_query = $wpdb->prepare(
2434 - "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2435 - {$where_sql}
2436 - GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2437 - $per_page, $offset
2438 - );
2439 - } else {
2440 - $urls_query = $wpdb->prepare(
2441 - "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2442 - GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2443 - $per_page, $offset
2444 - );
2445 - }
2446 - $page_urls = $wpdb->get_results($urls_query);
2447 -
2448 - // Step 2: Build list of source_urls to fetch
2449 - $url_list = array();
2450 - $url_order_map = array();
2451 - $order_index = 0;
2452 - foreach ($page_urls as $url_row) {
2453 - $url = $url_row->source_url;
2454 - $url_list[] = $url;
2455 - $url_order_map[$url] = $order_index++;
2456 - }
2457 -
2458 - // Step 3: Fetch all rows for these source_urls (with search filter if applicable)
2459 - $prompts = array();
2460 - if (!empty($url_list)) {
2461 - $placeholders = implode(',', array_fill(0, count($url_list), '%s'));
2462 - if ($search_query) {
2463 - // Include search filter in the final fetch
2464 - $prompts_query = $wpdb->prepare(
2465 - "SELECT id, article_content, source_url, timestamp, role_restriction
2466 - FROM {$table_name}
2467 - WHERE source_url IN ($placeholders) AND article_content LIKE %s
2468 - ORDER BY timestamp DESC",
2469 - ...array_merge($url_list, ['%' . $wpdb->esc_like($search_query) . '%'])
2470 - );
2471 - } else {
2472 - $prompts_query = $wpdb->prepare(
2473 - "SELECT id, article_content, source_url, timestamp, role_restriction
2474 - FROM {$table_name}
2475 - WHERE source_url IN ($placeholders)
2476 - ORDER BY timestamp DESC",
2477 - $url_list
2478 - );
2479 - }
2480 - $prompts = $wpdb->get_results($prompts_query);
2481 - }
2482 -
2483 - // Group prompts by source_url for chunk display
2484 - $grouped_prompts = array();
2485 - foreach ($prompts as $prompt) {
2486 - $source_url = $prompt->source_url ?? '';
2487 -
2488 - // Parse chunk metadata using the proper chunker method (same as initial page load)
2489 - if (class_exists('MxChat_Chunker')) {
2490 - $chunk_meta = MxChat_Chunker::parse_stored_chunk($prompt->article_content);
2491 - $prompt->chunk_metadata = $chunk_meta['metadata'];
2492 - $prompt->display_content = $chunk_meta['text'];
2493 - } else {
2494 - $prompt->chunk_metadata = array();
2495 - $prompt->display_content = $prompt->article_content;
2496 - }
2497 -
2498 - if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0) {
2499 - if (!isset($grouped_prompts[$source_url])) {
2500 - $grouped_prompts[$source_url] = array();
2501 - }
2502 - $grouped_prompts[$source_url][] = $prompt;
2503 - } else {
2504 - // Ungrouped entries
2505 - $grouped_prompts['_ungrouped_' . $prompt->id] = array($prompt);
2506 - }
2507 - }
2508 -
2509 - // Sort groups by the original URL order (newest first)
2510 - uksort($grouped_prompts, function($a, $b) use ($url_order_map) {
2511 - $order_a = isset($url_order_map[$a]) ? $url_order_map[$a] : PHP_INT_MAX;
2512 - $order_b = isset($url_order_map[$b]) ? $url_order_map[$b] : PHP_INT_MAX;
2513 - return $order_a - $order_b;
2514 - });
2515 -
2516 - // Sort each group internally by chunk_index
2517 - foreach ($grouped_prompts as $source_url => &$group) {
2518 - usort($group, function($a, $b) {
2519 - $index_a = isset($a->chunk_metadata['chunk_index']) ? intval($a->chunk_metadata['chunk_index']) : 0;
2520 - $index_b = isset($b->chunk_metadata['chunk_index']) ? intval($b->chunk_metadata['chunk_index']) : 0;
2521 - return $index_a - $index_b;
2522 - });
2523 - }
2524 - unset($group);
2525 -
2526 - // Build HTML for the table rows
2527 - ob_start();
2528 - $display_index = 0;
2529 - $current_page = $page;
2530 - $data_source = 'wordpress';
2531 - $current_bot_id = $bot_id;
2532 - $preview_length = 150;
2533 -
2534 - if (empty($grouped_prompts)) {
2535 - echo '<tr><td colspan="5" style="padding: 40px; text-align: center; color: var(--mxch-text-muted);">';
2536 - esc_html_e('No knowledge entries found. Use the Import Options to add content.', 'mxchat');
2537 - echo '</td></tr>';
2538 - } else {
2539 - foreach ($grouped_prompts as $source_url => $group) {
2540 - $chunk_count = count($group);
2541 - $first_prompt = $group[0];
2542 - $display_index++;
2543 -
2544 - if ($chunk_count > 1) {
2545 - // Multiple chunks - show grouped row with expand button
2546 - $group_id = 'group-' . md5($source_url);
2547 - ?>
2548 - <tr id="prompt-<?php echo esc_attr($first_prompt->id); ?>"
2549 - class="mxchat-chunk-group-header"
2550 - data-source="<?php echo esc_attr($data_source); ?>"
2551 - data-group-id="<?php echo esc_attr($group_id); ?>"
2552 - style="border-bottom: 1px solid var(--mxch-card-border);">
2553 - <td style="padding: 12px 16px; text-align: center;">
2554 - <input type="checkbox"
2555 - class="mxchat-entry-checkbox"
2556 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2557 - data-source="<?php echo esc_attr($data_source); ?>"
2558 - data-source-url="<?php echo esc_attr($source_url); ?>"
2559 - data-is-group="true"
2560 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>">
2561 - </td>
2562 - <td style="padding: 12px 16px; font-size: 13px;">
2563 - <?php echo esc_html($first_prompt->id); ?>
2564 - </td>
2565 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2566 - <div class="mxchat-chunk-group-info">
2567 - <button type="button" class="mxchat-chunk-toggle" data-group-id="<?php echo esc_attr($group_id); ?>">
2568 - <span class="dashicons dashicons-arrow-right-alt2"></span>
2569 - </button>
2570 - <span class="mxchat-chunk-badge"><?php echo esc_html($chunk_count); ?> <?php esc_html_e('chunks', 'mxchat'); ?></span>
2571 - <span class="mxchat-chunk-preview">
2572 - <?php
2573 - $parent_content = isset($first_prompt->display_content) ? $first_prompt->display_content : $first_prompt->article_content;
2574 - $content_preview = mb_substr($parent_content, 0, 100);
2575 - echo esc_html($content_preview . '...');
2576 - ?>
2577 - </span>
2578 - </div>
2579 - </td>
2580 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2581 - <?php if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0 && strpos($source_url, '_ungrouped_') !== 0) : ?>
2582 - <a href="<?php echo esc_url($source_url); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2583 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2584 - <?php esc_html_e('View Source', 'mxchat'); ?>
2585 - </a>
2586 - <?php else : ?>
2587 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
2588 - <?php endif; ?>
2589 - </td>
2590 - <td class="mxchat-actions-cell" style="padding: 12px 16px; white-space: nowrap;">
2591 - <?php if ($data_source !== 'pinecone') : ?>
2592 - <button type="button"
2593 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2594 - data-source-url="<?php echo esc_attr($source_url); ?>"
2595 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2596 - data-data-source="<?php echo esc_attr($data_source); ?>"
2597 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2598 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2599 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2600 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2601 - </button>
2602 - <?php endif; ?>
2603 - <button type="button"
2604 - class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
2605 - data-source-url="<?php echo esc_attr($source_url); ?>"
2606 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
2607 - data-data-source="<?php echo esc_attr($data_source); ?>"
2608 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2609 - data-nonce="<?php echo wp_create_nonce('mxchat_delete_chunks_nonce'); ?>"
2610 - style="color: var(--mxch-error);"
2611 - title="<?php esc_attr_e('Delete all chunks', 'mxchat'); ?>">
2612 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2613 - </button>
2614 - </td>
2615 - </tr>
2616 - <?php
2617 - // Render hidden chunk rows
2618 - foreach ($group as $chunk_index => $chunk) {
2619 - $meta_chunk_index = isset($chunk->chunk_metadata['chunk_index']) ? intval($chunk->chunk_metadata['chunk_index']) : $chunk_index;
2620 - $meta_total_chunks = isset($chunk->chunk_metadata['total_chunks']) ? intval($chunk->chunk_metadata['total_chunks']) : $chunk_count;
2621 - $content = isset($chunk->display_content) ? $chunk->display_content : $chunk->article_content;
2622 - $content_preview = mb_strlen($content) > $preview_length
2623 - ? mb_substr($content, 0, $preview_length) . '...'
2624 - : $content;
2625 - ?>
2626 - <tr id="prompt-<?php echo esc_attr($chunk->id); ?>"
2627 - class="mxchat-chunk-row <?php echo esc_attr($group_id); ?>"
2628 - data-source="<?php echo esc_attr($data_source); ?>"
2629 - style="display: none; background: #f8f9fa; border-bottom: 1px solid var(--mxch-card-border);">
2630 - <td style="padding: 12px 16px; text-align: center;">
2631 - <!-- Checkbox column placeholder for chunks (managed by group) -->
2632 - </td>
2633 - <td style="padding: 12px 16px 12px 30px; font-size: 13px;">
2634 - <!-- Hidden ID column for chunks -->
2635 - </td>
2636 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2637 - <div class="mxchat-accordion-wrapper">
2638 - <div class="mxchat-content-preview">
2639 - <span class="mxchat-chunk-indicator" style="margin-right: 10px; color: var(--mxch-text-secondary); font-size: 12px;">
2640 - <?php printf(esc_html__('Chunk %d of %d', 'mxchat'), $meta_chunk_index + 1, $meta_total_chunks); ?>
2641 - </span>
2642 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2643 - <?php if (mb_strlen($content) > $preview_length) : ?>
2644 - <button class="mxchat-expand-toggle" type="button">
2645 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2646 - </button>
2647 - <?php endif; ?>
2648 - </div>
2649 - <div class="mxchat-content-full" style="display: none;">
2650 - <div class="content-view">
2651 - <?php
2652 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2653 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2654 - echo wp_kses_post(wpautop($content));
2655 - echo '</div>';
2656 - } else {
2657 - echo wp_kses_post(wpautop($content));
2658 - }
2659 - ?>
2660 - </div>
2661 - </div>
2662 - </div>
2663 - </td>
2664 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2665 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Same as parent', 'mxchat'); ?></span>
2666 - </td>
2667 - <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2668 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Managed by group', 'mxchat'); ?></span>
2669 - </td>
2670 - </tr>
2671 - <?php
2672 - }
2673 - } else {
2674 - // Single entry - display normally with accordion
2675 - $prompt = $first_prompt;
2676 - $content = isset($prompt->display_content) ? $prompt->display_content : $prompt->article_content;
2677 - $content_preview = mb_strlen($content) > $preview_length
2678 - ? mb_substr($content, 0, $preview_length) . '...'
2679 - : $content;
2680 - ?>
2681 - <tr id="prompt-<?php echo esc_attr($prompt->id); ?>"
2682 - data-source="<?php echo esc_attr($data_source); ?>"
2683 - style="border-bottom: 1px solid var(--mxch-card-border);">
2684 - <td style="padding: 12px 16px; text-align: center;">
2685 - <input type="checkbox"
2686 - class="mxchat-entry-checkbox"
2687 - data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2688 - data-source="<?php echo esc_attr($data_source); ?>"
2689 - data-source-url="<?php echo esc_attr($source_url); ?>"
2690 - data-is-group="false">
2691 - </td>
2692 - <td style="padding: 12px 16px; font-size: 13px;">
2693 - <?php echo esc_html($prompt->id); ?>
2694 - </td>
2695 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2696 - <div class="mxchat-accordion-wrapper">
2697 - <div class="mxchat-content-preview">
2698 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2699 - <?php if (mb_strlen($content) > $preview_length) : ?>
2700 - <button class="mxchat-expand-toggle" type="button">
2701 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2702 - </button>
2703 - <?php endif; ?>
2704 - </div>
2705 - <div class="mxchat-content-full" style="display: none;">
2706 - <div class="content-view">
2707 - <?php
2708 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2709 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2710 - echo wp_kses_post(wpautop($content));
2711 - echo '</div>';
2712 - } else {
2713 - echo wp_kses_post(wpautop($content));
2714 - }
2715 - ?>
2716 - </div>
2717 - </div>
2718 - </div>
2719 - </td>
2720 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2721 - <?php
2722 - $actual_source = $source_url;
2723 - if (strpos($source_url, '_ungrouped_') === 0) {
2724 - $actual_source = $prompt->source_url ?? '';
2725 - }
2726 - if (!empty($actual_source) && strpos($actual_source, 'mxchat://') !== 0) : ?>
2727 - <a href="<?php echo esc_url($actual_source); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2728 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2729 - <?php esc_html_e('View', 'mxchat'); ?>
2730 - </a>
2731 - <?php else : ?>
2732 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
2733 - <?php endif; ?>
2734 - </td>
2735 - <td style="padding: 12px 16px; white-space: nowrap;">
2736 - <button type="button"
2737 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2738 - data-source-url="<?php echo esc_attr($prompt->source_url ?? ''); ?>"
2739 - data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2740 - data-data-source="<?php echo esc_attr($data_source); ?>"
2741 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2742 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2743 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2744 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2745 - </button>
2746 - <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);">
2747 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2748 - </button>
2749 - </td>
2750 - </tr>
2751 - <?php
2752 - }
2753 - }
2754 - }
2755 - $html = ob_get_clean();
2756 -
2757 - // Generate pagination HTML (include search/filter data for subsequent pages)
2758 - $pagination_html = '';
2759 - if ($total_pages > 1) {
2760 - $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) . '">';
2761 -
2762 - // Previous button
2763 - if ($page > 1) {
2764 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page - 1) . '">' . esc_html__('&laquo; Previous', 'mxchat') . '</a> ';
2765 - }
2766 -
2767 - // Page numbers
2768 - $start_page = max(1, $page - 2);
2769 - $end_page = min($total_pages, $page + 2);
2770 -
2771 - if ($start_page > 1) {
2772 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="1">1</a> ';
2773 - if ($start_page > 2) {
2774 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2775 - }
2776 - }
2777 -
2778 - for ($i = $start_page; $i <= $end_page; $i++) {
2779 - if ($i == $page) {
2780 - $pagination_html .= '<span class="mxchat-page-current">' . $i . '</span> ';
2781 - } else {
2782 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $i . '">' . $i . '</a> ';
2783 - }
2784 - }
2785 -
2786 - if ($end_page < $total_pages) {
2787 - if ($end_page < $total_pages - 1) {
2788 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2789 - }
2790 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $total_pages . '">' . $total_pages . '</a> ';
2791 - }
2792 -
2793 - // Next button
2794 - if ($page < $total_pages) {
2795 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page + 1) . '">' . esc_html__('Next &raquo;', 'mxchat') . '</a>';
2796 - }
2797 -
2798 - $pagination_html .= '</div>';
2799 - }
2800 -
2801 - wp_send_json_success(array(
2802 - 'html' => $html,
2803 - 'pagination_html' => $pagination_html,
2804 - 'total_count' => $total_records,
2805 - 'total_pages' => $total_pages,
2806 - 'page' => $page,
2807 - 'per_page' => $per_page,
2808 - 'data_source' => 'wordpress'
2809 - ));
2810 -}
2811 -
2812 -/**
2813 - * AJAX handler to detect available sitemaps on the site
2814 - * Optimized for speed - only checks primary sitemap indexes first
2815 - */
2816 -public function ajax_mxchat_detect_sitemaps() {
2817 - check_ajax_referer('mxchat_detect_sitemaps_nonce', 'nonce');
2818 -
2819 - if (!current_user_can('manage_options')) {
2820 - wp_send_json_error(array('message' => 'Unauthorized'));
2821 - return;
2822 - }
2823 -
2824 - $site_url = get_site_url();
2825 - $sitemaps = array();
2826 - $found_index = false;
2827 -
2828 - // Only check the main sitemap index files first (much faster)
2829 - // These are the primary entry points that contain sub-sitemaps
2830 - $primary_indexes = array(
2831 - 'sitemap_index.xml' => 'Yoast SEO / Rank Math', // Yoast & Rank Math
2832 - 'wp-sitemap.xml' => 'WordPress Core', // WordPress Core
2833 - 'sitemap.xml' => 'Standard', // Generic/AIOSEO
2834 - );
2835 -
2836 - foreach ($primary_indexes as $path => $source) {
2837 - $url = trailingslashit($site_url) . $path;
2838 -
2839 - $response = wp_remote_head($url, array(
2840 - 'timeout' => 10,
2841 - 'sslverify' => false,
2842 - 'redirection' => 1,
2843 - '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',
2844 - ));
2845 -
2846 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2847 - // Found a sitemap index - parse it to get sub-sitemaps
2848 - $sub_sitemaps = $this->parse_sitemap_index($url);
2849 - if (!empty($sub_sitemaps)) {
2850 - $sitemaps[] = array(
2851 - 'url' => $url,
2852 - 'type' => 'index',
2853 - 'source' => $source,
2854 - 'sub_sitemaps' => $sub_sitemaps
2855 - );
2856 - $found_index = true;
2857 - // Found a valid index, no need to check others
2858 - break;
2859 - }
2860 - }
2861 - }
2862 -
2863 - // If no sitemap index found, check for standalone sitemaps
2864 - if (!$found_index) {
2865 - $standalone_sitemaps = array(
2866 - 'post-sitemap.xml' => array('type' => 'content', 'source' => 'Yoast SEO'),
2867 - 'page-sitemap.xml' => array('type' => 'content', 'source' => 'Yoast SEO'),
2868 - );
2869 -
2870 - foreach ($standalone_sitemaps as $path => $info) {
2871 - $url = trailingslashit($site_url) . $path;
2872 -
2873 - $response = wp_remote_head($url, array(
2874 - 'timeout' => 2,
2875 - 'sslverify' => false
2876 - ));
2877 -
2878 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2879 - $sitemaps[] = array(
2880 - 'url' => $url,
2881 - 'type' => $info['type'],
2882 - 'source' => $info['source'],
2883 - 'url_count' => 0 // Skip URL count for speed
2884 - );
2885 - }
2886 - }
2887 - }
2888 -
2889 - wp_send_json_success(array(
2890 - 'sitemaps' => $sitemaps,
2891 - 'site_url' => $site_url
2892 - ));
2893 -}
2894 -
2895 -/**
2896 - * Parse a sitemap index to get sub-sitemaps
2897 - * Optimized: doesn't fetch URL count for each sub-sitemap (too slow)
2898 - */
2899 -private function parse_sitemap_index($url) {
2900 - $sub_sitemaps = array();
2901 -
2902 - $response = wp_remote_get($url, array(
2903 - 'timeout' => 30,
2904 - 'sslverify' => false,
2905 - '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',
2906 - 'headers' => array(
2907 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
2908 - 'Accept-Language' => 'en-US,en;q=0.9',
2909 - ),
2910 - ));
2911 -
2912 - if (is_wp_error($response)) {
2913 - return $sub_sitemaps;
2914 - }
2915 -
2916 - $body = wp_remote_retrieve_body($response);
2917 - if (empty($body)) {
2918 - return $sub_sitemaps;
2919 - }
2920 -
2921 - // Suppress XML errors
2922 - libxml_use_internal_errors(true);
2923 - $xml = simplexml_load_string($body);
2924 - libxml_clear_errors();
2925 -
2926 - if ($xml === false) {
2927 - return $sub_sitemaps;
2928 - }
2929 -
2930 - // Check if it's a sitemap index (contains <sitemap> elements)
2931 - if (isset($xml->sitemap)) {
2932 - foreach ($xml->sitemap as $sitemap) {
2933 - $loc = (string) $sitemap->loc;
2934 - if (!empty($loc)) {
2935 - // Try to determine the type from the URL
2936 - $type = 'content';
2937 - if (strpos($loc, 'category') !== false || strpos($loc, 'tag') !== false || strpos($loc, 'taxonomy') !== false) {
2938 - $type = 'taxonomy';
2939 - } elseif (strpos($loc, 'author') !== false || strpos($loc, 'user') !== false) {
2940 - $type = 'author';
2941 - }
2942 -
2943 - // Skip URL count - too slow to fetch for each sitemap
2944 - $sub_sitemaps[] = array(
2945 - 'url' => $loc,
2946 - 'type' => $type,
2947 - 'url_count' => 0, // Don't fetch - takes too long
2948 - 'name' => basename(parse_url($loc, PHP_URL_PATH))
2949 - );
2950 - }
2951 - }
2952 - }
2953 -
2954 - return $sub_sitemaps;
2955 -}
2956 -
2957 -/**
2958 - * Get URL count from a sitemap
2959 - */
2960 -private function get_sitemap_url_count($url) {
2961 - $response = wp_remote_get($url, array(
2962 - 'timeout' => 30,
2963 - 'sslverify' => false,
2964 - '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',
2965 - 'headers' => array(
2966 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
2967 - 'Accept-Language' => 'en-US,en;q=0.9',
2968 - ),
2969 - ));
2970 -
2971 - if (is_wp_error($response)) {
2972 - return 0;
2973 - }
2974 -
2975 - $body = wp_remote_retrieve_body($response);
2976 - if (empty($body)) {
2977 - return 0;
2978 - }
2979 -
2980 - // Count <url> or <loc> elements
2981 - $count = preg_match_all('/<url>/i', $body, $matches);
2982 - return $count ?: 0;
2983 -}
2984 -
2985 -/**
2986 - * Get sitemaps declared in robots.txt
2987 - */
2988 -private function get_sitemaps_from_robots($site_url) {
2989 - $sitemaps = array();
2990 - $robots_url = trailingslashit($site_url) . 'robots.txt';
2991 -
2992 - $response = wp_remote_get($robots_url, array(
2993 - 'timeout' => 15,
2994 - 'sslverify' => false,
2995 - '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',
2996 - ));
2997 -
2998 - if (is_wp_error($response)) {
2999 - return $sitemaps;
3000 - }
3001 -
3002 - $body = wp_remote_retrieve_body($response);
3003 - if (empty($body)) {
3004 - return $sitemaps;
3005 - }
3006 -
3007 - // Find Sitemap: declarations
3008 - if (preg_match_all('/^Sitemap:\s*(.+)$/mi', $body, $matches)) {
3009 - foreach ($matches[1] as $sitemap_url) {
3010 - $sitemap_url = trim($sitemap_url);
3011 - if (filter_var($sitemap_url, FILTER_VALIDATE_URL)) {
3012 - $sitemaps[] = $sitemap_url;
3013 - }
3014 - }
3015 - }
3016 -
3017 - return $sitemaps;
3018 -}
3019 -
3020 -public function mxchat_stop_processing() {
3021 - // Verify permissions
3022 - if (!current_user_can('manage_options')) {
3023 - wp_die(esc_html__('Unauthorized access', 'mxchat'));
3024 - }
3025 -
3026 - // Verify nonce
3027 - check_admin_referer('mxchat_stop_processing_action', 'mxchat_stop_processing_nonce');
3028 -
3029 - global $wpdb;
3030 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
3031 -
3032 - // Get active queue IDs
3033 - $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
3034 - $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
3035 -
3036 - // Delete all pending items from active queues
3037 - if ($sitemap_queue_id) {
3038 - $wpdb->delete(
3039 - $table_name,
3040 - array(
3041 - 'queue_id' => $sitemap_queue_id,
3042 - 'status' => 'pending'
3043 - ),
3044 - array('%s', '%s')
3045 - );
3046 -
3047 - delete_transient('mxchat_active_queue_sitemap');
3048 - delete_transient('mxchat_last_sitemap_url');
3049 - }
3050 -
3051 - if ($pdf_queue_id) {
3052 - // Get PDF path before deleting
3053 - $pdf_path = $this->mxchat_get_queue_meta($pdf_queue_id, 'pdf_path');
3054 -
3055 - $wpdb->delete(
3056 - $table_name,
3057 - array(
3058 - 'queue_id' => $pdf_queue_id,
3059 - 'status' => 'pending'
3060 - ),
3061 - array('%s', '%s')
3062 - );
3063 -
3064 - // Delete PDF file
3065 - if ($pdf_path && file_exists($pdf_path)) {
3066 - wp_delete_file($pdf_path);
3067 - }
3068 -
3069 - delete_transient('mxchat_active_queue_pdf');
3070 - delete_transient('mxchat_last_pdf_url');
3071 - }
3072 -
3073 - // Redirect back with a success message
3074 - set_transient('mxchat_admin_notice_success',
3075 - esc_html__('Processing has been stopped successfully.', 'mxchat'),
3076 - 30
3077 - );
3078 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
3079 - exit;
3080 -}
3081 -
3082 -/**
3083 - * Get content list for processing
3084 - */
3085 -public function ajax_mxchat_get_content_list() {
3086 - // Verify the nonce
3087 - check_ajax_referer('mxchat_content_selector_nonce', 'nonce');
3088 -
3089 - if (!current_user_can('manage_options')) {
3090 - wp_send_json_error(__('Unauthorized access', 'mxchat'));
3091 - }
3092 -
3093 - $page = isset($_GET['page']) ? absint($_GET['page']) : 1;
3094 - $per_page = isset($_GET['per_page']) ? absint($_GET['per_page']) : 100;
3095 - $search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
3096 - $post_type = isset($_GET['post_type']) ? sanitize_text_field($_GET['post_type']) : 'all';
3097 - $post_status = isset($_GET['post_status']) ? sanitize_text_field($_GET['post_status']) : 'publish';
3098 - $processed_filter = isset($_GET['processed_filter']) ? sanitize_text_field($_GET['processed_filter']) : 'all';
3099 -
3100 - // Build query args
3101 - $args = array(
3102 - 'posts_per_page' => $per_page,
3103 - 'paged' => $page,
3104 - 'post_status' => $post_status !== 'all' ? $post_status : array('publish', 'draft', 'pending'),
3105 - 'orderby' => 'date',
3106 - 'order' => 'DESC',
3107 - );
3108 -
3109 - // Handle post types - IMPROVED VERSION
3110 - if ($post_type !== 'all') {
3111 - $args['post_type'] = $post_type;
3112 - } else {
3113 - // Get all available post types that might contain content
3114 - $all_post_types = array();
3115 -
3116 - // First get all public post types
3117 - $public_types = get_post_types(array('public' => true), 'names');
3118 - $all_post_types = array_merge($all_post_types, $public_types);
3119 -
3120 - // Add common forum/community post types
3121 - $forum_types = array('topic', 'reply', 'forum', 'wpforo_topic', 'wpforo_post');
3122 - foreach ($forum_types as $forum_type) {
3123 - if (post_type_exists($forum_type)) {
3124 - $all_post_types[] = $forum_type;
3125 - }
3126 - }
3127 -
3128 - // Add other commonly used post types
3129 - $common_types = array('product', 'job_listing', 'event', 'portfolio');
3130 - foreach ($common_types as $common_type) {
3131 - if (post_type_exists($common_type)) {
3132 - $all_post_types[] = $common_type;
3133 - }
3134 - }
3135 -
3136 - // Remove duplicates and ensure we have at least some post types
3137 - $all_post_types = array_unique($all_post_types);
3138 -
3139 - if (empty($all_post_types)) {
3140 - // Fallback to basic post types
3141 - $all_post_types = array('post', 'page');
3142 - }
3143 -
3144 - $args['post_type'] = $all_post_types;
3145 -
3146 - // Debug logging to see what post types are being queried
3147 - //error_log('MxChat Debug: Querying post types: ' . implode(', ', $all_post_types));
3148 - }
3149 -
3150 - if (!empty($search)) {
3151 - $args['s'] = $search;
3152 - }
3153 -
3154 - // Get processed data from storage
3155 - $processed_data = array();
3156 -
3157 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3158 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3159 -
3160 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3161 - // Get fresh data from Pinecone - no caching
3162 - $processed_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
3163 - } else {
3164 - // WordPress DB checking with better URL matching for all post types
3165 - global $wpdb;
3166 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3167 - $processed_items = $wpdb->get_results("SELECT id, source_url, article_content, timestamp FROM {$table_name}");
3168 -
3169 - // Group items by source_url to count chunks
3170 - $url_chunk_counts = array();
3171 - $url_latest_timestamp = array();
3172 - $url_first_id = array();
3173 -
3174 - if (!empty($processed_items)) {
3175 - foreach ($processed_items as $item) {
3176 - $url = $item->source_url;
3177 - if (empty($url)) continue;
3178 -
3179 - // Count chunks per URL
3180 - if (!isset($url_chunk_counts[$url])) {
3181 - $url_chunk_counts[$url] = 0;
3182 - $url_latest_timestamp[$url] = $item->timestamp;
3183 - $url_first_id[$url] = $item->id;
3184 - }
3185 - $url_chunk_counts[$url]++;
3186 -
3187 - // Track latest timestamp
3188 - if (strtotime($item->timestamp) > strtotime($url_latest_timestamp[$url])) {
3189 - $url_latest_timestamp[$url] = $item->timestamp;
3190 - }
3191 - }
3192 -
3193 - // Now build processed_data with chunk counts
3194 - foreach ($url_chunk_counts as $url => $chunk_count) {
3195 - $post_id = $this->mxchat_url_to_post_id_improved($url);
3196 -
3197 - if ($post_id) {
3198 - $processed_data[$post_id] = array(
3199 - 'db_id' => $url_first_id[$url],
3200 - 'timestamp' => $url_latest_timestamp[$url],
3201 - 'url' => $url,
3202 - 'source' => 'wordpress',
3203 - 'chunk_count' => $chunk_count
3204 - );
3205 - }
3206 - }
3207 - }
3208 - }
3209 -
3210 - // Get processed IDs as a simple array for in_array checks
3211 - $processed_ids = array_keys($processed_data);
3212 -
3213 - // Handle processed/unprocessed filter
3214 - if ($processed_filter === 'processed' && !empty($processed_ids)) {
3215 - $args['post__in'] = $processed_ids;
3216 - } elseif ($processed_filter === 'unprocessed' && !empty($processed_ids)) {
3217 - $args['post__not_in'] = $processed_ids;
3218 - }
3219 -
3220 - // Run the query
3221 - $query = new WP_Query($args);
3222 - $content_items = array();
3223 -
3224 - if ($query->have_posts()) {
3225 - while ($query->have_posts()) {
3226 - $query->the_post();
3227 - $id = get_the_ID();
3228 - $post_date = get_the_date();
3229 - $excerpt = wp_trim_words(get_the_excerpt(), 20, '...');
3230 - $word_count = str_word_count(strip_tags(get_the_content()));
3231 -
3232 - $is_processed = in_array($id, $processed_ids);
3233 - $processed_date = '';
3234 - $db_record_id = 0;
3235 - $data_source = 'none';
3236 -
3237 - if ($is_processed && isset($processed_data[$id])) {
3238 - $item_data = $processed_data[$id];
3239 - $data_source = $item_data['source'];
3240 -
3241 - if ($data_source === 'wordpress' && isset($item_data['timestamp'])) {
3242 - // WordPress DB format
3243 - $timestamp = strtotime($item_data['timestamp']);
3244 - $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
3245 - $db_record_id = $item_data['db_id'];
3246 - } elseif ($data_source === 'pinecone') {
3247 - // Pinecone format
3248 - $processed_date = $item_data['processed_date'];
3249 - $db_record_id = $item_data['db_id'];
3250 - }
3251 - }
3252 -
3253 - // Get chunk count for this item
3254 - $chunk_count = 0;
3255 - if ($is_processed && isset($processed_data[$id]['chunk_count'])) {
3256 - $chunk_count = intval($processed_data[$id]['chunk_count']);
3257 - }
3258 -
3259 - $content_items[] = array(
3260 - 'id' => $id,
3261 - 'title' => get_the_title(),
3262 - 'permalink' => get_permalink(),
3263 - 'date' => $post_date,
3264 - 'type' => get_post_type(),
3265 - 'status' => get_post_status(),
3266 - 'excerpt' => $excerpt,
3267 - 'word_count' => $word_count,
3268 - 'already_processed' => $is_processed,
3269 - 'processed_date' => $processed_date,
3270 - 'db_record_id' => $db_record_id,
3271 - 'data_source' => $data_source,
3272 - 'chunk_count' => $chunk_count
3273 - );
3274 - }
3275 - wp_reset_postdata();
3276 - }
3277 -
3278 - $response = array(
3279 - 'items' => $content_items,
3280 - 'total' => $query->found_posts,
3281 - 'total_pages' => $query->max_num_pages,
3282 - 'current_page' => $page,
3283 - 'processed_count' => count($processed_ids)
3284 - );
3285 -
3286 - wp_send_json_success($response);
3287 - exit;
3288 -}
3289 -
3290 -
3291 -/**
3292 - * This function handles various WooCommerce URL formats and permalink structures
3293 - */
3294 -private function mxchat_url_to_post_id_improved($url) {
3295 - // First try the standard WordPress function
3296 - $post_id = url_to_postid($url);
3297 -
3298 - if ($post_id > 0) {
3299 - return $post_id;
3300 - }
3301 -
3302 - // If that fails, try more aggressive URL matching
3303 - // Remove trailing slashes and query parameters for better matching
3304 - $clean_url = rtrim($url, '/');
3305 - $clean_url = strtok($clean_url, '?'); // Remove query parameters
3306 -
3307 - // Try again with cleaned URL
3308 - $post_id = url_to_postid($clean_url);
3309 - if ($post_id > 0) {
3310 - return $post_id;
3311 - }
3312 -
3313 - // For bbPress forum topics, try extracting slug from URL
3314 - if (strpos($url, '/topic/') !== false || strpos($url, '/forums/') !== false) {
3315 - // Handle bbPress URLs: /forums/topic/topic-name/
3316 - if (preg_match('/\/forums\/topic\/([^\/\?]+)/', $url, $matches)) {
3317 - $topic_slug = $matches[1];
3318 -
3319 - // Look up topic by slug
3320 - $topic = get_page_by_path($topic_slug, OBJECT, 'topic');
3321 - if ($topic) {
3322 - return $topic->ID;
3323 - }
3324 -
3325 - // Alternative method: query by post_name
3326 - global $wpdb;
3327 - $post_id = $wpdb->get_var($wpdb->prepare(
3328 - "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
3329 - $topic_slug
3330 - ));
3331 -
3332 - if ($post_id) {
3333 - return intval($post_id);
3334 - }
3335 - }
3336 -
3337 - // Handle simpler topic URLs: /topic/topic-name/
3338 - if (preg_match('/\/topic\/([^\/\?]+)/', $url, $matches)) {
3339 - $topic_slug = $matches[1];
3340 -
3341 - global $wpdb;
3342 - $post_id = $wpdb->get_var($wpdb->prepare(
3343 - "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
3344 - $topic_slug
3345 - ));
3346 -
3347 - if ($post_id) {
3348 - return intval($post_id);
3349 - }
3350 - }
3351 - }
3352 -
3353 - // For WooCommerce products
3354 - if (strpos($url, '/product/') !== false || strpos($url, 'product=') !== false) {
3355 - // Extract product slug from various URL formats
3356 - $product_slug = '';
3357 -
3358 - // Handle pretty permalinks: /product/product-name/
3359 - if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
3360 - $product_slug = $matches[1];
3361 - }
3362 - // Handle query parameters: ?product=product-name
3363 - elseif (preg_match('/[\?&]product=([^&]+)/', $url, $matches)) {
3364 - $product_slug = $matches[1];
3365 - }
3366 -
3367 - if (!empty($product_slug)) {
3368 - // Look up product by slug
3369 - $product = get_page_by_path($product_slug, OBJECT, 'product');
3370 - if ($product) {
3371 - return $product->ID;
3372 - }
3373 -
3374 - // Alternative method: query by post_name
3375 - global $wpdb;
3376 - $post_id = $wpdb->get_var($wpdb->prepare(
3377 - "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'product' AND post_status = 'publish'",
3378 - $product_slug
3379 - ));
3380 -
3381 - if ($post_id) {
3382 - return intval($post_id);
3383 - }
3384 - }
3385 - }
3386 -
3387 - // Generic approach: try to extract slug and match against all post types
3388 - $parsed_url = wp_parse_url($clean_url);
3389 - $path = $parsed_url['path'] ?? '';
3390 -
3391 - if (!empty($path)) {
3392 - // Get the last part of the path as potential slug
3393 - $path_parts = array_filter(explode('/', trim($path, '/')));
3394 - $potential_slug = end($path_parts);
3395 -
3396 - if (!empty($potential_slug)) {
3397 - global $wpdb;
3398 -
3399 - // Try to find any post with this slug
3400 - $post_id = $wpdb->get_var($wpdb->prepare(
3401 - "SELECT ID FROM {$wpdb->posts}
3402 - WHERE post_name = %s
3403 - AND post_status IN ('publish', 'closed', 'private')
3404 - AND post_type NOT IN ('revision', 'attachment', 'nav_menu_item')
3405 - ORDER BY CASE
3406 - WHEN post_type = 'post' THEN 1
3407 - WHEN post_type = 'page' THEN 2
3408 - WHEN post_type = 'topic' THEN 3
3409 - WHEN post_type = 'product' THEN 4
3410 - ELSE 5
3411 - END
3412 - LIMIT 1",
3413 - $potential_slug
3414 - ));
3415 -
3416 - if ($post_id) {
3417 - return intval($post_id);
3418 - }
3419 - }
3420 - }
3421 -
3422 - // ADDITIONAL: Try direct database lookup by URL variations
3423 - global $wpdb;
3424 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3425 -
3426 - // Try variations of the URL (with/without trailing slash, http/https)
3427 - $url_variations = array(
3428 - $url,
3429 - rtrim($url, '/'),
3430 - $url . '/',
3431 - str_replace('http://', 'https://', $url),
3432 - str_replace('https://', 'http://', $url),
3433 - str_replace('http://', 'https://', rtrim($url, '/')),
3434 - str_replace('https://', 'http://', rtrim($url, '/'))
3435 - );
3436 -
3437 - // Remove duplicates
3438 - $url_variations = array_unique($url_variations);
3439 -
3440 - foreach ($url_variations as $variation) {
3441 - $existing_record = $wpdb->get_row($wpdb->prepare(
3442 - "SELECT id, source_url FROM $table_name WHERE source_url = %s",
3443 - $variation
3444 - ));
3445 -
3446 - if ($existing_record) {
3447 - // Try to get post ID from this stored URL
3448 - $stored_post_id = url_to_postid($existing_record->source_url);
3449 - if ($stored_post_id > 0) {
3450 - return $stored_post_id;
3451 - }
3452 - }
3453 - }
3454 -
3455 - return 0; // No match found
3456 -}
3457 -/**
3458 - * Process selected content via AJAX
3459 - */
3460 -public function ajax_mxchat_process_selected_content() {
3461 - // Basic request validation
3462 - if (!check_ajax_referer('mxchat_content_selector_nonce', 'nonce', false)) {
3463 - wp_send_json_error('Invalid nonce');
3464 - exit;
3465 - }
3466 -
3467 - if (!current_user_can('manage_options')) {
3468 - wp_send_json_error('Unauthorized access');
3469 - exit;
3470 - }
3471 -
3472 - // Get post IDs - safely parse the array
3473 - $post_ids = array();
3474 - if (isset($_POST['post_ids']) && is_array($_POST['post_ids'])) {
3475 - foreach ($_POST['post_ids'] as $id) {
3476 - $post_ids[] = absint($id);
3477 - }
3478 - }
3479 -
3480 - if (empty($post_ids)) {
3481 - wp_send_json_error('No content selected');
3482 - exit;
3483 - }
3484 -
3485 - // Get bot_id from request
3486 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
3487 -
3488 - // Process only ONE post at a time to avoid request size issues
3489 - $post_id = reset($post_ids);
3490 - $post = get_post($post_id);
3491 -
3492 - if (!$post) {
3493 - wp_send_json_error('Post not found');
3494 - exit;
3495 - }
3496 -
3497 - // Allow developers to modify post data before processing into knowledge base
3498 - $post = apply_filters('mxchat_before_process_post', $post, $bot_id);
3499 -
3500 - // Get content including title, short description (for WooCommerce), and main content
3501 - $content = $post->post_title . "\n\n";
3502 -
3503 - // Add short description if it exists (WooCommerce products use post_excerpt for short description)
3504 - if (!empty($post->post_excerpt)) {
3505 - // Remove shortcode tags but preserve content inside them
3506 - $clean_excerpt = $this->strip_shortcode_tags_preserve_content($post->post_excerpt);
3507 - $content .= "Short Description: " . wp_strip_all_tags($clean_excerpt) . "\n\n";
3508 - }
3509 -
3510 - // Add main content - remove shortcode tags but preserve content inside them
3511 - $clean_content = $this->strip_shortcode_tags_preserve_content($post->post_content);
3512 - $content .= wp_strip_all_tags($clean_content);
3513 -
3514 - // ADD WOOCOMMERCE PRODUCT DATA (pricing, stock, categories, custom tabs)
3515 - if (get_post_type($post_id) === 'product' && class_exists('WooCommerce')) {
3516 - $product = wc_get_product($post_id);
3517 -
3518 - if ($product) {
3519 - // Get pricing information
3520 - $regular_price = $product->get_regular_price();
3521 - $sale_price = $product->get_sale_price();
3522 - $price = $product->get_price();
3523 - $sku = $product->get_sku();
3524 -
3525 - // Get currency symbol
3526 - $currency_symbol = get_woocommerce_currency_symbol();
3527 -
3528 - // Add pricing information
3529 - $content .= "\n";
3530 - if (!empty($regular_price)) {
3531 - $content .= "Price: " . $currency_symbol . $regular_price . "\n";
3532 - } elseif (!empty($price)) {
3533 - $content .= "Price: " . $currency_symbol . $price . "\n";
3534 - }
3535 -
3536 - if (!empty($sale_price) && $sale_price !== $regular_price) {
3537 - $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
3538 - }
3539 -
3540 - // Handle variable products - show price range
3541 - if ($product->is_type('variable')) {
3542 - $min_price = $product->get_variation_price('min');
3543 - $max_price = $product->get_variation_price('max');
3544 - if ($min_price !== $max_price) {
3545 - $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
3546 - }
3547 - }
3548 -
3549 - if (!empty($sku)) {
3550 - $content .= "SKU: " . $sku . "\n";
3551 - }
3552 -
3553 - // Get product categories
3554 - $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
3555 - if (!empty($categories) && !is_wp_error($categories)) {
3556 - $content .= "Categories: " . implode(', ', $categories) . "\n";
3557 - }
3558 - }
3559 -
3560 - // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
3561 - $custom_tabs = get_post_meta($post_id, 'yikes_woo_products_tabs', true);
3562 - if (!empty($custom_tabs) && is_array($custom_tabs)) {
3563 - foreach ($custom_tabs as $tab) {
3564 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
3565 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
3566 -
3567 - if (!empty($tab_title) && !empty($tab_content)) {
3568 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
3569 - }
3570 - }
3571 - }
3572 -
3573 - // Also check for reusable/saved tabs applied to this product
3574 - $applied_saved_tabs = get_post_meta($post_id, 'yikes_woo_reusable_products_tabs_applied', true);
3575 - if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
3576 - $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
3577 - if (!empty($saved_tabs) && is_array($saved_tabs)) {
3578 - foreach ($applied_saved_tabs as $saved_tab_id) {
3579 - if (isset($saved_tabs[$saved_tab_id])) {
3580 - $tab = $saved_tabs[$saved_tab_id];
3581 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
3582 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
3583 -
3584 - if (!empty($tab_title) && !empty($tab_content)) {
3585 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
3586 - }
3587 - }
3588 - }
3589 - }
3590 - }
3591 - }
3592 -
3593 - // ADD ACF FIELDS SUPPORT
3594 - $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
3595 - if (!empty($acf_fields)) {
3596 - $acf_content_parts = array();
3597 -
3598 - foreach ($acf_fields as $field_name => $field_value) {
3599 - $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
3600 -
3601 - if (!empty($formatted_value)) {
3602 - $field_label = ucwords(str_replace('_', ' ', $field_name));
3603 - $acf_content_parts[] = $field_label . ": " . $formatted_value;
3604 - }
3605 - }
3606 -
3607 - if (!empty($acf_content_parts)) {
3608 - $content .= "\n\n" . implode("\n", $acf_content_parts);
3609 - }
3610 - }
3611 -
3612 - // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
3613 - $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
3614 - if (!empty($custom_meta)) {
3615 - $meta_content_parts = array();
3616 -
3617 - foreach ($custom_meta as $meta_key => $meta_value) {
3618 - // Convert meta key to readable label
3619 - $meta_label = ucwords(str_replace(array('_', '-'), ' ', $meta_key));
3620 - $meta_content_parts[] = $meta_label . ": " . $meta_value;
3621 - }
3622 -
3623 - if (!empty($meta_content_parts)) {
3624 - $content .= "\n\n" . implode("\n", $meta_content_parts);
3625 - }
3626 - }
3627 -
3628 - // Debug logging for WordPress Import content
3629 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Post ID: ' . $post_id . ' Title: ' . $post->post_title);
3630 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Raw post_content length: ' . strlen($post->post_content));
3631 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Final content length: ' . strlen($content));
3632 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Content preview: ' . substr($content, 0, 300));
3633 -
3634 - // Note: Removed 10,000 char limit - chunking now handles large content properly
3635 -
3636 - // Get bot-specific API key
3637 - $bot_options = $this->get_bot_options($bot_id);
3638 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
3639 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3640 -
3641 - if (strpos($selected_model, 'voyage') === 0) {
3642 - $api_key = $options['voyage_api_key'] ?? '';
3643 - $provider_name = 'Voyage AI';
3644 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3645 - $api_key = $options['gemini_api_key'] ?? '';
3646 - $provider_name = 'Google Gemini';
3647 - } else {
3648 - $api_key = $options['api_key'] ?? '';
3649 - $provider_name = 'OpenAI';
3650 - }
3651 -
3652 - if (empty($api_key)) {
3653 - MxChat_Admin::mxchat_log_debug('api_error', $provider_name . ' API key not configured for knowledge processing');
3654 - wp_send_json_error($provider_name . ' API key not configured');
3655 - exit;
3656 - }
3657 -
3658 - $source_url = get_permalink($post_id);
3659 - $vector_id = md5($source_url); // Vector ID for Pinecone
3660 -
3661 - // Check for existing content in bot-specific storage
3662 - $is_update = false;
3663 -
3664 - // Get bot-specific Pinecone configuration
3665 - $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
3666 - $use_pinecone = !empty($bot_pinecone_config) && ($bot_pinecone_config['use_pinecone'] ?? false);
3667 -
3668 - if ($use_pinecone && !empty($bot_pinecone_config['api_key'])) {
3669 - // Check Pinecone for this bot
3670 - $pinecone_data = $this->mxchat_get_pinecone_processed_content($bot_pinecone_config);
3671 - if (isset($pinecone_data[$post_id])) {
3672 - $is_update = true;
3673 - }
3674 - } else {
3675 - // Check WordPress DB (same as before since it's shared)
3676 - global $wpdb;
3677 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3678 - $existing_record = $wpdb->get_row($wpdb->prepare(
3679 - "SELECT id FROM $table_name WHERE source_url = %s",
3680 - $source_url
3681 - ));
3682 -
3683 - if ($existing_record) {
3684 - $is_update = true;
3685 - }
3686 - }
3687 -
3688 - // UPDATED 2.5.6: Determine content type based on post_type
3689 - $post_type = $post->post_type;
3690 - $content_type = 'content'; // Default fallback
3691 -
3692 - // Map WordPress post types to content types
3693 - switch ($post_type) {
3694 - case 'post':
3695 - $content_type = 'post';
3696 - break;
3697 - case 'page':
3698 - $content_type = 'page';
3699 - break;
3700 - case 'product':
3701 - $content_type = 'product';
3702 - break;
3703 - default:
3704 - // For custom post types, use the post type name
3705 - $content_type = sanitize_key($post_type);
3706 - break;
3707 - }
3708 -
3709 - // Use the centralized utility function with bot_id and content_type
3710 - $result = MxChat_Utils::submit_content_to_db(
3711 - $content,
3712 - $source_url,
3713 - $api_key,
3714 - $vector_id,
3715 - $bot_id,
3716 - $content_type
3717 - );
3718 -
3719 - if (is_wp_error($result)) {
3720 - MxChat_Admin::mxchat_log_debug('storage_error', 'Knowledge storage failed: ' . $result->get_error_message(), array('source_url' => $source_url));
3721 - wp_send_json_error('Storage failed: ' . $result->get_error_message());
3722 - exit;
3723 - }
3724 -
3725 - // Automatically apply role restriction based on tags
3726 - $this->apply_role_restriction_to_post($post_id, $source_url);
3727 -
3728 - $operation_type = $is_update ? 'update' : 'new';
3729 -
3730 - // Count ACF fields for debugging
3731 - $acf_field_count = count($acf_fields);
3732 -
3733 - // Success response with minimal data
3734 - wp_send_json_success(array(
3735 - 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully',
3736 - 'post_id' => $post_id,
3737 - 'title' => $post->post_title,
3738 - 'operation_type' => $operation_type,
3739 - 'vector_id' => $vector_id,
3740 - 'acf_fields_found' => $acf_field_count,
3741 - 'content_preview' => substr($content, 0, 100) . '...',
3742 - 'bot_id' => $bot_id
3743 - ));
3744 - exit;
3745 -}
3746 -
3747 -private function apply_role_restriction_to_post($post_id, $source_url) {
3748 - // Get tag-role mappings
3749 - $mappings = get_option('mxchat_tag_role_mappings', array());
3750 -
3751 - if (empty($mappings)) {
3752 - return; // No mappings, leave as public
3753 - }
3754 -
3755 - // Get all tags for the post
3756 - $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
3757 -
3758 - if (empty($post_tags)) {
3759 - return; // No tags, leave as public
3760 - }
3761 -
3762 - // Determine the highest role restriction based on tags
3763 - $highest_role = 'public';
3764 - $role_hierarchy = array(
3765 - 'public' => 0,
3766 - 'logged_in' => 1,
3767 - 'subscriber' => 2,
3768 - 'contributor' => 3,
3769 - 'author' => 4,
3770 - 'editor' => 5,
3771 - 'administrator' => 6
3772 - );
3773 -
3774 - foreach ($post_tags as $tag_slug) {
3775 - if (isset($mappings[$tag_slug])) {
3776 - $role = $mappings[$tag_slug];
3777 - if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
3778 - $highest_role = $role;
3779 - }
3780 - }
3781 - }
3782 -
3783 - // If no restricted tags found, return (leave as public)
3784 - if ($highest_role === 'public') {
3785 - return;
3786 - }
3787 -
3788 - // Update the role restriction in the database
3789 - global $wpdb;
3790 -
3791 - // Check if using Pinecone
3792 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3793 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3794 -
3795 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3796 - // Update Pinecone role restriction
3797 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
3798 - $vector_id = md5($source_url);
3799 -
3800 - $wpdb->replace(
3801 - $roles_table,
3802 - array(
3803 - 'vector_id' => $vector_id,
3804 - 'role_restriction' => $highest_role,
3805 - 'updated_at' => current_time('mysql')
3806 - ),
3807 - array('%s', '%s', '%s')
3808 - );
3809 - } else {
3810 - // Update WordPress DB
3811 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3812 -
3813 - $wpdb->update(
3814 - $table_name,
3815 - array('role_restriction' => $highest_role),
3816 - array('source_url' => $source_url),
3817 - array('%s'),
3818 - array('%s')
3819 - );
3820 - }
3821 -}
3822 -
3823 -public function mxchat_get_public_post_types() {
3824 - // Get all public post types
3825 - $post_types = get_post_types(array('public' => true), 'objects');
3826 - $post_type_options = array();
3827 -
3828 - foreach ($post_types as $post_type) {
3829 - $post_type_options[$post_type->name] = $post_type->label;
3830 - }
3831 -
3832 - // Also include common forum/community post types that might not be marked as public
3833 - $additional_types = array(
3834 - 'topic' => 'Forum Topics (bbPress)',
3835 - 'reply' => 'Forum Replies (bbPress)',
3836 - 'forum' => 'Forums (bbPress)',
3837 - 'wpforo_topic' => 'wpForo Topics',
3838 - 'wpforo_post' => 'wpForo Posts'
3839 - );
3840 -
3841 - foreach ($additional_types as $type_name => $type_label) {
3842 - if (post_type_exists($type_name) && !isset($post_type_options[$type_name])) {
3843 - $post_type_options[$type_name] = $type_label;
3844 - }
3845 - }
3846 -
3847 - return $post_type_options;
3848 -}
3849 -
3850 -/**
3851 - * Retrieves processed content from Pinecone API
3852 - */
3853 -public function mxchat_get_pinecone_processed_content($pinecone_options) {
3854 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3855 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3856 -
3857 - if (empty($api_key) || empty($host)) {
3858 - return array();
3859 - }
3860 -
3861 - $pinecone_data = array();
3862 -
3863 - try {
3864 - // Always get fresh data from Pinecone
3865 - $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
3866 -
3867 - // Method 2: Final fallback - try stats endpoint (if available)
3868 - if (empty($pinecone_data)) {
3869 - $stats_url = "https://{$host}/describe_index_stats";
3870 -
3871 - $response = wp_remote_post($stats_url, array(
3872 - 'headers' => array(
3873 - 'Api-Key' => $api_key,
3874 - 'Content-Type' => 'application/json'
3875 - ),
3876 - 'body' => json_encode(array()),
3877 - 'timeout' => 30
3878 - ));
3879 -
3880 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
3881 - $body = wp_remote_retrieve_body($response);
3882 - $stats_data = json_decode($body, true);
3883 - }
3884 - }
3885 -
3886 - } catch (Exception $e) {
3887 - // Log error but return fresh data only
3888 - }
3889 -
3890 - return $pinecone_data;
3891 -}
3892 -public function mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
3893 - //error_log('=== DEBUG: Starting mxchat_fetch_pinecone_vectors_by_ids ===');
3894 -
3895 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3896 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3897 -
3898 - if (empty($api_key) || empty($host) || empty($vector_ids)) {
3899 - //error_log('DEBUG: Missing parameters for fetch by IDs');
3900 - return array();
3901 - }
3902 -
3903 - try {
3904 - $fetch_url = "https://{$host}/vectors/fetch";
3905 - //error_log('DEBUG: Fetch URL: ' . $fetch_url);
3906 - //error_log('DEBUG: Fetching ' . count($vector_ids) . ' vector IDs');
3907 -
3908 - // Pinecone fetch API allows fetching specific vectors by ID
3909 - $fetch_data = array(
3910 - 'ids' => array_values($vector_ids)
3911 - );
3912 -
3913 - $response = wp_remote_post($fetch_url, array(
3914 - 'headers' => array(
3915 - 'Api-Key' => $api_key,
3916 - 'Content-Type' => 'application/json'
3917 - ),
3918 - 'body' => json_encode($fetch_data),
3919 - 'timeout' => 30
3920 - ));
3921 -
3922 - if (is_wp_error($response)) {
3923 - //error_log('DEBUG: Fetch by IDs WP error: ' . $response->get_error_message());
3924 - return array();
3925 - }
3926 -
3927 - $response_code = wp_remote_retrieve_response_code($response);
3928 - //error_log('DEBUG: Fetch response code: ' . $response_code);
3929 -
3930 - if ($response_code !== 200) {
3931 - $error_body = wp_remote_retrieve_body($response);
3932 - //error_log('DEBUG: Fetch failed with body: ' . $error_body);
3933 - return array();
3934 - }
3935 -
3936 - $body = wp_remote_retrieve_body($response);
3937 - $data = json_decode($body, true);
3938 -
3939 - //error_log('DEBUG: Fetch response structure: ' . print_r(array_keys($data), true));
3940 -
3941 - if (!isset($data['vectors'])) {
3942 - //error_log('DEBUG: No vectors key in response');
3943 - return array();
3944 - }
3945 -
3946 - $processed_data = array();
3947 -
3948 - foreach ($data['vectors'] as $vector_id => $vector_data) {
3949 - $metadata = $vector_data['metadata'] ?? array();
3950 - $source_url = $metadata['source_url'] ?? '';
3951 -
3952 - if (!empty($source_url)) {
3953 - $post_id = url_to_postid($source_url);
3954 - if ($post_id) {
3955 - $created_at = $metadata['created_at'] ?? '';
3956 - $processed_date = 'Recently';
3957 -
3958 - if (!empty($created_at)) {
3959 - $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
3960 - if ($timestamp) {
3961 - $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
3962 - }
3963 - }
3964 -
3965 - $processed_data[$post_id] = array(
3966 - 'db_id' => $vector_id,
3967 - 'processed_date' => $processed_date,
3968 - 'url' => $source_url,
3969 - 'source' => 'pinecone',
3970 - 'timestamp' => $timestamp ?? current_time('timestamp')
3971 - );
3972 - }
3973 - }
3974 - }
3975 -
3976 - //error_log('DEBUG: Processed ' . count($processed_data) . ' vectors from fetch');
3977 - return $processed_data;
3978 -
3979 - } catch (Exception $e) {
3980 - //error_log('DEBUG: Exception in fetch_pinecone_vectors_by_ids: ' . $e->getMessage());
3981 - return array();
3982 - }
3983 -}
3984 -
3985 -/**
3986 - * Get embedding dimensions based on the selected model.
3987 - */
3988 -private function mxchat_get_embedding_dimensions() {
3989 - $options = get_option('mxchat_options', array());
3990 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3991 -
3992 - $model_dimensions = array(
3993 - 'text-embedding-ada-002' => 1536,
3994 - 'text-embedding-3-small' => 1536,
3995 - 'text-embedding-3-large' => 3072,
3996 - 'voyage-2' => 1024,
3997 - 'voyage-large-2' => 1536,
3998 - 'voyage-3-large' => 2048,
3999 - 'gemini-embedding-001' => 1536,
4000 - );
4001 -
4002 - if (strpos($selected_model, 'voyage-3-large') === 0) {
4003 - $custom_dimensions = $options['voyage_output_dimension'] ?? 2048;
4004 - return intval($custom_dimensions);
4005 - }
4006 -
4007 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4008 - $custom_dimensions = $options['gemini_output_dimension'] ?? 1536;
4009 - return intval($custom_dimensions);
4010 - }
4011 -
4012 - return $model_dimensions[$selected_model] ?? 1536;
4013 -}
4014 -
4015 -/**
4016 - * Scan Pinecone for processed content
4017 - */
4018 -public function mxchat_scan_pinecone_for_processed_content($pinecone_options) {
4019 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
4020 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
4021 -
4022 - if (empty($api_key) || empty($host)) {
4023 - return array();
4024 - }
4025 -
4026 - try {
4027 - // Use multiple random vectors to get better coverage
4028 - $all_matches = array();
4029 - $seen_ids = array();
4030 -
4031 - // Get correct dimensions for the configured embedding model
4032 - $dimensions = $this->mxchat_get_embedding_dimensions();
4033 -
4034 - // Try 3 different random vectors to get better coverage
4035 - for ($i = 0; $i < 3; $i++) {
4036 - $query_url = "https://{$host}/query";
4037 -
4038 - // Generate a random unit vector instead of zeros
4039 - $random_vector = array();
4040 - for ($j = 0; $j < $dimensions; $j++) {
4041 - $random_vector[] = (rand(-1000, 1000) / 1000.0);
4042 - }
4043 -
4044 - // Normalize the vector to unit length
4045 - $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
4046 - if ($magnitude > 0) {
4047 - $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
4048 - }
4049 -
4050 - $query_data = array(
4051 - 'includeMetadata' => true,
4052 - 'includeValues' => false,
4053 - 'topK' => 10000,
4054 - 'vector' => $random_vector
4055 - );
4056 -
4057 - $response = wp_remote_post($query_url, array(
4058 - 'headers' => array(
4059 - 'Api-Key' => $api_key,
4060 - 'Content-Type' => 'application/json'
4061 - ),
4062 - 'body' => json_encode($query_data),
4063 - 'timeout' => 30
4064 - ));
4065 -
4066 - if (is_wp_error($response)) {
4067 - continue;
4068 - }
4069 -
4070 - $response_code = wp_remote_retrieve_response_code($response);
4071 -
4072 - if ($response_code !== 200) {
4073 - continue;
4074 - }
4075 -
4076 - $body = wp_remote_retrieve_body($response);
4077 - $data = json_decode($body, true);
4078 -
4079 - if (isset($data['matches'])) {
4080 - foreach ($data['matches'] as $match) {
4081 - $match_id = $match['id'] ?? '';
4082 - if (!empty($match_id) && !isset($seen_ids[$match_id])) {
4083 - $all_matches[] = $match;
4084 - $seen_ids[$match_id] = true;
4085 - }
4086 - }
4087 - }
4088 - }
4089 -
4090 - // Convert matches to processed data format, grouping by URL to count chunks
4091 - $processed_data = array();
4092 - $url_chunk_counts = array();
4093 -
4094 - foreach ($all_matches as $match) {
4095 - $metadata = $match['metadata'] ?? array();
4096 - $source_url = $metadata['source_url'] ?? '';
4097 - $match_id = $match['id'] ?? '';
4098 -
4099 - if (!empty($source_url) && !empty($match_id)) {
4100 - $post_id = url_to_postid($source_url);
4101 - if ($post_id) {
4102 - // Count chunks per post_id
4103 - if (!isset($url_chunk_counts[$post_id])) {
4104 - $url_chunk_counts[$post_id] = 0;
4105 - }
4106 - $url_chunk_counts[$post_id]++;
4107 -
4108 - $created_at = $metadata['created_at'] ?? '';
4109 - $processed_date = 'Recently';
4110 -
4111 - if (!empty($created_at)) {
4112 - $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
4113 - if ($timestamp) {
4114 - $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
4115 - }
4116 - }
4117 -
4118 - // Only store if not already set, or update with newer timestamp
4119 - if (!isset($processed_data[$post_id]) ||
4120 - ($timestamp ?? 0) > ($processed_data[$post_id]['timestamp'] ?? 0)) {
4121 - $processed_data[$post_id] = array(
4122 - 'db_id' => $match_id,
4123 - 'processed_date' => $processed_date,
4124 - 'url' => $source_url,
4125 - 'source' => 'pinecone',
4126 - 'timestamp' => $timestamp ?? current_time('timestamp')
4127 - );
4128 - }
4129 - }
4130 - }
4131 - }
4132 -
4133 - // Add chunk counts to processed data
4134 - foreach ($url_chunk_counts as $post_id => $chunk_count) {
4135 - if (isset($processed_data[$post_id])) {
4136 - $processed_data[$post_id]['chunk_count'] = $chunk_count;
4137 - }
4138 - }
4139 -
4140 - return $processed_data;
4141 -
4142 - } catch (Exception $e) {
4143 - return array();
4144 - }
4145 -}
4146 -/**
4147 - * Generate embeddings from input text for MXChat with bot support
4148 - */
4149 -private function mxchat_generate_embedding($text, $bot_id = 'default') {
4150 - // Enable detailed logging for debugging
4151 - //error_log('[MXCHAT-EMBED] Starting embedding generation for bot: ' . $bot_id . '. Text length: ' . strlen($text) . ' bytes');
4152 - //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
4153 -
4154 - // Get bot-specific options
4155 - $bot_options = $this->get_bot_options($bot_id);
4156 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
4157 -
4158 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4159 - //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model);
4160 -
4161 - // Determine provider and endpoint
4162 - if (strpos($selected_model, 'voyage') === 0) {
4163 - $api_key = $options['voyage_api_key'] ?? '';
4164 - $endpoint = 'https://api.voyageai.com/v1/embeddings';
4165 - $provider_name = 'Voyage AI';
4166 - //error_log('[MXCHAT-EMBED] Using Voyage AI API for bot ' . $bot_id);
4167 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
4168 - $api_key = $options['gemini_api_key'] ?? '';
4169 - $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
4170 - $provider_name = 'Google Gemini';
4171 - //error_log('[MXCHAT-EMBED] Using Google Gemini API for bot ' . $bot_id);
4172 - } else {
4173 - $api_key = $options['api_key'] ?? '';
4174 - $endpoint = 'https://api.openai.com/v1/embeddings';
4175 - $provider_name = 'OpenAI';
4176 - //error_log('[MXCHAT-EMBED] Using OpenAI API for bot ' . $bot_id);
4177 - }
4178 -
4179 - //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
4180 -
4181 - if (empty($api_key)) {
4182 - $error_message = sprintf('Missing %s API key for bot %s. Please configure your API key in the bot settings.', $provider_name, $bot_id);
4183 - //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
4184 - return $error_message;
4185 - }
4186 -
4187 - // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
4188 - $estimated_tokens = ceil(str_word_count($text) / 0.75);
4189 - //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
4190 -
4191 - if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
4192 - //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
4193 - // Consider truncating text here
4194 - }
4195 -
4196 - // Prepare request body based on provider
4197 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4198 - // Gemini API format
4199 - $request_body = array(
4200 - 'model' => 'models/' . $selected_model,
4201 - 'content' => array(
4202 - 'parts' => array(
4203 - array('text' => $text)
4204 - )
4205 - )
4206 - );
4207 -
4208 - // Set output dimensionality to 1536 for consistency with other models
4209 - $request_body['outputDimensionality'] = 1536;
4210 - } else {
4211 - // OpenAI/Voyage API format
4212 - $request_body = array(
4213 - 'model' => $selected_model,
4214 - 'input' => $text
4215 - );
4216 -
4217 - // Add output_dimension for voyage-3-large model
4218 - if ($selected_model === 'voyage-3-large') {
4219 - $request_body['output_dimension'] = 2048;
4220 - }
4221 - }
4222 -
4223 - //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
4224 -
4225 - // Prepare headers based on provider
4226 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4227 - // Gemini uses API key as query parameter
4228 - $endpoint .= '?key=' . $api_key;
4229 - $headers = array(
4230 - 'Content-Type' => 'application/json'
4231 - );
4232 - } else {
4233 - // OpenAI/Voyage use Bearer token
4234 - $headers = array(
4235 - 'Authorization' => 'Bearer ' . $api_key,
4236 - 'Content-Type' => 'application/json'
4237 - );
4238 - }
4239 -
4240 - // Make API request
4241 - //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
4242 - $response = wp_remote_post($endpoint, array(
4243 - 'body' => wp_json_encode($request_body),
4244 - 'headers' => $headers,
4245 - 'timeout' => 60 // Increased timeout for large inputs
4246 - ));
4247 -
4248 - // Handle wp_remote_post errors
4249 - if (is_wp_error($response)) {
4250 - $error_message = $response->get_error_message();
4251 - //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
4252 - return 'Connection error: ' . $error_message;
4253 - }
4254 -
4255 - // Get and check HTTP response code
4256 - $http_code = wp_remote_retrieve_response_code($response);
4257 - //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
4258 -
4259 - if ($http_code !== 200) {
4260 - $error_body = wp_remote_retrieve_body($response);
4261 - //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
4262 -
4263 - // Try to parse error for more details
4264 - $error_json = json_decode($error_body, true);
4265 - if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
4266 - $error_type = $error_json['error']['type'] ?? 'unknown';
4267 - $error_message = $error_json['error']['message'] ?? 'No message';
4268 - //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
4269 - //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
4270 -
4271 - // Customize error message for common API errors
4272 - if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
4273 - $error_message = sprintf('Invalid %s API key for bot %s. Please check your API key in the bot settings.', $provider_name, $bot_id);
4274 - } elseif ($error_type === 'authentication_error') {
4275 - $error_message = sprintf('%s authentication failed for bot %s. Please verify your API key in the bot settings.', $provider_name, $bot_id);
4276 - }
4277 -
4278 - //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
4279 - return $error_message;
4280 - }
4281 -
4282 - $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding for bot %s", $http_code, $bot_id);
4283 - //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
4284 - return $error_message;
4285 - }
4286 -
4287 - // Parse response body
4288 - $response_body = wp_remote_retrieve_body($response);
4289 - //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
4290 -
4291 - $response_data = json_decode($response_body, true);
4292 -
4293 - if (json_last_error() !== JSON_ERROR_NONE) {
4294 - $error = json_last_error_msg();
4295 - //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
4296 - //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
4297 - return "Failed to parse API response: $error";
4298 - }
4299 -
4300 - // Handle different response formats based on provider
4301 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4302 - // Gemini API response format
4303 - if (isset($response_data['embedding']['values'])) {
4304 - $embedding_dimensions = count($response_data['embedding']['values']);
4305 - //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
4306 -
4307 - // Check if embedding dimensions are as expected (should be 1536)
4308 - if ($embedding_dimensions !== 1536) {
4309 - //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
4310 - }
4311 -
4312 - return $response_data['embedding']['values'];
4313 - } else {
4314 - //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
4315 - //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
4316 -
4317 - if (isset($response_data['error'])) {
4318 - $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
4319 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4320 - return $error_message;
4321 - }
4322 -
4323 - $error_message = "Invalid Gemini API response format: No embedding found";
4324 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4325 - return $error_message;
4326 - }
4327 - } else {
4328 - // OpenAI/Voyage API response format
4329 - if (isset($response_data['data'][0]['embedding'])) {
4330 - $embedding_dimensions = count($response_data['data'][0]['embedding']);
4331 - //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
4332 -
4333 - // Check if embedding dimensions are as expected
4334 - if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
4335 - ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
4336 - //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
4337 - }
4338 -
4339 - return $response_data['data'][0]['embedding'];
4340 - } else {
4341 - //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
4342 - //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
4343 -
4344 - if (isset($response_data['error'])) {
4345 - $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
4346 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4347 - return $error_message;
4348 - }
4349 -
4350 - $error_message = "Invalid API response format: No embedding found";
4351 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4352 - return $error_message;
4353 - }
4354 - }
4355 -}
4356 -
4357 -/**
4358 - * Get bot-specific options for multi-bot functionality
4359 - * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
4360 - */
4361 -private function get_bot_options($bot_id = 'default') {
4362 - //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
4363 -
4364 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
4365 - //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
4366 - return array();
4367 - }
4368 -
4369 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
4370 -
4371 - if (!empty($bot_options)) {
4372 - //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
4373 - if (isset($bot_options['similarity_threshold'])) {
4374 - //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
4375 - }
4376 - }
4377 -
4378 - return is_array($bot_options) ? $bot_options : array();
4379 -}
4380 -
4381 -/**
4382 - * Get bot-specific Pinecone configuration
4383 - * Used in the knowledge retrieval functions
4384 - */
4385 -// Also add debugging to your get_bot_pinecone_config function
4386 -private function get_bot_pinecone_config($bot_id = 'default') {
4387 - //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
4388 -
4389 - // If default bot or multi-bot add-on not active, use default Pinecone config
4390 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
4391 - //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
4392 - $addon_options = get_option('mxchat_pinecone_addon_options', array());
4393 - $config = array(
4394 - 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
4395 - 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
4396 - 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
4397 - 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
4398 - );
4399 - //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
4400 - return $config;
4401 - }
4402 -
4403 - //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
4404 -
4405 - // Hook for multi-bot add-on to provide bot-specific Pinecone config
4406 - $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
4407 -
4408 - if (!empty($bot_pinecone_config)) {
4409 - //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
4410 - //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
4411 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
4412 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
4413 - } else {
4414 - //error_log("MXCHAT DEBUG: Filter returned empty config!");
4415 - }
4416 -
4417 - return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
4418 -}
4419 -
4420 -
4421 -public function mxchat_ajax_dismiss_completed_status() {
4422 - try {
4423 - // Verify the request
4424 - check_ajax_referer('mxchat_status_nonce', 'nonce');
4425 -
4426 - if (!current_user_can('manage_options')) {
4427 - wp_send_json_error('Unauthorized access');
4428 - exit;
4429 - }
4430 -
4431 - $card_type = isset($_POST['card_type']) ? sanitize_text_field($_POST['card_type']) : '';
4432 -
4433 - if ($card_type === 'pdf') {
4434 - // Clear PDF status
4435 - $pdf_url = get_transient('mxchat_last_pdf_url');
4436 - if ($pdf_url) {
4437 - delete_transient('mxchat_pdf_status_' . md5($pdf_url));
4438 - delete_transient('mxchat_last_pdf_url');
4439 - }
4440 - } elseif ($card_type === 'sitemap') {
4441 - // Clear sitemap status
4442 - $sitemap_url = get_transient('mxchat_last_sitemap_url');
4443 - if ($sitemap_url) {
4444 - delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
4445 - delete_transient('mxchat_last_sitemap_url');
4446 - }
4447 - }
4448 -
4449 - wp_send_json_success(array('message' => 'Status dismissed successfully'));
4450 -
4451 - } catch (Exception $e) {
4452 - wp_send_json_error(array('message' => 'Error dismissing status: ' . $e->getMessage()));
4453 - }
4454 -}
4455 -
4456 -/**
4457 - * Render completed status cards on page load
4458 - * This ensures completed processing status persists through page refreshes
4459 - */
4460 -public function mxchat_render_completed_status_cards() {
4461 - $output = '';
4462 -
4463 - // Check for completed PDF status
4464 - $pdf_url = get_transient('mxchat_last_pdf_url');
4465 - if ($pdf_url) {
4466 - $pdf_status = $this->mxchat_get_pdf_processing_status($pdf_url);
4467 - if ($pdf_status && ($pdf_status['status'] === 'complete' || $pdf_status['status'] === 'error')) {
4468 - $output .= $this->mxchat_render_pdf_status_card($pdf_status, $pdf_url);
4469 - }
4470 - }
4471 -
4472 - // Check for completed sitemap status
4473 - $sitemap_url = get_transient('mxchat_last_sitemap_url');
4474 - if ($sitemap_url) {
4475 - $sitemap_status = $this->mxchat_get_sitemap_processing_status($sitemap_url);
4476 - if ($sitemap_status && ($sitemap_status['status'] === 'complete' || $sitemap_status['status'] === 'error')) {
4477 - $output .= $this->mxchat_render_sitemap_status_card($sitemap_status, $sitemap_url);
4478 - }
4479 - }
4480 -
4481 - return $output;
4482 -}
4483 -
4484 -/**
4485 - * Render PDF status card HTML
4486 - */
4487 -private function mxchat_render_pdf_status_card($status, $pdf_url) {
4488 - $html = '<div class="mxchat-status-card" data-card-type="pdf">';
4489 - $html .= '<div class="mxchat-status-header">';
4490 - $html .= '<h4>' . esc_html__('PDF Processing Status', 'mxchat') . '</h4>';
4491 -
4492 - // Add dismiss button for completed status
4493 - if ($status['status'] === 'complete' || $status['status'] === 'error') {
4494 - $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
4495 - }
4496 -
4497 - // Process Batch button for processing status
4498 - if ($status['status'] === 'processing') {
4499 - $html .= '<button type="button" class="mxchat-manual-batch-btn"
4500 - data-process-type="pdf"
4501 - data-url="' . esc_attr($pdf_url) . '">
4502 - ' . esc_html__('Process Batch', 'mxchat') . '</button>';
4503 - }
4504 -
4505 - // Add status badges
4506 - if ($status['status'] === 'error') {
4507 - $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
4508 - } elseif ($status['status'] === 'complete') {
4509 - if ($status['failed_pages'] > 0) {
4510 - $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
4511 - sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_pages']) . '</span>';
4512 - } else {
4513 - $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
4514 - }
4515 - }
4516 -
4517 - $html .= '</div>'; // End header
4518 -
4519 - // Progress bar
4520 - $html .= '<div class="mxchat-progress-bar">';
4521 - $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
4522 - $html .= '</div>';
4523 -
4524 - // Status details
4525 - $html .= '<div class="mxchat-status-details">';
4526 - $html .= '<p>' . sprintf(
4527 - esc_html__('Progress: %d of %d pages (%d%%)', 'mxchat'),
4528 - $status['processed_pages'],
4529 - $status['total_pages'],
4530 - $status['percentage']
4531 - ) . '</p>';
4532 -
4533 - // Show failed pages count if any
4534 - if ($status['failed_pages'] > 0) {
4535 - $html .= '<p><strong>' . esc_html__('Failed pages:', 'mxchat') . '</strong> ' . esc_html($status['failed_pages']) . '</p>';
4536 - }
4537 -
4538 - $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
4539 - $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
4540 -
4541 - // Add completion summary if available AND it's an array
4542 - if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
4543 - $summary = $status['completion_summary'];
4544 - $html .= '<div class="mxchat-completion-summary">';
4545 - $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
4546 - $html .= '<p><strong>' . esc_html__('Total Pages:', 'mxchat') . '</strong> ' . esc_html($summary['total_pages']) . '</p>';
4547 - $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_pages']) . '</p>';
4548 - $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_pages']) . '</p>';
4549 - $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
4550 - $html .= '</div>';
4551 - }
4552 -
4553 - // Add failed pages list if any AND it's an array
4554 - if (isset($status['failed_pages_list']) && is_array($status['failed_pages_list']) && !empty($status['failed_pages_list'])) {
4555 - $html .= $this->mxchat_render_failed_pages_list($status['failed_pages_list']);
4556 - }
4557 -
4558 - // Add error message if any
4559 - if (isset($status['error']) && !empty($status['error'])) {
4560 - $html .= '<div class="mxchat-error-notice">';
4561 - $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
4562 - $html .= '</div>';
4563 - }
4564 -
4565 - $html .= '</div>'; // End details
4566 - $html .= '</div>'; // End card
4567 -
4568 - return $html;
4569 -}
4570 -/**
4571 - * Render sitemap status card HTML
4572 - */
4573 -private function mxchat_render_sitemap_status_card($status, $sitemap_url) {
4574 - $html = '<div class="mxchat-status-card" data-card-type="sitemap">';
4575 - $html .= '<div class="mxchat-status-header">';
4576 - $html .= '<h4>' . esc_html__('Sitemap Processing Status', 'mxchat') . '</h4>';
4577 -
4578 - // Add dismiss button for completed status
4579 - if ($status['status'] === 'complete' || $status['status'] === 'error') {
4580 - $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
4581 - }
4582 -
4583 - // Process Batch button for processing status
4584 - if ($status['status'] === 'processing') {
4585 - $html .= '<button type="button" class="mxchat-manual-batch-btn"
4586 - data-process-type="sitemap"
4587 - data-url="' . esc_attr($sitemap_url) . '">
4588 - ' . esc_html__('Process Batch', 'mxchat') . '</button>';
4589 - }
4590 -
4591 - // Add status badges
4592 - if ($status['status'] === 'error') {
4593 - $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
4594 - } elseif ($status['status'] === 'complete') {
4595 - if ($status['failed_urls'] > 0) {
4596 - $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
4597 - sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_urls']) . '</span>';
4598 - } else {
4599 - $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
4600 - }
4601 - }
4602 -
4603 - $html .= '</div>'; // End header
4604 -
4605 - // Progress bar
4606 - $html .= '<div class="mxchat-progress-bar">';
4607 - $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
4608 - $html .= '</div>';
4609 -
4610 - // Status details
4611 - $html .= '<div class="mxchat-status-details">';
4612 - $html .= '<p>' . sprintf(
4613 - esc_html__('Progress: %d of %d URLs (%d%%)', 'mxchat'),
4614 - $status['processed_urls'],
4615 - $status['total_urls'],
4616 - $status['percentage']
4617 - ) . '</p>';
4618 -
4619 - // Show failed URLs count if any
4620 - if ($status['failed_urls'] > 0) {
4621 - $html .= '<p><strong>' . esc_html__('Failed URLs:', 'mxchat') . '</strong> ' . esc_html($status['failed_urls']) . '</p>';
4622 - }
4623 -
4624 - $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
4625 - $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
4626 -
4627 - // Add completion summary if available AND it's an array
4628 - if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
4629 - $summary = $status['completion_summary'];
4630 - $html .= '<div class="mxchat-completion-summary">';
4631 - $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
4632 - $html .= '<p><strong>' . esc_html__('Total URLs:', 'mxchat') . '</strong> ' . esc_html($summary['total_urls']) . '</p>';
4633 - $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_urls']) . '</p>';
4634 - $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_urls']) . '</p>';
4635 - $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
4636 - $html .= '</div>';
4637 - }
4638 -
4639 - // Add error messages if any (but not the failed URLs list)
4640 - if (!empty($status['error']) || !empty($status['last_error'])) {
4641 - $html .= '<div class="mxchat-error-notice">';
4642 -
4643 - if (!empty($status['error'])) {
4644 - $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
4645 - }
4646 -
4647 - if (!empty($status['last_error'])) {
4648 - $html .= '<p class="last-error">' . esc_html__('Last error:', 'mxchat') . ' ' . esc_html($status['last_error']) . '</p>';
4649 - }
4650 -
4651 - $html .= '</div>';
4652 - }
4653 -
4654 - $html .= '</div>'; // End details
4655 - $html .= '</div>'; // End card
4656 -
4657 - return $html;
4658 -}
4659 -
4660 -
4661 -/**
4662 - * Render failed pages list
4663 - */
4664 -private function mxchat_render_failed_pages_list($failed_pages_list) {
4665 - // Validate that $failed_pages_list is an array and not empty
4666 - if (!is_array($failed_pages_list) || empty($failed_pages_list)) {
4667 - return '';
4668 - }
4669 -
4670 - $html = '<div class="mxchat-error-notice">';
4671 - $html .= '<div class="mxchat-failed-pages-container">';
4672 - $html .= '<h5>' . sprintf(esc_html__('Failed Pages (%d)', 'mxchat'), count($failed_pages_list)) . '</h5>';
4673 - $html .= '<details>';
4674 - $html .= '<summary>' . esc_html__('Show Failed Pages', 'mxchat') . '</summary>';
4675 - $html .= '<div class="mxchat-failed-pages-list">';
4676 -
4677 - // Create table for failed pages
4678 - $html .= '<table class="widefat striped">';
4679 - $html .= '<thead><tr>';
4680 - $html .= '<th>' . esc_html__('Page', 'mxchat') . '</th>';
4681 - $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
4682 - $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
4683 - $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
4684 - $html .= '</tr></thead><tbody>';
4685 -
4686 - // Sort failed pages by most recent
4687 - $sorted_failed_pages = $failed_pages_list;
4688 - usort($sorted_failed_pages, function($a, $b) {
4689 - return ($b['time'] ?? 0) - ($a['time'] ?? 0);
4690 - });
4691 -
4692 - foreach ($sorted_failed_pages as $item) {
4693 - // Ensure $item is an array before accessing its elements
4694 - if (!is_array($item)) {
4695 - continue;
4696 - }
4697 -
4698 - $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
4699 - $html .= '<tr>';
4700 - $html .= '<td>' . esc_html__('Page', 'mxchat') . ' ' . esc_html($item['page'] ?? 'Unknown') . '</td>';
4701 - $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
4702 - $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
4703 - $html .= '<td>' . esc_html($time_ago) . '</td>';
4704 - $html .= '</tr>';
4705 - }
4706 -
4707 - $html .= '</tbody></table>';
4708 - $html .= '</div></details></div></div>';
4709 -
4710 - return $html;
4711 -}
4712 -
4713 -/**
4714 - * Render failed URLs list
4715 - */
4716 -private function mxchat_render_failed_urls_list($failed_urls_list) {
4717 - // Validate that $failed_urls_list is an array and not empty
4718 - if (!is_array($failed_urls_list) || empty($failed_urls_list)) {
4719 - return '';
4720 - }
4721 -
4722 - $html = '<div class="mxchat-failed-urls-container">';
4723 - $html .= '<h5>' . sprintf(esc_html__('Failed URLs (%d)', 'mxchat'), count($failed_urls_list)) . '</h5>';
4724 - $html .= '<details>';
4725 - $html .= '<summary>' . esc_html__('Show Failed URLs', 'mxchat') . '</summary>';
4726 - $html .= '<div class="mxchat-failed-urls-list">';
4727 -
4728 - // Create table for failed URLs
4729 - $html .= '<table class="widefat striped">';
4730 - $html .= '<thead><tr>';
4731 - $html .= '<th>' . esc_html__('URL', 'mxchat') . '</th>';
4732 - $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
4733 - $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
4734 - $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
4735 - $html .= '</tr></thead><tbody>';
4736 -
4737 - // Sort failed URLs by most recent
4738 - $sorted_failed_urls = $failed_urls_list;
4739 - usort($sorted_failed_urls, function($a, $b) {
4740 - return ($b['time'] ?? 0) - ($a['time'] ?? 0);
4741 - });
4742 -
4743 - // Show up to 50 failed URLs
4744 - $display_urls = array_slice($sorted_failed_urls, 0, 50);
4745 -
4746 - foreach ($display_urls as $item) {
4747 - // Ensure $item is an array before accessing its elements
4748 - if (!is_array($item)) {
4749 - continue;
4750 - }
4751 -
4752 - $url = $item['url'] ?? '';
4753 - $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
4754 -
4755 - // Truncate URL for display
4756 - $display_url = strlen($url) > 50 ? substr($url, 0, 47) . '...' : $url;
4757 -
4758 - $html .= '<tr>';
4759 - $html .= '<td style="word-break: break-all;">';
4760 - if (!empty($url)) {
4761 - $html .= '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($display_url) . '</a>';
4762 - } else {
4763 - $html .= esc_html__('Unknown URL', 'mxchat');
4764 - }
4765 - $html .= '</td>';
4766 - $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
4767 - $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
4768 - $html .= '<td>' . esc_html($time_ago) . '</td>';
4769 - $html .= '</tr>';
4770 - }
4771 -
4772 - $html .= '</tbody></table>';
4773 -
4774 - if (count($failed_urls_list) > 50) {
4775 - $html .= '<div class="mxchat-failed-urls-more">+ ' .
4776 - (count($failed_urls_list) - 50) .
4777 - ' ' . esc_html__('more failed URLs not shown', 'mxchat') . '</div>';
4778 - }
4779 -
4780 - $html .= '</div></details></div>';
4781 -
4782 - return $html;
4783 -}
4784 -
4785 -/**
4786 - * Get all ACF fields for a specific post, excluding any fields the user has disabled
4787 - */
4788 -public function mxchat_get_acf_fields_for_post($post_id) {
4789 - if (!function_exists('get_fields')) {
4790 - return array();
4791 - }
4792 -
4793 - $fields = get_fields($post_id);
4794 - if (!$fields || !is_array($fields)) {
4795 - return array();
4796 - }
4797 -
4798 - // Get excluded fields from settings
4799 - $excluded_fields = get_option('mxchat_acf_excluded_fields', array());
4800 - if (!empty($excluded_fields) && is_array($excluded_fields)) {
4801 - foreach ($excluded_fields as $excluded_field) {
4802 - if (isset($fields[$excluded_field])) {
4803 - unset($fields[$excluded_field]);
4804 - }
4805 - }
4806 - }
4807 -
4808 - return $fields;
4809 -}
4810 -
4811 -/**
4812 - * Get all registered ACF field groups and their fields for the settings UI
4813 - */
4814 -public function mxchat_get_all_acf_fields() {
4815 - if (!function_exists('acf_get_field_groups') || !function_exists('acf_get_fields')) {
4816 - return array();
4817 - }
4818 -
4819 - $all_fields = array();
4820 - $field_groups = acf_get_field_groups();
4821 -
4822 - if (!empty($field_groups)) {
4823 - foreach ($field_groups as $group) {
4824 - $group_fields = acf_get_fields($group['key']);
4825 - if (!empty($group_fields)) {
4826 - $all_fields[$group['title']] = array();
4827 - foreach ($group_fields as $field) {
4828 - $all_fields[$group['title']][] = array(
4829 - 'name' => $field['name'],
4830 - 'label' => $field['label'],
4831 - 'type' => $field['type']
4832 - );
4833 - }
4834 - }
4835 - }
4836 - }
4837 -
4838 - return $all_fields;
4839 -}
4840 -
4841 -/**
4842 - * Get whitelisted custom post meta for a given post
4843 - * This allows non-ACF meta fields (like OptionTree, theme meta boxes) to be included in embeddings
4844 - */
4845 -public function mxchat_get_whitelisted_post_meta($post_id) {
4846 - $whitelist = get_option('mxchat_custom_meta_whitelist', '');
4847 -
4848 - if (empty($whitelist)) {
4849 - return array();
4850 - }
4851 -
4852 - // Parse the whitelist - one meta key per line
4853 - $meta_keys = array_filter(array_map('trim', explode("\n", $whitelist)));
4854 -
4855 - if (empty($meta_keys)) {
4856 - return array();
4857 - }
4858 -
4859 - $result = array();
4860 -
4861 - foreach ($meta_keys as $key) {
4862 - // Skip empty keys
4863 - if (empty($key)) {
4864 - continue;
4865 - }
4866 -
4867 - $value = get_post_meta($post_id, $key, true);
4868 -
4869 - // Only include non-empty string values
4870 - if (!empty($value) && is_string($value)) {
4871 - $result[$key] = $value;
4872 - } elseif (!empty($value) && is_array($value)) {
4873 - // Handle array values by joining them
4874 - $flat_value = $this->mxchat_flatten_meta_array($value);
4875 - if (!empty($flat_value)) {
4876 - $result[$key] = $flat_value;
4877 - }
4878 - }
4879 - }
4880 -
4881 - return $result;
4882 -}
4883 -
4884 -/**
4885 - * Flatten array meta values into a readable string
4886 - */
4887 -private function mxchat_flatten_meta_array($array, $depth = 0) {
4888 - if ($depth > 3) {
4889 - return ''; // Prevent infinite recursion
4890 - }
4891 -
4892 - $parts = array();
4893 -
4894 - foreach ($array as $key => $value) {
4895 - if (is_string($value) && !empty($value)) {
4896 - $parts[] = $value;
4897 - } elseif (is_array($value)) {
4898 - $nested = $this->mxchat_flatten_meta_array($value, $depth + 1);
4899 - if (!empty($nested)) {
4900 - $parts[] = $nested;
4901 - }
4902 - }
4903 - }
4904 -
4905 - return implode(', ', $parts);
4906 -}
4907 -
4908 -/**
4909 - * Format ACF field values for content extraction
4910 - */
4911 -public function mxchat_format_acf_field_value($value, $field_name = '', $post_id = 0) {
4912 - if (empty($value)) {
4913 - return '';
4914 - }
4915 -
4916 - // Handle WP_Post objects first (THIS IS THE KEY FIX)
4917 - if ($value instanceof WP_Post) {
4918 - return $value->post_title ?: '';
4919 - }
4920 -
4921 - // Handle other WP objects
4922 - if (is_object($value)) {
4923 - if (isset($value->post_title)) {
4924 - return $value->post_title;
4925 - } elseif (isset($value->display_name)) {
4926 - return $value->display_name;
4927 - } elseif (isset($value->name)) {
4928 - return $value->name;
4929 - } elseif (method_exists($value, '__toString')) {
4930 - try {
4931 - return (string) $value;
4932 - } catch (Exception $e) {
4933 - return '';
4934 - }
4935 - }
4936 - // For any other objects, return empty string
4937 - return '';
4938 - }
4939 -
4940 - // Handle different ACF field types
4941 - if (is_array($value)) {
4942 - // Check if it's an image/file field
4943 - if (isset($value['url'])) {
4944 - // Image field - return alt text, title, or caption
4945 - if (!empty($value['alt'])) {
4946 - return $value['alt'];
4947 - } elseif (!empty($value['title'])) {
4948 - return $value['title'];
4949 - } elseif (!empty($value['caption'])) {
4950 - return $value['caption'];
4951 - } else {
4952 - return ''; // Don't include just the URL
4953 - }
4954 - }
4955 -
4956 - // Check if it's a post object or relationship field
4957 - if (isset($value['post_title'])) {
4958 - return $value['post_title'];
4959 - }
4960 -
4961 - // Check if it's a user field
4962 - if (isset($value['display_name'])) {
4963 - return $value['display_name'];
4964 - }
4965 -
4966 - // Check if it's a taxonomy term
4967 - if (isset($value['name']) && isset($value['taxonomy'])) {
4968 - return $value['name'];
4969 - }
4970 -
4971 - // Check if it's a select field with label
4972 - if (isset($value['label'])) {
4973 - return $value['label'];
4974 - }
4975 -
4976 - // Check for repeater field or flexible content
4977 - if (is_numeric(key($value))) {
4978 - $sub_values = array();
4979 - foreach ($value as $sub_item) {
4980 - if (is_array($sub_item)) {
4981 - // For repeater/flexible content, extract text values
4982 - $sub_text = $this->mxchat_extract_text_from_acf_array($sub_item);
4983 - if (!empty($sub_text)) {
4984 - $sub_values[] = $sub_text;
4985 - }
4986 - } elseif ($sub_item instanceof WP_Post) {
4987 - // Handle WP_Post objects in arrays
4988 - $sub_values[] = $sub_item->post_title ?: '';
4989 - } else {
4990 - $sub_values[] = (string) $sub_item;
4991 - }
4992 - }
4993 - return implode(', ', array_filter($sub_values));
4994 - }
4995 -
4996 - // For other arrays, try to extract meaningful text
4997 - $text_values = array();
4998 - foreach ($value as $key => $val) {
4999 - if (is_string($val) && !empty(trim($val))) {
5000 - $text_values[] = trim($val);
5001 - } elseif ($val instanceof WP_Post) {
5002 - // Handle WP_Post objects in associative arrays
5003 - $text_values[] = $val->post_title ?: '';
5004 - } elseif (is_array($val) && isset($val['post_title'])) {
5005 - $text_values[] = $val['post_title'];
5006 - } elseif (is_array($val) && isset($val['name'])) {
5007 - $text_values[] = $val['name'];
5008 - }
5009 - }
5010 -
5011 - return implode(', ', array_filter($text_values));
5012 - }
5013 -
5014 - // Handle boolean values
5015 - if (is_bool($value)) {
5016 - return $value ? 'Yes' : 'No';
5017 - }
5018 -
5019 - // Handle numeric values
5020 - if (is_numeric($value)) {
5021 - return (string) $value;
5022 - }
5023 -
5024 - // Handle string values
5025 - if (is_string($value)) {
5026 - return trim($value);
5027 - }
5028 -
5029 - // For anything else that we can't handle, return empty string
5030 - // This prevents the "Object could not be converted to string" error
5031 - return '';
5032 -}
5033 -
5034 -/**
5035 - * Extract text from complex ACF array structures
5036 - */
5037 -private function mxchat_extract_text_from_acf_array($array) {
5038 - if (!is_array($array)) {
5039 - return '';
5040 - }
5041 -
5042 - $text_parts = array();
5043 -
5044 - foreach ($array as $key => $value) {
5045 - if (is_string($value) && !empty(trim($value))) {
5046 - // Skip keys that are likely to be IDs or technical values
5047 - if (!is_numeric($value) || strlen($value) > 10) {
5048 - $text_parts[] = trim($value);
5049 - }
5050 - } elseif ($value instanceof WP_Post) {
5051 - // Handle WP_Post objects
5052 - $text_parts[] = $value->post_title ?: '';
5053 - } elseif (is_array($value)) {
5054 - if (isset($value['post_title'])) {
5055 - $text_parts[] = $value['post_title'];
5056 - } elseif (isset($value['name'])) {
5057 - $text_parts[] = $value['name'];
5058 - } elseif (isset($value['label'])) {
5059 - $text_parts[] = $value['label'];
5060 - }
5061 - } elseif (is_object($value)) {
5062 - // Handle other objects safely
5063 - if (isset($value->post_title)) {
5064 - $text_parts[] = $value->post_title;
5065 - } elseif (isset($value->name)) {
5066 - $text_parts[] = $value->name;
5067 - } elseif (isset($value->display_name)) {
5068 - $text_parts[] = $value->display_name;
5069 - }
5070 - }
5071 - }
5072 -
5073 - return implode(', ', array_filter($text_parts));
5074 -}
5075 -
5076 -/**
5077 - * Handle ACF save - fires after ACF fields are saved
5078 - * This ensures ACF field data is available when syncing to knowledge base
5079 - */
5080 -public function mxchat_handle_acf_save($post_id) {
5081 - // Skip if not a valid post
5082 - if (!$post_id || $post_id === 'options') {
5083 - return;
5084 - }
5085 -
5086 - // Skip autosaves and revisions
5087 - if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
5088 - return;
5089 - }
5090 -
5091 - $post = get_post($post_id);
5092 - if (!$post) {
5093 - return;
5094 - }
5095 -
5096 - $post_type = $post->post_type;
5097 -
5098 - // Check if sync is enabled for this post type
5099 - $should_sync = false;
5100 -
5101 - if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
5102 - $should_sync = true;
5103 - } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
5104 - $should_sync = true;
5105 - } else if ($post_type === 'product' && class_exists('WooCommerce')) {
5106 - // WooCommerce products - check if WooCommerce integration is enabled
5107 - $options = get_option('mxchat_options', array());
5108 - if (isset($options['enable_woocommerce_integration']) &&
5109 - ($options['enable_woocommerce_integration'] === '1' || $options['enable_woocommerce_integration'] === 'on')) {
5110 - $should_sync = true;
5111 - }
5112 - } else {
5113 - // Check custom post types
5114 - $option_name = 'mxchat_auto_sync_' . $post_type;
5115 - if (get_option($option_name) === '1') {
5116 - $should_sync = true;
5117 - }
5118 - }
5119 -
5120 - if (!$should_sync) {
5121 - return;
5122 - }
5123 -
5124 - // Only process published posts
5125 - if ($post->post_status !== 'publish') {
5126 - return;
5127 - }
5128 -
5129 - // Check if this post has any ACF fields - if not, no need to re-sync
5130 - $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
5131 - if (empty($acf_fields)) {
5132 - return;
5133 - }
5134 -
5135 - // Use a transient to prevent duplicate processing (post_updated may have already run)
5136 - $transient_key = 'mxchat_acf_synced_' . $post_id;
5137 - if (get_transient($transient_key)) {
5138 - return;
5139 - }
5140 - set_transient($transient_key, true, 60); // Prevent re-processing for 60 seconds
5141 -
5142 - // Re-run the sync with ACF data now available
5143 - // We pass $update=true since this is effectively an update with ACF data
5144 - $this->mxchat_handle_post_update($post_id, $post, true);
5145 -}
5146 -
5147 -public function mxchat_handle_post_update($post_id, $post, $update) {
5148 - // Basic validation checks
5149 - if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
5150 - return;
5151 - }
5152 -
5153 - $post_type = $post->post_type;
5154 -
5155 - // Check if sync is enabled for this post type
5156 - $should_sync = false;
5157 -
5158 - // Check built-in post types first
5159 - if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
5160 - $should_sync = true;
5161 - } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
5162 - $should_sync = true;
5163 - } else {
5164 - // Check custom post types
5165 - $option_name = 'mxchat_auto_sync_' . $post_type;
5166 - if (get_option($option_name) === '1') {
5167 - $should_sync = true;
5168 - }
5169 - }
5170 -
5171 - if (!$should_sync) {
5172 - return;
5173 - }
5174 -
5175 - // Check if we have stored the previous status and URL in our transients
5176 - $previous_status_key = 'mxchat_prev_status_' . $post_id;
5177 - $previous_status = get_transient($previous_status_key);
5178 -
5179 - $previous_url_key = 'mxchat_prev_url_' . $post_id;
5180 - $previous_url = get_transient($previous_url_key);
5181 -
5182 - // If the post was previously published but is now not published, remove from knowledge base
5183 - if ($previous_status === 'publish' && $post->post_status !== 'publish') {
5184 - // Use the stored URL from when it was published, or fall back to current permalink
5185 - $source_url = $previous_url ?: get_permalink($post_id);
5186 -
5187 - if ($source_url) {
5188 - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks)
5189 - MxChat_Utils::delete_chunks_for_url($source_url, 'default');
5190 - }
5191 -
5192 - // Clean up the transients and exit early
5193 - delete_transient($previous_status_key);
5194 - delete_transient($previous_url_key);
5195 - return;
5196 - }
5197 -
5198 - // Slug/permalink rename while still published: delete the old vectors before upserting new ones.
5199 - // Without this, md5(old_url) vectors (base + chunks) would be orphaned under the stale URL.
5200 - if ($post->post_status === 'publish' && !empty($previous_url)) {
5201 - $current_url = get_permalink($post_id);
5202 - if ($current_url && $current_url !== $previous_url) {
5203 - MxChat_Utils::delete_chunks_for_url($previous_url, 'default');
5204 - }
5205 - }
5206 -
5207 - // Store the current status for next time (if this is an update)
5208 - if ($update) {
5209 - set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
5210 -
5211 - // If the post is currently published, also store its URL
5212 - if ($post->post_status === 'publish') {
5213 - $current_url = get_permalink($post_id);
5214 - set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
5215 - }
5216 - }
5217 -
5218 - // Only process currently published content for adding/updating
5219 - if ($post->post_status === 'publish') {
5220 - // Get the source URL
5221 - $source_url = get_permalink($post_id);
5222 -
5223 - // Get content with proper formatting (matching ajax_mxchat_process_selected_content)
5224 - $title = get_the_title($post_id);
5225 - $content = get_post_field('post_content', $post_id);
5226 - $excerpt = get_post_field('post_excerpt', $post_id);
5227 -
5228 - // Remove shortcode tags but preserve content inside them
5229 - $content = $this->strip_shortcode_tags_preserve_content($content);
5230 - $excerpt = $this->strip_shortcode_tags_preserve_content($excerpt);
5231 -
5232 - // Strip tags but preserve structure (don't use 'the_content' filter as it may re-add shortcodes)
5233 - $content = wp_strip_all_tags($content);
5234 -
5235 - // Combine title, short description (if exists), and content
5236 - $final_content = $title . "\n\n";
5237 -
5238 - // Add short description if it exists (WooCommerce products use post_excerpt for short description)
5239 - if (!empty($excerpt)) {
5240 - $final_content .= "Short Description: " . wp_strip_all_tags($excerpt) . "\n\n";
5241 - }
5242 -
5243 - $final_content .= $content;
5244 -
5245 - // For WooCommerce products, include pricing and product details
5246 - if ($post_type === 'product' && class_exists('WooCommerce')) {
5247 - $product = wc_get_product($post_id);
5248 -
5249 - if ($product) {
5250 - // Get pricing information
5251 - $regular_price = $product->get_regular_price();
5252 - $sale_price = $product->get_sale_price();
5253 - $price = $product->get_price();
5254 - $sku = $product->get_sku();
5255 -
5256 - // Get currency symbol
5257 - $currency_symbol = get_woocommerce_currency_symbol();
5258 -
5259 - // Add pricing information
5260 - $final_content .= "\n";
5261 - if (!empty($regular_price)) {
5262 - $final_content .= "Price: " . $currency_symbol . $regular_price . "\n";
5263 - } elseif (!empty($price)) {
5264 - $final_content .= "Price: " . $currency_symbol . $price . "\n";
5265 - }
5266 -
5267 - if (!empty($sale_price) && $sale_price !== $regular_price) {
5268 - $final_content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
5269 - }
5270 -
5271 - // Handle variable products - show price range
5272 - if ($product->is_type('variable')) {
5273 - $min_price = $product->get_variation_price('min');
5274 - $max_price = $product->get_variation_price('max');
5275 - if ($min_price !== $max_price) {
5276 - $final_content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
5277 - }
5278 - }
5279 -
5280 - if (!empty($sku)) {
5281 - $final_content .= "SKU: " . $sku . "\n";
5282 - }
5283 -
5284 - // Get product categories
5285 - $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
5286 - if (!empty($categories) && !is_wp_error($categories)) {
5287 - $final_content .= "Categories: " . implode(', ', $categories) . "\n";
5288 - }
5289 - }
5290 - }
5291 -
5292 - // For custom post types like job_listing, include additional fields
5293 - if ($post_type === 'job_listing') {
5294 - // Add job-specific meta if available
5295 - $job_location = get_post_meta($post_id, '_job_location', true);
5296 - if (!empty($job_location)) {
5297 - $final_content .= "\n\nLocation: " . $job_location;
5298 - }
5299 -
5300 - // Get job type terms
5301 - $job_types = get_the_terms($post_id, 'job_listing_type');
5302 - if (!empty($job_types) && !is_wp_error($job_types)) {
5303 - $types = array();
5304 - foreach ($job_types as $type) {
5305 - $types[] = $type->name;
5306 - }
5307 - $final_content .= "\n\nJob Type: " . implode(', ', $types);
5308 - }
5309 -
5310 - // Get company name if available
5311 - $company_name = get_post_meta($post_id, '_company_name', true);
5312 - if (!empty($company_name)) {
5313 - $final_content .= "\n\nCompany: " . $company_name;
5314 - }
5315 - }
5316 -
5317 - // ADD ACF FIELDS SUPPORT (matches ajax_mxchat_process_selected_content behavior)
5318 - $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
5319 - if (!empty($acf_fields)) {
5320 - $acf_content_parts = array();
5321 -
5322 - foreach ($acf_fields as $field_name => $field_value) {
5323 - $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
5324 - if (!empty($formatted_value)) {
5325 - // Convert field name to readable label
5326 - $field_label = ucwords(str_replace(['_', '-'], ' ', $field_name));
5327 - $acf_content_parts[] = $field_label . ": " . $formatted_value;
5328 - }
5329 - }
5330 -
5331 - if (!empty($acf_content_parts)) {
5332 - $final_content .= "\n\n" . implode("\n", $acf_content_parts);
5333 - }
5334 - }
5335 -
5336 - // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
5337 - $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
5338 - if (!empty($custom_meta)) {
5339 - $meta_content_parts = array();
5340 -
5341 - foreach ($custom_meta as $meta_key => $meta_value) {
5342 - // Convert meta key to readable label
5343 - $meta_label = ucwords(str_replace(array('_', '-'), ' ', $meta_key));
5344 - $meta_content_parts[] = $meta_label . ": " . $meta_value;
5345 - }
5346 -
5347 - if (!empty($meta_content_parts)) {
5348 - $final_content .= "\n\n" . implode("\n", $meta_content_parts);
5349 - }
5350 - }
5351 -
5352 - // Get API key with proper model detection
5353 - $options = get_option('mxchat_options');
5354 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
5355 -
5356 - if (strpos($selected_model, 'voyage') === 0) {
5357 - $api_key = $options['voyage_api_key'] ?? '';
5358 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
5359 - $api_key = $options['gemini_api_key'] ?? '';
5360 - } else {
5361 - $api_key = $options['api_key'] ?? '';
5362 - }
5363 -
5364 - if (empty($api_key)) {
5365 - return;
5366 - }
5367 -
5368 - // Use the centralized utility function for storage
5369 - $result = MxChat_Utils::submit_content_to_db(
5370 - $final_content,
5371 - $source_url,
5372 - $api_key,
5373 - md5($source_url) // Vector ID for Pinecone
5374 - );
5375 -
5376 - // After successful storage, apply role restriction based on tags
5377 - if (!is_wp_error($result)) {
5378 - $this->apply_role_restriction_to_post($post_id, $source_url);
5379 - }
5380 - }
5381 -
5382 - // Clean up the stored previous status if not used above
5383 - if ($previous_status !== 'publish' || $post->post_status === 'publish') {
5384 - delete_transient($previous_status_key);
5385 - delete_transient($previous_url_key);
5386 - }
5387 -}
5388 -
5389 -/**
5390 - * Store the post status and URL before update to detect status transitions
5391 - * This runs before the post is actually updated in the database
5392 - */
5393 -public function mxchat_store_pre_update_status($post_id, $data) {
5394 - // Get the current post from database (before update)
5395 - $current_post = get_post($post_id);
5396 -
5397 - if ($current_post) {
5398 - // Store the current status temporarily
5399 - $status_key = 'mxchat_prev_status_' . $post_id;
5400 - set_transient($status_key, $current_post->post_status, HOUR_IN_SECONDS);
5401 -
5402 - // If the post is currently published, also store its URL
5403 - if ($current_post->post_status === 'publish') {
5404 - $url_key = 'mxchat_prev_url_' . $post_id;
5405 - $current_url = get_permalink($post_id);
5406 - set_transient($url_key, $current_url, HOUR_IN_SECONDS);
5407 - }
5408 - }
5409 -}
5410 -
5411 -public function mxchat_handle_post_delete($post_id) {
5412 - // Get post data before it's deleted
5413 - $post = get_post($post_id);
5414 -
5415 - // Basic validation
5416 - if (!$post || wp_is_post_revision($post_id)) {
5417 - return;
5418 - }
5419 -
5420 - $post_type = $post->post_type;
5421 -
5422 - // Check if sync is enabled for this post type
5423 - $should_sync = false;
5424 -
5425 - // Check built-in post types first
5426 - if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
5427 - $should_sync = true;
5428 - } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
5429 - $should_sync = true;
5430 - } else {
5431 - // Check custom post types
5432 - $option_name = 'mxchat_auto_sync_' . $post_type;
5433 - if (get_option($option_name) === '1') {
5434 - $should_sync = true;
5435 - }
5436 - }
5437 -
5438 - if (!$should_sync) {
5439 - return;
5440 - }
5441 -
5442 - // Resolve the pre-trash URL. wp_trash_post renames the slug with "__trashed" before firing
5443 - // this hook, so get_permalink() here would return the trashed URL and md5() would miss the
5444 - // real vector IDs stored under the original URL.
5445 - $source_url = $this->mxchat_resolve_pre_trash_url($post_id);
5446 - if (!$source_url) {
5447 - //error_log('MXChat: Failed to resolve source URL for post ' . $post_id);
5448 - return;
5449 - }
5450 -
5451 - // Use chunk-aware deletion (handles both chunked and non-chunked content)
5452 - $delete_result = MxChat_Utils::delete_chunks_for_url($source_url, 'default');
5453 -
5454 - if (is_wp_error($delete_result)) {
5455 - //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message());
5456 - }
5457 -
5458 - delete_transient('mxchat_prev_url_' . $post_id);
5459 - delete_transient('mxchat_prev_status_' . $post_id);
5460 -}
5461 -
5462 -/**
5463 - * Resolve the source URL for a post being trashed/deleted.
5464 - *
5465 - * Why: wp_trash_post appends "__trashed" to the slug before the wp_trash_post action fires, so
5466 - * get_permalink() returns a URL whose md5() won't match the vector IDs stored in Pinecone or
5467 - * the source_url rows in the WP DB. Prefer the URL captured by mxchat_store_pre_update_status
5468 - * (runs on pre_post_update, before the rename); fall back to stripping the __trashed suffix.
5469 - */
5470 -private function mxchat_resolve_pre_trash_url($post_id) {
5471 - $previous_url = get_transient('mxchat_prev_url_' . $post_id);
5472 - if (!empty($previous_url)) {
5473 - return $previous_url;
5474 - }
5475 -
5476 - $current = get_permalink($post_id);
5477 - if (!$current) {
5478 - return '';
5479 - }
5480 - return preg_replace('#__trashed(/?)$#', '$1', $current);
5481 -}
5482 -
5483 -
5484 -
5485 -public function mxchat_handle_product_change($post_id, $post, $update) {
5486 - if ($post->post_type !== 'product') {
5487 - return;
5488 - }
5489 -
5490 - if ($post->post_status === 'publish') {
5491 - add_action('shutdown', function() use ($post_id) {
5492 - $product = wc_get_product($post_id);
5493 - if ($product) {
5494 - $this->mxchat_store_product_embedding($product);
5495 - }
5496 - });
5497 - }
5498 -}
5499 -
5500 -/**
5501 - * Store WooCommerce product embeddings
5502 - */
5503 -private function mxchat_store_product_embedding($product) {
5504 - if (!isset($this->options['enable_woocommerce_integration']) ||
5505 - !in_array($this->options['enable_woocommerce_integration'], ['1', 'on'])) {
5506 - return;
5507 - }
5508 -
5509 - $source_url = get_permalink($product->get_id());
5510 - $product_id = $product->get_id();
5511 -
5512 - // Build product content
5513 - $title = $product->get_name();
5514 - $description = $product->get_description();
5515 - $short_description = $product->get_short_description();
5516 - $regular_price = $product->get_regular_price();
5517 - $sale_price = $product->get_sale_price();
5518 - $price = $product->get_price();
5519 - $sku = $product->get_sku();
5520 -
5521 - // Get currency symbol
5522 - $currency_symbol = get_woocommerce_currency_symbol();
5523 -
5524 - // Format content consistently
5525 - $content = $title . "\n\n";
5526 -
5527 - if (!empty($short_description)) {
5528 - $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
5529 - }
5530 -
5531 - if (!empty($description)) {
5532 - $content .= wp_strip_all_tags($description) . "\n\n";
5533 - }
5534 -
5535 - // Add pricing information
5536 - if (!empty($regular_price)) {
5537 - $content .= "Price: " . $currency_symbol . $regular_price . "\n";
5538 - } elseif (!empty($price)) {
5539 - $content .= "Price: " . $currency_symbol . $price . "\n";
5540 - }
5541 -
5542 - if (!empty($sale_price) && $sale_price !== $regular_price) {
5543 - $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
5544 - }
5545 -
5546 - // Handle variable products - show price range
5547 - if ($product->is_type('variable')) {
5548 - $min_price = $product->get_variation_price('min');
5549 - $max_price = $product->get_variation_price('max');
5550 - if ($min_price !== $max_price) {
5551 - $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
5552 - }
5553 - }
5554 -
5555 - if (!empty($sku)) {
5556 - $content .= "SKU: " . $sku . "\n";
5557 - }
5558 -
5559 - // Get product categories
5560 - $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
5561 - if (!empty($categories) && !is_wp_error($categories)) {
5562 - $content .= "Categories: " . implode(', ', $categories) . "\n";
5563 - }
5564 -
5565 - // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
5566 - $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
5567 - if (!empty($custom_tabs) && is_array($custom_tabs)) {
5568 - foreach ($custom_tabs as $tab) {
5569 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
5570 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
5571 -
5572 - if (!empty($tab_title) && !empty($tab_content)) {
5573 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
5574 - }
5575 - }
5576 - }
5577 -
5578 - // Also check for reusable/saved tabs applied to this product
5579 - $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
5580 - if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
5581 - // Get the saved tabs option
5582 - $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
5583 - if (!empty($saved_tabs) && is_array($saved_tabs)) {
5584 - foreach ($applied_saved_tabs as $saved_tab_id) {
5585 - if (isset($saved_tabs[$saved_tab_id])) {
5586 - $tab = $saved_tabs[$saved_tab_id];
5587 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
5588 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
5589 -
5590 - if (!empty($tab_title) && !empty($tab_content)) {
5591 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
5592 - }
5593 - }
5594 - }
5595 - }
5596 - }
5597 -
5598 - // Get API key with proper model detection
5599 - $options = get_option('mxchat_options');
5600 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
5601 -
5602 - if (strpos($selected_model, 'voyage') === 0) {
5603 - $api_key = $options['voyage_api_key'] ?? '';
5604 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
5605 - $api_key = $options['gemini_api_key'] ?? '';
5606 - } else {
5607 - $api_key = $options['api_key'] ?? '';
5608 - }
5609 -
5610 - if (empty($api_key)) {
5611 - //error_log('MxChat Auto-sync: No API key configured for embedding model');
5612 - return;
5613 - }
5614 -
5615 - // Use the centralized utility function for storage
5616 - $result = MxChat_Utils::submit_content_to_db(
5617 - $content,
5618 - $source_url,
5619 - $api_key,
5620 - md5($source_url) // Vector ID for Pinecone
5621 - );
5622 -
5623 - // After successful storage, apply role restriction based on tags
5624 - if (!is_wp_error($result)) {
5625 - $this->apply_role_restriction_to_post($product_id, $source_url);
5626 - }
5627 -
5628 - if (is_wp_error($result)) {
5629 - //error_log('MxChat WooCommerce sync failed for product ' . $product_id . ': ' . $result->get_error_message());
5630 - }
5631 -}
5632 -
5633 -public function mxchat_handle_product_delete($post_id) {
5634 - if (get_post_type($post_id) !== 'product') {
5635 - return;
5636 - }
5637 -
5638 - $source_url = $this->mxchat_resolve_pre_trash_url($post_id);
5639 - if (!$source_url) {
5640 - return;
5641 - }
5642 -
5643 - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks)
5644 - MxChat_Utils::delete_chunks_for_url($source_url, 'default');
5645 -
5646 - delete_transient('mxchat_prev_url_' . $post_id);
5647 - delete_transient('mxchat_prev_status_' . $post_id);
5648 -}
5649 -
5650 -/**
5651 - * Handle individual Pinecone content deletion
5652 - */
5653 -public function mxchat_handle_pinecone_prompt_delete() {
5654 - // Check permissions
5655 - if (!current_user_can('manage_options')) {
5656 - wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
5657 - }
5658 -
5659 - // Verify nonce
5660 - if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'mxchat_delete_pinecone_prompt_nonce')) {
5661 - wp_die(esc_html__('Security check failed.', 'mxchat'));
5662 - }
5663 -
5664 - $vector_id = isset($_GET['vector_id']) ? sanitize_text_field($_GET['vector_id']) : '';
5665 -
5666 - if (empty($vector_id)) {
5667 - set_transient('mxchat_admin_notice_error',
5668 - esc_html__('Invalid vector ID.', 'mxchat'),
5669 - 30
5670 - );
5671 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5672 - exit;
5673 - }
5674 -
5675 - // Get Pinecone settings
5676 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5677 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5678 -
5679 - if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5680 - set_transient('mxchat_admin_notice_error',
5681 - esc_html__('Pinecone is not properly configured.', 'mxchat'),
5682 - 30
5683 - );
5684 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5685 - exit;
5686 - }
5687 -
5688 - // Delete from Pinecone
5689 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5690 - $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
5691 - $vector_id,
5692 - $pinecone_options['mxchat_pinecone_api_key'],
5693 - $pinecone_options['mxchat_pinecone_host']
5694 - );
5695 -
5696 - if ($result['success']) {
5697 - // No cache clearing needed since we removed caching
5698 - set_transient('mxchat_admin_notice_success',
5699 - esc_html__('Entry deleted successfully from Pinecone.', 'mxchat'),
5700 - 30
5701 - );
5702 - } else {
5703 - set_transient('mxchat_admin_notice_error',
5704 - esc_html__('Failed to delete entry: ', 'mxchat') . $result['message'],
5705 - 30
5706 - );
5707 - }
5708 -
5709 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
5710 - exit;
5711 -}
5712 -/**
5713 - * Handle individual Pinecone content deletion via AJAX
5714 - */
5715 -public function ajax_mxchat_delete_pinecone_prompt() {
5716 - // Verify nonce and permissions
5717 - if (!check_ajax_referer('mxchat_delete_pinecone_prompt_nonce', 'nonce', false)) {
5718 - wp_send_json_error('Invalid nonce');
5719 - exit;
5720 - }
5721 -
5722 - if (!current_user_can('manage_options')) {
5723 - wp_send_json_error('Unauthorized access');
5724 - exit;
5725 - }
5726 -
5727 - $vector_id = isset($_POST['vector_id']) ? sanitize_text_field($_POST['vector_id']) : '';
5728 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
5729 -
5730 - if (empty($vector_id)) {
5731 - wp_send_json_error('Missing vector ID');
5732 - exit;
5733 - }
5734 -
5735 - // Get bot-specific Pinecone settings
5736 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5737 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
5738 -
5739 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5740 -
5741 - if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5742 - wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
5743 - exit;
5744 - }
5745 -
5746 - // Delete from the correct Pinecone index
5747 - $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
5748 - $vector_id,
5749 - $pinecone_options['mxchat_pinecone_api_key'],
5750 - $pinecone_options['mxchat_pinecone_host']
5751 - );
5752 -
5753 - if ($result['success']) {
5754 - // No cache clearing needed since we removed caching
5755 - wp_send_json_success(array(
5756 - 'message' => 'Entry deleted successfully from Pinecone',
5757 - 'vector_id' => $vector_id,
5758 - 'bot_id' => $bot_id
5759 - ));
5760 - } else {
5761 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Failed to delete from Pinecone: ' . $result['message'], array('vector_id' => $vector_id, 'bot_id' => $bot_id));
5762 - wp_send_json_error('Failed to delete from Pinecone: ' . $result['message']);
5763 - }
5764 -
5765 - exit;
5766 -}
5767 -
5768 -/**
5769 - * Handle deletion of all chunks for a given source URL via AJAX
5770 - * Follows the same pattern as ajax_mxchat_delete_pinecone_prompt
5771 - */
5772 -public function ajax_mxchat_delete_chunks_by_url() {
5773 - // Verify nonce and permissions
5774 - if (!check_ajax_referer('mxchat_delete_chunks_nonce', 'nonce', false)) {
5775 - wp_send_json_error('Invalid nonce');
5776 - exit;
5777 - }
5778 -
5779 - if (!current_user_can('manage_options')) {
5780 - wp_send_json_error('Unauthorized access');
5781 - exit;
5782 - }
5783 -
5784 - $source_url = isset($_POST['source_url']) ? esc_url_raw($_POST['source_url']) : '';
5785 - $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
5786 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
5787 -
5788 - if (empty($source_url)) {
5789 - wp_send_json_error('Missing source URL');
5790 - exit;
5791 - }
5792 -
5793 - // Generate the base vector ID from the source URL (same as how chunks are created)
5794 - $base_vector_id = md5($source_url);
5795 -
5796 - if ($data_source === 'pinecone') {
5797 - // Get bot-specific Pinecone settings (same as working delete function)
5798 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
5799 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
5800 -
5801 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5802 -
5803 - if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
5804 - wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
5805 - exit;
5806 - }
5807 -
5808 - $api_key = $pinecone_options['mxchat_pinecone_api_key'];
5809 - $host = $pinecone_options['mxchat_pinecone_host'];
5810 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
5811 -
5812 - // Collect all vector IDs to delete
5813 - $vectors_to_delete = array();
5814 -
5815 - // Add the original single-vector ID (for non-chunked content)
5816 - $vectors_to_delete[] = $base_vector_id;
5817 -
5818 - // Use Pinecone list API to find all chunk vectors with this prefix
5819 - // NOTE: Pinecone List API is a GET request with query parameters, not POST
5820 - $prefix = $base_vector_id . '_chunk_';
5821 -
5822 - $query_params = array(
5823 - 'prefix' => $prefix,
5824 - 'limit' => 100
5825 - );
5826 -
5827 - if (!empty($namespace)) {
5828 - $query_params['namespace'] = $namespace;
5829 - }
5830 -
5831 - $list_url = "https://{$host}/vectors/list?" . http_build_query($query_params);
5832 -
5833 - $list_response = wp_remote_get($list_url, array(
5834 - 'headers' => array(
5835 - 'Api-Key' => $api_key,
5836 - 'accept' => 'application/json'
5837 - ),
5838 - 'timeout' => 30
5839 - ));
5840 -
5841 - if (!is_wp_error($list_response)) {
5842 - $list_body_response = wp_remote_retrieve_body($list_response);
5843 - $list_data = json_decode($list_body_response, true);
5844 - if (!empty($list_data['vectors'])) {
5845 - foreach ($list_data['vectors'] as $vector) {
5846 - if (isset($vector['id'])) {
5847 - $vectors_to_delete[] = $vector['id'];
5848 - }
5849 - }
5850 - }
5851 - }
5852 -
5853 - if (empty($vectors_to_delete)) {
5854 - wp_send_json_success(array(
5855 - 'message' => 'No vectors found to delete',
5856 - 'source_url' => $source_url
5857 - ));
5858 - exit;
5859 - }
5860 -
5861 - // Delete all vectors using the same endpoint as the working function
5862 - $delete_url = "https://{$host}/vectors/delete";
5863 -
5864 - $delete_body = array(
5865 - 'ids' => $vectors_to_delete
5866 - );
5867 -
5868 - if (!empty($namespace)) {
5869 - $delete_body['namespace'] = $namespace;
5870 - }
5871 -
5872 - $delete_response = wp_remote_post($delete_url, array(
5873 - 'headers' => array(
5874 - 'Api-Key' => $api_key,
5875 - 'accept' => 'application/json',
5876 - 'content-type' => 'application/json'
5877 - ),
5878 - 'body' => wp_json_encode($delete_body),
5879 - 'timeout' => 30
5880 - ));
5881 -
5882 - if (is_wp_error($delete_response)) {
5883 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Failed to delete chunks from Pinecone: ' . $delete_response->get_error_message(), array('source_url' => $source_url));
5884 - wp_send_json_error('Failed to delete from Pinecone: ' . $delete_response->get_error_message());
5885 - exit;
5886 - }
5887 -
5888 - $response_code = wp_remote_retrieve_response_code($delete_response);
5889 -
5890 - if ($response_code !== 200) {
5891 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Pinecone API error (HTTP ' . $response_code . ')', array('source_url' => $source_url));
5892 - wp_send_json_error('Pinecone API error (HTTP ' . $response_code . ')');
5893 - exit;
5894 - }
5895 -
5896 - wp_send_json_success(array(
5897 - 'message' => 'All chunks deleted successfully from Pinecone',
5898 - 'source_url' => $source_url,
5899 - 'deleted_count' => count($vectors_to_delete)
5900 - ));
5901 -
5902 - } else {
5903 - // WordPress database deletion
5904 - global $wpdb;
5905 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5906 -
5907 - $result = $wpdb->delete(
5908 - $table_name,
5909 - array('source_url' => $source_url),
5910 - array('%s')
5911 - );
5912 -
5913 - if ($result === false) {
5914 - MxChat_Admin::mxchat_log_debug('database_error', 'Failed to delete from database: ' . $wpdb->last_error, array('source_url' => $source_url));
5915 - wp_send_json_error('Failed to delete from database: ' . $wpdb->last_error);
5916 - exit;
5917 - }
5918 -
5919 - wp_send_json_success(array(
5920 - 'message' => 'All chunks deleted successfully from database',
5921 - 'source_url' => $source_url,
5922 - 'deleted_count' => $result
5923 - ));
5924 - }
5925 -
5926 - exit;
5927 -}
5928 -
5929 -/**
5930 - * Handle individual WordPress database content deletion via AJAX
5931 - * Mirrors the Pinecone delete handler but for WordPress database entries
5932 - */
5933 -public function ajax_mxchat_delete_wordpress_prompt() {
5934 - // Verify nonce and permissions
5935 - if (!check_ajax_referer('mxchat_delete_wordpress_prompt_nonce', 'nonce', false)) {
5936 - wp_send_json_error('Invalid nonce');
5937 - exit;
5938 - }
5939 -
5940 - if (!current_user_can('manage_options')) {
5941 - wp_send_json_error('Unauthorized access');
5942 - exit;
5943 - }
5944 -
5945 - $entry_id = isset($_POST['entry_id']) ? intval($_POST['entry_id']) : 0;
5946 -
5947 - if (empty($entry_id)) {
5948 - wp_send_json_error('Missing entry ID');
5949 - exit;
5950 - }
5951 -
5952 - global $wpdb;
5953 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5954 -
5955 - // Clear cache for this entry
5956 - wp_cache_delete('prompt_' . $entry_id, 'mxchat_prompts');
5957 -
5958 - // Delete from database
5959 - $result = $wpdb->delete(
5960 - $table_name,
5961 - array('id' => $entry_id),
5962 - array('%d')
5963 - );
5964 -
5965 - if ($result !== false) {
5966 - wp_send_json_success(array(
5967 - 'message' => 'Entry deleted successfully',
5968 - 'entry_id' => $entry_id
5969 - ));
5970 - } else {
5971 - wp_send_json_error('Failed to delete entry from database');
5972 - }
5973 -
5974 - exit;
5975 -}
5976 -
5977 -/**
5978 - * Handle bulk deletion of knowledge entries via AJAX
5979 - * Supports both Pinecone and WordPress database entries
5980 - */
5981 -public function ajax_mxchat_bulk_delete_knowledge() {
5982 - // Verify nonce and permissions
5983 - if (!check_ajax_referer('mxchat_bulk_delete_knowledge_nonce', 'nonce', false)) {
5984 - wp_send_json_error('Invalid nonce');
5985 - exit;
5986 - }
5987 -
5988 - if (!current_user_can('manage_options')) {
5989 - wp_send_json_error('Unauthorized access');
5990 - exit;
5991 - }
5992 -
5993 - $entries = isset($_POST['entries']) ? $_POST['entries'] : array();
5994 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
5995 -
5996 - if (empty($entries) || !is_array($entries)) {
5997 - wp_send_json_error('No entries provided');
5998 - exit;
5999 - }
6000 -
6001 - // Extend execution time — bulk Pinecone operations can take a while
6002 - if (function_exists('set_time_limit')) {
6003 - set_time_limit(120);
6004 - }
6005 -
6006 - $success_ids = array();
6007 - $failed_ids = array();
6008 - $errors = array();
6009 -
6010 - global $wpdb;
6011 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6012 -
6013 - // Get Pinecone manager for Pinecone deletions
6014 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
6015 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
6016 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6017 -
6018 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
6019 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
6020 -
6021 - // =============================================
6022 - // PHASE 1: Collect all Pinecone vector IDs
6023 - // and separate WordPress entries
6024 - // =============================================
6025 - $pinecone_entry_ids = array(); // entry IDs that are pinecone-sourced
6026 - $wordpress_entries = array(); // entries for WordPress DB deletion
6027 - $all_vector_ids = array(); // all pinecone vector IDs to delete in one batch
6028 -
6029 - foreach ($entries as $entry) {
6030 - $entry_id = sanitize_text_field($entry['id'] ?? '');
6031 - $source = sanitize_text_field($entry['source'] ?? 'wordpress');
6032 - $source_url = isset($entry['sourceUrl']) ? esc_url_raw($entry['sourceUrl']) : '';
6033 - $is_group = isset($entry['isGroup']) && ($entry['isGroup'] === true || $entry['isGroup'] === 'true');
6034 -
6035 - if (empty($entry_id)) {
6036 - continue;
6037 - }
6038 -
6039 - if ($source === 'pinecone') {
6040 - if (!$use_pinecone || empty($api_key)) {
6041 - $failed_ids[] = $entry_id;
6042 - $errors[] = "Pinecone not configured for entry: $entry_id";
6043 - continue;
6044 - }
6045 -
6046 - $pinecone_entry_ids[] = $entry_id;
6047 -
6048 - if ($is_group && !empty($source_url)) {
6049 - // Grouped/chunked entry: collect base ID + chunk IDs via List API
6050 - $base_vector_id = md5($source_url);
6051 - $all_vector_ids[] = $base_vector_id;
6052 -
6053 - $list_url = 'https://' . $host . '/vectors/list?prefix=' . urlencode($base_vector_id . '_chunk_') . '&limit=100';
6054 - $list_response = wp_remote_get($list_url, array(
6055 - 'headers' => array(
6056 - 'Api-Key' => $api_key,
6057 - 'accept' => 'application/json'
6058 - ),
6059 - 'timeout' => 30
6060 - ));
6061 -
6062 - if (!is_wp_error($list_response)) {
6063 - $list_body = json_decode(wp_remote_retrieve_body($list_response), true);
6064 - if (!empty($list_body['vectors']) && is_array($list_body['vectors'])) {
6065 - foreach ($list_body['vectors'] as $vector) {
6066 - if (isset($vector['id'])) {
6067 - $all_vector_ids[] = $vector['id'];
6068 - }
6069 - }
6070 - }
6071 - }
6072 - } else {
6073 - // Single entry: the entry_id IS the vector ID
6074 - $all_vector_ids[] = $entry_id;
6075 - }
6076 - } else {
6077 - $wordpress_entries[] = $entry;
6078 - }
6079 - }
6080 -
6081 - // =============================================
6082 - // PHASE 2: Single batch delete to Pinecone
6083 - // =============================================
6084 - if (!empty($all_vector_ids)) {
6085 - $all_vector_ids = array_values(array_unique($all_vector_ids));
6086 - $pinecone_success = true;
6087 - $batches = array_chunk($all_vector_ids, 100);
6088 -
6089 - foreach ($batches as $batch) {
6090 - $delete_response = wp_remote_post("https://{$host}/vectors/delete", array(
6091 - 'headers' => array(
6092 - 'Api-Key' => $api_key,
6093 - 'accept' => 'application/json',
6094 - 'content-type' => 'application/json'
6095 - ),
6096 - 'body' => wp_json_encode(array('ids' => $batch)),
6097 - 'timeout' => 60
6098 - ));
6099 -
6100 - if (is_wp_error($delete_response)) {
6101 - $pinecone_success = false;
6102 - $errors[] = 'Pinecone batch deletion failed: ' . $delete_response->get_error_message();
6103 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Bulk delete batch failed: ' . $delete_response->get_error_message());
6104 - } else {
6105 - $response_code = wp_remote_retrieve_response_code($delete_response);
6106 - if ($response_code !== 200) {
6107 - $pinecone_success = false;
6108 - $response_body = wp_remote_retrieve_body($delete_response);
6109 - $errors[] = "Pinecone API error (HTTP $response_code)";
6110 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Bulk delete batch failed (HTTP ' . $response_code . ')', array('response' => substr($response_body, 0, 200)));
6111 - }
6112 - }
6113 - }
6114 -
6115 - // Mark all pinecone entries based on batch result
6116 - foreach ($pinecone_entry_ids as $eid) {
6117 - if ($pinecone_success) {
6118 - $success_ids[] = $eid;
6119 - } else {
6120 - $failed_ids[] = $eid;
6121 - }
6122 - }
6123 - }
6124 -
6125 - // =============================================
6126 - // PHASE 3: WordPress database deletions
6127 - // =============================================
6128 - foreach ($wordpress_entries as $entry) {
6129 - $entry_id = sanitize_text_field($entry['id'] ?? '');
6130 - $source_url = isset($entry['sourceUrl']) ? esc_url_raw($entry['sourceUrl']) : '';
6131 - $is_group = isset($entry['isGroup']) && ($entry['isGroup'] === true || $entry['isGroup'] === 'true');
6132 -
6133 - if (empty($entry_id)) {
6134 - continue;
6135 - }
6136 -
6137 - try {
6138 - if ($is_group && !empty($source_url)) {
6139 - $result = $wpdb->delete(
6140 - $table_name,
6141 - array('source_url' => $source_url),
6142 - array('%s')
6143 - );
6144 - } else {
6145 - wp_cache_delete('prompt_' . $entry_id, 'mxchat_prompts');
6146 - $result = $wpdb->delete(
6147 - $table_name,
6148 - array('id' => intval($entry_id)),
6149 - array('%d')
6150 - );
6151 - }
6152 -
6153 - if ($result !== false) {
6154 - $success_ids[] = $entry_id;
6155 - } else {
6156 - $failed_ids[] = $entry_id;
6157 - $errors[] = "Database error for entry: $entry_id";
6158 - }
6159 - } catch (Exception $e) {
6160 - $failed_ids[] = $entry_id;
6161 - $errors[] = $e->getMessage();
6162 - }
6163 - }
6164 -
6165 - wp_send_json_success(array(
6166 - 'success_ids' => $success_ids,
6167 - 'failed_ids' => $failed_ids,
6168 - 'errors' => $errors,
6169 - 'total_processed' => count($success_ids) + count($failed_ids)
6170 - ));
6171 -
6172 - exit;
6173 -}
6174 -
6175 -/**
6176 - * Get hierarchical roles for dropdown
6177 - */
6178 -public function mxchat_get_role_options() {
6179 - return array(
6180 - 'public' => __('Public (Everyone)', 'mxchat'),
6181 - 'logged_in' => __('Logged In Users', 'mxchat'),
6182 - 'subscriber' => __('Subscribers & Above', 'mxchat'),
6183 - 'contributor' => __('Contributors & Above', 'mxchat'),
6184 - 'author' => __('Authors & Above', 'mxchat'),
6185 - 'editor' => __('Editors & Above', 'mxchat'),
6186 - 'administrator' => __('Administrators Only', 'mxchat')
6187 - );
6188 -}
6189 -
6190 -/**
6191 - * Check if user has access to content based on role restriction
6192 - */
6193 -public function mxchat_user_has_content_access($role_restriction) {
6194 - // Public content is always accessible
6195 - if ($role_restriction === 'public' || empty($role_restriction)) {
6196 - return true;
6197 - }
6198 -
6199 - // Check if user is logged in for logged_in restriction
6200 - if ($role_restriction === 'logged_in') {
6201 - return is_user_logged_in();
6202 - }
6203 -
6204 - // If not logged in, no access to role-restricted content
6205 - if (!is_user_logged_in()) {
6206 - return false;
6207 - }
6208 -
6209 - $user = wp_get_current_user();
6210 - $user_roles = $user->roles;
6211 -
6212 - if (empty($user_roles)) {
6213 - return false;
6214 - }
6215 -
6216 - // Define role hierarchy (higher number = higher access)
6217 - $hierarchy = array(
6218 - 'subscriber' => 1,
6219 - 'contributor' => 2,
6220 - 'author' => 3,
6221 - 'editor' => 4,
6222 - 'administrator' => 5
6223 - );
6224 -
6225 - // Get required level
6226 - $required_level = isset($hierarchy[$role_restriction]) ? $hierarchy[$role_restriction] : 0;
6227 -
6228 - // Check if user has required level or higher
6229 - foreach ($user_roles as $user_role) {
6230 - $user_level = isset($hierarchy[$user_role]) ? $hierarchy[$user_role] : 0;
6231 - if ($user_level >= $required_level) {
6232 - return true;
6233 - }
6234 - }
6235 -
6236 - return false;
6237 -}
6238 -
6239 -/**
6240 - * Handle role restriction updates via AJAX
6241 - * Removed cache clearing call since we removed caching
6242 - */
6243 -public function ajax_mxchat_update_role_restriction() {
6244 - // Verify nonce and permissions
6245 - if (!check_ajax_referer('mxchat_update_role_nonce', 'nonce', false)) {
6246 - wp_send_json_error('Invalid nonce');
6247 - exit;
6248 - }
6249 -
6250 - if (!current_user_can('manage_options')) {
6251 - wp_send_json_error('Unauthorized access');
6252 - exit;
6253 - }
6254 -
6255 - $entry_id = isset($_POST['entry_id']) ? sanitize_text_field($_POST['entry_id']) : '';
6256 - $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
6257 - $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
6258 -
6259 - if (empty($entry_id)) {
6260 - wp_send_json_error('Invalid entry ID');
6261 - exit;
6262 - }
6263 -
6264 - // Get knowledge manager instance to validate role restriction
6265 - $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
6266 - $valid_roles = array_keys($knowledge_manager->mxchat_get_role_options());
6267 - if (!in_array($role_restriction, $valid_roles)) {
6268 - wp_send_json_error('Invalid role restriction');
6269 - exit;
6270 - }
6271 -
6272 - global $wpdb;
6273 -
6274 - if ($data_source === 'pinecone') {
6275 - // Handle Pinecone role restriction (stored separately in WordPress table)
6276 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6277 -
6278 - // Use REPLACE to insert or update the role restriction
6279 - $result = $wpdb->replace(
6280 - $roles_table,
6281 - array(
6282 - 'vector_id' => $entry_id,
6283 - 'role_restriction' => $role_restriction,
6284 - 'updated_at' => current_time('mysql')
6285 - ),
6286 - array('%s', '%s', '%s')
6287 - );
6288 -
6289 - // No cache clearing needed since we removed caching
6290 -
6291 - } else {
6292 - // Handle WordPress database role restriction (existing functionality)
6293 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6294 -
6295 - $result = $wpdb->update(
6296 - $table_name,
6297 - array('role_restriction' => $role_restriction),
6298 - array('id' => absint($entry_id)),
6299 - array('%s'),
6300 - array('%d')
6301 - );
6302 - }
6303 -
6304 - if ($result === false) {
6305 - wp_send_json_error('Database update failed: ' . $wpdb->last_error);
6306 - exit;
6307 - }
6308 -
6309 - wp_send_json_success(array(
6310 - 'message' => 'Role restriction updated successfully',
6311 - 'role_restriction' => $role_restriction,
6312 - 'data_source' => $data_source,
6313 - 'entry_id' => $entry_id
6314 - ));
6315 - exit;
6316 -}
6317 -
6318 -// ========================================
6319 -// ROLE-BASED CONTENT RESTRICTIONS - NEW FUNCTIONS
6320 -// Add these to your MxChat_Knowledge_Manager class
6321 -// ========================================
6322 -
6323 -/**
6324 - * Initialize role-based content hooks
6325 - * Add this call to your __construct() or mxchat_init_hooks() method
6326 - */
6327 -private function mxchat_init_role_hooks() {
6328 - // AJAX handlers for tag-role mappings
6329 - add_action('wp_ajax_mxchat_add_tag_role_mapping', array($this, 'ajax_add_tag_role_mapping'));
6330 - add_action('wp_ajax_mxchat_delete_tag_role_mapping', array($this, 'ajax_delete_tag_role_mapping'));
6331 - add_action('wp_ajax_mxchat_get_tag_role_mappings', array($this, 'ajax_get_tag_role_mappings'));
6332 - add_action('wp_ajax_mxchat_bulk_update_tag_roles', array($this, 'ajax_bulk_update_tag_roles'));
6333 -
6334 - // Hook to automatically update role restrictions when tags are added/removed
6335 - add_action('set_object_terms', array($this, 'handle_tag_change'), 10, 6);
6336 -
6337 - // Hook to apply role restrictions on auto-sync
6338 - add_action('mxchat_content_stored', array($this, 'apply_role_restriction_after_storage'), 10, 2);
6339 -}
6340 -
6341 -/**
6342 - * Add tag-role mapping via AJAX
6343 - */
6344 -public function ajax_add_tag_role_mapping() {
6345 - // Verify nonce and permissions
6346 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6347 -
6348 - if (!current_user_can('manage_options')) {
6349 - wp_send_json_error('Unauthorized access');
6350 - exit;
6351 - }
6352 -
6353 - $tag_slug = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
6354 - $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
6355 -
6356 - if (empty($tag_slug)) {
6357 - wp_send_json_error('Tag slug is required');
6358 - exit;
6359 - }
6360 -
6361 - // Validate role restriction
6362 - $valid_roles = array_keys($this->mxchat_get_role_options());
6363 - if (!in_array($role_restriction, $valid_roles)) {
6364 - wp_send_json_error('Invalid role restriction');
6365 - exit;
6366 - }
6367 -
6368 - // Check if tag exists in WordPress
6369 - $term = get_term_by('slug', $tag_slug, 'post_tag');
6370 - if (!$term) {
6371 - wp_send_json_error('Tag does not exist in WordPress');
6372 - exit;
6373 - }
6374 -
6375 - // Get existing mappings
6376 - $mappings = get_option('mxchat_tag_role_mappings', array());
6377 -
6378 - // Check if mapping already exists
6379 - if (isset($mappings[$tag_slug])) {
6380 - wp_send_json_error('Mapping for this tag already exists');
6381 - exit;
6382 - }
6383 -
6384 - // Add new mapping
6385 - $mappings[$tag_slug] = $role_restriction;
6386 - update_option('mxchat_tag_role_mappings', $mappings);
6387 -
6388 - wp_send_json_success(array(
6389 - 'message' => 'Tag-role mapping added successfully',
6390 - 'tag_slug' => $tag_slug,
6391 - 'role_restriction' => $role_restriction
6392 - ));
6393 - exit;
6394 -}
6395 -
6396 -/**
6397 - * Delete tag-role mapping via AJAX
6398 - */
6399 -public function ajax_delete_tag_role_mapping() {
6400 - // Verify nonce and permissions
6401 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6402 -
6403 - if (!current_user_can('manage_options')) {
6404 - wp_send_json_error('Unauthorized access');
6405 - exit;
6406 - }
6407 -
6408 - $tag_slug = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
6409 -
6410 - if (empty($tag_slug)) {
6411 - wp_send_json_error('Tag slug is required');
6412 - exit;
6413 - }
6414 -
6415 - // Get existing mappings
6416 - $mappings = get_option('mxchat_tag_role_mappings', array());
6417 -
6418 - // Check if mapping exists
6419 - if (!isset($mappings[$tag_slug])) {
6420 - wp_send_json_error('Mapping does not exist');
6421 - exit;
6422 - }
6423 -
6424 - // Remove mapping
6425 - unset($mappings[$tag_slug]);
6426 - update_option('mxchat_tag_role_mappings', $mappings);
6427 -
6428 - wp_send_json_success(array(
6429 - 'message' => 'Tag-role mapping deleted successfully',
6430 - 'tag_slug' => $tag_slug
6431 - ));
6432 - exit;
6433 -}
6434 -
6435 -/**
6436 - * Get all tag-role mappings via AJAX
6437 - */
6438 -public function ajax_get_tag_role_mappings() {
6439 - // Verify nonce and permissions
6440 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6441 -
6442 - if (!current_user_can('manage_options')) {
6443 - wp_send_json_error('Unauthorized access');
6444 - exit;
6445 - }
6446 -
6447 - // Get mappings
6448 - $mappings = get_option('mxchat_tag_role_mappings', array());
6449 - $role_options = $this->mxchat_get_role_options();
6450 -
6451 - $formatted_mappings = array();
6452 -
6453 - foreach ($mappings as $tag_slug => $role_restriction) {
6454 - // Get tag object
6455 - $term = get_term_by('slug', $tag_slug, 'post_tag');
6456 -
6457 - // Count posts with this tag
6458 - $post_count = 0;
6459 - if ($term) {
6460 - $post_count = $term->count;
6461 - }
6462 -
6463 - $formatted_mappings[] = array(
6464 - 'tag_slug' => $tag_slug,
6465 - 'role_restriction' => $role_restriction,
6466 - 'role_label' => $role_options[$role_restriction] ?? $role_restriction,
6467 - 'post_count' => $post_count
6468 - );
6469 - }
6470 -
6471 - wp_send_json_success(array(
6472 - 'mappings' => $formatted_mappings
6473 - ));
6474 - exit;
6475 -}
6476 -
6477 -/**
6478 - * Bulk update role restrictions for all existing content with mapped tags
6479 - */
6480 -public function ajax_bulk_update_tag_roles() {
6481 - // Verify nonce and permissions
6482 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6483 -
6484 - if (!current_user_can('manage_options')) {
6485 - wp_send_json_error('Unauthorized access');
6486 - exit;
6487 - }
6488 -
6489 - // Get mappings
6490 - $mappings = get_option('mxchat_tag_role_mappings', array());
6491 -
6492 - if (empty($mappings)) {
6493 - wp_send_json_error('No tag-role mappings found');
6494 - exit;
6495 - }
6496 -
6497 - global $wpdb;
6498 -
6499 - // Check if using Pinecone
6500 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
6501 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6502 -
6503 - $updated_count = 0;
6504 - $details = array();
6505 -
6506 - foreach ($mappings as $tag_slug => $role_restriction) {
6507 - // Get all posts with this tag
6508 - $posts = get_posts(array(
6509 - 'tag' => $tag_slug,
6510 - 'post_type' => 'any',
6511 - 'posts_per_page' => -1,
6512 - 'fields' => 'ids',
6513 - 'post_status' => 'publish'
6514 - ));
6515 -
6516 - if (empty($posts)) {
6517 - continue;
6518 - }
6519 -
6520 - $tag_updated = 0;
6521 -
6522 - foreach ($posts as $post_id) {
6523 - $source_url = get_permalink($post_id);
6524 - if (!$source_url) {
6525 - continue;
6526 - }
6527 -
6528 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
6529 - // Update Pinecone role restriction
6530 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6531 - $vector_id = md5($source_url);
6532 -
6533 - $result = $wpdb->replace(
6534 - $roles_table,
6535 - array(
6536 - 'vector_id' => $vector_id,
6537 - 'role_restriction' => $role_restriction,
6538 - 'updated_at' => current_time('mysql')
6539 - ),
6540 - array('%s', '%s', '%s')
6541 - );
6542 - } else {
6543 - // Update WordPress DB
6544 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6545 -
6546 - $result = $wpdb->update(
6547 - $table_name,
6548 - array('role_restriction' => $role_restriction),
6549 - array('source_url' => $source_url),
6550 - array('%s'),
6551 - array('%s')
6552 - );
6553 - }
6554 -
6555 - if ($result !== false) {
6556 - $tag_updated++;
6557 - $updated_count++;
6558 - }
6559 - }
6560 -
6561 - if ($tag_updated > 0) {
6562 - $details[] = sprintf(
6563 - 'Tag "%s" (%s): %d posts updated',
6564 - $tag_slug,
6565 - $role_restriction,
6566 - $tag_updated
6567 - );
6568 - }
6569 - }
6570 -
6571 - wp_send_json_success(array(
6572 - 'message' => 'Bulk update completed',
6573 - 'updated_count' => $updated_count,
6574 - 'tags_processed' => count($mappings),
6575 - 'details' => $details
6576 - ));
6577 - exit;
6578 -}
6579 -
6580 -/**
6581 - * Handle tag changes on posts (when tags are added or removed)
6582 - */
6583 -public function handle_tag_change($object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids) {
6584 - // Only process post tags
6585 - if ($taxonomy !== 'post_tag') {
6586 - return;
6587 - }
6588 -
6589 - // Get tag-role mappings
6590 - $mappings = get_option('mxchat_tag_role_mappings', array());
6591 -
6592 - if (empty($mappings)) {
6593 - return;
6594 - }
6595 -
6596 - // Get the post's URL
6597 - $source_url = get_permalink($object_id);
6598 - if (!$source_url) {
6599 - return;
6600 - }
6601 -
6602 - // Determine the highest role restriction based on tags
6603 - $highest_role = 'public';
6604 - $role_hierarchy = array(
6605 - 'public' => 0,
6606 - 'logged_in' => 1,
6607 - 'subscriber' => 2,
6608 - 'contributor' => 3,
6609 - 'author' => 4,
6610 - 'editor' => 5,
6611 - 'administrator' => 6
6612 - );
6613 -
6614 - // Get all current tags for the post
6615 - $current_tags = wp_get_post_tags($object_id, array('fields' => 'slugs'));
6616 -
6617 - // Find the highest role restriction among the tags
6618 - foreach ($current_tags as $tag_slug) {
6619 - if (isset($mappings[$tag_slug])) {
6620 - $role = $mappings[$tag_slug];
6621 - if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
6622 - $highest_role = $role;
6623 - }
6624 - }
6625 - }
6626 -
6627 - // Update the role restriction in the database
6628 - global $wpdb;
6629 -
6630 - // Check if using Pinecone
6631 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
6632 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6633 -
6634 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
6635 - // Update Pinecone role restriction
6636 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6637 - $vector_id = md5($source_url);
6638 -
6639 - $wpdb->replace(
6640 - $roles_table,
6641 - array(
6642 - 'vector_id' => $vector_id,
6643 - 'role_restriction' => $highest_role,
6644 - 'updated_at' => current_time('mysql')
6645 - ),
6646 - array('%s', '%s', '%s')
6647 - );
6648 - } else {
6649 - // Update WordPress DB
6650 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6651 -
6652 - $wpdb->update(
6653 - $table_name,
6654 - array('role_restriction' => $highest_role),
6655 - array('source_url' => $source_url),
6656 - array('%s'),
6657 - array('%s')
6658 - );
6659 - }
6660 -}
6661 -
6662 -/**
6663 - * Apply role restriction after content is stored (for auto-sync)
6664 - */
6665 -public function apply_role_restriction_after_storage($post_id, $source_url) {
6666 - // Get tag-role mappings
6667 - $mappings = get_option('mxchat_tag_role_mappings', array());
6668 -
6669 - if (empty($mappings)) {
6670 - return;
6671 - }
6672 -
6673 - // Get all tags for the post
6674 - $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
6675 -
6676 - if (empty($post_tags)) {
6677 - return;
6678 - }
6679 -
6680 - // Determine the highest role restriction based on tags
6681 - $highest_role = 'public';
6682 - $role_hierarchy = array(
6683 - 'public' => 0,
6684 - 'logged_in' => 1,
6685 - 'subscriber' => 2,
6686 - 'contributor' => 3,
6687 - 'author' => 4,
6688 - 'editor' => 5,
6689 - 'administrator' => 6
6690 - );
6691 -
6692 - foreach ($post_tags as $tag_slug) {
6693 - if (isset($mappings[$tag_slug])) {
6694 - $role = $mappings[$tag_slug];
6695 - if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
6696 - $highest_role = $role;
6697 - }
6698 - }
6699 - }
6700 -
6701 - // If no restricted tags found, return (leave as public)
6702 - if ($highest_role === 'public') {
6703 - return;
6704 - }
6705 -
6706 - // Update the role restriction
6707 - global $wpdb;
6708 -
6709 - // Check if using Pinecone
6710 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
6711 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6712 -
6713 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
6714 - // Update Pinecone role restriction
6715 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6716 - $vector_id = md5($source_url);
6717 -
6718 - $wpdb->replace(
6719 - $roles_table,
6720 - array(
6721 - 'vector_id' => $vector_id,
6722 - 'role_restriction' => $highest_role,
6723 - 'updated_at' => current_time('mysql')
6724 - ),
6725 - array('%s', '%s', '%s')
6726 - );
6727 - } else {
6728 - // Update WordPress DB
6729 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6730 -
6731 - $wpdb->update(
6732 - $table_name,
6733 - array('role_restriction' => $highest_role),
6734 - array('source_url' => $source_url),
6735 - array('%s'),
6736 - array('%s')
6737 - );
6738 - }
6739 -}
6740 -
6741 -
6742 - // ========================================
6743 - // HELPER METHODS
6744 - // ========================================
6745 -
6746 - /**
6747 - * Check if user has required permissions for content processing
6748 - */
6749 - private function mxchat_check_user_permissions() {
6750 - if (!current_user_can('manage_options')) {
6751 - wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
6752 - }
6753 - }
6754 -
6755 - /**
6756 - * Validate nonce for security
6757 - */
6758 - private function mxchat_validate_nonce($nonce_name, $nonce_action) {
6759 - if (!isset($_POST[$nonce_name]) || !wp_verify_nonce($_POST[$nonce_name], $nonce_action)) {
6760 - wp_die(esc_html__('Security check failed.', 'mxchat'));
6761 - }
6762 - }
6763 -
6764 - /**
6765 - * Get embedding API credentials
6766 - */
6767 - private function mxchat_get_embedding_credentials() {
6768 - $embedding_model = $this->options['embedding_model'] ?? 'text-embedding-ada-002';
6769 -
6770 - if (strpos($embedding_model, 'text-embedding-') !== false) {
6771 - return array(
6772 - 'type' => 'openai',
6773 - 'api_key' => $this->options['api_key'] ?? ''
6774 - );
6775 - } elseif (strpos($embedding_model, 'voyage-') !== false) {
6776 - return array(
6777 - 'type' => 'voyage',
6778 - 'api_key' => $this->options['voyage_api_key'] ?? ''
6779 - );
6780 - } elseif (strpos($embedding_model, 'gemini-embedding-') !== false) {
6781 - return array(
6782 - 'type' => 'gemini',
6783 - 'api_key' => $this->options['gemini_api_key'] ?? ''
6784 - );
6785 - }
6786 -
6787 - return array('type' => 'unknown', 'api_key' => '');
6788 - }
6789 -
6790 - /**
6791 - * Log processing errors
6792 - */
6793 - private function mxchat_log_processing_error($operation, $error_message) {
6794 - //error_log("MxChat Knowledge Processing {$operation} Error: " . $error_message);
6795 - }
6796 -
6797 - /**
6798 - * Set admin notice transient
6799 - */
6800 - private function mxchat_set_admin_notice($type, $message) {
6801 - set_transient("mxchat_admin_notice_{$type}", $message, 30);
6802 - }
6803 -
6804 - /**
6805 - * Get Pinecone manager instance for vector operations
6806 - */
6807 - private function mxchat_get_pinecone_manager() {
6808 - return MxChat_Pinecone_Manager::get_instance();
6809 - }
6810 -
6811 -
6812 - // ========================================
6813 -// DATABASE QUEUE TABLE MANAGEMENT
6814 -// ========================================
6815 -
6816 -/**
6817 - * Create queue table on plugin activation
6818 - * Call this from your plugin activation hook
6819 - */
6820 -public function mxchat_create_queue_table() {
6821 - global $wpdb;
6822 -
6823 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6824 - $charset_collate = $wpdb->get_charset_collate();
6825 -
6826 - $sql = "CREATE TABLE IF NOT EXISTS $table_name (
6827 - id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
6828 - queue_id varchar(64) NOT NULL,
6829 - item_type varchar(20) NOT NULL,
6830 - item_data longtext NOT NULL,
6831 - status varchar(20) NOT NULL DEFAULT 'pending',
6832 - bot_id varchar(50) NOT NULL DEFAULT 'default',
6833 - priority int(11) NOT NULL DEFAULT 0,
6834 - attempts int(11) NOT NULL DEFAULT 0,
6835 - max_attempts int(11) NOT NULL DEFAULT 3,
6836 - error_message text DEFAULT NULL,
6837 - created_at datetime NOT NULL,
6838 - started_at datetime DEFAULT NULL,
6839 - completed_at datetime DEFAULT NULL,
6840 - PRIMARY KEY (id),
6841 - KEY queue_id (queue_id),
6842 - KEY status (status),
6843 - KEY item_type (item_type),
6844 - KEY priority (priority)
6845 - ) $charset_collate;";
6846 -
6847 - require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
6848 - dbDelta($sql);
6849 -
6850 - // Also create a meta table for queue metadata
6851 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
6852 -
6853 - $meta_sql = "CREATE TABLE IF NOT EXISTS $meta_table (
6854 - id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
6855 - queue_id varchar(64) NOT NULL,
6856 - meta_key varchar(255) NOT NULL,
6857 - meta_value longtext,
6858 - PRIMARY KEY (id),
6859 - KEY queue_id (queue_id),
6860 - KEY meta_key (meta_key)
6861 - ) $charset_collate;";
6862 -
6863 - dbDelta($meta_sql);
6864 -}
6865 -
6866 -/**
6867 - * Add items to the processing queue
6868 - *
6869 - * @param string $queue_id Unique identifier for this queue batch
6870 - * @param string $item_type Type of item (url, pdf_page)
6871 - * @param array $items Array of items to queue
6872 - * @param string $bot_id Bot ID for processing
6873 - * @return int Number of items queued
6874 - */
6875 -private function mxchat_add_to_queue($queue_id, $item_type, $items, $bot_id = 'default') {
6876 - global $wpdb;
6877 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6878 -
6879 - $queued_count = 0;
6880 - $priority = 0;
6881 -
6882 - foreach ($items as $item) {
6883 - $result = $wpdb->insert(
6884 - $table_name,
6885 - array(
6886 - 'queue_id' => $queue_id,
6887 - 'item_type' => $item_type,
6888 - 'item_data' => wp_json_encode($item),
6889 - 'status' => 'pending',
6890 - 'bot_id' => $bot_id,
6891 - 'priority' => $priority,
6892 - 'attempts' => 0,
6893 - 'max_attempts' => 3,
6894 - 'created_at' => current_time('mysql')
6895 - ),
6896 - array('%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%s')
6897 - );
6898 -
6899 - if ($result) {
6900 - $queued_count++;
6901 - }
6902 -
6903 - $priority++; // Process in order
6904 - }
6905 -
6906 - return $queued_count;
6907 -}
6908 -
6909 -/**
6910 - * Store queue metadata (total counts, source URL, etc.)
6911 - */
6912 -private function mxchat_set_queue_meta($queue_id, $meta_key, $meta_value) {
6913 - global $wpdb;
6914 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
6915 -
6916 - // Check if meta exists
6917 - $existing = $wpdb->get_var($wpdb->prepare(
6918 - "SELECT id FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
6919 - $queue_id,
6920 - $meta_key
6921 - ));
6922 -
6923 - if ($existing) {
6924 - // Update
6925 - $wpdb->update(
6926 - $meta_table,
6927 - array('meta_value' => maybe_serialize($meta_value)),
6928 - array('queue_id' => $queue_id, 'meta_key' => $meta_key),
6929 - array('%s'),
6930 - array('%s', '%s')
6931 - );
6932 - } else {
6933 - // Insert
6934 - $wpdb->insert(
6935 - $meta_table,
6936 - array(
6937 - 'queue_id' => $queue_id,
6938 - 'meta_key' => $meta_key,
6939 - 'meta_value' => maybe_serialize($meta_value)
6940 - ),
6941 - array('%s', '%s', '%s')
6942 - );
6943 - }
6944 -}
6945 -
6946 -/**
6947 - * Get queue metadata
6948 - */
6949 -private function mxchat_get_queue_meta($queue_id, $meta_key) {
6950 - global $wpdb;
6951 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
6952 -
6953 - $value = $wpdb->get_var($wpdb->prepare(
6954 - "SELECT meta_value FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
6955 - $queue_id,
6956 - $meta_key
6957 - ));
6958 -
6959 - return maybe_unserialize($value);
6960 -}
6961 -
6962 -// ========================================
6963 -// AJAX QUEUE PROCESSING HANDLERS
6964 -// ========================================
6965 -
6966 -/**
6967 - * AJAX: Get next item from queue to process
6968 - */
6969 -public function ajax_mxchat_get_next_queue_item() {
6970 - // Verify nonce and permissions
6971 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
6972 -
6973 - if (!current_user_can('manage_options')) {
6974 - wp_send_json_error('Unauthorized access');
6975 - }
6976 -
6977 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
6978 -
6979 - if (empty($queue_id)) {
6980 - wp_send_json_error('Missing queue ID');
6981 - }
6982 -
6983 - global $wpdb;
6984 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
6985 -
6986 - // Get next pending item with retry logic for failed items
6987 - $next_item = $wpdb->get_row($wpdb->prepare(
6988 - "SELECT * FROM $table_name
6989 - WHERE queue_id = %s
6990 - AND status IN ('pending', 'failed')
6991 - AND attempts < max_attempts
6992 - ORDER BY priority ASC, id ASC
6993 - LIMIT 1",
6994 - $queue_id
6995 - ));
6996 -
6997 - if (!$next_item) {
6998 - // No more items - queue complete
6999 - wp_send_json_success(array(
7000 - 'complete' => true,
7001 - 'message' => 'Queue processing complete'
7002 - ));
7003 - }
7004 -
7005 - // Mark item as processing
7006 - $wpdb->update(
7007 - $table_name,
7008 - array(
7009 - 'status' => 'processing',
7010 - 'started_at' => current_time('mysql'),
7011 - 'attempts' => $next_item->attempts + 1
7012 - ),
7013 - array('id' => $next_item->id),
7014 - array('%s', '%s', '%d'),
7015 - array('%d')
7016 - );
7017 -
7018 - wp_send_json_success(array(
7019 - 'complete' => false,
7020 - 'item' => array(
7021 - 'id' => $next_item->id,
7022 - 'type' => $next_item->item_type,
7023 - 'data' => json_decode($next_item->item_data, true),
7024 - 'bot_id' => $next_item->bot_id,
7025 - 'attempt' => $next_item->attempts + 1
7026 - )
7027 - ));
7028 -}
7029 -
7030 -/**
7031 - * AJAX: Process a single queue item
7032 - */
7033 -public function ajax_mxchat_process_queue_item() {
7034 - // Verify nonce and permissions
7035 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7036 -
7037 - if (!current_user_can('manage_options')) {
7038 - wp_send_json_error('Unauthorized access');
7039 - }
7040 -
7041 - $item_id = isset($_POST['item_id']) ? absint($_POST['item_id']) : 0;
7042 - $item_type = isset($_POST['item_type']) ? sanitize_text_field($_POST['item_type']) : '';
7043 - $item_data = isset($_POST['item_data']) ? $_POST['item_data'] : array();
7044 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
7045 -
7046 - if (empty($item_id) || empty($item_type)) {
7047 - wp_send_json_error('Missing item data');
7048 - }
7049 -
7050 - global $wpdb;
7051 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7052 -
7053 - // Process based on item type
7054 - try {
7055 - set_time_limit(60); // Give processing 60 seconds
7056 -
7057 - $result = false;
7058 - $error_message = '';
7059 -
7060 - // Read item directly from DB to get queue_id and preserve special chars in item_data
7061 - // (POST round-trip through JS mangles characters like apostrophes in URLs)
7062 - $db_item = $wpdb->get_row($wpdb->prepare(
7063 - "SELECT queue_id, item_data FROM $table_name WHERE id = %d",
7064 - $item_id
7065 - ));
7066 - $item_queue_id = $db_item ? $db_item->queue_id : '';
7067 - if ($db_item && !empty($db_item->item_data)) {
7068 - $db_data = json_decode($db_item->item_data, true);
7069 - if (is_array($db_data)) {
7070 - $item_data = $db_data;
7071 - }
7072 - }
7073 -
7074 - switch ($item_type) {
7075 - case 'url':
7076 - $result = $this->mxchat_process_queue_url($item_data, $bot_id, $item_queue_id);
7077 - break;
7078 -
7079 - case 'pdf_page':
7080 - $result = $this->mxchat_process_queue_pdf_page($item_data, $bot_id);
7081 - break;
7082 -
7083 - default:
7084 - throw new Exception('Unknown item type: ' . $item_type);
7085 - }
7086 -
7087 - if (is_wp_error($result)) {
7088 - $error_code = $result->get_error_code();
7089 - // Content errors (empty page, sanitization) are permanent — retrying won't help
7090 - $permanent_codes = array('empty_page', 'empty_after_sanitization', 'no_api_key', 'page_not_found');
7091 - if (in_array($error_code, $permanent_codes)) {
7092 - // Mark as permanently failed — set attempts = max_attempts so it won't be retried
7093 - $current_item = $wpdb->get_row($wpdb->prepare(
7094 - "SELECT max_attempts FROM $table_name WHERE id = %d", $item_id
7095 - ));
7096 - $wpdb->update(
7097 - $table_name,
7098 - array(
7099 - 'status' => 'failed',
7100 - 'error_message' => $result->get_error_message(),
7101 - 'attempts' => $current_item ? $current_item->max_attempts : 3
7102 - ),
7103 - array('id' => $item_id),
7104 - array('%s', '%s', '%d'),
7105 - array('%d')
7106 - );
7107 - wp_send_json_error(array(
7108 - 'message' => $result->get_error_message(),
7109 - 'permanent_failure' => true,
7110 - 'item_id' => $item_id
7111 - ));
7112 - return;
7113 - }
7114 - throw new Exception($result->get_error_message());
7115 - }
7116 -
7117 - if ($result === false) {
7118 - throw new Exception('Processing returned false - item may be empty or invalid');
7119 - }
7120 -
7121 - // Mark as completed
7122 - $wpdb->update(
7123 - $table_name,
7124 - array(
7125 - 'status' => 'completed',
7126 - 'completed_at' => current_time('mysql'),
7127 - 'error_message' => null
7128 - ),
7129 - array('id' => $item_id),
7130 - array('%s', '%s', '%s'),
7131 - array('%d')
7132 - );
7133 -
7134 - wp_send_json_success(array(
7135 - 'processed' => true,
7136 - 'item_id' => $item_id,
7137 - 'message' => 'Item processed successfully'
7138 - ));
7139 -
7140 - } catch (Exception $e) {
7141 - $error_message = $e->getMessage();
7142 -
7143 - // Get current attempt count
7144 - $item = $wpdb->get_row($wpdb->prepare(
7145 - "SELECT attempts, max_attempts FROM $table_name WHERE id = %d",
7146 - $item_id
7147 - ));
7148 -
7149 - // Check if we've exhausted retries
7150 - if ($item && $item->attempts >= $item->max_attempts) {
7151 - // Permanently failed
7152 - $wpdb->update(
7153 - $table_name,
7154 - array(
7155 - 'status' => 'failed',
7156 - 'error_message' => $error_message
7157 - ),
7158 - array('id' => $item_id),
7159 - array('%s', '%s'),
7160 - array('%d')
7161 - );
7162 -
7163 - wp_send_json_error(array(
7164 - 'message' => 'Item failed after maximum attempts: ' . $error_message,
7165 - 'permanent_failure' => true,
7166 - 'item_id' => $item_id
7167 - ));
7168 - } else {
7169 - // Mark for retry
7170 - $wpdb->update(
7171 - $table_name,
7172 - array(
7173 - 'status' => 'failed',
7174 - 'error_message' => $error_message
7175 - ),
7176 - array('id' => $item_id),
7177 - array('%s', '%s'),
7178 - array('%d')
7179 - );
7180 -
7181 - wp_send_json_error(array(
7182 - 'message' => 'Item processing failed, will retry: ' . $error_message,
7183 - 'can_retry' => true,
7184 - 'item_id' => $item_id,
7185 - 'attempts' => $item ? $item->attempts : 0
7186 - ));
7187 - }
7188 - }
7189 -}
7190 -
7191 -/**
7192 - * Process a URL from the queue
7193 - */
7194 -private function mxchat_process_queue_url($item_data, $bot_id = 'default', $queue_id = '') {
7195 - $url = isset($item_data['url']) ? $item_data['url'] : '';
7196 -
7197 - if (empty($url)) {
7198 - return new WP_Error('invalid_url', 'URL is empty');
7199 - }
7200 -
7201 - // Get bot-specific API key early (needed for both paths)
7202 - $bot_options = $this->get_bot_options($bot_id);
7203 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
7204 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
7205 -
7206 - if (strpos($selected_model, 'voyage') === 0) {
7207 - $api_key = $options['voyage_api_key'] ?? '';
7208 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
7209 - $api_key = $options['gemini_api_key'] ?? '';
7210 - } else {
7211 - $api_key = $options['api_key'] ?? '';
7212 - }
7213 -
7214 - if (empty($api_key)) {
7215 - return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
7216 - }
7217 -
7218 - // Check if this is a WooCommerce product URL and WooCommerce is active
7219 - $is_product_url = (strpos($url, '/product/') !== false || strpos($url, '/shop/') !== false);
7220 - $content_type = $is_product_url ? 'product' : 'url';
7221 -
7222 - // Try to get WooCommerce product data if it's a product URL
7223 - if ($is_product_url && class_exists('WooCommerce')) {
7224 - $product_content = $this->mxchat_extract_woocommerce_product_content($url);
7225 -
7226 - if (!empty($product_content)) {
7227 - // Successfully extracted WooCommerce product data with pricing
7228 - $result = MxChat_Utils::submit_content_to_db(
7229 - $product_content,
7230 - $url,
7231 - $api_key,
7232 - null,
7233 - $bot_id,
7234 - 'product'
7235 - );
7236 - return $result;
7237 - }
7238 - // If WooCommerce extraction failed, fall through to HTML extraction
7239 - }
7240 -
7241 - // Fetch URL content (fallback for non-products or when WooCommerce extraction fails)
7242 - $is_likely_pdf = (strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?: '', PATHINFO_EXTENSION)) === 'pdf');
7243 - $response = wp_remote_get($url, array(
7244 - 'timeout' => $is_likely_pdf ? 120 : 30,
7245 - 'redirection' => 5,
7246 - 'user-agent' => 'MxChat/1.0'
7247 - ));
7248 -
7249 - if (is_wp_error($response)) {
7250 - return $response;
7251 - }
7252 -
7253 - $response_code = wp_remote_retrieve_response_code($response);
7254 - if ($response_code !== 200) {
7255 - return new WP_Error('http_error', 'HTTP ' . $response_code . ' error for: ' . $url);
7256 - }
7257 -
7258 - // Check if URL is a PDF — expand into per-page queue items using the standard PDF pipeline
7259 - if ($this->mxchat_is_pdf_url($url, $response)) {
7260 - return $this->mxchat_expand_pdf_to_queue($url, $response, $bot_id, $queue_id);
7261 - }
7262 -
7263 - $html = wp_remote_retrieve_body($response);
7264 -
7265 - if (empty($html)) {
7266 - return new WP_Error('empty_response', 'Empty response body');
7267 - }
7268 -
7269 - // Extract and sanitize content
7270 - $content = $this->mxchat_extract_main_content($html);
7271 - $sanitized = $this->mxchat_sanitize_content_for_api($content);
7272 -
7273 - if (empty($sanitized)) {
7274 - // Not an error - just no content found (maybe a redirect or empty page)
7275 - return false;
7276 - }
7277 -
7278 - // Submit to database with content_type
7279 - $result = MxChat_Utils::submit_content_to_db(
7280 - $sanitized,
7281 - $url,
7282 - $api_key,
7283 - null,
7284 - $bot_id,
7285 - $content_type
7286 - );
7287 -
7288 - return $result;
7289 -}
7290 -
7291 -/**
7292 - * Expand a PDF URL into per-page queue items using the standard PDF pipeline.
7293 - * Called when a sitemap URL turns out to be a PDF — downloads, parses page count,
7294 - * and adds pdf_page items to the same queue so they process with full progress tracking.
7295 - */
7296 -private function mxchat_expand_pdf_to_queue($pdf_url, $response, $bot_id = 'default', $queue_id = '') {
7297 - set_time_limit(120); // PDFs need extra time for download + parsing
7298 -
7299 - $upload_dir = wp_upload_dir();
7300 - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf');
7301 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
7302 -
7303 - $response_body = wp_remote_retrieve_body($response);
7304 - if (empty($response_body)) {
7305 - return new WP_Error('empty_pdf', 'Empty PDF response for: ' . $pdf_url);
7306 - }
7307 -
7308 - if (!wp_mkdir_p(dirname($pdf_path))) {
7309 - return new WP_Error('dir_error', 'Failed to create upload directory');
7310 - }
7311 -
7312 - file_put_contents($pdf_path, $response_body);
7313 -
7314 - if (!file_exists($pdf_path)) {
7315 - return new WP_Error('save_error', 'Failed to save PDF file');
7316 - }
7317 -
7318 - try {
7319 - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
7320 -
7321 - if ($total_pages === false || $total_pages < 1) {
7322 - wp_delete_file($pdf_path);
7323 - return new WP_Error('no_pages', 'PDF has no pages: ' . $pdf_url);
7324 - }
7325 -
7326 - // Build per-page items identical to mxchat_handle_pdf_for_knowledge_base
7327 - $pages = array();
7328 - for ($i = 1; $i <= $total_pages; $i++) {
7329 - $pages[] = array(
7330 - 'pdf_path' => $pdf_path,
7331 - 'pdf_url' => $pdf_url,
7332 - 'page_number' => $i,
7333 - 'total_pages' => $total_pages
7334 - );
7335 - }
7336 -
7337 - // Add pdf_page items to the SAME queue so the JS picks them up automatically
7338 - if (!empty($queue_id)) {
7339 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
7340 - } else {
7341 - // Fallback: create a new PDF queue (shouldn't happen in sitemap flow)
7342 - $new_queue_id = 'pdf_' . md5($pdf_url . time());
7343 - $queued_count = $this->mxchat_add_to_queue($new_queue_id, 'pdf_page', $pages, $bot_id);
7344 - $this->mxchat_set_queue_meta($new_queue_id, 'source_url', $pdf_url);
7345 - $this->mxchat_set_queue_meta($new_queue_id, 'queue_type', 'pdf');
7346 - $this->mxchat_set_queue_meta($new_queue_id, 'total_items', $total_pages);
7347 - $this->mxchat_set_queue_meta($new_queue_id, 'bot_id', $bot_id);
7348 - $this->mxchat_set_queue_meta($new_queue_id, 'pdf_path', $pdf_path);
7349 - $this->mxchat_set_queue_meta($new_queue_id, 'created_at', current_time('mysql'));
7350 - }
7351 -
7352 - if ($queued_count === 0) {
7353 - wp_delete_file($pdf_path);
7354 - return new WP_Error('queue_error', 'Failed to add PDF pages to queue');
7355 - }
7356 -
7357 - // Return true so the original URL item is marked complete
7358 - // The new pdf_page items will be processed in subsequent batches
7359 - return true;
7360 -
7361 - } catch (Exception $e) {
7362 - if (file_exists($pdf_path)) {
7363 - wp_delete_file($pdf_path);
7364 - }
7365 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
7366 - }
7367 -}
7368 -
7369 -/**
7370 - * Legacy: Process a PDF URL inline during sitemap queue processing.
7371 - * @deprecated Use mxchat_expand_pdf_to_queue instead — kept for reference only.
7372 - */
7373 -private function mxchat_process_pdf_url_inline($pdf_url, $response, $api_key, $bot_id = 'default') {
7374 - set_time_limit(120); // PDFs need more time — downloading + parsing all pages
7375 -
7376 - $upload_dir = wp_upload_dir();
7377 - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf');
7378 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
7379 -
7380 - $response_body = wp_remote_retrieve_body($response);
7381 - if (empty($response_body)) {
7382 - return new WP_Error('empty_pdf', 'Empty PDF response');
7383 - }
7384 -
7385 - if (!wp_mkdir_p(dirname($pdf_path))) {
7386 - return new WP_Error('dir_error', 'Failed to create upload directory');
7387 - }
7388 -
7389 - file_put_contents($pdf_path, $response_body);
7390 -
7391 - if (!file_exists($pdf_path)) {
7392 - return new WP_Error('save_error', 'Failed to save PDF file');
7393 - }
7394 -
7395 - try {
7396 - mxchat_load_pdf_parser();
7397 - $parser = new \Smalot\PdfParser\Parser();
7398 - $pdf = $parser->parseFile($pdf_path);
7399 - $pages = $pdf->getPages();
7400 - $total_pages = count($pages);
7401 -
7402 - if ($total_pages < 1) {
7403 - wp_delete_file($pdf_path);
7404 - return new WP_Error('no_pages', 'PDF has no pages');
7405 - }
7406 -
7407 - $processed = 0;
7408 - $skipped_pages = array();
7409 -
7410 - for ($i = 0; $i < $total_pages; $i++) {
7411 - $page_num = $i + 1;
7412 - $text = $pages[$i]->getText();
7413 - if (empty($text)) {
7414 - $skipped_pages[] = 'Page ' . $page_num . ': No text could be extracted — page may contain only images, links, or non-standard encoding';
7415 - continue;
7416 - }
7417 -
7418 - $sanitized = $this->mxchat_sanitize_content_for_api($text);
7419 - if (empty($sanitized)) {
7420 - $skipped_pages[] = 'Page ' . $page_num . ': Text was extracted but contained only special characters, control codes, or unsupported content';
7421 - continue;
7422 - }
7423 -
7424 - $metadata = array(
7425 - 'document_type' => 'pdf',
7426 - 'total_pages' => $total_pages,
7427 - 'current_page' => $page_num,
7428 - 'source_url' => $pdf_url,
7429 - );
7430 -
7431 - $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized;
7432 - $page_url = esc_url($pdf_url . '#page=' . $page_num);
7433 -
7434 - MxChat_Utils::submit_content_to_db(
7435 - $content_with_metadata,
7436 - $page_url,
7437 - $api_key,
7438 - null,
7439 - $bot_id,
7440 - 'pdf'
7441 - );
7442 -
7443 - $processed++;
7444 - }
7445 -
7446 - // Clean up the temp PDF file
7447 - wp_delete_file($pdf_path);
7448 -
7449 - if (!empty($skipped_pages)) {
7450 - error_log('MxChat PDF: Skipped ' . count($skipped_pages) . ' of ' . $total_pages . ' pages: ' . implode('; ', $skipped_pages));
7451 - }
7452 -
7453 - return $processed > 0 ? true : false;
7454 -
7455 - } catch (Exception $e) {
7456 - if (file_exists($pdf_path)) {
7457 - wp_delete_file($pdf_path);
7458 - }
7459 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
7460 - }
7461 -}
7462 -
7463 -/**
7464 - * Extract WooCommerce product content including pricing
7465 - *
7466 - * @param string $url The product URL
7467 - * @return string|false Product content with pricing, or false if not found
7468 - */
7469 -private function mxchat_extract_woocommerce_product_content($url) {
7470 - // Try to get product ID from URL
7471 - $product_id = url_to_postid($url);
7472 -
7473 - // If url_to_postid fails, try to extract from URL pattern
7474 - if (!$product_id) {
7475 - $product_slug = '';
7476 -
7477 - // Handle pretty permalinks: /product/product-name/
7478 - if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
7479 - $product_slug = $matches[1];
7480 - }
7481 -
7482 - if (!empty($product_slug)) {
7483 - $product_post = get_page_by_path($product_slug, OBJECT, 'product');
7484 - if ($product_post) {
7485 - $product_id = $product_post->ID;
7486 - }
7487 - }
7488 - }
7489 -
7490 - if (!$product_id) {
7491 - return false;
7492 - }
7493 -
7494 - // Get WooCommerce product object
7495 - $product = wc_get_product($product_id);
7496 -
7497 - if (!$product) {
7498 - return false;
7499 - }
7500 -
7501 - // Build product content with pricing (similar to mxchat_store_product_embedding)
7502 - $title = $product->get_name();
7503 - $description = $product->get_description();
7504 - $short_description = $product->get_short_description();
7505 - $sku = $product->get_sku();
7506 -
7507 - // Get pricing information
7508 - $regular_price = $product->get_regular_price();
7509 - $sale_price = $product->get_sale_price();
7510 - $price = $product->get_price(); // Current active price
7511 -
7512 - // Get currency symbol
7513 - $currency_symbol = get_woocommerce_currency_symbol();
7514 -
7515 - // Format content
7516 - $content = $title . "\n\n";
7517 -
7518 - if (!empty($short_description)) {
7519 - $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
7520 - }
7521 -
7522 - if (!empty($description)) {
7523 - $content .= wp_strip_all_tags($description) . "\n\n";
7524 - }
7525 -
7526 - // Add pricing information
7527 - if (!empty($regular_price)) {
7528 - $content .= "Price: " . $currency_symbol . $regular_price . "\n";
7529 - } elseif (!empty($price)) {
7530 - $content .= "Price: " . $currency_symbol . $price . "\n";
7531 - }
7532 -
7533 - if (!empty($sale_price) && $sale_price !== $regular_price) {
7534 - $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
7535 - }
7536 -
7537 - // Handle variable products - show price range
7538 - if ($product->is_type('variable')) {
7539 - $min_price = $product->get_variation_price('min');
7540 - $max_price = $product->get_variation_price('max');
7541 - if ($min_price !== $max_price) {
7542 - $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
7543 - }
7544 - }
7545 -
7546 - if (!empty($sku)) {
7547 - $content .= "SKU: " . $sku . "\n";
7548 - }
7549 -
7550 - // Get product categories
7551 - $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
7552 - if (!empty($categories) && !is_wp_error($categories)) {
7553 - $content .= "Categories: " . implode(', ', $categories) . "\n";
7554 - }
7555 -
7556 - // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
7557 - $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
7558 - if (!empty($custom_tabs) && is_array($custom_tabs)) {
7559 - foreach ($custom_tabs as $tab) {
7560 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
7561 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
7562 -
7563 - if (!empty($tab_title) && !empty($tab_content)) {
7564 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
7565 - }
7566 - }
7567 - }
7568 -
7569 - // Also check for reusable/saved tabs applied to this product
7570 - $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
7571 - if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
7572 - $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
7573 - if (!empty($saved_tabs) && is_array($saved_tabs)) {
7574 - foreach ($applied_saved_tabs as $saved_tab_id) {
7575 - if (isset($saved_tabs[$saved_tab_id])) {
7576 - $tab = $saved_tabs[$saved_tab_id];
7577 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
7578 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
7579 -
7580 - if (!empty($tab_title) && !empty($tab_content)) {
7581 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
7582 - }
7583 - }
7584 - }
7585 - }
7586 - }
7587 -
7588 - return $this->mxchat_sanitize_content_for_api($content);
7589 -}
7590 -
7591 -/**
7592 - * Process a PDF page from the queue
7593 - */
7594 -private function mxchat_process_queue_pdf_page($item_data, $bot_id = 'default') {
7595 - $pdf_path = isset($item_data['pdf_path']) ? $item_data['pdf_path'] : '';
7596 - $pdf_url = isset($item_data['pdf_url']) ? $item_data['pdf_url'] : '';
7597 - $page_number = isset($item_data['page_number']) ? absint($item_data['page_number']) : 0;
7598 - $total_pages = isset($item_data['total_pages']) ? absint($item_data['total_pages']) : 0;
7599 -
7600 - if (empty($pdf_path) || !file_exists($pdf_path)) {
7601 - return new WP_Error('pdf_not_found', 'PDF file not found: ' . $pdf_path);
7602 - }
7603 -
7604 - if ($page_number < 1) {
7605 - return new WP_Error('invalid_page', 'Invalid page number');
7606 - }
7607 -
7608 - try {
7609 - mxchat_load_pdf_parser();
7610 - $parser = new \Smalot\PdfParser\Parser();
7611 - $pdf = $parser->parseFile($pdf_path);
7612 - $pages = $pdf->getPages();
7613 -
7614 - if (!isset($pages[$page_number - 1])) {
7615 - return new WP_Error('page_not_found', 'Page ' . $page_number . ' not found in PDF');
7616 - }
7617 -
7618 - $text = $pages[$page_number - 1]->getText();
7619 -
7620 - if (empty($text)) {
7621 - return new WP_Error('empty_page', 'Page ' . $page_number . ': No text could be extracted — page may contain only images, links, or non-standard encoding');
7622 - }
7623 -
7624 - $sanitized = $this->mxchat_sanitize_content_for_api($text);
7625 -
7626 - if (empty($sanitized)) {
7627 - 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');
7628 - }
7629 -
7630 - // Create metadata
7631 - $metadata = array(
7632 - 'document_type' => 'pdf',
7633 - 'total_pages' => $total_pages,
7634 - 'current_page' => $page_number,
7635 - 'source_url' => $pdf_url
7636 - );
7637 -
7638 - $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized;
7639 - $page_url = esc_url($pdf_url . "#page=" . $page_number);
7640 -
7641 - // Get bot-specific API key
7642 - $bot_options = $this->get_bot_options($bot_id);
7643 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
7644 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
7645 -
7646 - if (strpos($selected_model, 'voyage') === 0) {
7647 - $api_key = $options['voyage_api_key'] ?? '';
7648 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
7649 - $api_key = $options['gemini_api_key'] ?? '';
7650 - } else {
7651 - $api_key = $options['api_key'] ?? '';
7652 - }
7653 -
7654 - if (empty($api_key)) {
7655 - return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
7656 - }
7657 -
7658 - // Submit to database - UPDATED 2.5.6: Added content_type 'pdf'
7659 - $result = MxChat_Utils::submit_content_to_db(
7660 - $content_with_metadata,
7661 - $page_url,
7662 - $api_key,
7663 - null,
7664 - $bot_id,
7665 - 'pdf'
7666 - );
7667 -
7668 - return $result;
7669 -
7670 - } catch (Exception $e) {
7671 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
7672 - }
7673 -}
7674 -
7675 -/**
7676 - * AJAX: Get queue processing status
7677 - */
7678 -public function ajax_mxchat_get_queue_status() {
7679 - // Verify nonce and permissions
7680 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7681 -
7682 - if (!current_user_can('manage_options')) {
7683 - wp_send_json_error('Unauthorized access');
7684 - }
7685 -
7686 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
7687 -
7688 - if (empty($queue_id)) {
7689 - wp_send_json_error('Missing queue ID');
7690 - }
7691 -
7692 - global $wpdb;
7693 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7694 -
7695 - // Get counts by status
7696 - $counts = $wpdb->get_results($wpdb->prepare(
7697 - "SELECT status, COUNT(*) as count
7698 - FROM $table_name
7699 - WHERE queue_id = %s
7700 - GROUP BY status",
7701 - $queue_id
7702 - ), OBJECT_K);
7703 -
7704 - $total = 0;
7705 - $completed = 0;
7706 - $failed = 0;
7707 - $processing = 0;
7708 - $pending = 0;
7709 -
7710 - foreach ($counts as $status => $data) {
7711 - $count = absint($data->count);
7712 - $total += $count;
7713 -
7714 - switch ($status) {
7715 - case 'completed':
7716 - $completed = $count;
7717 - break;
7718 - case 'failed':
7719 - $failed = $count;
7720 - break;
7721 - case 'processing':
7722 - $processing = $count;
7723 - break;
7724 - case 'pending':
7725 - $pending = $count;
7726 - break;
7727 - }
7728 - }
7729 -
7730 - // Calculate percentage
7731 - $percentage = $total > 0 ? round((($completed + $failed) / $total) * 100) : 0;
7732 -
7733 - // Get failed items details (include all failed items, not just those that exhausted retries)
7734 - $failed_items = array();
7735 - if ($failed > 0) {
7736 - $failed_items = $wpdb->get_results($wpdb->prepare(
7737 - "SELECT item_type, item_data, error_message, attempts
7738 - FROM $table_name
7739 - WHERE queue_id = %s
7740 - AND status = 'failed'
7741 - ORDER BY id DESC
7742 - LIMIT 50",
7743 - $queue_id
7744 - ));
7745 - }
7746 -
7747 - // Get queue metadata
7748 - $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
7749 - $queue_type = $this->mxchat_get_queue_meta($queue_id, 'queue_type');
7750 -
7751 - // Determine if queue is complete
7752 - $is_complete = ($pending === 0 && $processing === 0);
7753 -
7754 - wp_send_json_success(array(
7755 - 'queue_id' => $queue_id,
7756 - 'queue_type' => $queue_type,
7757 - 'source_url' => $source_url,
7758 - 'total' => $total,
7759 - 'completed' => $completed,
7760 - 'failed' => $failed,
7761 - 'processing' => $processing,
7762 - 'pending' => $pending,
7763 - 'percentage' => $percentage,
7764 - 'is_complete' => $is_complete,
7765 - 'failed_items' => $failed_items,
7766 - 'status' => $is_complete ? 'complete' : 'processing'
7767 - ));
7768 -}
7769 -
7770 -/**
7771 - * AJAX: Clear completed queue
7772 - */
7773 -public function ajax_mxchat_clear_queue() {
7774 - // Verify nonce and permissions
7775 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7776 -
7777 - if (!current_user_can('manage_options')) {
7778 - wp_send_json_error('Unauthorized access');
7779 - }
7780 -
7781 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
7782 -
7783 - if (empty($queue_id)) {
7784 - wp_send_json_error('Missing queue ID');
7785 - }
7786 -
7787 - global $wpdb;
7788 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7789 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
7790 -
7791 - // Delete queue items
7792 - $wpdb->delete(
7793 - $table_name,
7794 - array('queue_id' => $queue_id),
7795 - array('%s')
7796 - );
7797 -
7798 - // Delete queue metadata
7799 - $wpdb->delete(
7800 - $meta_table,
7801 - array('queue_id' => $queue_id),
7802 - array('%s')
7803 - );
7804 -
7805 - wp_send_json_success(array(
7806 - 'message' => 'Queue cleared successfully'
7807 - ));
7808 -}
7809 -
7810 -/**
7811 - * AJAX: Retry failed items in queue
7812 - */
7813 -public function ajax_mxchat_retry_failed() {
7814 - // Verify nonce and permissions
7815 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7816 -
7817 - if (!current_user_can('manage_options')) {
7818 - wp_send_json_error('Unauthorized access');
7819 - }
7820 -
7821 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
7822 -
7823 - if (empty($queue_id)) {
7824 - wp_send_json_error('Missing queue ID');
7825 - }
7826 -
7827 - global $wpdb;
7828 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7829 -
7830 - // Reset failed items to pending and reset attempt count
7831 - $updated = $wpdb->update(
7832 - $table_name,
7833 - array(
7834 - 'status' => 'pending',
7835 - 'attempts' => 0,
7836 - 'error_message' => null
7837 - ),
7838 - array(
7839 - 'queue_id' => $queue_id,
7840 - 'status' => 'failed'
7841 - ),
7842 - array('%s', '%d', '%s'),
7843 - array('%s', '%s')
7844 - );
7845 -
7846 - wp_send_json_success(array(
7847 - 'message' => 'Reset ' . $updated . ' failed items for retry',
7848 - 'reset_count' => $updated
7849 - ));
7850 -}
7851 -
7852 -
7853 -public function ajax_mxchat_mark_queue_complete() {
7854 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7855 -
7856 - if (!current_user_can('manage_options')) {
7857 - wp_send_json_error('Unauthorized access');
7858 - }
7859 -
7860 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
7861 -
7862 - if (empty($queue_id)) {
7863 - wp_send_json_error('Missing queue ID');
7864 - }
7865 -
7866 - // Clear active queue transients
7867 - if (strpos($queue_id, 'sitemap_') === 0) {
7868 - delete_transient('mxchat_active_queue_sitemap');
7869 - } else if (strpos($queue_id, 'pdf_') === 0) {
7870 - delete_transient('mxchat_active_queue_pdf');
7871 - }
7872 -
7873 - wp_send_json_success(array('message' => 'Queue marked as complete'));
7874 -}
7875 -
7876 -
7877 - // ========================================
7878 - // STATIC ACCESS METHODS
7879 - // ========================================
7880 -
7881 - /**
7882 - * Get singleton instance
7883 - */
7884 - public static function get_instance() {
7885 - static $instance = null;
7886 - if ($instance === null) {
7887 - $instance = new self();
7888 - }
7889 - return $instance;
7890 - }
7891 -}
7892 -
7893 -// Initialize the Knowledge manager
1 +<?php
2 +/**
3 + * File: admin/class-knowledge-manager.php
4 + *
5 + * Handles all knowledge base content processing for MxChat
6 + * Including PDF, sitemap, content processing, and WordPress post management
7 + */
8 +if (!defined('ABSPATH')) {
9 + exit; // Exit if accessed directly
10 +}
11 +
12 +class MxChat_Knowledge_Manager {
13 +
14 + private $options;
15 +
16 + /**
17 + * Constructor - Register hooks for content processing
18 + */
19 +public function __construct() {
20 + $this->options = get_option('mxchat_options', array());
21 + $this->mxchat_init_hooks();
22 +
23 + $this->mxchat_init_role_hooks();
24 +}
25 +
26 +/**
27 + * Initialize WordPress hooks for content processing
28 + *
29 + */
30 +private function mxchat_init_hooks() {
31 + // Admin post handlers for form submissions
32 + add_action('admin_post_mxchat_submit_content', array($this, 'mxchat_handle_content_submission'));
33 + add_action('admin_post_mxchat_submit_sitemap', array($this, 'mxchat_handle_sitemap_submission'));
34 + add_action('admin_post_mxchat_stop_processing', array($this, 'mxchat_stop_processing'));
35 +
36 + // AJAX handlers for real-time processing and status updates
37 + add_action('wp_ajax_mxchat_get_status_updates', array($this, 'mxchat_ajax_get_status_updates'));
38 + add_action('wp_ajax_mxchat_dismiss_completed_status', array($this, 'mxchat_ajax_dismiss_completed_status'));
39 + add_action('wp_ajax_mxchat_get_content_list', array($this, 'ajax_mxchat_get_content_list'));
40 + add_action('wp_ajax_mxchat_process_selected_content', array($this, 'ajax_mxchat_process_selected_content'));
41 + add_action('wp_ajax_mxchat_save_inline_prompt', array($this, 'mxchat_save_inline_prompt'));
42 + add_action('admin_post_mxchat_delete_pinecone_prompt', array($this, 'mxchat_handle_pinecone_prompt_delete'));
43 + add_action('wp_ajax_mxchat_delete_pinecone_prompt', array($this, 'ajax_mxchat_delete_pinecone_prompt'));
44 + add_action('wp_ajax_mxchat_update_role_restriction', array($this, 'ajax_mxchat_update_role_restriction'));
45 +
46 + // Queue-based processing AJAX handlers
47 + add_action('wp_ajax_mxchat_get_next_queue_item', array($this, 'ajax_mxchat_get_next_queue_item'));
48 + add_action('wp_ajax_mxchat_process_queue_item', array($this, 'ajax_mxchat_process_queue_item'));
49 + add_action('wp_ajax_mxchat_get_queue_status', array($this, 'ajax_mxchat_get_queue_status'));
50 + add_action('wp_ajax_mxchat_clear_queue', array($this, 'ajax_mxchat_clear_queue'));
51 + add_action('wp_ajax_mxchat_retry_failed', array($this, 'ajax_mxchat_retry_failed'));
52 +
53 + // Hook for content deletion
54 + add_action('mxchat_delete_content', array($this, 'mxchat_delete_from_pinecone_by_url'), 10, 1);
55 +
56 + // WordPress post management hooks
57 + add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
58 + add_action('post_updated', array($this, 'mxchat_handle_post_update'), 10, 3);
59 + add_action('before_delete_post', array($this, 'mxchat_handle_post_delete'));
60 + add_action('wp_trash_post', array($this, 'mxchat_handle_post_delete'));
61 +
62 + add_action('wp_ajax_mxchat_mark_queue_complete', array($this, 'ajax_mxchat_mark_queue_complete'));
63 +
64 + // WooCommerce product hooks (if WooCommerce is active)
65 + if (class_exists('WooCommerce')) {
66 + add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
67 + add_action('save_post_product', array($this, 'mxchat_handle_product_change'), 10, 3);
68 + add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
69 + add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
70 + }
71 +}
72 +
73 + /**
74 + * Get current options (refreshed)
75 + */
76 + private function mxchat_get_options() {
77 + if (empty($this->options)) {
78 + $this->options = get_option('mxchat_options', array());
79 + }
80 + return $this->options;
81 + }
82 +
83 +
84 + // ========================================
85 + // MAIN CONTENT SUBMISSION HANDLERS
86 + // ========================================
87 +
88 +public function mxchat_handle_content_submission() {
89 + // Check if the form was submitted and the user has permission.
90 + if (!isset($_POST['submit_content']) || !current_user_can('manage_options')) {
91 + return;
92 + }
93 +
94 + // Verify the nonce.
95 + $nonce = isset($_POST['mxchat_submit_content_nonce']) ? sanitize_text_field(wp_unslash($_POST['mxchat_submit_content_nonce'])) : '';
96 + if (!wp_verify_nonce($nonce, 'mxchat_submit_content_action')) {
97 + wp_die(esc_html__('Nonce verification failed.', 'mxchat'));
98 + }
99 +
100 + // Sanitize the inputs.
101 + // Use wp_kses_post to allow safe HTML and wp_unslash to remove WordPress added slashes
102 + $article_content = wp_kses_post(wp_unslash($_POST['article_content']));
103 + $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
104 +
105 + // Get bot_id from form submission
106 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
107 +
108 + // Get bot-specific options and API key
109 + $bot_options = $this->get_bot_options($bot_id);
110 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
111 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
112 +
113 + if (strpos($selected_model, 'voyage') === 0) {
114 + $api_key = $options['voyage_api_key'] ?? '';
115 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
116 + $api_key = $options['gemini_api_key'] ?? '';
117 + } else {
118 + $api_key = $options['api_key'] ?? '';
119 + }
120 +
121 + if (empty($api_key)) {
122 + set_transient('mxchat_admin_notice_error',
123 + esc_html__('API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
124 + 30
125 + );
126 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
127 + exit;
128 + }
129 +
130 + // Use centralized utility function with bot_id
131 + $result = MxChat_Utils::submit_content_to_db($article_content, $article_url, $api_key, null, $bot_id);
132 +
133 + if (is_wp_error($result)) {
134 + set_transient('mxchat_admin_notice_error',
135 + esc_html__('Error storing content: ', 'mxchat') . $result->get_error_message(),
136 + 30
137 + );
138 + } else {
139 + set_transient('mxchat_admin_notice_success',
140 + esc_html__('Content successfully submitted!', 'mxchat'),
141 + 30
142 + );
143 + }
144 +
145 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
146 + exit;
147 +}
148 +
149 +public function mxchat_is_pdf_url($url, $response) {
150 + $content_type = wp_remote_retrieve_header($response, 'content-type');
151 + $file_extension = strtolower(pathinfo($url, PATHINFO_EXTENSION));
152 +
153 + return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf';
154 +}
155 +
156 +
157 +public function mxchat_handle_pdf_for_knowledge_base($pdf_url, $response, $bot_id = 'default') {
158 + if (!current_user_can('manage_options')) {
159 + return false;
160 + }
161 +
162 + $pdf_url = esc_url_raw($pdf_url);
163 + $upload_dir = wp_upload_dir();
164 +
165 + if (isset($upload_dir['error']) && $upload_dir['error'] !== false) {
166 + return false;
167 + }
168 +
169 + $pdf_filename = sanitize_file_name('mxchat_kb_' . time() . '.pdf');
170 + $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
171 +
172 + $response_body = wp_remote_retrieve_body($response);
173 + if (empty($response_body)) {
174 + return false;
175 + }
176 +
177 + if (!wp_mkdir_p(dirname($pdf_path))) {
178 + return false;
179 + }
180 +
181 + try {
182 + file_put_contents($pdf_path, $response_body);
183 +
184 + if (!file_exists($pdf_path)) {
185 + throw new Exception(__('Failed to save PDF file', 'mxchat'));
186 + }
187 +
188 + $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
189 +
190 + if ($total_pages === false || $total_pages < 1) {
191 + throw new Exception(__('Invalid PDF: Unable to parse or no pages found', 'mxchat'));
192 + }
193 +
194 + // Create unique queue ID
195 + $queue_id = 'pdf_' . md5($pdf_url . time());
196 +
197 + // Create array of pages to process
198 + $pages = array();
199 + for ($i = 1; $i <= $total_pages; $i++) {
200 + $pages[] = array(
201 + 'pdf_path' => $pdf_path,
202 + 'pdf_url' => $pdf_url,
203 + 'page_number' => $i,
204 + 'total_pages' => $total_pages
205 + );
206 + }
207 +
208 + // Add pages to queue
209 + $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
210 +
211 + if ($queued_count === 0) {
212 + wp_delete_file($pdf_path);
213 + throw new Exception(__('Failed to add PDF pages to processing queue', 'mxchat'));
214 + }
215 +
216 + // Store queue metadata
217 + $this->mxchat_set_queue_meta($queue_id, 'source_url', $pdf_url);
218 + $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'pdf');
219 + $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_pages);
220 + $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
221 + $this->mxchat_set_queue_meta($queue_id, 'pdf_path', $pdf_path);
222 + $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
223 +
224 + // Store queue ID in transient for status tracking
225 + set_transient('mxchat_active_queue_pdf', $queue_id, DAY_IN_SECONDS);
226 + set_transient('mxchat_last_pdf_url', $pdf_url, DAY_IN_SECONDS);
227 +
228 + return 'queued';
229 +
230 + } catch (Exception $e) {
231 + if (file_exists($pdf_path)) {
232 + wp_delete_file($pdf_path);
233 + }
234 + return $e->getMessage();
235 + }
236 +}
237 +
238 +/**
239 + * Validate PDF and count pages with multiple parser attempts
240 + */
241 +private function mxchat_validate_and_count_pdf_pages($pdf_path) {
242 + // Method 1: Try with Smalot PDF Parser (your current method)
243 + try {
244 + $parser = new \Smalot\PdfParser\Parser();
245 + $pdf = $parser->parseFile($pdf_path);
246 + $pages = $pdf->getPages();
247 + $page_count = count($pages);
248 +
249 + if ($page_count > 0) {
250 + //error_log('PDF parsed successfully with Smalot parser: ' . $page_count . ' pages');
251 + return $page_count;
252 + }
253 + } catch (Exception $e) {
254 + //error_log('Smalot PDF parser failed: ' . $e->getMessage());
255 + }
256 +
257 + // Method 2: Try with pdfinfo command (if available)
258 + if (function_exists('shell_exec') && !$this->mxchat_is_shell_disabled()) {
259 + try {
260 + $command = 'pdfinfo ' . escapeshellarg($pdf_path) . ' 2>&1';
261 + $output = shell_exec($command);
262 +
263 + if ($output && preg_match('/Pages:\s*(\d+)/', $output, $matches)) {
264 + $page_count = intval($matches[1]);
265 + if ($page_count > 0) {
266 + //error_log('PDF parsed successfully with pdfinfo: ' . $page_count . ' pages');
267 + return $page_count;
268 + }
269 + }
270 + } catch (Exception $e) {
271 + //error_log('pdfinfo command failed: ' . $e->getMessage());
272 + }
273 + }
274 +
275 + // Method 3: Try to repair PDF and parse again
276 + try {
277 + $repaired_path = $this->mxchat_attempt_pdf_repair($pdf_path);
278 + if ($repaired_path && $repaired_path !== $pdf_path) {
279 + $parser = new \Smalot\PdfParser\Parser();
280 + $pdf = $parser->parseFile($repaired_path);
281 + $pages = $pdf->getPages();
282 + $page_count = count($pages);
283 +
284 + if ($page_count > 0) {
285 + // Replace original with repaired version
286 + copy($repaired_path, $pdf_path);
287 + unlink($repaired_path);
288 + //error_log('PDF repaired and parsed successfully: ' . $page_count . ' pages');
289 + return $page_count;
290 + }
291 +
292 + // Clean up repaired file if it didn't work
293 + unlink($repaired_path);
294 + }
295 + } catch (Exception $e) {
296 + //error_log('PDF repair attempt failed: ' . $e->getMessage());
297 + }
298 +
299 + // Method 4: Manual PDF structure analysis (basic page count)
300 + try {
301 + $page_count = $this->mxchat_manual_pdf_page_count($pdf_path);
302 + if ($page_count > 0) {
303 + //error_log('PDF page count determined manually: ' . $page_count . ' pages');
304 + return $page_count;
305 + }
306 + } catch (Exception $e) {
307 + //error_log('Manual PDF analysis failed: ' . $e->getMessage());
308 + }
309 +
310 + //error_log('All PDF parsing methods failed for: ' . $pdf_path);
311 + return false;
312 +}
313 +
314 +/**
315 + * Check if shell_exec is disabled
316 + */
317 +private function mxchat_is_shell_disabled() {
318 + $disabled = explode(',', ini_get('disable_functions'));
319 + return in_array('shell_exec', $disabled);
320 +}
321 +
322 +/**
323 + * Attempt to repair PDF using basic methods
324 + */
325 +private function mxchat_attempt_pdf_repair($pdf_path) {
326 + try {
327 + $content = file_get_contents($pdf_path);
328 + if (!$content) {
329 + return false;
330 + }
331 +
332 + // Check if PDF starts with proper header
333 + if (substr($content, 0, 4) !== '%PDF') {
334 + // Try to find PDF header in the content
335 + $header_pos = strpos($content, '%PDF');
336 + if ($header_pos !== false && $header_pos < 1024) {
337 + // Remove junk before PDF header
338 + $content = substr($content, $header_pos);
339 + $repaired_path = $pdf_path . '.repaired';
340 + file_put_contents($repaired_path, $content);
341 + return $repaired_path;
342 + }
343 + }
344 +
345 + // Check for EOF marker
346 + $content = rtrim($content);
347 + if (!preg_match('/%%EOF\s*$/', $content)) {
348 + // Add EOF marker if missing
349 + $content .= "\n%%EOF";
350 + $repaired_path = $pdf_path . '.repaired';
351 + file_put_contents($repaired_path, $content);
352 + return $repaired_path;
353 + }
354 +
355 + } catch (Exception $e) {
356 + //error_log('PDF repair error: ' . $e->getMessage());
357 + }
358 +
359 + return false;
360 +}
361 +
362 +/**
363 + * Manual PDF page counting by analyzing PDF structure
364 + */
365 +private function mxchat_manual_pdf_page_count($pdf_path) {
366 + try {
367 + $content = file_get_contents($pdf_path);
368 + if (!$content) {
369 + return 0;
370 + }
371 +
372 + // Method 1: Count /Type /Page objects
373 + $page_count = preg_match_all('/\/Type\s*\/Page[^s]/', $content);
374 + if ($page_count > 0) {
375 + return $page_count;
376 + }
377 +
378 + // Method 2: Look for /Count in pages object
379 + if (preg_match('/\/Type\s*\/Pages.*?\/Count\s+(\d+)/', $content, $matches)) {
380 + return intval($matches[1]);
381 + }
382 +
383 + // Method 3: Count page references
384 + $page_count = preg_match_all('/\d+\s+0\s+obj\s*<<[^>]*\/Type\s*\/Page/', $content);
385 + if ($page_count > 0) {
386 + return $page_count;
387 + }
388 +
389 + } catch (Exception $e) {
390 + //error_log('Manual PDF analysis error: ' . $e->getMessage());
391 + }
392 +
393 + return 0;
394 +}
395 +
396 +
397 +public function mxchat_save_inline_prompt() {
398 + // DEBUG: Log what we're receiving
399 + //error_log('=== MXCHAT DEBUG ===');
400 + //error_log('POST data: ' . print_r($_POST, true));
401 + //error_log('Nonce from POST: ' . ($_POST['_ajax_nonce'] ?? 'NOT FOUND'));
402 +
403 + // Check for nonce security
404 + check_ajax_referer('mxchat_save_inline_nonce', '_ajax_nonce');
405 +
406 + // If we get here, nonce passed
407 + //error_log('Nonce verification PASSED');
408 +
409 + // Verify permissions
410 + if (!current_user_can('manage_options')) {
411 + wp_send_json_error(esc_html__('Permission denied.', 'mxchat'));
412 + return;
413 + }
414 +
415 + global $wpdb;
416 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
417 +
418 + // Validate and sanitize input data
419 + $prompt_id = isset($_POST['id']) ? absint($_POST['id']) : 0;
420 + $article_content = isset($_POST['article_content']) ? wp_kses_post(wp_unslash($_POST['article_content'])) : '';
421 + $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
422 +
423 + if ($prompt_id > 0 && !empty($article_content)) {
424 + // Re-generate the embedding vector for the updated content
425 + $embedding_vector = $this->mxchat_generate_embedding($article_content);
426 + if (is_array($embedding_vector)) {
427 + // Serialize the embedding vector before storing it
428 + $embedding_vector_serialized = serialize($embedding_vector);
429 + // Update the prompt in the database
430 + $updated = $wpdb->update(
431 + $table_name,
432 + array(
433 + 'article_content' => $article_content,
434 + 'embedding_vector' => $embedding_vector_serialized,
435 + 'source_url' => $article_url,
436 + ),
437 + array('id' => $prompt_id),
438 + array('%s', '%s', '%s'),
439 + array('%d')
440 + );
441 + if ($updated !== false) {
442 + wp_send_json_success();
443 + } else {
444 + wp_send_json_error(esc_html__('Database update failed.', 'mxchat'));
445 + }
446 + } else {
447 + wp_send_json_error(esc_html__('Embedding generation failed.', 'mxchat'));
448 + }
449 + } else {
450 + wp_send_json_error(esc_html__('Invalid data.', 'mxchat'));
451 + }
452 +}
453 +
454 +
455 +public function mxchat_get_pdf_processing_status($pdf_url) {
456 + $pdf_url = esc_url_raw($pdf_url);
457 + $status = get_transient(sanitize_key('mxchat_pdf_status_' . md5($pdf_url)));
458 +
459 + if (!$status || !is_array($status)) {
460 + return false;
461 + }
462 +
463 + // Check for stalled processing (no updates for 5 minutes)
464 + if ($status['status'] === 'processing' && (time() - absint($status['last_update'])) > 300) {
465 + $status['status'] = 'error';
466 + $status['error'] = __('PDF processing appears to be stalled. No updates for over 5 minutes.', 'mxchat');
467 +
468 + // Save the updated status
469 + set_transient(
470 + sanitize_key('mxchat_pdf_status_' . md5($pdf_url)),
471 + array_map('sanitize_text_field', $status),
472 + DAY_IN_SECONDS
473 + );
474 + }
475 +
476 + $result = array(
477 + 'total_pages' => absint($status['total_pages']),
478 + 'processed_pages' => absint($status['processed_pages']),
479 + 'failed_pages' => absint($status['failed_pages'] ?? 0),
480 + 'percentage' => ($status['total_pages'] > 0)
481 + ? round((absint($status['processed_pages']) / absint($status['total_pages'])) * 100)
482 + : 0,
483 + 'status' => sanitize_text_field($status['status']),
484 + 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
485 + 'failed_pages_list' => isset($status['failed_pages_list']) ? $status['failed_pages_list'] : array(),
486 + 'completion_summary' => isset($status['completion_summary']) ? $status['completion_summary'] : null
487 + );
488 +
489 + // Add error message if present
490 + if (isset($status['error']) && !empty($status['error'])) {
491 + $result['error'] = sanitize_text_field($status['error']);
492 + }
493 +
494 + return $result;
495 +}
496 +
497 +
498 +public function mxchat_handle_sitemap_submission() {
499 + // Check if the form was submitted and verify permissions
500 + if (!isset($_POST['submit_sitemap']) || !current_user_can('manage_options')) {
501 + wp_die(esc_html__('Unauthorized access', 'mxchat'));
502 + }
503 +
504 + // Verify nonce
505 + check_admin_referer('mxchat_submit_sitemap_action', 'mxchat_submit_sitemap_nonce');
506 +
507 + // Validate URL
508 + if (!isset($_POST['sitemap_url']) || empty($_POST['sitemap_url'])) {
509 + set_transient('mxchat_admin_notice_error',
510 + esc_html__('Please provide a valid URL.', 'mxchat'),
511 + 30
512 + );
513 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
514 + exit;
515 + }
516 +
517 + $submitted_url = esc_url_raw($_POST['sitemap_url']);
518 +
519 + // Get bot_id from form submission
520 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
521 +
522 + // Get bot-specific options and validate API key
523 + $bot_options = $this->get_bot_options($bot_id);
524 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
525 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
526 +
527 + if (strpos($selected_model, 'voyage') === 0) {
528 + $api_key = $options['voyage_api_key'] ?? '';
529 + $provider_name = 'Voyage AI';
530 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
531 + $api_key = $options['gemini_api_key'] ?? '';
532 + $provider_name = 'Google Gemini';
533 + } else {
534 + $api_key = $options['api_key'] ?? '';
535 + $provider_name = 'OpenAI';
536 + }
537 +
538 + if (empty($api_key)) {
539 + $error_message = sprintf(
540 + esc_html__('%s API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
541 + $provider_name
542 + );
543 + set_transient('mxchat_admin_notice_error', $error_message, 30);
544 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
545 + exit;
546 + }
547 +
548 + // Fetch URL
549 + $response = wp_remote_get($submitted_url, array('timeout' => 30));
550 +
551 + if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
552 + $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response);
553 + set_transient('mxchat_admin_notice_error',
554 + sprintf(
555 + esc_html__('Failed to fetch the URL: %s', 'mxchat'),
556 + esc_html($error_message)
557 + ),
558 + 30
559 + );
560 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
561 + exit;
562 + }
563 +
564 + $content_type = wp_remote_retrieve_header($response, 'content-type');
565 + $body_content = wp_remote_retrieve_body($response);
566 +
567 + if (empty($body_content)) {
568 + set_transient('mxchat_admin_notice_error',
569 + esc_html__('Empty response received from URL.', 'mxchat'),
570 + 30
571 + );
572 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
573 + exit;
574 + }
575 +
576 + // Handle PDF URL
577 + if ($this->mxchat_is_pdf_url($submitted_url, $response)) {
578 + $result = $this->mxchat_handle_pdf_for_knowledge_base($submitted_url, $response, $bot_id);
579 +
580 + if ($result === 'queued') {
581 + set_transient('mxchat_admin_notice_success',
582 + esc_html__('PDF queued for processing. Processing will start automatically.', 'mxchat'),
583 + 30
584 + );
585 + } else {
586 + set_transient('mxchat_admin_notice_error',
587 + esc_html__('Failed to queue PDF processing: ', 'mxchat') . esc_html($result),
588 + 30
589 + );
590 + }
591 +
592 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
593 + exit;
594 + }
595 +
596 + // Handle Sitemap XML
597 + if (strpos($content_type, 'xml') !== false || strpos($body_content, '<urlset') !== false) {
598 + libxml_use_internal_errors(true);
599 + $xml = simplexml_load_string($body_content);
600 + $xml_errors = libxml_get_errors();
601 + libxml_clear_errors();
602 +
603 + if ($xml === false || !empty($xml_errors)) {
604 + set_transient('mxchat_admin_notice_error',
605 + esc_html__('Invalid sitemap XML. Please provide a valid sitemap.', 'mxchat'),
606 + 30
607 + );
608 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
609 + exit;
610 + }
611 +
612 + $result = $this->mxchat_handle_sitemap_for_knowledge_base($xml, $submitted_url, $bot_id);
613 +
614 + if ($result === 'queued') {
615 + set_transient('mxchat_admin_notice_success',
616 + esc_html__('Sitemap queued for processing. Processing will start automatically.', 'mxchat'),
617 + 30
618 + );
619 + } else {
620 + set_transient('mxchat_admin_notice_error',
621 + esc_html__('Failed to queue sitemap processing. Please check the status below for details.', 'mxchat'),
622 + 30
623 + );
624 + }
625 +
626 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
627 + exit;
628 + }
629 +
630 + // Handle Regular URL (single page)
631 + $page_content = $this->mxchat_extract_main_content($body_content);
632 + $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
633 +
634 + if (empty($sanitized_content)) {
635 + set_transient('mxchat_admin_notice_error',
636 + esc_html__('No valid content found on the provided URL.', 'mxchat'),
637 + 30
638 + );
639 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
640 + exit;
641 + }
642 +
643 + // For single URLs, process immediately (not queued)
644 + $embedding_vector = $this->mxchat_generate_embedding($sanitized_content, $bot_id);
645 +
646 + if (is_string($embedding_vector)) {
647 + $error_message = esc_html__('Failed to generate embedding: ', 'mxchat') . esc_html($embedding_vector);
648 + set_transient('mxchat_admin_notice_error', $error_message, 30);
649 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
650 + exit;
651 + }
652 +
653 + if (is_array($embedding_vector)) {
654 + $db_result = MxChat_Utils::submit_content_to_db(
655 + $sanitized_content,
656 + $submitted_url,
657 + $api_key,
658 + null,
659 + $bot_id
660 + );
661 +
662 + if (is_wp_error($db_result)) {
663 + $error_message = esc_html__('Failed to store content in database: ', 'mxchat') . esc_html($db_result->get_error_message());
664 + set_transient('mxchat_admin_notice_error', $error_message, 30);
665 + } else {
666 + $success_message = esc_html__('URL content successfully submitted!', 'mxchat');
667 + set_transient('mxchat_admin_notice_success', $success_message, 30);
668 + }
669 + } else {
670 + $error_message = esc_html__('Failed to generate embedding: Unexpected result type. Please check your API key and try again.', 'mxchat');
671 + set_transient('mxchat_admin_notice_error', $error_message, 30);
672 + }
673 +
674 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
675 + exit;
676 +}
677 +
678 +
679 +public function mxchat_get_single_url_status() {
680 + $status = get_transient('mxchat_single_url_status');
681 + if (!$status) {
682 + return null;
683 + }
684 +
685 + // Add human-readable time
686 + if (isset($status['timestamp'])) {
687 + $status['human_time'] = human_time_diff(strtotime($status['timestamp']), current_time('timestamp')) . ' ' . __('ago', 'mxchat');
688 + }
689 +
690 + return $status;
691 +}
692 +
693 +public function mxchat_handle_sitemap_for_knowledge_base($xml, $sitemap_url, $bot_id = 'default') {
694 + if (!current_user_can('manage_options')) {
695 + return false;
696 + }
697 +
698 + try {
699 + $sitemap_url = esc_url_raw($sitemap_url);
700 +
701 + if (!$xml || !is_object($xml)) {
702 + throw new Exception(__('Invalid XML object provided', 'mxchat'));
703 + }
704 +
705 + // Get bot-specific embedding API for validation
706 + $bot_options = $this->get_bot_options($bot_id);
707 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
708 +
709 + // Test the embedding API before processing
710 + $test_phrase = "Test embedding generation for MxChat";
711 + $test_result = $this->mxchat_generate_embedding($test_phrase, $bot_id);
712 +
713 + if (is_string($test_result)) {
714 + throw new Exception(__('Embedding API validation failed: ', 'mxchat') . $test_result);
715 + }
716 +
717 + if (!is_array($test_result)) {
718 + throw new Exception(__('Embedding API returned unexpected result type. Please check your configuration.', 'mxchat'));
719 + }
720 +
721 + // Extract URLs from sitemap
722 + $urls = array();
723 + foreach ($xml->url as $url_element) {
724 + $url = esc_url_raw((string)$url_element->loc);
725 + if ($url) {
726 + $urls[] = array('url' => $url);
727 + }
728 + }
729 +
730 + $total_urls = count($urls);
731 +
732 + if ($total_urls < 1) {
733 + throw new Exception(__('No valid URLs found in sitemap', 'mxchat'));
734 + }
735 +
736 + // Create unique queue ID
737 + $queue_id = 'sitemap_' . md5($sitemap_url . time());
738 +
739 + // Add URLs to queue
740 + $queued_count = $this->mxchat_add_to_queue($queue_id, 'url', $urls, $bot_id);
741 +
742 + if ($queued_count === 0) {
743 + throw new Exception(__('Failed to add URLs to processing queue', 'mxchat'));
744 + }
745 +
746 + // Store queue metadata
747 + $this->mxchat_set_queue_meta($queue_id, 'source_url', $sitemap_url);
748 + $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'sitemap');
749 + $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_urls);
750 + $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
751 + $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
752 +
753 + // Store queue ID in transient for status tracking
754 + set_transient('mxchat_active_queue_sitemap', $queue_id, DAY_IN_SECONDS);
755 + set_transient('mxchat_last_sitemap_url', $sitemap_url, DAY_IN_SECONDS);
756 +
757 + return 'queued';
758 +
759 + } catch (Exception $e) {
760 + $error_message = $e->getMessage();
761 + error_log(sprintf(esc_html__('Error preparing sitemap for processing: %s', 'mxchat'), esc_html($error_message)));
762 +
763 + return $error_message;
764 + }
765 +
766 +}
767 +
768 +/**
769 + * Remove shortcode tags but preserve the content inside them
770 + * Example: [vc_column]Hello World[/vc_column] becomes "Hello World"
771 + *
772 + * @param string $content The content containing shortcodes
773 + * @return string Content with shortcode tags removed but inner content preserved
774 + */
775 +private function strip_shortcode_tags_preserve_content($content) {
776 + // Handle nested shortcodes by running multiple passes
777 + $prev_content = '';
778 + $max_iterations = 10; // Prevent infinite loops
779 + $iteration = 0;
780 + while ($prev_content !== $content && $iteration < $max_iterations) {
781 + $prev_content = $content;
782 + // Replace paired shortcodes [tag]content[/tag] with just the content
783 + $content = preg_replace('/\[([a-zA-Z0-9_-]+)[^\]]*\](.*?)\[\/\1\]/s', '$2', $content);
784 + $iteration++;
785 + }
786 + // Remove self-closing shortcodes [tag /] or [tag attr="val" /]
787 + $content = preg_replace('/\[[a-zA-Z0-9_-]+[^\]]*\/\]/', '', $content);
788 + // Remove any remaining opening shortcode tags [tag] or [tag attr="val"]
789 + $content = preg_replace('/\[[a-zA-Z0-9_-]+[^\]]*\]/', '', $content);
790 +
791 + return $content;
792 +}
793 +
794 +public function mxchat_sanitize_content_for_api($content) {
795 + //error_log('[MXCHAT-SANITIZE] Original content preview: ' . substr($content, 0, 500) . '...');
796 +
797 + // Remove shortcode tags but PRESERVE content inside them
798 + $content = $this->strip_shortcode_tags_preserve_content($content);
799 +
800 + // Remove script, style tags, and HTML comments
801 + $content = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $content);
802 + $content = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $content);
803 + $content = preg_replace('/<!--(.|\s)*?-->/', '', $content);
804 +
805 + // Remove all HTML tags and decode HTML entities
806 + $content = wp_strip_all_tags($content);
807 + $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5);
808 +
809 + // Normalize whitespace but preserve paragraph breaks
810 + // First, normalize line endings to \n
811 + $content = str_replace(["\r\n", "\r"], "\n", $content);
812 + // Replace multiple spaces/tabs with single space, but preserve newlines
813 + $content = preg_replace('/[ \t]+/', ' ', $content);
814 + // Replace 3+ newlines with 2 newlines (max 2 blank lines)
815 + $content = preg_replace('/\n{3,}/', "\n\n", $content);
816 + // Trim each line
817 + $lines = explode("\n", $content);
818 + $lines = array_map('trim', $lines);
819 + $content = implode("\n", $lines);
820 + // Final trim
821 + $content = trim($content);
822 +
823 + // Remove control characters (which can cause database issues) but preserve \n (0x0A) and \r (0x0D)
824 + $content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $content);
825 +
826 + // Remove NULL bytes which can cause database errors
827 + $content = str_replace("\0", "", $content);
828 +
829 + // Ensure valid UTF-8 encoding
830 + $content = wp_check_invalid_utf8($content);
831 +
832 + // Remove any extremely long strings without spaces (often garbage)
833 + $content = preg_replace('/\S{300,}/', ' ', $content);
834 +
835 + // Replace problematic characters that often cause database issues
836 + $content = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $content); // Remove emoji and other high Unicode characters
837 +
838 + // Replace any remaining potentially problematic characters with spaces
839 + // BUT preserve newlines by temporarily replacing them
840 + $content = str_replace("\n", "NEWLINE_PLACEHOLDER", $content);
841 + $content = preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\p{Sm}]/u', ' ', $content);
842 + $content = str_replace("NEWLINE_PLACEHOLDER", "\n", $content);
843 +
844 + // Limit to reasonable length if needed
845 + $max_length = 65000; // Just under MySQL TEXT field limit
846 + if (strlen($content) > $max_length) {
847 + $content = substr($content, 0, $max_length);
848 + }
849 +
850 + //error_log('[MXCHAT-SANITIZE] Sanitized content preview: ' . substr($content, 0, 500) . '...');
851 + return $content;
852 +}
853 +public function mxchat_extract_main_content($html) {
854 + if (empty($html)) {
855 + return '';
856 + }
857 + try {
858 + $dom = new DOMDocument;
859 + libxml_use_internal_errors(true); // Suppress HTML parsing errors
860 + @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
861 + $xpath = new DOMXPath($dom);
862 +
863 + // For debugging purposes
864 + $debugEnabled = false; // Set to true to enable debugging output
865 + $debug = function($message) use ($debugEnabled) {
866 + if ($debugEnabled) {
867 + //error_log('[MXCHAT-DEBUG] ' . $message);
868 + }
869 + };
870 +
871 + // Direct targeting for Gerow theme posts
872 + $post_text = $xpath->query('//div[contains(@class, "post-text")]');
873 + if ($post_text && $post_text->length > 0) {
874 + $debug("Found post-text directly");
875 + $content = '';
876 + foreach ($post_text as $node) {
877 + $content .= $dom->saveHTML($node);
878 + }
879 + if (!empty($content)) {
880 + $debug("Returning post-text content");
881 + return $content;
882 + }
883 + }
884 +
885 + // Try to get the blog details content which contains the post-text
886 + $blog_details = $xpath->query('//div[contains(@class, "blog-details-content")]');
887 + if ($blog_details && $blog_details->length > 0) {
888 + $debug("Found blog-details-content");
889 + $content = '';
890 + foreach ($blog_details as $node) {
891 + $content .= $dom->saveHTML($node);
892 + }
893 + if (!empty($content)) {
894 + $debug("Returning blog-details-content");
895 + return $content;
896 + }
897 + }
898 +
899 + // Try to get the article which contains the blog details
900 + $article = $xpath->query('//article[contains(@class, "blog-details-wrap")]');
901 + if ($article && $article->length > 0) {
902 + $debug("Found article with blog-details-wrap");
903 + $content = '';
904 + foreach ($article as $node) {
905 + $content .= $dom->saveHTML($node);
906 + }
907 + if (!empty($content)) {
908 + $debug("Returning article content");
909 + return $content;
910 + }
911 + }
912 +
913 + // Try even broader with the blog-item-wrap
914 + $blog_item = $xpath->query('//div[contains(@class, "blog-item-wrap")]');
915 + if ($blog_item && $blog_item->length > 0) {
916 + $debug("Found blog-item-wrap");
917 + $content = '';
918 + foreach ($blog_item as $node) {
919 + $content .= $dom->saveHTML($node);
920 + }
921 + if (!empty($content)) {
922 + $debug("Returning blog-item-wrap content");
923 + return $content;
924 + }
925 + }
926 +
927 + // Specific Gerow theme path
928 + $gerow_path = $xpath->query('//section[contains(@class, "blog-area")]//div[contains(@class, "post-text")]');
929 + if ($gerow_path && $gerow_path->length > 0) {
930 + $debug("Found Gerow theme path to post-text");
931 + $content = '';
932 + foreach ($gerow_path as $node) {
933 + $content .= $dom->saveHTML($node);
934 + }
935 + if (!empty($content)) {
936 + $debug("Returning Gerow post-text content");
937 + return $content;
938 + }
939 + }
940 +
941 + // Generic blog post selectors
942 + $selectors = [
943 + // Blog post specific selectors
944 + '//div[contains(@class, "post-text")]',
945 + '//article[contains(@class, "blog-post-item")]//div[contains(@class, "post-text")]',
946 + '//div[contains(@class, "blog-details-content")]',
947 + '//article[contains(@class, "blog-details-wrap")]',
948 + '//div[contains(@class, "entry-content")]',
949 + '//div[contains(@class, "blog-content")]',
950 + '//div[contains(@class, "blog-item-wrap")]',
951 +
952 + // More general content selectors
953 + '//div[contains(@class, "page__content")]',
954 + '//div[contains(@class, "elementor-widget-container")]',
955 + '//div[contains(@class, "elementor-text-editor")]',
956 + '//div[contains(@class, "elementor-widget-text-editor")]',
957 + '//*[contains(@class, "entry-content")]',
958 + '//*[contains(@class, "post-content")]',
959 + '//*[contains(@class, "article-content")]',
960 + '//*[@id="content"]',
961 + '//*[@id="main-content"]',
962 + '//section[contains(@class, "blog-area")]',
963 + '//article',
964 + '//main',
965 + '//div[contains(@class, "content")]'
966 + ];
967 +
968 + // First handle Elementor content
969 + $debug("Checking for Elementor content");
970 + $elementor_widgets = $xpath->query('//div[contains(@class, "elementor-element")]//div[contains(@class, "elementor-widget-container")]');
971 + if ($elementor_widgets && $elementor_widgets->length > 0) {
972 + $debug("Found Elementor widgets");
973 + $combined_content = '';
974 + foreach ($elementor_widgets as $widget) {
975 + $widget_content = $dom->saveHTML($widget);
976 + if (!empty($widget_content)) {
977 + $combined_content .= $widget_content;
978 + }
979 + }
980 + if (!empty($combined_content)) {
981 + $debug("Returning Elementor content");
982 + return $combined_content;
983 + }
984 + }
985 +
986 + // Try standard selectors one by one
987 + foreach ($selectors as $selector) {
988 + $debug("Trying selector: " . $selector);
989 + $nodes = $xpath->query($selector);
990 + if ($nodes && $nodes->length > 0) {
991 + $debug("Found matches for selector: " . $selector);
992 + $content = '';
993 + foreach ($nodes as $node) {
994 + $content .= $dom->saveHTML($node);
995 + }
996 + if (!empty($content)) {
997 + $debug("Returning content from selector: " . $selector);
998 + return $content;
999 + }
1000 + }
1001 + }
1002 +
1003 + // Manual regex fallback for post-text if DOM methods fail
1004 + $debug("Trying regex fallback");
1005 + if (preg_match('/<div class="post-text">(.*?)<\/div>\s*<\/div>\s*<\/div>/s', $html, $matches)) {
1006 + $debug("Found post-text via regex");
1007 + return '<div class="post-text">' . $matches[1] . '</div>';
1008 + }
1009 +
1010 + // Try to extract the blog section as a whole
1011 + $blog_section = $xpath->query('//section[contains(@class, "blog-area")]');
1012 + if ($blog_section && $blog_section->length > 0) {
1013 + $debug("Found blog-area section");
1014 + $content = '';
1015 + foreach ($blog_section as $node) {
1016 + $content .= $dom->saveHTML($node);
1017 + }
1018 + if (!empty($content)) {
1019 + $debug("Returning blog-area section content");
1020 + return $content;
1021 + }
1022 + }
1023 +
1024 + // Fallback: Return the body content if no specific selector matches
1025 + $debug("Using body fallback");
1026 + $body = $dom->getElementsByTagName('body');
1027 + if ($body->length > 0) {
1028 + return $dom->saveHTML($body->item(0));
1029 + }
1030 +
1031 + // Last resort: return the original HTML
1032 + $debug("Returning original HTML");
1033 + return $html;
1034 + } catch (Exception $e) {
1035 + //error_log('[MXCHAT-ERROR] Content extraction failed: ' . $e->getMessage());
1036 + return $html; // Return original HTML if parsing fails
1037 + } finally {
1038 + libxml_clear_errors();
1039 + }
1040 +}
1041 +public function mxchat_get_sitemap_processing_status($sitemap_url) {
1042 + $sitemap_url = esc_url_raw($sitemap_url);
1043 + $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
1044 + $status = get_transient($status_key);
1045 +
1046 + if (!$status || !is_array($status)) {
1047 + return false;
1048 + }
1049 +
1050 + // Auto-complete check: if all URLs are processed but status isn't complete
1051 + if (isset($status['processed_urls']) && isset($status['total_urls']) &&
1052 + $status['processed_urls'] >= $status['total_urls'] &&
1053 + isset($status['status']) && $status['status'] !== 'complete' &&
1054 + $status['status'] !== 'error') {
1055 +
1056 + // Mark as complete
1057 + $status['status'] = 'complete';
1058 + $status['processed_urls'] = $status['total_urls']; // Ensure exact match
1059 +
1060 + // Update the transient with the corrected status
1061 + set_transient($status_key, $status, DAY_IN_SECONDS);
1062 + }
1063 +
1064 + return array(
1065 + 'total_urls' => absint($status['total_urls']),
1066 + 'processed_urls' => absint($status['processed_urls']),
1067 + 'failed_urls' => absint($status['failed_urls'] ?? 0),
1068 + 'percentage' => ($status['total_urls'] > 0)
1069 + ? round((absint($status['processed_urls']) / absint($status['total_urls'])) * 100)
1070 + : 0,
1071 + 'status' => sanitize_text_field($status['status']),
1072 + 'last_update' => human_time_diff(absint($status['last_update']), time()) . ' ' . esc_html__('ago', 'mxchat'),
1073 + 'error' => isset($status['error']) ? sanitize_text_field($status['error']) : '',
1074 + 'last_error' => isset($status['last_error']) ? sanitize_text_field($status['last_error']) : '',
1075 + 'failed_urls_list' => isset($status['failed_urls_list']) ? $status['failed_urls_list'] : array()
1076 + );
1077 +}
1078 +
1079 +public function mxchat_ajax_get_status_updates() {
1080 + try {
1081 + // Verify the request
1082 + check_ajax_referer('mxchat_status_nonce', 'nonce');
1083 +
1084 + // Get active queue IDs
1085 + $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
1086 + $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
1087 +
1088 + $sitemap_status = false;
1089 + $pdf_status = false;
1090 +
1091 + // Get sitemap queue status
1092 + if ($sitemap_queue_id) {
1093 + $sitemap_status = $this->mxchat_get_queue_status_data($sitemap_queue_id, 'sitemap');
1094 + }
1095 +
1096 + // Get PDF queue status
1097 + if ($pdf_queue_id) {
1098 + $pdf_status = $this->mxchat_get_queue_status_data($pdf_queue_id, 'pdf');
1099 + }
1100 +
1101 + $is_active_processing =
1102 + ($sitemap_status && $sitemap_status['status'] === 'processing') ||
1103 + ($pdf_status && $pdf_status['status'] === 'processing');
1104 +
1105 + // Return JSON response with the status data
1106 + wp_send_json(array(
1107 + 'pdf_status' => $pdf_status,
1108 + 'sitemap_status' => $sitemap_status,
1109 + 'is_processing' => $is_active_processing,
1110 + 'sitemap_queue_id' => $sitemap_queue_id,
1111 + 'pdf_queue_id' => $pdf_queue_id
1112 + ));
1113 +
1114 + } catch (Exception $e) {
1115 + error_log('MxChat Status Update Error: ' . $e->getMessage());
1116 +
1117 + wp_send_json_error(array(
1118 + 'message' => 'Error getting status updates: ' . $e->getMessage(),
1119 + 'status' => 'error'
1120 + ));
1121 + }
1122 +}
1123 +
1124 +/**
1125 + * Helper function to get queue status data
1126 + */
1127 +private function mxchat_get_queue_status_data($queue_id, $type = 'sitemap') {
1128 + global $wpdb;
1129 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
1130 +
1131 + // Get counts by status
1132 + $counts = $wpdb->get_results($wpdb->prepare(
1133 + "SELECT status, COUNT(*) as count
1134 + FROM $table_name
1135 + WHERE queue_id = %s
1136 + GROUP BY status",
1137 + $queue_id
1138 + ), OBJECT_K);
1139 +
1140 + $total = 0;
1141 + $completed = 0;
1142 + $failed = 0;
1143 + $processing = 0;
1144 + $pending = 0;
1145 +
1146 + foreach ($counts as $status => $data) {
1147 + $count = absint($data->count);
1148 + $total += $count;
1149 +
1150 + switch ($status) {
1151 + case 'completed':
1152 + $completed = $count;
1153 + break;
1154 + case 'failed':
1155 + $failed = $count;
1156 + break;
1157 + case 'processing':
1158 + $processing = $count;
1159 + break;
1160 + case 'pending':
1161 + $pending = $count;
1162 + break;
1163 + }
1164 + }
1165 +
1166 + if ($total === 0) {
1167 + return false;
1168 + }
1169 +
1170 + // Calculate percentage
1171 + $percentage = round((($completed + $failed) / $total) * 100);
1172 +
1173 + // Get failed items details (limit to 50)
1174 + $failed_items = array();
1175 + if ($failed > 0) {
1176 + $failed_results = $wpdb->get_results($wpdb->prepare(
1177 + "SELECT item_type, item_data, error_message, attempts, completed_at
1178 + FROM $table_name
1179 + WHERE queue_id = %s
1180 + AND status = 'failed'
1181 + AND attempts >= max_attempts
1182 + ORDER BY id DESC
1183 + LIMIT 50",
1184 + $queue_id
1185 + ));
1186 +
1187 + foreach ($failed_results as $item) {
1188 + $data = json_decode($item->item_data, true);
1189 + $url = $type === 'pdf' ? ($data['pdf_url'] ?? '') : ($data['url'] ?? '');
1190 +
1191 + $failed_items[] = array(
1192 + 'url' => $url,
1193 + 'page' => $type === 'pdf' ? ($data['page_number'] ?? 0) : 0,
1194 + 'error' => $item->error_message,
1195 + 'retries' => $item->attempts,
1196 + 'time' => strtotime($item->completed_at)
1197 + );
1198 + }
1199 + }
1200 +
1201 + // Get queue metadata
1202 + $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
1203 +
1204 + // Determine if queue is complete
1205 + $is_complete = ($pending === 0 && $processing === 0);
1206 +
1207 + // Get last update time
1208 + $last_update = $wpdb->get_var($wpdb->prepare(
1209 + "SELECT MAX(UNIX_TIMESTAMP(COALESCE(completed_at, started_at, created_at)))
1210 + FROM $table_name
1211 + WHERE queue_id = %s",
1212 + $queue_id
1213 + ));
1214 +
1215 + $last_update_text = $last_update ? human_time_diff($last_update, time()) . ' ' . __('ago', 'mxchat') : __('Just now', 'mxchat');
1216 +
1217 + // Format based on type
1218 + if ($type === 'pdf') {
1219 + return array(
1220 + 'total_pages' => $total,
1221 + 'processed_pages' => $completed + $failed,
1222 + 'failed_pages' => $failed,
1223 + 'percentage' => $percentage,
1224 + 'status' => $is_complete ? 'complete' : 'processing',
1225 + 'last_update' => $last_update_text,
1226 + 'failed_pages_list' => $failed_items,
1227 + 'pdf_url' => $source_url,
1228 + 'queue_id' => $queue_id
1229 + );
1230 + } else {
1231 + return array(
1232 + 'total_urls' => $total,
1233 + 'processed_urls' => $completed + $failed,
1234 + 'failed_urls' => $failed,
1235 + 'percentage' => $percentage,
1236 + 'status' => $is_complete ? 'complete' : 'processing',
1237 + 'last_update' => $last_update_text,
1238 + 'failed_urls_list' => $failed_items,
1239 + 'sitemap_url' => $source_url,
1240 + 'queue_id' => $queue_id
1241 + );
1242 + }
1243 +}
1244 +
1245 +public function mxchat_stop_processing() {
1246 + // Verify permissions
1247 + if (!current_user_can('manage_options')) {
1248 + wp_die(esc_html__('Unauthorized access', 'mxchat'));
1249 + }
1250 +
1251 + // Verify nonce
1252 + check_admin_referer('mxchat_stop_processing_action', 'mxchat_stop_processing_nonce');
1253 +
1254 + global $wpdb;
1255 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
1256 +
1257 + // Get active queue IDs
1258 + $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
1259 + $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
1260 +
1261 + // Delete all pending items from active queues
1262 + if ($sitemap_queue_id) {
1263 + $wpdb->delete(
1264 + $table_name,
1265 + array(
1266 + 'queue_id' => $sitemap_queue_id,
1267 + 'status' => 'pending'
1268 + ),
1269 + array('%s', '%s')
1270 + );
1271 +
1272 + delete_transient('mxchat_active_queue_sitemap');
1273 + delete_transient('mxchat_last_sitemap_url');
1274 + }
1275 +
1276 + if ($pdf_queue_id) {
1277 + // Get PDF path before deleting
1278 + $pdf_path = $this->mxchat_get_queue_meta($pdf_queue_id, 'pdf_path');
1279 +
1280 + $wpdb->delete(
1281 + $table_name,
1282 + array(
1283 + 'queue_id' => $pdf_queue_id,
1284 + 'status' => 'pending'
1285 + ),
1286 + array('%s', '%s')
1287 + );
1288 +
1289 + // Delete PDF file
1290 + if ($pdf_path && file_exists($pdf_path)) {
1291 + wp_delete_file($pdf_path);
1292 + }
1293 +
1294 + delete_transient('mxchat_active_queue_pdf');
1295 + delete_transient('mxchat_last_pdf_url');
1296 + }
1297 +
1298 + // Redirect back with a success message
1299 + set_transient('mxchat_admin_notice_success',
1300 + esc_html__('Processing has been stopped successfully.', 'mxchat'),
1301 + 30
1302 + );
1303 + wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
1304 + exit;
1305 +}
1306 +
1307 +/**
1308 + * Get content list for processing
1309 + */
1310 +public function ajax_mxchat_get_content_list() {
1311 + // Verify the nonce
1312 + check_ajax_referer('mxchat_content_selector_nonce', 'nonce');
1313 +
1314 + if (!current_user_can('manage_options')) {
1315 + wp_send_json_error(__('Unauthorized access', 'mxchat'));
1316 + }
1317 +
1318 + $page = isset($_GET['page']) ? absint($_GET['page']) : 1;
1319 + $per_page = isset($_GET['per_page']) ? absint($_GET['per_page']) : 50;
1320 + $search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
1321 + $post_type = isset($_GET['post_type']) ? sanitize_text_field($_GET['post_type']) : 'all';
1322 + $post_status = isset($_GET['post_status']) ? sanitize_text_field($_GET['post_status']) : 'publish';
1323 + $processed_filter = isset($_GET['processed_filter']) ? sanitize_text_field($_GET['processed_filter']) : 'all';
1324 +
1325 + // Build query args
1326 + $args = array(
1327 + 'posts_per_page' => $per_page,
1328 + 'paged' => $page,
1329 + 'post_status' => $post_status !== 'all' ? $post_status : array('publish', 'draft', 'pending'),
1330 + 'orderby' => 'date',
1331 + 'order' => 'DESC',
1332 + );
1333 +
1334 + // Handle post types - IMPROVED VERSION
1335 + if ($post_type !== 'all') {
1336 + $args['post_type'] = $post_type;
1337 + } else {
1338 + // Get all available post types that might contain content
1339 + $all_post_types = array();
1340 +
1341 + // First get all public post types
1342 + $public_types = get_post_types(array('public' => true), 'names');
1343 + $all_post_types = array_merge($all_post_types, $public_types);
1344 +
1345 + // Add common forum/community post types
1346 + $forum_types = array('topic', 'reply', 'forum', 'wpforo_topic', 'wpforo_post');
1347 + foreach ($forum_types as $forum_type) {
1348 + if (post_type_exists($forum_type)) {
1349 + $all_post_types[] = $forum_type;
1350 + }
1351 + }
1352 +
1353 + // Add other commonly used post types
1354 + $common_types = array('product', 'job_listing', 'event', 'portfolio');
1355 + foreach ($common_types as $common_type) {
1356 + if (post_type_exists($common_type)) {
1357 + $all_post_types[] = $common_type;
1358 + }
1359 + }
1360 +
1361 + // Remove duplicates and ensure we have at least some post types
1362 + $all_post_types = array_unique($all_post_types);
1363 +
1364 + if (empty($all_post_types)) {
1365 + // Fallback to basic post types
1366 + $all_post_types = array('post', 'page');
1367 + }
1368 +
1369 + $args['post_type'] = $all_post_types;
1370 +
1371 + // Debug logging to see what post types are being queried
1372 + //error_log('MxChat Debug: Querying post types: ' . implode(', ', $all_post_types));
1373 + }
1374 +
1375 + if (!empty($search)) {
1376 + $args['s'] = $search;
1377 + }
1378 +
1379 + // Get processed data from storage
1380 + $processed_data = array();
1381 +
1382 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
1383 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1384 +
1385 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
1386 + // Get fresh data from Pinecone - no caching
1387 + $processed_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
1388 + } else {
1389 + // WordPress DB checking with better URL matching for all post types
1390 + global $wpdb;
1391 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1392 + $processed_items = $wpdb->get_results("SELECT id, source_url, timestamp FROM {$table_name}");
1393 +
1394 + if (!empty($processed_items)) {
1395 + foreach ($processed_items as $item) {
1396 + // Use improved URL matching that works for all post types
1397 + $post_id = $this->mxchat_url_to_post_id_improved($item->source_url);
1398 +
1399 + if ($post_id) {
1400 + $processed_data[$post_id] = array(
1401 + 'db_id' => $item->id,
1402 + 'timestamp' => $item->timestamp,
1403 + 'url' => $item->source_url,
1404 + 'source' => 'wordpress'
1405 + );
1406 + }
1407 + }
1408 + }
1409 + }
1410 +
1411 + // Get processed IDs as a simple array for in_array checks
1412 + $processed_ids = array_keys($processed_data);
1413 +
1414 + // Handle processed/unprocessed filter
1415 + if ($processed_filter === 'processed' && !empty($processed_ids)) {
1416 + $args['post__in'] = $processed_ids;
1417 + } elseif ($processed_filter === 'unprocessed' && !empty($processed_ids)) {
1418 + $args['post__not_in'] = $processed_ids;
1419 + }
1420 +
1421 + // Run the query
1422 + $query = new WP_Query($args);
1423 + $content_items = array();
1424 +
1425 + if ($query->have_posts()) {
1426 + while ($query->have_posts()) {
1427 + $query->the_post();
1428 + $id = get_the_ID();
1429 + $post_date = get_the_date();
1430 + $excerpt = wp_trim_words(get_the_excerpt(), 20, '...');
1431 + $word_count = str_word_count(strip_tags(get_the_content()));
1432 +
1433 + $is_processed = in_array($id, $processed_ids);
1434 + $processed_date = '';
1435 + $db_record_id = 0;
1436 + $data_source = 'none';
1437 +
1438 + if ($is_processed && isset($processed_data[$id])) {
1439 + $item_data = $processed_data[$id];
1440 + $data_source = $item_data['source'];
1441 +
1442 + if ($data_source === 'wordpress' && isset($item_data['timestamp'])) {
1443 + // WordPress DB format
1444 + $timestamp = strtotime($item_data['timestamp']);
1445 + $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
1446 + $db_record_id = $item_data['db_id'];
1447 + } elseif ($data_source === 'pinecone') {
1448 + // Pinecone format
1449 + $processed_date = $item_data['processed_date'];
1450 + $db_record_id = $item_data['db_id'];
1451 + }
1452 + }
1453 +
1454 + $content_items[] = array(
1455 + 'id' => $id,
1456 + 'title' => get_the_title(),
1457 + 'permalink' => get_permalink(),
1458 + 'date' => $post_date,
1459 + 'type' => get_post_type(),
1460 + 'status' => get_post_status(),
1461 + 'excerpt' => $excerpt,
1462 + 'word_count' => $word_count,
1463 + 'already_processed' => $is_processed,
1464 + 'processed_date' => $processed_date,
1465 + 'db_record_id' => $db_record_id,
1466 + 'data_source' => $data_source
1467 + );
1468 + }
1469 + wp_reset_postdata();
1470 + }
1471 +
1472 + $response = array(
1473 + 'items' => $content_items,
1474 + 'total' => $query->found_posts,
1475 + 'total_pages' => $query->max_num_pages,
1476 + 'current_page' => $page,
1477 + 'processed_count' => count($processed_ids)
1478 + );
1479 +
1480 + wp_send_json_success($response);
1481 + exit;
1482 +}
1483 +
1484 +
1485 +/**
1486 + * This function handles various WooCommerce URL formats and permalink structures
1487 + */
1488 +private function mxchat_url_to_post_id_improved($url) {
1489 + // First try the standard WordPress function
1490 + $post_id = url_to_postid($url);
1491 +
1492 + if ($post_id > 0) {
1493 + return $post_id;
1494 + }
1495 +
1496 + // If that fails, try more aggressive URL matching
1497 + // Remove trailing slashes and query parameters for better matching
1498 + $clean_url = rtrim($url, '/');
1499 + $clean_url = strtok($clean_url, '?'); // Remove query parameters
1500 +
1501 + // Try again with cleaned URL
1502 + $post_id = url_to_postid($clean_url);
1503 + if ($post_id > 0) {
1504 + return $post_id;
1505 + }
1506 +
1507 + // For bbPress forum topics, try extracting slug from URL
1508 + if (strpos($url, '/topic/') !== false || strpos($url, '/forums/') !== false) {
1509 + // Handle bbPress URLs: /forums/topic/topic-name/
1510 + if (preg_match('/\/forums\/topic\/([^\/\?]+)/', $url, $matches)) {
1511 + $topic_slug = $matches[1];
1512 +
1513 + // Look up topic by slug
1514 + $topic = get_page_by_path($topic_slug, OBJECT, 'topic');
1515 + if ($topic) {
1516 + return $topic->ID;
1517 + }
1518 +
1519 + // Alternative method: query by post_name
1520 + global $wpdb;
1521 + $post_id = $wpdb->get_var($wpdb->prepare(
1522 + "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
1523 + $topic_slug
1524 + ));
1525 +
1526 + if ($post_id) {
1527 + return intval($post_id);
1528 + }
1529 + }
1530 +
1531 + // Handle simpler topic URLs: /topic/topic-name/
1532 + if (preg_match('/\/topic\/([^\/\?]+)/', $url, $matches)) {
1533 + $topic_slug = $matches[1];
1534 +
1535 + global $wpdb;
1536 + $post_id = $wpdb->get_var($wpdb->prepare(
1537 + "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
1538 + $topic_slug
1539 + ));
1540 +
1541 + if ($post_id) {
1542 + return intval($post_id);
1543 + }
1544 + }
1545 + }
1546 +
1547 + // For WooCommerce products
1548 + if (strpos($url, '/product/') !== false || strpos($url, 'product=') !== false) {
1549 + // Extract product slug from various URL formats
1550 + $product_slug = '';
1551 +
1552 + // Handle pretty permalinks: /product/product-name/
1553 + if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
1554 + $product_slug = $matches[1];
1555 + }
1556 + // Handle query parameters: ?product=product-name
1557 + elseif (preg_match('/[\?&]product=([^&]+)/', $url, $matches)) {
1558 + $product_slug = $matches[1];
1559 + }
1560 +
1561 + if (!empty($product_slug)) {
1562 + // Look up product by slug
1563 + $product = get_page_by_path($product_slug, OBJECT, 'product');
1564 + if ($product) {
1565 + return $product->ID;
1566 + }
1567 +
1568 + // Alternative method: query by post_name
1569 + global $wpdb;
1570 + $post_id = $wpdb->get_var($wpdb->prepare(
1571 + "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'product' AND post_status = 'publish'",
1572 + $product_slug
1573 + ));
1574 +
1575 + if ($post_id) {
1576 + return intval($post_id);
1577 + }
1578 + }
1579 + }
1580 +
1581 + // Generic approach: try to extract slug and match against all post types
1582 + $parsed_url = wp_parse_url($clean_url);
1583 + $path = $parsed_url['path'] ?? '';
1584 +
1585 + if (!empty($path)) {
1586 + // Get the last part of the path as potential slug
1587 + $path_parts = array_filter(explode('/', trim($path, '/')));
1588 + $potential_slug = end($path_parts);
1589 +
1590 + if (!empty($potential_slug)) {
1591 + global $wpdb;
1592 +
1593 + // Try to find any post with this slug
1594 + $post_id = $wpdb->get_var($wpdb->prepare(
1595 + "SELECT ID FROM {$wpdb->posts}
1596 + WHERE post_name = %s
1597 + AND post_status IN ('publish', 'closed', 'private')
1598 + AND post_type NOT IN ('revision', 'attachment', 'nav_menu_item')
1599 + ORDER BY CASE
1600 + WHEN post_type = 'post' THEN 1
1601 + WHEN post_type = 'page' THEN 2
1602 + WHEN post_type = 'topic' THEN 3
1603 + WHEN post_type = 'product' THEN 4
1604 + ELSE 5
1605 + END
1606 + LIMIT 1",
1607 + $potential_slug
1608 + ));
1609 +
1610 + if ($post_id) {
1611 + return intval($post_id);
1612 + }
1613 + }
1614 + }
1615 +
1616 + // ADDITIONAL: Try direct database lookup by URL variations
1617 + global $wpdb;
1618 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1619 +
1620 + // Try variations of the URL (with/without trailing slash, http/https)
1621 + $url_variations = array(
1622 + $url,
1623 + rtrim($url, '/'),
1624 + $url . '/',
1625 + str_replace('http://', 'https://', $url),
1626 + str_replace('https://', 'http://', $url),
1627 + str_replace('http://', 'https://', rtrim($url, '/')),
1628 + str_replace('https://', 'http://', rtrim($url, '/'))
1629 + );
1630 +
1631 + // Remove duplicates
1632 + $url_variations = array_unique($url_variations);
1633 +
1634 + foreach ($url_variations as $variation) {
1635 + $existing_record = $wpdb->get_row($wpdb->prepare(
1636 + "SELECT id, source_url FROM $table_name WHERE source_url = %s",
1637 + $variation
1638 + ));
1639 +
1640 + if ($existing_record) {
1641 + // Try to get post ID from this stored URL
1642 + $stored_post_id = url_to_postid($existing_record->source_url);
1643 + if ($stored_post_id > 0) {
1644 + return $stored_post_id;
1645 + }
1646 + }
1647 + }
1648 +
1649 + return 0; // No match found
1650 +}
1651 +/**
1652 + * Process selected content via AJAX
1653 + */
1654 +public function ajax_mxchat_process_selected_content() {
1655 + // Basic request validation
1656 + if (!check_ajax_referer('mxchat_content_selector_nonce', 'nonce', false)) {
1657 + wp_send_json_error('Invalid nonce');
1658 + exit;
1659 + }
1660 +
1661 + if (!current_user_can('manage_options')) {
1662 + wp_send_json_error('Unauthorized access');
1663 + exit;
1664 + }
1665 +
1666 + // Get post IDs - safely parse the array
1667 + $post_ids = array();
1668 + if (isset($_POST['post_ids']) && is_array($_POST['post_ids'])) {
1669 + foreach ($_POST['post_ids'] as $id) {
1670 + $post_ids[] = absint($id);
1671 + }
1672 + }
1673 +
1674 + if (empty($post_ids)) {
1675 + wp_send_json_error('No content selected');
1676 + exit;
1677 + }
1678 +
1679 + // Get bot_id from request
1680 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
1681 +
1682 + // Process only ONE post at a time to avoid request size issues
1683 + $post_id = reset($post_ids);
1684 + $post = get_post($post_id);
1685 +
1686 + if (!$post) {
1687 + wp_send_json_error('Post not found');
1688 + exit;
1689 + }
1690 +
1691 + // Get content including title, short description (for WooCommerce), and main content
1692 + $content = $post->post_title . "\n\n";
1693 +
1694 + // Add short description if it exists (WooCommerce products use post_excerpt for short description)
1695 + if (!empty($post->post_excerpt)) {
1696 + // Remove shortcode tags but preserve content inside them
1697 + $clean_excerpt = $this->strip_shortcode_tags_preserve_content($post->post_excerpt);
1698 + $content .= "Short Description: " . wp_strip_all_tags($clean_excerpt) . "\n\n";
1699 + }
1700 +
1701 + // Add main content - remove shortcode tags but preserve content inside them
1702 + $clean_content = $this->strip_shortcode_tags_preserve_content($post->post_content);
1703 + $content .= wp_strip_all_tags($clean_content);
1704 +
1705 + // ADD WOOCOMMERCE PRODUCT DATA (pricing, stock, categories, custom tabs)
1706 + if (get_post_type($post_id) === 'product' && class_exists('WooCommerce')) {
1707 + $product = wc_get_product($post_id);
1708 +
1709 + if ($product) {
1710 + // Get pricing information
1711 + $regular_price = $product->get_regular_price();
1712 + $sale_price = $product->get_sale_price();
1713 + $price = $product->get_price();
1714 + $sku = $product->get_sku();
1715 +
1716 + // Get currency symbol
1717 + $currency_symbol = get_woocommerce_currency_symbol();
1718 +
1719 + // Add pricing information
1720 + $content .= "\n";
1721 + if (!empty($regular_price)) {
1722 + $content .= "Price: " . $currency_symbol . $regular_price . "\n";
1723 + } elseif (!empty($price)) {
1724 + $content .= "Price: " . $currency_symbol . $price . "\n";
1725 + }
1726 +
1727 + if (!empty($sale_price) && $sale_price !== $regular_price) {
1728 + $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
1729 + }
1730 +
1731 + // Handle variable products - show price range
1732 + if ($product->is_type('variable')) {
1733 + $min_price = $product->get_variation_price('min');
1734 + $max_price = $product->get_variation_price('max');
1735 + if ($min_price !== $max_price) {
1736 + $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
1737 + }
1738 + }
1739 +
1740 + if (!empty($sku)) {
1741 + $content .= "SKU: " . $sku . "\n";
1742 + }
1743 +
1744 + // Get product categories
1745 + $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
1746 + if (!empty($categories) && !is_wp_error($categories)) {
1747 + $content .= "Categories: " . implode(', ', $categories) . "\n";
1748 + }
1749 + }
1750 +
1751 + // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
1752 + $custom_tabs = get_post_meta($post_id, 'yikes_woo_products_tabs', true);
1753 + if (!empty($custom_tabs) && is_array($custom_tabs)) {
1754 + foreach ($custom_tabs as $tab) {
1755 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
1756 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
1757 +
1758 + if (!empty($tab_title) && !empty($tab_content)) {
1759 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
1760 + }
1761 + }
1762 + }
1763 +
1764 + // Also check for reusable/saved tabs applied to this product
1765 + $applied_saved_tabs = get_post_meta($post_id, 'yikes_woo_reusable_products_tabs_applied', true);
1766 + if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
1767 + $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
1768 + if (!empty($saved_tabs) && is_array($saved_tabs)) {
1769 + foreach ($applied_saved_tabs as $saved_tab_id) {
1770 + if (isset($saved_tabs[$saved_tab_id])) {
1771 + $tab = $saved_tabs[$saved_tab_id];
1772 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
1773 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
1774 +
1775 + if (!empty($tab_title) && !empty($tab_content)) {
1776 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
1777 + }
1778 + }
1779 + }
1780 + }
1781 + }
1782 + }
1783 +
1784 + // ADD ACF FIELDS SUPPORT
1785 + $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
1786 + if (!empty($acf_fields)) {
1787 + $acf_content_parts = array();
1788 +
1789 + foreach ($acf_fields as $field_name => $field_value) {
1790 + $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
1791 +
1792 + if (!empty($formatted_value)) {
1793 + $field_label = ucwords(str_replace('_', ' ', $field_name));
1794 + $acf_content_parts[] = $field_label . ": " . $formatted_value;
1795 + }
1796 + }
1797 +
1798 + if (!empty($acf_content_parts)) {
1799 + $content .= "\n\n" . implode("\n", $acf_content_parts);
1800 + }
1801 + }
1802 +
1803 + $content = substr($content, 0, 10000); // Limit content size
1804 +
1805 + // Get bot-specific API key
1806 + $bot_options = $this->get_bot_options($bot_id);
1807 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
1808 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
1809 +
1810 + if (strpos($selected_model, 'voyage') === 0) {
1811 + $api_key = $options['voyage_api_key'] ?? '';
1812 + $provider_name = 'Voyage AI';
1813 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
1814 + $api_key = $options['gemini_api_key'] ?? '';
1815 + $provider_name = 'Google Gemini';
1816 + } else {
1817 + $api_key = $options['api_key'] ?? '';
1818 + $provider_name = 'OpenAI';
1819 + }
1820 +
1821 + if (empty($api_key)) {
1822 + wp_send_json_error($provider_name . ' API key not configured');
1823 + exit;
1824 + }
1825 +
1826 + $source_url = get_permalink($post_id);
1827 + $vector_id = md5($source_url); // Vector ID for Pinecone
1828 +
1829 + // Check for existing content in bot-specific storage
1830 + $is_update = false;
1831 +
1832 + // Get bot-specific Pinecone configuration
1833 + $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
1834 + $use_pinecone = !empty($bot_pinecone_config) && ($bot_pinecone_config['use_pinecone'] ?? false);
1835 +
1836 + if ($use_pinecone && !empty($bot_pinecone_config['api_key'])) {
1837 + // Check Pinecone for this bot
1838 + $pinecone_data = $this->mxchat_get_pinecone_processed_content($bot_pinecone_config);
1839 + if (isset($pinecone_data[$post_id])) {
1840 + $is_update = true;
1841 + }
1842 + } else {
1843 + // Check WordPress DB (same as before since it's shared)
1844 + global $wpdb;
1845 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1846 + $existing_record = $wpdb->get_row($wpdb->prepare(
1847 + "SELECT id FROM $table_name WHERE source_url = %s",
1848 + $source_url
1849 + ));
1850 +
1851 + if ($existing_record) {
1852 + $is_update = true;
1853 + }
1854 + }
1855 +
1856 + // UPDATED 2.5.6: Determine content type based on post_type
1857 + $post_type = $post->post_type;
1858 + $content_type = 'content'; // Default fallback
1859 +
1860 + // Map WordPress post types to content types
1861 + switch ($post_type) {
1862 + case 'post':
1863 + $content_type = 'post';
1864 + break;
1865 + case 'page':
1866 + $content_type = 'page';
1867 + break;
1868 + case 'product':
1869 + $content_type = 'product';
1870 + break;
1871 + default:
1872 + // For custom post types, use the post type name
1873 + $content_type = sanitize_key($post_type);
1874 + break;
1875 + }
1876 +
1877 + // Use the centralized utility function with bot_id and content_type
1878 + $result = MxChat_Utils::submit_content_to_db(
1879 + $content,
1880 + $source_url,
1881 + $api_key,
1882 + $vector_id,
1883 + $bot_id,
1884 + $content_type
1885 + );
1886 +
1887 + if (is_wp_error($result)) {
1888 + wp_send_json_error('Storage failed: ' . $result->get_error_message());
1889 + exit;
1890 + }
1891 +
1892 + // Automatically apply role restriction based on tags
1893 + $this->apply_role_restriction_to_post($post_id, $source_url);
1894 +
1895 + $operation_type = $is_update ? 'update' : 'new';
1896 +
1897 + // Count ACF fields for debugging
1898 + $acf_field_count = count($acf_fields);
1899 +
1900 + // Success response with minimal data
1901 + wp_send_json_success(array(
1902 + 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully',
1903 + 'post_id' => $post_id,
1904 + 'title' => $post->post_title,
1905 + 'operation_type' => $operation_type,
1906 + 'vector_id' => $vector_id,
1907 + 'acf_fields_found' => $acf_field_count,
1908 + 'content_preview' => substr($content, 0, 100) . '...',
1909 + 'bot_id' => $bot_id
1910 + ));
1911 + exit;
1912 +}
1913 +
1914 +private function apply_role_restriction_to_post($post_id, $source_url) {
1915 + // Get tag-role mappings
1916 + $mappings = get_option('mxchat_tag_role_mappings', array());
1917 +
1918 + if (empty($mappings)) {
1919 + return; // No mappings, leave as public
1920 + }
1921 +
1922 + // Get all tags for the post
1923 + $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
1924 +
1925 + if (empty($post_tags)) {
1926 + return; // No tags, leave as public
1927 + }
1928 +
1929 + // Determine the highest role restriction based on tags
1930 + $highest_role = 'public';
1931 + $role_hierarchy = array(
1932 + 'public' => 0,
1933 + 'logged_in' => 1,
1934 + 'subscriber' => 2,
1935 + 'contributor' => 3,
1936 + 'author' => 4,
1937 + 'editor' => 5,
1938 + 'administrator' => 6
1939 + );
1940 +
1941 + foreach ($post_tags as $tag_slug) {
1942 + if (isset($mappings[$tag_slug])) {
1943 + $role = $mappings[$tag_slug];
1944 + if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
1945 + $highest_role = $role;
1946 + }
1947 + }
1948 + }
1949 +
1950 + // If no restricted tags found, return (leave as public)
1951 + if ($highest_role === 'public') {
1952 + return;
1953 + }
1954 +
1955 + // Update the role restriction in the database
1956 + global $wpdb;
1957 +
1958 + // Check if using Pinecone
1959 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
1960 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
1961 +
1962 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
1963 + // Update Pinecone role restriction
1964 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
1965 + $vector_id = md5($source_url);
1966 +
1967 + $wpdb->replace(
1968 + $roles_table,
1969 + array(
1970 + 'vector_id' => $vector_id,
1971 + 'role_restriction' => $highest_role,
1972 + 'updated_at' => current_time('mysql')
1973 + ),
1974 + array('%s', '%s', '%s')
1975 + );
1976 + } else {
1977 + // Update WordPress DB
1978 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1979 +
1980 + $wpdb->update(
1981 + $table_name,
1982 + array('role_restriction' => $highest_role),
1983 + array('source_url' => $source_url),
1984 + array('%s'),
1985 + array('%s')
1986 + );
1987 + }
1988 +}
1989 +
1990 +public function mxchat_get_public_post_types() {
1991 + // Get all public post types
1992 + $post_types = get_post_types(array('public' => true), 'objects');
1993 + $post_type_options = array();
1994 +
1995 + foreach ($post_types as $post_type) {
1996 + $post_type_options[$post_type->name] = $post_type->label;
1997 + }
1998 +
1999 + // Also include common forum/community post types that might not be marked as public
2000 + $additional_types = array(
2001 + 'topic' => 'Forum Topics (bbPress)',
2002 + 'reply' => 'Forum Replies (bbPress)',
2003 + 'forum' => 'Forums (bbPress)',
2004 + 'wpforo_topic' => 'wpForo Topics',
2005 + 'wpforo_post' => 'wpForo Posts'
2006 + );
2007 +
2008 + foreach ($additional_types as $type_name => $type_label) {
2009 + if (post_type_exists($type_name) && !isset($post_type_options[$type_name])) {
2010 + $post_type_options[$type_name] = $type_label;
2011 + }
2012 + }
2013 +
2014 + return $post_type_options;
2015 +}
2016 +
2017 +/**
2018 + * Retrieves processed content from Pinecone API
2019 + */
2020 +public function mxchat_get_pinecone_processed_content($pinecone_options) {
2021 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2022 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2023 +
2024 + if (empty($api_key) || empty($host)) {
2025 + return array();
2026 + }
2027 +
2028 + $pinecone_data = array();
2029 +
2030 + try {
2031 + // Always get fresh data from Pinecone
2032 + $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
2033 +
2034 + // Method 2: Final fallback - try stats endpoint (if available)
2035 + if (empty($pinecone_data)) {
2036 + $stats_url = "https://{$host}/describe_index_stats";
2037 +
2038 + $response = wp_remote_post($stats_url, array(
2039 + 'headers' => array(
2040 + 'Api-Key' => $api_key,
2041 + 'Content-Type' => 'application/json'
2042 + ),
2043 + 'body' => json_encode(array()),
2044 + 'timeout' => 30
2045 + ));
2046 +
2047 + if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2048 + $body = wp_remote_retrieve_body($response);
2049 + $stats_data = json_decode($body, true);
2050 + }
2051 + }
2052 +
2053 + } catch (Exception $e) {
2054 + // Log error but return fresh data only
2055 + }
2056 +
2057 + return $pinecone_data;
2058 +}
2059 +public function mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
2060 + //error_log('=== DEBUG: Starting mxchat_fetch_pinecone_vectors_by_ids ===');
2061 +
2062 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2063 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2064 +
2065 + if (empty($api_key) || empty($host) || empty($vector_ids)) {
2066 + //error_log('DEBUG: Missing parameters for fetch by IDs');
2067 + return array();
2068 + }
2069 +
2070 + try {
2071 + $fetch_url = "https://{$host}/vectors/fetch";
2072 + //error_log('DEBUG: Fetch URL: ' . $fetch_url);
2073 + //error_log('DEBUG: Fetching ' . count($vector_ids) . ' vector IDs');
2074 +
2075 + // Pinecone fetch API allows fetching specific vectors by ID
2076 + $fetch_data = array(
2077 + 'ids' => array_values($vector_ids)
2078 + );
2079 +
2080 + $response = wp_remote_post($fetch_url, array(
2081 + 'headers' => array(
2082 + 'Api-Key' => $api_key,
2083 + 'Content-Type' => 'application/json'
2084 + ),
2085 + 'body' => json_encode($fetch_data),
2086 + 'timeout' => 30
2087 + ));
2088 +
2089 + if (is_wp_error($response)) {
2090 + //error_log('DEBUG: Fetch by IDs WP error: ' . $response->get_error_message());
2091 + return array();
2092 + }
2093 +
2094 + $response_code = wp_remote_retrieve_response_code($response);
2095 + //error_log('DEBUG: Fetch response code: ' . $response_code);
2096 +
2097 + if ($response_code !== 200) {
2098 + $error_body = wp_remote_retrieve_body($response);
2099 + //error_log('DEBUG: Fetch failed with body: ' . $error_body);
2100 + return array();
2101 + }
2102 +
2103 + $body = wp_remote_retrieve_body($response);
2104 + $data = json_decode($body, true);
2105 +
2106 + //error_log('DEBUG: Fetch response structure: ' . print_r(array_keys($data), true));
2107 +
2108 + if (!isset($data['vectors'])) {
2109 + //error_log('DEBUG: No vectors key in response');
2110 + return array();
2111 + }
2112 +
2113 + $processed_data = array();
2114 +
2115 + foreach ($data['vectors'] as $vector_id => $vector_data) {
2116 + $metadata = $vector_data['metadata'] ?? array();
2117 + $source_url = $metadata['source_url'] ?? '';
2118 +
2119 + if (!empty($source_url)) {
2120 + $post_id = url_to_postid($source_url);
2121 + if ($post_id) {
2122 + $created_at = $metadata['created_at'] ?? '';
2123 + $processed_date = 'Recently';
2124 +
2125 + if (!empty($created_at)) {
2126 + $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
2127 + if ($timestamp) {
2128 + $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2129 + }
2130 + }
2131 +
2132 + $processed_data[$post_id] = array(
2133 + 'db_id' => $vector_id,
2134 + 'processed_date' => $processed_date,
2135 + 'url' => $source_url,
2136 + 'source' => 'pinecone',
2137 + 'timestamp' => $timestamp ?? current_time('timestamp')
2138 + );
2139 + }
2140 + }
2141 + }
2142 +
2143 + //error_log('DEBUG: Processed ' . count($processed_data) . ' vectors from fetch');
2144 + return $processed_data;
2145 +
2146 + } catch (Exception $e) {
2147 + //error_log('DEBUG: Exception in fetch_pinecone_vectors_by_ids: ' . $e->getMessage());
2148 + return array();
2149 + }
2150 +}
2151 +
2152 +/**
2153 + * Scan Pinecone for processed content
2154 + */
2155 +public function mxchat_scan_pinecone_for_processed_content($pinecone_options) {
2156 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2157 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2158 +
2159 + if (empty($api_key) || empty($host)) {
2160 + return array();
2161 + }
2162 +
2163 + try {
2164 + // Use multiple random vectors to get better coverage
2165 + $all_matches = array();
2166 + $seen_ids = array();
2167 +
2168 + // Try 3 different random vectors to get better coverage
2169 + for ($i = 0; $i < 3; $i++) {
2170 + $query_url = "https://{$host}/query";
2171 +
2172 + // Generate a random unit vector instead of zeros
2173 + $random_vector = array();
2174 + for ($j = 0; $j < 1536; $j++) {
2175 + $random_vector[] = (rand(-1000, 1000) / 1000.0);
2176 + }
2177 +
2178 + // Normalize the vector to unit length
2179 + $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
2180 + if ($magnitude > 0) {
2181 + $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
2182 + }
2183 +
2184 + $query_data = array(
2185 + 'includeMetadata' => true,
2186 + 'includeValues' => false,
2187 + 'topK' => 10000,
2188 + 'vector' => $random_vector
2189 + );
2190 +
2191 + $response = wp_remote_post($query_url, array(
2192 + 'headers' => array(
2193 + 'Api-Key' => $api_key,
2194 + 'Content-Type' => 'application/json'
2195 + ),
2196 + 'body' => json_encode($query_data),
2197 + 'timeout' => 30
2198 + ));
2199 +
2200 + if (is_wp_error($response)) {
2201 + continue;
2202 + }
2203 +
2204 + $response_code = wp_remote_retrieve_response_code($response);
2205 +
2206 + if ($response_code !== 200) {
2207 + continue;
2208 + }
2209 +
2210 + $body = wp_remote_retrieve_body($response);
2211 + $data = json_decode($body, true);
2212 +
2213 + if (isset($data['matches'])) {
2214 + foreach ($data['matches'] as $match) {
2215 + $match_id = $match['id'] ?? '';
2216 + if (!empty($match_id) && !isset($seen_ids[$match_id])) {
2217 + $all_matches[] = $match;
2218 + $seen_ids[$match_id] = true;
2219 + }
2220 + }
2221 + }
2222 + }
2223 +
2224 + // Convert matches to processed data format
2225 + $processed_data = array();
2226 +
2227 + foreach ($all_matches as $match) {
2228 + $metadata = $match['metadata'] ?? array();
2229 + $source_url = $metadata['source_url'] ?? '';
2230 + $match_id = $match['id'] ?? '';
2231 +
2232 + if (!empty($source_url) && !empty($match_id)) {
2233 + $post_id = url_to_postid($source_url);
2234 + if ($post_id) {
2235 + $created_at = $metadata['created_at'] ?? '';
2236 + $processed_date = 'Recently';
2237 +
2238 + if (!empty($created_at)) {
2239 + $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
2240 + if ($timestamp) {
2241 + $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
2242 + }
2243 + }
2244 +
2245 + $processed_data[$post_id] = array(
2246 + 'db_id' => $match_id,
2247 + 'processed_date' => $processed_date,
2248 + 'url' => $source_url,
2249 + 'source' => 'pinecone',
2250 + 'timestamp' => $timestamp ?? current_time('timestamp')
2251 + );
2252 + }
2253 + }
2254 + }
2255 +
2256 + return $processed_data;
2257 +
2258 + } catch (Exception $e) {
2259 + return array();
2260 + }
2261 +}
2262 +/**
2263 + * Generate embeddings from input text for MXChat with bot support
2264 + */
2265 +private function mxchat_generate_embedding($text, $bot_id = 'default') {
2266 + // Enable detailed logging for debugging
2267 + //error_log('[MXCHAT-EMBED] Starting embedding generation for bot: ' . $bot_id . '. Text length: ' . strlen($text) . ' bytes');
2268 + //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
2269 +
2270 + // Get bot-specific options
2271 + $bot_options = $this->get_bot_options($bot_id);
2272 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
2273 +
2274 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
2275 + //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model);
2276 +
2277 + // Determine provider and endpoint
2278 + if (strpos($selected_model, 'voyage') === 0) {
2279 + $api_key = $options['voyage_api_key'] ?? '';
2280 + $endpoint = 'https://api.voyageai.com/v1/embeddings';
2281 + $provider_name = 'Voyage AI';
2282 + //error_log('[MXCHAT-EMBED] Using Voyage AI API for bot ' . $bot_id);
2283 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2284 + $api_key = $options['gemini_api_key'] ?? '';
2285 + $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
2286 + $provider_name = 'Google Gemini';
2287 + //error_log('[MXCHAT-EMBED] Using Google Gemini API for bot ' . $bot_id);
2288 + } else {
2289 + $api_key = $options['api_key'] ?? '';
2290 + $endpoint = 'https://api.openai.com/v1/embeddings';
2291 + $provider_name = 'OpenAI';
2292 + //error_log('[MXCHAT-EMBED] Using OpenAI API for bot ' . $bot_id);
2293 + }
2294 +
2295 + //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
2296 +
2297 + if (empty($api_key)) {
2298 + $error_message = sprintf('Missing %s API key for bot %s. Please configure your API key in the bot settings.', $provider_name, $bot_id);
2299 + //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
2300 + return $error_message;
2301 + }
2302 +
2303 + // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
2304 + $estimated_tokens = ceil(str_word_count($text) / 0.75);
2305 + //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
2306 +
2307 + if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
2308 + //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
2309 + // Consider truncating text here
2310 + }
2311 +
2312 + // Prepare request body based on provider
2313 + if (strpos($selected_model, 'gemini-embedding') === 0) {
2314 + // Gemini API format
2315 + $request_body = array(
2316 + 'model' => 'models/' . $selected_model,
2317 + 'content' => array(
2318 + 'parts' => array(
2319 + array('text' => $text)
2320 + )
2321 + )
2322 + );
2323 +
2324 + // Set output dimensionality to 1536 for consistency with other models
2325 + $request_body['outputDimensionality'] = 1536;
2326 + } else {
2327 + // OpenAI/Voyage API format
2328 + $request_body = array(
2329 + 'model' => $selected_model,
2330 + 'input' => $text
2331 + );
2332 +
2333 + // Add output_dimension for voyage-3-large model
2334 + if ($selected_model === 'voyage-3-large') {
2335 + $request_body['output_dimension'] = 2048;
2336 + }
2337 + }
2338 +
2339 + //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
2340 +
2341 + // Prepare headers based on provider
2342 + if (strpos($selected_model, 'gemini-embedding') === 0) {
2343 + // Gemini uses API key as query parameter
2344 + $endpoint .= '?key=' . $api_key;
2345 + $headers = array(
2346 + 'Content-Type' => 'application/json'
2347 + );
2348 + } else {
2349 + // OpenAI/Voyage use Bearer token
2350 + $headers = array(
2351 + 'Authorization' => 'Bearer ' . $api_key,
2352 + 'Content-Type' => 'application/json'
2353 + );
2354 + }
2355 +
2356 + // Make API request
2357 + //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
2358 + $response = wp_remote_post($endpoint, array(
2359 + 'body' => wp_json_encode($request_body),
2360 + 'headers' => $headers,
2361 + 'timeout' => 60 // Increased timeout for large inputs
2362 + ));
2363 +
2364 + // Handle wp_remote_post errors
2365 + if (is_wp_error($response)) {
2366 + $error_message = $response->get_error_message();
2367 + //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
2368 + return 'Connection error: ' . $error_message;
2369 + }
2370 +
2371 + // Get and check HTTP response code
2372 + $http_code = wp_remote_retrieve_response_code($response);
2373 + //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
2374 +
2375 + if ($http_code !== 200) {
2376 + $error_body = wp_remote_retrieve_body($response);
2377 + //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
2378 +
2379 + // Try to parse error for more details
2380 + $error_json = json_decode($error_body, true);
2381 + if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
2382 + $error_type = $error_json['error']['type'] ?? 'unknown';
2383 + $error_message = $error_json['error']['message'] ?? 'No message';
2384 + //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
2385 + //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
2386 +
2387 + // Customize error message for common API errors
2388 + if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
2389 + $error_message = sprintf('Invalid %s API key for bot %s. Please check your API key in the bot settings.', $provider_name, $bot_id);
2390 + } elseif ($error_type === 'authentication_error') {
2391 + $error_message = sprintf('%s authentication failed for bot %s. Please verify your API key in the bot settings.', $provider_name, $bot_id);
2392 + }
2393 +
2394 + //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
2395 + return $error_message;
2396 + }
2397 +
2398 + $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding for bot %s", $http_code, $bot_id);
2399 + //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
2400 + return $error_message;
2401 + }
2402 +
2403 + // Parse response body
2404 + $response_body = wp_remote_retrieve_body($response);
2405 + //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
2406 +
2407 + $response_data = json_decode($response_body, true);
2408 +
2409 + if (json_last_error() !== JSON_ERROR_NONE) {
2410 + $error = json_last_error_msg();
2411 + //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
2412 + //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
2413 + return "Failed to parse API response: $error";
2414 + }
2415 +
2416 + // Handle different response formats based on provider
2417 + if (strpos($selected_model, 'gemini-embedding') === 0) {
2418 + // Gemini API response format
2419 + if (isset($response_data['embedding']['values'])) {
2420 + $embedding_dimensions = count($response_data['embedding']['values']);
2421 + //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
2422 +
2423 + // Check if embedding dimensions are as expected (should be 1536)
2424 + if ($embedding_dimensions !== 1536) {
2425 + //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
2426 + }
2427 +
2428 + return $response_data['embedding']['values'];
2429 + } else {
2430 + //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
2431 + //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
2432 +
2433 + if (isset($response_data['error'])) {
2434 + $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
2435 + //error_log('[MXCHAT-EMBED] ' . $error_message);
2436 + return $error_message;
2437 + }
2438 +
2439 + $error_message = "Invalid Gemini API response format: No embedding found";
2440 + //error_log('[MXCHAT-EMBED] ' . $error_message);
2441 + return $error_message;
2442 + }
2443 + } else {
2444 + // OpenAI/Voyage API response format
2445 + if (isset($response_data['data'][0]['embedding'])) {
2446 + $embedding_dimensions = count($response_data['data'][0]['embedding']);
2447 + //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
2448 +
2449 + // Check if embedding dimensions are as expected
2450 + if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
2451 + ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
2452 + //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
2453 + }
2454 +
2455 + return $response_data['data'][0]['embedding'];
2456 + } else {
2457 + //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
2458 + //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
2459 +
2460 + if (isset($response_data['error'])) {
2461 + $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
2462 + //error_log('[MXCHAT-EMBED] ' . $error_message);
2463 + return $error_message;
2464 + }
2465 +
2466 + $error_message = "Invalid API response format: No embedding found";
2467 + //error_log('[MXCHAT-EMBED] ' . $error_message);
2468 + return $error_message;
2469 + }
2470 + }
2471 +}
2472 +
2473 +/**
2474 + * Get bot-specific options for multi-bot functionality
2475 + * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
2476 + */
2477 +private function get_bot_options($bot_id = 'default') {
2478 + //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
2479 +
2480 + if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
2481 + //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
2482 + return array();
2483 + }
2484 +
2485 + $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
2486 +
2487 + if (!empty($bot_options)) {
2488 + //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
2489 + if (isset($bot_options['similarity_threshold'])) {
2490 + //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
2491 + }
2492 + }
2493 +
2494 + return is_array($bot_options) ? $bot_options : array();
2495 +}
2496 +
2497 +/**
2498 + * Get bot-specific Pinecone configuration
2499 + * Used in the knowledge retrieval functions
2500 + */
2501 +// Also add debugging to your get_bot_pinecone_config function
2502 +private function get_bot_pinecone_config($bot_id = 'default') {
2503 + //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
2504 +
2505 + // If default bot or multi-bot add-on not active, use default Pinecone config
2506 + if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
2507 + //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
2508 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
2509 + $config = array(
2510 + 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
2511 + 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
2512 + 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
2513 + 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
2514 + );
2515 + //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
2516 + return $config;
2517 + }
2518 +
2519 + //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
2520 +
2521 + // Hook for multi-bot add-on to provide bot-specific Pinecone config
2522 + $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
2523 +
2524 + if (!empty($bot_pinecone_config)) {
2525 + //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
2526 + //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
2527 + //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
2528 + //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
2529 + } else {
2530 + //error_log("MXCHAT DEBUG: Filter returned empty config!");
2531 + }
2532 +
2533 + return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
2534 +}
2535 +
2536 +
2537 +public function mxchat_ajax_dismiss_completed_status() {
2538 + try {
2539 + // Verify the request
2540 + check_ajax_referer('mxchat_status_nonce', 'nonce');
2541 +
2542 + if (!current_user_can('manage_options')) {
2543 + wp_send_json_error('Unauthorized access');
2544 + exit;
2545 + }
2546 +
2547 + $card_type = isset($_POST['card_type']) ? sanitize_text_field($_POST['card_type']) : '';
2548 +
2549 + if ($card_type === 'pdf') {
2550 + // Clear PDF status
2551 + $pdf_url = get_transient('mxchat_last_pdf_url');
2552 + if ($pdf_url) {
2553 + delete_transient('mxchat_pdf_status_' . md5($pdf_url));
2554 + delete_transient('mxchat_last_pdf_url');
2555 + }
2556 + } elseif ($card_type === 'sitemap') {
2557 + // Clear sitemap status
2558 + $sitemap_url = get_transient('mxchat_last_sitemap_url');
2559 + if ($sitemap_url) {
2560 + delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
2561 + delete_transient('mxchat_last_sitemap_url');
2562 + }
2563 + }
2564 +
2565 + wp_send_json_success(array('message' => 'Status dismissed successfully'));
2566 +
2567 + } catch (Exception $e) {
2568 + wp_send_json_error(array('message' => 'Error dismissing status: ' . $e->getMessage()));
2569 + }
2570 +}
2571 +
2572 +/**
2573 + * Render completed status cards on page load
2574 + * This ensures completed processing status persists through page refreshes
2575 + */
2576 +public function mxchat_render_completed_status_cards() {
2577 + $output = '';
2578 +
2579 + // Check for completed PDF status
2580 + $pdf_url = get_transient('mxchat_last_pdf_url');
2581 + if ($pdf_url) {
2582 + $pdf_status = $this->mxchat_get_pdf_processing_status($pdf_url);
2583 + if ($pdf_status && ($pdf_status['status'] === 'complete' || $pdf_status['status'] === 'error')) {
2584 + $output .= $this->mxchat_render_pdf_status_card($pdf_status, $pdf_url);
2585 + }
2586 + }
2587 +
2588 + // Check for completed sitemap status
2589 + $sitemap_url = get_transient('mxchat_last_sitemap_url');
2590 + if ($sitemap_url) {
2591 + $sitemap_status = $this->mxchat_get_sitemap_processing_status($sitemap_url);
2592 + if ($sitemap_status && ($sitemap_status['status'] === 'complete' || $sitemap_status['status'] === 'error')) {
2593 + $output .= $this->mxchat_render_sitemap_status_card($sitemap_status, $sitemap_url);
2594 + }
2595 + }
2596 +
2597 + return $output;
2598 +}
2599 +
2600 +/**
2601 + * Render PDF status card HTML
2602 + */
2603 +private function mxchat_render_pdf_status_card($status, $pdf_url) {
2604 + $html = '<div class="mxchat-status-card" data-card-type="pdf">';
2605 + $html .= '<div class="mxchat-status-header">';
2606 + $html .= '<h4>' . esc_html__('PDF Processing Status', 'mxchat') . '</h4>';
2607 +
2608 + // Add dismiss button for completed status
2609 + if ($status['status'] === 'complete' || $status['status'] === 'error') {
2610 + $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
2611 + }
2612 +
2613 + // Process Batch button for processing status
2614 + if ($status['status'] === 'processing') {
2615 + $html .= '<button type="button" class="mxchat-manual-batch-btn"
2616 + data-process-type="pdf"
2617 + data-url="' . esc_attr($pdf_url) . '">
2618 + ' . esc_html__('Process Batch', 'mxchat') . '</button>';
2619 + }
2620 +
2621 + // Add status badges
2622 + if ($status['status'] === 'error') {
2623 + $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
2624 + } elseif ($status['status'] === 'complete') {
2625 + if ($status['failed_pages'] > 0) {
2626 + $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
2627 + sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_pages']) . '</span>';
2628 + } else {
2629 + $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
2630 + }
2631 + }
2632 +
2633 + $html .= '</div>'; // End header
2634 +
2635 + // Progress bar
2636 + $html .= '<div class="mxchat-progress-bar">';
2637 + $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
2638 + $html .= '</div>';
2639 +
2640 + // Status details
2641 + $html .= '<div class="mxchat-status-details">';
2642 + $html .= '<p>' . sprintf(
2643 + esc_html__('Progress: %d of %d pages (%d%%)', 'mxchat'),
2644 + $status['processed_pages'],
2645 + $status['total_pages'],
2646 + $status['percentage']
2647 + ) . '</p>';
2648 +
2649 + // Show failed pages count if any
2650 + if ($status['failed_pages'] > 0) {
2651 + $html .= '<p><strong>' . esc_html__('Failed pages:', 'mxchat') . '</strong> ' . esc_html($status['failed_pages']) . '</p>';
2652 + }
2653 +
2654 + $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
2655 + $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
2656 +
2657 + // Add completion summary if available AND it's an array
2658 + if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
2659 + $summary = $status['completion_summary'];
2660 + $html .= '<div class="mxchat-completion-summary">';
2661 + $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
2662 + $html .= '<p><strong>' . esc_html__('Total Pages:', 'mxchat') . '</strong> ' . esc_html($summary['total_pages']) . '</p>';
2663 + $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_pages']) . '</p>';
2664 + $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_pages']) . '</p>';
2665 + $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
2666 + $html .= '</div>';
2667 + }
2668 +
2669 + // Add failed pages list if any AND it's an array
2670 + if (isset($status['failed_pages_list']) && is_array($status['failed_pages_list']) && !empty($status['failed_pages_list'])) {
2671 + $html .= $this->mxchat_render_failed_pages_list($status['failed_pages_list']);
2672 + }
2673 +
2674 + // Add error message if any
2675 + if (isset($status['error']) && !empty($status['error'])) {
2676 + $html .= '<div class="mxchat-error-notice">';
2677 + $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
2678 + $html .= '</div>';
2679 + }
2680 +
2681 + $html .= '</div>'; // End details
2682 + $html .= '</div>'; // End card
2683 +
2684 + return $html;
2685 +}
2686 +/**
2687 + * Render sitemap status card HTML
2688 + */
2689 +private function mxchat_render_sitemap_status_card($status, $sitemap_url) {
2690 + $html = '<div class="mxchat-status-card" data-card-type="sitemap">';
2691 + $html .= '<div class="mxchat-status-header">';
2692 + $html .= '<h4>' . esc_html__('Sitemap Processing Status', 'mxchat') . '</h4>';
2693 +
2694 + // Add dismiss button for completed status
2695 + if ($status['status'] === 'complete' || $status['status'] === 'error') {
2696 + $html .= '<button type="button" class="mxchat-dismiss-button">' . esc_html__('Dismiss', 'mxchat') . '</button>';
2697 + }
2698 +
2699 + // Process Batch button for processing status
2700 + if ($status['status'] === 'processing') {
2701 + $html .= '<button type="button" class="mxchat-manual-batch-btn"
2702 + data-process-type="sitemap"
2703 + data-url="' . esc_attr($sitemap_url) . '">
2704 + ' . esc_html__('Process Batch', 'mxchat') . '</button>';
2705 + }
2706 +
2707 + // Add status badges
2708 + if ($status['status'] === 'error') {
2709 + $html .= '<span class="mxchat-status-badge mxchat-status-failed">' . esc_html__('Error', 'mxchat') . '</span>';
2710 + } elseif ($status['status'] === 'complete') {
2711 + if ($status['failed_urls'] > 0) {
2712 + $html .= '<span class="mxchat-status-badge mxchat-status-warning">' .
2713 + sprintf(esc_html__('Completed with %d failures', 'mxchat'), $status['failed_urls']) . '</span>';
2714 + } else {
2715 + $html .= '<span class="mxchat-status-badge mxchat-status-success">' . esc_html__('Complete', 'mxchat') . '</span>';
2716 + }
2717 + }
2718 +
2719 + $html .= '</div>'; // End header
2720 +
2721 + // Progress bar
2722 + $html .= '<div class="mxchat-progress-bar">';
2723 + $html .= '<div class="mxchat-progress-fill" style="width: ' . esc_attr($status['percentage']) . '%"></div>';
2724 + $html .= '</div>';
2725 +
2726 + // Status details
2727 + $html .= '<div class="mxchat-status-details">';
2728 + $html .= '<p>' . sprintf(
2729 + esc_html__('Progress: %d of %d URLs (%d%%)', 'mxchat'),
2730 + $status['processed_urls'],
2731 + $status['total_urls'],
2732 + $status['percentage']
2733 + ) . '</p>';
2734 +
2735 + // Show failed URLs count if any
2736 + if ($status['failed_urls'] > 0) {
2737 + $html .= '<p><strong>' . esc_html__('Failed URLs:', 'mxchat') . '</strong> ' . esc_html($status['failed_urls']) . '</p>';
2738 + }
2739 +
2740 + $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
2741 + $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
2742 +
2743 + // Add completion summary if available AND it's an array
2744 + if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
2745 + $summary = $status['completion_summary'];
2746 + $html .= '<div class="mxchat-completion-summary">';
2747 + $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
2748 + $html .= '<p><strong>' . esc_html__('Total URLs:', 'mxchat') . '</strong> ' . esc_html($summary['total_urls']) . '</p>';
2749 + $html .= '<p><strong>' . esc_html__('Successful:', 'mxchat') . '</strong> ' . esc_html($summary['successful_urls']) . '</p>';
2750 + $html .= '<p><strong>' . esc_html__('Failed:', 'mxchat') . '</strong> ' . esc_html($summary['failed_urls']) . '</p>';
2751 + $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
2752 + $html .= '</div>';
2753 + }
2754 +
2755 + // Add error messages if any (but not the failed URLs list)
2756 + if (!empty($status['error']) || !empty($status['last_error'])) {
2757 + $html .= '<div class="mxchat-error-notice">';
2758 +
2759 + if (!empty($status['error'])) {
2760 + $html .= '<p class="error">' . esc_html($status['error']) . '</p>';
2761 + }
2762 +
2763 + if (!empty($status['last_error'])) {
2764 + $html .= '<p class="last-error">' . esc_html__('Last error:', 'mxchat') . ' ' . esc_html($status['last_error']) . '</p>';
2765 + }
2766 +
2767 + $html .= '</div>';
2768 + }
2769 +
2770 + $html .= '</div>'; // End details
2771 + $html .= '</div>'; // End card
2772 +
2773 + return $html;
2774 +}
2775 +
2776 +
2777 +/**
2778 + * Render failed pages list
2779 + */
2780 +private function mxchat_render_failed_pages_list($failed_pages_list) {
2781 + // Validate that $failed_pages_list is an array and not empty
2782 + if (!is_array($failed_pages_list) || empty($failed_pages_list)) {
2783 + return '';
2784 + }
2785 +
2786 + $html = '<div class="mxchat-error-notice">';
2787 + $html .= '<div class="mxchat-failed-pages-container">';
2788 + $html .= '<h5>' . sprintf(esc_html__('Failed Pages (%d)', 'mxchat'), count($failed_pages_list)) . '</h5>';
2789 + $html .= '<details>';
2790 + $html .= '<summary>' . esc_html__('Show Failed Pages', 'mxchat') . '</summary>';
2791 + $html .= '<div class="mxchat-failed-pages-list">';
2792 +
2793 + // Create table for failed pages
2794 + $html .= '<table class="widefat striped">';
2795 + $html .= '<thead><tr>';
2796 + $html .= '<th>' . esc_html__('Page', 'mxchat') . '</th>';
2797 + $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
2798 + $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
2799 + $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
2800 + $html .= '</tr></thead><tbody>';
2801 +
2802 + // Sort failed pages by most recent
2803 + $sorted_failed_pages = $failed_pages_list;
2804 + usort($sorted_failed_pages, function($a, $b) {
2805 + return ($b['time'] ?? 0) - ($a['time'] ?? 0);
2806 + });
2807 +
2808 + foreach ($sorted_failed_pages as $item) {
2809 + // Ensure $item is an array before accessing its elements
2810 + if (!is_array($item)) {
2811 + continue;
2812 + }
2813 +
2814 + $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
2815 + $html .= '<tr>';
2816 + $html .= '<td>' . esc_html__('Page', 'mxchat') . ' ' . esc_html($item['page'] ?? 'Unknown') . '</td>';
2817 + $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
2818 + $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
2819 + $html .= '<td>' . esc_html($time_ago) . '</td>';
2820 + $html .= '</tr>';
2821 + }
2822 +
2823 + $html .= '</tbody></table>';
2824 + $html .= '</div></details></div></div>';
2825 +
2826 + return $html;
2827 +}
2828 +
2829 +/**
2830 + * Render failed URLs list
2831 + */
2832 +private function mxchat_render_failed_urls_list($failed_urls_list) {
2833 + // Validate that $failed_urls_list is an array and not empty
2834 + if (!is_array($failed_urls_list) || empty($failed_urls_list)) {
2835 + return '';
2836 + }
2837 +
2838 + $html = '<div class="mxchat-failed-urls-container">';
2839 + $html .= '<h5>' . sprintf(esc_html__('Failed URLs (%d)', 'mxchat'), count($failed_urls_list)) . '</h5>';
2840 + $html .= '<details>';
2841 + $html .= '<summary>' . esc_html__('Show Failed URLs', 'mxchat') . '</summary>';
2842 + $html .= '<div class="mxchat-failed-urls-list">';
2843 +
2844 + // Create table for failed URLs
2845 + $html .= '<table class="widefat striped">';
2846 + $html .= '<thead><tr>';
2847 + $html .= '<th>' . esc_html__('URL', 'mxchat') . '</th>';
2848 + $html .= '<th>' . esc_html__('Error', 'mxchat') . '</th>';
2849 + $html .= '<th>' . esc_html__('Retries', 'mxchat') . '</th>';
2850 + $html .= '<th>' . esc_html__('Time', 'mxchat') . '</th>';
2851 + $html .= '</tr></thead><tbody>';
2852 +
2853 + // Sort failed URLs by most recent
2854 + $sorted_failed_urls = $failed_urls_list;
2855 + usort($sorted_failed_urls, function($a, $b) {
2856 + return ($b['time'] ?? 0) - ($a['time'] ?? 0);
2857 + });
2858 +
2859 + // Show up to 50 failed URLs
2860 + $display_urls = array_slice($sorted_failed_urls, 0, 50);
2861 +
2862 + foreach ($display_urls as $item) {
2863 + // Ensure $item is an array before accessing its elements
2864 + if (!is_array($item)) {
2865 + continue;
2866 + }
2867 +
2868 + $url = $item['url'] ?? '';
2869 + $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
2870 +
2871 + // Truncate URL for display
2872 + $display_url = strlen($url) > 50 ? substr($url, 0, 47) . '...' : $url;
2873 +
2874 + $html .= '<tr>';
2875 + $html .= '<td style="word-break: break-all;">';
2876 + if (!empty($url)) {
2877 + $html .= '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($display_url) . '</a>';
2878 + } else {
2879 + $html .= esc_html__('Unknown URL', 'mxchat');
2880 + }
2881 + $html .= '</td>';
2882 + $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
2883 + $html .= '<td>' . esc_html($item['retries'] ?? 'N/A') . '</td>';
2884 + $html .= '<td>' . esc_html($time_ago) . '</td>';
2885 + $html .= '</tr>';
2886 + }
2887 +
2888 + $html .= '</tbody></table>';
2889 +
2890 + if (count($failed_urls_list) > 50) {
2891 + $html .= '<div class="mxchat-failed-urls-more">+ ' .
2892 + (count($failed_urls_list) - 50) .
2893 + ' ' . esc_html__('more failed URLs not shown', 'mxchat') . '</div>';
2894 + }
2895 +
2896 + $html .= '</div></details></div>';
2897 +
2898 + return $html;
2899 +}
2900 +
2901 +/**
2902 + * Get all ACF fields for a specific post
2903 + */
2904 +public function mxchat_get_acf_fields_for_post($post_id) {
2905 + if (!function_exists('get_fields')) {
2906 + return array();
2907 + }
2908 +
2909 + $fields = get_fields($post_id);
2910 + if (!$fields || !is_array($fields)) {
2911 + return array();
2912 + }
2913 +
2914 + return $fields;
2915 +}
2916 +
2917 +/**
2918 + * Format ACF field values for content extraction
2919 + */
2920 +public function mxchat_format_acf_field_value($value, $field_name = '', $post_id = 0) {
2921 + if (empty($value)) {
2922 + return '';
2923 + }
2924 +
2925 + // Handle WP_Post objects first (THIS IS THE KEY FIX)
2926 + if ($value instanceof WP_Post) {
2927 + return $value->post_title ?: '';
2928 + }
2929 +
2930 + // Handle other WP objects
2931 + if (is_object($value)) {
2932 + if (isset($value->post_title)) {
2933 + return $value->post_title;
2934 + } elseif (isset($value->display_name)) {
2935 + return $value->display_name;
2936 + } elseif (isset($value->name)) {
2937 + return $value->name;
2938 + } elseif (method_exists($value, '__toString')) {
2939 + try {
2940 + return (string) $value;
2941 + } catch (Exception $e) {
2942 + return '';
2943 + }
2944 + }
2945 + // For any other objects, return empty string
2946 + return '';
2947 + }
2948 +
2949 + // Handle different ACF field types
2950 + if (is_array($value)) {
2951 + // Check if it's an image/file field
2952 + if (isset($value['url'])) {
2953 + // Image field - return alt text, title, or caption
2954 + if (!empty($value['alt'])) {
2955 + return $value['alt'];
2956 + } elseif (!empty($value['title'])) {
2957 + return $value['title'];
2958 + } elseif (!empty($value['caption'])) {
2959 + return $value['caption'];
2960 + } else {
2961 + return ''; // Don't include just the URL
2962 + }
2963 + }
2964 +
2965 + // Check if it's a post object or relationship field
2966 + if (isset($value['post_title'])) {
2967 + return $value['post_title'];
2968 + }
2969 +
2970 + // Check if it's a user field
2971 + if (isset($value['display_name'])) {
2972 + return $value['display_name'];
2973 + }
2974 +
2975 + // Check if it's a taxonomy term
2976 + if (isset($value['name']) && isset($value['taxonomy'])) {
2977 + return $value['name'];
2978 + }
2979 +
2980 + // Check if it's a select field with label
2981 + if (isset($value['label'])) {
2982 + return $value['label'];
2983 + }
2984 +
2985 + // Check for repeater field or flexible content
2986 + if (is_numeric(key($value))) {
2987 + $sub_values = array();
2988 + foreach ($value as $sub_item) {
2989 + if (is_array($sub_item)) {
2990 + // For repeater/flexible content, extract text values
2991 + $sub_text = $this->mxchat_extract_text_from_acf_array($sub_item);
2992 + if (!empty($sub_text)) {
2993 + $sub_values[] = $sub_text;
2994 + }
2995 + } elseif ($sub_item instanceof WP_Post) {
2996 + // Handle WP_Post objects in arrays
2997 + $sub_values[] = $sub_item->post_title ?: '';
2998 + } else {
2999 + $sub_values[] = (string) $sub_item;
3000 + }
3001 + }
3002 + return implode(', ', array_filter($sub_values));
3003 + }
3004 +
3005 + // For other arrays, try to extract meaningful text
3006 + $text_values = array();
3007 + foreach ($value as $key => $val) {
3008 + if (is_string($val) && !empty(trim($val))) {
3009 + $text_values[] = trim($val);
3010 + } elseif ($val instanceof WP_Post) {
3011 + // Handle WP_Post objects in associative arrays
3012 + $text_values[] = $val->post_title ?: '';
3013 + } elseif (is_array($val) && isset($val['post_title'])) {
3014 + $text_values[] = $val['post_title'];
3015 + } elseif (is_array($val) && isset($val['name'])) {
3016 + $text_values[] = $val['name'];
3017 + }
3018 + }
3019 +
3020 + return implode(', ', array_filter($text_values));
3021 + }
3022 +
3023 + // Handle boolean values
3024 + if (is_bool($value)) {
3025 + return $value ? 'Yes' : 'No';
3026 + }
3027 +
3028 + // Handle numeric values
3029 + if (is_numeric($value)) {
3030 + return (string) $value;
3031 + }
3032 +
3033 + // Handle string values
3034 + if (is_string($value)) {
3035 + return trim($value);
3036 + }
3037 +
3038 + // For anything else that we can't handle, return empty string
3039 + // This prevents the "Object could not be converted to string" error
3040 + return '';
3041 +}
3042 +
3043 +/**
3044 + * Extract text from complex ACF array structures
3045 + */
3046 +private function mxchat_extract_text_from_acf_array($array) {
3047 + if (!is_array($array)) {
3048 + return '';
3049 + }
3050 +
3051 + $text_parts = array();
3052 +
3053 + foreach ($array as $key => $value) {
3054 + if (is_string($value) && !empty(trim($value))) {
3055 + // Skip keys that are likely to be IDs or technical values
3056 + if (!is_numeric($value) || strlen($value) > 10) {
3057 + $text_parts[] = trim($value);
3058 + }
3059 + } elseif ($value instanceof WP_Post) {
3060 + // Handle WP_Post objects
3061 + $text_parts[] = $value->post_title ?: '';
3062 + } elseif (is_array($value)) {
3063 + if (isset($value['post_title'])) {
3064 + $text_parts[] = $value['post_title'];
3065 + } elseif (isset($value['name'])) {
3066 + $text_parts[] = $value['name'];
3067 + } elseif (isset($value['label'])) {
3068 + $text_parts[] = $value['label'];
3069 + }
3070 + } elseif (is_object($value)) {
3071 + // Handle other objects safely
3072 + if (isset($value->post_title)) {
3073 + $text_parts[] = $value->post_title;
3074 + } elseif (isset($value->name)) {
3075 + $text_parts[] = $value->name;
3076 + } elseif (isset($value->display_name)) {
3077 + $text_parts[] = $value->display_name;
3078 + }
3079 + }
3080 + }
3081 +
3082 + return implode(', ', array_filter($text_parts));
3083 +}
3084 +
3085 +public function mxchat_handle_post_update($post_id, $post, $update) {
3086 + // Basic validation checks
3087 + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
3088 + return;
3089 + }
3090 +
3091 + $post_type = $post->post_type;
3092 +
3093 + // Check if sync is enabled for this post type
3094 + $should_sync = false;
3095 +
3096 + // Check built-in post types first
3097 + if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
3098 + $should_sync = true;
3099 + } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
3100 + $should_sync = true;
3101 + } else {
3102 + // Check custom post types
3103 + $option_name = 'mxchat_auto_sync_' . $post_type;
3104 + if (get_option($option_name) === '1') {
3105 + $should_sync = true;
3106 + }
3107 + }
3108 +
3109 + if (!$should_sync) {
3110 + return;
3111 + }
3112 +
3113 + // Check if we have stored the previous status and URL in our transients
3114 + $previous_status_key = 'mxchat_prev_status_' . $post_id;
3115 + $previous_status = get_transient($previous_status_key);
3116 +
3117 + $previous_url_key = 'mxchat_prev_url_' . $post_id;
3118 + $previous_url = get_transient($previous_url_key);
3119 +
3120 + // If the post was previously published but is now not published, remove from knowledge base
3121 + if ($previous_status === 'publish' && $post->post_status !== 'publish') {
3122 + // Use the stored URL from when it was published, or fall back to current permalink
3123 + $source_url = $previous_url ?: get_permalink($post_id);
3124 +
3125 + if ($source_url) {
3126 + // Check if Pinecone is enabled
3127 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3128 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3129 +
3130 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3131 + // Delete from Pinecone
3132 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
3133 + } else {
3134 + // Delete from WordPress DB
3135 + global $wpdb;
3136 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3137 +
3138 + $result = $wpdb->delete(
3139 + $table_name,
3140 + array('source_url' => $source_url),
3141 + array('%s')
3142 + );
3143 + }
3144 + }
3145 +
3146 + // Clean up the transients and exit early
3147 + delete_transient($previous_status_key);
3148 + delete_transient($previous_url_key);
3149 + return;
3150 + }
3151 +
3152 + // Store the current status for next time (if this is an update)
3153 + if ($update) {
3154 + set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
3155 +
3156 + // If the post is currently published, also store its URL
3157 + if ($post->post_status === 'publish') {
3158 + $current_url = get_permalink($post_id);
3159 + set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
3160 + }
3161 + }
3162 +
3163 + // Only process currently published content for adding/updating
3164 + if ($post->post_status === 'publish') {
3165 + // Get the source URL
3166 + $source_url = get_permalink($post_id);
3167 +
3168 + // Get content with proper formatting (matching ajax_mxchat_process_selected_content)
3169 + $title = get_the_title($post_id);
3170 + $content = get_post_field('post_content', $post_id);
3171 + $excerpt = get_post_field('post_excerpt', $post_id);
3172 +
3173 + // Remove shortcode tags but preserve content inside them
3174 + $content = $this->strip_shortcode_tags_preserve_content($content);
3175 + $excerpt = $this->strip_shortcode_tags_preserve_content($excerpt);
3176 +
3177 + // Strip tags but preserve structure (don't use 'the_content' filter as it may re-add shortcodes)
3178 + $content = wp_strip_all_tags($content);
3179 +
3180 + // Combine title, short description (if exists), and content
3181 + $final_content = $title . "\n\n";
3182 +
3183 + // Add short description if it exists (WooCommerce products use post_excerpt for short description)
3184 + if (!empty($excerpt)) {
3185 + $final_content .= "Short Description: " . wp_strip_all_tags($excerpt) . "\n\n";
3186 + }
3187 +
3188 + $final_content .= $content;
3189 +
3190 + // For WooCommerce products, include pricing and product details
3191 + if ($post_type === 'product' && class_exists('WooCommerce')) {
3192 + $product = wc_get_product($post_id);
3193 +
3194 + if ($product) {
3195 + // Get pricing information
3196 + $regular_price = $product->get_regular_price();
3197 + $sale_price = $product->get_sale_price();
3198 + $price = $product->get_price();
3199 + $sku = $product->get_sku();
3200 +
3201 + // Get currency symbol
3202 + $currency_symbol = get_woocommerce_currency_symbol();
3203 +
3204 + // Add pricing information
3205 + $final_content .= "\n";
3206 + if (!empty($regular_price)) {
3207 + $final_content .= "Price: " . $currency_symbol . $regular_price . "\n";
3208 + } elseif (!empty($price)) {
3209 + $final_content .= "Price: " . $currency_symbol . $price . "\n";
3210 + }
3211 +
3212 + if (!empty($sale_price) && $sale_price !== $regular_price) {
3213 + $final_content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
3214 + }
3215 +
3216 + // Handle variable products - show price range
3217 + if ($product->is_type('variable')) {
3218 + $min_price = $product->get_variation_price('min');
3219 + $max_price = $product->get_variation_price('max');
3220 + if ($min_price !== $max_price) {
3221 + $final_content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
3222 + }
3223 + }
3224 +
3225 + if (!empty($sku)) {
3226 + $final_content .= "SKU: " . $sku . "\n";
3227 + }
3228 +
3229 + // Get product categories
3230 + $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
3231 + if (!empty($categories) && !is_wp_error($categories)) {
3232 + $final_content .= "Categories: " . implode(', ', $categories) . "\n";
3233 + }
3234 + }
3235 + }
3236 +
3237 + // For custom post types like job_listing, include additional fields
3238 + if ($post_type === 'job_listing') {
3239 + // Add job-specific meta if available
3240 + $job_location = get_post_meta($post_id, '_job_location', true);
3241 + if (!empty($job_location)) {
3242 + $final_content .= "\n\nLocation: " . $job_location;
3243 + }
3244 +
3245 + // Get job type terms
3246 + $job_types = get_the_terms($post_id, 'job_listing_type');
3247 + if (!empty($job_types) && !is_wp_error($job_types)) {
3248 + $types = array();
3249 + foreach ($job_types as $type) {
3250 + $types[] = $type->name;
3251 + }
3252 + $final_content .= "\n\nJob Type: " . implode(', ', $types);
3253 + }
3254 +
3255 + // Get company name if available
3256 + $company_name = get_post_meta($post_id, '_company_name', true);
3257 + if (!empty($company_name)) {
3258 + $final_content .= "\n\nCompany: " . $company_name;
3259 + }
3260 + }
3261 +
3262 + // ADD ACF FIELDS SUPPORT (matches ajax_mxchat_process_selected_content behavior)
3263 + $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
3264 + if (!empty($acf_fields)) {
3265 + $acf_content_parts = array();
3266 +
3267 + foreach ($acf_fields as $field_name => $field_value) {
3268 + $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
3269 + if (!empty($formatted_value)) {
3270 + // Convert field name to readable label
3271 + $field_label = ucwords(str_replace(['_', '-'], ' ', $field_name));
3272 + $acf_content_parts[] = $field_label . ": " . $formatted_value;
3273 + }
3274 + }
3275 +
3276 + if (!empty($acf_content_parts)) {
3277 + $final_content .= "\n\n" . implode("\n", $acf_content_parts);
3278 + }
3279 + }
3280 +
3281 + // Get API key with proper model detection
3282 + $options = get_option('mxchat_options');
3283 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3284 +
3285 + if (strpos($selected_model, 'voyage') === 0) {
3286 + $api_key = $options['voyage_api_key'] ?? '';
3287 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3288 + $api_key = $options['gemini_api_key'] ?? '';
3289 + } else {
3290 + $api_key = $options['api_key'] ?? '';
3291 + }
3292 +
3293 + if (empty($api_key)) {
3294 + return;
3295 + }
3296 +
3297 + // Use the centralized utility function for storage
3298 + $result = MxChat_Utils::submit_content_to_db(
3299 + $final_content,
3300 + $source_url,
3301 + $api_key,
3302 + md5($source_url) // Vector ID for Pinecone
3303 + );
3304 +
3305 + // After successful storage, apply role restriction based on tags
3306 + if (!is_wp_error($result)) {
3307 + $this->apply_role_restriction_to_post($post_id, $source_url);
3308 + }
3309 + }
3310 +
3311 + // Clean up the stored previous status if not used above
3312 + if ($previous_status !== 'publish' || $post->post_status === 'publish') {
3313 + delete_transient($previous_status_key);
3314 + delete_transient($previous_url_key);
3315 + }
3316 +}
3317 +
3318 +/**
3319 + * Store the post status and URL before update to detect status transitions
3320 + * This runs before the post is actually updated in the database
3321 + */
3322 +public function mxchat_store_pre_update_status($post_id, $data) {
3323 + // Get the current post from database (before update)
3324 + $current_post = get_post($post_id);
3325 +
3326 + if ($current_post) {
3327 + // Store the current status temporarily
3328 + $status_key = 'mxchat_prev_status_' . $post_id;
3329 + set_transient($status_key, $current_post->post_status, HOUR_IN_SECONDS);
3330 +
3331 + // If the post is currently published, also store its URL
3332 + if ($current_post->post_status === 'publish') {
3333 + $url_key = 'mxchat_prev_url_' . $post_id;
3334 + $current_url = get_permalink($post_id);
3335 + set_transient($url_key, $current_url, HOUR_IN_SECONDS);
3336 + }
3337 + }
3338 +}
3339 +
3340 +public function mxchat_handle_post_delete($post_id) {
3341 + // Get post data before it's deleted
3342 + $post = get_post($post_id);
3343 +
3344 + // Basic validation
3345 + if (!$post || wp_is_post_revision($post_id)) {
3346 + return;
3347 + }
3348 +
3349 + $post_type = $post->post_type;
3350 +
3351 + // Check if sync is enabled for this post type
3352 + $should_sync = false;
3353 +
3354 + // Check built-in post types first
3355 + if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
3356 + $should_sync = true;
3357 + } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
3358 + $should_sync = true;
3359 + } else {
3360 + // Check custom post types
3361 + $option_name = 'mxchat_auto_sync_' . $post_type;
3362 + if (get_option($option_name) === '1') {
3363 + $should_sync = true;
3364 + }
3365 + }
3366 +
3367 + if (!$should_sync) {
3368 + return;
3369 + }
3370 +
3371 + // Get the URL before post is deleted
3372 + $source_url = get_permalink($post_id);
3373 + if (!$source_url) {
3374 + //error_log('MXChat: Failed to get permalink for post ' . $post_id);
3375 + return;
3376 + }
3377 +
3378 + // Check if Pinecone is enabled
3379 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3380 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3381 +
3382 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3383 + // Delete from Pinecone
3384 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
3385 + } else {
3386 + // Delete from WordPress DB
3387 + global $wpdb;
3388 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3389 +
3390 + $result = $wpdb->delete(
3391 + $table_name,
3392 + array('source_url' => $source_url),
3393 + array('%s')
3394 + );
3395 +
3396 + if ($result === false) {
3397 + //error_log('MXChat: WordPress DB deletion failed for URL: ' . $source_url);
3398 + }
3399 + }
3400 +}
3401 +
3402 +
3403 + /**
3404 + * Deletes data from Pinecone using a source URL
3405 + */
3406 + public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
3407 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3408 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3409 +
3410 + if (empty($host) || empty($api_key)) {
3411 + //error_log('MXChat: Pinecone deletion failed - missing configuration');
3412 + return false;
3413 + }
3414 +
3415 + $api_endpoint = "https://{$host}/vectors/delete";
3416 + $vector_id = md5($source_url);
3417 +
3418 + $request_body = array(
3419 + 'ids' => array($vector_id)
3420 + );
3421 +
3422 + $response = wp_remote_post($api_endpoint, array(
3423 + 'headers' => array(
3424 + 'Api-Key' => $api_key,
3425 + 'accept' => 'application/json',
3426 + 'content-type' => 'application/json'
3427 + ),
3428 + 'body' => wp_json_encode($request_body),
3429 + 'timeout' => 30
3430 + ));
3431 +
3432 + if (is_wp_error($response)) {
3433 + //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
3434 + return false;
3435 + }
3436 +
3437 + $response_code = wp_remote_retrieve_response_code($response);
3438 + if ($response_code !== 200) {
3439 + //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
3440 + return false;
3441 + }
3442 +
3443 + return true;
3444 + }
3445 +
3446 +
3447 +
3448 +public function mxchat_handle_product_change($post_id, $post, $update) {
3449 + if ($post->post_type !== 'product') {
3450 + return;
3451 + }
3452 +
3453 + if ($post->post_status === 'publish') {
3454 + add_action('shutdown', function() use ($post_id) {
3455 + $product = wc_get_product($post_id);
3456 + if ($product) {
3457 + $this->mxchat_store_product_embedding($product);
3458 + }
3459 + });
3460 + }
3461 +}
3462 +
3463 +/**
3464 + * Store WooCommerce product embeddings
3465 + */
3466 +private function mxchat_store_product_embedding($product) {
3467 + if (!isset($this->options['enable_woocommerce_integration']) ||
3468 + !in_array($this->options['enable_woocommerce_integration'], ['1', 'on'])) {
3469 + return;
3470 + }
3471 +
3472 + $source_url = get_permalink($product->get_id());
3473 + $product_id = $product->get_id();
3474 +
3475 + // Build product content
3476 + $title = $product->get_name();
3477 + $description = $product->get_description();
3478 + $short_description = $product->get_short_description();
3479 + $regular_price = $product->get_regular_price();
3480 + $sale_price = $product->get_sale_price();
3481 + $price = $product->get_price();
3482 + $sku = $product->get_sku();
3483 +
3484 + // Get currency symbol
3485 + $currency_symbol = get_woocommerce_currency_symbol();
3486 +
3487 + // Format content consistently
3488 + $content = $title . "\n\n";
3489 +
3490 + if (!empty($short_description)) {
3491 + $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
3492 + }
3493 +
3494 + if (!empty($description)) {
3495 + $content .= wp_strip_all_tags($description) . "\n\n";
3496 + }
3497 +
3498 + // Add pricing information
3499 + if (!empty($regular_price)) {
3500 + $content .= "Price: " . $currency_symbol . $regular_price . "\n";
3501 + } elseif (!empty($price)) {
3502 + $content .= "Price: " . $currency_symbol . $price . "\n";
3503 + }
3504 +
3505 + if (!empty($sale_price) && $sale_price !== $regular_price) {
3506 + $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
3507 + }
3508 +
3509 + // Handle variable products - show price range
3510 + if ($product->is_type('variable')) {
3511 + $min_price = $product->get_variation_price('min');
3512 + $max_price = $product->get_variation_price('max');
3513 + if ($min_price !== $max_price) {
3514 + $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
3515 + }
3516 + }
3517 +
3518 + if (!empty($sku)) {
3519 + $content .= "SKU: " . $sku . "\n";
3520 + }
3521 +
3522 + // Get product categories
3523 + $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
3524 + if (!empty($categories) && !is_wp_error($categories)) {
3525 + $content .= "Categories: " . implode(', ', $categories) . "\n";
3526 + }
3527 +
3528 + // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
3529 + $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
3530 + if (!empty($custom_tabs) && is_array($custom_tabs)) {
3531 + foreach ($custom_tabs as $tab) {
3532 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
3533 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
3534 +
3535 + if (!empty($tab_title) && !empty($tab_content)) {
3536 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
3537 + }
3538 + }
3539 + }
3540 +
3541 + // Also check for reusable/saved tabs applied to this product
3542 + $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
3543 + if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
3544 + // Get the saved tabs option
3545 + $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
3546 + if (!empty($saved_tabs) && is_array($saved_tabs)) {
3547 + foreach ($applied_saved_tabs as $saved_tab_id) {
3548 + if (isset($saved_tabs[$saved_tab_id])) {
3549 + $tab = $saved_tabs[$saved_tab_id];
3550 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
3551 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
3552 +
3553 + if (!empty($tab_title) && !empty($tab_content)) {
3554 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
3555 + }
3556 + }
3557 + }
3558 + }
3559 + }
3560 +
3561 + // Get API key with proper model detection
3562 + $options = get_option('mxchat_options');
3563 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3564 +
3565 + if (strpos($selected_model, 'voyage') === 0) {
3566 + $api_key = $options['voyage_api_key'] ?? '';
3567 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3568 + $api_key = $options['gemini_api_key'] ?? '';
3569 + } else {
3570 + $api_key = $options['api_key'] ?? '';
3571 + }
3572 +
3573 + if (empty($api_key)) {
3574 + //error_log('MxChat Auto-sync: No API key configured for embedding model');
3575 + return;
3576 + }
3577 +
3578 + // Use the centralized utility function for storage
3579 + $result = MxChat_Utils::submit_content_to_db(
3580 + $content,
3581 + $source_url,
3582 + $api_key,
3583 + md5($source_url) // Vector ID for Pinecone
3584 + );
3585 +
3586 + // After successful storage, apply role restriction based on tags
3587 + if (!is_wp_error($result)) {
3588 + $this->apply_role_restriction_to_post($product_id, $source_url);
3589 + }
3590 +
3591 + if (is_wp_error($result)) {
3592 + //error_log('MxChat WooCommerce sync failed for product ' . $product_id . ': ' . $result->get_error_message());
3593 + }
3594 +}
3595 +
3596 +public function mxchat_handle_product_delete($post_id) {
3597 + if (get_post_type($post_id) !== 'product') {
3598 + return;
3599 + }
3600 +
3601 + $source_url = get_permalink($post_id);
3602 +
3603 + // Check if Pinecone is enabled
3604 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3605 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3606 +
3607 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3608 + // Delete from Pinecone
3609 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
3610 + } else {
3611 + // Delete from WordPress DB
3612 + global $wpdb;
3613 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3614 +
3615 + $wpdb->delete(
3616 + $table_name,
3617 + array('source_url' => $source_url),
3618 + array('%s')
3619 + );
3620 + }
3621 +}
3622 +
3623 +/**
3624 + * Handle individual Pinecone content deletion
3625 + */
3626 +public function mxchat_handle_pinecone_prompt_delete() {
3627 + // Check permissions
3628 + if (!current_user_can('manage_options')) {
3629 + wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
3630 + }
3631 +
3632 + // Verify nonce
3633 + if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'mxchat_delete_pinecone_prompt_nonce')) {
3634 + wp_die(esc_html__('Security check failed.', 'mxchat'));
3635 + }
3636 +
3637 + $vector_id = isset($_GET['vector_id']) ? sanitize_text_field($_GET['vector_id']) : '';
3638 +
3639 + if (empty($vector_id)) {
3640 + set_transient('mxchat_admin_notice_error',
3641 + esc_html__('Invalid vector ID.', 'mxchat'),
3642 + 30
3643 + );
3644 + wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
3645 + exit;
3646 + }
3647 +
3648 + // Get Pinecone settings
3649 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3650 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3651 +
3652 + if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
3653 + set_transient('mxchat_admin_notice_error',
3654 + esc_html__('Pinecone is not properly configured.', 'mxchat'),
3655 + 30
3656 + );
3657 + wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
3658 + exit;
3659 + }
3660 +
3661 + // Delete from Pinecone
3662 + $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
3663 + $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
3664 + $vector_id,
3665 + $pinecone_options['mxchat_pinecone_api_key'],
3666 + $pinecone_options['mxchat_pinecone_host']
3667 + );
3668 +
3669 + if ($result['success']) {
3670 + // No cache clearing needed since we removed caching
3671 + set_transient('mxchat_admin_notice_success',
3672 + esc_html__('Entry deleted successfully from Pinecone.', 'mxchat'),
3673 + 30
3674 + );
3675 + } else {
3676 + set_transient('mxchat_admin_notice_error',
3677 + esc_html__('Failed to delete entry: ', 'mxchat') . $result['message'],
3678 + 30
3679 + );
3680 + }
3681 +
3682 + wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
3683 + exit;
3684 +}
3685 +/**
3686 + * Handle individual Pinecone content deletion via AJAX
3687 + */
3688 +public function ajax_mxchat_delete_pinecone_prompt() {
3689 + // Verify nonce and permissions
3690 + if (!check_ajax_referer('mxchat_delete_pinecone_prompt_nonce', 'nonce', false)) {
3691 + wp_send_json_error('Invalid nonce');
3692 + exit;
3693 + }
3694 +
3695 + if (!current_user_can('manage_options')) {
3696 + wp_send_json_error('Unauthorized access');
3697 + exit;
3698 + }
3699 +
3700 + $vector_id = isset($_POST['vector_id']) ? sanitize_text_field($_POST['vector_id']) : '';
3701 + $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
3702 +
3703 + if (empty($vector_id)) {
3704 + wp_send_json_error('Missing vector ID');
3705 + exit;
3706 + }
3707 +
3708 + // Get bot-specific Pinecone settings
3709 + $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
3710 + $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
3711 +
3712 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3713 +
3714 + if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
3715 + wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
3716 + exit;
3717 + }
3718 +
3719 + // Delete from the correct Pinecone index
3720 + $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
3721 + $vector_id,
3722 + $pinecone_options['mxchat_pinecone_api_key'],
3723 + $pinecone_options['mxchat_pinecone_host']
3724 + );
3725 +
3726 + if ($result['success']) {
3727 + // No cache clearing needed since we removed caching
3728 + wp_send_json_success(array(
3729 + 'message' => 'Entry deleted successfully from Pinecone',
3730 + 'vector_id' => $vector_id,
3731 + 'bot_id' => $bot_id
3732 + ));
3733 + } else {
3734 + wp_send_json_error('Failed to delete from Pinecone: ' . $result['message']);
3735 + }
3736 +
3737 + exit;
3738 +}
3739 +
3740 +/**
3741 + * Get hierarchical roles for dropdown
3742 + */
3743 +public function mxchat_get_role_options() {
3744 + return array(
3745 + 'public' => __('Public (Everyone)', 'mxchat'),
3746 + 'logged_in' => __('Logged In Users', 'mxchat'),
3747 + 'subscriber' => __('Subscribers & Above', 'mxchat'),
3748 + 'contributor' => __('Contributors & Above', 'mxchat'),
3749 + 'author' => __('Authors & Above', 'mxchat'),
3750 + 'editor' => __('Editors & Above', 'mxchat'),
3751 + 'administrator' => __('Administrators Only', 'mxchat')
3752 + );
3753 +}
3754 +
3755 +/**
3756 + * Check if user has access to content based on role restriction
3757 + */
3758 +public function mxchat_user_has_content_access($role_restriction) {
3759 + // Public content is always accessible
3760 + if ($role_restriction === 'public' || empty($role_restriction)) {
3761 + return true;
3762 + }
3763 +
3764 + // Check if user is logged in for logged_in restriction
3765 + if ($role_restriction === 'logged_in') {
3766 + return is_user_logged_in();
3767 + }
3768 +
3769 + // If not logged in, no access to role-restricted content
3770 + if (!is_user_logged_in()) {
3771 + return false;
3772 + }
3773 +
3774 + $user = wp_get_current_user();
3775 + $user_roles = $user->roles;
3776 +
3777 + if (empty($user_roles)) {
3778 + return false;
3779 + }
3780 +
3781 + // Define role hierarchy (higher number = higher access)
3782 + $hierarchy = array(
3783 + 'subscriber' => 1,
3784 + 'contributor' => 2,
3785 + 'author' => 3,
3786 + 'editor' => 4,
3787 + 'administrator' => 5
3788 + );
3789 +
3790 + // Get required level
3791 + $required_level = isset($hierarchy[$role_restriction]) ? $hierarchy[$role_restriction] : 0;
3792 +
3793 + // Check if user has required level or higher
3794 + foreach ($user_roles as $user_role) {
3795 + $user_level = isset($hierarchy[$user_role]) ? $hierarchy[$user_role] : 0;
3796 + if ($user_level >= $required_level) {
3797 + return true;
3798 + }
3799 + }
3800 +
3801 + return false;
3802 +}
3803 +
3804 +/**
3805 + * Handle role restriction updates via AJAX
3806 + * Removed cache clearing call since we removed caching
3807 + */
3808 +public function ajax_mxchat_update_role_restriction() {
3809 + // Verify nonce and permissions
3810 + if (!check_ajax_referer('mxchat_update_role_nonce', 'nonce', false)) {
3811 + wp_send_json_error('Invalid nonce');
3812 + exit;
3813 + }
3814 +
3815 + if (!current_user_can('manage_options')) {
3816 + wp_send_json_error('Unauthorized access');
3817 + exit;
3818 + }
3819 +
3820 + $entry_id = isset($_POST['entry_id']) ? sanitize_text_field($_POST['entry_id']) : '';
3821 + $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
3822 + $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
3823 +
3824 + if (empty($entry_id)) {
3825 + wp_send_json_error('Invalid entry ID');
3826 + exit;
3827 + }
3828 +
3829 + // Get knowledge manager instance to validate role restriction
3830 + $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
3831 + $valid_roles = array_keys($knowledge_manager->mxchat_get_role_options());
3832 + if (!in_array($role_restriction, $valid_roles)) {
3833 + wp_send_json_error('Invalid role restriction');
3834 + exit;
3835 + }
3836 +
3837 + global $wpdb;
3838 +
3839 + if ($data_source === 'pinecone') {
3840 + // Handle Pinecone role restriction (stored separately in WordPress table)
3841 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
3842 +
3843 + // Use REPLACE to insert or update the role restriction
3844 + $result = $wpdb->replace(
3845 + $roles_table,
3846 + array(
3847 + 'vector_id' => $entry_id,
3848 + 'role_restriction' => $role_restriction,
3849 + 'updated_at' => current_time('mysql')
3850 + ),
3851 + array('%s', '%s', '%s')
3852 + );
3853 +
3854 + // No cache clearing needed since we removed caching
3855 +
3856 + } else {
3857 + // Handle WordPress database role restriction (existing functionality)
3858 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3859 +
3860 + $result = $wpdb->update(
3861 + $table_name,
3862 + array('role_restriction' => $role_restriction),
3863 + array('id' => absint($entry_id)),
3864 + array('%s'),
3865 + array('%d')
3866 + );
3867 + }
3868 +
3869 + if ($result === false) {
3870 + wp_send_json_error('Database update failed: ' . $wpdb->last_error);
3871 + exit;
3872 + }
3873 +
3874 + wp_send_json_success(array(
3875 + 'message' => 'Role restriction updated successfully',
3876 + 'role_restriction' => $role_restriction,
3877 + 'data_source' => $data_source,
3878 + 'entry_id' => $entry_id
3879 + ));
3880 + exit;
3881 +}
3882 +
3883 +// ========================================
3884 +// ROLE-BASED CONTENT RESTRICTIONS - NEW FUNCTIONS
3885 +// Add these to your MxChat_Knowledge_Manager class
3886 +// ========================================
3887 +
3888 +/**
3889 + * Initialize role-based content hooks
3890 + * Add this call to your __construct() or mxchat_init_hooks() method
3891 + */
3892 +private function mxchat_init_role_hooks() {
3893 + // AJAX handlers for tag-role mappings
3894 + add_action('wp_ajax_mxchat_add_tag_role_mapping', array($this, 'ajax_add_tag_role_mapping'));
3895 + add_action('wp_ajax_mxchat_delete_tag_role_mapping', array($this, 'ajax_delete_tag_role_mapping'));
3896 + add_action('wp_ajax_mxchat_get_tag_role_mappings', array($this, 'ajax_get_tag_role_mappings'));
3897 + add_action('wp_ajax_mxchat_bulk_update_tag_roles', array($this, 'ajax_bulk_update_tag_roles'));
3898 +
3899 + // Hook to automatically update role restrictions when tags are added/removed
3900 + add_action('set_object_terms', array($this, 'handle_tag_change'), 10, 6);
3901 +
3902 + // Hook to apply role restrictions on auto-sync
3903 + add_action('mxchat_content_stored', array($this, 'apply_role_restriction_after_storage'), 10, 2);
3904 +}
3905 +
3906 +/**
3907 + * Add tag-role mapping via AJAX
3908 + */
3909 +public function ajax_add_tag_role_mapping() {
3910 + // Verify nonce and permissions
3911 + check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
3912 +
3913 + if (!current_user_can('manage_options')) {
3914 + wp_send_json_error('Unauthorized access');
3915 + exit;
3916 + }
3917 +
3918 + $tag_slug = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
3919 + $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
3920 +
3921 + if (empty($tag_slug)) {
3922 + wp_send_json_error('Tag slug is required');
3923 + exit;
3924 + }
3925 +
3926 + // Validate role restriction
3927 + $valid_roles = array_keys($this->mxchat_get_role_options());
3928 + if (!in_array($role_restriction, $valid_roles)) {
3929 + wp_send_json_error('Invalid role restriction');
3930 + exit;
3931 + }
3932 +
3933 + // Check if tag exists in WordPress
3934 + $term = get_term_by('slug', $tag_slug, 'post_tag');
3935 + if (!$term) {
3936 + wp_send_json_error('Tag does not exist in WordPress');
3937 + exit;
3938 + }
3939 +
3940 + // Get existing mappings
3941 + $mappings = get_option('mxchat_tag_role_mappings', array());
3942 +
3943 + // Check if mapping already exists
3944 + if (isset($mappings[$tag_slug])) {
3945 + wp_send_json_error('Mapping for this tag already exists');
3946 + exit;
3947 + }
3948 +
3949 + // Add new mapping
3950 + $mappings[$tag_slug] = $role_restriction;
3951 + update_option('mxchat_tag_role_mappings', $mappings);
3952 +
3953 + wp_send_json_success(array(
3954 + 'message' => 'Tag-role mapping added successfully',
3955 + 'tag_slug' => $tag_slug,
3956 + 'role_restriction' => $role_restriction
3957 + ));
3958 + exit;
3959 +}
3960 +
3961 +/**
3962 + * Delete tag-role mapping via AJAX
3963 + */
3964 +public function ajax_delete_tag_role_mapping() {
3965 + // Verify nonce and permissions
3966 + check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
3967 +
3968 + if (!current_user_can('manage_options')) {
3969 + wp_send_json_error('Unauthorized access');
3970 + exit;
3971 + }
3972 +
3973 + $tag_slug = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
3974 +
3975 + if (empty($tag_slug)) {
3976 + wp_send_json_error('Tag slug is required');
3977 + exit;
3978 + }
3979 +
3980 + // Get existing mappings
3981 + $mappings = get_option('mxchat_tag_role_mappings', array());
3982 +
3983 + // Check if mapping exists
3984 + if (!isset($mappings[$tag_slug])) {
3985 + wp_send_json_error('Mapping does not exist');
3986 + exit;
3987 + }
3988 +
3989 + // Remove mapping
3990 + unset($mappings[$tag_slug]);
3991 + update_option('mxchat_tag_role_mappings', $mappings);
3992 +
3993 + wp_send_json_success(array(
3994 + 'message' => 'Tag-role mapping deleted successfully',
3995 + 'tag_slug' => $tag_slug
3996 + ));
3997 + exit;
3998 +}
3999 +
4000 +/**
4001 + * Get all tag-role mappings via AJAX
4002 + */
4003 +public function ajax_get_tag_role_mappings() {
4004 + // Verify nonce and permissions
4005 + check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
4006 +
4007 + if (!current_user_can('manage_options')) {
4008 + wp_send_json_error('Unauthorized access');
4009 + exit;
4010 + }
4011 +
4012 + // Get mappings
4013 + $mappings = get_option('mxchat_tag_role_mappings', array());
4014 + $role_options = $this->mxchat_get_role_options();
4015 +
4016 + $formatted_mappings = array();
4017 +
4018 + foreach ($mappings as $tag_slug => $role_restriction) {
4019 + // Get tag object
4020 + $term = get_term_by('slug', $tag_slug, 'post_tag');
4021 +
4022 + // Count posts with this tag
4023 + $post_count = 0;
4024 + if ($term) {
4025 + $post_count = $term->count;
4026 + }
4027 +
4028 + $formatted_mappings[] = array(
4029 + 'tag_slug' => $tag_slug,
4030 + 'role_restriction' => $role_restriction,
4031 + 'role_label' => $role_options[$role_restriction] ?? $role_restriction,
4032 + 'post_count' => $post_count
4033 + );
4034 + }
4035 +
4036 + wp_send_json_success(array(
4037 + 'mappings' => $formatted_mappings
4038 + ));
4039 + exit;
4040 +}
4041 +
4042 +/**
4043 + * Bulk update role restrictions for all existing content with mapped tags
4044 + */
4045 +public function ajax_bulk_update_tag_roles() {
4046 + // Verify nonce and permissions
4047 + check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
4048 +
4049 + if (!current_user_can('manage_options')) {
4050 + wp_send_json_error('Unauthorized access');
4051 + exit;
4052 + }
4053 +
4054 + // Get mappings
4055 + $mappings = get_option('mxchat_tag_role_mappings', array());
4056 +
4057 + if (empty($mappings)) {
4058 + wp_send_json_error('No tag-role mappings found');
4059 + exit;
4060 + }
4061 +
4062 + global $wpdb;
4063 +
4064 + // Check if using Pinecone
4065 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4066 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4067 +
4068 + $updated_count = 0;
4069 + $details = array();
4070 +
4071 + foreach ($mappings as $tag_slug => $role_restriction) {
4072 + // Get all posts with this tag
4073 + $posts = get_posts(array(
4074 + 'tag' => $tag_slug,
4075 + 'post_type' => 'any',
4076 + 'posts_per_page' => -1,
4077 + 'fields' => 'ids',
4078 + 'post_status' => 'publish'
4079 + ));
4080 +
4081 + if (empty($posts)) {
4082 + continue;
4083 + }
4084 +
4085 + $tag_updated = 0;
4086 +
4087 + foreach ($posts as $post_id) {
4088 + $source_url = get_permalink($post_id);
4089 + if (!$source_url) {
4090 + continue;
4091 + }
4092 +
4093 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
4094 + // Update Pinecone role restriction
4095 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
4096 + $vector_id = md5($source_url);
4097 +
4098 + $result = $wpdb->replace(
4099 + $roles_table,
4100 + array(
4101 + 'vector_id' => $vector_id,
4102 + 'role_restriction' => $role_restriction,
4103 + 'updated_at' => current_time('mysql')
4104 + ),
4105 + array('%s', '%s', '%s')
4106 + );
4107 + } else {
4108 + // Update WordPress DB
4109 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4110 +
4111 + $result = $wpdb->update(
4112 + $table_name,
4113 + array('role_restriction' => $role_restriction),
4114 + array('source_url' => $source_url),
4115 + array('%s'),
4116 + array('%s')
4117 + );
4118 + }
4119 +
4120 + if ($result !== false) {
4121 + $tag_updated++;
4122 + $updated_count++;
4123 + }
4124 + }
4125 +
4126 + if ($tag_updated > 0) {
4127 + $details[] = sprintf(
4128 + 'Tag "%s" (%s): %d posts updated',
4129 + $tag_slug,
4130 + $role_restriction,
4131 + $tag_updated
4132 + );
4133 + }
4134 + }
4135 +
4136 + wp_send_json_success(array(
4137 + 'message' => 'Bulk update completed',
4138 + 'updated_count' => $updated_count,
4139 + 'tags_processed' => count($mappings),
4140 + 'details' => $details
4141 + ));
4142 + exit;
4143 +}
4144 +
4145 +/**
4146 + * Handle tag changes on posts (when tags are added or removed)
4147 + */
4148 +public function handle_tag_change($object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids) {
4149 + // Only process post tags
4150 + if ($taxonomy !== 'post_tag') {
4151 + return;
4152 + }
4153 +
4154 + // Get tag-role mappings
4155 + $mappings = get_option('mxchat_tag_role_mappings', array());
4156 +
4157 + if (empty($mappings)) {
4158 + return;
4159 + }
4160 +
4161 + // Get the post's URL
4162 + $source_url = get_permalink($object_id);
4163 + if (!$source_url) {
4164 + return;
4165 + }
4166 +
4167 + // Determine the highest role restriction based on tags
4168 + $highest_role = 'public';
4169 + $role_hierarchy = array(
4170 + 'public' => 0,
4171 + 'logged_in' => 1,
4172 + 'subscriber' => 2,
4173 + 'contributor' => 3,
4174 + 'author' => 4,
4175 + 'editor' => 5,
4176 + 'administrator' => 6
4177 + );
4178 +
4179 + // Get all current tags for the post
4180 + $current_tags = wp_get_post_tags($object_id, array('fields' => 'slugs'));
4181 +
4182 + // Find the highest role restriction among the tags
4183 + foreach ($current_tags as $tag_slug) {
4184 + if (isset($mappings[$tag_slug])) {
4185 + $role = $mappings[$tag_slug];
4186 + if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
4187 + $highest_role = $role;
4188 + }
4189 + }
4190 + }
4191 +
4192 + // Update the role restriction in the database
4193 + global $wpdb;
4194 +
4195 + // Check if using Pinecone
4196 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4197 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4198 +
4199 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
4200 + // Update Pinecone role restriction
4201 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
4202 + $vector_id = md5($source_url);
4203 +
4204 + $wpdb->replace(
4205 + $roles_table,
4206 + array(
4207 + 'vector_id' => $vector_id,
4208 + 'role_restriction' => $highest_role,
4209 + 'updated_at' => current_time('mysql')
4210 + ),
4211 + array('%s', '%s', '%s')
4212 + );
4213 + } else {
4214 + // Update WordPress DB
4215 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4216 +
4217 + $wpdb->update(
4218 + $table_name,
4219 + array('role_restriction' => $highest_role),
4220 + array('source_url' => $source_url),
4221 + array('%s'),
4222 + array('%s')
4223 + );
4224 + }
4225 +}
4226 +
4227 +/**
4228 + * Apply role restriction after content is stored (for auto-sync)
4229 + */
4230 +public function apply_role_restriction_after_storage($post_id, $source_url) {
4231 + // Get tag-role mappings
4232 + $mappings = get_option('mxchat_tag_role_mappings', array());
4233 +
4234 + if (empty($mappings)) {
4235 + return;
4236 + }
4237 +
4238 + // Get all tags for the post
4239 + $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
4240 +
4241 + if (empty($post_tags)) {
4242 + return;
4243 + }
4244 +
4245 + // Determine the highest role restriction based on tags
4246 + $highest_role = 'public';
4247 + $role_hierarchy = array(
4248 + 'public' => 0,
4249 + 'logged_in' => 1,
4250 + 'subscriber' => 2,
4251 + 'contributor' => 3,
4252 + 'author' => 4,
4253 + 'editor' => 5,
4254 + 'administrator' => 6
4255 + );
4256 +
4257 + foreach ($post_tags as $tag_slug) {
4258 + if (isset($mappings[$tag_slug])) {
4259 + $role = $mappings[$tag_slug];
4260 + if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
4261 + $highest_role = $role;
4262 + }
4263 + }
4264 + }
4265 +
4266 + // If no restricted tags found, return (leave as public)
4267 + if ($highest_role === 'public') {
4268 + return;
4269 + }
4270 +
4271 + // Update the role restriction
4272 + global $wpdb;
4273 +
4274 + // Check if using Pinecone
4275 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4276 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4277 +
4278 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
4279 + // Update Pinecone role restriction
4280 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
4281 + $vector_id = md5($source_url);
4282 +
4283 + $wpdb->replace(
4284 + $roles_table,
4285 + array(
4286 + 'vector_id' => $vector_id,
4287 + 'role_restriction' => $highest_role,
4288 + 'updated_at' => current_time('mysql')
4289 + ),
4290 + array('%s', '%s', '%s')
4291 + );
4292 + } else {
4293 + // Update WordPress DB
4294 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4295 +
4296 + $wpdb->update(
4297 + $table_name,
4298 + array('role_restriction' => $highest_role),
4299 + array('source_url' => $source_url),
4300 + array('%s'),
4301 + array('%s')
4302 + );
4303 + }
4304 +}
4305 +
4306 +
4307 + // ========================================
4308 + // HELPER METHODS
4309 + // ========================================
4310 +
4311 + /**
4312 + * Check if user has required permissions for content processing
4313 + */
4314 + private function mxchat_check_user_permissions() {
4315 + if (!current_user_can('manage_options')) {
4316 + wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
4317 + }
4318 + }
4319 +
4320 + /**
4321 + * Validate nonce for security
4322 + */
4323 + private function mxchat_validate_nonce($nonce_name, $nonce_action) {
4324 + if (!isset($_POST[$nonce_name]) || !wp_verify_nonce($_POST[$nonce_name], $nonce_action)) {
4325 + wp_die(esc_html__('Security check failed.', 'mxchat'));
4326 + }
4327 + }
4328 +
4329 + /**
4330 + * Get embedding API credentials
4331 + */
4332 + private function mxchat_get_embedding_credentials() {
4333 + $embedding_model = $this->options['embedding_model'] ?? 'text-embedding-ada-002';
4334 +
4335 + if (strpos($embedding_model, 'text-embedding-') !== false) {
4336 + return array(
4337 + 'type' => 'openai',
4338 + 'api_key' => $this->options['api_key'] ?? ''
4339 + );
4340 + } elseif (strpos($embedding_model, 'voyage-') !== false) {
4341 + return array(
4342 + 'type' => 'voyage',
4343 + 'api_key' => $this->options['voyage_api_key'] ?? ''
4344 + );
4345 + } elseif (strpos($embedding_model, 'gemini-embedding-') !== false) {
4346 + return array(
4347 + 'type' => 'gemini',
4348 + 'api_key' => $this->options['gemini_api_key'] ?? ''
4349 + );
4350 + }
4351 +
4352 + return array('type' => 'unknown', 'api_key' => '');
4353 + }
4354 +
4355 + /**
4356 + * Log processing errors
4357 + */
4358 + private function mxchat_log_processing_error($operation, $error_message) {
4359 + //error_log("MxChat Knowledge Processing {$operation} Error: " . $error_message);
4360 + }
4361 +
4362 + /**
4363 + * Set admin notice transient
4364 + */
4365 + private function mxchat_set_admin_notice($type, $message) {
4366 + set_transient("mxchat_admin_notice_{$type}", $message, 30);
4367 + }
4368 +
4369 + /**
4370 + * Get Pinecone manager instance for vector operations
4371 + */
4372 + private function mxchat_get_pinecone_manager() {
4373 + return MxChat_Pinecone_Manager::get_instance();
4374 + }
4375 +
4376 +
4377 + // ========================================
4378 +// DATABASE QUEUE TABLE MANAGEMENT
4379 +// ========================================
4380 +
4381 +/**
4382 + * Create queue table on plugin activation
4383 + * Call this from your plugin activation hook
4384 + */
4385 +public function mxchat_create_queue_table() {
4386 + global $wpdb;
4387 +
4388 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
4389 + $charset_collate = $wpdb->get_charset_collate();
4390 +
4391 + $sql = "CREATE TABLE IF NOT EXISTS $table_name (
4392 + id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
4393 + queue_id varchar(64) NOT NULL,
4394 + item_type varchar(20) NOT NULL,
4395 + item_data longtext NOT NULL,
4396 + status varchar(20) NOT NULL DEFAULT 'pending',
4397 + bot_id varchar(50) NOT NULL DEFAULT 'default',
4398 + priority int(11) NOT NULL DEFAULT 0,
4399 + attempts int(11) NOT NULL DEFAULT 0,
4400 + max_attempts int(11) NOT NULL DEFAULT 3,
4401 + error_message text DEFAULT NULL,
4402 + created_at datetime NOT NULL,
4403 + started_at datetime DEFAULT NULL,
4404 + completed_at datetime DEFAULT NULL,
4405 + PRIMARY KEY (id),
4406 + KEY queue_id (queue_id),
4407 + KEY status (status),
4408 + KEY item_type (item_type),
4409 + KEY priority (priority)
4410 + ) $charset_collate;";
4411 +
4412 + require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
4413 + dbDelta($sql);
4414 +
4415 + // Also create a meta table for queue metadata
4416 + $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
4417 +
4418 + $meta_sql = "CREATE TABLE IF NOT EXISTS $meta_table (
4419 + id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
4420 + queue_id varchar(64) NOT NULL,
4421 + meta_key varchar(255) NOT NULL,
4422 + meta_value longtext,
4423 + PRIMARY KEY (id),
4424 + KEY queue_id (queue_id),
4425 + KEY meta_key (meta_key)
4426 + ) $charset_collate;";
4427 +
4428 + dbDelta($meta_sql);
4429 +}
4430 +
4431 +/**
4432 + * Add items to the processing queue
4433 + *
4434 + * @param string $queue_id Unique identifier for this queue batch
4435 + * @param string $item_type Type of item (url, pdf_page)
4436 + * @param array $items Array of items to queue
4437 + * @param string $bot_id Bot ID for processing
4438 + * @return int Number of items queued
4439 + */
4440 +private function mxchat_add_to_queue($queue_id, $item_type, $items, $bot_id = 'default') {
4441 + global $wpdb;
4442 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
4443 +
4444 + $queued_count = 0;
4445 + $priority = 0;
4446 +
4447 + foreach ($items as $item) {
4448 + $result = $wpdb->insert(
4449 + $table_name,
4450 + array(
4451 + 'queue_id' => $queue_id,
4452 + 'item_type' => $item_type,
4453 + 'item_data' => wp_json_encode($item),
4454 + 'status' => 'pending',
4455 + 'bot_id' => $bot_id,
4456 + 'priority' => $priority,
4457 + 'attempts' => 0,
4458 + 'max_attempts' => 3,
4459 + 'created_at' => current_time('mysql')
4460 + ),
4461 + array('%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%s')
4462 + );
4463 +
4464 + if ($result) {
4465 + $queued_count++;
4466 + }
4467 +
4468 + $priority++; // Process in order
4469 + }
4470 +
4471 + return $queued_count;
4472 +}
4473 +
4474 +/**
4475 + * Store queue metadata (total counts, source URL, etc.)
4476 + */
4477 +private function mxchat_set_queue_meta($queue_id, $meta_key, $meta_value) {
4478 + global $wpdb;
4479 + $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
4480 +
4481 + // Check if meta exists
4482 + $existing = $wpdb->get_var($wpdb->prepare(
4483 + "SELECT id FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
4484 + $queue_id,
4485 + $meta_key
4486 + ));
4487 +
4488 + if ($existing) {
4489 + // Update
4490 + $wpdb->update(
4491 + $meta_table,
4492 + array('meta_value' => maybe_serialize($meta_value)),
4493 + array('queue_id' => $queue_id, 'meta_key' => $meta_key),
4494 + array('%s'),
4495 + array('%s', '%s')
4496 + );
4497 + } else {
4498 + // Insert
4499 + $wpdb->insert(
4500 + $meta_table,
4501 + array(
4502 + 'queue_id' => $queue_id,
4503 + 'meta_key' => $meta_key,
4504 + 'meta_value' => maybe_serialize($meta_value)
4505 + ),
4506 + array('%s', '%s', '%s')
4507 + );
4508 + }
4509 +}
4510 +
4511 +/**
4512 + * Get queue metadata
4513 + */
4514 +private function mxchat_get_queue_meta($queue_id, $meta_key) {
4515 + global $wpdb;
4516 + $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
4517 +
4518 + $value = $wpdb->get_var($wpdb->prepare(
4519 + "SELECT meta_value FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
4520 + $queue_id,
4521 + $meta_key
4522 + ));
4523 +
4524 + return maybe_unserialize($value);
4525 +}
4526 +
4527 +// ========================================
4528 +// AJAX QUEUE PROCESSING HANDLERS
4529 +// ========================================
4530 +
4531 +/**
4532 + * AJAX: Get next item from queue to process
4533 + */
4534 +public function ajax_mxchat_get_next_queue_item() {
4535 + // Verify nonce and permissions
4536 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
4537 +
4538 + if (!current_user_can('manage_options')) {
4539 + wp_send_json_error('Unauthorized access');
4540 + }
4541 +
4542 + $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
4543 +
4544 + if (empty($queue_id)) {
4545 + wp_send_json_error('Missing queue ID');
4546 + }
4547 +
4548 + global $wpdb;
4549 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
4550 +
4551 + // Get next pending item with retry logic for failed items
4552 + $next_item = $wpdb->get_row($wpdb->prepare(
4553 + "SELECT * FROM $table_name
4554 + WHERE queue_id = %s
4555 + AND status IN ('pending', 'failed')
4556 + AND attempts < max_attempts
4557 + ORDER BY priority ASC, id ASC
4558 + LIMIT 1",
4559 + $queue_id
4560 + ));
4561 +
4562 + if (!$next_item) {
4563 + // No more items - queue complete
4564 + wp_send_json_success(array(
4565 + 'complete' => true,
4566 + 'message' => 'Queue processing complete'
4567 + ));
4568 + }
4569 +
4570 + // Mark item as processing
4571 + $wpdb->update(
4572 + $table_name,
4573 + array(
4574 + 'status' => 'processing',
4575 + 'started_at' => current_time('mysql'),
4576 + 'attempts' => $next_item->attempts + 1
4577 + ),
4578 + array('id' => $next_item->id),
4579 + array('%s', '%s', '%d'),
4580 + array('%d')
4581 + );
4582 +
4583 + wp_send_json_success(array(
4584 + 'complete' => false,
4585 + 'item' => array(
4586 + 'id' => $next_item->id,
4587 + 'type' => $next_item->item_type,
4588 + 'data' => json_decode($next_item->item_data, true),
4589 + 'bot_id' => $next_item->bot_id,
4590 + 'attempt' => $next_item->attempts + 1
4591 + )
4592 + ));
4593 +}
4594 +
4595 +/**
4596 + * AJAX: Process a single queue item
4597 + */
4598 +public function ajax_mxchat_process_queue_item() {
4599 + // Verify nonce and permissions
4600 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
4601 +
4602 + if (!current_user_can('manage_options')) {
4603 + wp_send_json_error('Unauthorized access');
4604 + }
4605 +
4606 + $item_id = isset($_POST['item_id']) ? absint($_POST['item_id']) : 0;
4607 + $item_type = isset($_POST['item_type']) ? sanitize_text_field($_POST['item_type']) : '';
4608 + $item_data = isset($_POST['item_data']) ? $_POST['item_data'] : array();
4609 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
4610 +
4611 + if (empty($item_id) || empty($item_type)) {
4612 + wp_send_json_error('Missing item data');
4613 + }
4614 +
4615 + global $wpdb;
4616 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
4617 +
4618 + // Process based on item type
4619 + try {
4620 + set_time_limit(60); // Give processing 60 seconds
4621 +
4622 + $result = false;
4623 + $error_message = '';
4624 +
4625 + switch ($item_type) {
4626 + case 'url':
4627 + $result = $this->mxchat_process_queue_url($item_data, $bot_id);
4628 + break;
4629 +
4630 + case 'pdf_page':
4631 + $result = $this->mxchat_process_queue_pdf_page($item_data, $bot_id);
4632 + break;
4633 +
4634 + default:
4635 + throw new Exception('Unknown item type: ' . $item_type);
4636 + }
4637 +
4638 + if (is_wp_error($result)) {
4639 + throw new Exception($result->get_error_message());
4640 + }
4641 +
4642 + if ($result === false) {
4643 + throw new Exception('Processing returned false - item may be empty or invalid');
4644 + }
4645 +
4646 + // Mark as completed
4647 + $wpdb->update(
4648 + $table_name,
4649 + array(
4650 + 'status' => 'completed',
4651 + 'completed_at' => current_time('mysql'),
4652 + 'error_message' => null
4653 + ),
4654 + array('id' => $item_id),
4655 + array('%s', '%s', '%s'),
4656 + array('%d')
4657 + );
4658 +
4659 + wp_send_json_success(array(
4660 + 'processed' => true,
4661 + 'item_id' => $item_id,
4662 + 'message' => 'Item processed successfully'
4663 + ));
4664 +
4665 + } catch (Exception $e) {
4666 + $error_message = $e->getMessage();
4667 +
4668 + // Get current attempt count
4669 + $item = $wpdb->get_row($wpdb->prepare(
4670 + "SELECT attempts, max_attempts FROM $table_name WHERE id = %d",
4671 + $item_id
4672 + ));
4673 +
4674 + // Check if we've exhausted retries
4675 + if ($item && $item->attempts >= $item->max_attempts) {
4676 + // Permanently failed
4677 + $wpdb->update(
4678 + $table_name,
4679 + array(
4680 + 'status' => 'failed',
4681 + 'error_message' => $error_message
4682 + ),
4683 + array('id' => $item_id),
4684 + array('%s', '%s'),
4685 + array('%d')
4686 + );
4687 +
4688 + wp_send_json_error(array(
4689 + 'message' => 'Item failed after maximum attempts: ' . $error_message,
4690 + 'permanent_failure' => true,
4691 + 'item_id' => $item_id
4692 + ));
4693 + } else {
4694 + // Mark for retry
4695 + $wpdb->update(
4696 + $table_name,
4697 + array(
4698 + 'status' => 'failed',
4699 + 'error_message' => $error_message
4700 + ),
4701 + array('id' => $item_id),
4702 + array('%s', '%s'),
4703 + array('%d')
4704 + );
4705 +
4706 + wp_send_json_error(array(
4707 + 'message' => 'Item processing failed, will retry: ' . $error_message,
4708 + 'can_retry' => true,
4709 + 'item_id' => $item_id,
4710 + 'attempts' => $item ? $item->attempts : 0
4711 + ));
4712 + }
4713 + }
4714 +}
4715 +
4716 +/**
4717 + * Process a URL from the queue
4718 + */
4719 +private function mxchat_process_queue_url($item_data, $bot_id = 'default') {
4720 + $url = isset($item_data['url']) ? $item_data['url'] : '';
4721 +
4722 + if (empty($url)) {
4723 + return new WP_Error('invalid_url', 'URL is empty');
4724 + }
4725 +
4726 + // Get bot-specific API key early (needed for both paths)
4727 + $bot_options = $this->get_bot_options($bot_id);
4728 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
4729 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4730 +
4731 + if (strpos($selected_model, 'voyage') === 0) {
4732 + $api_key = $options['voyage_api_key'] ?? '';
4733 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
4734 + $api_key = $options['gemini_api_key'] ?? '';
4735 + } else {
4736 + $api_key = $options['api_key'] ?? '';
4737 + }
4738 +
4739 + if (empty($api_key)) {
4740 + return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
4741 + }
4742 +
4743 + // Check if this is a WooCommerce product URL and WooCommerce is active
4744 + $is_product_url = (strpos($url, '/product/') !== false || strpos($url, '/shop/') !== false);
4745 + $content_type = $is_product_url ? 'product' : 'url';
4746 +
4747 + // Try to get WooCommerce product data if it's a product URL
4748 + if ($is_product_url && class_exists('WooCommerce')) {
4749 + $product_content = $this->mxchat_extract_woocommerce_product_content($url);
4750 +
4751 + if (!empty($product_content)) {
4752 + // Successfully extracted WooCommerce product data with pricing
4753 + $result = MxChat_Utils::submit_content_to_db(
4754 + $product_content,
4755 + $url,
4756 + $api_key,
4757 + null,
4758 + $bot_id,
4759 + 'product'
4760 + );
4761 + return $result;
4762 + }
4763 + // If WooCommerce extraction failed, fall through to HTML extraction
4764 + }
4765 +
4766 + // Fetch URL content (fallback for non-products or when WooCommerce extraction fails)
4767 + $response = wp_remote_get($url, array(
4768 + 'timeout' => 30,
4769 + 'redirection' => 5,
4770 + 'user-agent' => 'MxChat/1.0'
4771 + ));
4772 +
4773 + if (is_wp_error($response)) {
4774 + return $response;
4775 + }
4776 +
4777 + $response_code = wp_remote_retrieve_response_code($response);
4778 + if ($response_code !== 200) {
4779 + return new WP_Error('http_error', 'HTTP ' . $response_code . ' error');
4780 + }
4781 +
4782 + $html = wp_remote_retrieve_body($response);
4783 +
4784 + if (empty($html)) {
4785 + return new WP_Error('empty_response', 'Empty response body');
4786 + }
4787 +
4788 + // Extract and sanitize content
4789 + $content = $this->mxchat_extract_main_content($html);
4790 + $sanitized = $this->mxchat_sanitize_content_for_api($content);
4791 +
4792 + if (empty($sanitized)) {
4793 + // Not an error - just no content found (maybe a redirect or empty page)
4794 + return false;
4795 + }
4796 +
4797 + // Submit to database with content_type
4798 + $result = MxChat_Utils::submit_content_to_db(
4799 + $sanitized,
4800 + $url,
4801 + $api_key,
4802 + null,
4803 + $bot_id,
4804 + $content_type
4805 + );
4806 +
4807 + return $result;
4808 +}
4809 +
4810 +/**
4811 + * Extract WooCommerce product content including pricing
4812 + *
4813 + * @param string $url The product URL
4814 + * @return string|false Product content with pricing, or false if not found
4815 + */
4816 +private function mxchat_extract_woocommerce_product_content($url) {
4817 + // Try to get product ID from URL
4818 + $product_id = url_to_postid($url);
4819 +
4820 + // If url_to_postid fails, try to extract from URL pattern
4821 + if (!$product_id) {
4822 + $product_slug = '';
4823 +
4824 + // Handle pretty permalinks: /product/product-name/
4825 + if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
4826 + $product_slug = $matches[1];
4827 + }
4828 +
4829 + if (!empty($product_slug)) {
4830 + $product_post = get_page_by_path($product_slug, OBJECT, 'product');
4831 + if ($product_post) {
4832 + $product_id = $product_post->ID;
4833 + }
4834 + }
4835 + }
4836 +
4837 + if (!$product_id) {
4838 + return false;
4839 + }
4840 +
4841 + // Get WooCommerce product object
4842 + $product = wc_get_product($product_id);
4843 +
4844 + if (!$product) {
4845 + return false;
4846 + }
4847 +
4848 + // Build product content with pricing (similar to mxchat_store_product_embedding)
4849 + $title = $product->get_name();
4850 + $description = $product->get_description();
4851 + $short_description = $product->get_short_description();
4852 + $sku = $product->get_sku();
4853 +
4854 + // Get pricing information
4855 + $regular_price = $product->get_regular_price();
4856 + $sale_price = $product->get_sale_price();
4857 + $price = $product->get_price(); // Current active price
4858 +
4859 + // Get currency symbol
4860 + $currency_symbol = get_woocommerce_currency_symbol();
4861 +
4862 + // Format content
4863 + $content = $title . "\n\n";
4864 +
4865 + if (!empty($short_description)) {
4866 + $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
4867 + }
4868 +
4869 + if (!empty($description)) {
4870 + $content .= wp_strip_all_tags($description) . "\n\n";
4871 + }
4872 +
4873 + // Add pricing information
4874 + if (!empty($regular_price)) {
4875 + $content .= "Price: " . $currency_symbol . $regular_price . "\n";
4876 + } elseif (!empty($price)) {
4877 + $content .= "Price: " . $currency_symbol . $price . "\n";
4878 + }
4879 +
4880 + if (!empty($sale_price) && $sale_price !== $regular_price) {
4881 + $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
4882 + }
4883 +
4884 + // Handle variable products - show price range
4885 + if ($product->is_type('variable')) {
4886 + $min_price = $product->get_variation_price('min');
4887 + $max_price = $product->get_variation_price('max');
4888 + if ($min_price !== $max_price) {
4889 + $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
4890 + }
4891 + }
4892 +
4893 + if (!empty($sku)) {
4894 + $content .= "SKU: " . $sku . "\n";
4895 + }
4896 +
4897 + // Get product categories
4898 + $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
4899 + if (!empty($categories) && !is_wp_error($categories)) {
4900 + $content .= "Categories: " . implode(', ', $categories) . "\n";
4901 + }
4902 +
4903 + // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
4904 + $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
4905 + if (!empty($custom_tabs) && is_array($custom_tabs)) {
4906 + foreach ($custom_tabs as $tab) {
4907 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
4908 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
4909 +
4910 + if (!empty($tab_title) && !empty($tab_content)) {
4911 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
4912 + }
4913 + }
4914 + }
4915 +
4916 + // Also check for reusable/saved tabs applied to this product
4917 + $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
4918 + if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
4919 + $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
4920 + if (!empty($saved_tabs) && is_array($saved_tabs)) {
4921 + foreach ($applied_saved_tabs as $saved_tab_id) {
4922 + if (isset($saved_tabs[$saved_tab_id])) {
4923 + $tab = $saved_tabs[$saved_tab_id];
4924 + $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
4925 + $tab_content = isset($tab['content']) ? $tab['content'] : '';
4926 +
4927 + if (!empty($tab_title) && !empty($tab_content)) {
4928 + $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
4929 + }
4930 + }
4931 + }
4932 + }
4933 + }
4934 +
4935 + return $this->mxchat_sanitize_content_for_api($content);
4936 +}
4937 +
4938 +/**
4939 + * Process a PDF page from the queue
4940 + */
4941 +private function mxchat_process_queue_pdf_page($item_data, $bot_id = 'default') {
4942 + $pdf_path = isset($item_data['pdf_path']) ? $item_data['pdf_path'] : '';
4943 + $pdf_url = isset($item_data['pdf_url']) ? $item_data['pdf_url'] : '';
4944 + $page_number = isset($item_data['page_number']) ? absint($item_data['page_number']) : 0;
4945 + $total_pages = isset($item_data['total_pages']) ? absint($item_data['total_pages']) : 0;
4946 +
4947 + if (empty($pdf_path) || !file_exists($pdf_path)) {
4948 + return new WP_Error('pdf_not_found', 'PDF file not found: ' . $pdf_path);
4949 + }
4950 +
4951 + if ($page_number < 1) {
4952 + return new WP_Error('invalid_page', 'Invalid page number');
4953 + }
4954 +
4955 + try {
4956 + $parser = new \Smalot\PdfParser\Parser();
4957 + $pdf = $parser->parseFile($pdf_path);
4958 + $pages = $pdf->getPages();
4959 +
4960 + if (!isset($pages[$page_number - 1])) {
4961 + return new WP_Error('page_not_found', 'Page ' . $page_number . ' not found in PDF');
4962 + }
4963 +
4964 + $text = $pages[$page_number - 1]->getText();
4965 +
4966 + if (empty($text)) {
4967 + // Not an error - just an empty page
4968 + return false;
4969 + }
4970 +
4971 + $sanitized = $this->mxchat_sanitize_content_for_api($text);
4972 +
4973 + if (empty($sanitized)) {
4974 + return false;
4975 + }
4976 +
4977 + // Create metadata
4978 + $metadata = array(
4979 + 'document_type' => 'pdf',
4980 + 'total_pages' => $total_pages,
4981 + 'current_page' => $page_number,
4982 + 'source_url' => $pdf_url
4983 + );
4984 +
4985 + $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized;
4986 + $page_url = esc_url($pdf_url . "#page=" . $page_number);
4987 +
4988 + // Get bot-specific API key
4989 + $bot_options = $this->get_bot_options($bot_id);
4990 + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
4991 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4992 +
4993 + if (strpos($selected_model, 'voyage') === 0) {
4994 + $api_key = $options['voyage_api_key'] ?? '';
4995 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
4996 + $api_key = $options['gemini_api_key'] ?? '';
4997 + } else {
4998 + $api_key = $options['api_key'] ?? '';
4999 + }
5000 +
5001 + if (empty($api_key)) {
5002 + return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
5003 + }
5004 +
5005 + // Submit to database - UPDATED 2.5.6: Added content_type 'pdf'
5006 + $result = MxChat_Utils::submit_content_to_db(
5007 + $content_with_metadata,
5008 + $page_url,
5009 + $api_key,
5010 + null,
5011 + $bot_id,
5012 + 'pdf'
5013 + );
5014 +
5015 + return $result;
5016 +
5017 + } catch (Exception $e) {
5018 + return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
5019 + }
5020 +}
5021 +
5022 +/**
5023 + * AJAX: Get queue processing status
5024 + */
5025 +public function ajax_mxchat_get_queue_status() {
5026 + // Verify nonce and permissions
5027 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
5028 +
5029 + if (!current_user_can('manage_options')) {
5030 + wp_send_json_error('Unauthorized access');
5031 + }
5032 +
5033 + $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
5034 +
5035 + if (empty($queue_id)) {
5036 + wp_send_json_error('Missing queue ID');
5037 + }
5038 +
5039 + global $wpdb;
5040 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
5041 +
5042 + // Get counts by status
5043 + $counts = $wpdb->get_results($wpdb->prepare(
5044 + "SELECT status, COUNT(*) as count
5045 + FROM $table_name
5046 + WHERE queue_id = %s
5047 + GROUP BY status",
5048 + $queue_id
5049 + ), OBJECT_K);
5050 +
5051 + $total = 0;
5052 + $completed = 0;
5053 + $failed = 0;
5054 + $processing = 0;
5055 + $pending = 0;
5056 +
5057 + foreach ($counts as $status => $data) {
5058 + $count = absint($data->count);
5059 + $total += $count;
5060 +
5061 + switch ($status) {
5062 + case 'completed':
5063 + $completed = $count;
5064 + break;
5065 + case 'failed':
5066 + $failed = $count;
5067 + break;
5068 + case 'processing':
5069 + $processing = $count;
5070 + break;
5071 + case 'pending':
5072 + $pending = $count;
5073 + break;
5074 + }
5075 + }
5076 +
5077 + // Calculate percentage
5078 + $percentage = $total > 0 ? round((($completed + $failed) / $total) * 100) : 0;
5079 +
5080 + // Get failed items details
5081 + $failed_items = array();
5082 + if ($failed > 0) {
5083 + $failed_items = $wpdb->get_results($wpdb->prepare(
5084 + "SELECT item_type, item_data, error_message, attempts
5085 + FROM $table_name
5086 + WHERE queue_id = %s
5087 + AND status = 'failed'
5088 + AND attempts >= max_attempts
5089 + ORDER BY id DESC
5090 + LIMIT 50",
5091 + $queue_id
5092 + ));
5093 + }
5094 +
5095 + // Get queue metadata
5096 + $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
5097 + $queue_type = $this->mxchat_get_queue_meta($queue_id, 'queue_type');
5098 +
5099 + // Determine if queue is complete
5100 + $is_complete = ($pending === 0 && $processing === 0);
5101 +
5102 + wp_send_json_success(array(
5103 + 'queue_id' => $queue_id,
5104 + 'queue_type' => $queue_type,
5105 + 'source_url' => $source_url,
5106 + 'total' => $total,
5107 + 'completed' => $completed,
5108 + 'failed' => $failed,
5109 + 'processing' => $processing,
5110 + 'pending' => $pending,
5111 + 'percentage' => $percentage,
5112 + 'is_complete' => $is_complete,
5113 + 'failed_items' => $failed_items,
5114 + 'status' => $is_complete ? 'complete' : 'processing'
5115 + ));
5116 +}
5117 +
5118 +/**
5119 + * AJAX: Clear completed queue
5120 + */
5121 +public function ajax_mxchat_clear_queue() {
5122 + // Verify nonce and permissions
5123 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
5124 +
5125 + if (!current_user_can('manage_options')) {
5126 + wp_send_json_error('Unauthorized access');
5127 + }
5128 +
5129 + $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
5130 +
5131 + if (empty($queue_id)) {
5132 + wp_send_json_error('Missing queue ID');
5133 + }
5134 +
5135 + global $wpdb;
5136 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
5137 + $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
5138 +
5139 + // Delete queue items
5140 + $wpdb->delete(
5141 + $table_name,
5142 + array('queue_id' => $queue_id),
5143 + array('%s')
5144 + );
5145 +
5146 + // Delete queue metadata
5147 + $wpdb->delete(
5148 + $meta_table,
5149 + array('queue_id' => $queue_id),
5150 + array('%s')
5151 + );
5152 +
5153 + wp_send_json_success(array(
5154 + 'message' => 'Queue cleared successfully'
5155 + ));
5156 +}
5157 +
5158 +/**
5159 + * AJAX: Retry failed items in queue
5160 + */
5161 +public function ajax_mxchat_retry_failed() {
5162 + // Verify nonce and permissions
5163 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
5164 +
5165 + if (!current_user_can('manage_options')) {
5166 + wp_send_json_error('Unauthorized access');
5167 + }
5168 +
5169 + $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
5170 +
5171 + if (empty($queue_id)) {
5172 + wp_send_json_error('Missing queue ID');
5173 + }
5174 +
5175 + global $wpdb;
5176 + $table_name = $wpdb->prefix . 'mxchat_processing_queue';
5177 +
5178 + // Reset failed items to pending and reset attempt count
5179 + $updated = $wpdb->update(
5180 + $table_name,
5181 + array(
5182 + 'status' => 'pending',
5183 + 'attempts' => 0,
5184 + 'error_message' => null
5185 + ),
5186 + array(
5187 + 'queue_id' => $queue_id,
5188 + 'status' => 'failed'
5189 + ),
5190 + array('%s', '%d', '%s'),
5191 + array('%s', '%s')
5192 + );
5193 +
5194 + wp_send_json_success(array(
5195 + 'message' => 'Reset ' . $updated . ' failed items for retry',
5196 + 'reset_count' => $updated
5197 + ));
5198 +}
5199 +
5200 +
5201 +public function ajax_mxchat_mark_queue_complete() {
5202 + check_ajax_referer('mxchat_queue_nonce', 'nonce');
5203 +
5204 + if (!current_user_can('manage_options')) {
5205 + wp_send_json_error('Unauthorized access');
5206 + }
5207 +
5208 + $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
5209 +
5210 + if (empty($queue_id)) {
5211 + wp_send_json_error('Missing queue ID');
5212 + }
5213 +
5214 + // Clear active queue transients
5215 + if (strpos($queue_id, 'sitemap_') === 0) {
5216 + delete_transient('mxchat_active_queue_sitemap');
5217 + } else if (strpos($queue_id, 'pdf_') === 0) {
5218 + delete_transient('mxchat_active_queue_pdf');
5219 + }
5220 +
5221 + wp_send_json_success(array('message' => 'Queue marked as complete'));
5222 +}
5223 +
5224 +
5225 + // ========================================
5226 + // STATIC ACCESS METHODS
5227 + // ========================================
5228 +
5229 + /**
5230 + * Get singleton instance
5231 + */
5232 + public static function get_instance() {
5233 + static $instance = null;
5234 + if ($instance === null) {
5235 + $instance = new self();
5236 + }
5237 + return $instance;
5238 + }
5239 +}
5240 +
5241 +// Initialize the Knowledge manager
7894 5242 $mxchat_knowledge_manager = MxChat_Knowledge_Manager::get_instance();