PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.2.3
MxChat – AI Chatbot & Content Generation for WordPress v2.2.3
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | admin/class-knowledge-manager.php +1610 -7084 3.2.162.2.3 View file →
@@ -9,96 +9,55 @@
9 9 exit; // Exit if accessed directly
10 10 }
11 11
12 12 class MxChat_Knowledge_Manager {
13 -
13 +
14 14 private $options;
15 -
16 - // Post IDs whose vectors were already deleted by mxchat_handle_status_transition this
17 - // request, so the transient-based branch in mxchat_handle_post_update can skip the
18 - // redundant (idempotent but network-visible) second deletion.
19 - private $transition_deleted_posts = array();
20 -
15 +
21 16 /**
22 17 * Constructor - Register hooks for content processing
23 18 */
24 -public function __construct() {
25 - $this->options = get_option('mxchat_options', array());
26 - $this->mxchat_init_hooks();
19 + public function __construct() {
20 + $this->options = get_option('mxchat_options', array());
21 + $this->mxchat_init_hooks();
22 + }
27 23
28 - $this->mxchat_init_role_hooks();
29 -}
30 -
31 -/**
32 - * Initialize WordPress hooks for content processing
33 - *
34 - */
35 -private function mxchat_init_hooks() {
36 - // Admin post handlers for form submissions
37 - add_action('admin_post_mxchat_submit_content', array($this, 'mxchat_handle_content_submission'));
38 - add_action('admin_post_mxchat_submit_sitemap', array($this, 'mxchat_handle_sitemap_submission'));
39 - add_action('admin_post_mxchat_submit_pdf_file', array($this, 'mxchat_handle_pdf_file_submission'));
40 - add_action('admin_post_mxchat_submit_youtube', array($this, 'mxchat_handle_youtube_submission'));
41 - add_action('admin_post_mxchat_stop_processing', array($this, 'mxchat_stop_processing'));
42 -
43 - // AJAX handlers for real-time processing and status updates
44 - add_action('wp_ajax_mxchat_get_status_updates', array($this, 'mxchat_ajax_get_status_updates'));
45 - add_action('wp_ajax_mxchat_dismiss_completed_status', array($this, 'mxchat_ajax_dismiss_completed_status'));
46 - add_action('wp_ajax_mxchat_get_content_list', array($this, 'ajax_mxchat_get_content_list'));
47 - add_action('wp_ajax_mxchat_process_selected_content', array($this, 'ajax_mxchat_process_selected_content'));
48 - add_action('wp_ajax_mxchat_save_inline_prompt', array($this, 'mxchat_save_inline_prompt'));
49 - add_action('admin_post_mxchat_delete_pinecone_prompt', array($this, 'mxchat_handle_pinecone_prompt_delete'));
50 - add_action('wp_ajax_mxchat_delete_pinecone_prompt', array($this, 'ajax_mxchat_delete_pinecone_prompt'));
51 - add_action('wp_ajax_mxchat_delete_chunks_by_url', array($this, 'ajax_mxchat_delete_chunks_by_url'));
52 - add_action('wp_ajax_mxchat_delete_wordpress_prompt', array($this, 'ajax_mxchat_delete_wordpress_prompt'));
53 - add_action('wp_ajax_mxchat_bulk_delete_knowledge', array($this, 'ajax_mxchat_bulk_delete_knowledge'));
54 - add_action('wp_ajax_mxchat_update_role_restriction', array($this, 'ajax_mxchat_update_role_restriction'));
55 -
56 - // Queue-based processing AJAX handlers
57 - add_action('wp_ajax_mxchat_get_next_queue_item', array($this, 'ajax_mxchat_get_next_queue_item'));
58 - add_action('wp_ajax_mxchat_process_queue_item', array($this, 'ajax_mxchat_process_queue_item'));
59 - add_action('wp_ajax_mxchat_get_queue_status', array($this, 'ajax_mxchat_get_queue_status'));
60 - add_action('wp_ajax_mxchat_clear_queue', array($this, 'ajax_mxchat_clear_queue'));
61 - add_action('wp_ajax_mxchat_retry_failed', array($this, 'ajax_mxchat_retry_failed'));
62 - add_action('wp_ajax_mxchat_get_recent_entries', array($this, 'ajax_mxchat_get_recent_entries'));
63 - add_action('wp_ajax_mxchat_detect_sitemaps', array($this, 'ajax_mxchat_detect_sitemaps'));
64 - add_action('wp_ajax_mxchat_refresh_pinecone_entries', array($this, 'ajax_mxchat_refresh_pinecone_entries'));
65 - add_action('wp_ajax_mxchat_paginate_entries', array($this, 'ajax_mxchat_paginate_entries'));
66 - add_action('wp_ajax_mxchat_get_entry_content', array($this, 'ajax_mxchat_get_entry_content'));
67 - add_action('wp_ajax_mxchat_save_entry_content', array($this, 'ajax_mxchat_save_entry_content'));
68 - add_action('wp_ajax_mxchat_inspect_entry', array($this, 'ajax_mxchat_inspect_entry'));
24 + /**
25 + * Initialize WordPress hooks for content processing
26 + */
27 + private function mxchat_init_hooks() {
28 + // Admin post handlers for form submissions
29 + add_action('admin_post_mxchat_submit_content', array($this, 'mxchat_handle_content_submission'));
30 + add_action('admin_post_mxchat_submit_sitemap', array($this, 'mxchat_handle_sitemap_submission'));
31 + add_action('admin_post_mxchat_stop_processing', array($this, 'mxchat_stop_processing'));
32 +
33 + // AJAX handlers for real-time processing and status updates
34 + add_action('wp_ajax_mxchat_get_status_updates', array($this, 'mxchat_ajax_get_status_updates'));
35 + add_action('wp_ajax_mxchat_dismiss_completed_status', array($this, 'mxchat_ajax_dismiss_completed_status')); // NEW
36 + add_action('wp_ajax_mxchat_get_content_list', array($this, 'ajax_mxchat_get_content_list'));
37 + add_action('wp_ajax_mxchat_process_selected_content', array($this, 'ajax_mxchat_process_selected_content'));
38 + add_action('wp_ajax_mxchat_manual_batch_process', array($this, 'ajax_manual_batch_process'));
39 + add_action('mxchat_delete_content', array($this, 'mxchat_delete_from_pinecone_by_url'), 10, 1);
69 40
70 - // WordPress post management hooks
71 - add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
72 - add_action('post_updated', array($this, 'mxchat_handle_post_update'), 10, 3);
73 - add_action('before_delete_post', array($this, 'mxchat_handle_post_delete'));
74 - add_action('wp_trash_post', array($this, 'mxchat_handle_post_delete'));
75 - // Authoritative unpublish detection: core hands this hook the REAL previous status, so
76 - // removal no longer depends on the mxchat_prev_status_* transients (evictable by persistent
77 - // object caches, never written by paths that bypass wp_update_post, e.g. plugins flipping
78 - // post_status directly and calling wp_transition_post_status themselves).
79 - add_action('transition_post_status', array($this, 'mxchat_handle_status_transition'), 10, 3);
41 +
42 + // Cron handlers for background processing
43 + add_action('mxchat_process_sitemap_urls', array($this, 'mxchat_process_sitemap_urls_cron'), 10, 5);
44 + add_action('mxchat_process_pdf_pages', array($this, 'mxchat_process_pdf_pages_cron'), 10, 5);
45 +
46 + // WordPress post management hooks
47 + add_action('post_updated', array($this, 'mxchat_handle_post_update'), 10, 3);
48 + add_action('before_delete_post', array($this, 'mxchat_handle_post_delete'));
49 + add_action('wp_trash_post', array($this, 'mxchat_handle_post_delete'));
50 + add_action('wp_ajax_mxchat_save_inline_prompt', array($this, 'mxchat_save_inline_prompt'));
80 51
81 - // ACF hook - fires AFTER ACF fields are saved, ensuring ACF data is available
82 - // Priority 20 to run after ACF's own save (which runs at priority 10)
83 - add_action('acf/save_post', array($this, 'mxchat_handle_acf_save'), 20);
84 -
85 - // One-time cleanup for vectors orphaned by unpublishes that predate the
86 - // transition_post_status handler (plan 816fb1): wp mxchat prune-unpublished
87 - if (defined('WP_CLI') && WP_CLI) {
88 - WP_CLI::add_command('mxchat prune-unpublished', array($this, 'cli_prune_unpublished'));
52 + // WooCommerce product hooks (if WooCommerce is active)
53 + if (class_exists('WooCommerce')) {
54 + add_action('save_post_product', array($this, 'mxchat_handle_product_change'), 10, 3);
55 + add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
56 + add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
57 + }
58 +
89 59 }
90 -
91 - add_action('wp_ajax_mxchat_mark_queue_complete', array($this, 'ajax_mxchat_mark_queue_complete'));
92 -
93 - // WooCommerce product hooks (if WooCommerce is active)
94 - if (class_exists('WooCommerce')) {
95 - add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
96 - add_action('save_post_product', array($this, 'mxchat_handle_product_change'), 10, 3);
97 - add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
98 - add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
99 - }
100 -}
101 60
102 61 /**
103 62 * Get current options (refreshed)
104 63 */
@@ -108,9 +67,315 @@
108 67 }
109 68 return $this->options;
110 69 }
111 70
71 +
72 + /**
73 + * Handle manual batch processing via AJAX
74 + */
75 +public function ajax_manual_batch_process() {
76 + try {
77 + // Verify nonce and permissions
78 + check_ajax_referer('mxchat_status_nonce', 'nonce');
79 +
80 + if (!current_user_can('manage_options')) {
81 + wp_send_json_error('Unauthorized access');
82 + }
83 +
84 + $process_type = sanitize_text_field($_POST['process_type'] ?? '');
85 + $url = sanitize_text_field($_POST['url'] ?? '');
86 +
87 + if (empty($process_type) || empty($url)) {
88 + wp_send_json_error('Missing required parameters');
89 + }
90 +
91 + $processed = 0;
92 +
93 + if ($process_type === 'pdf') {
94 + $processed = $this->mxchat_manual_process_pdf_batch($url);
95 + } elseif ($process_type === 'sitemap') {
96 + $processed = $this->mxchat_manual_process_sitemap_batch($url);
97 + }
98 +
99 + if ($processed > 0) {
100 + wp_send_json_success(array(
101 + 'message' => "Processed {$processed} items successfully",
102 + 'processed' => $processed
103 + ));
104 + } else {
105 + wp_send_json_error('No items were processed');
106 + }
107 +
108 + } catch (Exception $e) {
109 + //error_log('Manual batch process error: ' . $e->getMessage());
110 + wp_send_json_error('Processing failed: ' . $e->getMessage());
111 + }
112 +}
112 113
114 +/**
115 + * Process a small PDF batch manually - DIRECT PROCESSING
116 + */
117 +private function mxchat_manual_process_pdf_batch($pdf_url) {
118 + try {
119 + $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
120 + $status = get_transient($status_key);
121 +
122 + if (!$status || $status['status'] !== 'processing') {
123 + //error_log('Manual PDF: No processing status found');
124 + return 0;
125 + }
126 +
127 + //error_log('Manual PDF: Starting direct processing for ' . $pdf_url);
128 +
129 + // Get current progress
130 + $current_page = $status['processed_pages'] ?? 0;
131 + $total_pages = $status['total_pages'] ?? 0;
132 +
133 + if ($current_page >= $total_pages) {
134 + //error_log('Manual PDF: Already completed');
135 + return 0;
136 + }
137 +
138 + // Try to download the PDF again for processing
139 + $response = wp_remote_get($pdf_url, array('timeout' => 30));
140 +
141 + if (is_wp_error($response)) {
142 + //error_log('Manual PDF: Failed to download PDF: ' . $response->get_error_message());
143 + return 0;
144 + }
145 +
146 + $pdf_content = wp_remote_retrieve_body($response);
147 + if (empty($pdf_content)) {
148 + //error_log('Manual PDF: Empty PDF content');
149 + return 0;
150 + }
151 +
152 + // Save PDF temporarily
153 + $upload_dir = wp_upload_dir();
154 + $temp_pdf_path = trailingslashit($upload_dir['path']) . 'temp_manual_' . time() . '.pdf';
155 + file_put_contents($temp_pdf_path, $pdf_content);
156 +
157 + // Process 2 pages directly
158 + $processed = $this->mxchat_process_pdf_pages_direct($temp_pdf_path, $pdf_url, $current_page, 5);
159 +
160 + // Clean up temp file
161 + if (file_exists($temp_pdf_path)) {
162 + wp_delete_file($temp_pdf_path);
163 + }
164 +
165 + //error_log('Manual PDF: Processed ' . $processed . ' pages');
166 + return $processed;
167 +
168 + } catch (Exception $e) {
169 + //error_log('Manual PDF batch error: ' . $e->getMessage());
170 + return 0;
171 + }
172 +}
173 +
174 +/**
175 + * Process PDF pages directly without cron
176 + */
177 +private function mxchat_process_pdf_pages_direct($pdf_path, $pdf_url, $start_page, $batch_size) {
178 + try {
179 + if (!file_exists($pdf_path)) {
180 + //error_log('Direct PDF: File not found at ' . $pdf_path);
181 + return 0;
182 + }
183 +
184 + $parser = new \Smalot\PdfParser\Parser();
185 + $pdf = $parser->parseFile($pdf_path);
186 + $pages = $pdf->getPages();
187 +
188 + $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
189 + $status = get_transient($status_key);
190 +
191 + if (!$status) {
192 + return 0;
193 + }
194 +
195 + $options = get_option('mxchat_options');
196 + $api_key = $options['api_key'] ?? '';
197 +
198 + if (empty($api_key)) {
199 + //error_log('Direct PDF: No API key');
200 + return 0;
201 + }
202 +
203 + $processed = 0;
204 + $end_page = min($start_page + $batch_size, count($pages));
205 +
206 + for ($i = $start_page; $i < $end_page; $i++) {
207 + try {
208 + $page_number = $i + 1;
209 + $text = $pages[$i]->getText();
210 +
211 + if (empty($text)) {
212 + //error_log('Direct PDF: Empty text on page ' . $page_number);
213 + continue;
214 + }
215 +
216 + $sanitized_content = $this->mxchat_sanitize_content_for_api($text);
217 + if (empty($sanitized_content)) {
218 + //error_log('Direct PDF: No content after sanitization on page ' . $page_number);
219 + continue;
220 + }
221 +
222 + // Generate embedding
223 + $embedding_vector = $this->mxchat_generate_embedding($sanitized_content);
224 + if (is_string($embedding_vector)) {
225 + //error_log('Direct PDF: Embedding failed on page ' . $page_number . ': ' . $embedding_vector);
226 + continue;
227 + }
228 +
229 + // Create metadata
230 + $metadata = array(
231 + 'document_type' => 'pdf',
232 + 'total_pages' => count($pages),
233 + 'current_page' => $page_number,
234 + 'source_url' => $pdf_url
235 + );
236 +
237 + $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized_content;
238 + $page_url = esc_url($pdf_url . "#page=" . $page_number);
239 +
240 + // Store in database
241 + $db_result = MxChat_Utils::submit_content_to_db($content_with_metadata, $page_url, $api_key);
242 +
243 + if (is_wp_error($db_result)) {
244 + //error_log('Direct PDF: DB error on page ' . $page_number . ': ' . $db_result->get_error_message());
245 + continue;
246 + }
247 +
248 + $processed++;
249 + //error_log('Direct PDF: Successfully processed page ' . $page_number);
250 +
251 + // Update status
252 + $status['processed_pages'] = $i + 1;
253 + $status['last_update'] = time();
254 + $status['percentage'] = round(($status['processed_pages'] / $status['total_pages']) * 100);
255 + set_transient($status_key, $status, DAY_IN_SECONDS);
256 +
257 + } catch (Exception $e) {
258 + //error_log('Direct PDF: Error processing page ' . ($i + 1) . ': ' . $e->getMessage());
259 + continue;
260 + }
261 + }
262 +
263 + // Check if completed
264 + if ($status['processed_pages'] >= $status['total_pages']) {
265 + $status['status'] = 'complete';
266 + set_transient($status_key, $status, DAY_IN_SECONDS);
267 + //error_log('Direct PDF: Processing completed');
268 + }
269 +
270 + return $processed;
271 +
272 + } catch (Exception $e) {
273 + //error_log('Direct PDF processing error: ' . $e->getMessage());
274 + return 0;
275 + }
276 +}
277 +
278 +/**
279 + * Process a small sitemap batch manually - DIRECT PROCESSING
280 + */
281 +private function mxchat_manual_process_sitemap_batch($sitemap_url) {
282 + try {
283 + $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
284 + $status = get_transient($status_key);
285 +
286 + if (!$status || $status['status'] !== 'processing') {
287 + return 0;
288 + }
289 +
290 + //error_log('Manual Sitemap: Starting direct processing for ' . $sitemap_url);
291 +
292 + // Re-fetch the sitemap to get URLs
293 + $response = wp_remote_get($sitemap_url, array('timeout' => 30));
294 + if (is_wp_error($response)) {
295 + //error_log('Manual Sitemap: Failed to fetch sitemap');
296 + return 0;
297 + }
298 +
299 + $sitemap_content = wp_remote_retrieve_body($response);
300 + $xml = simplexml_load_string($sitemap_content);
301 +
302 + if (!$xml) {
303 + //error_log('Manual Sitemap: Invalid XML');
304 + return 0;
305 + }
306 +
307 + $urls = array();
308 + foreach ($xml->url as $url_element) {
309 + $urls[] = (string)$url_element->loc;
310 + }
311 +
312 + $current_processed = $status['processed_urls'] ?? 0;
313 + $batch_size = 5;
314 + $processed = 0;
315 +
316 + // Process next 2 URLs
317 + for ($i = $current_processed; $i < min($current_processed + $batch_size, count($urls)); $i++) {
318 + $url = $urls[$i];
319 +
320 + if ($this->mxchat_process_single_url_direct($url)) {
321 + $processed++;
322 + }
323 +
324 + // Update status
325 + $status['processed_urls'] = $i + 1;
326 + $status['last_update'] = time();
327 + $status['percentage'] = round(($status['processed_urls'] / $status['total_urls']) * 100);
328 + set_transient($status_key, $status, DAY_IN_SECONDS);
329 + }
330 +
331 + // Check if completed
332 + if ($status['processed_urls'] >= $status['total_urls']) {
333 + $status['status'] = 'complete';
334 + set_transient($status_key, $status, DAY_IN_SECONDS);
335 + }
336 +
337 + //error_log('Manual Sitemap: Processed ' . $processed . ' URLs');
338 + return $processed;
339 +
340 + } catch (Exception $e) {
341 + //error_log('Manual sitemap batch error: ' . $e->getMessage());
342 + return 0;
343 + }
344 +}
345 +
346 +/**
347 + * Process a single URL directly
348 + */
349 +private function mxchat_process_single_url_direct($url) {
350 + try {
351 + $response = wp_remote_get($url, array('timeout' => 30));
352 + if (is_wp_error($response)) {
353 + return false;
354 + }
355 +
356 + $html = wp_remote_retrieve_body($response);
357 + $content = $this->mxchat_extract_main_content($html);
358 + $sanitized = $this->mxchat_sanitize_content_for_api($content);
359 +
360 + if (empty($sanitized)) {
361 + return false;
362 + }
363 +
364 + $options = get_option('mxchat_options');
365 + $api_key = $options['api_key'] ?? '';
366 +
367 + $result = MxChat_Utils::submit_content_to_db($sanitized, $url, $api_key);
368 +
369 + return !is_wp_error($result);
370 +
371 + } catch (Exception $e) {
372 + //error_log('Single URL processing error: ' . $e->getMessage());
373 + return false;
374 + }
375 +}
376 +
377 +
113 378 // ========================================
114 379 // MAIN CONTENT SUBMISSION HANDLERS
115 380 // ========================================
116 381
@@ -126,18 +391,13 @@
126 391 wp_die(esc_html__('Nonce verification failed.', 'mxchat'));
127 392 }
128 393
129 394 // Sanitize the inputs.
130 - // Use wp_kses_post to allow safe HTML and wp_unslash to remove WordPress added slashes
131 - $article_content = wp_kses_post(wp_unslash($_POST['article_content']));
395 + $article_content = sanitize_textarea_field($_POST['article_content']);
132 396 $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
133 397
134 - // Get bot_id from form submission
135 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
136 -
137 - // Get bot-specific options and API key
138 - $bot_options = $this->get_bot_options($bot_id);
139 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
398 + // Get API key for submission
399 + $options = get_option('mxchat_options');
140 400 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
141 401
142 402 if (strpos($selected_model, 'voyage') === 0) {
143 403 $api_key = $options['voyage_api_key'] ?? '';
@@ -155,10 +415,10 @@
155 415 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
156 416 exit;
157 417 }
158 418
159 - // Use centralized utility function with bot_id
160 - $result = MxChat_Utils::submit_content_to_db($article_content, $article_url, $api_key, null, $bot_id);
419 + // Use centralized utility function for storage
420 + $result = MxChat_Utils::submit_content_to_db($article_content, $article_url, $api_key);
161 421
162 422 if (is_wp_error($result)) {
163 423 set_transient('mxchat_admin_notice_error',
164 424 esc_html__('Error storing content: ', 'mxchat') . $result->get_error_message(),
@@ -173,268 +433,17 @@
173 433
174 434 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
175 435 exit;
176 436 }
177 -
178 -/**
179 - * Handle the "YouTube" KB import source (admin-post form submission).
180 - *
181 - * Per-video description mode:
182 - * - auto: fetch oEmbed metadata (reliable) + best-effort captions transcript.
183 - * If no usable transcript, index the metadata anyway, tell the admin,
184 - * and bounce back with the manual box pre-filled (never fail silently).
185 - * - manual: the admin's own description is what gets indexed; metadata rides along.
186 - *
187 - * The row is stored with content_type 'youtube' and source_url = the canonical
188 - * watch URL, so re-importing the same video UPDATES the entry (source_url
189 - * duplicate handling in MxChat_Utils::store_in_wordpress_db) — that is also the
190 - * "augment a metadata-only entry" path.
191 - */
192 -public function mxchat_handle_youtube_submission() {
193 - if (!isset($_POST['submit_youtube']) || !current_user_can('manage_options')) {
194 - wp_die(esc_html__('Unauthorized access', 'mxchat'));
195 - }
196 -
197 - check_admin_referer('mxchat_submit_youtube_action', 'mxchat_submit_youtube_nonce');
198 -
199 - $redirect_url = admin_url('admin.php?page=mxchat-prompts');
200 -
201 - $youtube_url = isset($_POST['youtube_url']) ? esc_url_raw(wp_unslash($_POST['youtube_url'])) : '';
202 - $video_id = MxChat_Utils::parse_youtube_id($youtube_url);
203 -
204 - if (empty($video_id)) {
205 - set_transient('mxchat_admin_notice_error',
206 - esc_html__('That does not look like a link to a single YouTube video. Please paste a watch, youtu.be, or Shorts URL.', 'mxchat'),
207 - 30
208 - );
209 - wp_safe_redirect(esc_url($redirect_url));
210 - exit;
211 - }
212 -
213 - $canonical_url = 'https://www.youtube.com/watch?v=' . $video_id;
214 -
215 - $description_mode = (isset($_POST['youtube_description_mode']) && $_POST['youtube_description_mode'] === 'manual') ? 'manual' : 'auto';
216 - $manual_description = isset($_POST['youtube_description']) ? trim(wp_kses_post(wp_unslash($_POST['youtube_description']))) : '';
217 -
218 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
219 -
220 - // Resolve the embedding API key exactly like the sibling handlers.
221 - $bot_options = $this->get_bot_options($bot_id);
222 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
223 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
224 -
225 - if (strpos($selected_model, 'voyage') === 0) {
226 - $api_key = $options['voyage_api_key'] ?? '';
227 - $provider_name = 'Voyage AI';
228 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
229 - $api_key = $options['gemini_api_key'] ?? '';
230 - $provider_name = 'Google Gemini';
231 - } else {
232 - $api_key = $options['api_key'] ?? '';
233 - $provider_name = 'OpenAI';
234 - }
235 -
236 - if (empty($api_key)) {
237 - set_transient('mxchat_admin_notice_error',
238 - sprintf(
239 - esc_html__('%s API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
240 - $provider_name
241 - ),
242 - 30
243 - );
244 - wp_safe_redirect(esc_url($redirect_url));
245 - exit;
246 - }
247 -
248 - // Metadata is fetched in BOTH modes — it is the reliable half of auto, and in
249 - // manual mode it enriches the indexed text with the real title/channel.
250 - $meta = $this->mxchat_fetch_youtube_oembed($video_id);
251 - $video_title = isset($meta['title']) ? sanitize_text_field($meta['title']) : '';
252 - $video_channel = isset($meta['author_name']) ? sanitize_text_field($meta['author_name']) : '';
253 -
254 - $header_lines = 'YouTube Video: ' . ($video_title !== '' ? $video_title : $canonical_url) . "\n";
255 - if ($video_channel !== '') {
256 - $header_lines .= 'Channel: ' . $video_channel . "\n";
257 - }
258 - $header_lines .= 'URL: ' . $canonical_url . "\n\n";
259 -
260 - $transcript_missing = false;
261 -
262 - if ($description_mode === 'manual') {
263 - if ($manual_description === '') {
264 - set_transient('mxchat_admin_notice_error',
265 - esc_html__('Please write a description for the video, or switch to auto-fetch.', 'mxchat'),
266 - 30
267 - );
268 - wp_safe_redirect(esc_url($redirect_url));
269 - exit;
270 - }
271 - $indexed_text = $header_lines . $manual_description;
272 - } else {
273 - $transcript = $this->mxchat_fetch_youtube_transcript($video_id);
274 -
275 - if (strlen($transcript) >= 200) {
276 - $indexed_text = $header_lines . $transcript;
277 - } else {
278 - // Graceful fallback: captions disabled / blocked / no speech. Auto
279 - // reliably gets metadata; it does NOT guarantee a transcript.
280 - $transcript_missing = true;
281 -
282 - if ($video_title === '' && $video_channel === '') {
283 - // Both halves failed — nothing meaningful to index.
284 - set_transient('mxchat_admin_notice_error',
285 - esc_html__('Could not retrieve any information for that video (no metadata and no captions). Please check the URL, or use the manual description option.', 'mxchat'),
286 - 30
287 - );
288 - wp_safe_redirect(esc_url($redirect_url));
289 - exit;
290 - }
291 -
292 - $indexed_text = $header_lines . sprintf(
293 - /* translators: 1: video title, 2: channel name */
294 - __('A YouTube video titled "%1$s" from the channel %2$s.', 'mxchat'),
295 - $video_title !== '' ? $video_title : $canonical_url,
296 - $video_channel !== '' ? $video_channel : 'YouTube'
297 - );
298 - }
299 - }
300 -
301 - $result = MxChat_Utils::submit_content_to_db($indexed_text, $canonical_url, $api_key, null, $bot_id, 'youtube');
302 -
303 - if (is_wp_error($result)) {
304 - set_transient('mxchat_admin_notice_error',
305 - esc_html__('Error storing video in the knowledge base: ', 'mxchat') . $result->get_error_message(),
306 - 30
307 - );
308 - wp_safe_redirect(esc_url($redirect_url));
309 - exit;
310 - }
311 -
312 - if ($transcript_missing) {
313 - set_transient('mxchat_admin_notice_success',
314 - esc_html__('Video indexed from its title and channel — no captions were available for a transcript. The form below is pre-filled: write your own description and import again to improve matching (it updates the same entry).', 'mxchat'),
315 - 30
316 - );
317 - // Bounce back with prefill args so the page reopens the YouTube form in
318 - // manual mode with the URL + fetched title ready to augment.
319 - $redirect_url = add_query_arg(array(
320 - 'mxchat_yt_prefill' => '1',
321 - 'yt_url' => rawurlencode($canonical_url),
322 - 'yt_title' => rawurlencode($video_title),
323 - ), $redirect_url);
324 - } else {
325 - set_transient('mxchat_admin_notice_success',
326 - esc_html__('YouTube video successfully added to the knowledge base!', 'mxchat'),
327 - 30
328 - );
329 - }
330 -
331 - wp_safe_redirect(esc_url_raw($redirect_url));
332 - exit;
333 -}
334 -
335 -/**
336 - * Fetch YouTube oEmbed metadata for a video (no API key required).
337 - * Returns the decoded array (title, author_name, thumbnail_url, ...) or array().
338 - */
339 -private function mxchat_fetch_youtube_oembed($video_id) {
340 - $oembed_url = 'https://www.youtube.com/oembed?url=' . rawurlencode('https://www.youtube.com/watch?v=' . $video_id) . '&format=json';
341 - $response = wp_remote_get($oembed_url, array('timeout' => 15));
342 - if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
343 - return array();
344 - }
345 - $data = json_decode(wp_remote_retrieve_body($response), true);
346 - return is_array($data) ? $data : array();
347 -}
348 -
349 -/**
350 - * Best-effort captions transcript for a video. Deliberately ISOLATED: this uses
351 - * YouTube's unofficial timedtext route (the caption track list embedded in the
352 - * watch page), which YouTube has broken before and will break again. Every
353 - * failure mode returns '' so a break degrades to the metadata-only import path
354 - * instead of erroring the whole submission. Do not let anything in here throw.
355 - */
356 -private function mxchat_fetch_youtube_transcript($video_id) {
357 - $watch_url = 'https://www.youtube.com/watch?v=' . $video_id . '&hl=en';
358 -
359 - // First try the honest ingest UA; some responses omit the player config for
360 - // bot UAs, so retry once with a browser UA before giving up.
361 - $user_agents = array(
362 - mxchat_ingest_user_agent(),
363 - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
364 - );
365 -
366 - $tracks = array();
367 - foreach ($user_agents as $ua) {
368 - $response = wp_remote_get($watch_url, array(
369 - 'timeout' => 20,
370 - 'user-agent' => $ua,
371 - ));
372 - if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
373 - continue;
374 - }
375 - $body = wp_remote_retrieve_body($response);
376 - if (!is_string($body) || $body === '' || !preg_match('/"captionTracks":(\[.*?\])(?=,")/s', $body, $m)) {
377 - continue;
378 - }
379 - $decoded = json_decode($m[1], true);
380 - if (is_array($decoded) && !empty($decoded)) {
381 - $tracks = $decoded;
382 - break;
383 - }
384 - }
385 -
386 - if (empty($tracks)) {
387 - return '';
388 - }
389 -
390 - // Prefer an English track, else take the first offered.
391 - $chosen = null;
392 - foreach ($tracks as $track) {
393 - if (isset($track['languageCode']) && strpos($track['languageCode'], 'en') === 0) {
394 - $chosen = $track;
395 - break;
396 - }
397 - }
398 - if ($chosen === null) {
399 - $chosen = $tracks[0];
400 - }
401 - if (empty($chosen['baseUrl']) || !is_string($chosen['baseUrl'])) {
402 - return '';
403 - }
404 -
405 - $timedtext = wp_remote_get($chosen['baseUrl'], array('timeout' => 20));
406 - if (is_wp_error($timedtext) || wp_remote_retrieve_response_code($timedtext) !== 200) {
407 - return '';
408 - }
409 - $xml = wp_remote_retrieve_body($timedtext);
410 - if (!is_string($xml) || strpos($xml, '<text') === false) {
411 - return '';
412 - }
413 -
414 - // <text start=".." dur="..">caption</text> — strip tags, decode the
415 - // double-encoded entities timedtext ships, collapse whitespace.
416 - $text = preg_replace('/<[^>]+>/', ' ', $xml);
417 - $text = html_entity_decode(html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'), ENT_QUOTES | ENT_HTML5, 'UTF-8');
418 - $text = trim(preg_replace('/\s+/u', ' ', $text));
419 -
420 - return $text;
421 -}
422 -
423 437 public function mxchat_is_pdf_url($url, $response) {
424 438 $content_type = wp_remote_retrieve_header($response, 'content-type');
425 439 $file_extension = strtolower(pathinfo($url, PATHINFO_EXTENSION));
426 440
427 - // Check Content-Disposition header for .pdf filename (Google Drive sends this)
428 - $disposition = wp_remote_retrieve_header($response, 'content-disposition');
429 - $has_pdf_disposition = ! empty($disposition) && stripos($disposition, '.pdf') !== false;
430 -
431 - return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf' || $has_pdf_disposition;
441 + return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf';
432 442 }
433 -
434 -
435 -public function mxchat_handle_pdf_for_knowledge_base($pdf_url, $response, $bot_id = 'default') {
443 +public function mxchat_handle_pdf_for_knowledge_base($pdf_url, $response) {
436 444 if (!current_user_can('manage_options')) {
445 + //error_log(esc_html__('Unauthorized PDF processing attempt', 'mxchat'));
437 446 return false;
438 447 }
439 448
440 449 $pdf_url = esc_url_raw($pdf_url);
@@ -440,8 +449,9 @@
440 449 $pdf_url = esc_url_raw($pdf_url);
441 450 $upload_dir = wp_upload_dir();
442 451
443 452 if (isset($upload_dir['error']) && $upload_dir['error'] !== false) {
453 + //error_log(sprintf(esc_html__('Upload directory error: %s', 'mxchat'), esc_html($upload_dir['error'])));
444 454 return false;
445 455 }
446 456
447 457 $pdf_filename = sanitize_file_name('mxchat_kb_' . time() . '.pdf');
@@ -448,12 +458,14 @@
448 458 $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
449 459
450 460 $response_body = wp_remote_retrieve_body($response);
451 461 if (empty($response_body)) {
462 + //error_log(esc_html__('Empty PDF response body', 'mxchat'));
452 463 return false;
453 464 }
454 465
455 466 if (!wp_mkdir_p(dirname($pdf_path))) {
467 + //error_log(sprintf(esc_html__('Failed to create directory for PDF: %s', 'mxchat'), esc_html($pdf_path)));
456 468 return false;
457 469 }
458 470
459 471 try {
@@ -462,915 +474,295 @@
462 474 if (!file_exists($pdf_path)) {
463 475 throw new Exception(__('Failed to save PDF file', 'mxchat'));
464 476 }
465 477
466 - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
467 -
468 - if ($total_pages === false || $total_pages < 1) {
469 - throw new Exception(__('Invalid PDF: Unable to parse or no pages found', 'mxchat'));
470 - }
478 + $parser = new \Smalot\PdfParser\Parser();
479 + $pdf = $parser->parseFile($pdf_path);
480 + $total_pages = absint(count($pdf->getPages()));
471 481
472 - // Create unique queue ID
473 - $queue_id = 'pdf_' . md5($pdf_url . time());
474 -
475 - // Create array of pages to process
476 - $pages = array();
477 - for ($i = 1; $i <= $total_pages; $i++) {
478 - $pages[] = array(
479 - 'pdf_path' => $pdf_path,
480 - 'pdf_url' => $pdf_url,
481 - 'page_number' => $i,
482 - 'total_pages' => $total_pages
483 - );
482 + if ($total_pages < 1) {
483 + throw new Exception(__('Invalid PDF: no pages found', 'mxchat'));
484 484 }
485 485
486 - // Add pages to queue
487 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
486 + wp_schedule_single_event(time(), 'mxchat_process_pdf_pages', array(
487 + 'pdf_path' => $pdf_path,
488 + 'pdf_url' => $pdf_url,
489 + 'total_pages' => $total_pages,
490 + 'batch_size' => absint(15),
491 + 'batch_pause' => absint(10)
492 + ));
488 493
489 - if ($queued_count === 0) {
490 - wp_delete_file($pdf_path);
491 - throw new Exception(__('Failed to add PDF pages to processing queue', 'mxchat'));
492 - }
494 + $status_data = array(
495 + 'total_pages' => $total_pages,
496 + 'processed_pages' => 0,
497 + 'status' => 'processing',
498 + 'last_update' => time()
499 + );
493 500
494 - // Store queue metadata
495 - $this->mxchat_set_queue_meta($queue_id, 'source_url', $pdf_url);
496 - $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'pdf');
497 - $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_pages);
498 - $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
499 - $this->mxchat_set_queue_meta($queue_id, 'pdf_path', $pdf_path);
500 - $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
501 + set_transient(
502 + sanitize_key('mxchat_pdf_status_' . md5($pdf_url)),
503 + array_map('sanitize_text_field', $status_data),
504 + DAY_IN_SECONDS
505 + );
501 506
502 - // Store queue ID in transient for status tracking
503 - set_transient('mxchat_active_queue_pdf', $queue_id, DAY_IN_SECONDS);
504 - set_transient('mxchat_last_pdf_url', $pdf_url, DAY_IN_SECONDS);
507 + return __('scheduled', 'mxchat');
505 508
506 - return 'queued';
507 -
508 509 } catch (Exception $e) {
510 + //error_log(sprintf(esc_html__('Error preparing PDF for processing: %s', 'mxchat'), esc_html($e->getMessage())));
509 511 if (file_exists($pdf_path)) {
510 512 wp_delete_file($pdf_path);
511 513 }
512 - return $e->getMessage();
514 + return false;
513 515 }
514 516 }
515 517
516 -/**
517 - * Handle direct PDF file upload from the knowledge base page
518 - */
519 -public function mxchat_handle_pdf_file_submission() {
520 - if (!isset($_POST['submit_pdf_file']) || !current_user_can('manage_options')) {
521 - wp_die(esc_html__('Unauthorized access', 'mxchat'));
522 - }
518 +public function mxchat_process_pdf_pages_cron($pdf_path, $pdf_url, $total_pages, $batch_size, $batch_pause) {
519 + // Validate inputs
520 + $pdf_path = sanitize_text_field($pdf_path);
521 + $pdf_url = esc_url_raw($pdf_url);
522 + $total_pages = absint($total_pages);
523 + $batch_size = absint($batch_size);
524 + $batch_pause = absint($batch_pause);
523 525
524 - check_admin_referer('mxchat_submit_pdf_file_action', 'mxchat_submit_pdf_file_nonce');
525 -
526 - $redirect_url = admin_url('admin.php?page=mxchat-prompts');
527 -
528 - // Validate file upload
529 - if (empty($_FILES['pdf_file']) || $_FILES['pdf_file']['error'] !== UPLOAD_ERR_OK) {
530 - $error_code = isset($_FILES['pdf_file']['error']) ? $_FILES['pdf_file']['error'] : UPLOAD_ERR_NO_FILE;
531 - $error_messages = array(
532 - UPLOAD_ERR_INI_SIZE => __('The uploaded file exceeds the server upload_max_filesize limit.', 'mxchat'),
533 - UPLOAD_ERR_FORM_SIZE => __('The uploaded file exceeds the form MAX_FILE_SIZE limit.', 'mxchat'),
534 - UPLOAD_ERR_PARTIAL => __('The file was only partially uploaded.', 'mxchat'),
535 - UPLOAD_ERR_NO_FILE => __('No file was uploaded. Please select a PDF file.', 'mxchat'),
536 - UPLOAD_ERR_NO_TMP_DIR => __('Server missing temporary folder.', 'mxchat'),
537 - UPLOAD_ERR_CANT_WRITE => __('Server failed to write file to disk.', 'mxchat'),
538 - );
539 - $error_msg = isset($error_messages[$error_code]) ? $error_messages[$error_code] : __('Unknown upload error.', 'mxchat');
540 - set_transient('mxchat_admin_notice_error', $error_msg, 30);
541 - wp_safe_redirect(esc_url($redirect_url));
542 - exit;
543 - }
544 -
545 - $file = $_FILES['pdf_file'];
546 -
547 - // Validate MIME type
548 - $finfo = finfo_open(FILEINFO_MIME_TYPE);
549 - $mime_type = finfo_file($finfo, $file['tmp_name']);
550 - finfo_close($finfo);
551 -
552 - if ($mime_type !== 'application/pdf') {
553 - set_transient('mxchat_admin_notice_error',
554 - esc_html__('Invalid file type. Only PDF files are accepted.', 'mxchat'),
555 - 30
556 - );
557 - wp_safe_redirect(esc_url($redirect_url));
558 - exit;
559 - }
560 -
561 - // Validate extension
562 - $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
563 - if ($ext !== 'pdf') {
564 - set_transient('mxchat_admin_notice_error',
565 - esc_html__('Invalid file extension. Only .pdf files are accepted.', 'mxchat'),
566 - 30
567 - );
568 - wp_safe_redirect(esc_url($redirect_url));
569 - exit;
570 - }
571 -
572 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
573 - $original_filename = sanitize_file_name($file['name']);
574 -
575 - $upload_dir = wp_upload_dir();
576 - if (isset($upload_dir['error']) && $upload_dir['error'] !== false) {
577 - set_transient('mxchat_admin_notice_error',
578 - esc_html__('WordPress upload directory is not writable.', 'mxchat'),
579 - 30
580 - );
581 - wp_safe_redirect(esc_url($redirect_url));
582 - exit;
583 - }
584 -
585 - $pdf_filename = sanitize_file_name('mxchat_kb_' . time() . '.pdf');
586 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
587 -
588 - if (!wp_mkdir_p(dirname($pdf_path))) {
589 - set_transient('mxchat_admin_notice_error',
590 - esc_html__('Failed to create upload directory.', 'mxchat'),
591 - 30
592 - );
593 - wp_safe_redirect(esc_url($redirect_url));
594 - exit;
595 - }
596 -
597 - // Move uploaded file
598 - if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
599 - set_transient('mxchat_admin_notice_error',
600 - esc_html__('Failed to save uploaded PDF file.', 'mxchat'),
601 - 30
602 - );
603 - wp_safe_redirect(esc_url($redirect_url));
604 - exit;
605 - }
606 -
607 526 try {
608 - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
609 -
610 - if ($total_pages === false || $total_pages < 1) {
611 - throw new Exception(__('Invalid PDF: Unable to parse or no pages found', 'mxchat'));
527 + if (!file_exists($pdf_path)) {
528 + throw new Exception(sprintf('PDF file not found at path: %s', esc_html($pdf_path)));
612 529 }
613 530
614 - // Use original filename as the source identifier
615 - $source_label = 'upload://' . $original_filename;
616 -
617 - $queue_id = 'pdf_' . md5($source_label . time());
618 -
619 - $pages = array();
620 - for ($i = 1; $i <= $total_pages; $i++) {
621 - $pages[] = array(
622 - 'pdf_path' => $pdf_path,
623 - 'pdf_url' => $source_label,
624 - 'page_number' => $i,
625 - 'total_pages' => $total_pages,
626 - );
627 - }
628 -
629 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
630 -
631 - if ($queued_count === 0) {
632 - wp_delete_file($pdf_path);
633 - throw new Exception(__('Failed to add PDF pages to processing queue', 'mxchat'));
634 - }
635 -
636 - $this->mxchat_set_queue_meta($queue_id, 'source_url', $source_label);
637 - $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'pdf');
638 - $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_pages);
639 - $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
640 - $this->mxchat_set_queue_meta($queue_id, 'pdf_path', $pdf_path);
641 - $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
642 -
643 - set_transient('mxchat_active_queue_pdf', $queue_id, DAY_IN_SECONDS);
644 - set_transient('mxchat_last_pdf_url', $source_label, DAY_IN_SECONDS);
645 -
646 - set_transient('mxchat_admin_notice_success',
647 - sprintf(
648 - esc_html__('PDF "%s" (%d pages) queued for processing. Processing will start automatically.', 'mxchat'),
649 - esc_html($original_filename),
650 - $total_pages
651 - ),
652 - 30
653 - );
654 -
655 - } catch (Exception $e) {
656 - if (file_exists($pdf_path)) {
657 - wp_delete_file($pdf_path);
658 - }
659 - set_transient('mxchat_admin_notice_error',
660 - esc_html__('Failed to process uploaded PDF: ', 'mxchat') . esc_html($e->getMessage()),
661 - 30
662 - );
663 - }
664 -
665 - wp_safe_redirect(esc_url($redirect_url));
666 - exit;
667 -}
668 -
669 -/**
670 - * Validate PDF and count pages with multiple parser attempts
671 - */
672 -private function mxchat_validate_and_count_pdf_pages($pdf_path) {
673 - // Method 1: Try with Smalot PDF Parser (your current method)
674 - try {
675 - mxchat_load_pdf_parser();
676 531 $parser = new \Smalot\PdfParser\Parser();
677 532 $pdf = $parser->parseFile($pdf_path);
678 533 $pages = $pdf->getPages();
679 - $page_count = count($pages);
680 -
681 - if ($page_count > 0) {
682 - //error_log('PDF parsed successfully with Smalot parser: ' . $page_count . ' pages');
683 - return $page_count;
684 - }
685 - } catch (Exception $e) {
686 - //error_log('Smalot PDF parser failed: ' . $e->getMessage());
687 - }
688 534
689 - // Method 2: Try with pdfinfo command (if available)
690 - if (function_exists('shell_exec') && !$this->mxchat_is_shell_disabled()) {
691 - try {
692 - $command = 'pdfinfo ' . escapeshellarg($pdf_path) . ' 2>&1';
693 - $output = shell_exec($command);
694 -
695 - if ($output && preg_match('/Pages:\s*(\d+)/', $output, $matches)) {
696 - $page_count = intval($matches[1]);
697 - if ($page_count > 0) {
698 - //error_log('PDF parsed successfully with pdfinfo: ' . $page_count . ' pages');
699 - return $page_count;
700 - }
701 - }
702 - } catch (Exception $e) {
703 - //error_log('pdfinfo command failed: ' . $e->getMessage());
704 - }
705 - }
535 + // Get current progress
536 + $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
537 + $status = get_transient($status_key);
706 538
707 - // Method 3: Try to repair PDF and parse again
708 - try {
709 - $repaired_path = $this->mxchat_attempt_pdf_repair($pdf_path);
710 - if ($repaired_path && $repaired_path !== $pdf_path) {
711 - mxchat_load_pdf_parser();
712 - $parser = new \Smalot\PdfParser\Parser();
713 - $pdf = $parser->parseFile($repaired_path);
714 - $pages = $pdf->getPages();
715 - $page_count = count($pages);
716 -
717 - if ($page_count > 0) {
718 - // Replace original with repaired version
719 - copy($repaired_path, $pdf_path);
720 - unlink($repaired_path);
721 - //error_log('PDF repaired and parsed successfully: ' . $page_count . ' pages');
722 - return $page_count;
723 - }
724 -
725 - // Clean up repaired file if it didn't work
726 - unlink($repaired_path);
539 + if (!$status || !is_array($status)) {
540 + throw new Exception('Invalid status data retrieved from transient');
727 541 }
728 - } catch (Exception $e) {
729 - //error_log('PDF repair attempt failed: ' . $e->getMessage());
730 - }
731 542
732 - // Method 4: Manual PDF structure analysis (basic page count)
733 - try {
734 - $page_count = $this->mxchat_manual_pdf_page_count($pdf_path);
735 - if ($page_count > 0) {
736 - //error_log('PDF page count determined manually: ' . $page_count . ' pages');
737 - return $page_count;
543 + // Initialize failed pages list if it doesn't exist
544 + if (!isset($status['failed_pages_list']) || !is_array($status['failed_pages_list'])) {
545 + $status['failed_pages_list'] = [];
738 546 }
739 - } catch (Exception $e) {
740 - //error_log('Manual PDF analysis failed: ' . $e->getMessage());
741 - }
742 547
743 - //error_log('All PDF parsing methods failed for: ' . $pdf_path);
744 - return false;
745 -}
746 -
747 -/**
748 - * Check if shell_exec is disabled
749 - */
750 -private function mxchat_is_shell_disabled() {
751 - $disabled = explode(',', ini_get('disable_functions'));
752 - return in_array('shell_exec', $disabled);
753 -}
754 -
755 -/**
756 - * Attempt to repair PDF using basic methods
757 - */
758 -private function mxchat_attempt_pdf_repair($pdf_path) {
759 - try {
760 - $content = file_get_contents($pdf_path);
761 - if (!$content) {
762 - return false;
548 + $start_page = absint($status['processed_pages']);
549 + $end_page = min($start_page + $batch_size, $total_pages);
550 + $options = get_option('mxchat_options');
551 +
552 + if (empty($options['api_key'])) {
553 + throw new Exception('API key is missing or invalid');
763 554 }
764 555
765 - // Check if PDF starts with proper header
766 - if (substr($content, 0, 4) !== '%PDF') {
767 - // Try to find PDF header in the content
768 - $header_pos = strpos($content, '%PDF');
769 - if ($header_pos !== false && $header_pos < 1024) {
770 - // Remove junk before PDF header
771 - $content = substr($content, $header_pos);
772 - $repaired_path = $pdf_path . '.repaired';
773 - file_put_contents($repaired_path, $content);
774 - return $repaired_path;
775 - }
776 - }
556 + $successful_pages = 0;
557 + $failed_pages = 0;
777 558
778 - // Check for EOF marker
779 - $content = rtrim($content);
780 - if (!preg_match('/%%EOF\s*$/', $content)) {
781 - // Add EOF marker if missing
782 - $content .= "\n%%EOF";
783 - $repaired_path = $pdf_path . '.repaired';
784 - file_put_contents($repaired_path, $content);
785 - return $repaired_path;
786 - }
559 + for ($i = $start_page; $i < $end_page; $i++) {
560 + $page_number = $i + 1;
561 + $max_retries = 3;
562 + $retry_count = 0;
563 + $page_processed = false;
564 + $last_error = '';
787 565
788 - } catch (Exception $e) {
789 - //error_log('PDF repair error: ' . $e->getMessage());
790 - }
566 + while (!$page_processed && $retry_count < $max_retries) {
567 + try {
568 + $text = $pages[$i]->getText();
569 +
570 + if (empty($text)) {
571 + throw new Exception("Empty text on page {$page_number}");
572 + }
573 +
574 + $sanitized_content = $this->mxchat_sanitize_content_for_api($text);
791 575
792 - return false;
793 -}
576 + if (empty($sanitized_content)) {
577 + throw new Exception("No valid content after sanitization on page {$page_number}");
578 + }
794 579
795 -/**
796 - * Manual PDF page counting by analyzing PDF structure
797 - */
798 -private function mxchat_manual_pdf_page_count($pdf_path) {
799 - try {
800 - $content = file_get_contents($pdf_path);
801 - if (!$content) {
802 - return 0;
803 - }
580 + $embedding_vector = $this->mxchat_generate_embedding($sanitized_content);
581 +
582 + if (is_string($embedding_vector)) {
583 + throw new Exception("Embedding generation failed: " . $embedding_vector);
584 + }
585 +
586 + if (!is_array($embedding_vector)) {
587 + throw new Exception("Embedding generation returned unexpected result type: " . gettype($embedding_vector));
588 + }
589 +
590 + $metadata = array(
591 + 'document_type' => 'pdf',
592 + 'total_pages' => $total_pages,
593 + 'current_page' => $page_number,
594 + 'prev_page' => $i > 0 ? $i : null,
595 + 'next_page' => $i < ($total_pages - 1) ? $i + 2 : null,
596 + 'source_url' => $pdf_url
597 + );
804 598
805 - // Method 1: Count /Type /Page objects
806 - $page_count = preg_match_all('/\/Type\s*\/Page[^s]/', $content);
807 - if ($page_count > 0) {
808 - return $page_count;
809 - }
599 + $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized_content;
600 + $page_url = esc_url($pdf_url . "#page=" . $page_number);
810 601
811 - // Method 2: Look for /Count in pages object
812 - if (preg_match('/\/Type\s*\/Pages.*?\/Count\s+(\d+)/', $content, $matches)) {
813 - return intval($matches[1]);
814 - }
602 + $db_result = MxChat_Utils::submit_content_to_db($content_with_metadata, $page_url, $options['api_key']);
603 +
604 + if (is_wp_error($db_result)) {
605 + throw new Exception("Database submission failed: " . $db_result->get_error_message());
606 + }
815 607
816 - // Method 3: Count page references
817 - $page_count = preg_match_all('/\d+\s+0\s+obj\s*<<[^>]*\/Type\s*\/Page/', $content);
818 - if ($page_count > 0) {
819 - return $page_count;
820 - }
821 -
822 - } catch (Exception $e) {
823 - //error_log('Manual PDF analysis error: ' . $e->getMessage());
824 - }
825 -
826 - return 0;
827 -}
828 -
829 -
830 -public function mxchat_save_inline_prompt() {
831 - // DEBUG: Log what we're receiving
832 - //error_log('=== MXCHAT DEBUG ===');
833 - //error_log('POST data: ' . print_r($_POST, true));
834 - //error_log('Nonce from POST: ' . ($_POST['_ajax_nonce'] ?? 'NOT FOUND'));
835 -
836 - // Check for nonce security
837 - check_ajax_referer('mxchat_save_inline_nonce', '_ajax_nonce');
838 -
839 - // If we get here, nonce passed
840 - //error_log('Nonce verification PASSED');
841 -
842 - // Verify permissions
843 - if (!current_user_can('manage_options')) {
844 - wp_send_json_error(esc_html__('Permission denied.', 'mxchat'));
845 - return;
846 - }
847 -
848 - global $wpdb;
849 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
850 -
851 - // Validate and sanitize input data
852 - $prompt_id = isset($_POST['id']) ? absint($_POST['id']) : 0;
853 - $article_content = isset($_POST['article_content']) ? wp_kses_post(wp_unslash($_POST['article_content'])) : '';
854 - $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
855 -
856 - if ($prompt_id > 0 && !empty($article_content)) {
857 - // Re-generate the embedding vector for the updated content
858 - $embedding_vector = $this->mxchat_generate_embedding($article_content);
859 - if (is_array($embedding_vector)) {
860 - // Serialize the embedding vector before storing it
861 - $embedding_vector_serialized = serialize($embedding_vector);
862 - // Update the prompt in the database
863 - $updated = $wpdb->update(
864 - $table_name,
865 - array(
866 - 'article_content' => $article_content,
867 - 'embedding_vector' => $embedding_vector_serialized,
868 - 'source_url' => $article_url,
869 - ),
870 - array('id' => $prompt_id),
871 - array('%s', '%s', '%s'),
872 - array('%d')
873 - );
874 - if ($updated !== false) {
875 - wp_send_json_success();
876 - } else {
877 - MxChat_Admin::mxchat_log_debug('knowledge_error', 'Database update failed when saving knowledge entry');
878 - wp_send_json_error(esc_html__('Database update failed.', 'mxchat'));
608 + // Success!
609 + $page_processed = true;
610 + $successful_pages++;
611 +
612 + } catch (Exception $e) {
613 + $retry_count++;
614 + $last_error = $e->getMessage();
615 +
616 + //error_log("PDF page {$page_number} failed (attempt {$retry_count}/{$max_retries}): " . $last_error);
617 +
618 + if ($retry_count < $max_retries) {
619 + // Wait before retry (exponential backoff: 1s, 2s, 4s)
620 + sleep(pow(2, $retry_count - 1));
621 + }
622 + }
879 623 }
880 - } else {
881 - MxChat_Admin::mxchat_log_debug('embedding_error', 'Embedding generation failed for knowledge entry');
882 - wp_send_json_error(esc_html__('Embedding generation failed.', 'mxchat'));
883 - }
884 - } else {
885 - wp_send_json_error(esc_html__('Invalid data.', 'mxchat'));
886 - }
887 -}
888 624
889 -
890 -/**
891 - * AJAX: Get full content for editing — reassembles chunks if needed.
892 - * Works for both WordPress DB and Pinecone entries.
893 - */
894 -public function ajax_mxchat_get_entry_content() {
895 - check_ajax_referer('mxchat_edit_entry_nonce', 'nonce');
896 -
897 - if ( ! current_user_can('manage_options') ) {
898 - wp_send_json_error( array( 'message' => 'Permission denied.' ) );
899 - }
900 -
901 - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : '';
902 - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0;
903 - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress';
904 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
905 -
906 - if ( $data_source === 'pinecone' ) {
907 - // Pinecone: fetch vectors by source_url, reassemble chunks
908 - $content = $this->get_pinecone_entry_content( $source_url, $entry_id, $bot_id );
909 - } else {
910 - // WordPress DB
911 - $content = $this->get_wordpress_entry_content( $source_url, $entry_id );
912 - }
913 -
914 - if ( is_wp_error( $content ) ) {
915 - wp_send_json_error( array( 'message' => $content->get_error_message() ) );
916 - }
917 -
918 - wp_send_json_success( $content );
919 -}
920 -
921 -/**
922 - * Get content from WordPress DB — reassembles chunks by source_url.
923 - */
924 -private function get_wordpress_entry_content( $source_url, $entry_id ) {
925 - global $wpdb;
926 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
927 -
928 - // If we have a source_url, check for chunks
929 - if ( ! empty( $source_url ) && strpos( $source_url, 'mxchat://' ) !== 0 ) {
930 - $rows = $wpdb->get_results( $wpdb->prepare(
931 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE source_url = %s ORDER BY id ASC",
932 - $source_url
933 - ) );
934 -
935 - if ( $rows && count( $rows ) > 1 ) {
936 - // Multiple rows = chunked. Reassemble.
937 - $chunks = array();
938 - foreach ( $rows as $row ) {
939 - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content );
940 - $index = isset( $parsed['metadata']['chunk_index'] ) ? intval( $parsed['metadata']['chunk_index'] ) : count( $chunks );
941 - $chunks[ $index ] = $parsed['text'];
625 + // If page still not processed after all retries, mark as failed
626 + if (!$page_processed) {
627 + $failed_pages++;
628 + $status['failed_pages_list'][] = [
629 + 'page' => $page_number,
630 + 'error' => $last_error,
631 + 'time' => time(),
632 + 'retries' => $max_retries
633 + ];
634 +
635 + // Limit failed pages list to prevent memory issues
636 + if (count($status['failed_pages_list']) > 50) {
637 + $status['failed_pages_list'] = array_slice($status['failed_pages_list'], -50);
638 + }
639 +
640 + //error_log("PDF page {$page_number} permanently failed after {$max_retries} attempts: " . $last_error);
942 641 }
943 - ksort( $chunks );
944 - return array(
945 - 'content' => implode( "\n\n", $chunks ),
946 - 'source_url' => $source_url,
947 - 'is_chunked' => true,
948 - 'chunk_count' => count( $chunks ),
949 - 'content_type' => $rows[0]->content_type,
950 - );
951 - } elseif ( $rows && count( $rows ) === 1 ) {
952 - $parsed = MxChat_Chunker::parse_stored_chunk( $rows[0]->article_content );
953 - return array(
954 - 'content' => $parsed['text'],
955 - 'source_url' => $source_url,
956 - 'entry_id' => $rows[0]->id,
957 - 'is_chunked' => false,
958 - 'content_type' => $rows[0]->content_type,
959 - );
960 - }
961 - }
962 642
963 - // Fallback: fetch by ID
964 - if ( $entry_id > 0 ) {
965 - $row = $wpdb->get_row( $wpdb->prepare(
966 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE id = %d",
967 - $entry_id
968 - ) );
969 - if ( $row ) {
970 - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content );
971 - return array(
972 - 'content' => $parsed['text'],
973 - 'source_url' => $row->source_url,
974 - 'entry_id' => $row->id,
975 - 'is_chunked' => false,
976 - 'content_type' => $row->content_type,
977 - );
643 + // Update progress
644 + $status['processed_pages'] = absint($page_number);
645 + $status['last_update'] = time();
646 + $status['failed_pages'] = absint($status['failed_pages'] ?? 0) + ($page_processed ? 0 : 1);
647 +
648 + set_transient($status_key, array_map('sanitize_text_field', $status), DAY_IN_SECONDS);
978 649 }
979 - }
980 650
981 - return new WP_Error( 'not_found', 'Entry not found.' );
982 -}
983 -
984 -/**
985 - * Get content from Pinecone — fetches vectors by source_url, reassembles chunks.
986 - */
987 -private function get_pinecone_entry_content( $source_url, $entry_id, $bot_id ) {
988 - if ( ! class_exists('MxChat_Pinecone_Manager') ) {
989 - return new WP_Error( 'pinecone_unavailable', 'Pinecone manager not available.' );
990 - }
991 -
992 - // Get Pinecone config
993 - if ( $bot_id === 'default' || ! class_exists('MxChat_Multi_Bot_Manager') ) {
994 - $pinecone_options = get_option('mxchat_pinecone_addon_options');
995 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
996 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
997 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
998 - } else {
999 - $bot_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
1000 - $api_key = $bot_config['api_key'] ?? '';
1001 - $host = $bot_config['host'] ?? '';
1002 - $namespace = $bot_config['namespace'] ?? '';
1003 - }
1004 -
1005 - if ( empty($host) || empty($api_key) ) {
1006 - return new WP_Error( 'pinecone_config', 'Pinecone not configured.' );
1007 - }
1008 -
1009 - // List vectors with the source_url prefix
1010 - $base_id = md5( $source_url );
1011 - $vector_ids = array( $base_id );
1012 -
1013 - // Find chunk vectors
1014 - $list_url = "https://{$host}/vectors/list";
1015 - $list_body = array( 'prefix' => $base_id . '_chunk_', 'limit' => 100 );
1016 - if ( ! empty($namespace) ) {
1017 - $list_body['namespace'] = $namespace;
1018 - }
1019 -
1020 - $list_resp = wp_remote_post( $list_url, array(
1021 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
1022 - 'body' => wp_json_encode( $list_body ),
1023 - 'timeout' => 15,
1024 - ) );
1025 -
1026 - if ( ! is_wp_error($list_resp) ) {
1027 - $list_data = json_decode( wp_remote_retrieve_body($list_resp), true );
1028 - if ( ! empty($list_data['vectors']) ) {
1029 - foreach ( $list_data['vectors'] as $v ) {
1030 - $vector_ids[] = $v['id'];
651 + // Schedule next batch if needed
652 + if ($end_page < $total_pages) {
653 + wp_schedule_single_event(time() + $batch_pause, 'mxchat_process_pdf_pages', array(
654 + 'pdf_path' => $pdf_path,
655 + 'pdf_url' => $pdf_url,
656 + 'total_pages' => $total_pages,
657 + 'batch_size' => $batch_size,
658 + 'batch_pause' => $batch_pause
659 + ));
660 + } else {
661 + // Processing complete
662 + $status['status'] = 'complete';
663 + $status['processed_pages'] = $total_pages;
664 +
665 + // Add completion summary
666 + $status['completion_summary'] = [
667 + 'total_pages' => $total_pages,
668 + 'successful_pages' => $total_pages - absint($status['failed_pages'] ?? 0),
669 + 'failed_pages' => absint($status['failed_pages'] ?? 0),
670 + 'completion_time' => current_time('mysql')
671 + ];
672 +
673 + // Save the completed status (don't delete it - let user dismiss manually)
674 + set_transient($status_key, array_map('sanitize_text_field', $status), DAY_IN_SECONDS);
675 +
676 + // Clean up the temporary PDF file
677 + if (file_exists($pdf_path)) {
678 + wp_delete_file($pdf_path);
1031 679 }
680 +
681 + // DON'T delete the status transients here - let user dismiss manually
1032 682 }
1033 - }
1034 683
1035 - // Fetch vectors with metadata
1036 - $fetch_url = "https://{$host}/vectors/fetch";
1037 - $fetch_body = array( 'ids' => $vector_ids );
1038 - if ( ! empty($namespace) ) {
1039 - $fetch_body['namespace'] = $namespace;
1040 - }
1041 -
1042 - $fetch_resp = wp_remote_post( $fetch_url, array(
1043 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
1044 - 'body' => wp_json_encode( $fetch_body ),
1045 - 'timeout' => 15,
1046 - ) );
1047 -
1048 - if ( is_wp_error($fetch_resp) ) {
1049 - return new WP_Error( 'pinecone_fetch', 'Failed to fetch from Pinecone.' );
1050 - }
1051 -
1052 - $fetch_data = json_decode( wp_remote_retrieve_body($fetch_resp), true );
1053 - $vectors = $fetch_data['vectors'] ?? array();
1054 -
1055 - if ( empty($vectors) ) {
1056 - return new WP_Error( 'not_found', 'Entry not found in Pinecone.' );
1057 - }
1058 -
1059 - // Reassemble chunks
1060 - $chunks = array();
1061 - $content_type = 'content';
1062 - foreach ( $vectors as $vid => $vector ) {
1063 - $meta = $vector['metadata'] ?? array();
1064 - $text = $meta['text'] ?? '';
1065 - $index = $meta['chunk_index'] ?? 0;
1066 - $content_type = $meta['type'] ?? 'content';
1067 - $chunks[ intval($index) ] = $text;
1068 - }
1069 - ksort( $chunks );
1070 -
1071 - return array(
1072 - 'content' => implode( "\n\n", $chunks ),
1073 - 'source_url' => $source_url,
1074 - 'is_chunked' => count($chunks) > 1,
1075 - 'chunk_count' => count($chunks),
1076 - 'content_type' => $content_type,
1077 - );
1078 -}
1079 -
1080 -/**
1081 - * AJAX: Inspect a knowledge entry — returns the per-chunk stored text + metadata
1082 - * WITHOUT collapsing it, so a site owner can see exactly what was indexed for an
1083 - * entry (plan-mxchat-20260628-d8cb4b). READ-ONLY: never re-embeds or mutates.
1084 - */
1085 -public function ajax_mxchat_inspect_entry() {
1086 - check_ajax_referer('mxchat_inspect_entry_nonce', 'nonce');
1087 -
1088 - if ( ! current_user_can('manage_options') ) {
1089 - wp_send_json_error( array( 'message' => esc_html__('Permission denied.', 'mxchat') ) );
1090 - }
1091 -
1092 - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : '';
1093 - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0;
1094 - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress';
1095 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
1096 -
1097 - if ( $data_source === 'pinecone' ) {
1098 - $result = $this->inspect_pinecone_entry( $source_url, $entry_id, $bot_id );
1099 - } else {
1100 - $result = $this->inspect_wordpress_entry( $source_url, $entry_id );
1101 - }
1102 -
1103 - if ( is_wp_error( $result ) ) {
1104 - wp_send_json_error( array( 'message' => $result->get_error_message() ) );
1105 - }
1106 -
1107 - wp_send_json_success( $result );
1108 -}
1109 -
1110 -/**
1111 - * Read-only inspector for WordPress-DB entries. Mirrors get_wordpress_entry_content()
1112 - * but returns each STORED chunk's exact text + length (no implode), plus the assembled
1113 - * embedded text. This shows what is actually in the index, not a re-derivation from the post.
1114 - */
1115 -private function inspect_wordpress_entry( $source_url, $entry_id ) {
1116 - global $wpdb;
1117 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
1118 -
1119 - $rows = array();
1120 -
1121 - // Group by the real stored source_url — this INCLUDES "mxchat://" manual
1122 - // Direct Content entries (the spec's manual-entry case), which share one
1123 - // source_url across their chunk rows. Only the synthetic "_ungrouped_<id>"
1124 - // display key (invented by the table view for rows with no source_url) is
1125 - // excluded; those fall through to the entry_id lookup below.
1126 - if ( ! empty( $source_url ) && strpos( $source_url, '_ungrouped_' ) !== 0 ) {
1127 - $rows = $wpdb->get_results( $wpdb->prepare(
1128 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE source_url = %s ORDER BY id ASC",
1129 - $source_url
1130 - ) );
1131 - }
1132 -
1133 - // Fallback / manual "Direct Content" entries: fetch the single row by id.
1134 - if ( empty( $rows ) && $entry_id > 0 ) {
1135 - $row = $wpdb->get_row( $wpdb->prepare(
1136 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE id = %d",
1137 - $entry_id
1138 - ) );
1139 - if ( $row ) {
1140 - $rows = array( $row );
684 + } catch (\Exception $e) {
685 + //error_log(sprintf('[MXCHAT-PDF] Error processing PDF: %s', $e->getMessage()));
686 +
687 + $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
688 + $status = get_transient($status_key);
689 +
690 + if (!$status || !is_array($status)) {
691 + $status = array(
692 + 'total_pages' => $total_pages,
693 + 'processed_pages' => 0,
694 + 'status' => 'error',
695 + 'error' => sanitize_text_field($e->getMessage()),
696 + 'last_update' => time()
697 + );
698 + } else {
699 + $status['status'] = 'error';
700 + $status['error'] = sanitize_text_field($e->getMessage());
701 + $status['last_update'] = time();
1141 702 }
1142 - }
1143 -
1144 - if ( empty( $rows ) ) {
1145 - return new WP_Error( 'not_found', esc_html__('Entry not found in the local knowledge database.', 'mxchat') );
1146 - }
1147 -
1148 - $chunks = array();
1149 - $content_type = '';
1150 - foreach ( $rows as $row ) {
1151 - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content );
1152 - $text = isset( $parsed['text'] ) ? $parsed['text'] : '';
1153 - $index = isset( $parsed['metadata']['chunk_index'] ) ? intval( $parsed['metadata']['chunk_index'] ) : count( $chunks );
1154 - $content_type = $row->content_type;
1155 - $chunks[] = array(
1156 - 'index' => $index,
1157 - 'text' => $text,
1158 - 'length' => function_exists('mb_strlen') ? mb_strlen( $text ) : strlen( $text ),
1159 - 'row_id' => intval( $row->id ),
1160 - );
1161 - }
1162 -
1163 - usort( $chunks, function( $a, $b ) { return $a['index'] - $b['index']; } );
1164 -
1165 - $assembled = implode( "\n\n", wp_list_pluck( $chunks, 'text' ) );
1166 -
1167 - return array(
1168 - 'store' => 'wordpress',
1169 - 'source_url' => $source_url,
1170 - 'content_type' => $content_type,
1171 - 'is_chunked' => count( $chunks ) > 1,
1172 - 'chunk_count' => count( $chunks ),
1173 - 'assembled' => $assembled,
1174 - 'assembled_length' => function_exists('mb_strlen') ? mb_strlen( $assembled ) : strlen( $assembled ),
1175 - 'chunks' => array_values( $chunks ),
1176 - // WP-DB storage carries no separate vector metadata; surface that fact
1177 - // rather than letting the owner guess (the spec's taxonomy question).
1178 - 'metadata' => array(),
1179 - 'metadata_note' => esc_html__('Stored in the local WordPress database. Only the assembled text shown here is embedded — there are no separate vector metadata fields (e.g. taxonomy terms are not stored unless they were injected into the text itself).', 'mxchat'),
1180 - );
1181 -}
1182 -
1183 -/**
1184 - * Read-only inspector for Pinecone entries. Mirrors get_pinecone_entry_content()
1185 - * but keeps each vector's text + metadata instead of imploding, so the owner can
1186 - * confirm exactly which metadata fields (text/source_url/type/last_updated/created_at/bot_id)
1187 - * are present per chunk. READ-ONLY.
1188 - */
1189 -private function inspect_pinecone_entry( $source_url, $entry_id, $bot_id ) {
1190 - if ( ! class_exists('MxChat_Pinecone_Manager') ) {
1191 - return new WP_Error( 'pinecone_unavailable', esc_html__('Pinecone manager not available.', 'mxchat') );
1192 - }
1193 -
1194 - if ( $bot_id === 'default' || ! class_exists('MxChat_Multi_Bot_Manager') ) {
1195 - $pinecone_options = get_option('mxchat_pinecone_addon_options');
1196 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1197 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1198 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
1199 - } else {
1200 - $bot_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
1201 - $api_key = $bot_config['api_key'] ?? '';
1202 - $host = $bot_config['host'] ?? '';
1203 - $namespace = $bot_config['namespace'] ?? '';
1204 - }
1205 -
1206 - if ( empty($host) || empty($api_key) ) {
1207 - return new WP_Error( 'pinecone_config', esc_html__('Pinecone not configured.', 'mxchat') );
1208 - }
1209 -
1210 - $base_id = md5( $source_url );
1211 - $vector_ids = array( $base_id );
1212 -
1213 - $list_url = "https://{$host}/vectors/list";
1214 - $list_body = array( 'prefix' => $base_id . '_chunk_', 'limit' => 100 );
1215 - if ( ! empty($namespace) ) {
1216 - $list_body['namespace'] = $namespace;
1217 - }
1218 -
1219 - $list_resp = wp_remote_post( $list_url, array(
1220 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
1221 - 'body' => wp_json_encode( $list_body ),
1222 - 'timeout' => 15,
1223 - ) );
1224 -
1225 - if ( ! is_wp_error($list_resp) ) {
1226 - $list_data = json_decode( wp_remote_retrieve_body($list_resp), true );
1227 - if ( ! empty($list_data['vectors']) ) {
1228 - foreach ( $list_data['vectors'] as $v ) {
1229 - $vector_ids[] = $v['id'];
1230 - }
703 +
704 + set_transient($status_key, array_map('sanitize_text_field', $status), DAY_IN_SECONDS);
705 +
706 + if (file_exists($pdf_path)) {
707 + wp_delete_file($pdf_path);
1231 708 }
1232 709 }
1233 -
1234 - $fetch_url = "https://{$host}/vectors/fetch";
1235 - $fetch_body = array( 'ids' => $vector_ids );
1236 - if ( ! empty($namespace) ) {
1237 - $fetch_body['namespace'] = $namespace;
1238 - }
1239 -
1240 - $fetch_resp = wp_remote_post( $fetch_url, array(
1241 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
1242 - 'body' => wp_json_encode( $fetch_body ),
1243 - 'timeout' => 15,
1244 - ) );
1245 -
1246 - if ( is_wp_error($fetch_resp) ) {
1247 - return new WP_Error( 'pinecone_fetch', esc_html__('Failed to fetch from Pinecone.', 'mxchat') );
1248 - }
1249 -
1250 - $fetch_data = json_decode( wp_remote_retrieve_body($fetch_resp), true );
1251 - $vectors = $fetch_data['vectors'] ?? array();
1252 -
1253 - if ( empty($vectors) ) {
1254 - return new WP_Error( 'not_found', esc_html__('Entry not found in Pinecone.', 'mxchat') );
1255 - }
1256 -
1257 - // Whitelisted metadata fields the spec calls out — shown so devs can confirm
1258 - // what is (and is NOT) stored per vector.
1259 - $meta_fields = array( 'text', 'source_url', 'type', 'last_updated', 'created_at', 'bot_id', 'chunk_index', 'total_chunks' );
1260 - $chunks = array();
1261 - $content_type = '';
1262 - foreach ( $vectors as $vid => $vector ) {
1263 - $meta = isset($vector['metadata']) && is_array($vector['metadata']) ? $vector['metadata'] : array();
1264 - $text = $meta['text'] ?? '';
1265 - $index = isset($meta['chunk_index']) ? intval($meta['chunk_index']) : count($chunks);
1266 - $content_type = $meta['type'] ?? $content_type;
1267 -
1268 - $clean_meta = array();
1269 - foreach ( $meta_fields as $field ) {
1270 - if ( array_key_exists( $field, $meta ) && $field !== 'text' ) {
1271 - $clean_meta[ $field ] = is_scalar( $meta[ $field ] ) ? (string) $meta[ $field ] : wp_json_encode( $meta[ $field ] );
1272 - }
1273 - }
1274 -
1275 - $chunks[] = array(
1276 - 'index' => $index,
1277 - 'text' => $text,
1278 - 'length' => function_exists('mb_strlen') ? mb_strlen( $text ) : strlen( $text ),
1279 - 'vector_id' => (string) $vid,
1280 - 'metadata' => $clean_meta,
1281 - );
1282 - }
1283 -
1284 - usort( $chunks, function( $a, $b ) { return $a['index'] - $b['index']; } );
1285 -
1286 - $assembled = implode( "\n\n", wp_list_pluck( $chunks, 'text' ) );
1287 -
1288 - return array(
1289 - 'store' => 'pinecone',
1290 - 'source_url' => $source_url,
1291 - 'content_type' => $content_type,
1292 - 'is_chunked' => count( $chunks ) > 1,
1293 - 'chunk_count' => count( $chunks ),
1294 - 'assembled' => $assembled,
1295 - 'assembled_length' => function_exists('mb_strlen') ? mb_strlen( $assembled ) : strlen( $assembled ),
1296 - 'chunks' => array_values( $chunks ),
1297 - 'metadata' => array(),
1298 - 'metadata_note' => esc_html__('Stored in Pinecone. Each chunk above lists the vector metadata fields actually present — if a field you expect (such as taxonomy terms) is missing here, it was not stored as metadata and is only searchable if it appears in the embedded text.', 'mxchat'),
1299 - );
1300 710 }
1301 711
1302 -/**
1303 - * AJAX: Save edited content — re-chunks and re-embeds as needed.
1304 - * Works for both WordPress DB and Pinecone entries.
1305 - */
1306 -public function ajax_mxchat_save_entry_content() {
1307 - check_ajax_referer('mxchat_edit_entry_nonce', 'nonce');
712 + public function mxchat_save_inline_prompt() {
713 + // Check for nonce security
714 + check_ajax_referer('mxchat_save_inline_nonce');
1308 715
1309 - if ( ! current_user_can('manage_options') ) {
1310 - wp_send_json_error( array( 'message' => 'Permission denied.' ) );
1311 - }
716 + // Verify permissions
717 + if (!current_user_can('manage_options')) {
718 + wp_send_json_error(esc_html__('Permission denied.', 'mxchat'));
719 + return;
720 + }
1312 721
1313 - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : '';
1314 - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0;
1315 - $content = isset($_POST['content']) ? wp_kses_post( wp_unslash($_POST['content']) ) : '';
1316 - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress';
1317 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
1318 - $content_type = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : 'content';
722 + global $wpdb;
723 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1319 724
1320 - if ( empty($content) ) {
1321 - wp_send_json_error( array( 'message' => 'Content cannot be empty.' ) );
1322 - }
725 + // Validate and sanitize input data
726 + $prompt_id = isset($_POST['id']) ? absint($_POST['id']) : 0;
727 + $article_content = isset($_POST['article_content']) ? sanitize_textarea_field($_POST['article_content']) : '';
728 + $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
1323 729
1324 - // Get the embedding API key
1325 - $options = get_option('mxchat_options', array());
1326 - $api_key = '';
730 + if ($prompt_id > 0 && !empty($article_content)) {
731 + // Re-generate the embedding vector for the updated content
732 + $embedding_vector = $this->mxchat_generate_embedding($article_content);
1327 733
1328 - if ( $bot_id !== 'default' && class_exists('MxChat_Multi_Bot_Manager') ) {
1329 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
1330 - $api_key = $bot_options['api_key'] ?? '';
1331 - }
1332 - if ( empty($api_key) ) {
1333 - $api_key = $options['api_key'] ?? '';
1334 - }
734 + if (is_array($embedding_vector)) {
735 + // Serialize the embedding vector before storing it
736 + $embedding_vector_serialized = serialize($embedding_vector);
1335 737
1336 - global $wpdb;
1337 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
738 + // Update the prompt in the database
739 + $updated = $wpdb->update(
740 + $table_name,
741 + array(
742 + 'article_content' => $article_content,
743 + 'embedding_vector' => $embedding_vector_serialized,
744 + 'source_url' => $article_url,
745 + ),
746 + array('id' => $prompt_id),
747 + array('%s', '%s', '%s'),
748 + array('%d')
749 + );
1338 750
1339 - // If source_url is empty but we have an entry_id, look it up
1340 - if ( empty($source_url) && $entry_id > 0 && $data_source === 'wordpress' ) {
1341 - $row = $wpdb->get_row( $wpdb->prepare( "SELECT source_url FROM {$table} WHERE id = %d", $entry_id ) );
1342 - if ( $row && ! empty($row->source_url) ) {
1343 - $source_url = $row->source_url;
1344 - }
1345 - }
751 + if ($updated !== false) {
752 + wp_send_json_success();
753 + } else {
754 + wp_send_json_error(esc_html__('Database update failed.', 'mxchat'));
755 + }
756 + } else {
757 + wp_send_json_error(esc_html__('Embedding generation failed.', 'mxchat'));
758 + }
759 + } else {
760 + wp_send_json_error(esc_html__('Invalid data.', 'mxchat'));
761 + }
762 + }
1346 763
1347 - // For manual entries (no source_url or mxchat:// prefix), delete the old entry by ID first
1348 - // so submit_content_to_db creates a replacement instead of a duplicate
1349 - // Also treat legacy mxchat.ai source URLs as manual — old bug assigned the site URL to manual entries
1350 - $is_legacy_manual = !empty($source_url) && strpos($source_url, 'mxchat.ai') !== false && strpos($source_url, 'mxchat://') !== 0;
1351 - if ( $entry_id > 0 && (empty($source_url) || strpos($source_url, 'mxchat://') === 0 || $is_legacy_manual) ) {
1352 - $wpdb->delete( $table, array( 'id' => $entry_id ), array( '%d' ) );
1353 - // Clear legacy URL so submit_content_to_db generates a unique mxchat:// identifier
1354 - // instead of reusing the shared URL (which would mass-delete other entries with the same URL)
1355 - if ( $is_legacy_manual ) {
1356 - $source_url = '';
1357 - }
1358 - }
1359 764
1360 - // Use the existing submit_content_to_db which handles chunking, Pinecone, and WP DB
1361 - $vector_id = ! empty($source_url) ? md5($source_url) : md5('mxchat_manual_' . $entry_id);
1362 -
1363 - // submit_content_to_db already handles: delete old chunks → re-chunk → re-embed → store
1364 - $result = MxChat_Utils::submit_content_to_db( $content, $source_url, $api_key, $vector_id, $bot_id, $content_type );
1365 -
1366 - if ( is_wp_error($result) ) {
1367 - wp_send_json_error( array( 'message' => $result->get_error_message() ) );
1368 - }
1369 -
1370 - wp_send_json_success( array( 'message' => 'Content saved and re-embedded successfully.' ) );
1371 -}
1372 -
1373 765 public function mxchat_get_pdf_processing_status($pdf_url) {
1374 766 $pdf_url = esc_url_raw($pdf_url);
1375 767 $status = get_transient(sanitize_key('mxchat_pdf_status_' . md5($pdf_url)));
1376 768
@@ -1413,18 +805,24 @@
1413 805 }
1414 806
1415 807
1416 808 public function mxchat_handle_sitemap_submission() {
809 + // Start logging the submission process
810 + //error_log('[MXCHAT-URL] ===== Starting URL submission process =====');
811 +
1417 812 // Check if the form was submitted and verify permissions
1418 813 if (!isset($_POST['submit_sitemap']) || !current_user_can('manage_options')) {
814 + //error_log('[MXCHAT-URL] Error: Unauthorized access or form not submitted properly');
1419 815 wp_die(esc_html__('Unauthorized access', 'mxchat'));
1420 816 }
1421 817
1422 818 // Verify nonce
819 + //error_log('[MXCHAT-URL] Verifying nonce');
1423 820 check_admin_referer('mxchat_submit_sitemap_action', 'mxchat_submit_sitemap_nonce');
1424 821
1425 822 // Validate URL
1426 823 if (!isset($_POST['sitemap_url']) || empty($_POST['sitemap_url'])) {
824 + //error_log('[MXCHAT-URL] Error: Empty or missing URL');
1427 825 set_transient('mxchat_admin_notice_error',
1428 826 esc_html__('Please provide a valid URL.', 'mxchat'),
1429 827 30
1430 828 );
@@ -1432,28 +830,12 @@
1432 830 exit;
1433 831 }
1434 832
1435 833 $submitted_url = esc_url_raw($_POST['sitemap_url']);
834 + //error_log('[MXCHAT-URL] Processing URL: ' . $submitted_url);
1436 835
1437 - // Convert Google Drive sharing URLs to direct download URLs
1438 - if ( strpos($submitted_url, 'drive.google.com') !== false ) {
1439 - $file_id = '';
1440 - if ( preg_match('/[?&]id=([a-zA-Z0-9_-]+)/', $submitted_url, $m) ) {
1441 - $file_id = $m[1];
1442 - } elseif ( preg_match('#/file/d/([a-zA-Z0-9_-]+)#', $submitted_url, $m) ) {
1443 - $file_id = $m[1];
1444 - }
1445 - if ( ! empty($file_id) ) {
1446 - $submitted_url = 'https://drive.google.com/uc?export=download&id=' . $file_id;
1447 - }
1448 - }
1449 -
1450 - // Get bot_id from form submission
1451 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
1452 -
1453 - // Get bot-specific options and validate API key
1454 - $bot_options = $this->get_bot_options($bot_id);
1455 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
836 + // Validate API key first
837 + $options = get_option('mxchat_options');
1456 838 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
1457 839
1458 840 if (strpos($selected_model, 'voyage') === 0) {
1459 841 $api_key = $options['voyage_api_key'] ?? '';
@@ -1470,31 +852,21 @@
1470 852 $error_message = sprintf(
1471 853 esc_html__('%s API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
1472 854 $provider_name
1473 855 );
856 + //error_log('[MXCHAT-URL] Error: ' . $error_message);
1474 857 set_transient('mxchat_admin_notice_error', $error_message, 30);
858 + //error_log('[MXCHAT-URL] Set error transient: ' . $error_message);
1475 859 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1476 860 exit;
1477 861 }
1478 862
1479 - // Fetch URL — send an honest, versioned MXChat crawler UA (not a spoofed
1480 - // browser). Stale browser UAs are exactly what WAFs like SiteGround's
1481 - // ModSecurity flag as scrapers, 403-ing the fetch (including PDFs served
1482 - // from the site's own media library, which route through this same call).
1483 - // See mxchat_ingest_user_agent(). Accept is kept for content negotiation;
1484 - // the browser-only Accept-Language fingerprint is dropped so it stays
1485 - // coherent with a bot identity.
1486 - $response = wp_remote_get($submitted_url, array(
1487 - 'timeout' => 30,
1488 - 'sslverify' => false,
1489 - 'user-agent' => mxchat_ingest_user_agent(),
1490 - 'headers' => array(
1491 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
1492 - ),
1493 - ));
863 + //error_log('[MXCHAT-URL] Fetching URL content');
864 + $response = wp_remote_get($submitted_url, array('timeout' => 30));
1494 865
1495 866 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1496 867 $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response);
868 + //error_log('[MXCHAT-URL] Error fetching URL: ' . $error_message);
1497 869 set_transient('mxchat_admin_notice_error',
1498 870 sprintf(
1499 871 esc_html__('Failed to fetch the URL: %s', 'mxchat'),
1500 872 esc_html($error_message)
@@ -1505,11 +877,13 @@
1505 877 exit;
1506 878 }
1507 879
1508 880 $content_type = wp_remote_retrieve_header($response, 'content-type');
881 + //error_log('[MXCHAT-URL] Content type: ' . $content_type);
1509 882 $body_content = wp_remote_retrieve_body($response);
1510 883
1511 884 if (empty($body_content)) {
885 + //error_log('[MXCHAT-URL] Error: Empty response body');
1512 886 set_transient('mxchat_admin_notice_error',
1513 887 esc_html__('Empty response received from URL.', 'mxchat'),
1514 888 30
1515 889 );
@@ -1515,21 +889,29 @@
1515 889 );
1516 890 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1517 891 exit;
1518 892 }
893 + //error_log('[MXCHAT-URL] Retrieved body content length: ' . strlen($body_content) . ' bytes');
1519 894
1520 895 // Handle PDF URL
1521 896 if ($this->mxchat_is_pdf_url($submitted_url, $response)) {
1522 - $result = $this->mxchat_handle_pdf_for_knowledge_base($submitted_url, $response, $bot_id);
897 + //error_log('[MXCHAT-URL] Detected PDF URL, handling PDF for knowledge base');
898 + $result = $this->mxchat_handle_pdf_for_knowledge_base($submitted_url, $response);
899 + //error_log('[MXCHAT-URL] PDF handling result: ' . $result);
1523 900
1524 - if ($result === 'queued') {
1525 - set_transient('mxchat_admin_notice_success',
1526 - esc_html__('PDF queued for processing. Processing will start automatically.', 'mxchat'),
901 + if ($result === 'scheduled') {
902 + set_transient(
903 + 'mxchat_last_pdf_url',
904 + sanitize_text_field($submitted_url),
905 + DAY_IN_SECONDS
906 + );
907 + set_transient('mxchat_admin_notice_info',
908 + esc_html__('PDF processing has started in the background. You can check the progress in the Knowledge Base section.', 'mxchat'),
1527 909 30
1528 910 );
1529 911 } else {
1530 912 set_transient('mxchat_admin_notice_error',
1531 - esc_html__('Failed to queue PDF processing: ', 'mxchat') . esc_html($result),
913 + esc_html__('Failed to start PDF processing: ', 'mxchat') . esc_html($result),
1532 914 30
1533 915 );
1534 916 }
1535 917
@@ -1538,8 +920,9 @@
1538 920 }
1539 921
1540 922 // Handle Sitemap XML
1541 923 if (strpos($content_type, 'xml') !== false || strpos($body_content, '<urlset') !== false) {
924 + //error_log('[MXCHAT-URL] Detected XML content, processing as sitemap');
1542 925 libxml_use_internal_errors(true);
1543 926 $xml = simplexml_load_string($body_content);
1544 927 $xml_errors = libxml_get_errors();
1545 928 libxml_clear_errors();
@@ -1544,8 +927,15 @@
1544 927 $xml_errors = libxml_get_errors();
1545 928 libxml_clear_errors();
1546 929
1547 930 if ($xml === false || !empty($xml_errors)) {
931 + //error_log('[MXCHAT-URL] Error: Invalid XML format');
932 + if (!empty($xml_errors)) {
933 + foreach ($xml_errors as $error) {
934 + //error_log('[MXCHAT-URL] XML Error: ' . $error->message);
935 + }
936 + }
937 +
1548 938 set_transient('mxchat_admin_notice_error',
1549 939 esc_html__('Invalid sitemap XML. Please provide a valid sitemap.', 'mxchat'),
1550 940 30
1551 941 );
@@ -1552,18 +942,27 @@
1552 942 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1553 943 exit;
1554 944 }
1555 945
1556 - $result = $this->mxchat_handle_sitemap_for_knowledge_base($xml, $submitted_url, $bot_id);
946 + //error_log('[MXCHAT-URL] Valid XML found, handling sitemap for knowledge base');
947 + $result = $this->mxchat_handle_sitemap_for_knowledge_base($xml, $submitted_url);
948 + //error_log('[MXCHAT-URL] Sitemap handling result: ' . $result);
1557 949
1558 - if ($result === 'queued') {
1559 - set_transient('mxchat_admin_notice_success',
1560 - esc_html__('Sitemap queued for processing. Processing will start automatically.', 'mxchat'),
950 + if ($result === 'scheduled') {
951 + set_transient(
952 + 'mxchat_last_sitemap_url',
953 + sanitize_text_field($submitted_url),
954 + DAY_IN_SECONDS
955 + );
956 + set_transient('mxchat_admin_notice_info',
957 + esc_html__('Sitemap processing has started in the background. You can check the progress in the Knowledge Base section.', 'mxchat'),
1561 958 30
1562 959 );
1563 960 } else {
961 + // Return to the admin page without a redirect for better error display
962 + // The error is already stored in the sitemap status transient
1564 963 set_transient('mxchat_admin_notice_error',
1565 - esc_html__('Failed to queue sitemap processing. Please check the status below for details.', 'mxchat'),
964 + esc_html__('Failed to start sitemap processing. Please check the status below for details.', 'mxchat'),
1566 965 30
1567 966 );
1568 967 }
1569 968
@@ -1570,50 +969,127 @@
1570 969 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1571 970 exit;
1572 971 }
1573 972
1574 - // Handle Regular URL (single page)
973 + // Handle Regular URL
974 + //error_log('[MXCHAT-URL] Processing as regular webpage');
1575 975 $page_content = $this->mxchat_extract_main_content($body_content);
976 + //error_log('[MXCHAT-URL] Extracted content length: ' . strlen($page_content) . ' bytes');
977 +
1576 978 $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
979 + //error_log('[MXCHAT-URL] Sanitized content length: ' . strlen($sanitized_content) . ' bytes');
1577 980
1578 - //error_log('[MXCHAT-URL-DEBUG] URL: ' . $submitted_url);
1579 - //error_log('[MXCHAT-URL-DEBUG] Extracted page_content length: ' . strlen($page_content));
1580 - //error_log('[MXCHAT-URL-DEBUG] Sanitized content length: ' . strlen($sanitized_content));
1581 - //error_log('[MXCHAT-URL-DEBUG] Content preview: ' . substr($sanitized_content, 0, 500));
1582 -
1583 981 if (empty($sanitized_content)) {
982 + //error_log('[MXCHAT-URL] Error: No valid content after sanitization');
983 +
984 + // Set both transients - the error notice and the URL status
1584 985 set_transient('mxchat_admin_notice_error',
1585 986 esc_html__('No valid content found on the provided URL.', 'mxchat'),
1586 987 30
1587 988 );
989 +
990 + // Set URL status transient
991 + set_transient('mxchat_single_url_status', [
992 + 'url' => $submitted_url,
993 + 'timestamp' => current_time('mysql'),
994 + 'status' => 'failed',
995 + 'error' => esc_html__('No valid content found on the provided URL.', 'mxchat')
996 + ], DAY_IN_SECONDS);
997 +
1588 998 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1589 999 exit;
1590 1000 }
1591 1001
1592 - // For single URLs, process immediately using submit_content_to_db
1593 - // This handles chunking automatically for large content
1594 - $db_result = MxChat_Utils::submit_content_to_db(
1595 - $sanitized_content,
1596 - $submitted_url,
1597 - $api_key,
1598 - null,
1599 - $bot_id,
1600 - 'url' // content_type
1601 - );
1002 + //error_log('[MXCHAT-URL] Generating embedding for content');
1003 + $embedding_vector = $this->mxchat_generate_embedding($sanitized_content);
1602 1004
1603 - if (is_wp_error($db_result)) {
1604 - $error_message = esc_html__('Failed to store content in database: ', 'mxchat') . esc_html($db_result->get_error_message());
1005 + // Check if embedding_vector is a string (error message)
1006 + if (is_string($embedding_vector)) {
1007 + //error_log('[MXCHAT-URL] Error generating embedding: ' . $embedding_vector);
1008 + $error_message = esc_html__('Failed to generate embedding: ', 'mxchat') . esc_html($embedding_vector);
1009 + //error_log('[MXCHAT-URL] Setting error transient: ' . $error_message);
1010 +
1011 + // Set both transients
1605 1012 set_transient('mxchat_admin_notice_error', $error_message, 30);
1606 - } else {
1013 +
1014 + // Set URL status transient
1015 + set_transient('mxchat_single_url_status', [
1016 + 'url' => $submitted_url,
1017 + 'timestamp' => current_time('mysql'),
1018 + 'status' => 'failed',
1019 + 'error' => $error_message
1020 + ], DAY_IN_SECONDS);
1021 +
1022 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1023 + exit;
1024 + }
1025 +
1026 + if (is_array($embedding_vector)) {
1027 + //error_log('[MXCHAT-URL] Successfully generated embedding with ' . count($embedding_vector) . ' dimensions');
1028 +
1029 + $db_result = MxChat_Utils::submit_content_to_db(
1030 + $sanitized_content,
1031 + $submitted_url,
1032 + $api_key
1033 + );
1034 +
1035 + if (is_wp_error($db_result)) {
1036 + //error_log('[MXCHAT-URL] Error: Failed to store content in database: ' . $db_result->get_error_message());
1037 + $error_message = esc_html__('Failed to store content in database: ', 'mxchat') . esc_html($db_result->get_error_message());
1038 + //error_log('[MXCHAT-URL] Setting error transient: ' . $error_message);
1039 +
1040 + // Set both transients
1041 + set_transient('mxchat_admin_notice_error', $error_message, 30);
1042 +
1043 + // Set URL status transient
1044 + set_transient('mxchat_single_url_status', [
1045 + 'url' => $submitted_url,
1046 + 'timestamp' => current_time('mysql'),
1047 + 'status' => 'failed',
1048 + 'error' => $error_message
1049 + ], DAY_IN_SECONDS);
1050 +
1051 + wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1052 + exit;
1053 + }
1054 +
1055 + //error_log('[MXCHAT-URL] Successfully stored content in database');
1607 1056 $success_message = esc_html__('URL content successfully submitted!', 'mxchat');
1057 + //error_log('[MXCHAT-URL] Setting success transient: ' . $success_message);
1058 +
1059 + // Set both transients
1608 1060 set_transient('mxchat_admin_notice_success', $success_message, 30);
1061 +
1062 + // Set URL status transient with success
1063 + set_transient('mxchat_single_url_status', [
1064 + 'url' => $submitted_url,
1065 + 'timestamp' => current_time('mysql'),
1066 + 'status' => 'complete',
1067 + 'content_length' => strlen($sanitized_content),
1068 + 'embedding_dimensions' => count($embedding_vector)
1069 + ], DAY_IN_SECONDS);
1070 +
1071 + } else {
1072 + //error_log('[MXCHAT-URL] Error: Failed to generate embedding. Unexpected result type: ' . gettype($embedding_vector));
1073 + $error_message = esc_html__('Failed to generate embedding: Unexpected result type. Please check your API key and try again.', 'mxchat');
1074 + //error_log('[MXCHAT-URL] Setting error transient: ' . $error_message);
1075 +
1076 + // Set both transients
1077 + set_transient('mxchat_admin_notice_error', $error_message, 30);
1078 +
1079 + // Set URL status transient
1080 + set_transient('mxchat_single_url_status', [
1081 + 'url' => $submitted_url,
1082 + 'timestamp' => current_time('mysql'),
1083 + 'status' => 'failed',
1084 + 'error' => $error_message
1085 + ], DAY_IN_SECONDS);
1609 1086 }
1610 1087
1088 + //error_log('[MXCHAT-URL] ===== Completed URL submission process =====');
1611 1089 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1612 1090 exit;
1613 1091 }
1614 -
1615 -
1616 1092 public function mxchat_get_single_url_status() {
1617 1093 $status = get_transient('mxchat_single_url_status');
1618 1094 if (!$status) {
1619 1095 return null;
@@ -1625,11 +1101,13 @@
1625 1101 }
1626 1102
1627 1103 return $status;
1628 1104 }
1629 -
1630 -public function mxchat_handle_sitemap_for_knowledge_base($xml, $sitemap_url, $bot_id = 'default') {
1105 +public function mxchat_handle_sitemap_for_knowledge_base($xml, $sitemap_url) {
1106 + // Clear any single URL status when starting sitemap processing
1107 + delete_transient('mxchat_single_url_status');
1631 1108 if (!current_user_can('manage_options')) {
1109 + //error_log(esc_html__('Unauthorized sitemap processing attempt', 'mxchat'));
1632 1110 return false;
1633 1111 }
1634 1112
1635 1113 try {
@@ -1638,96 +1116,331 @@
1638 1116 if (!$xml || !is_object($xml)) {
1639 1117 throw new Exception(__('Invalid XML object provided', 'mxchat'));
1640 1118 }
1641 1119
1642 - // Get bot-specific embedding API for validation
1643 - $bot_options = $this->get_bot_options($bot_id);
1644 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
1120 + // Add embedding validation before processing
1121 + // Test embedding with a small sample text to verify API key is working
1122 + $test_result = $this->mxchat_generate_embedding("This is a test to verify the embedding API key is working.");
1645 1123
1646 - // Test the embedding API before processing
1647 - $test_phrase = "Test embedding generation for MxChat";
1648 - $test_result = $this->mxchat_generate_embedding($test_phrase, $bot_id);
1649 -
1124 + // Check if test_result is a string (error message) rather than an array (valid embedding)
1650 1125 if (is_string($test_result)) {
1126 + //error_log('[MXCHAT-URL] Embedding API validation failed: ' . $test_result);
1127 +
1128 + // Store the error in the status transient so it can be displayed later
1129 + $status_data = array(
1130 + 'total_urls' => 0,
1131 + 'processed_urls' => 0,
1132 + 'status' => 'error',
1133 + 'error' => __('Embedding API validation failed: ', 'mxchat') . $test_result,
1134 + 'last_update' => time()
1135 + );
1136 +
1137 + set_transient(
1138 + sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)),
1139 + array_map('sanitize_text_field', $status_data),
1140 + DAY_IN_SECONDS
1141 + );
1142 +
1651 1143 throw new Exception(__('Embedding API validation failed: ', 'mxchat') . $test_result);
1652 1144 }
1653 1145
1146 + // Make sure it's an array (valid embedding)
1654 1147 if (!is_array($test_result)) {
1148 + //error_log('[MXCHAT-URL] Embedding API returned unexpected result type: ' . gettype($test_result));
1655 1149 throw new Exception(__('Embedding API returned unexpected result type. Please check your configuration.', 'mxchat'));
1656 1150 }
1657 1151
1658 - // Extract URLs from sitemap
1659 - $urls = array();
1152 + $urls = [];
1660 1153 foreach ($xml->url as $url_element) {
1661 1154 $url = esc_url_raw((string)$url_element->loc);
1662 1155 if ($url) {
1663 - $urls[] = array('url' => $url);
1156 + $urls[] = $url;
1664 1157 }
1665 1158 }
1666 1159
1667 - $total_urls = count($urls);
1160 + $total_urls = absint(count($urls));
1668 1161
1669 1162 if ($total_urls < 1) {
1670 1163 throw new Exception(__('No valid URLs found in sitemap', 'mxchat'));
1671 1164 }
1672 1165
1673 - // Create unique queue ID
1674 - $queue_id = 'sitemap_' . md5($sitemap_url . time());
1166 + wp_schedule_single_event(time(), 'mxchat_process_sitemap_urls', array(
1167 + 'urls' => $urls,
1168 + 'sitemap_url' => $sitemap_url,
1169 + 'total_urls' => $total_urls,
1170 + 'batch_size' => absint(10),
1171 + 'batch_pause' => absint(5)
1172 + ));
1675 1173
1676 - // Add URLs to queue
1677 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'url', $urls, $bot_id);
1174 + $status_data = array(
1175 + 'total_urls' => $total_urls,
1176 + 'processed_urls' => 0,
1177 + 'status' => 'processing',
1178 + 'last_update' => time()
1179 + );
1678 1180
1679 - if ($queued_count === 0) {
1680 - throw new Exception(__('Failed to add URLs to processing queue', 'mxchat'));
1681 - }
1181 + set_transient(
1182 + sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)),
1183 + array_map('sanitize_text_field', $status_data),
1184 + DAY_IN_SECONDS
1185 + );
1682 1186
1683 - // Store queue metadata
1684 - $this->mxchat_set_queue_meta($queue_id, 'source_url', $sitemap_url);
1685 - $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'sitemap');
1686 - $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_urls);
1687 - $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
1688 - $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
1187 + return __('scheduled', 'mxchat');
1689 1188
1690 - // Store queue ID in transient for status tracking
1691 - set_transient('mxchat_active_queue_sitemap', $queue_id, DAY_IN_SECONDS);
1692 - set_transient('mxchat_last_sitemap_url', $sitemap_url, DAY_IN_SECONDS);
1693 -
1694 - return 'queued';
1695 -
1696 - } catch (Exception $e) {
1189 + } catch (\Exception $e) {
1697 1190 $error_message = $e->getMessage();
1698 1191 //error_log(sprintf(esc_html__('Error preparing sitemap for processing: %s', 'mxchat'), esc_html($error_message)));
1699 1192
1193 + // Store the sitemap URL and error in transients so they can be displayed
1194 + set_transient(
1195 + 'mxchat_last_sitemap_url',
1196 + sanitize_text_field($sitemap_url),
1197 + DAY_IN_SECONDS
1198 + );
1199 +
1200 + $status_data = array(
1201 + 'total_urls' => 0,
1202 + 'processed_urls' => 0,
1203 + 'status' => 'error',
1204 + 'error' => $error_message,
1205 + 'last_update' => time()
1206 + );
1207 +
1208 + set_transient(
1209 + sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url)),
1210 + array_map('sanitize_text_field', $status_data),
1211 + DAY_IN_SECONDS
1212 + );
1213 +
1700 1214 return $error_message;
1701 1215 }
1702 -
1703 1216 }
1704 1217
1705 -/**
1706 - * Remove shortcode tags but preserve the content inside them
1707 - * Example: [vc_column]Hello World[/vc_column] becomes "Hello World"
1708 - *
1709 - * @param string $content The content containing shortcodes
1710 - * @return string Content with shortcode tags removed but inner content preserved
1711 - */
1712 -private function strip_shortcode_tags_preserve_content($content) {
1713 - // Single-pass regex removes all shortcode brackets: [tag], [tag attr="val"], [/tag], [tag /]
1714 - // Content between tags is inherently preserved since only brackets are targeted
1715 - $result = preg_replace('/\[\/?\w[\w-]*[^\]]*\]/', '', $content);
1716 - return ($result !== null) ? $result : $content;
1218 +public function mxchat_process_sitemap_urls_cron($urls, $sitemap_url, $total_urls, $batch_size, $batch_pause) {
1219 + // Validate inputs
1220 + $sitemap_url = esc_url_raw($sitemap_url);
1221 + $total_urls = absint($total_urls);
1222 + $batch_size = absint($batch_size);
1223 + $batch_pause = absint($batch_pause);
1224 +
1225 + if (!is_array($urls) || empty($urls)) {
1226 + return;
1227 + }
1228 +
1229 + try {
1230 + $status_key = sanitize_key('mxchat_sitemap_status_' . md5($sitemap_url));
1231 + $status = get_transient($status_key);
1232 +
1233 + if (!$status || !is_array($status)) {
1234 + throw new Exception('Invalid status data retrieved from transient');
1235 + }
1236 +
1237 + // Initialize failed_urls array if it doesn't exist
1238 + if (!isset($status['failed_urls_list']) || !is_array($status['failed_urls_list'])) {
1239 + $status['failed_urls_list'] = [];
1240 + }
1241 +
1242 + $start_url = absint($status['processed_urls']);
1243 + $end_url = min($start_url + $batch_size, $total_urls);
1244 +
1245 + // Track batch statistics
1246 + $batch_stats = [
1247 + 'processed' => 0,
1248 + 'failed' => 0,
1249 + 'last_error' => '',
1250 + 'embedding_errors' => 0
1251 + ];
1252 +
1253 + // Check embedding configuration with first URL (only on first batch)
1254 + if ($start_url === 0) {
1255 + $test_url = esc_url_raw($urls[0]);
1256 + $test_response = wp_remote_get($test_url);
1257 +
1258 + if (!is_wp_error($test_response) && wp_remote_retrieve_response_code($test_response) === 200) {
1259 + $test_html = wp_remote_retrieve_body($test_response);
1260 + $test_content = $this->mxchat_extract_main_content($test_html);
1261 + $test_sanitized = $this->mxchat_sanitize_content_for_api($test_content);
1262 +
1263 + if (!empty($test_sanitized)) {
1264 + $test_embedding = $this->mxchat_generate_embedding($test_sanitized);
1265 +
1266 + if (is_string($test_embedding)) {
1267 + throw new Exception('Embedding generation failed: ' . $test_embedding);
1268 + }
1269 +
1270 + if (!is_array($test_embedding)) {
1271 + throw new Exception('Embedding generation returned unexpected result type: ' . gettype($test_embedding));
1272 + }
1273 + }
1274 + }
1275 + }
1276 +
1277 + for ($i = $start_url; $i < $end_url; $i++) {
1278 + $page_url = esc_url_raw($urls[$i]);
1279 + $max_retries = 3;
1280 + $retry_count = 0;
1281 + $url_processed = false;
1282 + $last_error = '';
1283 +
1284 + while (!$url_processed && $retry_count < $max_retries) {
1285 + try {
1286 + // Attempt to fetch the URL
1287 + $page_response = wp_remote_get($page_url, array('timeout' => 30));
1288 +
1289 + if (is_wp_error($page_response)) {
1290 + throw new Exception('HTTP request failed: ' . $page_response->get_error_message());
1291 + }
1292 +
1293 + $response_code = wp_remote_retrieve_response_code($page_response);
1294 + if ($response_code !== 200) {
1295 + throw new Exception('HTTP Status: ' . $response_code);
1296 + }
1297 +
1298 + $page_html = wp_remote_retrieve_body($page_response);
1299 +
1300 + if (empty($page_html)) {
1301 + throw new Exception('Empty response body');
1302 + }
1303 +
1304 + $page_content = $this->mxchat_extract_main_content($page_html);
1305 + $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
1306 +
1307 + if (empty($sanitized_content)) {
1308 + throw new Exception('No valid content found after processing');
1309 + }
1310 +
1311 + $embedding_vector = $this->mxchat_generate_embedding($sanitized_content);
1312 +
1313 + if (is_string($embedding_vector)) {
1314 + throw new Exception('Embedding generation failed: ' . $embedding_vector);
1315 + }
1316 +
1317 + if (!is_array($embedding_vector)) {
1318 + throw new Exception('Embedding generation returned unexpected result type: ' . gettype($embedding_vector));
1319 + }
1320 +
1321 + // Submit to database
1322 + $options = get_option('mxchat_options');
1323 + $submission_result = MxChat_Utils::submit_content_to_db($sanitized_content, $page_url, $options['api_key']);
1324 +
1325 + if (is_wp_error($submission_result)) {
1326 + throw new Exception('Database submission failed: ' . $submission_result->get_error_message());
1327 + }
1328 +
1329 + // Success!
1330 + $url_processed = true;
1331 + $batch_stats['processed']++;
1332 +
1333 + } catch (Exception $e) {
1334 + $retry_count++;
1335 + $last_error = $e->getMessage();
1336 +
1337 + //error_log("URL {$page_url} failed (attempt {$retry_count}/{$max_retries}): " . $last_error);
1338 +
1339 + // Track embedding errors specifically
1340 + if (strpos($last_error, 'Embedding') !== false) {
1341 + $batch_stats['embedding_errors']++;
1342 + }
1343 +
1344 + if ($retry_count < $max_retries) {
1345 + // Wait before retry (exponential backoff: 1s, 2s, 4s)
1346 + sleep(pow(2, $retry_count - 1));
1347 + }
1348 + }
1349 + }
1350 +
1351 + // If URL still not processed after all retries, mark as failed
1352 + if (!$url_processed) {
1353 + $batch_stats['failed']++;
1354 + $batch_stats['last_error'] = $last_error;
1355 +
1356 + // Add to failed URLs list
1357 + $status['failed_urls_list'][] = [
1358 + 'url' => $page_url,
1359 + 'error' => $last_error,
1360 + 'time' => time(),
1361 + 'retries' => $max_retries
1362 + ];
1363 +
1364 + // Limit the number of failed URLs we store to prevent transient size issues
1365 + if (count($status['failed_urls_list']) > 100) {
1366 + $status['failed_urls_list'] = array_slice($status['failed_urls_list'], -100);
1367 + }
1368 + }
1369 +
1370 + // Update progress
1371 + $status['processed_urls'] = absint($i + 1);
1372 + $status['last_update'] = time();
1373 + $status['failed_urls'] = absint($status['failed_urls'] ?? 0) + ($url_processed ? 0 : 1);
1374 + $status['last_error'] = $batch_stats['last_error'];
1375 +
1376 + set_transient($status_key, $status, DAY_IN_SECONDS);
1377 +
1378 + // If we have too many consecutive embedding errors, stop processing
1379 + if ($batch_stats['embedding_errors'] >= 10) {
1380 + throw new Exception('Too many consecutive embedding failures detected. Please check your API configuration.');
1381 + }
1382 + }
1383 +
1384 + // If all URLs in this batch failed, stop processing
1385 + if ($batch_stats['processed'] === 0 && $batch_stats['failed'] > 0) {
1386 + $status['status'] = 'error';
1387 + $status['error'] = sprintf(
1388 + 'Processing stopped: %d consecutive failures in batch. Last error: %s',
1389 + $batch_stats['failed'],
1390 + $batch_stats['last_error']
1391 + );
1392 + set_transient($status_key, $status, DAY_IN_SECONDS);
1393 + return;
1394 + }
1395 +
1396 + // Update final progress
1397 + $status['processed_urls'] = min($end_url, $total_urls);
1398 + $status['last_update'] = time();
1399 + set_transient($status_key, $status, DAY_IN_SECONDS);
1400 +
1401 + // Check if we've processed all URLs
1402 + if ($end_url >= $total_urls) {
1403 + // All URLs have been processed - mark as complete
1404 + $status['status'] = 'complete';
1405 + $status['processed_urls'] = $total_urls;
1406 +
1407 + // Add completion summary
1408 + $status['completion_summary'] = [
1409 + 'total_urls' => $total_urls,
1410 + 'successful_urls' => $total_urls - absint($status['failed_urls'] ?? 0),
1411 + 'failed_urls' => absint($status['failed_urls'] ?? 0),
1412 + 'completion_time' => current_time('mysql')
1413 + ];
1414 +
1415 + // Save the completed status (don't delete it - let user dismiss manually)
1416 + set_transient($status_key, $status, DAY_IN_SECONDS);
1417 +
1418 + // DON'T delete the status transients here - let user dismiss manually
1419 + } else {
1420 + // Schedule next batch
1421 + wp_schedule_single_event(time() + $batch_pause, 'mxchat_process_sitemap_urls', array(
1422 + 'urls' => $urls,
1423 + 'sitemap_url' => $sitemap_url,
1424 + 'total_urls' => $total_urls,
1425 + 'batch_size' => $batch_size,
1426 + 'batch_pause' => $batch_pause,
1427 + ));
1428 + }
1429 + } catch (\Exception $e) {
1430 + $status['status'] = 'error';
1431 + $status['error'] = $e->getMessage();
1432 + set_transient($status_key, $status, DAY_IN_SECONDS);
1433 + }
1717 1434 }
1718 -
1719 1435 public function mxchat_sanitize_content_for_api($content) {
1720 1436 //error_log('[MXCHAT-SANITIZE] Original content preview: ' . substr($content, 0, 500) . '...');
1721 -
1722 - // Remove shortcode tags but PRESERVE content inside them
1723 - $content = $this->strip_shortcode_tags_preserve_content($content);
1724 -
1437 +
1725 1438 // Remove script, style tags, and HTML comments
1726 1439 $content = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $content);
1727 1440 $content = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $content);
1728 1441 $content = preg_replace('/<!--(.|\s)*?-->/', '', $content);
1729 -
1442 +
1730 1443 // Remove all HTML tags and decode HTML entities
1731 1444 $content = wp_strip_all_tags($content);
1732 1445 $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5);
1733 1446
@@ -1785,12 +1498,12 @@
1785 1498 @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
1786 1499 $xpath = new DOMXPath($dom);
1787 1500
1788 1501 // For debugging purposes
1789 - $debugEnabled = true; // Set to true to enable debugging output
1502 + $debugEnabled = false; // Set to true to enable debugging output
1790 1503 $debug = function($message) use ($debugEnabled) {
1791 1504 if ($debugEnabled) {
1792 - //error_log('[MXCHAT-EXTRACT-DEBUG] ' . $message);
1505 + //error_log('[MXCHAT-DEBUG] ' . $message);
1793 1506 }
1794 1507 };
1795 1508
1796 1509 // Direct targeting for Gerow theme posts
@@ -1889,25 +1602,18 @@
1889 1602 '//main',
1890 1603 '//div[contains(@class, "content")]'
1891 1604 ];
1892 1605
1893 - // First handle Elementor content - get only leaf widget containers to avoid duplicates
1606 + // First handle Elementor content
1894 1607 $debug("Checking for Elementor content");
1895 - // Get widget containers that are direct children of widgets (not nested inside other widget containers)
1896 - $elementor_widgets = $xpath->query('//div[contains(@class, "elementor-widget")]//div[contains(@class, "elementor-widget-container")]');
1608 + $elementor_widgets = $xpath->query('//div[contains(@class, "elementor-element")]//div[contains(@class, "elementor-widget-container")]');
1897 1609 if ($elementor_widgets && $elementor_widgets->length > 0) {
1898 1610 $debug("Found Elementor widgets");
1899 - $seen_content = array(); // Track seen content to avoid duplicates
1900 1611 $combined_content = '';
1901 1612 foreach ($elementor_widgets as $widget) {
1902 1613 $widget_content = $dom->saveHTML($widget);
1903 1614 if (!empty($widget_content)) {
1904 - // Create a hash of the content to detect duplicates
1905 - $content_hash = md5($widget_content);
1906 - if (!isset($seen_content[$content_hash])) {
1907 - $seen_content[$content_hash] = true;
1908 - $combined_content .= $widget_content;
1909 - }
1615 + $combined_content .= $widget_content;
1910 1616 }
1911 1617 }
1912 1618 if (!empty($combined_content)) {
1913 1619 $debug("Returning Elementor content");
@@ -1919,14 +1625,15 @@
1919 1625 foreach ($selectors as $selector) {
1920 1626 $debug("Trying selector: " . $selector);
1921 1627 $nodes = $xpath->query($selector);
1922 1628 if ($nodes && $nodes->length > 0) {
1923 - $debug("Found " . $nodes->length . " matches for selector: " . $selector);
1924 - // Only take the FIRST matching node to avoid duplicate content
1925 - // (pages often have nested or multiple containers with same class)
1926 - $content = $dom->saveHTML($nodes->item(0));
1629 + $debug("Found matches for selector: " . $selector);
1630 + $content = '';
1631 + foreach ($nodes as $node) {
1632 + $content .= $dom->saveHTML($node);
1633 + }
1927 1634 if (!empty($content)) {
1928 - $debug("Returning content from selector: " . $selector . " (first match only)");
1635 + $debug("Returning content from selector: " . $selector);
1929 1636 return $content;
1930 1637 }
1931 1638 }
1932 1639 }
@@ -1950,106 +1657,16 @@
1950 1657 $debug("Returning blog-area section content");
1951 1658 return $content;
1952 1659 }
1953 1660 }
1954 -
1955 - // Generic container selectors for non-CMS sites (like .asp pages)
1956 - $debug("Trying generic container selectors");
1957 - $generic_selectors = [
1958 - '//div[@id="main"]',
1959 - '//div[@id="wrapper"]',
1960 - '//div[@id="page"]',
1961 - '//div[@id="site-content"]',
1962 - '//div[contains(@class, "main-content")]',
1963 - '//div[contains(@class, "page-content")]',
1964 - '//div[contains(@class, "site-content")]',
1965 - ];
1966 -
1967 - foreach ($generic_selectors as $selector) {
1968 - $debug("Trying generic selector: " . $selector);
1969 - $nodes = $xpath->query($selector);
1970 - if ($nodes && $nodes->length > 0) {
1971 - $content = $dom->saveHTML($nodes->item(0));
1972 - if (!empty($content)) {
1973 - $debug("Returning content from generic selector: " . $selector);
1974 - return $content;
1975 - }
1976 - }
1977 - }
1978 -
1979 - // Paragraph-based content detection - find regions with substantial text
1980 - $debug("Trying paragraph-based content detection");
1981 - $paragraphs = $xpath->query('//p[string-length(normalize-space()) > 50]');
1982 - if ($paragraphs && $paragraphs->length >= 3) {
1983 - $debug("Found " . $paragraphs->length . " substantial paragraphs");
1984 - // Collect all substantial paragraphs and their content
1985 - $paragraph_content = '';
1986 - foreach ($paragraphs as $p) {
1987 - $paragraph_content .= $dom->saveHTML($p) . "\n";
1988 - }
1989 - if (!empty($paragraph_content)) {
1990 - $debug("Returning paragraph-based content");
1991 - return $paragraph_content;
1992 - }
1993 - }
1994 -
1995 - // Improved body fallback - strip nav/header/footer elements first
1996 - $debug("Using improved body fallback");
1661 +
1662 + // Fallback: Return the body content if no specific selector matches
1663 + $debug("Using body fallback");
1997 1664 $body = $dom->getElementsByTagName('body');
1998 1665 if ($body->length > 0) {
1999 - // Clone the body to avoid modifying the original DOM
2000 - $body_clone = $body->item(0)->cloneNode(true);
2001 -
2002 - // Remove common non-content elements by tag name
2003 - $remove_tags = ['nav', 'header', 'footer', 'aside', 'script', 'style', 'noscript'];
2004 - foreach ($remove_tags as $tag) {
2005 - $elements = $body_clone->getElementsByTagName($tag);
2006 - // Iterate backwards to safely remove elements
2007 - for ($i = $elements->length - 1; $i >= 0; $i--) {
2008 - $el = $elements->item($i);
2009 - if ($el && $el->parentNode) {
2010 - $el->parentNode->removeChild($el);
2011 - }
2012 - }
2013 - }
2014 -
2015 - // Remove elements with common non-content class names using XPath on the cloned body
2016 - $temp_dom = new DOMDocument();
2017 - @$temp_dom->appendChild($temp_dom->importNode($body_clone, true));
2018 - $temp_xpath = new DOMXPath($temp_dom);
2019 -
2020 - $remove_class_patterns = [
2021 - '//*[contains(@class, "nav")]',
2022 - '//*[contains(@class, "menu")]',
2023 - '//*[contains(@class, "sidebar")]',
2024 - '//*[contains(@class, "footer")]',
2025 - '//*[contains(@class, "header")]',
2026 - '//*[contains(@id, "nav")]',
2027 - '//*[contains(@id, "menu")]',
2028 - '//*[contains(@id, "sidebar")]',
2029 - '//*[contains(@id, "footer")]',
2030 - '//*[contains(@id, "header")]',
2031 - ];
2032 -
2033 - foreach ($remove_class_patterns as $pattern) {
2034 - $elements = $temp_xpath->query($pattern);
2035 - if ($elements) {
2036 - for ($i = $elements->length - 1; $i >= 0; $i--) {
2037 - $el = $elements->item($i);
2038 - if ($el && $el->parentNode) {
2039 - $el->parentNode->removeChild($el);
2040 - }
2041 - }
2042 - }
2043 - }
2044 -
2045 - $cleaned_content = $temp_dom->saveHTML();
2046 - if (!empty($cleaned_content)) {
2047 - $debug("Returning cleaned body content");
2048 - return $cleaned_content;
2049 - }
1666 + return $dom->saveHTML($body->item(0));
2050 1667 }
2051 -
1668 +
2052 1669 // Last resort: return the original HTML
2053 1670 $debug("Returning original HTML");
2054 1671 return $html;
2055 1672 } catch (Exception $e) {
@@ -2101,41 +1718,47 @@
2101 1718 try {
2102 1719 // Verify the request
2103 1720 check_ajax_referer('mxchat_status_nonce', 'nonce');
2104 1721
2105 - // Get active queue IDs
2106 - $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
2107 - $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
1722 + // Get the status just like in your admin page
1723 + $pdf_url = get_transient('mxchat_last_pdf_url');
1724 + $sitemap_url = get_transient('mxchat_last_sitemap_url');
2108 1725
2109 - $sitemap_status = false;
2110 - $pdf_status = false;
1726 + $pdf_status = $pdf_url ? $this->mxchat_get_pdf_processing_status($pdf_url) : false;
1727 + $sitemap_status = $sitemap_url ? $this->mxchat_get_sitemap_processing_status($sitemap_url) : false;
2111 1728
2112 - // Get sitemap queue status
2113 - if ($sitemap_queue_id) {
2114 - $sitemap_status = $this->mxchat_get_queue_status_data($sitemap_queue_id, 'sitemap');
1729 + // Add the PDF URL to the status object
1730 + if ($pdf_status && $pdf_url) {
1731 + $pdf_status['pdf_url'] = $pdf_url;
2115 1732 }
2116 1733
2117 - // Get PDF queue status
2118 - if ($pdf_queue_id) {
2119 - $pdf_status = $this->mxchat_get_queue_status_data($pdf_queue_id, 'pdf');
2120 - }
1734 + // Set the current PDF URL for the manual batch processing button
1735 + $current_pdf_url = $pdf_url;
2121 1736
1737 + // Check for true processing status, not just presence of status
2122 1738 $is_active_processing =
2123 - ($sitemap_status && $sitemap_status['status'] === 'processing') ||
2124 - ($pdf_status && $pdf_status['status'] === 'processing');
1739 + ($sitemap_status && isset($sitemap_status['status']) && $sitemap_status['status'] === 'processing') ||
1740 + ($pdf_status && isset($pdf_status['status']) && $pdf_status['status'] === 'processing');
2125 1741
1742 + // Get single URL status, but only if no processing is active
1743 + $single_url_status = !$is_active_processing ? $this->mxchat_get_single_url_status() : false;
1744 +
1745 + // REMOVED: Auto-clearing of completed status - now only done via dismiss button
1746 +
2126 1747 // Return JSON response with the status data
2127 1748 wp_send_json(array(
2128 1749 'pdf_status' => $pdf_status,
2129 1750 'sitemap_status' => $sitemap_status,
1751 + 'single_url_status' => $single_url_status,
2130 1752 'is_processing' => $is_active_processing,
2131 - 'sitemap_queue_id' => $sitemap_queue_id,
2132 - 'pdf_queue_id' => $pdf_queue_id
1753 + 'current_pdf_url' => $current_pdf_url
2133 1754 ));
2134 1755
2135 1756 } catch (Exception $e) {
1757 + // Log the error
2136 1758 //error_log('MxChat Status Update Error: ' . $e->getMessage());
2137 1759
1760 + // Return a friendly error response
2138 1761 wp_send_json_error(array(
2139 1762 'message' => 'Error getting status updates: ' . $e->getMessage(),
2140 1763 'status' => 'error'
2141 1764 ));
@@ -2140,1371 +1763,8 @@
2140 1763 'status' => 'error'
2141 1764 ));
2142 1765 }
2143 1766 }
2144 -
2145 -/**
2146 - * Helper function to get queue status data
2147 - */
2148 -private function mxchat_get_queue_status_data($queue_id, $type = 'sitemap') {
2149 - global $wpdb;
2150 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
2151 -
2152 - // Get counts by status
2153 - $counts = $wpdb->get_results($wpdb->prepare(
2154 - "SELECT status, COUNT(*) as count
2155 - FROM $table_name
2156 - WHERE queue_id = %s
2157 - GROUP BY status",
2158 - $queue_id
2159 - ), OBJECT_K);
2160 -
2161 - $total = 0;
2162 - $completed = 0;
2163 - $failed = 0;
2164 - $processing = 0;
2165 - $pending = 0;
2166 -
2167 - foreach ($counts as $status => $data) {
2168 - $count = absint($data->count);
2169 - $total += $count;
2170 -
2171 - switch ($status) {
2172 - case 'completed':
2173 - $completed = $count;
2174 - break;
2175 - case 'failed':
2176 - $failed = $count;
2177 - break;
2178 - case 'processing':
2179 - $processing = $count;
2180 - break;
2181 - case 'pending':
2182 - $pending = $count;
2183 - break;
2184 - }
2185 - }
2186 -
2187 - if ($total === 0) {
2188 - return false;
2189 - }
2190 -
2191 - // Calculate percentage
2192 - $percentage = round((($completed + $failed) / $total) * 100);
2193 -
2194 - // Get failed items details (limit to 50)
2195 - $failed_items = array();
2196 - if ($failed > 0) {
2197 - $failed_results = $wpdb->get_results($wpdb->prepare(
2198 - "SELECT item_type, item_data, error_message, attempts, completed_at
2199 - FROM $table_name
2200 - WHERE queue_id = %s
2201 - AND status = 'failed'
2202 - AND attempts >= max_attempts
2203 - ORDER BY id DESC
2204 - LIMIT 50",
2205 - $queue_id
2206 - ));
2207 -
2208 - foreach ($failed_results as $item) {
2209 - $data = json_decode($item->item_data, true);
2210 - $url = $type === 'pdf' ? ($data['pdf_url'] ?? '') : ($data['url'] ?? '');
2211 -
2212 - $failed_items[] = array(
2213 - 'url' => $url,
2214 - 'page' => $type === 'pdf' ? ($data['page_number'] ?? 0) : 0,
2215 - 'error' => $item->error_message,
2216 - 'retries' => $item->attempts,
2217 - 'time' => strtotime($item->completed_at)
2218 - );
2219 - }
2220 - }
2221 -
2222 - // Get queue metadata
2223 - $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
2224 -
2225 - // Determine if queue is complete
2226 - $is_complete = ($pending === 0 && $processing === 0);
2227 -
2228 - // Get last update time
2229 - $last_update = $wpdb->get_var($wpdb->prepare(
2230 - "SELECT MAX(UNIX_TIMESTAMP(COALESCE(completed_at, started_at, created_at)))
2231 - FROM $table_name
2232 - WHERE queue_id = %s",
2233 - $queue_id
2234 - ));
2235 -
2236 - $last_update_text = $last_update ? human_time_diff($last_update, time()) . ' ' . __('ago', 'mxchat') : __('Just now', 'mxchat');
2237 -
2238 - // Format based on type
2239 - if ($type === 'pdf') {
2240 - return array(
2241 - 'total_pages' => $total,
2242 - 'processed_pages' => $completed + $failed,
2243 - 'failed_pages' => $failed,
2244 - 'percentage' => $percentage,
2245 - 'status' => $is_complete ? 'complete' : 'processing',
2246 - 'last_update' => $last_update_text,
2247 - 'failed_pages_list' => $failed_items,
2248 - 'pdf_url' => $source_url,
2249 - 'queue_id' => $queue_id
2250 - );
2251 - } else {
2252 - return array(
2253 - 'total_urls' => $total,
2254 - 'processed_urls' => $completed + $failed,
2255 - 'failed_urls' => $failed,
2256 - 'percentage' => $percentage,
2257 - 'status' => $is_complete ? 'complete' : 'processing',
2258 - 'last_update' => $last_update_text,
2259 - 'failed_urls_list' => $failed_items,
2260 - 'sitemap_url' => $source_url,
2261 - 'queue_id' => $queue_id
2262 - );
2263 - }
2264 -}
2265 -
2266 -/**
2267 - * Public method to get processing status for both sitemap and PDF queues
2268 - * Used by admin pages to display processing status
2269 - *
2270 - * @return array Array with 'sitemap_status', 'pdf_status', and 'is_processing' keys
2271 - */
2272 -public function mxchat_get_processing_statuses() {
2273 - $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
2274 - $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
2275 -
2276 - $sitemap_status = false;
2277 - $pdf_status = false;
2278 -
2279 - if ($sitemap_queue_id) {
2280 - $sitemap_status = $this->mxchat_get_queue_status_data($sitemap_queue_id, 'sitemap');
2281 - }
2282 -
2283 - if ($pdf_queue_id) {
2284 - $pdf_status = $this->mxchat_get_queue_status_data($pdf_queue_id, 'pdf');
2285 - }
2286 -
2287 - $is_processing =
2288 - ($sitemap_status && $sitemap_status['status'] === 'processing') ||
2289 - ($pdf_status && $pdf_status['status'] === 'processing');
2290 -
2291 - return array(
2292 - 'sitemap_status' => $sitemap_status,
2293 - 'pdf_status' => $pdf_status,
2294 - 'is_processing' => $is_processing
2295 - );
2296 -}
2297 -
2298 -/**
2299 - * AJAX handler to get recent knowledge entries for real-time table updates
2300 - * UPDATED: Now supports both WordPress DB and Pinecone data sources
2301 - */
2302 -public function ajax_mxchat_get_recent_entries() {
2303 - check_ajax_referer('mxchat_entries_nonce', 'nonce');
2304 -
2305 - if (!current_user_can('manage_options')) {
2306 - wp_send_json_error(array('message' => 'Unauthorized'));
2307 - return;
2308 - }
2309 -
2310 - global $wpdb;
2311 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2312 -
2313 - // Get parameters
2314 - $last_id = isset($_POST['last_id']) ? absint($_POST['last_id']) : 0;
2315 - $limit = isset($_POST['limit']) ? min(absint($_POST['limit']), 50) : 10;
2316 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
2317 -
2318 - // Check if Pinecone is enabled for this bot
2319 - $pinecone_manager = $this->mxchat_get_pinecone_manager();
2320 - $pinecone_options = $pinecone_manager ? $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id) : array();
2321 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2322 - $has_pinecone_api = !empty($pinecone_options['mxchat_pinecone_api_key']);
2323 -
2324 - if ($use_pinecone && $has_pinecone_api) {
2325 - // PINECONE DATA SOURCE - Get grouped entry count (not raw vector count)
2326 - // Use mxchat_fetch_pinecone_records which returns total_unique_entries
2327 - $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, '', 1, 10, $bot_id, '');
2328 - $total_count = $records['total'] ?? 0;
2329 -
2330 - // For Pinecone, we don't return individual entries during polling
2331 - // (entries are already displayed on page load via mxchat_fetch_pinecone_records)
2332 - // We just return the updated count
2333 - wp_send_json_success(array(
2334 - 'entries' => array(),
2335 - 'total_count' => absint($total_count),
2336 - 'max_id' => $last_id,
2337 - 'data_source' => 'pinecone'
2338 - ));
2339 - return;
2340 - }
2341 -
2342 - // WORDPRESS DB DATA SOURCE
2343 - // Build query to get entries newer than last_id
2344 - $where_clauses = array('1=1');
2345 - $where_values = array();
2346 -
2347 - if ($last_id > 0) {
2348 - $where_clauses[] = 'id > %d';
2349 - $where_values[] = $last_id;
2350 - }
2351 -
2352 - // Note: WordPress DB table doesn't have bot_id column
2353 - // Multi-bot filtering is handled via Pinecone namespaces
2354 -
2355 - $where_sql = implode(' AND ', $where_clauses);
2356 -
2357 - // Get recent entries
2358 - $query = "SELECT id, article_content, source_url, timestamp
2359 - FROM $table_name
2360 - WHERE $where_sql
2361 - ORDER BY id DESC
2362 - LIMIT %d";
2363 -
2364 - $where_values[] = $limit;
2365 -
2366 - $entries = $wpdb->get_results($wpdb->prepare($query, $where_values));
2367 -
2368 - // Get total count of GROUPED entries (by source_url) - matches pagination display
2369 - // Count unique source_urls (excluding mxchat:// internal refs) + count of ungrouped rows
2370 - $total_count = $wpdb->get_var(
2371 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2372 - (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')"
2373 - );
2374 -
2375 - // Format entries for response
2376 - $formatted_entries = array();
2377 - $preview_length = 150;
2378 - foreach ($entries as $entry) {
2379 - // Parse chunk metadata using the proper chunker method (same as initial page load)
2380 - if (class_exists('MxChat_Chunker')) {
2381 - $chunk_meta = MxChat_Chunker::parse_stored_chunk($entry->article_content);
2382 - $display_content = $chunk_meta['text'];
2383 - $chunk_metadata = $chunk_meta['metadata'];
2384 - } else {
2385 - $display_content = $entry->article_content;
2386 - $chunk_metadata = array();
2387 - }
2388 -
2389 - $content_preview = mb_strlen($display_content) > $preview_length
2390 - ? mb_substr($display_content, 0, $preview_length) . '...'
2391 - : $display_content;
2392 -
2393 - $formatted_entries[] = array(
2394 - 'id' => $entry->id,
2395 - 'preview' => esc_html($content_preview),
2396 - 'full_content' => wp_kses_post(wpautop($display_content)),
2397 - 'content_length' => mb_strlen($display_content),
2398 - 'preview_length' => $preview_length,
2399 - 'source_url' => $entry->source_url,
2400 - 'has_link' => !empty($entry->source_url) && strpos($entry->source_url, 'mxchat://') !== 0,
2401 - 'chunk_metadata' => $chunk_metadata,
2402 - 'bot_id' => $entry->bot_id ?? 'default',
2403 - 'edit_nonce' => wp_create_nonce('mxchat_edit_entry_nonce'),
2404 - 'delete_nonce' => wp_create_nonce('mxchat_delete_prompt_nonce')
2405 - );
2406 - }
2407 -
2408 - wp_send_json_success(array(
2409 - 'entries' => $formatted_entries,
2410 - 'total_count' => absint($total_count),
2411 - 'max_id' => !empty($entries) ? $entries[0]->id : $last_id,
2412 - 'data_source' => 'wordpress'
2413 - ));
2414 -}
2415 -
2416 -/**
2417 - * Get Pinecone total count from stats API
2418 - * Helper function for ajax_mxchat_get_recent_entries
2419 - */
2420 -private function mxchat_get_pinecone_count_from_stats($pinecone_options) {
2421 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2422 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2423 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
2424 -
2425 - if (empty($api_key) || empty($host)) {
2426 - return 0;
2427 - }
2428 -
2429 - try {
2430 - $stats_url = "https://{$host}/describe_index_stats";
2431 -
2432 - $response = wp_remote_post($stats_url, array(
2433 - 'headers' => array(
2434 - 'Api-Key' => $api_key,
2435 - 'Content-Type' => 'application/json'
2436 - ),
2437 - 'body' => '{}',
2438 - 'timeout' => 10
2439 - ));
2440 -
2441 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2442 - $body = wp_remote_retrieve_body($response);
2443 - $stats_data = json_decode($body, true);
2444 -
2445 - // If namespace is specified, get count from that specific namespace
2446 - if (!empty($namespace) && isset($stats_data['namespaces'][$namespace]['vectorCount'])) {
2447 - return intval($stats_data['namespaces'][$namespace]['vectorCount']);
2448 - }
2449 -
2450 - // If no namespace specified or namespace not found in response, use total
2451 - return intval($stats_data['totalVectorCount'] ?? 0);
2452 - }
2453 -
2454 - return 0;
2455 -
2456 - } catch (Exception $e) {
2457 - return 0;
2458 - }
2459 -}
2460 -
2461 -/**
2462 - * AJAX handler to refresh Pinecone entries table via AJAX
2463 - * Returns the table HTML for updating the UI without a full page reload
2464 - */
2465 -public function ajax_mxchat_refresh_pinecone_entries() {
2466 - check_ajax_referer('mxchat_entries_nonce', 'nonce');
2467 -
2468 - if (!current_user_can('manage_options')) {
2469 - wp_send_json_error(array('message' => 'Unauthorized'));
2470 - return;
2471 - }
2472 -
2473 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
2474 - $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
2475 - $per_page = 25;
2476 - $search_query = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
2477 - $content_type_filter = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : '';
2478 -
2479 - // Get Pinecone manager and options
2480 - $pinecone_manager = $this->mxchat_get_pinecone_manager();
2481 - if (!$pinecone_manager) {
2482 - wp_send_json_error(array('message' => 'Pinecone manager not available'));
2483 - return;
2484 - }
2485 -
2486 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
2487 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2488 - $pinecone_api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2489 -
2490 - if (!$use_pinecone || empty($pinecone_api_key)) {
2491 - wp_send_json_error(array('message' => 'Pinecone not configured'));
2492 - return;
2493 - }
2494 -
2495 - // Fetch records from Pinecone
2496 - $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, $search_query, $page, $per_page, $bot_id, $content_type_filter);
2497 - $prompts = $records['data'] ?? array();
2498 - $total_records = $records['total'] ?? 0;
2499 -
2500 - // Preprocess Pinecone records — set chunk_metadata and display_content
2501 - // (matches admin-knowledge-page.php preprocessing)
2502 - foreach ($prompts as $prompt) {
2503 - if (isset($prompt->chunk_index) && $prompt->chunk_index !== null) {
2504 - $prompt->chunk_metadata = array(
2505 - 'chunk_index' => intval($prompt->chunk_index),
2506 - 'total_chunks' => isset($prompt->total_chunks) ? intval($prompt->total_chunks) : null,
2507 - 'is_chunked' => isset($prompt->is_chunked) ? (bool) $prompt->is_chunked : true,
2508 - 'source_url' => $prompt->source_url ?? ''
2509 - );
2510 - $prompt->display_content = $prompt->article_content;
2511 - } else {
2512 - $prompt->chunk_metadata = array();
2513 - $prompt->display_content = $prompt->article_content ?? '';
2514 - }
2515 - }
2516 -
2517 - // Group prompts by source_url
2518 - $grouped_prompts = array();
2519 - foreach ($prompts as $prompt) {
2520 - $source_url = '';
2521 - if (!empty($prompt->chunk_metadata['source_url'])) {
2522 - $source_url = $prompt->chunk_metadata['source_url'];
2523 - } elseif (!empty($prompt->source_url)) {
2524 - $source_url = $prompt->source_url;
2525 - }
2526 -
2527 - if (!empty($source_url)) {
2528 - if (!isset($grouped_prompts[$source_url])) {
2529 - $grouped_prompts[$source_url] = array();
2530 - }
2531 - $grouped_prompts[$source_url][] = $prompt;
2532 - } else {
2533 - $grouped_prompts['_ungrouped_' . $prompt->id] = array($prompt);
2534 - }
2535 - }
2536 -
2537 - // Sort each group by chunk_index
2538 - foreach ($grouped_prompts as $source_url => &$group) {
2539 - usort($group, function($a, $b) {
2540 - $index_a = isset($a->chunk_metadata['chunk_index']) ? intval($a->chunk_metadata['chunk_index']) : 0;
2541 - $index_b = isset($b->chunk_metadata['chunk_index']) ? intval($b->chunk_metadata['chunk_index']) : 0;
2542 - return $index_a - $index_b;
2543 - });
2544 - }
2545 - unset($group);
2546 -
2547 - // Build HTML for the table rows - must match admin-knowledge-page.php structure exactly
2548 - ob_start();
2549 - $display_index = 0;
2550 - $current_page = $page;
2551 - $data_source = 'pinecone';
2552 - $current_bot_id = $bot_id;
2553 - $preview_length = 150;
2554 -
2555 - if (empty($grouped_prompts)) {
2556 - echo '<tr><td colspan="4" style="padding: 40px; text-align: center; color: var(--mxch-text-muted);">';
2557 - esc_html_e('No knowledge entries found in Pinecone.', 'mxchat');
2558 - echo '</td></tr>';
2559 - } else {
2560 - foreach ($grouped_prompts as $source_url => $group) {
2561 - $chunk_count = count($group);
2562 - $first_prompt = $group[0];
2563 - $display_index++;
2564 -
2565 - if ($chunk_count > 1) {
2566 - // Multiple chunks - show grouped row with expand button
2567 - $group_id = 'group-' . md5($source_url);
2568 - ?>
2569 - <tr id="prompt-<?php echo esc_attr($first_prompt->id); ?>"
2570 - class="mxchat-chunk-group-header"
2571 - data-source="<?php echo esc_attr($data_source); ?>"
2572 - data-group-id="<?php echo esc_attr($group_id); ?>"
2573 - style="border-bottom: 1px solid var(--mxch-card-border); background: rgba(33, 150, 243, 0.02);">
2574 - <td style="padding: 12px 16px; text-align: center;">
2575 - <input type="checkbox"
2576 - class="mxchat-entry-checkbox"
2577 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2578 - data-source="<?php echo esc_attr($data_source); ?>"
2579 - data-source-url="<?php echo esc_attr($source_url); ?>"
2580 - data-is-group="true"
2581 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>">
2582 - </td>
2583 - <td style="padding: 12px 16px; font-size: 13px;">
2584 - <?php echo esc_html($display_index + (($current_page - 1) * $per_page)); ?>
2585 - </td>
2586 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2587 - <div class="mxchat-chunk-group-info">
2588 - <button type="button" class="mxchat-chunk-toggle" data-group-id="<?php echo esc_attr($group_id); ?>">
2589 - <span class="dashicons dashicons-arrow-right-alt2"></span>
2590 - </button>
2591 - <span class="mxchat-chunk-badge"><?php echo esc_html($chunk_count); ?> <?php esc_html_e('chunks', 'mxchat'); ?></span>
2592 - <span class="mxchat-chunk-preview">
2593 - <?php
2594 - $parent_content = isset($first_prompt->display_content) ? $first_prompt->display_content : (isset($first_prompt->article_content) ? $first_prompt->article_content : '');
2595 - $content_preview = mb_substr($parent_content, 0, 100);
2596 - echo esc_html($content_preview . '...');
2597 - ?>
2598 - </span>
2599 - </div>
2600 - </td>
2601 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2602 - <?php if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0 && strpos($source_url, '_ungrouped_') !== 0) : ?>
2603 - <a href="<?php echo esc_url($source_url); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2604 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2605 - <?php esc_html_e('View Source', 'mxchat'); ?>
2606 - </a>
2607 - <?php else : ?>
2608 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
2609 - <?php endif; ?>
2610 - </td>
2611 - <td class="mxchat-actions-cell" style="padding: 12px 16px; white-space: nowrap;">
2612 - <?php if ($data_source !== 'pinecone') : ?>
2613 - <button type="button"
2614 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2615 - data-source-url="<?php echo esc_attr($source_url); ?>"
2616 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2617 - data-data-source="<?php echo esc_attr($data_source); ?>"
2618 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2619 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2620 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2621 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2622 - </button>
2623 - <?php endif; ?>
2624 - <button type="button"
2625 - class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
2626 - data-source-url="<?php echo esc_attr($source_url); ?>"
2627 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
2628 - data-data-source="<?php echo esc_attr($data_source); ?>"
2629 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2630 - data-nonce="<?php echo wp_create_nonce('mxchat_delete_chunks_nonce'); ?>"
2631 - style="color: var(--mxch-error);"
2632 - title="<?php esc_attr_e('Delete all chunks', 'mxchat'); ?>">
2633 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2634 - </button>
2635 - </td>
2636 - </tr>
2637 - <?php
2638 - // Render hidden chunk rows
2639 - foreach ($group as $chunk_index => $chunk) {
2640 - $meta_chunk_index = isset($chunk->chunk_metadata['chunk_index']) ? intval($chunk->chunk_metadata['chunk_index']) : $chunk_index;
2641 - $meta_total_chunks = isset($chunk->chunk_metadata['total_chunks']) ? intval($chunk->chunk_metadata['total_chunks']) : $chunk_count;
2642 - $content = isset($chunk->display_content) ? $chunk->display_content : (isset($chunk->article_content) ? $chunk->article_content : '');
2643 - $content_preview = mb_strlen($content) > $preview_length
2644 - ? mb_substr($content, 0, $preview_length) . '...'
2645 - : $content;
2646 - ?>
2647 - <tr id="prompt-<?php echo esc_attr($chunk->id); ?>"
2648 - class="mxchat-chunk-row <?php echo esc_attr($group_id); ?>"
2649 - data-source="<?php echo esc_attr($data_source); ?>"
2650 - style="display: none; background: #f8f9fa; border-bottom: 1px solid var(--mxch-card-border);">
2651 - <td style="padding: 12px 16px; text-align: center;">
2652 - <!-- Checkbox column placeholder for chunks (managed by group) -->
2653 - </td>
2654 - <td style="padding: 12px 16px 12px 30px; font-size: 13px;">
2655 - <!-- Hidden ID column for chunks -->
2656 - </td>
2657 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2658 - <div class="mxchat-accordion-wrapper">
2659 - <div class="mxchat-content-preview">
2660 - <span class="mxchat-chunk-indicator" style="margin-right: 10px; color: var(--mxch-text-secondary); font-size: 12px;">
2661 - <?php printf(esc_html__('Chunk %d of %d', 'mxchat'), $meta_chunk_index + 1, $meta_total_chunks); ?>
2662 - </span>
2663 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2664 - <?php if (mb_strlen($content) > $preview_length) : ?>
2665 - <button class="mxchat-expand-toggle" type="button">
2666 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2667 - </button>
2668 - <?php endif; ?>
2669 - </div>
2670 - <div class="mxchat-content-full" style="display: none;">
2671 - <div class="content-view">
2672 - <?php
2673 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2674 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2675 - echo wp_kses_post(wpautop($content));
2676 - echo '</div>';
2677 - } else {
2678 - echo wp_kses_post(wpautop($content));
2679 - }
2680 - ?>
2681 - </div>
2682 - </div>
2683 - </div>
2684 - </td>
2685 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2686 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Same as parent', 'mxchat'); ?></span>
2687 - </td>
2688 - <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2689 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Managed by group', 'mxchat'); ?></span>
2690 - </td>
2691 - </tr>
2692 - <?php
2693 - }
2694 - } else {
2695 - // Single entry - display normally with accordion
2696 - $prompt = $first_prompt;
2697 - $content = isset($prompt->display_content) ? $prompt->display_content : (isset($prompt->article_content) ? $prompt->article_content : '');
2698 - $content_preview = mb_strlen($content) > $preview_length
2699 - ? mb_substr($content, 0, $preview_length) . '...'
2700 - : $content;
2701 - ?>
2702 - <tr id="prompt-<?php echo esc_attr($prompt->id); ?>"
2703 - data-source="<?php echo esc_attr($data_source); ?>"
2704 - style="border-bottom: 1px solid var(--mxch-card-border); background: rgba(33, 150, 243, 0.02);">
2705 - <td style="padding: 12px 16px; text-align: center;">
2706 - <input type="checkbox"
2707 - class="mxchat-entry-checkbox"
2708 - data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2709 - data-source="<?php echo esc_attr($data_source); ?>"
2710 - data-source-url="<?php echo esc_attr($prompt->source_url ?? ''); ?>"
2711 - data-is-group="false"
2712 - data-chunk-count="1">
2713 - </td>
2714 - <td style="padding: 12px 16px; font-size: 13px;">
2715 - <?php echo esc_html($display_index + (($current_page - 1) * $per_page)); ?>
2716 - </td>
2717 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2718 - <div class="mxchat-accordion-wrapper">
2719 - <div class="mxchat-content-preview">
2720 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2721 - <?php if (mb_strlen($content) > $preview_length) : ?>
2722 - <button class="mxchat-expand-toggle" type="button">
2723 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2724 - </button>
2725 - <?php endif; ?>
2726 - </div>
2727 - <div class="mxchat-content-full" style="display: none;">
2728 - <div class="content-view">
2729 - <?php
2730 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2731 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2732 - echo wp_kses_post(wpautop($content));
2733 - echo '</div>';
2734 - } else {
2735 - echo wp_kses_post(wpautop($content));
2736 - }
2737 - ?>
2738 - </div>
2739 - </div>
2740 - </div>
2741 - </td>
2742 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2743 - <?php
2744 - $actual_source = $source_url;
2745 - if (strpos($source_url, '_ungrouped_') === 0) {
2746 - $actual_source = $prompt->source_url ?? '';
2747 - }
2748 - if (!empty($actual_source) && strpos($actual_source, 'mxchat://') !== 0) : ?>
2749 - <a href="<?php echo esc_url($actual_source); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2750 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2751 - <?php esc_html_e('View', 'mxchat'); ?>
2752 - </a>
2753 - <?php else : ?>
2754 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
2755 - <?php endif; ?>
2756 - </td>
2757 - <td style="padding: 12px 16px;">
2758 - <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);">
2759 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2760 - </button>
2761 - </td>
2762 - </tr>
2763 - <?php
2764 - }
2765 - }
2766 - }
2767 - $html = ob_get_clean();
2768 -
2769 - // Generate pagination HTML for Pinecone
2770 - $total_pages = ceil($total_records / $per_page);
2771 - $pagination_html = '';
2772 - if ($total_pages > 1) {
2773 - $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) . '">';
2774 -
2775 - // Previous button
2776 - if ($page > 1) {
2777 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page - 1) . '">' . esc_html__('&laquo; Previous', 'mxchat') . '</a> ';
2778 - }
2779 -
2780 - // Page numbers
2781 - $start_page = max(1, $page - 2);
2782 - $end_page = min($total_pages, $page + 2);
2783 -
2784 - if ($start_page > 1) {
2785 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="1">1</a> ';
2786 - if ($start_page > 2) {
2787 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2788 - }
2789 - }
2790 -
2791 - for ($i = $start_page; $i <= $end_page; $i++) {
2792 - if ($i == $page) {
2793 - $pagination_html .= '<span class="mxchat-page-current">' . $i . '</span> ';
2794 - } else {
2795 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $i . '">' . $i . '</a> ';
2796 - }
2797 - }
2798 -
2799 - if ($end_page < $total_pages) {
2800 - if ($end_page < $total_pages - 1) {
2801 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2802 - }
2803 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $total_pages . '">' . $total_pages . '</a> ';
2804 - }
2805 -
2806 - // Next button
2807 - if ($page < $total_pages) {
2808 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page + 1) . '">' . esc_html__('Next &raquo;', 'mxchat') . '</a>';
2809 - }
2810 -
2811 - $pagination_html .= '</div>';
2812 - }
2813 -
2814 - wp_send_json_success(array(
2815 - 'html' => $html,
2816 - 'pagination_html' => $pagination_html,
2817 - 'total_count' => $total_records,
2818 - 'total_pages' => $total_pages,
2819 - 'page' => $page,
2820 - 'per_page' => $per_page,
2821 - 'data_source' => 'pinecone'
2822 - ));
2823 -}
2824 -
2825 -/**
2826 - * AJAX handler for pagination - handles both WordPress DB and Pinecone data sources
2827 - * Returns paginated entries without requiring a full page reload
2828 - */
2829 -public function ajax_mxchat_paginate_entries() {
2830 - check_ajax_referer('mxchat_entries_nonce', 'nonce');
2831 -
2832 - if (!current_user_can('manage_options')) {
2833 - wp_send_json_error(array('message' => 'Unauthorized'));
2834 - return;
2835 - }
2836 -
2837 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
2838 - $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
2839 - $search_query = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
2840 - $content_type_filter = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : '';
2841 - $per_page = 25;
2842 -
2843 - // Check if Pinecone is enabled for this bot
2844 - $pinecone_manager = $this->mxchat_get_pinecone_manager();
2845 - $pinecone_options = $pinecone_manager ? $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id) : array();
2846 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2847 - $has_pinecone_api = !empty($pinecone_options['mxchat_pinecone_api_key']);
2848 -
2849 - if ($use_pinecone && $has_pinecone_api) {
2850 - // Delegate to Pinecone pagination handler (pass search params)
2851 - $_POST['page'] = $page;
2852 - $_POST['search'] = $search_query;
2853 - $_POST['content_type'] = $content_type_filter;
2854 - $this->ajax_mxchat_refresh_pinecone_entries();
2855 - return;
2856 - }
2857 -
2858 - // WordPress DB pagination - MUST match initial page load logic exactly
2859 - global $wpdb;
2860 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2861 - $offset = ($page - 1) * $per_page;
2862 -
2863 - // Build WHERE clause for search and content type filtering
2864 - $where_clauses = array();
2865 - $where_values = array();
2866 -
2867 - if ($search_query) {
2868 - $where_clauses[] = "article_content LIKE %s";
2869 - $where_values[] = '%' . $wpdb->esc_like($search_query) . '%';
2870 - }
2871 -
2872 - if ($content_type_filter) {
2873 - switch ($content_type_filter) {
2874 - case 'manual':
2875 - $where_clauses[] = "(source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')";
2876 - break;
2877 - case 'pdf':
2878 - $where_clauses[] = "source_url LIKE '%.pdf'";
2879 - break;
2880 - case 'url':
2881 - $where_clauses[] = "source_url != '' AND source_url IS NOT NULL AND source_url NOT LIKE 'mxchat://%' AND source_url NOT LIKE '%.pdf'";
2882 - break;
2883 - }
2884 - }
2885 -
2886 - $where_sql = !empty($where_clauses) ? 'WHERE ' . implode(' AND ', $where_clauses) : '';
2887 -
2888 - // Count grouped entries with filters applied
2889 - if (!empty($where_values)) {
2890 - $count_args = array_merge($where_values, $where_values);
2891 - $count_query = $wpdb->prepare(
2892 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$where_sql} AND source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2893 - (SELECT COUNT(*) FROM {$table_name} {$where_sql} AND (source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%'))",
2894 - ...$count_args
2895 - );
2896 - $total_records = $wpdb->get_var($count_query);
2897 - } else if (!empty($where_sql)) {
2898 - // Content type filter only (no search), no prepared values needed
2899 - $total_records = $wpdb->get_var(
2900 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$where_sql} AND source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2901 - (SELECT COUNT(*) FROM {$table_name} {$where_sql} AND (source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%'))"
2902 - );
2903 - } else {
2904 - // No filters
2905 - $total_records = $wpdb->get_var(
2906 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2907 - (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')"
2908 - );
2909 - }
2910 - $total_pages = ceil($total_records / $per_page);
2911 -
2912 - // Step 1: Get unique source_urls for this page (ordered by latest timestamp) with filters
2913 - if (!empty($where_values)) {
2914 - $query_args = array_merge($where_values, array($per_page, $offset));
2915 - $urls_query = $wpdb->prepare(
2916 - "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2917 - {$where_sql}
2918 - GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2919 - ...$query_args
2920 - );
2921 - } else if (!empty($where_sql)) {
2922 - $urls_query = $wpdb->prepare(
2923 - "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2924 - {$where_sql}
2925 - GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2926 - $per_page, $offset
2927 - );
2928 - } else {
2929 - $urls_query = $wpdb->prepare(
2930 - "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2931 - GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2932 - $per_page, $offset
2933 - );
2934 - }
2935 - $page_urls = $wpdb->get_results($urls_query);
2936 -
2937 - // Step 2: Build list of source_urls to fetch
2938 - $url_list = array();
2939 - $url_order_map = array();
2940 - $order_index = 0;
2941 - foreach ($page_urls as $url_row) {
2942 - $url = $url_row->source_url;
2943 - $url_list[] = $url;
2944 - $url_order_map[$url] = $order_index++;
2945 - }
2946 -
2947 - // Step 3: Fetch all rows for these source_urls (with search filter if applicable)
2948 - $prompts = array();
2949 - if (!empty($url_list)) {
2950 - $placeholders = implode(',', array_fill(0, count($url_list), '%s'));
2951 - if ($search_query) {
2952 - // Include search filter in the final fetch
2953 - $prompts_query = $wpdb->prepare(
2954 - "SELECT id, article_content, source_url, timestamp, role_restriction
2955 - FROM {$table_name}
2956 - WHERE source_url IN ($placeholders) AND article_content LIKE %s
2957 - ORDER BY timestamp DESC",
2958 - ...array_merge($url_list, ['%' . $wpdb->esc_like($search_query) . '%'])
2959 - );
2960 - } else {
2961 - $prompts_query = $wpdb->prepare(
2962 - "SELECT id, article_content, source_url, timestamp, role_restriction
2963 - FROM {$table_name}
2964 - WHERE source_url IN ($placeholders)
2965 - ORDER BY timestamp DESC",
2966 - $url_list
2967 - );
2968 - }
2969 - $prompts = $wpdb->get_results($prompts_query);
2970 - }
2971 -
2972 - // Group prompts by source_url for chunk display
2973 - $grouped_prompts = array();
2974 - foreach ($prompts as $prompt) {
2975 - $source_url = $prompt->source_url ?? '';
2976 -
2977 - // Parse chunk metadata using the proper chunker method (same as initial page load)
2978 - if (class_exists('MxChat_Chunker')) {
2979 - $chunk_meta = MxChat_Chunker::parse_stored_chunk($prompt->article_content);
2980 - $prompt->chunk_metadata = $chunk_meta['metadata'];
2981 - $prompt->display_content = $chunk_meta['text'];
2982 - } else {
2983 - $prompt->chunk_metadata = array();
2984 - $prompt->display_content = $prompt->article_content;
2985 - }
2986 -
2987 - if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0) {
2988 - if (!isset($grouped_prompts[$source_url])) {
2989 - $grouped_prompts[$source_url] = array();
2990 - }
2991 - $grouped_prompts[$source_url][] = $prompt;
2992 - } else {
2993 - // Ungrouped entries
2994 - $grouped_prompts['_ungrouped_' . $prompt->id] = array($prompt);
2995 - }
2996 - }
2997 -
2998 - // Sort groups by the original URL order (newest first)
2999 - uksort($grouped_prompts, function($a, $b) use ($url_order_map) {
3000 - $order_a = isset($url_order_map[$a]) ? $url_order_map[$a] : PHP_INT_MAX;
3001 - $order_b = isset($url_order_map[$b]) ? $url_order_map[$b] : PHP_INT_MAX;
3002 - return $order_a - $order_b;
3003 - });
3004 -
3005 - // Sort each group internally by chunk_index
3006 - foreach ($grouped_prompts as $source_url => &$group) {
3007 - usort($group, function($a, $b) {
3008 - $index_a = isset($a->chunk_metadata['chunk_index']) ? intval($a->chunk_metadata['chunk_index']) : 0;
3009 - $index_b = isset($b->chunk_metadata['chunk_index']) ? intval($b->chunk_metadata['chunk_index']) : 0;
3010 - return $index_a - $index_b;
3011 - });
3012 - }
3013 - unset($group);
3014 -
3015 - // Build HTML for the table rows
3016 - ob_start();
3017 - $display_index = 0;
3018 - $current_page = $page;
3019 - $data_source = 'wordpress';
3020 - $current_bot_id = $bot_id;
3021 - $preview_length = 150;
3022 -
3023 - if (empty($grouped_prompts)) {
3024 - echo '<tr><td colspan="5" style="padding: 40px; text-align: center; color: var(--mxch-text-muted);">';
3025 - esc_html_e('No knowledge entries found. Use the Import Options to add content.', 'mxchat');
3026 - echo '</td></tr>';
3027 - } else {
3028 - foreach ($grouped_prompts as $source_url => $group) {
3029 - $chunk_count = count($group);
3030 - $first_prompt = $group[0];
3031 - $display_index++;
3032 -
3033 - if ($chunk_count > 1) {
3034 - // Multiple chunks - show grouped row with expand button
3035 - $group_id = 'group-' . md5($source_url);
3036 - ?>
3037 - <tr id="prompt-<?php echo esc_attr($first_prompt->id); ?>"
3038 - class="mxchat-chunk-group-header"
3039 - data-source="<?php echo esc_attr($data_source); ?>"
3040 - data-group-id="<?php echo esc_attr($group_id); ?>"
3041 - style="border-bottom: 1px solid var(--mxch-card-border);">
3042 - <td style="padding: 12px 16px; text-align: center;">
3043 - <input type="checkbox"
3044 - class="mxchat-entry-checkbox"
3045 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
3046 - data-source="<?php echo esc_attr($data_source); ?>"
3047 - data-source-url="<?php echo esc_attr($source_url); ?>"
3048 - data-is-group="true"
3049 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>">
3050 - </td>
3051 - <td style="padding: 12px 16px; font-size: 13px;">
3052 - <?php echo esc_html($first_prompt->id); ?>
3053 - </td>
3054 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
3055 - <div class="mxchat-chunk-group-info">
3056 - <button type="button" class="mxchat-chunk-toggle" data-group-id="<?php echo esc_attr($group_id); ?>">
3057 - <span class="dashicons dashicons-arrow-right-alt2"></span>
3058 - </button>
3059 - <span class="mxchat-chunk-badge"><?php echo esc_html($chunk_count); ?> <?php esc_html_e('chunks', 'mxchat'); ?></span>
3060 - <span class="mxchat-chunk-preview">
3061 - <?php
3062 - $parent_content = isset($first_prompt->display_content) ? $first_prompt->display_content : $first_prompt->article_content;
3063 - $content_preview = mb_substr($parent_content, 0, 100);
3064 - echo esc_html($content_preview . '...');
3065 - ?>
3066 - </span>
3067 - </div>
3068 - </td>
3069 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
3070 - <?php if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0 && strpos($source_url, '_ungrouped_') !== 0) : ?>
3071 - <a href="<?php echo esc_url($source_url); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
3072 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
3073 - <?php esc_html_e('View Source', 'mxchat'); ?>
3074 - </a>
3075 - <?php else : ?>
3076 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
3077 - <?php endif; ?>
3078 - </td>
3079 - <td class="mxchat-actions-cell" style="padding: 12px 16px; white-space: nowrap;">
3080 - <?php if ($data_source !== 'pinecone') : ?>
3081 - <button type="button"
3082 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
3083 - data-source-url="<?php echo esc_attr($source_url); ?>"
3084 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
3085 - data-data-source="<?php echo esc_attr($data_source); ?>"
3086 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
3087 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
3088 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
3089 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
3090 - </button>
3091 - <?php endif; ?>
3092 - <button type="button"
3093 - class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
3094 - data-source-url="<?php echo esc_attr($source_url); ?>"
3095 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
3096 - data-data-source="<?php echo esc_attr($data_source); ?>"
3097 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
3098 - data-nonce="<?php echo wp_create_nonce('mxchat_delete_chunks_nonce'); ?>"
3099 - style="color: var(--mxch-error);"
3100 - title="<?php esc_attr_e('Delete all chunks', 'mxchat'); ?>">
3101 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
3102 - </button>
3103 - </td>
3104 - </tr>
3105 - <?php
3106 - // Render hidden chunk rows
3107 - foreach ($group as $chunk_index => $chunk) {
3108 - $meta_chunk_index = isset($chunk->chunk_metadata['chunk_index']) ? intval($chunk->chunk_metadata['chunk_index']) : $chunk_index;
3109 - $meta_total_chunks = isset($chunk->chunk_metadata['total_chunks']) ? intval($chunk->chunk_metadata['total_chunks']) : $chunk_count;
3110 - $content = isset($chunk->display_content) ? $chunk->display_content : $chunk->article_content;
3111 - $content_preview = mb_strlen($content) > $preview_length
3112 - ? mb_substr($content, 0, $preview_length) . '...'
3113 - : $content;
3114 - ?>
3115 - <tr id="prompt-<?php echo esc_attr($chunk->id); ?>"
3116 - class="mxchat-chunk-row <?php echo esc_attr($group_id); ?>"
3117 - data-source="<?php echo esc_attr($data_source); ?>"
3118 - style="display: none; background: #f8f9fa; border-bottom: 1px solid var(--mxch-card-border);">
3119 - <td style="padding: 12px 16px; text-align: center;">
3120 - <!-- Checkbox column placeholder for chunks (managed by group) -->
3121 - </td>
3122 - <td style="padding: 12px 16px 12px 30px; font-size: 13px;">
3123 - <!-- Hidden ID column for chunks -->
3124 - </td>
3125 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
3126 - <div class="mxchat-accordion-wrapper">
3127 - <div class="mxchat-content-preview">
3128 - <span class="mxchat-chunk-indicator" style="margin-right: 10px; color: var(--mxch-text-secondary); font-size: 12px;">
3129 - <?php printf(esc_html__('Chunk %d of %d', 'mxchat'), $meta_chunk_index + 1, $meta_total_chunks); ?>
3130 - </span>
3131 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
3132 - <?php if (mb_strlen($content) > $preview_length) : ?>
3133 - <button class="mxchat-expand-toggle" type="button">
3134 - <span class="dashicons dashicons-arrow-down-alt2"></span>
3135 - </button>
3136 - <?php endif; ?>
3137 - </div>
3138 - <div class="mxchat-content-full" style="display: none;">
3139 - <div class="content-view">
3140 - <?php
3141 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
3142 - echo '<div dir="rtl" lang="he" class="rtl-content">';
3143 - echo wp_kses_post(wpautop($content));
3144 - echo '</div>';
3145 - } else {
3146 - echo wp_kses_post(wpautop($content));
3147 - }
3148 - ?>
3149 - </div>
3150 - </div>
3151 - </div>
3152 - </td>
3153 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
3154 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Same as parent', 'mxchat'); ?></span>
3155 - </td>
3156 - <td class="mxchat-actions-cell" style="padding: 12px 16px;">
3157 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Managed by group', 'mxchat'); ?></span>
3158 - </td>
3159 - </tr>
3160 - <?php
3161 - }
3162 - } else {
3163 - // Single entry - display normally with accordion
3164 - $prompt = $first_prompt;
3165 - $content = isset($prompt->display_content) ? $prompt->display_content : $prompt->article_content;
3166 - $content_preview = mb_strlen($content) > $preview_length
3167 - ? mb_substr($content, 0, $preview_length) . '...'
3168 - : $content;
3169 - ?>
3170 - <tr id="prompt-<?php echo esc_attr($prompt->id); ?>"
3171 - data-source="<?php echo esc_attr($data_source); ?>"
3172 - style="border-bottom: 1px solid var(--mxch-card-border);">
3173 - <td style="padding: 12px 16px; text-align: center;">
3174 - <input type="checkbox"
3175 - class="mxchat-entry-checkbox"
3176 - data-entry-id="<?php echo esc_attr($prompt->id); ?>"
3177 - data-source="<?php echo esc_attr($data_source); ?>"
3178 - data-source-url="<?php echo esc_attr($source_url); ?>"
3179 - data-is-group="false">
3180 - </td>
3181 - <td style="padding: 12px 16px; font-size: 13px;">
3182 - <?php echo esc_html($prompt->id); ?>
3183 - </td>
3184 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
3185 - <div class="mxchat-accordion-wrapper">
3186 - <div class="mxchat-content-preview">
3187 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
3188 - <?php if (mb_strlen($content) > $preview_length) : ?>
3189 - <button class="mxchat-expand-toggle" type="button">
3190 - <span class="dashicons dashicons-arrow-down-alt2"></span>
3191 - </button>
3192 - <?php endif; ?>
3193 - </div>
3194 - <div class="mxchat-content-full" style="display: none;">
3195 - <div class="content-view">
3196 - <?php
3197 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
3198 - echo '<div dir="rtl" lang="he" class="rtl-content">';
3199 - echo wp_kses_post(wpautop($content));
3200 - echo '</div>';
3201 - } else {
3202 - echo wp_kses_post(wpautop($content));
3203 - }
3204 - ?>
3205 - </div>
3206 - </div>
3207 - </div>
3208 - </td>
3209 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
3210 - <?php
3211 - $actual_source = $source_url;
3212 - if (strpos($source_url, '_ungrouped_') === 0) {
3213 - $actual_source = $prompt->source_url ?? '';
3214 - }
3215 - if (!empty($actual_source) && strpos($actual_source, 'mxchat://') !== 0) : ?>
3216 - <a href="<?php echo esc_url($actual_source); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
3217 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
3218 - <?php esc_html_e('View', 'mxchat'); ?>
3219 - </a>
3220 - <?php else : ?>
3221 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
3222 - <?php endif; ?>
3223 - </td>
3224 - <td style="padding: 12px 16px; white-space: nowrap;">
3225 - <button type="button"
3226 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
3227 - data-source-url="<?php echo esc_attr($prompt->source_url ?? ''); ?>"
3228 - data-entry-id="<?php echo esc_attr($prompt->id); ?>"
3229 - data-data-source="<?php echo esc_attr($data_source); ?>"
3230 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
3231 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
3232 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
3233 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
3234 - </button>
3235 - <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);">
3236 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
3237 - </button>
3238 - </td>
3239 - </tr>
3240 - <?php
3241 - }
3242 - }
3243 - }
3244 - $html = ob_get_clean();
3245 -
3246 - // Generate pagination HTML (include search/filter data for subsequent pages)
3247 - $pagination_html = '';
3248 - if ($total_pages > 1) {
3249 - $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) . '">';
3250 -
3251 - // Previous button
3252 - if ($page > 1) {
3253 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page - 1) . '">' . esc_html__('&laquo; Previous', 'mxchat') . '</a> ';
3254 - }
3255 -
3256 - // Page numbers
3257 - $start_page = max(1, $page - 2);
3258 - $end_page = min($total_pages, $page + 2);
3259 -
3260 - if ($start_page > 1) {
3261 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="1">1</a> ';
3262 - if ($start_page > 2) {
3263 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
3264 - }
3265 - }
3266 -
3267 - for ($i = $start_page; $i <= $end_page; $i++) {
3268 - if ($i == $page) {
3269 - $pagination_html .= '<span class="mxchat-page-current">' . $i . '</span> ';
3270 - } else {
3271 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $i . '">' . $i . '</a> ';
3272 - }
3273 - }
3274 -
3275 - if ($end_page < $total_pages) {
3276 - if ($end_page < $total_pages - 1) {
3277 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
3278 - }
3279 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $total_pages . '">' . $total_pages . '</a> ';
3280 - }
3281 -
3282 - // Next button
3283 - if ($page < $total_pages) {
3284 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page + 1) . '">' . esc_html__('Next &raquo;', 'mxchat') . '</a>';
3285 - }
3286 -
3287 - $pagination_html .= '</div>';
3288 - }
3289 -
3290 - wp_send_json_success(array(
3291 - 'html' => $html,
3292 - 'pagination_html' => $pagination_html,
3293 - 'total_count' => $total_records,
3294 - 'total_pages' => $total_pages,
3295 - 'page' => $page,
3296 - 'per_page' => $per_page,
3297 - 'data_source' => 'wordpress'
3298 - ));
3299 -}
3300 -
3301 -/**
3302 - * AJAX handler to detect available sitemaps on the site
3303 - * Optimized for speed - only checks primary sitemap indexes first
3304 - */
3305 -public function ajax_mxchat_detect_sitemaps() {
3306 - check_ajax_referer('mxchat_detect_sitemaps_nonce', 'nonce');
3307 -
3308 - if (!current_user_can('manage_options')) {
3309 - wp_send_json_error(array('message' => 'Unauthorized'));
3310 - return;
3311 - }
3312 -
3313 - $site_url = get_site_url();
3314 - $sitemaps = array();
3315 - $found_index = false;
3316 -
3317 - // Only check the main sitemap index files first (much faster)
3318 - // These are the primary entry points that contain sub-sitemaps
3319 - $primary_indexes = array(
3320 - 'sitemap_index.xml' => 'Yoast SEO / Rank Math', // Yoast & Rank Math
3321 - 'wp-sitemap.xml' => 'WordPress Core', // WordPress Core
3322 - 'sitemap.xml' => 'Standard', // Generic/AIOSEO
3323 - );
3324 -
3325 - foreach ($primary_indexes as $path => $source) {
3326 - $url = trailingslashit($site_url) . $path;
3327 -
3328 - $response = wp_remote_head($url, array(
3329 - 'timeout' => 10,
3330 - 'sslverify' => false,
3331 - 'redirection' => 1,
3332 - 'user-agent' => mxchat_ingest_user_agent(),
3333 - ));
3334 -
3335 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
3336 - // Found a sitemap index - parse it to get sub-sitemaps
3337 - $sub_sitemaps = $this->parse_sitemap_index($url);
3338 - if (!empty($sub_sitemaps)) {
3339 - $sitemaps[] = array(
3340 - 'url' => $url,
3341 - 'type' => 'index',
3342 - 'source' => $source,
3343 - 'sub_sitemaps' => $sub_sitemaps
3344 - );
3345 - $found_index = true;
3346 - // Found a valid index, no need to check others
3347 - break;
3348 - }
3349 - }
3350 - }
3351 -
3352 - // If no sitemap index found, check for standalone sitemaps
3353 - if (!$found_index) {
3354 - $standalone_sitemaps = array(
3355 - 'post-sitemap.xml' => array('type' => 'content', 'source' => 'Yoast SEO'),
3356 - 'page-sitemap.xml' => array('type' => 'content', 'source' => 'Yoast SEO'),
3357 - );
3358 -
3359 - foreach ($standalone_sitemaps as $path => $info) {
3360 - $url = trailingslashit($site_url) . $path;
3361 -
3362 - $response = wp_remote_head($url, array(
3363 - 'timeout' => 2,
3364 - 'sslverify' => false
3365 - ));
3366 -
3367 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
3368 - $sitemaps[] = array(
3369 - 'url' => $url,
3370 - 'type' => $info['type'],
3371 - 'source' => $info['source'],
3372 - 'url_count' => 0 // Skip URL count for speed
3373 - );
3374 - }
3375 - }
3376 - }
3377 -
3378 - wp_send_json_success(array(
3379 - 'sitemaps' => $sitemaps,
3380 - 'site_url' => $site_url
3381 - ));
3382 -}
3383 -
3384 -/**
3385 - * Parse a sitemap index to get sub-sitemaps
3386 - * Optimized: doesn't fetch URL count for each sub-sitemap (too slow)
3387 - */
3388 -private function parse_sitemap_index($url) {
3389 - $sub_sitemaps = array();
3390 -
3391 - $response = wp_remote_get($url, array(
3392 - 'timeout' => 30,
3393 - 'sslverify' => false,
3394 - 'user-agent' => mxchat_ingest_user_agent(),
3395 - 'headers' => array(
3396 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
3397 - ),
3398 - ));
3399 -
3400 - if (is_wp_error($response)) {
3401 - return $sub_sitemaps;
3402 - }
3403 -
3404 - $body = wp_remote_retrieve_body($response);
3405 - if (empty($body)) {
3406 - return $sub_sitemaps;
3407 - }
3408 -
3409 - // Suppress XML errors
3410 - libxml_use_internal_errors(true);
3411 - $xml = simplexml_load_string($body);
3412 - libxml_clear_errors();
3413 -
3414 - if ($xml === false) {
3415 - return $sub_sitemaps;
3416 - }
3417 -
3418 - // Check if it's a sitemap index (contains <sitemap> elements)
3419 - if (isset($xml->sitemap)) {
3420 - foreach ($xml->sitemap as $sitemap) {
3421 - $loc = (string) $sitemap->loc;
3422 - if (!empty($loc)) {
3423 - // Try to determine the type from the URL
3424 - $type = 'content';
3425 - if (strpos($loc, 'category') !== false || strpos($loc, 'tag') !== false || strpos($loc, 'taxonomy') !== false) {
3426 - $type = 'taxonomy';
3427 - } elseif (strpos($loc, 'author') !== false || strpos($loc, 'user') !== false) {
3428 - $type = 'author';
3429 - }
3430 -
3431 - // Skip URL count - too slow to fetch for each sitemap
3432 - $sub_sitemaps[] = array(
3433 - 'url' => $loc,
3434 - 'type' => $type,
3435 - 'url_count' => 0, // Don't fetch - takes too long
3436 - 'name' => basename(parse_url($loc, PHP_URL_PATH))
3437 - );
3438 - }
3439 - }
3440 - }
3441 -
3442 - return $sub_sitemaps;
3443 -}
3444 -
3445 -/**
3446 - * Get URL count from a sitemap
3447 - */
3448 -private function get_sitemap_url_count($url) {
3449 - $response = wp_remote_get($url, array(
3450 - 'timeout' => 30,
3451 - 'sslverify' => false,
3452 - 'user-agent' => mxchat_ingest_user_agent(),
3453 - 'headers' => array(
3454 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
3455 - ),
3456 - ));
3457 -
3458 - if (is_wp_error($response)) {
3459 - return 0;
3460 - }
3461 -
3462 - $body = wp_remote_retrieve_body($response);
3463 - if (empty($body)) {
3464 - return 0;
3465 - }
3466 -
3467 - // Count <url> or <loc> elements
3468 - $count = preg_match_all('/<url>/i', $body, $matches);
3469 - return $count ?: 0;
3470 -}
3471 -
3472 -/**
3473 - * Get sitemaps declared in robots.txt
3474 - */
3475 -private function get_sitemaps_from_robots($site_url) {
3476 - $sitemaps = array();
3477 - $robots_url = trailingslashit($site_url) . 'robots.txt';
3478 -
3479 - $response = wp_remote_get($robots_url, array(
3480 - 'timeout' => 15,
3481 - 'sslverify' => false,
3482 - 'user-agent' => mxchat_ingest_user_agent(),
3483 - ));
3484 -
3485 - if (is_wp_error($response)) {
3486 - return $sitemaps;
3487 - }
3488 -
3489 - $body = wp_remote_retrieve_body($response);
3490 - if (empty($body)) {
3491 - return $sitemaps;
3492 - }
3493 -
3494 - // Find Sitemap: declarations
3495 - if (preg_match_all('/^Sitemap:\s*(.+)$/mi', $body, $matches)) {
3496 - foreach ($matches[1] as $sitemap_url) {
3497 - $sitemap_url = trim($sitemap_url);
3498 - if (filter_var($sitemap_url, FILTER_VALIDATE_URL)) {
3499 - $sitemaps[] = $sitemap_url;
3500 - }
3501 - }
3502 - }
3503 -
3504 - return $sitemaps;
3505 -}
3506 -
3507 1767 public function mxchat_stop_processing() {
3508 1768 // Verify permissions
3509 1769 if (!current_user_can('manage_options')) {
3510 1770 wp_die(esc_html__('Unauthorized access', 'mxchat'));
@@ -3512,52 +1772,28 @@
3512 1772
3513 1773 // Verify nonce
3514 1774 check_admin_referer('mxchat_stop_processing_action', 'mxchat_stop_processing_nonce');
3515 1775
3516 - global $wpdb;
3517 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
3518 -
3519 - // Get active queue IDs
3520 - $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
3521 - $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
3522 -
3523 - // Delete all pending items from active queues
3524 - if ($sitemap_queue_id) {
3525 - $wpdb->delete(
3526 - $table_name,
3527 - array(
3528 - 'queue_id' => $sitemap_queue_id,
3529 - 'status' => 'pending'
3530 - ),
3531 - array('%s', '%s')
3532 - );
3533 -
3534 - delete_transient('mxchat_active_queue_sitemap');
1776 + // Get the last sitemap URL and clear its transient
1777 + $sitemap_url = get_transient('mxchat_last_sitemap_url');
1778 + if ($sitemap_url) {
1779 + delete_transient('mxchat_sitemap_status_' . md5($sitemap_url));
3535 1780 delete_transient('mxchat_last_sitemap_url');
3536 1781 }
3537 -
3538 - if ($pdf_queue_id) {
3539 - // Get PDF path before deleting
3540 - $pdf_path = $this->mxchat_get_queue_meta($pdf_queue_id, 'pdf_path');
3541 -
3542 - $wpdb->delete(
3543 - $table_name,
3544 - array(
3545 - 'queue_id' => $pdf_queue_id,
3546 - 'status' => 'pending'
3547 - ),
3548 - array('%s', '%s')
3549 - );
3550 -
3551 - // Delete PDF file
3552 - if ($pdf_path && file_exists($pdf_path)) {
3553 - wp_delete_file($pdf_path);
3554 - }
3555 -
3556 - delete_transient('mxchat_active_queue_pdf');
1782 +
1783 + // Get the last PDF URL and clear its transient
1784 + $pdf_url = get_transient('mxchat_last_pdf_url');
1785 + if ($pdf_url) {
1786 + delete_transient('mxchat_pdf_status_' . md5($pdf_url));
3557 1787 delete_transient('mxchat_last_pdf_url');
3558 1788 }
3559 1789
1790 + // Unschedule any pending sitemap events
1791 + $timestamp = wp_next_scheduled('mxchat_process_sitemap_urls');
1792 + if ($timestamp) {
1793 + wp_unschedule_event($timestamp, 'mxchat_process_sitemap_urls');
1794 + }
1795 +
3560 1796 // Redirect back with a success message
3561 1797 set_transient('mxchat_admin_notice_success',
3562 1798 esc_html__('Processing has been stopped successfully.', 'mxchat'),
3563 1799 30
@@ -3564,12 +1800,8 @@
3564 1800 );
3565 1801 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
3566 1802 exit;
3567 1803 }
3568 -
3569 -/**
3570 - * Get content list for processing
3571 - */
3572 1804 public function ajax_mxchat_get_content_list() {
3573 1805 // Verify the nonce
3574 1806 check_ajax_referer('mxchat_content_selector_nonce', 'nonce');
3575 1807
@@ -3577,9 +1809,9 @@
3577 1809 wp_send_json_error(__('Unauthorized access', 'mxchat'));
3578 1810 }
3579 1811
3580 1812 $page = isset($_GET['page']) ? absint($_GET['page']) : 1;
3581 - $per_page = isset($_GET['per_page']) ? absint($_GET['per_page']) : 100;
1813 + $per_page = isset($_GET['per_page']) ? absint($_GET['per_page']) : 20;
3582 1814 $search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
3583 1815 $post_type = isset($_GET['post_type']) ? sanitize_text_field($_GET['post_type']) : 'all';
3584 1816 $post_status = isset($_GET['post_status']) ? sanitize_text_field($_GET['post_status']) : 'publish';
3585 1817 $processed_filter = isset($_GET['processed_filter']) ? sanitize_text_field($_GET['processed_filter']) : 'all';
@@ -3592,47 +1824,20 @@
3592 1824 'orderby' => 'date',
3593 1825 'order' => 'DESC',
3594 1826 );
3595 1827
3596 - // Handle post types - IMPROVED VERSION
1828 + // Handle post types
3597 1829 if ($post_type !== 'all') {
3598 1830 $args['post_type'] = $post_type;
3599 1831 } else {
3600 - // Get all available post types that might contain content
3601 - $all_post_types = array();
1832 + // Default to post and page if we can't get post types
1833 + $args['post_type'] = array('post', 'page');
3602 1834
3603 - // First get all public post types
3604 - $public_types = get_post_types(array('public' => true), 'names');
3605 - $all_post_types = array_merge($all_post_types, $public_types);
3606 -
3607 - // Add common forum/community post types
3608 - $forum_types = array('topic', 'reply', 'forum', 'wpforo_topic', 'wpforo_post');
3609 - foreach ($forum_types as $forum_type) {
3610 - if (post_type_exists($forum_type)) {
3611 - $all_post_types[] = $forum_type;
3612 - }
1835 + // Try to get public post types
1836 + $public_types = $this->mxchat_get_public_post_types();
1837 + if (is_array($public_types) && !empty($public_types)) {
1838 + $args['post_type'] = array_keys($public_types);
3613 1839 }
3614 -
3615 - // Add other commonly used post types
3616 - $common_types = array('product', 'job_listing', 'event', 'portfolio');
3617 - foreach ($common_types as $common_type) {
3618 - if (post_type_exists($common_type)) {
3619 - $all_post_types[] = $common_type;
3620 - }
3621 - }
3622 -
3623 - // Remove duplicates and ensure we have at least some post types
3624 - $all_post_types = array_unique($all_post_types);
3625 -
3626 - if (empty($all_post_types)) {
3627 - // Fallback to basic post types
3628 - $all_post_types = array('post', 'page');
3629 - }
3630 -
3631 - $args['post_type'] = $all_post_types;
3632 -
3633 - // Debug logging to see what post types are being queried
3634 - //error_log('MxChat Debug: Querying post types: ' . implode(', ', $all_post_types));
3635 1840 }
3636 1841
3637 1842 if (!empty($search)) {
3638 1843 $args['s'] = $search;
@@ -3637,58 +1842,36 @@
3637 1842 if (!empty($search)) {
3638 1843 $args['s'] = $search;
3639 1844 }
3640 1845
3641 - // Get processed data from storage
1846 + // ================================
1847 + // FIXED: Check only the ACTIVE storage method
1848 + // ================================
1849 +
3642 1850 $processed_data = array();
3643 1851
1852 + // Check if Pinecone is enabled
3644 1853 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3645 1854 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3646 -
1855 +
3647 1856 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3648 - // Get fresh data from Pinecone - no caching
1857 + // ONLY check Pinecone if it's enabled
3649 1858 $processed_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
3650 1859 } else {
3651 - // WordPress DB checking with better URL matching for all post types
1860 + // ONLY check WordPress DB if Pinecone is not enabled
3652 1861 global $wpdb;
3653 1862 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3654 - $processed_items = $wpdb->get_results("SELECT id, source_url, article_content, timestamp FROM {$table_name}");
3655 -
3656 - // Group items by source_url to count chunks
3657 - $url_chunk_counts = array();
3658 - $url_latest_timestamp = array();
3659 - $url_first_id = array();
3660 -
1863 + $processed_items = $wpdb->get_results("SELECT id, source_url, timestamp FROM {$table_name}");
1864 +
3661 1865 if (!empty($processed_items)) {
3662 1866 foreach ($processed_items as $item) {
3663 - $url = $item->source_url;
3664 - if (empty($url)) continue;
3665 -
3666 - // Count chunks per URL
3667 - if (!isset($url_chunk_counts[$url])) {
3668 - $url_chunk_counts[$url] = 0;
3669 - $url_latest_timestamp[$url] = $item->timestamp;
3670 - $url_first_id[$url] = $item->id;
3671 - }
3672 - $url_chunk_counts[$url]++;
3673 -
3674 - // Track latest timestamp
3675 - if (strtotime($item->timestamp) > strtotime($url_latest_timestamp[$url])) {
3676 - $url_latest_timestamp[$url] = $item->timestamp;
3677 - }
3678 - }
3679 -
3680 - // Now build processed_data with chunk counts
3681 - foreach ($url_chunk_counts as $url => $chunk_count) {
3682 - $post_id = $this->mxchat_url_to_post_id_improved($url);
3683 -
1867 + $post_id = url_to_postid($item->source_url);
3684 1868 if ($post_id) {
3685 1869 $processed_data[$post_id] = array(
3686 - 'db_id' => $url_first_id[$url],
3687 - 'timestamp' => $url_latest_timestamp[$url],
3688 - 'url' => $url,
3689 - 'source' => 'wordpress',
3690 - 'chunk_count' => $chunk_count
1870 + 'db_id' => $item->id,
1871 + 'timestamp' => $item->timestamp,
1872 + 'url' => $item->source_url,
1873 + 'source' => 'wordpress'
3691 1874 );
3692 1875 }
3693 1876 }
3694 1877 }
@@ -3693,8 +1876,10 @@
3693 1876 }
3694 1877 }
3695 1878 }
3696 1879
1880 + // ================================
1881 +
3697 1882 // Get processed IDs as a simple array for in_array checks
3698 1883 $processed_ids = array_keys($processed_data);
3699 1884
3700 1885 // Handle processed/unprocessed filter
@@ -3736,14 +1921,8 @@
3736 1921 $db_record_id = $item_data['db_id'];
3737 1922 }
3738 1923 }
3739 1924
3740 - // Get chunk count for this item
3741 - $chunk_count = 0;
3742 - if ($is_processed && isset($processed_data[$id]['chunk_count'])) {
3743 - $chunk_count = intval($processed_data[$id]['chunk_count']);
3744 - }
3745 -
3746 1925 $content_items[] = array(
3747 1926 'id' => $id,
3748 1927 'title' => get_the_title(),
3749 1928 'permalink' => get_permalink(),
@@ -3754,10 +1933,9 @@
3754 1933 'word_count' => $word_count,
3755 1934 'already_processed' => $is_processed,
3756 1935 'processed_date' => $processed_date,
3757 1936 'db_record_id' => $db_record_id,
3758 - 'data_source' => $data_source,
3759 - 'chunk_count' => $chunk_count
1937 + 'data_source' => $data_source
3760 1938 );
3761 1939 }
3762 1940 wp_reset_postdata();
3763 1941 }
@@ -3773,178 +1951,8 @@
3773 1951 wp_send_json_success($response);
3774 1952 exit;
3775 1953 }
3776 1954
3777 -
3778 -/**
3779 - * This function handles various WooCommerce URL formats and permalink structures
3780 - */
3781 -private function mxchat_url_to_post_id_improved($url) {
3782 - // First try the standard WordPress function
3783 - $post_id = url_to_postid($url);
3784 -
3785 - if ($post_id > 0) {
3786 - return $post_id;
3787 - }
3788 -
3789 - // If that fails, try more aggressive URL matching
3790 - // Remove trailing slashes and query parameters for better matching
3791 - $clean_url = rtrim($url, '/');
3792 - $clean_url = strtok($clean_url, '?'); // Remove query parameters
3793 -
3794 - // Try again with cleaned URL
3795 - $post_id = url_to_postid($clean_url);
3796 - if ($post_id > 0) {
3797 - return $post_id;
3798 - }
3799 -
3800 - // For bbPress forum topics, try extracting slug from URL
3801 - if (strpos($url, '/topic/') !== false || strpos($url, '/forums/') !== false) {
3802 - // Handle bbPress URLs: /forums/topic/topic-name/
3803 - if (preg_match('/\/forums\/topic\/([^\/\?]+)/', $url, $matches)) {
3804 - $topic_slug = $matches[1];
3805 -
3806 - // Look up topic by slug
3807 - $topic = get_page_by_path($topic_slug, OBJECT, 'topic');
3808 - if ($topic) {
3809 - return $topic->ID;
3810 - }
3811 -
3812 - // Alternative method: query by post_name
3813 - global $wpdb;
3814 - $post_id = $wpdb->get_var($wpdb->prepare(
3815 - "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
3816 - $topic_slug
3817 - ));
3818 -
3819 - if ($post_id) {
3820 - return intval($post_id);
3821 - }
3822 - }
3823 -
3824 - // Handle simpler topic URLs: /topic/topic-name/
3825 - if (preg_match('/\/topic\/([^\/\?]+)/', $url, $matches)) {
3826 - $topic_slug = $matches[1];
3827 -
3828 - global $wpdb;
3829 - $post_id = $wpdb->get_var($wpdb->prepare(
3830 - "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
3831 - $topic_slug
3832 - ));
3833 -
3834 - if ($post_id) {
3835 - return intval($post_id);
3836 - }
3837 - }
3838 - }
3839 -
3840 - // For WooCommerce products
3841 - if (strpos($url, '/product/') !== false || strpos($url, 'product=') !== false) {
3842 - // Extract product slug from various URL formats
3843 - $product_slug = '';
3844 -
3845 - // Handle pretty permalinks: /product/product-name/
3846 - if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
3847 - $product_slug = $matches[1];
3848 - }
3849 - // Handle query parameters: ?product=product-name
3850 - elseif (preg_match('/[\?&]product=([^&]+)/', $url, $matches)) {
3851 - $product_slug = $matches[1];
3852 - }
3853 -
3854 - if (!empty($product_slug)) {
3855 - // Look up product by slug
3856 - $product = get_page_by_path($product_slug, OBJECT, 'product');
3857 - if ($product) {
3858 - return $product->ID;
3859 - }
3860 -
3861 - // Alternative method: query by post_name
3862 - global $wpdb;
3863 - $post_id = $wpdb->get_var($wpdb->prepare(
3864 - "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'product' AND post_status = 'publish'",
3865 - $product_slug
3866 - ));
3867 -
3868 - if ($post_id) {
3869 - return intval($post_id);
3870 - }
3871 - }
3872 - }
3873 -
3874 - // Generic approach: try to extract slug and match against all post types
3875 - $parsed_url = wp_parse_url($clean_url);
3876 - $path = $parsed_url['path'] ?? '';
3877 -
3878 - if (!empty($path)) {
3879 - // Get the last part of the path as potential slug
3880 - $path_parts = array_filter(explode('/', trim($path, '/')));
3881 - $potential_slug = end($path_parts);
3882 -
3883 - if (!empty($potential_slug)) {
3884 - global $wpdb;
3885 -
3886 - // Try to find any post with this slug
3887 - $post_id = $wpdb->get_var($wpdb->prepare(
3888 - "SELECT ID FROM {$wpdb->posts}
3889 - WHERE post_name = %s
3890 - AND post_status IN ('publish', 'closed', 'private')
3891 - AND post_type NOT IN ('revision', 'attachment', 'nav_menu_item')
3892 - ORDER BY CASE
3893 - WHEN post_type = 'post' THEN 1
3894 - WHEN post_type = 'page' THEN 2
3895 - WHEN post_type = 'topic' THEN 3
3896 - WHEN post_type = 'product' THEN 4
3897 - ELSE 5
3898 - END
3899 - LIMIT 1",
3900 - $potential_slug
3901 - ));
3902 -
3903 - if ($post_id) {
3904 - return intval($post_id);
3905 - }
3906 - }
3907 - }
3908 -
3909 - // ADDITIONAL: Try direct database lookup by URL variations
3910 - global $wpdb;
3911 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3912 -
3913 - // Try variations of the URL (with/without trailing slash, http/https)
3914 - $url_variations = array(
3915 - $url,
3916 - rtrim($url, '/'),
3917 - $url . '/',
3918 - str_replace('http://', 'https://', $url),
3919 - str_replace('https://', 'http://', $url),
3920 - str_replace('http://', 'https://', rtrim($url, '/')),
3921 - str_replace('https://', 'http://', rtrim($url, '/'))
3922 - );
3923 -
3924 - // Remove duplicates
3925 - $url_variations = array_unique($url_variations);
3926 -
3927 - foreach ($url_variations as $variation) {
3928 - $existing_record = $wpdb->get_row($wpdb->prepare(
3929 - "SELECT id, source_url FROM $table_name WHERE source_url = %s",
3930 - $variation
3931 - ));
3932 -
3933 - if ($existing_record) {
3934 - // Try to get post ID from this stored URL
3935 - $stored_post_id = url_to_postid($existing_record->source_url);
3936 - if ($stored_post_id > 0) {
3937 - return $stored_post_id;
3938 - }
3939 - }
3940 - }
3941 -
3942 - return 0; // No match found
3943 -}
3944 -/**
3945 - * Process selected content via AJAX
3946 - */
3947 1955 public function ajax_mxchat_process_selected_content() {
3948 1956 // Basic request validation
3949 1957 if (!check_ajax_referer('mxchat_content_selector_nonce', 'nonce', false)) {
3950 1958 wp_send_json_error('Invalid nonce');
@@ -3968,24 +1976,8 @@
3968 1976 wp_send_json_error('No content selected');
3969 1977 exit;
3970 1978 }
3971 1979
3972 - // Get bot_id from request
3973 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
3974 -
3975 - // ACF→PDF extraction is opt-in per import batch. Persist the last-used value so users
3976 - // don't re-check on every batch; the default is OFF for installs that haven't set it.
3977 - $extract_acf_pdfs = !empty($_POST['extract_acf_pdfs']) && $_POST['extract_acf_pdfs'] !== 'false';
3978 - $mxchat_options = get_option('mxchat_options', array());
3979 - if (!is_array($mxchat_options)) {
3980 - $mxchat_options = array();
3981 - }
3982 - $prior_default = !empty($mxchat_options['acf_pdf_extract_default']);
3983 - if ($prior_default !== $extract_acf_pdfs) {
3984 - $mxchat_options['acf_pdf_extract_default'] = $extract_acf_pdfs ? 1 : 0;
3985 - update_option('mxchat_options', $mxchat_options);
3986 - }
3987 -
3988 1980 // Process only ONE post at a time to avoid request size issues
3989 1981 $post_id = reset($post_ids);
3990 1982 $post = get_post($post_id);
3991 1983
@@ -3992,185 +1984,15 @@
3992 1984 if (!$post) {
3993 1985 wp_send_json_error('Post not found');
3994 1986 exit;
3995 1987 }
3996 -
3997 - // Allow developers to modify post data before processing into knowledge base
3998 - $post = apply_filters('mxchat_before_process_post', $post, $bot_id);
3999 -
4000 - // Get content including title, short description (for WooCommerce), and main content
4001 - $content = $post->post_title . "\n\n";
4002 -
4003 - // Add short description if it exists (WooCommerce products use post_excerpt for short description)
4004 - if (!empty($post->post_excerpt)) {
4005 - // Remove shortcode tags but preserve content inside them
4006 - $clean_excerpt = $this->strip_shortcode_tags_preserve_content($post->post_excerpt);
4007 - $content .= "Short Description: " . wp_strip_all_tags($clean_excerpt) . "\n\n";
4008 - }
4009 -
4010 - // Add main content - remove shortcode tags but preserve content inside them
4011 - $clean_content = $this->strip_shortcode_tags_preserve_content($post->post_content);
4012 - $content .= wp_strip_all_tags($clean_content);
4013 -
4014 - // ADD WOOCOMMERCE PRODUCT DATA (pricing, stock, categories, custom tabs)
4015 - if (get_post_type($post_id) === 'product' && class_exists('WooCommerce')) {
4016 - $product = wc_get_product($post_id);
4017 -
4018 - if ($product) {
4019 - // Get pricing information
4020 - $regular_price = $product->get_regular_price();
4021 - $sale_price = $product->get_sale_price();
4022 - $price = $product->get_price();
4023 - $sku = $product->get_sku();
4024 -
4025 - // Get currency symbol
4026 - $currency_symbol = get_woocommerce_currency_symbol();
4027 -
4028 - // Add pricing information
4029 - $content .= "\n";
4030 - if (!empty($regular_price)) {
4031 - $content .= "Price: " . $currency_symbol . $regular_price . "\n";
4032 - } elseif (!empty($price)) {
4033 - $content .= "Price: " . $currency_symbol . $price . "\n";
4034 - }
4035 -
4036 - if (!empty($sale_price) && $sale_price !== $regular_price) {
4037 - $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
4038 - }
4039 -
4040 - // Handle variable products - show price range
4041 - if ($product->is_type('variable')) {
4042 - $min_price = $product->get_variation_price('min');
4043 - $max_price = $product->get_variation_price('max');
4044 - if ($min_price !== $max_price) {
4045 - $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
4046 - }
4047 - }
4048 -
4049 - if (!empty($sku)) {
4050 - $content .= "SKU: " . $sku . "\n";
4051 - }
4052 -
4053 - // Get product categories
4054 - $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
4055 - if (!empty($categories) && !is_wp_error($categories)) {
4056 - $content .= "Categories: " . implode(', ', $categories) . "\n";
4057 - }
4058 - }
4059 -
4060 - // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
4061 - $custom_tabs = get_post_meta($post_id, 'yikes_woo_products_tabs', true);
4062 - if (!empty($custom_tabs) && is_array($custom_tabs)) {
4063 - foreach ($custom_tabs as $tab) {
4064 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
4065 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
4066 -
4067 - if (!empty($tab_title) && !empty($tab_content)) {
4068 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
4069 - }
4070 - }
4071 - }
4072 -
4073 - // Also check for reusable/saved tabs applied to this product
4074 - $applied_saved_tabs = get_post_meta($post_id, 'yikes_woo_reusable_products_tabs_applied', true);
4075 - if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
4076 - $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
4077 - if (!empty($saved_tabs) && is_array($saved_tabs)) {
4078 - foreach ($applied_saved_tabs as $saved_tab_id) {
4079 - if (isset($saved_tabs[$saved_tab_id])) {
4080 - $tab = $saved_tabs[$saved_tab_id];
4081 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
4082 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
4083 -
4084 - if (!empty($tab_title) && !empty($tab_content)) {
4085 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
4086 - }
4087 - }
4088 - }
4089 - }
4090 - }
4091 - }
4092 -
4093 - // ADD ACF FIELDS SUPPORT
4094 - $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
4095 - $pdf_extracted_count = 0;
4096 - if (!empty($acf_fields)) {
4097 - $acf_content_parts = array();
4098 - $pdf_attachment_ids = array();
4099 -
4100 - foreach ($acf_fields as $field_name => $field_value) {
4101 - $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
4102 -
4103 - if (!empty($formatted_value)) {
4104 - $field_label = ucwords(str_replace('_', ' ', $field_name));
4105 - $acf_content_parts[] = $field_label . ": " . $formatted_value;
4106 - }
4107 -
4108 - // Walk this field's value tree for any PDF attachment references and queue them for extraction.
4109 - // Only when the user opted into ACF→PDF extraction for this batch; otherwise the ACF text
4110 - // still lands in the KB but the heavier PDF parsing is skipped.
4111 - if ($extract_acf_pdfs) {
4112 - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($field_value, $pdf_attachment_ids);
4113 - }
4114 - }
4115 -
4116 - if (!empty($acf_content_parts)) {
4117 - $content .= "\n\n" . implode("\n", $acf_content_parts);
4118 - }
4119 -
4120 - // Extract text from each unique PDF found in ACF fields and append as a labeled section
4121 - if ($extract_acf_pdfs && !empty($pdf_attachment_ids)) {
4122 - $pdf_attachment_ids = array_unique(array_filter(array_map('intval', $pdf_attachment_ids)));
4123 - $pdf_sections = array();
4124 - foreach ($pdf_attachment_ids as $att_id) {
4125 - $pdf_text = $this->mxchat_extract_pdf_text_by_attachment_id($att_id);
4126 - if (!empty($pdf_text)) {
4127 - $pdf_title = get_the_title($att_id);
4128 - $pdf_url = wp_get_attachment_url($att_id);
4129 - $header = 'PDF Attachment';
4130 - if (!empty($pdf_title)) {
4131 - $header .= ': ' . $pdf_title;
4132 - }
4133 - if (!empty($pdf_url)) {
4134 - $header .= ' (' . $pdf_url . ')';
4135 - }
4136 - $pdf_sections[] = $header . "\n" . $pdf_text;
4137 - $pdf_extracted_count++;
4138 - }
4139 - }
4140 - if (!empty($pdf_sections)) {
4141 - $content .= "\n\nAttached PDFs:\n" . implode("\n\n", $pdf_sections);
4142 - }
4143 - }
4144 - }
4145 -
4146 - // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
4147 - $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
4148 - if (!empty($custom_meta)) {
4149 - $meta_content_parts = array();
4150 -
4151 - foreach ($custom_meta as $meta_key => $meta_value) {
4152 - // Convert meta key to readable label
4153 - $meta_label = ucwords(str_replace(array('_', '-'), ' ', $meta_key));
4154 - $meta_content_parts[] = $meta_label . ": " . $meta_value;
4155 - }
4156 -
4157 - if (!empty($meta_content_parts)) {
4158 - $content .= "\n\n" . implode("\n", $meta_content_parts);
4159 - }
4160 - }
4161 -
4162 - // Debug logging for WordPress Import content
4163 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Post ID: ' . $post_id . ' Title: ' . $post->post_title);
4164 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Raw post_content length: ' . strlen($post->post_content));
4165 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Final content length: ' . strlen($content));
4166 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Content preview: ' . substr($content, 0, 300));
4167 -
4168 - // Note: Removed 10,000 char limit - chunking now handles large content properly
4169 -
4170 - // Get bot-specific API key
4171 - $bot_options = $this->get_bot_options($bot_id);
4172 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
1988 +
1989 + // Get minimal content
1990 + $content = $post->post_title . "\n\n" . wp_strip_all_tags($post->post_content);
1991 + $content = substr($content, 0, 10000); // Limit content size
1992 +
1993 + // Get API key with proper model detection
1994 + $options = get_option('mxchat_options');
4173 1995 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4174 1996
4175 1997 if (strpos($selected_model, 'voyage') === 0) {
4176 1998 $api_key = $options['voyage_api_key'] ?? '';
@@ -4183,9 +2005,8 @@
4183 2005 $provider_name = 'OpenAI';
4184 2006 }
4185 2007
4186 2008 if (empty($api_key)) {
4187 - MxChat_Admin::mxchat_log_debug('api_error', $provider_name . ' API key not configured for knowledge processing');
4188 2009 wp_send_json_error($provider_name . ' API key not configured');
4189 2010 exit;
4190 2011 }
4191 2012
@@ -4191,23 +2012,26 @@
4191 2012
4192 2013 $source_url = get_permalink($post_id);
4193 2014 $vector_id = md5($source_url); // Vector ID for Pinecone
4194 2015
4195 - // Check for existing content in bot-specific storage
2016 + // ================================
2017 + // FIXED: Check for existing content in ONLY the active storage method
2018 + // ================================
2019 +
4196 2020 $is_update = false;
4197 2021
4198 - // Get bot-specific Pinecone configuration
4199 - $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
4200 - $use_pinecone = !empty($bot_pinecone_config) && ($bot_pinecone_config['use_pinecone'] ?? false);
2022 + // Check if Pinecone is enabled
2023 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
2024 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4201 2025
4202 - if ($use_pinecone && !empty($bot_pinecone_config['api_key'])) {
4203 - // Check Pinecone for this bot
4204 - $pinecone_data = $this->mxchat_get_pinecone_processed_content($bot_pinecone_config);
2026 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
2027 + // ONLY check Pinecone if it's enabled
2028 + $pinecone_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
4205 2029 if (isset($pinecone_data[$post_id])) {
4206 2030 $is_update = true;
4207 2031 }
4208 2032 } else {
4209 - // Check WordPress DB (same as before since it's shared)
2033 + // ONLY check WordPress DB if Pinecone is not enabled
4210 2034 global $wpdb;
4211 2035 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4212 2036 $existing_record = $wpdb->get_row($wpdb->prepare(
4213 2037 "SELECT id FROM $table_name WHERE source_url = %s",
@@ -4217,54 +2041,54 @@
4217 2041 if ($existing_record) {
4218 2042 $is_update = true;
4219 2043 }
4220 2044 }
4221 -
4222 - // UPDATED 2.5.6: Determine content type based on post_type
4223 - $post_type = $post->post_type;
4224 - $content_type = 'content'; // Default fallback
4225 -
4226 - // Map WordPress post types to content types
4227 - switch ($post_type) {
4228 - case 'post':
4229 - $content_type = 'post';
4230 - break;
4231 - case 'page':
4232 - $content_type = 'page';
4233 - break;
4234 - case 'product':
4235 - $content_type = 'product';
4236 - break;
4237 - default:
4238 - // For custom post types, use the post type name
4239 - $content_type = sanitize_key($post_type);
4240 - break;
4241 - }
4242 -
4243 - // Use the centralized utility function with bot_id and content_type
2045 +
2046 + // Use the centralized utility function for storage
4244 2047 $result = MxChat_Utils::submit_content_to_db(
4245 - $content,
4246 - $source_url,
2048 + $content,
2049 + $source_url,
4247 2050 $api_key,
4248 - $vector_id,
4249 - $bot_id,
4250 - $content_type
2051 + $vector_id
4251 2052 );
4252 -
2053 +
4253 2054 if (is_wp_error($result)) {
4254 - MxChat_Admin::mxchat_log_debug('storage_error', 'Knowledge storage failed: ' . $result->get_error_message(), array('source_url' => $source_url));
4255 2055 wp_send_json_error('Storage failed: ' . $result->get_error_message());
4256 2056 exit;
4257 2057 }
4258 -
4259 - // Automatically apply role restriction based on tags
4260 - $this->apply_role_restriction_to_post($post_id, $source_url);
4261 -
2058 +
2059 + // ================================
2060 + // UPDATE: Only update caches if Pinecone is enabled
2061 + // ================================
2062 +
2063 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
2064 + // Update vector ID cache for improved fetching
2065 + $this->mxchat_update_pinecone_vector_cache($vector_id);
2066 +
2067 + // Update local processed content cache for immediate UI feedback
2068 + $pinecone_cache = get_option('mxchat_pinecone_processed_cache', array());
2069 + $pinecone_cache[$post_id] = array(
2070 + 'db_id' => $vector_id,
2071 + 'processed_date' => 'Just now',
2072 + 'url' => $source_url,
2073 + 'source' => 'pinecone',
2074 + 'timestamp' => current_time('timestamp')
2075 + );
2076 + update_option('mxchat_pinecone_processed_cache', $pinecone_cache);
2077 +
2078 + // Also update the general processed content cache
2079 + $processed_cache = get_option('mxchat_processed_content_cache', array());
2080 + $processed_cache[$post_id] = array(
2081 + 'db_id' => $vector_id,
2082 + 'timestamp' => current_time('timestamp'),
2083 + 'url' => $source_url,
2084 + 'source' => 'pinecone'
2085 + );
2086 + update_option('mxchat_processed_content_cache', $processed_cache);
2087 + }
2088 +
4262 2089 $operation_type = $is_update ? 'update' : 'new';
4263 2090
4264 - // Count ACF fields for debugging
4265 - $acf_field_count = count($acf_fields);
4266 -
4267 2091 // Success response with minimal data
4268 2092 wp_send_json_success(array(
4269 2093 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully',
4270 2094 'post_id' => $post_id,
@@ -4269,95 +2093,27 @@
4269 2093 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully',
4270 2094 'post_id' => $post_id,
4271 2095 'title' => $post->post_title,
4272 2096 'operation_type' => $operation_type,
4273 - 'vector_id' => $vector_id,
4274 - 'acf_fields_found' => $acf_field_count,
4275 - 'pdf_extracted_count' => (int) $pdf_extracted_count,
4276 - 'content_preview' => substr($content, 0, 100) . '...',
4277 - 'bot_id' => $bot_id
2097 + 'vector_id' => $vector_id, // Include vector ID for debugging
2098 + 'cache_updated' => $use_pinecone // Indicate if cache was updated
4278 2099 ));
4279 2100 exit;
4280 2101 }
4281 2102
4282 -private function apply_role_restriction_to_post($post_id, $source_url) {
4283 - // Get tag-role mappings
4284 - $mappings = get_option('mxchat_tag_role_mappings', array());
4285 -
4286 - if (empty($mappings)) {
4287 - return; // No mappings, leave as public
4288 - }
4289 -
4290 - // Get all tags for the post
4291 - $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
4292 -
4293 - if (empty($post_tags)) {
4294 - return; // No tags, leave as public
4295 - }
4296 -
4297 - // Determine the highest role restriction based on tags
4298 - $highest_role = 'public';
4299 - $role_hierarchy = array(
4300 - 'public' => 0,
4301 - 'logged_in' => 1,
4302 - 'subscriber' => 2,
4303 - 'contributor' => 3,
4304 - 'author' => 4,
4305 - 'editor' => 5,
4306 - 'administrator' => 6
4307 - );
4308 -
4309 - foreach ($post_tags as $tag_slug) {
4310 - if (isset($mappings[$tag_slug])) {
4311 - $role = $mappings[$tag_slug];
4312 - if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
4313 - $highest_role = $role;
4314 - }
4315 - }
4316 - }
4317 -
4318 - // If no restricted tags found, return (leave as public)
4319 - if ($highest_role === 'public') {
4320 - return;
4321 - }
4322 -
4323 - // Update the role restriction in the database
4324 - global $wpdb;
4325 -
4326 - // Check if using Pinecone
4327 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4328 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4329 -
4330 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
4331 - // Update Pinecone role restriction
4332 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
4333 - $vector_id = md5($source_url);
4334 -
4335 - $wpdb->replace(
4336 - $roles_table,
4337 - array(
4338 - 'vector_id' => $vector_id,
4339 - 'role_restriction' => $highest_role,
4340 - 'updated_at' => current_time('mysql')
4341 - ),
4342 - array('%s', '%s', '%s')
4343 - );
4344 - } else {
4345 - // Update WordPress DB
4346 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4347 -
4348 - $wpdb->update(
4349 - $table_name,
4350 - array('role_restriction' => $highest_role),
4351 - array('source_url' => $source_url),
4352 - array('%s'),
4353 - array('%s')
4354 - );
4355 - }
4356 -}
4357 2103
2104 +
2105 + /**
2106 + * Updates cache with new vector ID if absent
2107 + */
2108 + public function mxchat_update_pinecone_vector_cache($vector_id) {
2109 + $cached_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
2110 + if (!in_array($vector_id, $cached_ids)) {
2111 + $cached_ids[] = $vector_id;
2112 + update_option('mxchat_pinecone_vector_ids_cache', $cached_ids);
2113 + }
2114 + }
4358 2115 public function mxchat_get_public_post_types() {
4359 - // Get all public post types
4360 2116 $post_types = get_post_types(array('public' => true), 'objects');
4361 2117 $post_type_options = array();
4362 2118
4363 2119 foreach ($post_types as $post_type) {
@@ -4363,47 +2119,41 @@
4363 2119 foreach ($post_types as $post_type) {
4364 2120 $post_type_options[$post_type->name] = $post_type->label;
4365 2121 }
4366 2122
4367 - // Also include common forum/community post types that might not be marked as public
4368 - $additional_types = array(
4369 - 'topic' => 'Forum Topics (bbPress)',
4370 - 'reply' => 'Forum Replies (bbPress)',
4371 - 'forum' => 'Forums (bbPress)',
4372 - 'wpforo_topic' => 'wpForo Topics',
4373 - 'wpforo_post' => 'wpForo Posts'
4374 - );
4375 -
4376 - foreach ($additional_types as $type_name => $type_label) {
4377 - if (post_type_exists($type_name) && !isset($post_type_options[$type_name])) {
4378 - $post_type_options[$type_name] = $type_label;
4379 - }
4380 - }
4381 -
4382 2123 return $post_type_options;
4383 2124 }
4384 -
4385 -/**
4386 - * Retrieves processed content from Pinecone API
4387 - */
4388 2125 public function mxchat_get_pinecone_processed_content($pinecone_options) {
2126 + // First check local cache for immediate updates
2127 + $cached_data = get_option('mxchat_pinecone_processed_cache', array());
2128 +
4389 2129 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
4390 2130 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
4391 -
2131 +
4392 2132 if (empty($api_key) || empty($host)) {
4393 - return array();
2133 + // Return only cached data if API credentials are missing
2134 + return $cached_data;
4394 2135 }
4395 -
2136 +
4396 2137 $pinecone_data = array();
4397 -
2138 +
4398 2139 try {
4399 - // Always get fresh data from Pinecone
4400 - $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
4401 -
4402 - // Method 2: Final fallback - try stats endpoint (if available)
2140 + // Method 1: Try to get vectors using cached vector IDs first
2141 + $cached_vector_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
2142 +
2143 + if (!empty($cached_vector_ids)) {
2144 + $pinecone_data = $this->mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $cached_vector_ids);
2145 + }
2146 +
2147 + // Method 2: If no cached IDs or fetch failed, use scanning approach
4403 2148 if (empty($pinecone_data)) {
2149 + $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
2150 + }
2151 +
2152 + // Method 3: Final fallback - try stats endpoint (if available)
2153 + if (empty($pinecone_data)) {
4404 2154 $stats_url = "https://{$host}/describe_index_stats";
4405 -
2155 +
4406 2156 $response = wp_remote_post($stats_url, array(
4407 2157 'headers' => array(
4408 2158 'Api-Key' => $api_key,
4409 2159 'Content-Type' => 'application/json'
@@ -4410,42 +2160,59 @@
4410 2160 ),
4411 2161 'body' => json_encode(array()),
4412 2162 'timeout' => 30
4413 2163 ));
4414 -
2164 +
4415 2165 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
4416 2166 $body = wp_remote_retrieve_body($response);
4417 2167 $stats_data = json_decode($body, true);
2168 +
2169 + // Log stats for debugging but don't rely on them for vector listing
2170 + //error_log('Pinecone index stats: ' . print_r($stats_data, true));
4418 2171 }
4419 2172 }
4420 -
2173 +
4421 2174 } catch (Exception $e) {
4422 - // Log error but return fresh data only
2175 + //error_log('Pinecone processed content exception: ' . $e->getMessage());
4423 2176 }
4424 -
4425 - return $pinecone_data;
2177 +
2178 + // Merge cached data with Pinecone data
2179 + // Cache takes priority for recent updates (within last 5 minutes)
2180 + $merged_data = $pinecone_data;
2181 +
2182 + foreach ($cached_data as $post_id => $cache_item) {
2183 + $cache_timestamp = $cache_item['timestamp'] ?? 0;
2184 + $time_diff = current_time('timestamp') - $cache_timestamp;
2185 +
2186 + // If cache item is recent (less than 5 minutes), prioritize it
2187 + if ($time_diff < 300) { // 5 minutes = 300 seconds
2188 + $merged_data[$post_id] = $cache_item;
2189 + } else {
2190 + // If not in Pinecone data and cache is old, keep cache but mark as potentially stale
2191 + if (!isset($merged_data[$post_id])) {
2192 + $merged_data[$post_id] = $cache_item;
2193 + }
2194 + }
2195 + }
2196 +
2197 + return $merged_data;
4426 2198 }
4427 2199 public function mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
4428 - //error_log('=== DEBUG: Starting mxchat_fetch_pinecone_vectors_by_ids ===');
4429 -
4430 2200 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
4431 2201 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
4432 -
2202 +
4433 2203 if (empty($api_key) || empty($host) || empty($vector_ids)) {
4434 - //error_log('DEBUG: Missing parameters for fetch by IDs');
4435 2204 return array();
4436 2205 }
4437 -
2206 +
4438 2207 try {
4439 2208 $fetch_url = "https://{$host}/vectors/fetch";
4440 - //error_log('DEBUG: Fetch URL: ' . $fetch_url);
4441 - //error_log('DEBUG: Fetching ' . count($vector_ids) . ' vector IDs');
4442 -
2209 +
4443 2210 // Pinecone fetch API allows fetching specific vectors by ID
4444 2211 $fetch_data = array(
4445 2212 'ids' => array_values($vector_ids)
4446 2213 );
4447 -
2214 +
4448 2215 $response = wp_remote_post($fetch_url, array(
4449 2216 'headers' => array(
4450 2217 'Api-Key' => $api_key,
4451 2218 'Content-Type' => 'application/json'
@@ -4452,45 +2219,39 @@
4452 2219 ),
4453 2220 'body' => json_encode($fetch_data),
4454 2221 'timeout' => 30
4455 2222 ));
4456 -
2223 +
4457 2224 if (is_wp_error($response)) {
4458 - //error_log('DEBUG: Fetch by IDs WP error: ' . $response->get_error_message());
2225 + //error_log('Pinecone fetch by IDs error: ' . $response->get_error_message());
4459 2226 return array();
4460 2227 }
4461 -
2228 +
4462 2229 $response_code = wp_remote_retrieve_response_code($response);
4463 - //error_log('DEBUG: Fetch response code: ' . $response_code);
4464 -
4465 2230 if ($response_code !== 200) {
4466 - $error_body = wp_remote_retrieve_body($response);
4467 - //error_log('DEBUG: Fetch failed with body: ' . $error_body);
2231 + //error_log('Pinecone fetch by IDs failed with code: ' . $response_code);
4468 2232 return array();
4469 2233 }
4470 -
2234 +
4471 2235 $body = wp_remote_retrieve_body($response);
4472 2236 $data = json_decode($body, true);
4473 2237
4474 - //error_log('DEBUG: Fetch response structure: ' . print_r(array_keys($data), true));
4475 -
4476 2238 if (!isset($data['vectors'])) {
4477 - //error_log('DEBUG: No vectors key in response');
4478 2239 return array();
4479 2240 }
4480 -
2241 +
4481 2242 $processed_data = array();
4482 -
2243 +
4483 2244 foreach ($data['vectors'] as $vector_id => $vector_data) {
4484 2245 $metadata = $vector_data['metadata'] ?? array();
4485 2246 $source_url = $metadata['source_url'] ?? '';
4486 -
2247 +
4487 2248 if (!empty($source_url)) {
4488 2249 $post_id = url_to_postid($source_url);
4489 2250 if ($post_id) {
4490 2251 $created_at = $metadata['created_at'] ?? '';
4491 - $processed_date = 'Recently';
4492 -
2252 + $processed_date = 'Recently'; // Default
2253 +
4493 2254 if (!empty($created_at)) {
4494 2255 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
4495 2256 if ($timestamp) {
4496 2257 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
@@ -4495,9 +2256,9 @@
4495 2256 if ($timestamp) {
4496 2257 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
4497 2258 }
4498 2259 }
4499 -
2260 +
4500 2261 $processed_data[$post_id] = array(
4501 2262 'db_id' => $vector_id,
4502 2263 'processed_date' => $processed_date,
4503 2264 'url' => $source_url,
@@ -4506,90 +2267,52 @@
4506 2267 );
4507 2268 }
4508 2269 }
4509 2270 }
4510 -
4511 - //error_log('DEBUG: Processed ' . count($processed_data) . ' vectors from fetch');
2271 +
4512 2272 return $processed_data;
4513 -
2273 +
4514 2274 } catch (Exception $e) {
4515 - //error_log('DEBUG: Exception in fetch_pinecone_vectors_by_ids: ' . $e->getMessage());
2275 + //error_log('Pinecone fetch by IDs exception: ' . $e->getMessage());
4516 2276 return array();
4517 2277 }
4518 2278 }
4519 -
4520 -/**
4521 - * Get embedding dimensions based on the selected model.
4522 - */
4523 -private function mxchat_get_embedding_dimensions() {
4524 - $options = get_option('mxchat_options', array());
4525 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4526 -
4527 - $model_dimensions = array(
4528 - 'text-embedding-ada-002' => 1536,
4529 - 'text-embedding-3-small' => 1536,
4530 - 'text-embedding-3-large' => 3072,
4531 - 'voyage-2' => 1024,
4532 - 'voyage-large-2' => 1536,
4533 - 'voyage-3-large' => 2048,
4534 - 'gemini-embedding-001' => 1536,
4535 - );
4536 -
4537 - if (strpos($selected_model, 'voyage-3-large') === 0) {
4538 - $custom_dimensions = $options['voyage_output_dimension'] ?? 2048;
4539 - return intval($custom_dimensions);
4540 - }
4541 -
4542 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4543 - $custom_dimensions = $options['gemini_output_dimension'] ?? 1536;
4544 - return intval($custom_dimensions);
4545 - }
4546 -
4547 - return $model_dimensions[$selected_model] ?? 1536;
4548 -}
4549 -
4550 -/**
4551 - * Scan Pinecone for processed content
4552 - */
4553 2279 public function mxchat_scan_pinecone_for_processed_content($pinecone_options) {
4554 2280 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
4555 2281 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
4556 -
2282 +
4557 2283 if (empty($api_key) || empty($host)) {
4558 2284 return array();
4559 2285 }
4560 -
2286 +
4561 2287 try {
4562 2288 // Use multiple random vectors to get better coverage
4563 2289 $all_matches = array();
4564 2290 $seen_ids = array();
4565 -
4566 - // Get correct dimensions for the configured embedding model
4567 - $dimensions = $this->mxchat_get_embedding_dimensions();
4568 -
2291 +
4569 2292 // Try 3 different random vectors to get better coverage
4570 2293 for ($i = 0; $i < 3; $i++) {
4571 2294 $query_url = "https://{$host}/query";
4572 -
2295 +
4573 2296 // Generate a random unit vector instead of zeros
4574 2297 $random_vector = array();
4575 - for ($j = 0; $j < $dimensions; $j++) {
4576 - $random_vector[] = (rand(-1000, 1000) / 1000.0);
2298 + for ($j = 0; $j < 1536; $j++) {
2299 + $random_vector[] = (rand(-1000, 1000) / 1000.0); // Random values between -1 and 1
4577 2300 }
4578 -
2301 +
4579 2302 // Normalize the vector to unit length
4580 2303 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
4581 2304 if ($magnitude > 0) {
4582 2305 $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
4583 2306 }
4584 -
2307 +
4585 2308 $query_data = array(
4586 2309 'includeMetadata' => true,
4587 2310 'includeValues' => false,
4588 - 'topK' => 10000,
2311 + 'topK' => 10000, // Get many results
4589 2312 'vector' => $random_vector
4590 2313 );
4591 -
2314 +
4592 2315 $response = wp_remote_post($query_url, array(
4593 2316 'headers' => array(
4594 2317 'Api-Key' => $api_key,
4595 2318 'Content-Type' => 'application/json'
@@ -4596,22 +2319,21 @@
4596 2319 ),
4597 2320 'body' => json_encode($query_data),
4598 2321 'timeout' => 30
4599 2322 ));
4600 -
2323 +
4601 2324 if (is_wp_error($response)) {
4602 2325 continue;
4603 2326 }
4604 -
2327 +
4605 2328 $response_code = wp_remote_retrieve_response_code($response);
4606 -
4607 2329 if ($response_code !== 200) {
4608 2330 continue;
4609 2331 }
4610 -
2332 +
4611 2333 $body = wp_remote_retrieve_body($response);
4612 2334 $data = json_decode($body, true);
4613 -
2335 +
4614 2336 if (isset($data['matches'])) {
4615 2337 foreach ($data['matches'] as $match) {
4616 2338 $match_id = $match['id'] ?? '';
4617 2339 if (!empty($match_id) && !isset($seen_ids[$match_id])) {
@@ -4620,30 +2342,24 @@
4620 2342 }
4621 2343 }
4622 2344 }
4623 2345 }
4624 -
4625 - // Convert matches to processed data format, grouping by URL to count chunks
2346 +
2347 + // Convert matches to processed data format
4626 2348 $processed_data = array();
4627 - $url_chunk_counts = array();
4628 -
2349 + $vector_ids_for_cache = array();
2350 +
4629 2351 foreach ($all_matches as $match) {
4630 2352 $metadata = $match['metadata'] ?? array();
4631 2353 $source_url = $metadata['source_url'] ?? '';
4632 2354 $match_id = $match['id'] ?? '';
4633 -
2355 +
4634 2356 if (!empty($source_url) && !empty($match_id)) {
4635 2357 $post_id = url_to_postid($source_url);
4636 2358 if ($post_id) {
4637 - // Count chunks per post_id
4638 - if (!isset($url_chunk_counts[$post_id])) {
4639 - $url_chunk_counts[$post_id] = 0;
4640 - }
4641 - $url_chunk_counts[$post_id]++;
4642 -
4643 2359 $created_at = $metadata['created_at'] ?? '';
4644 - $processed_date = 'Recently';
4645 -
2360 + $processed_date = 'Recently'; // Default
2361 +
4646 2362 if (!empty($created_at)) {
4647 2363 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
4648 2364 if ($timestamp) {
4649 2365 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
@@ -4648,323 +2364,242 @@
4648 2364 if ($timestamp) {
4649 2365 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
4650 2366 }
4651 2367 }
4652 -
4653 - // Only store if not already set, or update with newer timestamp
4654 - if (!isset($processed_data[$post_id]) ||
4655 - ($timestamp ?? 0) > ($processed_data[$post_id]['timestamp'] ?? 0)) {
4656 - $processed_data[$post_id] = array(
4657 - 'db_id' => $match_id,
4658 - 'processed_date' => $processed_date,
4659 - 'url' => $source_url,
4660 - 'source' => 'pinecone',
4661 - 'timestamp' => $timestamp ?? current_time('timestamp')
4662 - );
4663 - }
2368 +
2369 + $processed_data[$post_id] = array(
2370 + 'db_id' => $match_id,
2371 + 'processed_date' => $processed_date,
2372 + 'url' => $source_url,
2373 + 'source' => 'pinecone',
2374 + 'timestamp' => $timestamp ?? current_time('timestamp')
2375 + );
2376 +
2377 + $vector_ids_for_cache[] = $match_id;
4664 2378 }
4665 2379 }
4666 2380 }
4667 -
4668 - // Add chunk counts to processed data
4669 - foreach ($url_chunk_counts as $post_id => $chunk_count) {
4670 - if (isset($processed_data[$post_id])) {
4671 - $processed_data[$post_id]['chunk_count'] = $chunk_count;
4672 - }
2381 +
2382 + // Update the vector IDs cache for future use
2383 + if (!empty($vector_ids_for_cache)) {
2384 + update_option('mxchat_pinecone_vector_ids_cache', $vector_ids_for_cache);
4673 2385 }
4674 -
2386 +
4675 2387 return $processed_data;
4676 -
2388 +
4677 2389 } catch (Exception $e) {
2390 + //error_log('Pinecone scan exception: ' . $e->getMessage());
4678 2391 return array();
4679 2392 }
4680 -}
4681 -/**
4682 - * Generate embeddings from input text for MXChat with bot support
4683 - */
4684 -private function mxchat_generate_embedding($text, $bot_id = 'default') {
4685 - // Enable detailed logging for debugging
4686 - //error_log('[MXCHAT-EMBED] Starting embedding generation for bot: ' . $bot_id . '. Text length: ' . strlen($text) . ' bytes');
4687 - //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
2393 +}
4688 2394
4689 - // Get bot-specific options
4690 - $bot_options = $this->get_bot_options($bot_id);
4691 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
2395 + /**
2396 + * Generates embeddings from input text for MXChat
2397 + */
2398 + private function mxchat_generate_embedding($text) {
2399 + // Enable detailed logging for debugging
2400 + //error_log('[MXCHAT-EMBED] Starting embedding generation. Text length: ' . strlen($text) . ' bytes');
2401 + //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
4692 2402
4693 - // Opt-in: when the custom provider is selected for embeddings, index through
4694 - // the same custom endpoint the query path uses so stored vectors and query
4695 - // vectors share a model. Returns the vector array on success, or an error
4696 - // string on failure (this function's existing failure contract).
4697 - if (isset($options['custom_provider_for_embeddings']) && $options['custom_provider_for_embeddings'] === 'on') {
4698 - if (!class_exists('MxChat_Utils')) {
4699 - require_once dirname(__FILE__) . '/../includes/class-mxchat-utils.php';
4700 - }
4701 - return MxChat_Utils::generate_embedding_custom($text, $options);
4702 - }
2403 + $options = get_option('mxchat_options');
2404 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
2405 + //error_log('[MXCHAT-EMBED] Selected embedding model: ' . $selected_model);
4703 2406
4704 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4705 - //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model);
2407 + // Determine provider and endpoint
2408 + if (strpos($selected_model, 'voyage') === 0) {
2409 + $api_key = $options['voyage_api_key'] ?? '';
2410 + $endpoint = 'https://api.voyageai.com/v1/embeddings';
2411 + $provider_name = 'Voyage AI';
2412 + //error_log('[MXCHAT-EMBED] Using Voyage AI API');
2413 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2414 + $api_key = $options['gemini_api_key'] ?? '';
2415 + $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
2416 + $provider_name = 'Google Gemini';
2417 + //error_log('[MXCHAT-EMBED] Using Google Gemini API');
2418 + } else {
2419 + $api_key = $options['api_key'] ?? '';
2420 + $endpoint = 'https://api.openai.com/v1/embeddings';
2421 + $provider_name = 'OpenAI';
2422 + //error_log('[MXCHAT-EMBED] Using OpenAI API');
2423 + }
4706 2424
4707 - // Determine provider and endpoint
4708 - if (strpos($selected_model, 'voyage') === 0) {
4709 - $api_key = $options['voyage_api_key'] ?? '';
4710 - $endpoint = 'https://api.voyageai.com/v1/embeddings';
4711 - $provider_name = 'Voyage AI';
4712 - //error_log('[MXCHAT-EMBED] Using Voyage AI API for bot ' . $bot_id);
4713 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
4714 - $api_key = $options['gemini_api_key'] ?? '';
4715 - $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
4716 - $provider_name = 'Google Gemini';
4717 - //error_log('[MXCHAT-EMBED] Using Google Gemini API for bot ' . $bot_id);
4718 - } else {
4719 - $api_key = $options['api_key'] ?? '';
4720 - $endpoint = 'https://api.openai.com/v1/embeddings';
4721 - $provider_name = 'OpenAI';
4722 - //error_log('[MXCHAT-EMBED] Using OpenAI API for bot ' . $bot_id);
4723 - }
2425 + //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
4724 2426
4725 - //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
2427 + if (empty($api_key)) {
2428 + $error_message = sprintf('Missing %s API key. Please configure your API key in the MxChat settings.', $provider_name);
2429 + //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
2430 + return $error_message;
2431 + }
4726 2432
4727 - if (empty($api_key)) {
4728 - $error_message = sprintf('Missing %s API key for bot %s. Please configure your API key in the bot settings.', $provider_name, $bot_id);
4729 - //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
4730 - return $error_message;
4731 - }
2433 + // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
2434 + $estimated_tokens = ceil(str_word_count($text) / 0.75);
2435 + //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
4732 2436
4733 - // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
4734 - $estimated_tokens = ceil(str_word_count($text) / 0.75);
4735 - //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
2437 + if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
2438 + //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
2439 + // Consider truncating text here
2440 + }
4736 2441
4737 - if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
4738 - //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
4739 - // Consider truncating text here
4740 - }
2442 + // Prepare request body based on provider
2443 + if (strpos($selected_model, 'gemini-embedding') === 0) {
2444 + // Gemini API format
2445 + $request_body = array(
2446 + 'model' => 'models/' . $selected_model,
2447 + 'content' => array(
2448 + 'parts' => array(
2449 + array('text' => $text)
2450 + )
2451 + )
2452 + );
4741 2453
4742 - // Prepare request body based on provider
4743 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4744 - // Gemini API format
4745 - $request_body = array(
4746 - 'model' => 'models/' . $selected_model,
4747 - 'content' => array(
4748 - 'parts' => array(
4749 - array('text' => $text)
4750 - )
4751 - )
4752 - );
2454 + // Set output dimensionality to 1536 for consistency with other models
2455 + $request_body['outputDimensionality'] = 1536;
2456 + } else {
2457 + // OpenAI/Voyage API format
2458 + $request_body = array(
2459 + 'model' => $selected_model,
2460 + 'input' => $text
2461 + );
4753 2462
4754 - // Set output dimensionality to 1536 for consistency with other models
4755 - $request_body['outputDimensionality'] = 1536;
4756 - } else {
4757 - // OpenAI/Voyage API format
4758 - $request_body = array(
4759 - 'model' => $selected_model,
4760 - 'input' => $text
4761 - );
2463 + // Add output_dimension for voyage-3-large model
2464 + if ($selected_model === 'voyage-3-large') {
2465 + $request_body['output_dimension'] = 2048;
2466 + }
2467 + }
4762 2468
4763 - // Add output_dimension for voyage-3-large model
4764 - if ($selected_model === 'voyage-3-large') {
4765 - $request_body['output_dimension'] = 2048;
4766 - }
4767 - }
2469 + //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
4768 2470
4769 - //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
2471 + // Prepare headers based on provider
2472 + if (strpos($selected_model, 'gemini-embedding') === 0) {
2473 + // Gemini uses API key as query parameter
2474 + $endpoint .= '?key=' . $api_key;
2475 + $headers = array(
2476 + 'Content-Type' => 'application/json'
2477 + );
2478 + } else {
2479 + // OpenAI/Voyage use Bearer token
2480 + $headers = array(
2481 + 'Authorization' => 'Bearer ' . $api_key,
2482 + 'Content-Type' => 'application/json'
2483 + );
2484 + }
4770 2485
4771 - // Prepare headers based on provider
4772 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4773 - // Gemini uses API key as query parameter
4774 - $endpoint .= '?key=' . $api_key;
4775 - $headers = array(
4776 - 'Content-Type' => 'application/json'
4777 - );
4778 - } else {
4779 - // OpenAI/Voyage use Bearer token
4780 - $headers = array(
4781 - 'Authorization' => 'Bearer ' . $api_key,
4782 - 'Content-Type' => 'application/json'
4783 - );
4784 - }
2486 + // Make API request
2487 + //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
2488 + $response = wp_remote_post($endpoint, array(
2489 + 'body' => wp_json_encode($request_body),
2490 + 'headers' => $headers,
2491 + 'timeout' => 60 // Increased timeout for large inputs
2492 + ));
4785 2493
4786 - // Make API request
4787 - //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
4788 - $response = wp_remote_post($endpoint, array(
4789 - 'body' => wp_json_encode($request_body),
4790 - 'headers' => $headers,
4791 - 'timeout' => 60 // Increased timeout for large inputs
4792 - ));
2494 + // Handle wp_remote_post errors
2495 + if (is_wp_error($response)) {
2496 + $error_message = $response->get_error_message();
2497 + //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
2498 + return 'Connection error: ' . $error_message;
2499 + }
4793 2500
4794 - // Handle wp_remote_post errors
4795 - if (is_wp_error($response)) {
4796 - $error_message = $response->get_error_message();
4797 - //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
4798 - return 'Connection error: ' . $error_message;
4799 - }
2501 + // Get and check HTTP response code
2502 + $http_code = wp_remote_retrieve_response_code($response);
2503 + //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
4800 2504
4801 - // Get and check HTTP response code
4802 - $http_code = wp_remote_retrieve_response_code($response);
4803 - //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
2505 + if ($http_code !== 200) {
2506 + $error_body = wp_remote_retrieve_body($response);
2507 + //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
4804 2508
4805 - if ($http_code !== 200) {
4806 - $error_body = wp_remote_retrieve_body($response);
4807 - //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
2509 + // Try to parse error for more details
2510 + $error_json = json_decode($error_body, true);
2511 + if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
2512 + $error_type = $error_json['error']['type'] ?? 'unknown';
2513 + $error_message = $error_json['error']['message'] ?? 'No message';
2514 + //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
2515 + //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
4808 2516
4809 - // Try to parse error for more details
4810 - $error_json = json_decode($error_body, true);
4811 - if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
4812 - $error_type = $error_json['error']['type'] ?? 'unknown';
4813 - $error_message = $error_json['error']['message'] ?? 'No message';
4814 - //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
4815 - //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
2517 + // Customize error message for common API errors
2518 + if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
2519 + $error_message = sprintf('Invalid %s API key. Please check your API key in the MxChat settings.', $provider_name);
2520 + } elseif ($error_type === 'authentication_error') {
2521 + $error_message = sprintf('%s authentication failed. Please verify your API key in the MxChat settings.', $provider_name);
2522 + }
4816 2523
4817 - // Customize error message for common API errors
4818 - if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
4819 - $error_message = sprintf('Invalid %s API key for bot %s. Please check your API key in the bot settings.', $provider_name, $bot_id);
4820 - } elseif ($error_type === 'authentication_error') {
4821 - $error_message = sprintf('%s authentication failed for bot %s. Please verify your API key in the bot settings.', $provider_name, $bot_id);
4822 - }
2524 + //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
2525 + return $error_message;
2526 + }
4823 2527
4824 - //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
4825 - return $error_message;
4826 - }
2528 + $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding", $http_code);
2529 + //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
2530 + return $error_message;
2531 + }
4827 2532
4828 - $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding for bot %s", $http_code, $bot_id);
4829 - //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
4830 - return $error_message;
4831 - }
2533 + // Parse response body
2534 + $response_body = wp_remote_retrieve_body($response);
2535 + //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
4832 2536
4833 - // Parse response body
4834 - $response_body = wp_remote_retrieve_body($response);
4835 - //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
2537 + $response_data = json_decode($response_body, true);
4836 2538
4837 - $response_data = json_decode($response_body, true);
2539 + if (json_last_error() !== JSON_ERROR_NONE) {
2540 + $error = json_last_error_msg();
2541 + //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
2542 + //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
2543 + return "Failed to parse API response: $error";
2544 + }
4838 2545
4839 - if (json_last_error() !== JSON_ERROR_NONE) {
4840 - $error = json_last_error_msg();
4841 - //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
4842 - //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
4843 - return "Failed to parse API response: $error";
4844 - }
2546 + // Handle different response formats based on provider
2547 + if (strpos($selected_model, 'gemini-embedding') === 0) {
2548 + // Gemini API response format
2549 + if (isset($response_data['embedding']['values'])) {
2550 + $embedding_dimensions = count($response_data['embedding']['values']);
2551 + //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
4845 2552
4846 - // Handle different response formats based on provider
4847 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4848 - // Gemini API response format
4849 - if (isset($response_data['embedding']['values'])) {
4850 - $embedding_dimensions = count($response_data['embedding']['values']);
4851 - //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
2553 + // Check if embedding dimensions are as expected (should be 1536)
2554 + if ($embedding_dimensions !== 1536) {
2555 + //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
2556 + }
4852 2557
4853 - // Check if embedding dimensions are as expected (should be 1536)
4854 - if ($embedding_dimensions !== 1536) {
4855 - //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
4856 - }
2558 + return $response_data['embedding']['values'];
2559 + } else {
2560 + //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
2561 + //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
4857 2562
4858 - return $response_data['embedding']['values'];
4859 - } else {
4860 - //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
4861 - //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
2563 + if (isset($response_data['error'])) {
2564 + $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
2565 + //error_log('[MXCHAT-EMBED] ' . $error_message);
2566 + return $error_message;
2567 + }
4862 2568
4863 - if (isset($response_data['error'])) {
4864 - $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
4865 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4866 - return $error_message;
4867 - }
2569 + $error_message = "Invalid Gemini API response format: No embedding found";
2570 + //error_log('[MXCHAT-EMBED] ' . $error_message);
2571 + return $error_message;
2572 + }
2573 + } else {
2574 + // OpenAI/Voyage API response format
2575 + if (isset($response_data['data'][0]['embedding'])) {
2576 + $embedding_dimensions = count($response_data['data'][0]['embedding']);
2577 + //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
4868 2578
4869 - $error_message = "Invalid Gemini API response format: No embedding found";
4870 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4871 - return $error_message;
4872 - }
4873 - } else {
4874 - // OpenAI/Voyage API response format
4875 - if (isset($response_data['data'][0]['embedding'])) {
4876 - $embedding_dimensions = count($response_data['data'][0]['embedding']);
4877 - //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
2579 + // Check if embedding dimensions are as expected
2580 + if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
2581 + ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
2582 + //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
2583 + }
4878 2584
4879 - // Check if embedding dimensions are as expected
4880 - if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
4881 - ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
4882 - //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
4883 - }
2585 + return $response_data['data'][0]['embedding'];
2586 + } else {
2587 + //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
2588 + //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
4884 2589
4885 - return $response_data['data'][0]['embedding'];
4886 - } else {
4887 - //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
4888 - //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
2590 + if (isset($response_data['error'])) {
2591 + $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
2592 + //error_log('[MXCHAT-EMBED] ' . $error_message);
2593 + return $error_message;
2594 + }
4889 2595
4890 - if (isset($response_data['error'])) {
4891 - $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
4892 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4893 - return $error_message;
4894 - }
4895 -
4896 - $error_message = "Invalid API response format: No embedding found";
4897 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4898 - return $error_message;
4899 - }
4900 - }
4901 -}
4902 -
4903 -/**
4904 - * Get bot-specific options for multi-bot functionality
4905 - * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
4906 - */
4907 -private function get_bot_options($bot_id = 'default') {
4908 - //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
4909 -
4910 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
4911 - //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
4912 - return array();
4913 - }
4914 -
4915 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
4916 -
4917 - if (!empty($bot_options)) {
4918 - //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
4919 - if (isset($bot_options['similarity_threshold'])) {
4920 - //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
4921 - }
4922 - }
4923 -
4924 - return is_array($bot_options) ? $bot_options : array();
4925 -}
4926 -
4927 -/**
4928 - * Get bot-specific Pinecone configuration
4929 - * Used in the knowledge retrieval functions
4930 - */
4931 -// Also add debugging to your get_bot_pinecone_config function
4932 -private function get_bot_pinecone_config($bot_id = 'default') {
4933 - //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
4934 -
4935 - // If default bot or multi-bot add-on not active, use default Pinecone config
4936 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
4937 - //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
4938 - $addon_options = get_option('mxchat_pinecone_addon_options', array());
4939 - $config = array(
4940 - 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
4941 - 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
4942 - 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
4943 - 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
4944 - );
4945 - //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
4946 - return $config;
4947 - }
4948 -
4949 - //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
4950 -
4951 - // Hook for multi-bot add-on to provide bot-specific Pinecone config
4952 - $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
4953 -
4954 - if (!empty($bot_pinecone_config)) {
4955 - //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
4956 - //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
4957 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
4958 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
4959 - } else {
4960 - //error_log("MXCHAT DEBUG: Filter returned empty config!");
4961 - }
4962 -
4963 - return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
4964 -}
4965 -
4966 -
2596 + $error_message = "Invalid API response format: No embedding found";
2597 + //error_log('[MXCHAT-EMBED] ' . $error_message);
2598 + return $error_message;
2599 + }
2600 + }
2601 + }
4967 2602 public function mxchat_ajax_dismiss_completed_status() {
4968 2603 try {
4969 2604 // Verify the request
4970 2605 check_ajax_referer('mxchat_status_nonce', 'nonce');
@@ -5083,10 +2718,10 @@
5083 2718
5084 2719 $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
5085 2720 $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
5086 2721
5087 - // Add completion summary if available AND it's an array
5088 - if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
2722 + // Add completion summary if available
2723 + if (isset($status['completion_summary'])) {
5089 2724 $summary = $status['completion_summary'];
5090 2725 $html .= '<div class="mxchat-completion-summary">';
5091 2726 $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
5092 2727 $html .= '<p><strong>' . esc_html__('Total Pages:', 'mxchat') . '</strong> ' . esc_html($summary['total_pages']) . '</p>';
@@ -5095,10 +2730,10 @@
5095 2730 $html .= '<p><strong>' . esc_html__('Completed:', 'mxchat') . '</strong> ' . esc_html($summary['completion_time']) . '</p>';
5096 2731 $html .= '</div>';
5097 2732 }
5098 2733
5099 - // Add failed pages list if any AND it's an array
5100 - if (isset($status['failed_pages_list']) && is_array($status['failed_pages_list']) && !empty($status['failed_pages_list'])) {
2734 + // Add failed pages list if any
2735 + if (!empty($status['failed_pages_list'])) {
5101 2736 $html .= $this->mxchat_render_failed_pages_list($status['failed_pages_list']);
5102 2737 }
5103 2738
5104 2739 // Add error message if any
@@ -5112,8 +2747,9 @@
5112 2747 $html .= '</div>'; // End card
5113 2748
5114 2749 return $html;
5115 2750 }
2751 +
5116 2752 /**
5117 2753 * Render sitemap status card HTML
5118 2754 */
5119 2755 private function mxchat_render_sitemap_status_card($status, $sitemap_url) {
@@ -5169,10 +2805,10 @@
5169 2805
5170 2806 $html .= '<p><strong>' . esc_html__('Status:', 'mxchat') . '</strong> ' . esc_html(ucfirst($status['status'])) . '</p>';
5171 2807 $html .= '<p><strong>' . esc_html__('Last update:', 'mxchat') . '</strong> ' . esc_html($status['last_update']) . '</p>';
5172 2808
5173 - // Add completion summary if available AND it's an array
5174 - if (isset($status['completion_summary']) && is_array($status['completion_summary']) && !empty($status['completion_summary'])) {
2809 + // Add completion summary if available
2810 + if (isset($status['completion_summary'])) {
5175 2811 $summary = $status['completion_summary'];
5176 2812 $html .= '<div class="mxchat-completion-summary">';
5177 2813 $html .= '<h5>' . esc_html__('Processing Summary', 'mxchat') . '</h5>';
5178 2814 $html .= '<p><strong>' . esc_html__('Total URLs:', 'mxchat') . '</strong> ' . esc_html($summary['total_urls']) . '</p>';
@@ -5201,16 +2837,13 @@
5201 2837 $html .= '</div>'; // End card
5202 2838
5203 2839 return $html;
5204 2840 }
5205 -
5206 -
5207 2841 /**
5208 2842 * Render failed pages list
5209 2843 */
5210 2844 private function mxchat_render_failed_pages_list($failed_pages_list) {
5211 - // Validate that $failed_pages_list is an array and not empty
5212 - if (!is_array($failed_pages_list) || empty($failed_pages_list)) {
2845 + if (empty($failed_pages_list)) {
5213 2846 return '';
5214 2847 }
5215 2848
5216 2849 $html = '<div class="mxchat-error-notice">';
@@ -5235,13 +2868,8 @@
5235 2868 return ($b['time'] ?? 0) - ($a['time'] ?? 0);
5236 2869 });
5237 2870
5238 2871 foreach ($sorted_failed_pages as $item) {
5239 - // Ensure $item is an array before accessing its elements
5240 - if (!is_array($item)) {
5241 - continue;
5242 - }
5243 -
5244 2872 $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
5245 2873 $html .= '<tr>';
5246 2874 $html .= '<td>' . esc_html__('Page', 'mxchat') . ' ' . esc_html($item['page'] ?? 'Unknown') . '</td>';
5247 2875 $html .= '<td style="word-break: break-word;">' . esc_html($item['error'] ?? 'Unknown error') . '</td>';
@@ -5259,10 +2887,9 @@
5259 2887 /**
5260 2888 * Render failed URLs list
5261 2889 */
5262 2890 private function mxchat_render_failed_urls_list($failed_urls_list) {
5263 - // Validate that $failed_urls_list is an array and not empty
5264 - if (!is_array($failed_urls_list) || empty($failed_urls_list)) {
2891 + if (empty($failed_urls_list)) {
5265 2892 return '';
5266 2893 }
5267 2894
5268 2895 $html = '<div class="mxchat-failed-urls-container">';
@@ -5289,13 +2916,8 @@
5289 2916 // Show up to 50 failed URLs
5290 2917 $display_urls = array_slice($sorted_failed_urls, 0, 50);
5291 2918
5292 2919 foreach ($display_urls as $item) {
5293 - // Ensure $item is an array before accessing its elements
5294 - if (!is_array($item)) {
5295 - continue;
5296 - }
5297 -
5298 2920 $url = $item['url'] ?? '';
5299 2921 $time_ago = isset($item['time']) ? human_time_diff($item['time'], current_time('timestamp')) . ' ' . esc_html__('ago', 'mxchat') : 'Unknown';
5300 2922
5301 2923 // Truncate URL for display
@@ -5327,564 +2949,18 @@
5327 2949
5328 2950 return $html;
5329 2951 }
5330 2952
5331 -/**
5332 - * Get all ACF fields for a specific post, excluding any fields the user has disabled
5333 - */
5334 -public function mxchat_get_acf_fields_for_post($post_id) {
5335 - if (!function_exists('get_fields')) {
5336 - return array();
5337 - }
5338 -
5339 - $fields = get_fields($post_id);
5340 - if (!$fields || !is_array($fields)) {
5341 - return array();
5342 - }
5343 -
5344 - // Get excluded fields from settings
5345 - $excluded_fields = get_option('mxchat_acf_excluded_fields', array());
5346 - if (!empty($excluded_fields) && is_array($excluded_fields)) {
5347 - foreach ($excluded_fields as $excluded_field) {
5348 - if (isset($fields[$excluded_field])) {
5349 - unset($fields[$excluded_field]);
5350 - }
5351 - }
5352 - }
5353 -
5354 - return $fields;
5355 -}
5356 -
5357 -/**
5358 - * Get all registered ACF field groups and their fields for the settings UI
5359 - */
5360 -public function mxchat_get_all_acf_fields() {
5361 - if (!function_exists('acf_get_field_groups') || !function_exists('acf_get_fields')) {
5362 - return array();
5363 - }
5364 -
5365 - $all_fields = array();
5366 - $field_groups = acf_get_field_groups();
5367 -
5368 - if (!empty($field_groups)) {
5369 - foreach ($field_groups as $group) {
5370 - $group_fields = acf_get_fields($group['key']);
5371 - if (!empty($group_fields)) {
5372 - $all_fields[$group['title']] = array();
5373 - foreach ($group_fields as $field) {
5374 - $all_fields[$group['title']][] = array(
5375 - 'name' => $field['name'],
5376 - 'label' => $field['label'],
5377 - 'type' => $field['type']
5378 - );
5379 - }
5380 - }
5381 - }
5382 - }
5383 -
5384 - return $all_fields;
5385 -}
5386 -
5387 -/**
5388 - * Get whitelisted custom post meta for a given post
5389 - * This allows non-ACF meta fields (like OptionTree, theme meta boxes) to be included in embeddings
5390 - */
5391 -public function mxchat_get_whitelisted_post_meta($post_id) {
5392 - $whitelist = get_option('mxchat_custom_meta_whitelist', '');
5393 -
5394 - if (empty($whitelist)) {
5395 - return array();
5396 - }
5397 -
5398 - // Parse the whitelist - one meta key per line
5399 - $meta_keys = array_filter(array_map('trim', explode("\n", $whitelist)));
5400 -
5401 - if (empty($meta_keys)) {
5402 - return array();
5403 - }
5404 -
5405 - $result = array();
5406 -
5407 - foreach ($meta_keys as $key) {
5408 - // Skip empty keys
5409 - if (empty($key)) {
5410 - continue;
5411 - }
5412 -
5413 - $value = get_post_meta($post_id, $key, true);
5414 -
5415 - // Only include non-empty string values
5416 - if (!empty($value) && is_string($value)) {
5417 - $result[$key] = $value;
5418 - } elseif (!empty($value) && is_array($value)) {
5419 - // Handle array values by joining them
5420 - $flat_value = $this->mxchat_flatten_meta_array($value);
5421 - if (!empty($flat_value)) {
5422 - $result[$key] = $flat_value;
5423 - }
5424 - }
5425 - }
5426 -
5427 - return $result;
5428 -}
5429 -
5430 -/**
5431 - * Flatten array meta values into a readable string
5432 - */
5433 -private function mxchat_flatten_meta_array($array, $depth = 0) {
5434 - if ($depth > 3) {
5435 - return ''; // Prevent infinite recursion
5436 - }
5437 -
5438 - $parts = array();
5439 -
5440 - foreach ($array as $key => $value) {
5441 - if (is_string($value) && !empty($value)) {
5442 - $parts[] = $value;
5443 - } elseif (is_array($value)) {
5444 - $nested = $this->mxchat_flatten_meta_array($value, $depth + 1);
5445 - if (!empty($nested)) {
5446 - $parts[] = $nested;
5447 - }
5448 - }
5449 - }
5450 -
5451 - return implode(', ', $parts);
5452 -}
5453 -
5454 -/**
5455 - * Format ACF field values for content extraction
5456 - */
5457 -public function mxchat_format_acf_field_value($value, $field_name = '', $post_id = 0) {
5458 - if (empty($value)) {
5459 - return '';
5460 - }
5461 -
5462 - // Handle WP_Post objects first (THIS IS THE KEY FIX)
5463 - if ($value instanceof WP_Post) {
5464 - return $value->post_title ?: '';
5465 - }
5466 -
5467 - // Handle other WP objects
5468 - if (is_object($value)) {
5469 - if (isset($value->post_title)) {
5470 - return $value->post_title;
5471 - } elseif (isset($value->display_name)) {
5472 - return $value->display_name;
5473 - } elseif (isset($value->name)) {
5474 - return $value->name;
5475 - } elseif (method_exists($value, '__toString')) {
5476 - try {
5477 - return (string) $value;
5478 - } catch (Exception $e) {
5479 - return '';
5480 - }
5481 - }
5482 - // For any other objects, return empty string
5483 - return '';
5484 - }
5485 -
5486 - // Handle different ACF field types
5487 - if (is_array($value)) {
5488 - // Check if it's an image/file field
5489 - if (isset($value['url'])) {
5490 - // Image field - return alt text, title, or caption
5491 - if (!empty($value['alt'])) {
5492 - return $value['alt'];
5493 - } elseif (!empty($value['title'])) {
5494 - return $value['title'];
5495 - } elseif (!empty($value['caption'])) {
5496 - return $value['caption'];
5497 - } else {
5498 - return ''; // Don't include just the URL
5499 - }
5500 - }
5501 -
5502 - // Check if it's a post object or relationship field
5503 - if (isset($value['post_title'])) {
5504 - return $value['post_title'];
5505 - }
5506 -
5507 - // Check if it's a user field
5508 - if (isset($value['display_name'])) {
5509 - return $value['display_name'];
5510 - }
5511 -
5512 - // Check if it's a taxonomy term
5513 - if (isset($value['name']) && isset($value['taxonomy'])) {
5514 - return $value['name'];
5515 - }
5516 -
5517 - // Check if it's a select field with label
5518 - if (isset($value['label'])) {
5519 - return $value['label'];
5520 - }
5521 -
5522 - // Check for repeater field or flexible content
5523 - if (is_numeric(key($value))) {
5524 - $sub_values = array();
5525 - foreach ($value as $sub_item) {
5526 - if (is_array($sub_item)) {
5527 - // For repeater/flexible content, extract text values
5528 - $sub_text = $this->mxchat_extract_text_from_acf_array($sub_item);
5529 - if (!empty($sub_text)) {
5530 - $sub_values[] = $sub_text;
5531 - }
5532 - } elseif ($sub_item instanceof WP_Post) {
5533 - // Handle WP_Post objects in arrays
5534 - $sub_values[] = $sub_item->post_title ?: '';
5535 - } else {
5536 - $sub_values[] = (string) $sub_item;
5537 - }
5538 - }
5539 - return implode(', ', array_filter($sub_values));
5540 - }
5541 -
5542 - // For other arrays, try to extract meaningful text
5543 - $text_values = array();
5544 - foreach ($value as $key => $val) {
5545 - if (is_string($val) && !empty(trim($val))) {
5546 - $text_values[] = trim($val);
5547 - } elseif ($val instanceof WP_Post) {
5548 - // Handle WP_Post objects in associative arrays
5549 - $text_values[] = $val->post_title ?: '';
5550 - } elseif (is_array($val) && isset($val['post_title'])) {
5551 - $text_values[] = $val['post_title'];
5552 - } elseif (is_array($val) && isset($val['name'])) {
5553 - $text_values[] = $val['name'];
5554 - }
5555 - }
5556 -
5557 - return implode(', ', array_filter($text_values));
5558 - }
5559 -
5560 - // Handle boolean values
5561 - if (is_bool($value)) {
5562 - return $value ? 'Yes' : 'No';
5563 - }
5564 -
5565 - // Handle numeric values
5566 - if (is_numeric($value)) {
5567 - return (string) $value;
5568 - }
5569 -
5570 - // Handle string values
5571 - if (is_string($value)) {
5572 - return trim($value);
5573 - }
5574 -
5575 - // For anything else that we can't handle, return empty string
5576 - // This prevents the "Object could not be converted to string" error
5577 - return '';
5578 -}
5579 -
5580 -/**
5581 - * Extract text from complex ACF array structures
5582 - */
5583 -private function mxchat_extract_text_from_acf_array($array) {
5584 - if (!is_array($array)) {
5585 - return '';
5586 - }
5587 -
5588 - $text_parts = array();
5589 -
5590 - foreach ($array as $key => $value) {
5591 - if (is_string($value) && !empty(trim($value))) {
5592 - // Skip keys that are likely to be IDs or technical values
5593 - if (!is_numeric($value) || strlen($value) > 10) {
5594 - $text_parts[] = trim($value);
5595 - }
5596 - } elseif ($value instanceof WP_Post) {
5597 - // Handle WP_Post objects
5598 - $text_parts[] = $value->post_title ?: '';
5599 - } elseif (is_array($value)) {
5600 - if (isset($value['post_title'])) {
5601 - $text_parts[] = $value['post_title'];
5602 - } elseif (isset($value['name'])) {
5603 - $text_parts[] = $value['name'];
5604 - } elseif (isset($value['label'])) {
5605 - $text_parts[] = $value['label'];
5606 - }
5607 - } elseif (is_object($value)) {
5608 - // Handle other objects safely
5609 - if (isset($value->post_title)) {
5610 - $text_parts[] = $value->post_title;
5611 - } elseif (isset($value->name)) {
5612 - $text_parts[] = $value->name;
5613 - } elseif (isset($value->display_name)) {
5614 - $text_parts[] = $value->display_name;
5615 - }
5616 - }
5617 - }
5618 -
5619 - return implode(', ', array_filter($text_parts));
5620 -}
5621 -
5622 -/**
5623 - * Walk an ACF field value tree and collect attachment IDs for any value that
5624 - * resolves to a PDF in the WordPress media library. Handles the three shapes
5625 - * ACF returns for File/Image/URL fields (array with ID+url, integer attachment ID,
5626 - * plain URL string), and recurses through repeater/group/flexible content.
5627 - *
5628 - * @param mixed $value The ACF field value (any depth)
5629 - * @param array $out Accumulator (passed by reference) for attachment IDs
5630 - * @param int $depth Recursion guard
5631 - */
5632 -private function mxchat_collect_pdf_attachment_ids_from_acf_value($value, &$out, $depth = 0) {
5633 - if ($depth > 6) {
5634 - return; // prevent runaway recursion on circular/very-deep structures
5635 - }
5636 -
5637 - if (empty($value)) {
5638 - return;
5639 - }
5640 -
5641 - // Array shapes: ACF File/Image return value=array; repeaters/groups are arrays of arrays
5642 - if (is_array($value)) {
5643 - // Direct File/Image-style array (has 'url' and usually 'ID' + 'mime_type')
5644 - $looks_like_attachment = isset($value['url']) || isset($value['ID']) || isset($value['id']);
5645 - if ($looks_like_attachment) {
5646 - $att_id = 0;
5647 - if (!empty($value['ID']) && is_numeric($value['ID'])) {
5648 - $att_id = (int) $value['ID'];
5649 - } elseif (!empty($value['id']) && is_numeric($value['id'])) {
5650 - $att_id = (int) $value['id'];
5651 - } elseif (!empty($value['url']) && is_string($value['url'])) {
5652 - $att_id = (int) attachment_url_to_postid($value['url']);
5653 - }
5654 -
5655 - $is_pdf = false;
5656 - if (!empty($value['mime_type']) && $value['mime_type'] === 'application/pdf') {
5657 - $is_pdf = true;
5658 - } elseif (!empty($value['subtype']) && strtolower((string) $value['subtype']) === 'pdf') {
5659 - $is_pdf = true;
5660 - } elseif (!empty($value['url']) && is_string($value['url']) && $this->mxchat_url_looks_like_pdf($value['url'])) {
5661 - $is_pdf = true;
5662 - } elseif ($att_id && get_post_mime_type($att_id) === 'application/pdf') {
5663 - $is_pdf = true;
5664 - }
5665 -
5666 - if ($is_pdf && $att_id && get_post_mime_type($att_id) === 'application/pdf') {
5667 - $out[] = $att_id;
5668 - }
5669 - // An array node that represents one attachment doesn't contain other
5670 - // attachments inside it — done with this branch.
5671 - return;
5672 - }
5673 -
5674 - // Recurse: repeater rows, flexible-content layouts, groups, etc.
5675 - foreach ($value as $sub) {
5676 - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($sub, $out, $depth + 1);
5677 - }
5678 - return;
5679 - }
5680 -
5681 - // Plain numeric attachment ID (ACF File field set to "Return: ID")
5682 - if (is_numeric($value)) {
5683 - $att_id = (int) $value;
5684 - if ($att_id > 0 && get_post_mime_type($att_id) === 'application/pdf') {
5685 - $out[] = $att_id;
5686 - }
5687 - return;
5688 - }
5689 -
5690 - // Plain string — URL pointing at a PDF (ACF File field set to "Return: URL", or a custom URL/text field)
5691 - if (is_string($value)) {
5692 - $trimmed = trim($value);
5693 - if ($trimmed !== '' && $this->mxchat_url_looks_like_pdf($trimmed)) {
5694 - $att_id = (int) attachment_url_to_postid($trimmed);
5695 - if ($att_id > 0 && get_post_mime_type($att_id) === 'application/pdf') {
5696 - $out[] = $att_id;
5697 - }
5698 - }
5699 - return;
5700 - }
5701 -}
5702 -
5703 -/**
5704 - * Heuristic: does this URL/string look like a PDF reference?
5705 - * Tolerates query strings and fragments (#page=2).
5706 - */
5707 -private function mxchat_url_looks_like_pdf($url) {
5708 - if (!is_string($url) || $url === '') {
5709 - return false;
5710 - }
5711 - // Strip query + fragment before checking extension
5712 - $path = preg_replace('/[?#].*$/', '', $url);
5713 - return (bool) preg_match('/\.pdf$/i', $path);
5714 -}
5715 -
5716 -/**
5717 - * Extract text from a PDF attachment by ID using the bundled Smalot parser.
5718 - * Reads the file directly from disk via get_attached_file (no HTTP fetch).
5719 - * Result is cached on the attachment as post_meta keyed by file mtime so we
5720 - * only parse the same PDF once unless the file changes on disk.
5721 - *
5722 - * @param int $attachment_id
5723 - * @return string Extracted plain text, or '' on failure.
5724 - */
5725 -private function mxchat_extract_pdf_text_by_attachment_id($attachment_id) {
5726 - $attachment_id = (int) $attachment_id;
5727 - if ($attachment_id <= 0) {
5728 - return '';
5729 - }
5730 - if (get_post_mime_type($attachment_id) !== 'application/pdf') {
5731 - return '';
5732 - }
5733 -
5734 - $pdf_path = get_attached_file($attachment_id);
5735 - if (empty($pdf_path) || !file_exists($pdf_path) || !is_readable($pdf_path)) {
5736 - return '';
5737 - }
5738 -
5739 - // Raw-file size cap. Parsing very large PDFs can OOM the request; skip with a log entry
5740 - // and let the rest of the ACF content land in the KB. Filterable for users who need it bigger.
5741 - $default_max_bytes = 25 * 1024 * 1024;
5742 - $max_bytes = (int) apply_filters('mxchat_acf_pdf_max_bytes', $default_max_bytes, $attachment_id, $pdf_path);
5743 - if ($max_bytes > 0) {
5744 - $file_size = @filesize($pdf_path);
5745 - if ($file_size !== false && $file_size > $max_bytes) {
5746 - error_log(sprintf(
5747 - '[mxchat] ACF PDF skipped (over size cap): attachment %d "%s" %d bytes > cap %d',
5748 - $attachment_id,
5749 - basename($pdf_path),
5750 - $file_size,
5751 - $max_bytes
5752 - ));
5753 - return '';
5754 - }
5755 - }
5756 -
5757 - $mtime = @filemtime($pdf_path);
5758 - $cache_meta_key = '_mxchat_acf_pdf_text_v1';
5759 - $cached = get_post_meta($attachment_id, $cache_meta_key, true);
5760 - if (is_array($cached) && isset($cached['mtime'], $cached['text']) && (int) $cached['mtime'] === (int) $mtime) {
5761 - return (string) $cached['text'];
5762 - }
5763 -
5764 - $text = '';
5765 - try {
5766 - if (function_exists('mxchat_load_pdf_parser')) {
5767 - mxchat_load_pdf_parser();
5768 - }
5769 - if (!class_exists('\\Smalot\\PdfParser\\Parser')) {
5770 - return '';
5771 - }
5772 - $parser = new \Smalot\PdfParser\Parser();
5773 - $pdf = $parser->parseFile($pdf_path);
5774 - $pages = $pdf->getPages();
5775 - $page_texts = array();
5776 - foreach ($pages as $page) {
5777 - $page_text = '';
5778 - try {
5779 - $page_text = $page->getText();
5780 - } catch (\Exception $e) {
5781 - $page_text = '';
5782 - }
5783 - if (!empty($page_text)) {
5784 - $page_texts[] = $page_text;
5785 - }
5786 - }
5787 - $text = trim(implode("\n\n", $page_texts));
5788 - } catch (\Exception $e) {
5789 - error_log('[mxchat] ACF PDF extraction failed for attachment ' . $attachment_id . ': ' . $e->getMessage());
5790 - return '';
5791 - } catch (\Throwable $e) {
5792 - error_log('[mxchat] ACF PDF extraction error for attachment ' . $attachment_id . ': ' . $e->getMessage());
5793 - return '';
5794 - }
5795 -
5796 - // Cap per-PDF text to avoid blowing up the embedding payload on enormous PDFs.
5797 - // The chunker downstream will still split this into multiple vectors.
5798 - $max_len = (int) apply_filters('mxchat_acf_pdf_text_max_length', 50000);
5799 - if ($max_len > 0 && strlen($text) > $max_len) {
5800 - $text = substr($text, 0, $max_len);
5801 - }
5802 -
5803 - update_post_meta($attachment_id, $cache_meta_key, array(
5804 - 'mtime' => (int) $mtime,
5805 - 'text' => $text,
5806 - ));
5807 -
5808 - return $text;
5809 -}
5810 -
5811 -/**
5812 - * Handle ACF save - fires after ACF fields are saved
5813 - * This ensures ACF field data is available when syncing to knowledge base
5814 - */
5815 -public function mxchat_handle_acf_save($post_id) {
5816 - // Skip if not a valid post
5817 - if (!$post_id || $post_id === 'options') {
5818 - return;
5819 - }
5820 -
5821 - // Skip autosaves and revisions
2953 +public function mxchat_handle_post_update($post_id, $post, $update) {
2954 + // Basic validation checks
5822 2955 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
5823 2956 return;
5824 2957 }
5825 -
5826 - $post = get_post($post_id);
5827 - if (!$post) {
5828 - return;
5829 - }
5830 -
5831 - $post_type = $post->post_type;
5832 -
5833 - // Check if sync is enabled for this post type
5834 - $should_sync = false;
5835 -
5836 - if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
5837 - $should_sync = true;
5838 - } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
5839 - $should_sync = true;
5840 - } else if ($post_type === 'product' && class_exists('WooCommerce')) {
5841 - // WooCommerce products - check if WooCommerce integration is enabled
5842 - $options = get_option('mxchat_options', array());
5843 - if (isset($options['enable_woocommerce_integration']) &&
5844 - ($options['enable_woocommerce_integration'] === '1' || $options['enable_woocommerce_integration'] === 'on')) {
5845 - $should_sync = true;
5846 - }
5847 - } else {
5848 - // Check custom post types
5849 - $option_name = 'mxchat_auto_sync_' . $post_type;
5850 - if (get_option($option_name) === '1') {
5851 - $should_sync = true;
5852 - }
5853 - }
5854 -
5855 - if (!$should_sync) {
5856 - return;
5857 - }
5858 -
5859 - // Only process published posts
2958 +
2959 + // Only process published content
5860 2960 if ($post->post_status !== 'publish') {
5861 2961 return;
5862 2962 }
5863 -
5864 - // Check if this post has any ACF fields - if not, no need to re-sync
5865 - $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
5866 - if (empty($acf_fields)) {
5867 - return;
5868 - }
5869 -
5870 - // Use a transient to prevent duplicate processing (post_updated may have already run)
5871 - $transient_key = 'mxchat_acf_synced_' . $post_id;
5872 - if (get_transient($transient_key)) {
5873 - return;
5874 - }
5875 - set_transient($transient_key, true, 60); // Prevent re-processing for 60 seconds
5876 -
5877 - // Re-run the sync with ACF data now available
5878 - // We pass $update=true since this is effectively an update with ACF data
5879 - $this->mxchat_handle_post_update($post_id, $post, true);
5880 -}
5881 -
5882 -public function mxchat_handle_post_update($post_id, $post, $update) {
5883 - // Basic validation checks
5884 - if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
5885 - return;
5886 - }
5887 2963
5888 2964 $post_type = $post->post_type;
5889 2965
5890 2966 // Check if sync is enabled for this post type
@@ -5906,442 +2982,80 @@
5906 2982 if (!$should_sync) {
5907 2983 return;
5908 2984 }
5909 2985
5910 - // Check if we have stored the previous status and URL in our transients
5911 - $previous_status_key = 'mxchat_prev_status_' . $post_id;
5912 - $previous_status = get_transient($previous_status_key);
2986 + // Get content with proper formatting (matching ajax_mxchat_process_selected_content)
2987 + $title = get_the_title($post_id);
2988 + $content = get_post_field('post_content', $post_id);
5913 2989
5914 - $previous_url_key = 'mxchat_prev_url_' . $post_id;
5915 - $previous_url = get_transient($previous_url_key);
2990 + // Apply WordPress content filters to get properly formatted content
2991 + $content = apply_filters('the_content', $content);
5916 2992
5917 - // If the post was previously published but is now not published, remove from knowledge base
5918 - if ($previous_status === 'publish' && $post->post_status !== 'publish') {
5919 - // Use the stored URL from when it was published, or fall back to current permalink
5920 - $source_url = $previous_url ?: get_permalink($post_id);
5921 -
5922 - // mxchat_handle_status_transition already deleted for this post earlier in this
5923 - // request (it fires first inside wp_insert_post); skip the redundant round-trip.
5924 - if ($source_url && empty($this->transition_deleted_posts[$post_id])) {
5925 - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks)
5926 - MxChat_Utils::delete_chunks_for_url($source_url, 'default');
2993 + // Strip tags but preserve structure
2994 + $content = wp_strip_all_tags($content);
2995 +
2996 + // Combine title and content
2997 + $final_content = $title . "\n\n" . $content;
2998 +
2999 + // For custom post types like job_listing, include additional fields
3000 + if ($post_type === 'job_listing') {
3001 + // Add job-specific meta if available
3002 + $job_location = get_post_meta($post_id, '_job_location', true);
3003 + if (!empty($job_location)) {
3004 + $final_content .= "\n\nLocation: " . $job_location;
5927 3005 }
5928 -
5929 - // Clean up the transients and exit early
5930 - delete_transient($previous_status_key);
5931 - delete_transient($previous_url_key);
5932 - return;
5933 - }
5934 -
5935 - // Slug/permalink rename while still published: delete the old vectors before upserting new ones.
5936 - // Without this, md5(old_url) vectors (base + chunks) would be orphaned under the stale URL.
5937 - if ($post->post_status === 'publish' && !empty($previous_url)) {
5938 - $current_url = get_permalink($post_id);
5939 - if ($current_url && $current_url !== $previous_url) {
5940 - MxChat_Utils::delete_chunks_for_url($previous_url, 'default');
5941 - }
5942 - }
5943 -
5944 - // Store the current status for next time (if this is an update)
5945 - if ($update) {
5946 - set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
5947 -
5948 - // If the post is currently published, also store its URL
5949 - if ($post->post_status === 'publish') {
5950 - $current_url = get_permalink($post_id);
5951 - set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
5952 - }
5953 - }
5954 -
5955 - // Only process currently published content for adding/updating
5956 - if ($post->post_status === 'publish') {
5957 - // Get the source URL
5958 - $source_url = get_permalink($post_id);
5959 3006
5960 - // Get content with proper formatting (matching ajax_mxchat_process_selected_content)
5961 - $title = get_the_title($post_id);
5962 - $content = get_post_field('post_content', $post_id);
5963 - $excerpt = get_post_field('post_excerpt', $post_id);
5964 -
5965 - // Remove shortcode tags but preserve content inside them
5966 - $content = $this->strip_shortcode_tags_preserve_content($content);
5967 - $excerpt = $this->strip_shortcode_tags_preserve_content($excerpt);
5968 -
5969 - // Strip tags but preserve structure (don't use 'the_content' filter as it may re-add shortcodes)
5970 - $content = wp_strip_all_tags($content);
5971 -
5972 - // Combine title, short description (if exists), and content
5973 - $final_content = $title . "\n\n";
5974 -
5975 - // Add short description if it exists (WooCommerce products use post_excerpt for short description)
5976 - if (!empty($excerpt)) {
5977 - $final_content .= "Short Description: " . wp_strip_all_tags($excerpt) . "\n\n";
5978 - }
5979 -
5980 - $final_content .= $content;
5981 -
5982 - // For WooCommerce products, include pricing and product details
5983 - if ($post_type === 'product' && class_exists('WooCommerce')) {
5984 - $product = wc_get_product($post_id);
5985 -
5986 - if ($product) {
5987 - // Get pricing information
5988 - $regular_price = $product->get_regular_price();
5989 - $sale_price = $product->get_sale_price();
5990 - $price = $product->get_price();
5991 - $sku = $product->get_sku();
5992 -
5993 - // Get currency symbol
5994 - $currency_symbol = get_woocommerce_currency_symbol();
5995 -
5996 - // Add pricing information
5997 - $final_content .= "\n";
5998 - if (!empty($regular_price)) {
5999 - $final_content .= "Price: " . $currency_symbol . $regular_price . "\n";
6000 - } elseif (!empty($price)) {
6001 - $final_content .= "Price: " . $currency_symbol . $price . "\n";
6002 - }
6003 -
6004 - if (!empty($sale_price) && $sale_price !== $regular_price) {
6005 - $final_content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
6006 - }
6007 -
6008 - // Handle variable products - show price range
6009 - if ($product->is_type('variable')) {
6010 - $min_price = $product->get_variation_price('min');
6011 - $max_price = $product->get_variation_price('max');
6012 - if ($min_price !== $max_price) {
6013 - $final_content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
6014 - }
6015 - }
6016 -
6017 - if (!empty($sku)) {
6018 - $final_content .= "SKU: " . $sku . "\n";
6019 - }
6020 -
6021 - // Get product categories
6022 - $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
6023 - if (!empty($categories) && !is_wp_error($categories)) {
6024 - $final_content .= "Categories: " . implode(', ', $categories) . "\n";
6025 - }
3007 + // Get job type terms
3008 + $job_types = get_the_terms($post_id, 'job_listing_type');
3009 + if (!empty($job_types) && !is_wp_error($job_types)) {
3010 + $types = array();
3011 + foreach ($job_types as $type) {
3012 + $types[] = $type->name;
6026 3013 }
3014 + $final_content .= "\n\nJob Type: " . implode(', ', $types);
6027 3015 }
6028 -
6029 - // For custom post types like job_listing, include additional fields
6030 - if ($post_type === 'job_listing') {
6031 - // Add job-specific meta if available
6032 - $job_location = get_post_meta($post_id, '_job_location', true);
6033 - if (!empty($job_location)) {
6034 - $final_content .= "\n\nLocation: " . $job_location;
6035 - }
6036 -
6037 - // Get job type terms
6038 - $job_types = get_the_terms($post_id, 'job_listing_type');
6039 - if (!empty($job_types) && !is_wp_error($job_types)) {
6040 - $types = array();
6041 - foreach ($job_types as $type) {
6042 - $types[] = $type->name;
6043 - }
6044 - $final_content .= "\n\nJob Type: " . implode(', ', $types);
6045 - }
6046 -
6047 - // Get company name if available
6048 - $company_name = get_post_meta($post_id, '_company_name', true);
6049 - if (!empty($company_name)) {
6050 - $final_content .= "\n\nCompany: " . $company_name;
6051 - }
6052 - }
6053 -
6054 - // ADD ACF FIELDS SUPPORT (matches ajax_mxchat_process_selected_content behavior)
6055 - $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
6056 - if (!empty($acf_fields)) {
6057 - $acf_content_parts = array();
6058 - $pdf_attachment_ids = array();
6059 -
6060 - foreach ($acf_fields as $field_name => $field_value) {
6061 - $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
6062 - if (!empty($formatted_value)) {
6063 - // Convert field name to readable label
6064 - $field_label = ucwords(str_replace(['_', '-'], ' ', $field_name));
6065 - $acf_content_parts[] = $field_label . ": " . $formatted_value;
6066 - }
6067 -
6068 - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($field_value, $pdf_attachment_ids);
6069 - }
6070 -
6071 - if (!empty($acf_content_parts)) {
6072 - $final_content .= "\n\n" . implode("\n", $acf_content_parts);
6073 - }
6074 -
6075 - // Gate the auto-sync PDF-extraction loop behind an opt-in option.
6076 - // Mirrors the per-batch checkbox the manual content selector has; the
6077 - // 25 MB size cap lives in the shared extractor so it applies in both
6078 - // paths regardless. Default OFF — re-parsing every ACF PDF on every
6079 - // editor save is expensive and most sites don't want it.
6080 - $autosync_extract_acf_pdfs = get_option('mxchat_auto_sync_acf_pdfs', '0') === '1';
6081 - if ($autosync_extract_acf_pdfs && !empty($pdf_attachment_ids)) {
6082 - $pdf_attachment_ids = array_unique(array_filter(array_map('intval', $pdf_attachment_ids)));
6083 - $pdf_sections = array();
6084 - foreach ($pdf_attachment_ids as $att_id) {
6085 - $pdf_text = $this->mxchat_extract_pdf_text_by_attachment_id($att_id);
6086 - if (!empty($pdf_text)) {
6087 - $pdf_title = get_the_title($att_id);
6088 - $pdf_url = wp_get_attachment_url($att_id);
6089 - $header = 'PDF Attachment';
6090 - if (!empty($pdf_title)) {
6091 - $header .= ': ' . $pdf_title;
6092 - }
6093 - if (!empty($pdf_url)) {
6094 - $header .= ' (' . $pdf_url . ')';
6095 - }
6096 - $pdf_sections[] = $header . "\n" . $pdf_text;
6097 - }
6098 - }
6099 - if (!empty($pdf_sections)) {
6100 - $final_content .= "\n\nAttached PDFs:\n" . implode("\n\n", $pdf_sections);
6101 - }
6102 - }
6103 - }
6104 -
6105 - // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
6106 - $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
6107 - if (!empty($custom_meta)) {
6108 - $meta_content_parts = array();
6109 -
6110 - foreach ($custom_meta as $meta_key => $meta_value) {
6111 - // Convert meta key to readable label
6112 - $meta_label = ucwords(str_replace(array('_', '-'), ' ', $meta_key));
6113 - $meta_content_parts[] = $meta_label . ": " . $meta_value;
6114 - }
6115 -
6116 - if (!empty($meta_content_parts)) {
6117 - $final_content .= "\n\n" . implode("\n", $meta_content_parts);
6118 - }
6119 - }
6120 -
6121 - // Get API key with proper model detection
6122 - $options = get_option('mxchat_options');
6123 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
6124 3016
6125 - if (strpos($selected_model, 'voyage') === 0) {
6126 - $api_key = $options['voyage_api_key'] ?? '';
6127 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
6128 - $api_key = $options['gemini_api_key'] ?? '';
6129 - } else {
6130 - $api_key = $options['api_key'] ?? '';
3017 + // Get company name if available
3018 + $company_name = get_post_meta($post_id, '_company_name', true);
3019 + if (!empty($company_name)) {
3020 + $final_content .= "\n\nCompany: " . $company_name;
6131 3021 }
6132 -
6133 - if (empty($api_key)) {
6134 - return;
6135 - }
6136 -
6137 - // Use the centralized utility function for storage
6138 - $result = MxChat_Utils::submit_content_to_db(
6139 - $final_content,
6140 - $source_url,
6141 - $api_key,
6142 - md5($source_url) // Vector ID for Pinecone
6143 - );
6144 -
6145 - // After successful storage, apply role restriction based on tags
6146 - if (!is_wp_error($result)) {
6147 - $this->apply_role_restriction_to_post($post_id, $source_url);
6148 - }
6149 3022 }
6150 3023
6151 - // Clean up the stored previous status if not used above
6152 - if ($previous_status !== 'publish' || $post->post_status === 'publish') {
6153 - delete_transient($previous_status_key);
6154 - delete_transient($previous_url_key);
3024 + // Get the source URL
3025 + $source_url = get_permalink($post_id);
3026 +
3027 + // Get API key with proper model detection
3028 + $options = get_option('mxchat_options');
3029 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3030 +
3031 + if (strpos($selected_model, 'voyage') === 0) {
3032 + $api_key = $options['voyage_api_key'] ?? '';
3033 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3034 + $api_key = $options['gemini_api_key'] ?? '';
3035 + } else {
3036 + $api_key = $options['api_key'] ?? '';
6155 3037 }
6156 -}
6157 -
6158 -/**
6159 - * Store the post status and URL before update to detect status transitions
6160 - * This runs before the post is actually updated in the database
6161 - */
6162 -public function mxchat_store_pre_update_status($post_id, $data) {
6163 - // Get the current post from database (before update)
6164 - $current_post = get_post($post_id);
6165 3038
6166 - if ($current_post) {
6167 - // Store the current status temporarily
6168 - $status_key = 'mxchat_prev_status_' . $post_id;
6169 - set_transient($status_key, $current_post->post_status, HOUR_IN_SECONDS);
6170 -
6171 - // If the post is currently published, also store its URL
6172 - if ($current_post->post_status === 'publish') {
6173 - $url_key = 'mxchat_prev_url_' . $post_id;
6174 - $current_url = get_permalink($post_id);
6175 - set_transient($url_key, $current_url, HOUR_IN_SECONDS);
6176 - }
6177 - }
6178 -}
6179 -
6180 -/**
6181 - * Whether auto-sync is enabled for a post type (mirrors the checks used by the
6182 - * update/delete handlers; kept as one helper so new call sites cannot drift).
6183 - */
6184 -private function mxchat_is_auto_sync_enabled($post_type) {
6185 - if ($post_type === 'post') {
6186 - return get_option('mxchat_auto_sync_posts') === '1';
6187 - }
6188 - if ($post_type === 'page') {
6189 - return get_option('mxchat_auto_sync_pages') === '1';
6190 - }
6191 - return get_option('mxchat_auto_sync_' . $post_type) === '1';
6192 -}
6193 -
6194 -/**
6195 - * Remove a post's vectors the moment it leaves 'publish', using the authoritative
6196 - * old status core passes to transition_post_status — no transient involved (plan 816fb1).
6197 - *
6198 - * Covers status changes that never route through wp_update_post (scheduled-expiry
6199 - * plugins and others that flip post_status directly and call wp_transition_post_status),
6200 - * where neither pre_post_update nor post_updated fires and the old detection missed.
6201 - */
6202 -public function mxchat_handle_status_transition($new_status, $old_status, $post) {
6203 - if (!($post instanceof WP_Post) || wp_is_post_revision($post->ID)) {
3039 + if (empty($api_key)) {
3040 + error_log('MxChat Auto-sync: No API key configured for embedding model');
6204 3041 return;
6205 3042 }
6206 - // Only the publish -> not-publish edge matters here.
6207 - if ($old_status !== 'publish' || $new_status === 'publish') {
6208 - return;
3043 +
3044 + // Use the centralized utility function for storage
3045 + $result = MxChat_Utils::submit_content_to_db(
3046 + $final_content,
3047 + $source_url,
3048 + $api_key,
3049 + md5($source_url) // Vector ID for Pinecone
3050 + );
3051 +
3052 + if (is_wp_error($result)) {
3053 + error_log('MxChat Auto-sync failed for post ' . $post_id . ': ' . $result->get_error_message());
6209 3054 }
6210 - // Trash is handled by mxchat_handle_post_delete (wp_trash_post) with pre-trash URL
6211 - // resolution; skip to avoid a second network round-trip per trash.
6212 - if ($new_status === 'trash') {
6213 - return;
6214 - }
6215 - if (!$this->mxchat_is_auto_sync_enabled($post->post_type)) {
6216 - return;
6217 - }
6218 -
6219 - $urls = array();
6220 -
6221 - // The DB may already hold the new status when this fires, so get_permalink() on the
6222 - // live post could build a draft-style URL whose md5 misses the stored vector IDs.
6223 - // Reconstruct the published permalink from a clone instead.
6224 - $published_clone = clone $post;
6225 - $published_clone->post_status = 'publish';
6226 - $published_url = get_permalink($published_clone);
6227 - if ($published_url) {
6228 - $urls[] = $published_url;
6229 - }
6230 -
6231 - // Honour the pre-update capture when present (covers a slug change in the same save).
6232 - $previous_url = get_transient('mxchat_prev_url_' . $post->ID);
6233 - if (!empty($previous_url)) {
6234 - $urls[] = $previous_url;
6235 - }
6236 -
6237 - foreach (array_unique($urls) as $url) {
6238 - MxChat_Utils::delete_chunks_for_url($url, 'default');
6239 - }
6240 -
6241 - if (!empty($urls)) {
6242 - $this->transition_deleted_posts[$post->ID] = true;
6243 - }
6244 3055 }
6245 3056
6246 -/**
6247 - * WP-CLI: remove knowledge-base entries left behind by posts that were unpublished,
6248 - * trashed, or made private before the transition_post_status handler existed.
6249 - *
6250 - * Walks every auto-synced post type's non-published posts, reconstructs each one's
6251 - * published-era permalink, and deletes its vectors (routes to Pinecone or the WP table).
6252 - * Deletion is idempotent, so never-indexed posts are a cheap no-op.
6253 - *
6254 - * ## OPTIONS
6255 - *
6256 - * [--dry-run]
6257 - * : Report what would be removed without deleting anything.
6258 - *
6259 - * ## EXAMPLES
6260 - *
6261 - * wp mxchat prune-unpublished --dry-run
6262 - * wp mxchat prune-unpublished
6263 - */
6264 -public function cli_prune_unpublished($args, $assoc_args) {
6265 - global $wpdb;
6266 - $dry_run = !empty($assoc_args['dry-run']);
6267 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
6268 3057
6269 - $candidate_types = array_merge(array('post', 'page'), array_values(get_post_types(array('_builtin' => false), 'names')));
6270 - $synced_types = array();
6271 - foreach ($candidate_types as $type) {
6272 - if ($this->mxchat_is_auto_sync_enabled($type)) {
6273 - $synced_types[] = $type;
6274 - }
6275 - }
6276 - if (empty($synced_types)) {
6277 - WP_CLI::success('No post types have auto-sync enabled; nothing to prune.');
6278 - return;
6279 - }
6280 -
6281 - $scanned = 0;
6282 - $pruned = 0;
6283 - $paged = 1;
6284 - do {
6285 - $query = new WP_Query(array(
6286 - 'post_type' => $synced_types,
6287 - 'post_status' => array('draft', 'pending', 'private', 'future', 'trash'),
6288 - 'posts_per_page' => 100,
6289 - 'paged' => $paged,
6290 - 'fields' => 'ids',
6291 - ));
6292 - foreach ($query->posts as $post_id) {
6293 - $post = get_post($post_id);
6294 - if (!$post) {
6295 - continue;
6296 - }
6297 - $scanned++;
6298 -
6299 - // Rebuild the permalink the post had while published: publish-status clone,
6300 - // with wp_trash_post's __trashed slug suffix stripped for trashed posts.
6301 - $clone = clone $post;
6302 - $clone->post_status = 'publish';
6303 - if (substr($clone->post_name, -9) === '__trashed') {
6304 - $clone->post_name = substr($clone->post_name, 0, -9);
6305 - }
6306 - $url = get_permalink($clone);
6307 - if (!$url) {
6308 - continue;
6309 - }
6310 -
6311 - // Local-table row count is exact in WordPress-DB mode; in Pinecone mode it
6312 - // reads 0 but the delete below still routes to Pinecone and is idempotent.
6313 - $local_rows = (int) $wpdb->get_var($wpdb->prepare(
6314 - "SELECT COUNT(*) FROM {$table} WHERE source_url = %s", $url
6315 - ));
6316 -
6317 - if ($dry_run) {
6318 - if ($local_rows > 0) {
6319 - WP_CLI::log(sprintf('Would remove %d row(s): %s (post %d, %s)', $local_rows, $url, $post_id, $post->post_status));
6320 - $pruned += $local_rows;
6321 - }
6322 - continue;
6323 - }
6324 -
6325 - MxChat_Utils::delete_chunks_for_url($url, 'default');
6326 - if ($local_rows > 0) {
6327 - WP_CLI::log(sprintf('Removed %d row(s): %s (post %d, %s)', $local_rows, $url, $post_id, $post->post_status));
6328 - $pruned += $local_rows;
6329 - }
6330 - }
6331 - $more = $paged < $query->max_num_pages;
6332 - $paged++;
6333 - } while ($more);
6334 -
6335 - WP_CLI::success(sprintf(
6336 - '%s %d local knowledge row(s) across %d non-published post(s) scanned.%s',
6337 - $dry_run ? 'Would remove' : 'Removed',
6338 - $pruned,
6339 - $scanned,
6340 - ' (Pinecone-mode deletions are not counted locally.)'
6341 - ));
6342 -}
6343 -
6344 3058 public function mxchat_handle_post_delete($post_id) {
6345 3059 // Get post data before it's deleted
6346 3060 $post = get_post($post_id);
6347 3061
@@ -6371,51 +3085,85 @@
6371 3085 if (!$should_sync) {
6372 3086 return;
6373 3087 }
6374 3088
6375 - // Resolve the pre-trash URL. wp_trash_post renames the slug with "__trashed" before firing
6376 - // this hook, so get_permalink() here would return the trashed URL and md5() would miss the
6377 - // real vector IDs stored under the original URL.
6378 - $source_url = $this->mxchat_resolve_pre_trash_url($post_id);
3089 + // Get the URL before post is deleted
3090 + $source_url = get_permalink($post_id);
6379 3091 if (!$source_url) {
6380 - //error_log('MXChat: Failed to resolve source URL for post ' . $post_id);
3092 + error_log('MXChat: Failed to get permalink for post ' . $post_id);
6381 3093 return;
6382 3094 }
6383 3095
6384 - // Use chunk-aware deletion (handles both chunked and non-chunked content)
6385 - $delete_result = MxChat_Utils::delete_chunks_for_url($source_url, 'default');
3096 + // Check if Pinecone is enabled
3097 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3098 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6386 3099
6387 - if (is_wp_error($delete_result)) {
6388 - //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message());
3100 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3101 + // Delete from Pinecone
3102 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
3103 + } else {
3104 + // Delete from WordPress DB
3105 + global $wpdb;
3106 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3107 +
3108 + $result = $wpdb->delete(
3109 + $table_name,
3110 + array('source_url' => $source_url),
3111 + array('%s')
3112 + );
3113 +
3114 + if ($result === false) {
3115 + error_log('MXChat: WordPress DB deletion failed for URL: ' . $source_url);
3116 + }
6389 3117 }
3118 +}
6390 3119
6391 - delete_transient('mxchat_prev_url_' . $post_id);
6392 - delete_transient('mxchat_prev_status_' . $post_id);
6393 -}
6394 3120
6395 -/**
6396 - * Resolve the source URL for a post being trashed/deleted.
6397 - *
6398 - * Why: wp_trash_post appends "__trashed" to the slug before the wp_trash_post action fires, so
6399 - * get_permalink() returns a URL whose md5() won't match the vector IDs stored in Pinecone or
6400 - * the source_url rows in the WP DB. Prefer the URL captured by mxchat_store_pre_update_status
6401 - * (runs on pre_post_update, before the rename); fall back to stripping the __trashed suffix.
6402 - */
6403 -private function mxchat_resolve_pre_trash_url($post_id) {
6404 - $previous_url = get_transient('mxchat_prev_url_' . $post_id);
6405 - if (!empty($previous_url)) {
6406 - return $previous_url;
6407 - }
3121 + /**
3122 + * Deletes data from Pinecone using a source URL
3123 + */
3124 + public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
3125 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3126 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
6408 3127
6409 - $current = get_permalink($post_id);
6410 - if (!$current) {
6411 - return '';
6412 - }
6413 - return preg_replace('#__trashed(/?)$#', '$1', $current);
6414 -}
3128 + if (empty($host) || empty($api_key)) {
3129 + //error_log('MXChat: Pinecone deletion failed - missing configuration');
3130 + return false;
3131 + }
6415 3132
3133 + $api_endpoint = "https://{$host}/vectors/delete";
3134 + $vector_id = md5($source_url);
6416 3135
3136 + $request_body = array(
3137 + 'ids' => array($vector_id)
3138 + );
6417 3139
3140 + $response = wp_remote_post($api_endpoint, array(
3141 + 'headers' => array(
3142 + 'Api-Key' => $api_key,
3143 + 'accept' => 'application/json',
3144 + 'content-type' => 'application/json'
3145 + ),
3146 + 'body' => wp_json_encode($request_body),
3147 + 'timeout' => 30
3148 + ));
3149 +
3150 + if (is_wp_error($response)) {
3151 + //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
3152 + return false;
3153 + }
3154 +
3155 + $response_code = wp_remote_retrieve_response_code($response);
3156 + if ($response_code !== 200) {
3157 + //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
3158 + return false;
3159 + }
3160 +
3161 + return true;
3162 + }
3163 +
3164 +
3165 +
6418 3166 public function mxchat_handle_product_change($post_id, $post, $update) {
6419 3167 if ($post->post_type !== 'product') {
6420 3168 return;
6421 3169 }
@@ -6437,12 +3185,11 @@
6437 3185 if (!isset($this->options['enable_woocommerce_integration']) ||
6438 3186 !in_array($this->options['enable_woocommerce_integration'], ['1', 'on'])) {
6439 3187 return;
6440 3188 }
6441 -
3189 +
6442 3190 $source_url = get_permalink($product->get_id());
6443 - $product_id = $product->get_id();
6444 -
3191 +
6445 3192 // Build product content
6446 3193 $title = $product->get_name();
6447 3194 $description = $product->get_description();
6448 3195 $short_description = $product->get_short_description();
@@ -6447,92 +3194,35 @@
6447 3194 $description = $product->get_description();
6448 3195 $short_description = $product->get_short_description();
6449 3196 $regular_price = $product->get_regular_price();
6450 3197 $sale_price = $product->get_sale_price();
6451 - $price = $product->get_price();
6452 3198 $sku = $product->get_sku();
6453 -
6454 - // Get currency symbol
6455 - $currency_symbol = get_woocommerce_currency_symbol();
6456 -
3199 +
6457 3200 // Format content consistently
6458 3201 $content = $title . "\n\n";
6459 -
6460 - if (!empty($short_description)) {
6461 - $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
6462 - }
6463 -
3202 +
6464 3203 if (!empty($description)) {
6465 3204 $content .= wp_strip_all_tags($description) . "\n\n";
6466 3205 }
6467 -
6468 - // Add pricing information
6469 - if (!empty($regular_price)) {
6470 - $content .= "Price: " . $currency_symbol . $regular_price . "\n";
6471 - } elseif (!empty($price)) {
6472 - $content .= "Price: " . $currency_symbol . $price . "\n";
3206 +
3207 + if (!empty($short_description)) {
3208 + $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
6473 3209 }
6474 -
6475 - if (!empty($sale_price) && $sale_price !== $regular_price) {
6476 - $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
3210 +
3211 + $content .= "Price: $" . $regular_price . "\n";
3212 +
3213 + if (!empty($sale_price)) {
3214 + $content .= "Sale Price: $" . $sale_price . "\n";
6477 3215 }
6478 -
6479 - // Handle variable products - show price range
6480 - if ($product->is_type('variable')) {
6481 - $min_price = $product->get_variation_price('min');
6482 - $max_price = $product->get_variation_price('max');
6483 - if ($min_price !== $max_price) {
6484 - $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
6485 - }
6486 - }
6487 -
3216 +
6488 3217 if (!empty($sku)) {
6489 3218 $content .= "SKU: " . $sku . "\n";
6490 3219 }
6491 -
6492 - // Get product categories
6493 - $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
6494 - if (!empty($categories) && !is_wp_error($categories)) {
6495 - $content .= "Categories: " . implode(', ', $categories) . "\n";
6496 - }
6497 -
6498 - // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
6499 - $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
6500 - if (!empty($custom_tabs) && is_array($custom_tabs)) {
6501 - foreach ($custom_tabs as $tab) {
6502 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
6503 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
6504 -
6505 - if (!empty($tab_title) && !empty($tab_content)) {
6506 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
6507 - }
6508 - }
6509 - }
6510 -
6511 - // Also check for reusable/saved tabs applied to this product
6512 - $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
6513 - if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
6514 - // Get the saved tabs option
6515 - $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
6516 - if (!empty($saved_tabs) && is_array($saved_tabs)) {
6517 - foreach ($applied_saved_tabs as $saved_tab_id) {
6518 - if (isset($saved_tabs[$saved_tab_id])) {
6519 - $tab = $saved_tabs[$saved_tab_id];
6520 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
6521 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
6522 -
6523 - if (!empty($tab_title) && !empty($tab_content)) {
6524 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
6525 - }
6526 - }
6527 - }
6528 - }
6529 - }
6530 -
3220 +
6531 3221 // Get API key with proper model detection
6532 3222 $options = get_option('mxchat_options');
6533 3223 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
6534 -
3224 +
6535 3225 if (strpos($selected_model, 'voyage') === 0) {
6536 3226 $api_key = $options['voyage_api_key'] ?? '';
6537 3227 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
6538 3228 $api_key = $options['gemini_api_key'] ?? '';
@@ -6538,14 +3228,14 @@
6538 3228 $api_key = $options['gemini_api_key'] ?? '';
6539 3229 } else {
6540 3230 $api_key = $options['api_key'] ?? '';
6541 3231 }
6542 -
3232 +
6543 3233 if (empty($api_key)) {
6544 - //error_log('MxChat Auto-sync: No API key configured for embedding model');
3234 + error_log('MxChat Auto-sync: No API key configured for embedding model');
6545 3235 return;
6546 3236 }
6547 -
3237 +
6548 3238 // Use the centralized utility function for storage
6549 3239 $result = MxChat_Utils::submit_content_to_db(
6550 3240 $content,
6551 3241 $source_url,
@@ -6551,16 +3241,11 @@
6551 3241 $source_url,
6552 3242 $api_key,
6553 3243 md5($source_url) // Vector ID for Pinecone
6554 3244 );
6555 -
6556 - // After successful storage, apply role restriction based on tags
6557 - if (!is_wp_error($result)) {
6558 - $this->apply_role_restriction_to_post($product_id, $source_url);
6559 - }
6560 -
3245 +
6561 3246 if (is_wp_error($result)) {
6562 - //error_log('MxChat WooCommerce sync failed for product ' . $product_id . ': ' . $result->get_error_message());
3247 + error_log('MxChat WooCommerce sync failed for product ' . $product->get_id() . ': ' . $result->get_error_message());
6563 3248 }
6564 3249 }
6565 3250
6566 3251 public function mxchat_handle_product_delete($post_id) {
@@ -6567,1123 +3252,30 @@
6567 3252 if (get_post_type($post_id) !== 'product') {
6568 3253 return;
6569 3254 }
6570 3255
6571 - $source_url = $this->mxchat_resolve_pre_trash_url($post_id);
6572 - if (!$source_url) {
6573 - return;
6574 - }
3256 + $source_url = get_permalink($post_id);
6575 3257
6576 - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks)
6577 - MxChat_Utils::delete_chunks_for_url($source_url, 'default');
6578 -
6579 - delete_transient('mxchat_prev_url_' . $post_id);
6580 - delete_transient('mxchat_prev_status_' . $post_id);
6581 -}
6582 -
6583 -/**
6584 - * Handle individual Pinecone content deletion
6585 - */
6586 -public function mxchat_handle_pinecone_prompt_delete() {
6587 - // Check permissions
6588 - if (!current_user_can('manage_options')) {
6589 - wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
6590 - }
6591 -
6592 - // Verify nonce
6593 - if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'mxchat_delete_pinecone_prompt_nonce')) {
6594 - wp_die(esc_html__('Security check failed.', 'mxchat'));
6595 - }
6596 -
6597 - $vector_id = isset($_GET['vector_id']) ? sanitize_text_field($_GET['vector_id']) : '';
6598 -
6599 - if (empty($vector_id)) {
6600 - set_transient('mxchat_admin_notice_error',
6601 - esc_html__('Invalid vector ID.', 'mxchat'),
6602 - 30
6603 - );
6604 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
6605 - exit;
6606 - }
6607 -
6608 - // Get Pinecone settings
3258 + // Check if Pinecone is enabled
6609 3259 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
6610 3260 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6611 -
6612 - if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
6613 - set_transient('mxchat_admin_notice_error',
6614 - esc_html__('Pinecone is not properly configured.', 'mxchat'),
6615 - 30
6616 - );
6617 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
6618 - exit;
6619 - }
6620 -
6621 - // Delete from Pinecone
6622 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
6623 - $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
6624 - $vector_id,
6625 - $pinecone_options['mxchat_pinecone_api_key'],
6626 - $pinecone_options['mxchat_pinecone_host']
6627 - );
6628 -
6629 - if ($result['success']) {
6630 - // No cache clearing needed since we removed caching
6631 - set_transient('mxchat_admin_notice_success',
6632 - esc_html__('Entry deleted successfully from Pinecone.', 'mxchat'),
6633 - 30
6634 - );
6635 - } else {
6636 - set_transient('mxchat_admin_notice_error',
6637 - esc_html__('Failed to delete entry: ', 'mxchat') . $result['message'],
6638 - 30
6639 - );
6640 - }
6641 -
6642 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
6643 - exit;
6644 -}
6645 -/**
6646 - * Handle individual Pinecone content deletion via AJAX
6647 - */
6648 -public function ajax_mxchat_delete_pinecone_prompt() {
6649 - // Verify nonce and permissions
6650 - if (!check_ajax_referer('mxchat_delete_pinecone_prompt_nonce', 'nonce', false)) {
6651 - wp_send_json_error('Invalid nonce');
6652 - exit;
6653 - }
6654 -
6655 - if (!current_user_can('manage_options')) {
6656 - wp_send_json_error('Unauthorized access');
6657 - exit;
6658 - }
6659 -
6660 - $vector_id = isset($_POST['vector_id']) ? sanitize_text_field($_POST['vector_id']) : '';
6661 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
6662 -
6663 - if (empty($vector_id)) {
6664 - wp_send_json_error('Missing vector ID');
6665 - exit;
6666 - }
6667 -
6668 - // Get bot-specific Pinecone settings
6669 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
6670 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
6671 -
6672 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6673 -
6674 - if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
6675 - wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
6676 - exit;
6677 - }
6678 -
6679 - // Delete from the correct Pinecone index
6680 - $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
6681 - $vector_id,
6682 - $pinecone_options['mxchat_pinecone_api_key'],
6683 - $pinecone_options['mxchat_pinecone_host']
6684 - );
6685 -
6686 - if ($result['success']) {
6687 - // No cache clearing needed since we removed caching
6688 - wp_send_json_success(array(
6689 - 'message' => 'Entry deleted successfully from Pinecone',
6690 - 'vector_id' => $vector_id,
6691 - 'bot_id' => $bot_id
6692 - ));
6693 - } else {
6694 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Failed to delete from Pinecone: ' . $result['message'], array('vector_id' => $vector_id, 'bot_id' => $bot_id));
6695 - wp_send_json_error('Failed to delete from Pinecone: ' . $result['message']);
6696 - }
6697 -
6698 - exit;
6699 -}
6700 3261
6701 -/**
6702 - * Handle deletion of all chunks for a given source URL via AJAX
6703 - * Follows the same pattern as ajax_mxchat_delete_pinecone_prompt
6704 - */
6705 -public function ajax_mxchat_delete_chunks_by_url() {
6706 - // Verify nonce and permissions
6707 - if (!check_ajax_referer('mxchat_delete_chunks_nonce', 'nonce', false)) {
6708 - wp_send_json_error('Invalid nonce');
6709 - exit;
6710 - }
6711 -
6712 - if (!current_user_can('manage_options')) {
6713 - wp_send_json_error('Unauthorized access');
6714 - exit;
6715 - }
6716 -
6717 - $source_url = isset($_POST['source_url']) ? esc_url_raw($_POST['source_url']) : '';
6718 - $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
6719 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
6720 -
6721 - if (empty($source_url)) {
6722 - wp_send_json_error('Missing source URL');
6723 - exit;
6724 - }
6725 -
6726 - // Generate the base vector ID from the source URL (same as how chunks are created)
6727 - $base_vector_id = md5($source_url);
6728 -
6729 - if ($data_source === 'pinecone') {
6730 - // Get bot-specific Pinecone settings (same as working delete function)
6731 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
6732 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
6733 -
6734 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6735 -
6736 - if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
6737 - wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
6738 - exit;
6739 - }
6740 -
6741 - $api_key = $pinecone_options['mxchat_pinecone_api_key'];
6742 - $host = $pinecone_options['mxchat_pinecone_host'];
6743 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
6744 -
6745 - // Collect all vector IDs to delete
6746 - $vectors_to_delete = array();
6747 -
6748 - // Add the original single-vector ID (for non-chunked content)
6749 - $vectors_to_delete[] = $base_vector_id;
6750 -
6751 - // Use Pinecone list API to find all chunk vectors with this prefix
6752 - // NOTE: Pinecone List API is a GET request with query parameters, not POST
6753 - $prefix = $base_vector_id . '_chunk_';
6754 -
6755 - $query_params = array(
6756 - 'prefix' => $prefix,
6757 - 'limit' => 100
6758 - );
6759 -
6760 - if (!empty($namespace)) {
6761 - $query_params['namespace'] = $namespace;
6762 - }
6763 -
6764 - $list_url = "https://{$host}/vectors/list?" . http_build_query($query_params);
6765 -
6766 - $list_response = wp_remote_get($list_url, array(
6767 - 'headers' => array(
6768 - 'Api-Key' => $api_key,
6769 - 'accept' => 'application/json'
6770 - ),
6771 - 'timeout' => 30
6772 - ));
6773 -
6774 - if (!is_wp_error($list_response)) {
6775 - $list_body_response = wp_remote_retrieve_body($list_response);
6776 - $list_data = json_decode($list_body_response, true);
6777 - if (!empty($list_data['vectors'])) {
6778 - foreach ($list_data['vectors'] as $vector) {
6779 - if (isset($vector['id'])) {
6780 - $vectors_to_delete[] = $vector['id'];
6781 - }
6782 - }
6783 - }
6784 - }
6785 -
6786 - if (empty($vectors_to_delete)) {
6787 - wp_send_json_success(array(
6788 - 'message' => 'No vectors found to delete',
6789 - 'source_url' => $source_url
6790 - ));
6791 - exit;
6792 - }
6793 -
6794 - // Delete all vectors using the same endpoint as the working function
6795 - $delete_url = "https://{$host}/vectors/delete";
6796 -
6797 - $delete_body = array(
6798 - 'ids' => $vectors_to_delete
6799 - );
6800 -
6801 - if (!empty($namespace)) {
6802 - $delete_body['namespace'] = $namespace;
6803 - }
6804 -
6805 - $delete_response = wp_remote_post($delete_url, array(
6806 - 'headers' => array(
6807 - 'Api-Key' => $api_key,
6808 - 'accept' => 'application/json',
6809 - 'content-type' => 'application/json'
6810 - ),
6811 - 'body' => wp_json_encode($delete_body),
6812 - 'timeout' => 30
6813 - ));
6814 -
6815 - if (is_wp_error($delete_response)) {
6816 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Failed to delete chunks from Pinecone: ' . $delete_response->get_error_message(), array('source_url' => $source_url));
6817 - wp_send_json_error('Failed to delete from Pinecone: ' . $delete_response->get_error_message());
6818 - exit;
6819 - }
6820 -
6821 - $response_code = wp_remote_retrieve_response_code($delete_response);
6822 -
6823 - if ($response_code !== 200) {
6824 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Pinecone API error (HTTP ' . $response_code . ')', array('source_url' => $source_url));
6825 - wp_send_json_error('Pinecone API error (HTTP ' . $response_code . ')');
6826 - exit;
6827 - }
6828 -
6829 - wp_send_json_success(array(
6830 - 'message' => 'All chunks deleted successfully from Pinecone',
6831 - 'source_url' => $source_url,
6832 - 'deleted_count' => count($vectors_to_delete)
6833 - ));
6834 -
3262 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3263 + // Delete from Pinecone
3264 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
6835 3265 } else {
6836 - // WordPress database deletion
3266 + // Delete from WordPress DB
6837 3267 global $wpdb;
6838 3268 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6839 -
6840 - $result = $wpdb->delete(
6841 - $table_name,
6842 - array('source_url' => $source_url),
6843 - array('%s')
6844 - );
6845 -
6846 - if ($result === false) {
6847 - MxChat_Admin::mxchat_log_debug('database_error', 'Failed to delete from database: ' . $wpdb->last_error, array('source_url' => $source_url));
6848 - wp_send_json_error('Failed to delete from database: ' . $wpdb->last_error);
6849 - exit;
6850 - }
6851 -
6852 - wp_send_json_success(array(
6853 - 'message' => 'All chunks deleted successfully from database',
6854 - 'source_url' => $source_url,
6855 - 'deleted_count' => $result
6856 - ));
6857 - }
6858 -
6859 - exit;
6860 -}
6861 -
6862 -/**
6863 - * Handle individual WordPress database content deletion via AJAX
6864 - * Mirrors the Pinecone delete handler but for WordPress database entries
6865 - */
6866 -public function ajax_mxchat_delete_wordpress_prompt() {
6867 - // Verify nonce and permissions
6868 - if (!check_ajax_referer('mxchat_delete_wordpress_prompt_nonce', 'nonce', false)) {
6869 - wp_send_json_error('Invalid nonce');
6870 - exit;
6871 - }
6872 -
6873 - if (!current_user_can('manage_options')) {
6874 - wp_send_json_error('Unauthorized access');
6875 - exit;
6876 - }
6877 -
6878 - $entry_id = isset($_POST['entry_id']) ? intval($_POST['entry_id']) : 0;
6879 -
6880 - if (empty($entry_id)) {
6881 - wp_send_json_error('Missing entry ID');
6882 - exit;
6883 - }
6884 -
6885 - global $wpdb;
6886 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6887 -
6888 - // Clear cache for this entry
6889 - wp_cache_delete('prompt_' . $entry_id, 'mxchat_prompts');
6890 -
6891 - // Delete from database
6892 - $result = $wpdb->delete(
6893 - $table_name,
6894 - array('id' => $entry_id),
6895 - array('%d')
6896 - );
6897 -
6898 - if ($result !== false) {
6899 - wp_send_json_success(array(
6900 - 'message' => 'Entry deleted successfully',
6901 - 'entry_id' => $entry_id
6902 - ));
6903 - } else {
6904 - wp_send_json_error('Failed to delete entry from database');
6905 - }
6906 -
6907 - exit;
6908 -}
6909 -
6910 -/**
6911 - * Handle bulk deletion of knowledge entries via AJAX
6912 - * Supports both Pinecone and WordPress database entries
6913 - */
6914 -public function ajax_mxchat_bulk_delete_knowledge() {
6915 - // Verify nonce and permissions
6916 - if (!check_ajax_referer('mxchat_bulk_delete_knowledge_nonce', 'nonce', false)) {
6917 - wp_send_json_error('Invalid nonce');
6918 - exit;
6919 - }
6920 -
6921 - if (!current_user_can('manage_options')) {
6922 - wp_send_json_error('Unauthorized access');
6923 - exit;
6924 - }
6925 -
6926 - $entries = isset($_POST['entries']) ? $_POST['entries'] : array();
6927 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
6928 -
6929 - if (empty($entries) || !is_array($entries)) {
6930 - wp_send_json_error('No entries provided');
6931 - exit;
6932 - }
6933 -
6934 - // Extend execution time — bulk Pinecone operations can take a while
6935 - if (function_exists('set_time_limit')) {
6936 - set_time_limit(120);
6937 - }
6938 -
6939 - $success_ids = array();
6940 - $failed_ids = array();
6941 - $errors = array();
6942 -
6943 - global $wpdb;
6944 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6945 -
6946 - // Get Pinecone manager for Pinecone deletions
6947 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
6948 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
6949 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6950 -
6951 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
6952 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
6953 -
6954 - // =============================================
6955 - // PHASE 1: Collect all Pinecone vector IDs
6956 - // and separate WordPress entries
6957 - // =============================================
6958 - $pinecone_entry_ids = array(); // entry IDs that are pinecone-sourced
6959 - $wordpress_entries = array(); // entries for WordPress DB deletion
6960 - $all_vector_ids = array(); // all pinecone vector IDs to delete in one batch
6961 -
6962 - foreach ($entries as $entry) {
6963 - $entry_id = sanitize_text_field($entry['id'] ?? '');
6964 - $source = sanitize_text_field($entry['source'] ?? 'wordpress');
6965 - $source_url = isset($entry['sourceUrl']) ? esc_url_raw($entry['sourceUrl']) : '';
6966 - $is_group = isset($entry['isGroup']) && ($entry['isGroup'] === true || $entry['isGroup'] === 'true');
6967 -
6968 - if (empty($entry_id)) {
6969 - continue;
6970 - }
6971 -
6972 - if ($source === 'pinecone') {
6973 - if (!$use_pinecone || empty($api_key)) {
6974 - $failed_ids[] = $entry_id;
6975 - $errors[] = "Pinecone not configured for entry: $entry_id";
6976 - continue;
6977 - }
6978 -
6979 - $pinecone_entry_ids[] = $entry_id;
6980 -
6981 - if ($is_group && !empty($source_url)) {
6982 - // Grouped/chunked entry: collect base ID + chunk IDs via List API
6983 - $base_vector_id = md5($source_url);
6984 - $all_vector_ids[] = $base_vector_id;
6985 -
6986 - $list_url = 'https://' . $host . '/vectors/list?prefix=' . urlencode($base_vector_id . '_chunk_') . '&limit=100';
6987 - $list_response = wp_remote_get($list_url, array(
6988 - 'headers' => array(
6989 - 'Api-Key' => $api_key,
6990 - 'accept' => 'application/json'
6991 - ),
6992 - 'timeout' => 30
6993 - ));
6994 -
6995 - if (!is_wp_error($list_response)) {
6996 - $list_body = json_decode(wp_remote_retrieve_body($list_response), true);
6997 - if (!empty($list_body['vectors']) && is_array($list_body['vectors'])) {
6998 - foreach ($list_body['vectors'] as $vector) {
6999 - if (isset($vector['id'])) {
7000 - $all_vector_ids[] = $vector['id'];
7001 - }
7002 - }
7003 - }
7004 - }
7005 - } else {
7006 - // Single entry: the entry_id IS the vector ID
7007 - $all_vector_ids[] = $entry_id;
7008 - }
7009 - } else {
7010 - $wordpress_entries[] = $entry;
7011 - }
7012 - }
7013 -
7014 - // =============================================
7015 - // PHASE 2: Single batch delete to Pinecone
7016 - // =============================================
7017 - if (!empty($all_vector_ids)) {
7018 - $all_vector_ids = array_values(array_unique($all_vector_ids));
7019 - $pinecone_success = true;
7020 - $batches = array_chunk($all_vector_ids, 100);
7021 -
7022 - foreach ($batches as $batch) {
7023 - $delete_response = wp_remote_post("https://{$host}/vectors/delete", array(
7024 - 'headers' => array(
7025 - 'Api-Key' => $api_key,
7026 - 'accept' => 'application/json',
7027 - 'content-type' => 'application/json'
7028 - ),
7029 - 'body' => wp_json_encode(array('ids' => $batch)),
7030 - 'timeout' => 60
7031 - ));
7032 -
7033 - if (is_wp_error($delete_response)) {
7034 - $pinecone_success = false;
7035 - $errors[] = 'Pinecone batch deletion failed: ' . $delete_response->get_error_message();
7036 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Bulk delete batch failed: ' . $delete_response->get_error_message());
7037 - } else {
7038 - $response_code = wp_remote_retrieve_response_code($delete_response);
7039 - if ($response_code !== 200) {
7040 - $pinecone_success = false;
7041 - $response_body = wp_remote_retrieve_body($delete_response);
7042 - $errors[] = "Pinecone API error (HTTP $response_code)";
7043 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Bulk delete batch failed (HTTP ' . $response_code . ')', array('response' => substr($response_body, 0, 200)));
7044 - }
7045 - }
7046 - }
7047 -
7048 - // Mark all pinecone entries based on batch result
7049 - foreach ($pinecone_entry_ids as $eid) {
7050 - if ($pinecone_success) {
7051 - $success_ids[] = $eid;
7052 - } else {
7053 - $failed_ids[] = $eid;
7054 - }
7055 - }
7056 - }
7057 -
7058 - // =============================================
7059 - // PHASE 3: WordPress database deletions
7060 - // =============================================
7061 - foreach ($wordpress_entries as $entry) {
7062 - $entry_id = sanitize_text_field($entry['id'] ?? '');
7063 - $source_url = isset($entry['sourceUrl']) ? esc_url_raw($entry['sourceUrl']) : '';
7064 - $is_group = isset($entry['isGroup']) && ($entry['isGroup'] === true || $entry['isGroup'] === 'true');
7065 -
7066 - if (empty($entry_id)) {
7067 - continue;
7068 - }
7069 -
7070 - try {
7071 - if ($is_group && !empty($source_url)) {
7072 - $result = $wpdb->delete(
7073 - $table_name,
7074 - array('source_url' => $source_url),
7075 - array('%s')
7076 - );
7077 - } else {
7078 - wp_cache_delete('prompt_' . $entry_id, 'mxchat_prompts');
7079 - $result = $wpdb->delete(
7080 - $table_name,
7081 - array('id' => intval($entry_id)),
7082 - array('%d')
7083 - );
7084 - }
7085 -
7086 - if ($result !== false) {
7087 - $success_ids[] = $entry_id;
7088 - } else {
7089 - $failed_ids[] = $entry_id;
7090 - $errors[] = "Database error for entry: $entry_id";
7091 - }
7092 - } catch (Exception $e) {
7093 - $failed_ids[] = $entry_id;
7094 - $errors[] = $e->getMessage();
7095 - }
7096 - }
7097 -
7098 - wp_send_json_success(array(
7099 - 'success_ids' => $success_ids,
7100 - 'failed_ids' => $failed_ids,
7101 - 'errors' => $errors,
7102 - 'total_processed' => count($success_ids) + count($failed_ids)
7103 - ));
7104 -
7105 - exit;
7106 -}
7107 -
7108 -/**
7109 - * Get hierarchical roles for dropdown
7110 - */
7111 -public function mxchat_get_role_options() {
7112 - return array(
7113 - 'public' => __('Public (Everyone)', 'mxchat'),
7114 - 'logged_in' => __('Logged In Users', 'mxchat'),
7115 - 'subscriber' => __('Subscribers & Above', 'mxchat'),
7116 - 'contributor' => __('Contributors & Above', 'mxchat'),
7117 - 'author' => __('Authors & Above', 'mxchat'),
7118 - 'editor' => __('Editors & Above', 'mxchat'),
7119 - 'administrator' => __('Administrators Only', 'mxchat')
7120 - );
7121 -}
7122 -
7123 -/**
7124 - * Check if user has access to content based on role restriction
7125 - */
7126 -public function mxchat_user_has_content_access($role_restriction) {
7127 - // Public content is always accessible
7128 - if ($role_restriction === 'public' || empty($role_restriction)) {
7129 - return true;
7130 - }
7131 -
7132 - // Check if user is logged in for logged_in restriction
7133 - if ($role_restriction === 'logged_in') {
7134 - return is_user_logged_in();
7135 - }
7136 -
7137 - // If not logged in, no access to role-restricted content
7138 - if (!is_user_logged_in()) {
7139 - return false;
7140 - }
7141 -
7142 - $user = wp_get_current_user();
7143 - $user_roles = $user->roles;
7144 -
7145 - if (empty($user_roles)) {
7146 - return false;
7147 - }
7148 -
7149 - // Define role hierarchy (higher number = higher access)
7150 - $hierarchy = array(
7151 - 'subscriber' => 1,
7152 - 'contributor' => 2,
7153 - 'author' => 3,
7154 - 'editor' => 4,
7155 - 'administrator' => 5
7156 - );
7157 -
7158 - // Get required level
7159 - $required_level = isset($hierarchy[$role_restriction]) ? $hierarchy[$role_restriction] : 0;
7160 -
7161 - // Check if user has required level or higher
7162 - foreach ($user_roles as $user_role) {
7163 - $user_level = isset($hierarchy[$user_role]) ? $hierarchy[$user_role] : 0;
7164 - if ($user_level >= $required_level) {
7165 - return true;
7166 - }
7167 - }
7168 -
7169 - return false;
7170 -}
7171 -
7172 -/**
7173 - * Handle role restriction updates via AJAX
7174 - * Removed cache clearing call since we removed caching
7175 - */
7176 -public function ajax_mxchat_update_role_restriction() {
7177 - // Verify nonce and permissions
7178 - if (!check_ajax_referer('mxchat_update_role_nonce', 'nonce', false)) {
7179 - wp_send_json_error('Invalid nonce');
7180 - exit;
7181 - }
7182 -
7183 - if (!current_user_can('manage_options')) {
7184 - wp_send_json_error('Unauthorized access');
7185 - exit;
7186 - }
7187 -
7188 - $entry_id = isset($_POST['entry_id']) ? sanitize_text_field($_POST['entry_id']) : '';
7189 - $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
7190 - $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
7191 -
7192 - if (empty($entry_id)) {
7193 - wp_send_json_error('Invalid entry ID');
7194 - exit;
7195 - }
7196 -
7197 - // Get knowledge manager instance to validate role restriction
7198 - $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
7199 - $valid_roles = array_keys($knowledge_manager->mxchat_get_role_options());
7200 - if (!in_array($role_restriction, $valid_roles)) {
7201 - wp_send_json_error('Invalid role restriction');
7202 - exit;
7203 - }
7204 -
7205 - global $wpdb;
7206 -
7207 - if ($data_source === 'pinecone') {
7208 - // Handle Pinecone role restriction (stored separately in WordPress table)
7209 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
7210 3269
7211 - // Use REPLACE to insert or update the role restriction
7212 - $result = $wpdb->replace(
7213 - $roles_table,
7214 - array(
7215 - 'vector_id' => $entry_id,
7216 - 'role_restriction' => $role_restriction,
7217 - 'updated_at' => current_time('mysql')
7218 - ),
7219 - array('%s', '%s', '%s')
7220 - );
7221 -
7222 - // No cache clearing needed since we removed caching
7223 -
7224 - } else {
7225 - // Handle WordPress database role restriction (existing functionality)
7226 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
7227 -
7228 - $result = $wpdb->update(
3270 + $wpdb->delete(
7229 3271 $table_name,
7230 - array('role_restriction' => $role_restriction),
7231 - array('id' => absint($entry_id)),
7232 - array('%s'),
7233 - array('%d')
7234 - );
7235 - }
7236 -
7237 - if ($result === false) {
7238 - wp_send_json_error('Database update failed: ' . $wpdb->last_error);
7239 - exit;
7240 - }
7241 -
7242 - wp_send_json_success(array(
7243 - 'message' => 'Role restriction updated successfully',
7244 - 'role_restriction' => $role_restriction,
7245 - 'data_source' => $data_source,
7246 - 'entry_id' => $entry_id
7247 - ));
7248 - exit;
7249 -}
7250 -
7251 -// ========================================
7252 -// ROLE-BASED CONTENT RESTRICTIONS - NEW FUNCTIONS
7253 -// Add these to your MxChat_Knowledge_Manager class
7254 -// ========================================
7255 -
7256 -/**
7257 - * Initialize role-based content hooks
7258 - * Add this call to your __construct() or mxchat_init_hooks() method
7259 - */
7260 -private function mxchat_init_role_hooks() {
7261 - // AJAX handlers for tag-role mappings
7262 - add_action('wp_ajax_mxchat_add_tag_role_mapping', array($this, 'ajax_add_tag_role_mapping'));
7263 - add_action('wp_ajax_mxchat_delete_tag_role_mapping', array($this, 'ajax_delete_tag_role_mapping'));
7264 - add_action('wp_ajax_mxchat_get_tag_role_mappings', array($this, 'ajax_get_tag_role_mappings'));
7265 - add_action('wp_ajax_mxchat_bulk_update_tag_roles', array($this, 'ajax_bulk_update_tag_roles'));
7266 -
7267 - // Hook to automatically update role restrictions when tags are added/removed
7268 - add_action('set_object_terms', array($this, 'handle_tag_change'), 10, 6);
7269 -
7270 - // Hook to apply role restrictions on auto-sync
7271 - add_action('mxchat_content_stored', array($this, 'apply_role_restriction_after_storage'), 10, 2);
7272 -}
7273 -
7274 -/**
7275 - * Add tag-role mapping via AJAX
7276 - */
7277 -public function ajax_add_tag_role_mapping() {
7278 - // Verify nonce and permissions
7279 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
7280 -
7281 - if (!current_user_can('manage_options')) {
7282 - wp_send_json_error('Unauthorized access');
7283 - exit;
7284 - }
7285 -
7286 - $tag_input = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
7287 - $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
7288 -
7289 - if (empty($tag_input)) {
7290 - wp_send_json_error('Please enter a tag name or slug');
7291 - exit;
7292 - }
7293 -
7294 - // Validate role restriction
7295 - $valid_roles = array_keys($this->mxchat_get_role_options());
7296 - if (!in_array($role_restriction, $valid_roles)) {
7297 - wp_send_json_error('Invalid role restriction');
7298 - exit;
7299 - }
7300 -
7301 - // Resolve the tag by slug first, then fall back to its display name, so users can
7302 - // enter either "premium-content" or "Premium Content". (plan b8bcf5 — the field is
7303 - // labeled by name but previously validated by slug only, producing the confusing
7304 - // "Tag does not exist in WordPress" error when a real tag's name was typed.)
7305 - $term = get_term_by('slug', $tag_input, 'post_tag');
7306 - if (!$term) {
7307 - $term = get_term_by('name', $tag_input, 'post_tag');
7308 - }
7309 - if (!$term) {
7310 - wp_send_json_error('No tag with that name or slug exists yet. Create it under Posts → Tags first, then enter its name or slug.');
7311 - exit;
7312 - }
7313 -
7314 - // Always key the mapping by the RESOLVED slug — apply_role_restriction_to_post()
7315 - // compares against each post's tag slugs, so the stored key must be a slug,
7316 - // never the raw (possibly display-name) input.
7317 - $tag_slug = $term->slug;
7318 -
7319 - // Get existing mappings
7320 - $mappings = get_option('mxchat_tag_role_mappings', array());
7321 -
7322 - // Check if mapping already exists
7323 - if (isset($mappings[$tag_slug])) {
7324 - wp_send_json_error('Mapping for this tag already exists');
7325 - exit;
7326 - }
7327 -
7328 - // Add new mapping
7329 - $mappings[$tag_slug] = $role_restriction;
7330 - update_option('mxchat_tag_role_mappings', $mappings);
7331 -
7332 - wp_send_json_success(array(
7333 - 'message' => 'Tag-role mapping added successfully',
7334 - 'tag_slug' => $tag_slug,
7335 - 'role_restriction' => $role_restriction
7336 - ));
7337 - exit;
7338 -}
7339 -
7340 -/**
7341 - * Delete tag-role mapping via AJAX
7342 - */
7343 -public function ajax_delete_tag_role_mapping() {
7344 - // Verify nonce and permissions
7345 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
7346 -
7347 - if (!current_user_can('manage_options')) {
7348 - wp_send_json_error('Unauthorized access');
7349 - exit;
7350 - }
7351 -
7352 - $tag_slug = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
7353 -
7354 - if (empty($tag_slug)) {
7355 - wp_send_json_error('Tag slug is required');
7356 - exit;
7357 - }
7358 -
7359 - // Get existing mappings
7360 - $mappings = get_option('mxchat_tag_role_mappings', array());
7361 -
7362 - // Check if mapping exists
7363 - if (!isset($mappings[$tag_slug])) {
7364 - wp_send_json_error('Mapping does not exist');
7365 - exit;
7366 - }
7367 -
7368 - // Remove mapping
7369 - unset($mappings[$tag_slug]);
7370 - update_option('mxchat_tag_role_mappings', $mappings);
7371 -
7372 - wp_send_json_success(array(
7373 - 'message' => 'Tag-role mapping deleted successfully',
7374 - 'tag_slug' => $tag_slug
7375 - ));
7376 - exit;
7377 -}
7378 -
7379 -/**
7380 - * Get all tag-role mappings via AJAX
7381 - */
7382 -public function ajax_get_tag_role_mappings() {
7383 - // Verify nonce and permissions
7384 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
7385 -
7386 - if (!current_user_can('manage_options')) {
7387 - wp_send_json_error('Unauthorized access');
7388 - exit;
7389 - }
7390 -
7391 - // Get mappings
7392 - $mappings = get_option('mxchat_tag_role_mappings', array());
7393 - $role_options = $this->mxchat_get_role_options();
7394 -
7395 - $formatted_mappings = array();
7396 -
7397 - foreach ($mappings as $tag_slug => $role_restriction) {
7398 - // Get tag object
7399 - $term = get_term_by('slug', $tag_slug, 'post_tag');
7400 -
7401 - // Count posts with this tag
7402 - $post_count = 0;
7403 - if ($term) {
7404 - $post_count = $term->count;
7405 - }
7406 -
7407 - $formatted_mappings[] = array(
7408 - 'tag_slug' => $tag_slug,
7409 - 'role_restriction' => $role_restriction,
7410 - 'role_label' => $role_options[$role_restriction] ?? $role_restriction,
7411 - 'post_count' => $post_count
7412 - );
7413 - }
7414 -
7415 - wp_send_json_success(array(
7416 - 'mappings' => $formatted_mappings
7417 - ));
7418 - exit;
7419 -}
7420 -
7421 -/**
7422 - * Bulk update role restrictions for all existing content with mapped tags
7423 - */
7424 -public function ajax_bulk_update_tag_roles() {
7425 - // Verify nonce and permissions
7426 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
7427 -
7428 - if (!current_user_can('manage_options')) {
7429 - wp_send_json_error('Unauthorized access');
7430 - exit;
7431 - }
7432 -
7433 - // Get mappings
7434 - $mappings = get_option('mxchat_tag_role_mappings', array());
7435 -
7436 - if (empty($mappings)) {
7437 - wp_send_json_error('No tag-role mappings found');
7438 - exit;
7439 - }
7440 -
7441 - global $wpdb;
7442 -
7443 - // Check if using Pinecone
7444 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
7445 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
7446 -
7447 - $updated_count = 0;
7448 - $details = array();
7449 -
7450 - foreach ($mappings as $tag_slug => $role_restriction) {
7451 - // Get all posts with this tag
7452 - $posts = get_posts(array(
7453 - 'tag' => $tag_slug,
7454 - 'post_type' => 'any',
7455 - 'posts_per_page' => -1,
7456 - 'fields' => 'ids',
7457 - 'post_status' => 'publish'
7458 - ));
7459 -
7460 - if (empty($posts)) {
7461 - continue;
7462 - }
7463 -
7464 - $tag_updated = 0;
7465 -
7466 - foreach ($posts as $post_id) {
7467 - $source_url = get_permalink($post_id);
7468 - if (!$source_url) {
7469 - continue;
7470 - }
7471 -
7472 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
7473 - // Update Pinecone role restriction
7474 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
7475 - $vector_id = md5($source_url);
7476 -
7477 - $result = $wpdb->replace(
7478 - $roles_table,
7479 - array(
7480 - 'vector_id' => $vector_id,
7481 - 'role_restriction' => $role_restriction,
7482 - 'updated_at' => current_time('mysql')
7483 - ),
7484 - array('%s', '%s', '%s')
7485 - );
7486 - } else {
7487 - // Update WordPress DB
7488 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
7489 -
7490 - $result = $wpdb->update(
7491 - $table_name,
7492 - array('role_restriction' => $role_restriction),
7493 - array('source_url' => $source_url),
7494 - array('%s'),
7495 - array('%s')
7496 - );
7497 - }
7498 -
7499 - if ($result !== false) {
7500 - $tag_updated++;
7501 - $updated_count++;
7502 - }
7503 - }
7504 -
7505 - if ($tag_updated > 0) {
7506 - $details[] = sprintf(
7507 - 'Tag "%s" (%s): %d posts updated',
7508 - $tag_slug,
7509 - $role_restriction,
7510 - $tag_updated
7511 - );
7512 - }
7513 - }
7514 -
7515 - wp_send_json_success(array(
7516 - 'message' => 'Bulk update completed',
7517 - 'updated_count' => $updated_count,
7518 - 'tags_processed' => count($mappings),
7519 - 'details' => $details
7520 - ));
7521 - exit;
7522 -}
7523 -
7524 -/**
7525 - * Handle tag changes on posts (when tags are added or removed)
7526 - */
7527 -public function handle_tag_change($object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids) {
7528 - // Only process post tags
7529 - if ($taxonomy !== 'post_tag') {
7530 - return;
7531 - }
7532 -
7533 - // Get tag-role mappings
7534 - $mappings = get_option('mxchat_tag_role_mappings', array());
7535 -
7536 - if (empty($mappings)) {
7537 - return;
7538 - }
7539 -
7540 - // Get the post's URL
7541 - $source_url = get_permalink($object_id);
7542 - if (!$source_url) {
7543 - return;
7544 - }
7545 -
7546 - // Determine the highest role restriction based on tags
7547 - $highest_role = 'public';
7548 - $role_hierarchy = array(
7549 - 'public' => 0,
7550 - 'logged_in' => 1,
7551 - 'subscriber' => 2,
7552 - 'contributor' => 3,
7553 - 'author' => 4,
7554 - 'editor' => 5,
7555 - 'administrator' => 6
7556 - );
7557 -
7558 - // Get all current tags for the post
7559 - $current_tags = wp_get_post_tags($object_id, array('fields' => 'slugs'));
7560 -
7561 - // Find the highest role restriction among the tags
7562 - foreach ($current_tags as $tag_slug) {
7563 - if (isset($mappings[$tag_slug])) {
7564 - $role = $mappings[$tag_slug];
7565 - if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
7566 - $highest_role = $role;
7567 - }
7568 - }
7569 - }
7570 -
7571 - // Update the role restriction in the database
7572 - global $wpdb;
7573 -
7574 - // Check if using Pinecone
7575 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
7576 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
7577 -
7578 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
7579 - // Update Pinecone role restriction
7580 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
7581 - $vector_id = md5($source_url);
7582 -
7583 - $wpdb->replace(
7584 - $roles_table,
7585 - array(
7586 - 'vector_id' => $vector_id,
7587 - 'role_restriction' => $highest_role,
7588 - 'updated_at' => current_time('mysql')
7589 - ),
7590 - array('%s', '%s', '%s')
7591 - );
7592 - } else {
7593 - // Update WordPress DB
7594 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
7595 -
7596 - $wpdb->update(
7597 - $table_name,
7598 - array('role_restriction' => $highest_role),
7599 3272 array('source_url' => $source_url),
7600 - array('%s'),
7601 3273 array('%s')
7602 3274 );
7603 3275 }
7604 3276 }
7605 -
7606 -/**
7607 - * Apply role restriction after content is stored (for auto-sync)
7608 - */
7609 -public function apply_role_restriction_after_storage($post_id, $source_url) {
7610 - // Get tag-role mappings
7611 - $mappings = get_option('mxchat_tag_role_mappings', array());
7612 3277
7613 - if (empty($mappings)) {
7614 - return;
7615 - }
7616 -
7617 - // Get all tags for the post
7618 - $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
7619 -
7620 - if (empty($post_tags)) {
7621 - return;
7622 - }
7623 -
7624 - // Determine the highest role restriction based on tags
7625 - $highest_role = 'public';
7626 - $role_hierarchy = array(
7627 - 'public' => 0,
7628 - 'logged_in' => 1,
7629 - 'subscriber' => 2,
7630 - 'contributor' => 3,
7631 - 'author' => 4,
7632 - 'editor' => 5,
7633 - 'administrator' => 6
7634 - );
7635 -
7636 - foreach ($post_tags as $tag_slug) {
7637 - if (isset($mappings[$tag_slug])) {
7638 - $role = $mappings[$tag_slug];
7639 - if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
7640 - $highest_role = $role;
7641 - }
7642 - }
7643 - }
7644 -
7645 - // If no restricted tags found, return (leave as public)
7646 - if ($highest_role === 'public') {
7647 - return;
7648 - }
7649 -
7650 - // Update the role restriction
7651 - global $wpdb;
7652 -
7653 - // Check if using Pinecone
7654 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
7655 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
7656 -
7657 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
7658 - // Update Pinecone role restriction
7659 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
7660 - $vector_id = md5($source_url);
7661 -
7662 - $wpdb->replace(
7663 - $roles_table,
7664 - array(
7665 - 'vector_id' => $vector_id,
7666 - 'role_restriction' => $highest_role,
7667 - 'updated_at' => current_time('mysql')
7668 - ),
7669 - array('%s', '%s', '%s')
7670 - );
7671 - } else {
7672 - // Update WordPress DB
7673 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
7674 -
7675 - $wpdb->update(
7676 - $table_name,
7677 - array('role_restriction' => $highest_role),
7678 - array('source_url' => $source_url),
7679 - array('%s'),
7680 - array('%s')
7681 - );
7682 - }
7683 -}
7684 -
7685 -
7686 3278 // ========================================
7687 3279 // HELPER METHODS
7688 3280 // ========================================
7689 3281
@@ -7750,1074 +3342,8 @@
7750 3342 */
7751 3343 private function mxchat_get_pinecone_manager() {
7752 3344 return MxChat_Pinecone_Manager::get_instance();
7753 3345 }
7754 -
7755 -
7756 - // ========================================
7757 -// DATABASE QUEUE TABLE MANAGEMENT
7758 -// ========================================
7759 -
7760 -/**
7761 - * Create queue table on plugin activation
7762 - * Call this from your plugin activation hook
7763 - */
7764 -public function mxchat_create_queue_table() {
7765 - global $wpdb;
7766 -
7767 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7768 - $charset_collate = $wpdb->get_charset_collate();
7769 -
7770 - $sql = "CREATE TABLE IF NOT EXISTS $table_name (
7771 - id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
7772 - queue_id varchar(64) NOT NULL,
7773 - item_type varchar(20) NOT NULL,
7774 - item_data longtext NOT NULL,
7775 - status varchar(20) NOT NULL DEFAULT 'pending',
7776 - bot_id varchar(50) NOT NULL DEFAULT 'default',
7777 - priority int(11) NOT NULL DEFAULT 0,
7778 - attempts int(11) NOT NULL DEFAULT 0,
7779 - max_attempts int(11) NOT NULL DEFAULT 3,
7780 - error_message text DEFAULT NULL,
7781 - created_at datetime NOT NULL,
7782 - started_at datetime DEFAULT NULL,
7783 - completed_at datetime DEFAULT NULL,
7784 - PRIMARY KEY (id),
7785 - KEY queue_id (queue_id),
7786 - KEY status (status),
7787 - KEY item_type (item_type),
7788 - KEY priority (priority)
7789 - ) $charset_collate;";
7790 -
7791 - require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
7792 - dbDelta($sql);
7793 -
7794 - // Also create a meta table for queue metadata
7795 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
7796 -
7797 - $meta_sql = "CREATE TABLE IF NOT EXISTS $meta_table (
7798 - id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
7799 - queue_id varchar(64) NOT NULL,
7800 - meta_key varchar(255) NOT NULL,
7801 - meta_value longtext,
7802 - PRIMARY KEY (id),
7803 - KEY queue_id (queue_id),
7804 - KEY meta_key (meta_key)
7805 - ) $charset_collate;";
7806 -
7807 - dbDelta($meta_sql);
7808 -}
7809 -
7810 -/**
7811 - * Add items to the processing queue
7812 - *
7813 - * @param string $queue_id Unique identifier for this queue batch
7814 - * @param string $item_type Type of item (url, pdf_page)
7815 - * @param array $items Array of items to queue
7816 - * @param string $bot_id Bot ID for processing
7817 - * @return int Number of items queued
7818 - */
7819 -private function mxchat_add_to_queue($queue_id, $item_type, $items, $bot_id = 'default') {
7820 - global $wpdb;
7821 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7822 -
7823 - $queued_count = 0;
7824 - $priority = 0;
7825 -
7826 - foreach ($items as $item) {
7827 - $result = $wpdb->insert(
7828 - $table_name,
7829 - array(
7830 - 'queue_id' => $queue_id,
7831 - 'item_type' => $item_type,
7832 - 'item_data' => wp_json_encode($item),
7833 - 'status' => 'pending',
7834 - 'bot_id' => $bot_id,
7835 - 'priority' => $priority,
7836 - 'attempts' => 0,
7837 - 'max_attempts' => 3,
7838 - 'created_at' => current_time('mysql')
7839 - ),
7840 - array('%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%s')
7841 - );
7842 -
7843 - if ($result) {
7844 - $queued_count++;
7845 - }
7846 -
7847 - $priority++; // Process in order
7848 - }
7849 -
7850 - return $queued_count;
7851 -}
7852 -
7853 -/**
7854 - * Store queue metadata (total counts, source URL, etc.)
7855 - */
7856 -private function mxchat_set_queue_meta($queue_id, $meta_key, $meta_value) {
7857 - global $wpdb;
7858 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
7859 -
7860 - // Check if meta exists
7861 - $existing = $wpdb->get_var($wpdb->prepare(
7862 - "SELECT id FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
7863 - $queue_id,
7864 - $meta_key
7865 - ));
7866 -
7867 - if ($existing) {
7868 - // Update
7869 - $wpdb->update(
7870 - $meta_table,
7871 - array('meta_value' => maybe_serialize($meta_value)),
7872 - array('queue_id' => $queue_id, 'meta_key' => $meta_key),
7873 - array('%s'),
7874 - array('%s', '%s')
7875 - );
7876 - } else {
7877 - // Insert
7878 - $wpdb->insert(
7879 - $meta_table,
7880 - array(
7881 - 'queue_id' => $queue_id,
7882 - 'meta_key' => $meta_key,
7883 - 'meta_value' => maybe_serialize($meta_value)
7884 - ),
7885 - array('%s', '%s', '%s')
7886 - );
7887 - }
7888 -}
7889 -
7890 -/**
7891 - * Get queue metadata
7892 - */
7893 -private function mxchat_get_queue_meta($queue_id, $meta_key) {
7894 - global $wpdb;
7895 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
7896 -
7897 - $value = $wpdb->get_var($wpdb->prepare(
7898 - "SELECT meta_value FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
7899 - $queue_id,
7900 - $meta_key
7901 - ));
7902 -
7903 - return maybe_unserialize($value);
7904 -}
7905 -
7906 -// ========================================
7907 -// AJAX QUEUE PROCESSING HANDLERS
7908 -// ========================================
7909 -
7910 -/**
7911 - * AJAX: Get next item from queue to process
7912 - */
7913 -public function ajax_mxchat_get_next_queue_item() {
7914 - // Verify nonce and permissions
7915 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7916 -
7917 - if (!current_user_can('manage_options')) {
7918 - wp_send_json_error('Unauthorized access');
7919 - }
7920 -
7921 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
7922 -
7923 - if (empty($queue_id)) {
7924 - wp_send_json_error('Missing queue ID');
7925 - }
7926 -
7927 - global $wpdb;
7928 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7929 -
7930 - // Get next pending item with retry logic for failed items
7931 - $next_item = $wpdb->get_row($wpdb->prepare(
7932 - "SELECT * FROM $table_name
7933 - WHERE queue_id = %s
7934 - AND status IN ('pending', 'failed')
7935 - AND attempts < max_attempts
7936 - ORDER BY priority ASC, id ASC
7937 - LIMIT 1",
7938 - $queue_id
7939 - ));
7940 -
7941 - if (!$next_item) {
7942 - // No more items - queue complete
7943 - wp_send_json_success(array(
7944 - 'complete' => true,
7945 - 'message' => 'Queue processing complete'
7946 - ));
7947 - }
7948 -
7949 - // Mark item as processing
7950 - $wpdb->update(
7951 - $table_name,
7952 - array(
7953 - 'status' => 'processing',
7954 - 'started_at' => current_time('mysql'),
7955 - 'attempts' => $next_item->attempts + 1
7956 - ),
7957 - array('id' => $next_item->id),
7958 - array('%s', '%s', '%d'),
7959 - array('%d')
7960 - );
7961 -
7962 - wp_send_json_success(array(
7963 - 'complete' => false,
7964 - 'item' => array(
7965 - 'id' => $next_item->id,
7966 - 'type' => $next_item->item_type,
7967 - 'data' => json_decode($next_item->item_data, true),
7968 - 'bot_id' => $next_item->bot_id,
7969 - 'attempt' => $next_item->attempts + 1
7970 - )
7971 - ));
7972 -}
7973 -
7974 -/**
7975 - * AJAX: Process a single queue item
7976 - */
7977 -public function ajax_mxchat_process_queue_item() {
7978 - // Verify nonce and permissions
7979 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7980 -
7981 - if (!current_user_can('manage_options')) {
7982 - wp_send_json_error('Unauthorized access');
7983 - }
7984 -
7985 - $item_id = isset($_POST['item_id']) ? absint($_POST['item_id']) : 0;
7986 - $item_type = isset($_POST['item_type']) ? sanitize_text_field($_POST['item_type']) : '';
7987 - $item_data = isset($_POST['item_data']) ? $_POST['item_data'] : array();
7988 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
7989 -
7990 - if (empty($item_id) || empty($item_type)) {
7991 - wp_send_json_error('Missing item data');
7992 - }
7993 -
7994 - global $wpdb;
7995 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7996 -
7997 - // Process based on item type
7998 - try {
7999 - set_time_limit(60); // Give processing 60 seconds
8000 -
8001 - $result = false;
8002 - $error_message = '';
8003 -
8004 - // Read item directly from DB to get queue_id and preserve special chars in item_data
8005 - // (POST round-trip through JS mangles characters like apostrophes in URLs)
8006 - $db_item = $wpdb->get_row($wpdb->prepare(
8007 - "SELECT queue_id, item_data FROM $table_name WHERE id = %d",
8008 - $item_id
8009 - ));
8010 - $item_queue_id = $db_item ? $db_item->queue_id : '';
8011 - if ($db_item && !empty($db_item->item_data)) {
8012 - $db_data = json_decode($db_item->item_data, true);
8013 - if (is_array($db_data)) {
8014 - $item_data = $db_data;
8015 - }
8016 - }
8017 -
8018 - switch ($item_type) {
8019 - case 'url':
8020 - $result = $this->mxchat_process_queue_url($item_data, $bot_id, $item_queue_id);
8021 - break;
8022 -
8023 - case 'pdf_page':
8024 - $result = $this->mxchat_process_queue_pdf_page($item_data, $bot_id);
8025 - break;
8026 -
8027 - default:
8028 - throw new Exception('Unknown item type: ' . $item_type);
8029 - }
8030 -
8031 - if (is_wp_error($result)) {
8032 - $error_code = $result->get_error_code();
8033 - // Content errors (empty page, sanitization) are permanent — retrying won't help
8034 - $permanent_codes = array('empty_page', 'empty_after_sanitization', 'no_api_key', 'page_not_found');
8035 - if (in_array($error_code, $permanent_codes)) {
8036 - // Mark as permanently failed — set attempts = max_attempts so it won't be retried
8037 - $current_item = $wpdb->get_row($wpdb->prepare(
8038 - "SELECT max_attempts FROM $table_name WHERE id = %d", $item_id
8039 - ));
8040 - $wpdb->update(
8041 - $table_name,
8042 - array(
8043 - 'status' => 'failed',
8044 - 'error_message' => $result->get_error_message(),
8045 - 'attempts' => $current_item ? $current_item->max_attempts : 3
8046 - ),
8047 - array('id' => $item_id),
8048 - array('%s', '%s', '%d'),
8049 - array('%d')
8050 - );
8051 - wp_send_json_error(array(
8052 - 'message' => $result->get_error_message(),
8053 - 'permanent_failure' => true,
8054 - 'item_id' => $item_id
8055 - ));
8056 - return;
8057 - }
8058 - throw new Exception($result->get_error_message());
8059 - }
8060 -
8061 - if ($result === false) {
8062 - throw new Exception('Processing returned false - item may be empty or invalid');
8063 - }
8064 -
8065 - // Mark as completed
8066 - $wpdb->update(
8067 - $table_name,
8068 - array(
8069 - 'status' => 'completed',
8070 - 'completed_at' => current_time('mysql'),
8071 - 'error_message' => null
8072 - ),
8073 - array('id' => $item_id),
8074 - array('%s', '%s', '%s'),
8075 - array('%d')
8076 - );
8077 -
8078 - wp_send_json_success(array(
8079 - 'processed' => true,
8080 - 'item_id' => $item_id,
8081 - 'message' => 'Item processed successfully'
8082 - ));
8083 -
8084 - } catch (Exception $e) {
8085 - $error_message = $e->getMessage();
8086 -
8087 - // Get current attempt count
8088 - $item = $wpdb->get_row($wpdb->prepare(
8089 - "SELECT attempts, max_attempts FROM $table_name WHERE id = %d",
8090 - $item_id
8091 - ));
8092 -
8093 - // Check if we've exhausted retries
8094 - if ($item && $item->attempts >= $item->max_attempts) {
8095 - // Permanently failed
8096 - $wpdb->update(
8097 - $table_name,
8098 - array(
8099 - 'status' => 'failed',
8100 - 'error_message' => $error_message
8101 - ),
8102 - array('id' => $item_id),
8103 - array('%s', '%s'),
8104 - array('%d')
8105 - );
8106 -
8107 - wp_send_json_error(array(
8108 - 'message' => 'Item failed after maximum attempts: ' . $error_message,
8109 - 'permanent_failure' => true,
8110 - 'item_id' => $item_id
8111 - ));
8112 - } else {
8113 - // Mark for retry
8114 - $wpdb->update(
8115 - $table_name,
8116 - array(
8117 - 'status' => 'failed',
8118 - 'error_message' => $error_message
8119 - ),
8120 - array('id' => $item_id),
8121 - array('%s', '%s'),
8122 - array('%d')
8123 - );
8124 -
8125 - wp_send_json_error(array(
8126 - 'message' => 'Item processing failed, will retry: ' . $error_message,
8127 - 'can_retry' => true,
8128 - 'item_id' => $item_id,
8129 - 'attempts' => $item ? $item->attempts : 0
8130 - ));
8131 - }
8132 - }
8133 -}
8134 -
8135 -/**
8136 - * Process a URL from the queue
8137 - */
8138 -private function mxchat_process_queue_url($item_data, $bot_id = 'default', $queue_id = '') {
8139 - $url = isset($item_data['url']) ? $item_data['url'] : '';
8140 -
8141 - if (empty($url)) {
8142 - return new WP_Error('invalid_url', 'URL is empty');
8143 - }
8144 -
8145 - // Get bot-specific API key early (needed for both paths)
8146 - $bot_options = $this->get_bot_options($bot_id);
8147 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
8148 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
8149 -
8150 - if (strpos($selected_model, 'voyage') === 0) {
8151 - $api_key = $options['voyage_api_key'] ?? '';
8152 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
8153 - $api_key = $options['gemini_api_key'] ?? '';
8154 - } else {
8155 - $api_key = $options['api_key'] ?? '';
8156 - }
8157 -
8158 - if (empty($api_key)) {
8159 - return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
8160 - }
8161 -
8162 - // Check if this is a WooCommerce product URL and WooCommerce is active
8163 - $is_product_url = (strpos($url, '/product/') !== false || strpos($url, '/shop/') !== false);
8164 - $content_type = $is_product_url ? 'product' : 'url';
8165 -
8166 - // Try to get WooCommerce product data if it's a product URL
8167 - if ($is_product_url && class_exists('WooCommerce')) {
8168 - $product_content = $this->mxchat_extract_woocommerce_product_content($url);
8169 -
8170 - if (!empty($product_content)) {
8171 - // Successfully extracted WooCommerce product data with pricing
8172 - $result = MxChat_Utils::submit_content_to_db(
8173 - $product_content,
8174 - $url,
8175 - $api_key,
8176 - null,
8177 - $bot_id,
8178 - 'product'
8179 - );
8180 - return $result;
8181 - }
8182 - // If WooCommerce extraction failed, fall through to HTML extraction
8183 - }
8184 -
8185 - // Fetch URL content (fallback for non-products or when WooCommerce extraction fails)
8186 - $is_likely_pdf = (strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?: '', PATHINFO_EXTENSION)) === 'pdf');
8187 - $response = wp_remote_get($url, array(
8188 - 'timeout' => $is_likely_pdf ? 120 : 30,
8189 - 'redirection' => 5,
8190 - 'user-agent' => mxchat_ingest_user_agent(),
8191 - ));
8192 -
8193 - if (is_wp_error($response)) {
8194 - return $response;
8195 - }
8196 -
8197 - $response_code = wp_remote_retrieve_response_code($response);
8198 - if ($response_code !== 200) {
8199 - return new WP_Error('http_error', 'HTTP ' . $response_code . ' error for: ' . $url);
8200 - }
8201 -
8202 - // Check if URL is a PDF — expand into per-page queue items using the standard PDF pipeline
8203 - if ($this->mxchat_is_pdf_url($url, $response)) {
8204 - return $this->mxchat_expand_pdf_to_queue($url, $response, $bot_id, $queue_id);
8205 - }
8206 -
8207 - $html = wp_remote_retrieve_body($response);
8208 -
8209 - if (empty($html)) {
8210 - return new WP_Error('empty_response', 'Empty response body');
8211 - }
8212 -
8213 - // Extract and sanitize content
8214 - $content = $this->mxchat_extract_main_content($html);
8215 - $sanitized = $this->mxchat_sanitize_content_for_api($content);
8216 -
8217 - if (empty($sanitized)) {
8218 - // Not an error - just no content found (maybe a redirect or empty page)
8219 - return false;
8220 - }
8221 -
8222 - // Submit to database with content_type
8223 - $result = MxChat_Utils::submit_content_to_db(
8224 - $sanitized,
8225 - $url,
8226 - $api_key,
8227 - null,
8228 - $bot_id,
8229 - $content_type
8230 - );
8231 -
8232 - return $result;
8233 -}
8234 -
8235 -/**
8236 - * Expand a PDF URL into per-page queue items using the standard PDF pipeline.
8237 - * Called when a sitemap URL turns out to be a PDF — downloads, parses page count,
8238 - * and adds pdf_page items to the same queue so they process with full progress tracking.
8239 - */
8240 -private function mxchat_expand_pdf_to_queue($pdf_url, $response, $bot_id = 'default', $queue_id = '') {
8241 - set_time_limit(120); // PDFs need extra time for download + parsing
8242 -
8243 - $upload_dir = wp_upload_dir();
8244 - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf');
8245 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
8246 -
8247 - $response_body = wp_remote_retrieve_body($response);
8248 - if (empty($response_body)) {
8249 - return new WP_Error('empty_pdf', 'Empty PDF response for: ' . $pdf_url);
8250 - }
8251 -
8252 - if (!wp_mkdir_p(dirname($pdf_path))) {
8253 - return new WP_Error('dir_error', 'Failed to create upload directory');
8254 - }
8255 -
8256 - file_put_contents($pdf_path, $response_body);
8257 -
8258 - if (!file_exists($pdf_path)) {
8259 - return new WP_Error('save_error', 'Failed to save PDF file');
8260 - }
8261 -
8262 - try {
8263 - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
8264 -
8265 - if ($total_pages === false || $total_pages < 1) {
8266 - wp_delete_file($pdf_path);
8267 - return new WP_Error('no_pages', 'PDF has no pages: ' . $pdf_url);
8268 - }
8269 -
8270 - // Build per-page items identical to mxchat_handle_pdf_for_knowledge_base
8271 - $pages = array();
8272 - for ($i = 1; $i <= $total_pages; $i++) {
8273 - $pages[] = array(
8274 - 'pdf_path' => $pdf_path,
8275 - 'pdf_url' => $pdf_url,
8276 - 'page_number' => $i,
8277 - 'total_pages' => $total_pages
8278 - );
8279 - }
8280 -
8281 - // Add pdf_page items to the SAME queue so the JS picks them up automatically
8282 - if (!empty($queue_id)) {
8283 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
8284 - } else {
8285 - // Fallback: create a new PDF queue (shouldn't happen in sitemap flow)
8286 - $new_queue_id = 'pdf_' . md5($pdf_url . time());
8287 - $queued_count = $this->mxchat_add_to_queue($new_queue_id, 'pdf_page', $pages, $bot_id);
8288 - $this->mxchat_set_queue_meta($new_queue_id, 'source_url', $pdf_url);
8289 - $this->mxchat_set_queue_meta($new_queue_id, 'queue_type', 'pdf');
8290 - $this->mxchat_set_queue_meta($new_queue_id, 'total_items', $total_pages);
8291 - $this->mxchat_set_queue_meta($new_queue_id, 'bot_id', $bot_id);
8292 - $this->mxchat_set_queue_meta($new_queue_id, 'pdf_path', $pdf_path);
8293 - $this->mxchat_set_queue_meta($new_queue_id, 'created_at', current_time('mysql'));
8294 - }
8295 -
8296 - if ($queued_count === 0) {
8297 - wp_delete_file($pdf_path);
8298 - return new WP_Error('queue_error', 'Failed to add PDF pages to queue');
8299 - }
8300 -
8301 - // Return true so the original URL item is marked complete
8302 - // The new pdf_page items will be processed in subsequent batches
8303 - return true;
8304 -
8305 - } catch (Exception $e) {
8306 - if (file_exists($pdf_path)) {
8307 - wp_delete_file($pdf_path);
8308 - }
8309 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
8310 - }
8311 -}
8312 -
8313 -/**
8314 - * Legacy: Process a PDF URL inline during sitemap queue processing.
8315 - * @deprecated Use mxchat_expand_pdf_to_queue instead — kept for reference only.
8316 - */
8317 -private function mxchat_process_pdf_url_inline($pdf_url, $response, $api_key, $bot_id = 'default') {
8318 - set_time_limit(120); // PDFs need more time — downloading + parsing all pages
8319 -
8320 - $upload_dir = wp_upload_dir();
8321 - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf');
8322 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
8323 -
8324 - $response_body = wp_remote_retrieve_body($response);
8325 - if (empty($response_body)) {
8326 - return new WP_Error('empty_pdf', 'Empty PDF response');
8327 - }
8328 -
8329 - if (!wp_mkdir_p(dirname($pdf_path))) {
8330 - return new WP_Error('dir_error', 'Failed to create upload directory');
8331 - }
8332 -
8333 - file_put_contents($pdf_path, $response_body);
8334 -
8335 - if (!file_exists($pdf_path)) {
8336 - return new WP_Error('save_error', 'Failed to save PDF file');
8337 - }
8338 -
8339 - try {
8340 - mxchat_load_pdf_parser();
8341 - $parser = new \Smalot\PdfParser\Parser();
8342 - $pdf = $parser->parseFile($pdf_path);
8343 - $pages = $pdf->getPages();
8344 - $total_pages = count($pages);
8345 -
8346 - if ($total_pages < 1) {
8347 - wp_delete_file($pdf_path);
8348 - return new WP_Error('no_pages', 'PDF has no pages');
8349 - }
8350 -
8351 - $processed = 0;
8352 - $skipped_pages = array();
8353 -
8354 - for ($i = 0; $i < $total_pages; $i++) {
8355 - $page_num = $i + 1;
8356 - $text = $pages[$i]->getText();
8357 - if (empty($text)) {
8358 - $skipped_pages[] = 'Page ' . $page_num . ': No text could be extracted — page may contain only images, links, or non-standard encoding';
8359 - continue;
8360 - }
8361 -
8362 - $sanitized = $this->mxchat_sanitize_content_for_api($text);
8363 - if (empty($sanitized)) {
8364 - $skipped_pages[] = 'Page ' . $page_num . ': Text was extracted but contained only special characters, control codes, or unsupported content';
8365 - continue;
8366 - }
8367 -
8368 - $metadata = array(
8369 - 'document_type' => 'pdf',
8370 - 'total_pages' => $total_pages,
8371 - 'current_page' => $page_num,
8372 - 'source_url' => $pdf_url,
8373 - );
8374 -
8375 - $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized;
8376 - $page_url = esc_url($pdf_url . '#page=' . $page_num);
8377 -
8378 - MxChat_Utils::submit_content_to_db(
8379 - $content_with_metadata,
8380 - $page_url,
8381 - $api_key,
8382 - null,
8383 - $bot_id,
8384 - 'pdf'
8385 - );
8386 -
8387 - $processed++;
8388 - }
8389 -
8390 - // Clean up the temp PDF file
8391 - wp_delete_file($pdf_path);
8392 -
8393 - if (!empty($skipped_pages)) {
8394 - error_log('MxChat PDF: Skipped ' . count($skipped_pages) . ' of ' . $total_pages . ' pages: ' . implode('; ', $skipped_pages));
8395 - }
8396 -
8397 - return $processed > 0 ? true : false;
8398 -
8399 - } catch (Exception $e) {
8400 - if (file_exists($pdf_path)) {
8401 - wp_delete_file($pdf_path);
8402 - }
8403 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
8404 - }
8405 -}
8406 -
8407 -/**
8408 - * Extract WooCommerce product content including pricing
8409 - *
8410 - * @param string $url The product URL
8411 - * @return string|false Product content with pricing, or false if not found
8412 - */
8413 -private function mxchat_extract_woocommerce_product_content($url) {
8414 - // Try to get product ID from URL
8415 - $product_id = url_to_postid($url);
8416 -
8417 - // If url_to_postid fails, try to extract from URL pattern
8418 - if (!$product_id) {
8419 - $product_slug = '';
8420 -
8421 - // Handle pretty permalinks: /product/product-name/
8422 - if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
8423 - $product_slug = $matches[1];
8424 - }
8425 -
8426 - if (!empty($product_slug)) {
8427 - $product_post = get_page_by_path($product_slug, OBJECT, 'product');
8428 - if ($product_post) {
8429 - $product_id = $product_post->ID;
8430 - }
8431 - }
8432 - }
8433 -
8434 - if (!$product_id) {
8435 - return false;
8436 - }
8437 -
8438 - // Get WooCommerce product object
8439 - $product = wc_get_product($product_id);
8440 -
8441 - if (!$product) {
8442 - return false;
8443 - }
8444 -
8445 - // Build product content with pricing (similar to mxchat_store_product_embedding)
8446 - $title = $product->get_name();
8447 - $description = $product->get_description();
8448 - $short_description = $product->get_short_description();
8449 - $sku = $product->get_sku();
8450 -
8451 - // Get pricing information
8452 - $regular_price = $product->get_regular_price();
8453 - $sale_price = $product->get_sale_price();
8454 - $price = $product->get_price(); // Current active price
8455 -
8456 - // Get currency symbol
8457 - $currency_symbol = get_woocommerce_currency_symbol();
8458 -
8459 - // Format content
8460 - $content = $title . "\n\n";
8461 -
8462 - if (!empty($short_description)) {
8463 - $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
8464 - }
8465 -
8466 - if (!empty($description)) {
8467 - $content .= wp_strip_all_tags($description) . "\n\n";
8468 - }
8469 -
8470 - // Add pricing information
8471 - if (!empty($regular_price)) {
8472 - $content .= "Price: " . $currency_symbol . $regular_price . "\n";
8473 - } elseif (!empty($price)) {
8474 - $content .= "Price: " . $currency_symbol . $price . "\n";
8475 - }
8476 -
8477 - if (!empty($sale_price) && $sale_price !== $regular_price) {
8478 - $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
8479 - }
8480 -
8481 - // Handle variable products - show price range
8482 - if ($product->is_type('variable')) {
8483 - $min_price = $product->get_variation_price('min');
8484 - $max_price = $product->get_variation_price('max');
8485 - if ($min_price !== $max_price) {
8486 - $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
8487 - }
8488 - }
8489 -
8490 - if (!empty($sku)) {
8491 - $content .= "SKU: " . $sku . "\n";
8492 - }
8493 -
8494 - // Get product categories
8495 - $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
8496 - if (!empty($categories) && !is_wp_error($categories)) {
8497 - $content .= "Categories: " . implode(', ', $categories) . "\n";
8498 - }
8499 -
8500 - // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
8501 - $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
8502 - if (!empty($custom_tabs) && is_array($custom_tabs)) {
8503 - foreach ($custom_tabs as $tab) {
8504 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
8505 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
8506 -
8507 - if (!empty($tab_title) && !empty($tab_content)) {
8508 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
8509 - }
8510 - }
8511 - }
8512 -
8513 - // Also check for reusable/saved tabs applied to this product
8514 - $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
8515 - if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
8516 - $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
8517 - if (!empty($saved_tabs) && is_array($saved_tabs)) {
8518 - foreach ($applied_saved_tabs as $saved_tab_id) {
8519 - if (isset($saved_tabs[$saved_tab_id])) {
8520 - $tab = $saved_tabs[$saved_tab_id];
8521 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
8522 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
8523 -
8524 - if (!empty($tab_title) && !empty($tab_content)) {
8525 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
8526 - }
8527 - }
8528 - }
8529 - }
8530 - }
8531 -
8532 - return $this->mxchat_sanitize_content_for_api($content);
8533 -}
8534 -
8535 -/**
8536 - * Process a PDF page from the queue
8537 - */
8538 -private function mxchat_process_queue_pdf_page($item_data, $bot_id = 'default') {
8539 - $pdf_path = isset($item_data['pdf_path']) ? $item_data['pdf_path'] : '';
8540 - $pdf_url = isset($item_data['pdf_url']) ? $item_data['pdf_url'] : '';
8541 - $page_number = isset($item_data['page_number']) ? absint($item_data['page_number']) : 0;
8542 - $total_pages = isset($item_data['total_pages']) ? absint($item_data['total_pages']) : 0;
8543 -
8544 - if (empty($pdf_path) || !file_exists($pdf_path)) {
8545 - return new WP_Error('pdf_not_found', 'PDF file not found: ' . $pdf_path);
8546 - }
8547 -
8548 - if ($page_number < 1) {
8549 - return new WP_Error('invalid_page', 'Invalid page number');
8550 - }
8551 -
8552 - try {
8553 - mxchat_load_pdf_parser();
8554 - $parser = new \Smalot\PdfParser\Parser();
8555 - $pdf = $parser->parseFile($pdf_path);
8556 - $pages = $pdf->getPages();
8557 -
8558 - if (!isset($pages[$page_number - 1])) {
8559 - return new WP_Error('page_not_found', 'Page ' . $page_number . ' not found in PDF');
8560 - }
8561 -
8562 - $text = $pages[$page_number - 1]->getText();
8563 -
8564 - if (empty($text)) {
8565 - return new WP_Error('empty_page', 'Page ' . $page_number . ': No text could be extracted — page may contain only images, links, or non-standard encoding');
8566 - }
8567 -
8568 - $sanitized = $this->mxchat_sanitize_content_for_api($text);
8569 -
8570 - if (empty($sanitized)) {
8571 - 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');
8572 - }
8573 -
8574 - // Create metadata
8575 - $metadata = array(
8576 - 'document_type' => 'pdf',
8577 - 'total_pages' => $total_pages,
8578 - 'current_page' => $page_number,
8579 - 'source_url' => $pdf_url
8580 - );
8581 -
8582 - $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized;
8583 - $page_url = esc_url($pdf_url . "#page=" . $page_number);
8584 -
8585 - // Get bot-specific API key
8586 - $bot_options = $this->get_bot_options($bot_id);
8587 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
8588 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
8589 -
8590 - if (strpos($selected_model, 'voyage') === 0) {
8591 - $api_key = $options['voyage_api_key'] ?? '';
8592 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
8593 - $api_key = $options['gemini_api_key'] ?? '';
8594 - } else {
8595 - $api_key = $options['api_key'] ?? '';
8596 - }
8597 -
8598 - if (empty($api_key)) {
8599 - return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
8600 - }
8601 -
8602 - // Submit to database - UPDATED 2.5.6: Added content_type 'pdf'
8603 - $result = MxChat_Utils::submit_content_to_db(
8604 - $content_with_metadata,
8605 - $page_url,
8606 - $api_key,
8607 - null,
8608 - $bot_id,
8609 - 'pdf'
8610 - );
8611 -
8612 - return $result;
8613 -
8614 - } catch (Exception $e) {
8615 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
8616 - }
8617 -}
8618 -
8619 -/**
8620 - * AJAX: Get queue processing status
8621 - */
8622 -public function ajax_mxchat_get_queue_status() {
8623 - // Verify nonce and permissions
8624 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
8625 -
8626 - if (!current_user_can('manage_options')) {
8627 - wp_send_json_error('Unauthorized access');
8628 - }
8629 -
8630 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
8631 -
8632 - if (empty($queue_id)) {
8633 - wp_send_json_error('Missing queue ID');
8634 - }
8635 -
8636 - global $wpdb;
8637 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
8638 -
8639 - // Get counts by status
8640 - $counts = $wpdb->get_results($wpdb->prepare(
8641 - "SELECT status, COUNT(*) as count
8642 - FROM $table_name
8643 - WHERE queue_id = %s
8644 - GROUP BY status",
8645 - $queue_id
8646 - ), OBJECT_K);
8647 -
8648 - $total = 0;
8649 - $completed = 0;
8650 - $failed = 0;
8651 - $processing = 0;
8652 - $pending = 0;
8653 -
8654 - foreach ($counts as $status => $data) {
8655 - $count = absint($data->count);
8656 - $total += $count;
8657 -
8658 - switch ($status) {
8659 - case 'completed':
8660 - $completed = $count;
8661 - break;
8662 - case 'failed':
8663 - $failed = $count;
8664 - break;
8665 - case 'processing':
8666 - $processing = $count;
8667 - break;
8668 - case 'pending':
8669 - $pending = $count;
8670 - break;
8671 - }
8672 - }
8673 -
8674 - // Calculate percentage
8675 - $percentage = $total > 0 ? round((($completed + $failed) / $total) * 100) : 0;
8676 -
8677 - // Get failed items details (include all failed items, not just those that exhausted retries)
8678 - $failed_items = array();
8679 - if ($failed > 0) {
8680 - $failed_items = $wpdb->get_results($wpdb->prepare(
8681 - "SELECT item_type, item_data, error_message, attempts
8682 - FROM $table_name
8683 - WHERE queue_id = %s
8684 - AND status = 'failed'
8685 - ORDER BY id DESC
8686 - LIMIT 50",
8687 - $queue_id
8688 - ));
8689 - }
8690 -
8691 - // Get queue metadata
8692 - $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
8693 - $queue_type = $this->mxchat_get_queue_meta($queue_id, 'queue_type');
8694 -
8695 - // Determine if queue is complete
8696 - $is_complete = ($pending === 0 && $processing === 0);
8697 -
8698 - wp_send_json_success(array(
8699 - 'queue_id' => $queue_id,
8700 - 'queue_type' => $queue_type,
8701 - 'source_url' => $source_url,
8702 - 'total' => $total,
8703 - 'completed' => $completed,
8704 - 'failed' => $failed,
8705 - 'processing' => $processing,
8706 - 'pending' => $pending,
8707 - 'percentage' => $percentage,
8708 - 'is_complete' => $is_complete,
8709 - 'failed_items' => $failed_items,
8710 - 'status' => $is_complete ? 'complete' : 'processing'
8711 - ));
8712 -}
8713 -
8714 -/**
8715 - * AJAX: Clear completed queue
8716 - */
8717 -public function ajax_mxchat_clear_queue() {
8718 - // Verify nonce and permissions
8719 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
8720 -
8721 - if (!current_user_can('manage_options')) {
8722 - wp_send_json_error('Unauthorized access');
8723 - }
8724 -
8725 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
8726 -
8727 - if (empty($queue_id)) {
8728 - wp_send_json_error('Missing queue ID');
8729 - }
8730 -
8731 - global $wpdb;
8732 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
8733 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
8734 -
8735 - // Delete queue items
8736 - $wpdb->delete(
8737 - $table_name,
8738 - array('queue_id' => $queue_id),
8739 - array('%s')
8740 - );
8741 -
8742 - // Delete queue metadata
8743 - $wpdb->delete(
8744 - $meta_table,
8745 - array('queue_id' => $queue_id),
8746 - array('%s')
8747 - );
8748 -
8749 - wp_send_json_success(array(
8750 - 'message' => 'Queue cleared successfully'
8751 - ));
8752 -}
8753 -
8754 -/**
8755 - * AJAX: Retry failed items in queue
8756 - */
8757 -public function ajax_mxchat_retry_failed() {
8758 - // Verify nonce and permissions
8759 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
8760 -
8761 - if (!current_user_can('manage_options')) {
8762 - wp_send_json_error('Unauthorized access');
8763 - }
8764 -
8765 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
8766 -
8767 - if (empty($queue_id)) {
8768 - wp_send_json_error('Missing queue ID');
8769 - }
8770 -
8771 - global $wpdb;
8772 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
8773 -
8774 - // Reset failed items to pending and reset attempt count
8775 - $updated = $wpdb->update(
8776 - $table_name,
8777 - array(
8778 - 'status' => 'pending',
8779 - 'attempts' => 0,
8780 - 'error_message' => null
8781 - ),
8782 - array(
8783 - 'queue_id' => $queue_id,
8784 - 'status' => 'failed'
8785 - ),
8786 - array('%s', '%d', '%s'),
8787 - array('%s', '%s')
8788 - );
8789 -
8790 - wp_send_json_success(array(
8791 - 'message' => 'Reset ' . $updated . ' failed items for retry',
8792 - 'reset_count' => $updated
8793 - ));
8794 -}
8795 -
8796 -
8797 -public function ajax_mxchat_mark_queue_complete() {
8798 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
8799 -
8800 - if (!current_user_can('manage_options')) {
8801 - wp_send_json_error('Unauthorized access');
8802 - }
8803 -
8804 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
8805 -
8806 - if (empty($queue_id)) {
8807 - wp_send_json_error('Missing queue ID');
8808 - }
8809 -
8810 - // Clear active queue transients
8811 - if (strpos($queue_id, 'sitemap_') === 0) {
8812 - delete_transient('mxchat_active_queue_sitemap');
8813 - } else if (strpos($queue_id, 'pdf_') === 0) {
8814 - delete_transient('mxchat_active_queue_pdf');
8815 - }
8816 -
8817 - wp_send_json_success(array('message' => 'Queue marked as complete'));
8818 -}
8819 -
8820 3346
8821 3347 // ========================================
8822 3348 // STATIC ACCESS METHODS
8823 3349 // ========================================