PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.2.4
MxChat – AI Chatbot & Content Generation for WordPress v2.2.4
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 +1586 -6572 3.2.132.2.4 View file →
@@ -15,73 +15,49 @@
15 15
16 16 /**
17 17 * Constructor - Register hooks for content processing
18 18 */
19 -public function __construct() {
20 - $this->options = get_option('mxchat_options', array());
21 - $this->mxchat_init_hooks();
19 + public function __construct() {
20 + $this->options = get_option('mxchat_options', array());
21 + $this->mxchat_init_hooks();
22 + }
22 23
23 - $this->mxchat_init_role_hooks();
24 -}
25 -
26 -/**
27 - * Initialize WordPress hooks for content processing
28 - *
29 - */
30 -private function mxchat_init_hooks() {
31 - // Admin post handlers for form submissions
32 - add_action('admin_post_mxchat_submit_content', array($this, 'mxchat_handle_content_submission'));
33 - add_action('admin_post_mxchat_submit_sitemap', array($this, 'mxchat_handle_sitemap_submission'));
34 - add_action('admin_post_mxchat_submit_pdf_file', array($this, 'mxchat_handle_pdf_file_submission'));
35 - add_action('admin_post_mxchat_stop_processing', array($this, 'mxchat_stop_processing'));
36 -
37 - // AJAX handlers for real-time processing and status updates
38 - add_action('wp_ajax_mxchat_get_status_updates', array($this, 'mxchat_ajax_get_status_updates'));
39 - add_action('wp_ajax_mxchat_dismiss_completed_status', array($this, 'mxchat_ajax_dismiss_completed_status'));
40 - add_action('wp_ajax_mxchat_get_content_list', array($this, 'ajax_mxchat_get_content_list'));
41 - add_action('wp_ajax_mxchat_process_selected_content', array($this, 'ajax_mxchat_process_selected_content'));
42 - add_action('wp_ajax_mxchat_save_inline_prompt', array($this, 'mxchat_save_inline_prompt'));
43 - add_action('admin_post_mxchat_delete_pinecone_prompt', array($this, 'mxchat_handle_pinecone_prompt_delete'));
44 - add_action('wp_ajax_mxchat_delete_pinecone_prompt', array($this, 'ajax_mxchat_delete_pinecone_prompt'));
45 - add_action('wp_ajax_mxchat_delete_chunks_by_url', array($this, 'ajax_mxchat_delete_chunks_by_url'));
46 - add_action('wp_ajax_mxchat_delete_wordpress_prompt', array($this, 'ajax_mxchat_delete_wordpress_prompt'));
47 - add_action('wp_ajax_mxchat_bulk_delete_knowledge', array($this, 'ajax_mxchat_bulk_delete_knowledge'));
48 - add_action('wp_ajax_mxchat_update_role_restriction', array($this, 'ajax_mxchat_update_role_restriction'));
49 -
50 - // Queue-based processing AJAX handlers
51 - add_action('wp_ajax_mxchat_get_next_queue_item', array($this, 'ajax_mxchat_get_next_queue_item'));
52 - add_action('wp_ajax_mxchat_process_queue_item', array($this, 'ajax_mxchat_process_queue_item'));
53 - add_action('wp_ajax_mxchat_get_queue_status', array($this, 'ajax_mxchat_get_queue_status'));
54 - add_action('wp_ajax_mxchat_clear_queue', array($this, 'ajax_mxchat_clear_queue'));
55 - add_action('wp_ajax_mxchat_retry_failed', array($this, 'ajax_mxchat_retry_failed'));
56 - add_action('wp_ajax_mxchat_get_recent_entries', array($this, 'ajax_mxchat_get_recent_entries'));
57 - add_action('wp_ajax_mxchat_detect_sitemaps', array($this, 'ajax_mxchat_detect_sitemaps'));
58 - add_action('wp_ajax_mxchat_refresh_pinecone_entries', array($this, 'ajax_mxchat_refresh_pinecone_entries'));
59 - add_action('wp_ajax_mxchat_paginate_entries', array($this, 'ajax_mxchat_paginate_entries'));
60 - add_action('wp_ajax_mxchat_get_entry_content', array($this, 'ajax_mxchat_get_entry_content'));
61 - add_action('wp_ajax_mxchat_save_entry_content', array($this, 'ajax_mxchat_save_entry_content'));
62 - 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);
63 40
64 - // WordPress post management hooks
65 - add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
66 - add_action('post_updated', array($this, 'mxchat_handle_post_update'), 10, 3);
67 - add_action('before_delete_post', array($this, 'mxchat_handle_post_delete'));
68 - add_action('wp_trash_post', array($this, 'mxchat_handle_post_delete'));
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'));
69 51
70 - // ACF hook - fires AFTER ACF fields are saved, ensuring ACF data is available
71 - // Priority 20 to run after ACF's own save (which runs at priority 10)
72 - add_action('acf/save_post', array($this, 'mxchat_handle_acf_save'), 20);
73 -
74 - add_action('wp_ajax_mxchat_mark_queue_complete', array($this, 'ajax_mxchat_mark_queue_complete'));
75 -
76 - // WooCommerce product hooks (if WooCommerce is active)
77 - if (class_exists('WooCommerce')) {
78 - add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
79 - add_action('save_post_product', array($this, 'mxchat_handle_product_change'), 10, 3);
80 - add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
81 - add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
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 +
82 59 }
83 -}
84 60
85 61 /**
86 62 * Get current options (refreshed)
87 63 */
@@ -91,9 +67,315 @@
91 67 }
92 68 return $this->options;
93 69 }
94 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 +}
95 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 +
96 378 // ========================================
97 379 // MAIN CONTENT SUBMISSION HANDLERS
98 380 // ========================================
99 381
@@ -109,18 +391,13 @@
109 391 wp_die(esc_html__('Nonce verification failed.', 'mxchat'));
110 392 }
111 393
112 394 // Sanitize the inputs.
113 - // Use wp_kses_post to allow safe HTML and wp_unslash to remove WordPress added slashes
114 - $article_content = wp_kses_post(wp_unslash($_POST['article_content']));
395 + $article_content = sanitize_textarea_field($_POST['article_content']);
115 396 $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
116 397
117 - // Get bot_id from form submission
118 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
119 -
120 - // Get bot-specific options and API key
121 - $bot_options = $this->get_bot_options($bot_id);
122 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
398 + // Get API key for submission
399 + $options = get_option('mxchat_options');
123 400 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
124 401
125 402 if (strpos($selected_model, 'voyage') === 0) {
126 403 $api_key = $options['voyage_api_key'] ?? '';
@@ -138,10 +415,10 @@
138 415 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
139 416 exit;
140 417 }
141 418
142 - // Use centralized utility function with bot_id
143 - $result = MxChat_Utils::submit_content_to_db($article_content, $article_url, $api_key, null, $bot_id);
419 + // Use centralized utility function for storage
420 + $result = MxChat_Utils::submit_content_to_db($article_content, $article_url, $api_key);
144 421
145 422 if (is_wp_error($result)) {
146 423 set_transient('mxchat_admin_notice_error',
147 424 esc_html__('Error storing content: ', 'mxchat') . $result->get_error_message(),
@@ -156,23 +433,17 @@
156 433
157 434 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
158 435 exit;
159 436 }
160 -
161 437 public function mxchat_is_pdf_url($url, $response) {
162 438 $content_type = wp_remote_retrieve_header($response, 'content-type');
163 439 $file_extension = strtolower(pathinfo($url, PATHINFO_EXTENSION));
164 440
165 - // Check Content-Disposition header for .pdf filename (Google Drive sends this)
166 - $disposition = wp_remote_retrieve_header($response, 'content-disposition');
167 - $has_pdf_disposition = ! empty($disposition) && stripos($disposition, '.pdf') !== false;
168 -
169 - return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf' || $has_pdf_disposition;
441 + return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf';
170 442 }
171 -
172 -
173 -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) {
174 444 if (!current_user_can('manage_options')) {
445 + //error_log(esc_html__('Unauthorized PDF processing attempt', 'mxchat'));
175 446 return false;
176 447 }
177 448
178 449 $pdf_url = esc_url_raw($pdf_url);
@@ -178,8 +449,9 @@
178 449 $pdf_url = esc_url_raw($pdf_url);
179 450 $upload_dir = wp_upload_dir();
180 451
181 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'])));
182 454 return false;
183 455 }
184 456
185 457 $pdf_filename = sanitize_file_name('mxchat_kb_' . time() . '.pdf');
@@ -186,12 +458,14 @@
186 458 $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
187 459
188 460 $response_body = wp_remote_retrieve_body($response);
189 461 if (empty($response_body)) {
462 + //error_log(esc_html__('Empty PDF response body', 'mxchat'));
190 463 return false;
191 464 }
192 465
193 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)));
194 468 return false;
195 469 }
196 470
197 471 try {
@@ -200,915 +474,295 @@
200 474 if (!file_exists($pdf_path)) {
201 475 throw new Exception(__('Failed to save PDF file', 'mxchat'));
202 476 }
203 477
204 - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
205 -
206 - if ($total_pages === false || $total_pages < 1) {
207 - throw new Exception(__('Invalid PDF: Unable to parse or no pages found', 'mxchat'));
208 - }
478 + $parser = new \Smalot\PdfParser\Parser();
479 + $pdf = $parser->parseFile($pdf_path);
480 + $total_pages = absint(count($pdf->getPages()));
209 481
210 - // Create unique queue ID
211 - $queue_id = 'pdf_' . md5($pdf_url . time());
212 -
213 - // Create array of pages to process
214 - $pages = array();
215 - for ($i = 1; $i <= $total_pages; $i++) {
216 - $pages[] = array(
217 - 'pdf_path' => $pdf_path,
218 - 'pdf_url' => $pdf_url,
219 - 'page_number' => $i,
220 - 'total_pages' => $total_pages
221 - );
482 + if ($total_pages < 1) {
483 + throw new Exception(__('Invalid PDF: no pages found', 'mxchat'));
222 484 }
223 485
224 - // Add pages to queue
225 - $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 + ));
226 493
227 - if ($queued_count === 0) {
228 - wp_delete_file($pdf_path);
229 - throw new Exception(__('Failed to add PDF pages to processing queue', 'mxchat'));
230 - }
494 + $status_data = array(
495 + 'total_pages' => $total_pages,
496 + 'processed_pages' => 0,
497 + 'status' => 'processing',
498 + 'last_update' => time()
499 + );
231 500
232 - // Store queue metadata
233 - $this->mxchat_set_queue_meta($queue_id, 'source_url', $pdf_url);
234 - $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'pdf');
235 - $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_pages);
236 - $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
237 - $this->mxchat_set_queue_meta($queue_id, 'pdf_path', $pdf_path);
238 - $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 + );
239 506
240 - // Store queue ID in transient for status tracking
241 - set_transient('mxchat_active_queue_pdf', $queue_id, DAY_IN_SECONDS);
242 - set_transient('mxchat_last_pdf_url', $pdf_url, DAY_IN_SECONDS);
507 + return __('scheduled', 'mxchat');
243 508
244 - return 'queued';
245 -
246 509 } catch (Exception $e) {
510 + //error_log(sprintf(esc_html__('Error preparing PDF for processing: %s', 'mxchat'), esc_html($e->getMessage())));
247 511 if (file_exists($pdf_path)) {
248 512 wp_delete_file($pdf_path);
249 513 }
250 - return $e->getMessage();
514 + return false;
251 515 }
252 516 }
253 517
254 -/**
255 - * Handle direct PDF file upload from the knowledge base page
256 - */
257 -public function mxchat_handle_pdf_file_submission() {
258 - if (!isset($_POST['submit_pdf_file']) || !current_user_can('manage_options')) {
259 - wp_die(esc_html__('Unauthorized access', 'mxchat'));
260 - }
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);
261 525
262 - check_admin_referer('mxchat_submit_pdf_file_action', 'mxchat_submit_pdf_file_nonce');
263 -
264 - $redirect_url = admin_url('admin.php?page=mxchat-prompts');
265 -
266 - // Validate file upload
267 - if (empty($_FILES['pdf_file']) || $_FILES['pdf_file']['error'] !== UPLOAD_ERR_OK) {
268 - $error_code = isset($_FILES['pdf_file']['error']) ? $_FILES['pdf_file']['error'] : UPLOAD_ERR_NO_FILE;
269 - $error_messages = array(
270 - UPLOAD_ERR_INI_SIZE => __('The uploaded file exceeds the server upload_max_filesize limit.', 'mxchat'),
271 - UPLOAD_ERR_FORM_SIZE => __('The uploaded file exceeds the form MAX_FILE_SIZE limit.', 'mxchat'),
272 - UPLOAD_ERR_PARTIAL => __('The file was only partially uploaded.', 'mxchat'),
273 - UPLOAD_ERR_NO_FILE => __('No file was uploaded. Please select a PDF file.', 'mxchat'),
274 - UPLOAD_ERR_NO_TMP_DIR => __('Server missing temporary folder.', 'mxchat'),
275 - UPLOAD_ERR_CANT_WRITE => __('Server failed to write file to disk.', 'mxchat'),
276 - );
277 - $error_msg = isset($error_messages[$error_code]) ? $error_messages[$error_code] : __('Unknown upload error.', 'mxchat');
278 - set_transient('mxchat_admin_notice_error', $error_msg, 30);
279 - wp_safe_redirect(esc_url($redirect_url));
280 - exit;
281 - }
282 -
283 - $file = $_FILES['pdf_file'];
284 -
285 - // Validate MIME type
286 - $finfo = finfo_open(FILEINFO_MIME_TYPE);
287 - $mime_type = finfo_file($finfo, $file['tmp_name']);
288 - finfo_close($finfo);
289 -
290 - if ($mime_type !== 'application/pdf') {
291 - set_transient('mxchat_admin_notice_error',
292 - esc_html__('Invalid file type. Only PDF files are accepted.', 'mxchat'),
293 - 30
294 - );
295 - wp_safe_redirect(esc_url($redirect_url));
296 - exit;
297 - }
298 -
299 - // Validate extension
300 - $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
301 - if ($ext !== 'pdf') {
302 - set_transient('mxchat_admin_notice_error',
303 - esc_html__('Invalid file extension. Only .pdf files are accepted.', 'mxchat'),
304 - 30
305 - );
306 - wp_safe_redirect(esc_url($redirect_url));
307 - exit;
308 - }
309 -
310 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
311 - $original_filename = sanitize_file_name($file['name']);
312 -
313 - $upload_dir = wp_upload_dir();
314 - if (isset($upload_dir['error']) && $upload_dir['error'] !== false) {
315 - set_transient('mxchat_admin_notice_error',
316 - esc_html__('WordPress upload directory is not writable.', 'mxchat'),
317 - 30
318 - );
319 - wp_safe_redirect(esc_url($redirect_url));
320 - exit;
321 - }
322 -
323 - $pdf_filename = sanitize_file_name('mxchat_kb_' . time() . '.pdf');
324 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
325 -
326 - if (!wp_mkdir_p(dirname($pdf_path))) {
327 - set_transient('mxchat_admin_notice_error',
328 - esc_html__('Failed to create upload directory.', 'mxchat'),
329 - 30
330 - );
331 - wp_safe_redirect(esc_url($redirect_url));
332 - exit;
333 - }
334 -
335 - // Move uploaded file
336 - if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
337 - set_transient('mxchat_admin_notice_error',
338 - esc_html__('Failed to save uploaded PDF file.', 'mxchat'),
339 - 30
340 - );
341 - wp_safe_redirect(esc_url($redirect_url));
342 - exit;
343 - }
344 -
345 526 try {
346 - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
347 -
348 - if ($total_pages === false || $total_pages < 1) {
349 - 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)));
350 529 }
351 530
352 - // Use original filename as the source identifier
353 - $source_label = 'upload://' . $original_filename;
354 -
355 - $queue_id = 'pdf_' . md5($source_label . time());
356 -
357 - $pages = array();
358 - for ($i = 1; $i <= $total_pages; $i++) {
359 - $pages[] = array(
360 - 'pdf_path' => $pdf_path,
361 - 'pdf_url' => $source_label,
362 - 'page_number' => $i,
363 - 'total_pages' => $total_pages,
364 - );
365 - }
366 -
367 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
368 -
369 - if ($queued_count === 0) {
370 - wp_delete_file($pdf_path);
371 - throw new Exception(__('Failed to add PDF pages to processing queue', 'mxchat'));
372 - }
373 -
374 - $this->mxchat_set_queue_meta($queue_id, 'source_url', $source_label);
375 - $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'pdf');
376 - $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_pages);
377 - $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
378 - $this->mxchat_set_queue_meta($queue_id, 'pdf_path', $pdf_path);
379 - $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
380 -
381 - set_transient('mxchat_active_queue_pdf', $queue_id, DAY_IN_SECONDS);
382 - set_transient('mxchat_last_pdf_url', $source_label, DAY_IN_SECONDS);
383 -
384 - set_transient('mxchat_admin_notice_success',
385 - sprintf(
386 - esc_html__('PDF "%s" (%d pages) queued for processing. Processing will start automatically.', 'mxchat'),
387 - esc_html($original_filename),
388 - $total_pages
389 - ),
390 - 30
391 - );
392 -
393 - } catch (Exception $e) {
394 - if (file_exists($pdf_path)) {
395 - wp_delete_file($pdf_path);
396 - }
397 - set_transient('mxchat_admin_notice_error',
398 - esc_html__('Failed to process uploaded PDF: ', 'mxchat') . esc_html($e->getMessage()),
399 - 30
400 - );
401 - }
402 -
403 - wp_safe_redirect(esc_url($redirect_url));
404 - exit;
405 -}
406 -
407 -/**
408 - * Validate PDF and count pages with multiple parser attempts
409 - */
410 -private function mxchat_validate_and_count_pdf_pages($pdf_path) {
411 - // Method 1: Try with Smalot PDF Parser (your current method)
412 - try {
413 - mxchat_load_pdf_parser();
414 531 $parser = new \Smalot\PdfParser\Parser();
415 532 $pdf = $parser->parseFile($pdf_path);
416 533 $pages = $pdf->getPages();
417 - $page_count = count($pages);
418 -
419 - if ($page_count > 0) {
420 - //error_log('PDF parsed successfully with Smalot parser: ' . $page_count . ' pages');
421 - return $page_count;
422 - }
423 - } catch (Exception $e) {
424 - //error_log('Smalot PDF parser failed: ' . $e->getMessage());
425 - }
426 534
427 - // Method 2: Try with pdfinfo command (if available)
428 - if (function_exists('shell_exec') && !$this->mxchat_is_shell_disabled()) {
429 - try {
430 - $command = 'pdfinfo ' . escapeshellarg($pdf_path) . ' 2>&1';
431 - $output = shell_exec($command);
432 -
433 - if ($output && preg_match('/Pages:\s*(\d+)/', $output, $matches)) {
434 - $page_count = intval($matches[1]);
435 - if ($page_count > 0) {
436 - //error_log('PDF parsed successfully with pdfinfo: ' . $page_count . ' pages');
437 - return $page_count;
438 - }
439 - }
440 - } catch (Exception $e) {
441 - //error_log('pdfinfo command failed: ' . $e->getMessage());
442 - }
443 - }
535 + // Get current progress
536 + $status_key = sanitize_key('mxchat_pdf_status_' . md5($pdf_url));
537 + $status = get_transient($status_key);
444 538
445 - // Method 3: Try to repair PDF and parse again
446 - try {
447 - $repaired_path = $this->mxchat_attempt_pdf_repair($pdf_path);
448 - if ($repaired_path && $repaired_path !== $pdf_path) {
449 - mxchat_load_pdf_parser();
450 - $parser = new \Smalot\PdfParser\Parser();
451 - $pdf = $parser->parseFile($repaired_path);
452 - $pages = $pdf->getPages();
453 - $page_count = count($pages);
454 -
455 - if ($page_count > 0) {
456 - // Replace original with repaired version
457 - copy($repaired_path, $pdf_path);
458 - unlink($repaired_path);
459 - //error_log('PDF repaired and parsed successfully: ' . $page_count . ' pages');
460 - return $page_count;
461 - }
462 -
463 - // Clean up repaired file if it didn't work
464 - unlink($repaired_path);
539 + if (!$status || !is_array($status)) {
540 + throw new Exception('Invalid status data retrieved from transient');
465 541 }
466 - } catch (Exception $e) {
467 - //error_log('PDF repair attempt failed: ' . $e->getMessage());
468 - }
469 542
470 - // Method 4: Manual PDF structure analysis (basic page count)
471 - try {
472 - $page_count = $this->mxchat_manual_pdf_page_count($pdf_path);
473 - if ($page_count > 0) {
474 - //error_log('PDF page count determined manually: ' . $page_count . ' pages');
475 - 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'] = [];
476 546 }
477 - } catch (Exception $e) {
478 - //error_log('Manual PDF analysis failed: ' . $e->getMessage());
479 - }
480 547
481 - //error_log('All PDF parsing methods failed for: ' . $pdf_path);
482 - return false;
483 -}
484 -
485 -/**
486 - * Check if shell_exec is disabled
487 - */
488 -private function mxchat_is_shell_disabled() {
489 - $disabled = explode(',', ini_get('disable_functions'));
490 - return in_array('shell_exec', $disabled);
491 -}
492 -
493 -/**
494 - * Attempt to repair PDF using basic methods
495 - */
496 -private function mxchat_attempt_pdf_repair($pdf_path) {
497 - try {
498 - $content = file_get_contents($pdf_path);
499 - if (!$content) {
500 - 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');
501 554 }
502 555
503 - // Check if PDF starts with proper header
504 - if (substr($content, 0, 4) !== '%PDF') {
505 - // Try to find PDF header in the content
506 - $header_pos = strpos($content, '%PDF');
507 - if ($header_pos !== false && $header_pos < 1024) {
508 - // Remove junk before PDF header
509 - $content = substr($content, $header_pos);
510 - $repaired_path = $pdf_path . '.repaired';
511 - file_put_contents($repaired_path, $content);
512 - return $repaired_path;
513 - }
514 - }
556 + $successful_pages = 0;
557 + $failed_pages = 0;
515 558
516 - // Check for EOF marker
517 - $content = rtrim($content);
518 - if (!preg_match('/%%EOF\s*$/', $content)) {
519 - // Add EOF marker if missing
520 - $content .= "\n%%EOF";
521 - $repaired_path = $pdf_path . '.repaired';
522 - file_put_contents($repaired_path, $content);
523 - return $repaired_path;
524 - }
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 = '';
525 565
526 - } catch (Exception $e) {
527 - //error_log('PDF repair error: ' . $e->getMessage());
528 - }
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);
529 575
530 - return false;
531 -}
576 + if (empty($sanitized_content)) {
577 + throw new Exception("No valid content after sanitization on page {$page_number}");
578 + }
532 579
533 -/**
534 - * Manual PDF page counting by analyzing PDF structure
535 - */
536 -private function mxchat_manual_pdf_page_count($pdf_path) {
537 - try {
538 - $content = file_get_contents($pdf_path);
539 - if (!$content) {
540 - return 0;
541 - }
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 + );
542 598
543 - // Method 1: Count /Type /Page objects
544 - $page_count = preg_match_all('/\/Type\s*\/Page[^s]/', $content);
545 - if ($page_count > 0) {
546 - return $page_count;
547 - }
599 + $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized_content;
600 + $page_url = esc_url($pdf_url . "#page=" . $page_number);
548 601
549 - // Method 2: Look for /Count in pages object
550 - if (preg_match('/\/Type\s*\/Pages.*?\/Count\s+(\d+)/', $content, $matches)) {
551 - return intval($matches[1]);
552 - }
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 + }
553 607
554 - // Method 3: Count page references
555 - $page_count = preg_match_all('/\d+\s+0\s+obj\s*<<[^>]*\/Type\s*\/Page/', $content);
556 - if ($page_count > 0) {
557 - return $page_count;
558 - }
559 -
560 - } catch (Exception $e) {
561 - //error_log('Manual PDF analysis error: ' . $e->getMessage());
562 - }
563 -
564 - return 0;
565 -}
566 -
567 -
568 -public function mxchat_save_inline_prompt() {
569 - // DEBUG: Log what we're receiving
570 - //error_log('=== MXCHAT DEBUG ===');
571 - //error_log('POST data: ' . print_r($_POST, true));
572 - //error_log('Nonce from POST: ' . ($_POST['_ajax_nonce'] ?? 'NOT FOUND'));
573 -
574 - // Check for nonce security
575 - check_ajax_referer('mxchat_save_inline_nonce', '_ajax_nonce');
576 -
577 - // If we get here, nonce passed
578 - //error_log('Nonce verification PASSED');
579 -
580 - // Verify permissions
581 - if (!current_user_can('manage_options')) {
582 - wp_send_json_error(esc_html__('Permission denied.', 'mxchat'));
583 - return;
584 - }
585 -
586 - global $wpdb;
587 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
588 -
589 - // Validate and sanitize input data
590 - $prompt_id = isset($_POST['id']) ? absint($_POST['id']) : 0;
591 - $article_content = isset($_POST['article_content']) ? wp_kses_post(wp_unslash($_POST['article_content'])) : '';
592 - $article_url = isset($_POST['article_url']) ? esc_url_raw($_POST['article_url']) : '';
593 -
594 - if ($prompt_id > 0 && !empty($article_content)) {
595 - // Re-generate the embedding vector for the updated content
596 - $embedding_vector = $this->mxchat_generate_embedding($article_content);
597 - if (is_array($embedding_vector)) {
598 - // Serialize the embedding vector before storing it
599 - $embedding_vector_serialized = serialize($embedding_vector);
600 - // Update the prompt in the database
601 - $updated = $wpdb->update(
602 - $table_name,
603 - array(
604 - 'article_content' => $article_content,
605 - 'embedding_vector' => $embedding_vector_serialized,
606 - 'source_url' => $article_url,
607 - ),
608 - array('id' => $prompt_id),
609 - array('%s', '%s', '%s'),
610 - array('%d')
611 - );
612 - if ($updated !== false) {
613 - wp_send_json_success();
614 - } else {
615 - MxChat_Admin::mxchat_log_debug('knowledge_error', 'Database update failed when saving knowledge entry');
616 - wp_send_json_error(esc_html__('Database update failed.', 'mxchat'));
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 + }
617 623 }
618 - } else {
619 - MxChat_Admin::mxchat_log_debug('embedding_error', 'Embedding generation failed for knowledge entry');
620 - wp_send_json_error(esc_html__('Embedding generation failed.', 'mxchat'));
621 - }
622 - } else {
623 - wp_send_json_error(esc_html__('Invalid data.', 'mxchat'));
624 - }
625 -}
626 624
627 -
628 -/**
629 - * AJAX: Get full content for editing — reassembles chunks if needed.
630 - * Works for both WordPress DB and Pinecone entries.
631 - */
632 -public function ajax_mxchat_get_entry_content() {
633 - check_ajax_referer('mxchat_edit_entry_nonce', 'nonce');
634 -
635 - if ( ! current_user_can('manage_options') ) {
636 - wp_send_json_error( array( 'message' => 'Permission denied.' ) );
637 - }
638 -
639 - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : '';
640 - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0;
641 - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress';
642 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
643 -
644 - if ( $data_source === 'pinecone' ) {
645 - // Pinecone: fetch vectors by source_url, reassemble chunks
646 - $content = $this->get_pinecone_entry_content( $source_url, $entry_id, $bot_id );
647 - } else {
648 - // WordPress DB
649 - $content = $this->get_wordpress_entry_content( $source_url, $entry_id );
650 - }
651 -
652 - if ( is_wp_error( $content ) ) {
653 - wp_send_json_error( array( 'message' => $content->get_error_message() ) );
654 - }
655 -
656 - wp_send_json_success( $content );
657 -}
658 -
659 -/**
660 - * Get content from WordPress DB — reassembles chunks by source_url.
661 - */
662 -private function get_wordpress_entry_content( $source_url, $entry_id ) {
663 - global $wpdb;
664 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
665 -
666 - // If we have a source_url, check for chunks
667 - if ( ! empty( $source_url ) && strpos( $source_url, 'mxchat://' ) !== 0 ) {
668 - $rows = $wpdb->get_results( $wpdb->prepare(
669 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE source_url = %s ORDER BY id ASC",
670 - $source_url
671 - ) );
672 -
673 - if ( $rows && count( $rows ) > 1 ) {
674 - // Multiple rows = chunked. Reassemble.
675 - $chunks = array();
676 - foreach ( $rows as $row ) {
677 - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content );
678 - $index = isset( $parsed['metadata']['chunk_index'] ) ? intval( $parsed['metadata']['chunk_index'] ) : count( $chunks );
679 - $chunks[ $index ] = $parsed['text'];
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);
680 641 }
681 - ksort( $chunks );
682 - return array(
683 - 'content' => implode( "\n\n", $chunks ),
684 - 'source_url' => $source_url,
685 - 'is_chunked' => true,
686 - 'chunk_count' => count( $chunks ),
687 - 'content_type' => $rows[0]->content_type,
688 - );
689 - } elseif ( $rows && count( $rows ) === 1 ) {
690 - $parsed = MxChat_Chunker::parse_stored_chunk( $rows[0]->article_content );
691 - return array(
692 - 'content' => $parsed['text'],
693 - 'source_url' => $source_url,
694 - 'entry_id' => $rows[0]->id,
695 - 'is_chunked' => false,
696 - 'content_type' => $rows[0]->content_type,
697 - );
698 - }
699 - }
700 642
701 - // Fallback: fetch by ID
702 - if ( $entry_id > 0 ) {
703 - $row = $wpdb->get_row( $wpdb->prepare(
704 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE id = %d",
705 - $entry_id
706 - ) );
707 - if ( $row ) {
708 - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content );
709 - return array(
710 - 'content' => $parsed['text'],
711 - 'source_url' => $row->source_url,
712 - 'entry_id' => $row->id,
713 - 'is_chunked' => false,
714 - 'content_type' => $row->content_type,
715 - );
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);
716 649 }
717 - }
718 650
719 - return new WP_Error( 'not_found', 'Entry not found.' );
720 -}
721 -
722 -/**
723 - * Get content from Pinecone — fetches vectors by source_url, reassembles chunks.
724 - */
725 -private function get_pinecone_entry_content( $source_url, $entry_id, $bot_id ) {
726 - if ( ! class_exists('MxChat_Pinecone_Manager') ) {
727 - return new WP_Error( 'pinecone_unavailable', 'Pinecone manager not available.' );
728 - }
729 -
730 - // Get Pinecone config
731 - if ( $bot_id === 'default' || ! class_exists('MxChat_Multi_Bot_Manager') ) {
732 - $pinecone_options = get_option('mxchat_pinecone_addon_options');
733 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
734 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
735 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
736 - } else {
737 - $bot_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
738 - $api_key = $bot_config['api_key'] ?? '';
739 - $host = $bot_config['host'] ?? '';
740 - $namespace = $bot_config['namespace'] ?? '';
741 - }
742 -
743 - if ( empty($host) || empty($api_key) ) {
744 - return new WP_Error( 'pinecone_config', 'Pinecone not configured.' );
745 - }
746 -
747 - // List vectors with the source_url prefix
748 - $base_id = md5( $source_url );
749 - $vector_ids = array( $base_id );
750 -
751 - // Find chunk vectors
752 - $list_url = "https://{$host}/vectors/list";
753 - $list_body = array( 'prefix' => $base_id . '_chunk_', 'limit' => 100 );
754 - if ( ! empty($namespace) ) {
755 - $list_body['namespace'] = $namespace;
756 - }
757 -
758 - $list_resp = wp_remote_post( $list_url, array(
759 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
760 - 'body' => wp_json_encode( $list_body ),
761 - 'timeout' => 15,
762 - ) );
763 -
764 - if ( ! is_wp_error($list_resp) ) {
765 - $list_data = json_decode( wp_remote_retrieve_body($list_resp), true );
766 - if ( ! empty($list_data['vectors']) ) {
767 - foreach ( $list_data['vectors'] as $v ) {
768 - $vector_ids[] = $v['id'];
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);
769 679 }
680 +
681 + // DON'T delete the status transients here - let user dismiss manually
770 682 }
771 - }
772 683
773 - // Fetch vectors with metadata
774 - $fetch_url = "https://{$host}/vectors/fetch";
775 - $fetch_body = array( 'ids' => $vector_ids );
776 - if ( ! empty($namespace) ) {
777 - $fetch_body['namespace'] = $namespace;
778 - }
779 -
780 - $fetch_resp = wp_remote_post( $fetch_url, array(
781 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
782 - 'body' => wp_json_encode( $fetch_body ),
783 - 'timeout' => 15,
784 - ) );
785 -
786 - if ( is_wp_error($fetch_resp) ) {
787 - return new WP_Error( 'pinecone_fetch', 'Failed to fetch from Pinecone.' );
788 - }
789 -
790 - $fetch_data = json_decode( wp_remote_retrieve_body($fetch_resp), true );
791 - $vectors = $fetch_data['vectors'] ?? array();
792 -
793 - if ( empty($vectors) ) {
794 - return new WP_Error( 'not_found', 'Entry not found in Pinecone.' );
795 - }
796 -
797 - // Reassemble chunks
798 - $chunks = array();
799 - $content_type = 'content';
800 - foreach ( $vectors as $vid => $vector ) {
801 - $meta = $vector['metadata'] ?? array();
802 - $text = $meta['text'] ?? '';
803 - $index = $meta['chunk_index'] ?? 0;
804 - $content_type = $meta['type'] ?? 'content';
805 - $chunks[ intval($index) ] = $text;
806 - }
807 - ksort( $chunks );
808 -
809 - return array(
810 - 'content' => implode( "\n\n", $chunks ),
811 - 'source_url' => $source_url,
812 - 'is_chunked' => count($chunks) > 1,
813 - 'chunk_count' => count($chunks),
814 - 'content_type' => $content_type,
815 - );
816 -}
817 -
818 -/**
819 - * AJAX: Inspect a knowledge entry — returns the per-chunk stored text + metadata
820 - * WITHOUT collapsing it, so a site owner can see exactly what was indexed for an
821 - * entry (plan-mxchat-20260628-d8cb4b). READ-ONLY: never re-embeds or mutates.
822 - */
823 -public function ajax_mxchat_inspect_entry() {
824 - check_ajax_referer('mxchat_inspect_entry_nonce', 'nonce');
825 -
826 - if ( ! current_user_can('manage_options') ) {
827 - wp_send_json_error( array( 'message' => esc_html__('Permission denied.', 'mxchat') ) );
828 - }
829 -
830 - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : '';
831 - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0;
832 - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress';
833 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
834 -
835 - if ( $data_source === 'pinecone' ) {
836 - $result = $this->inspect_pinecone_entry( $source_url, $entry_id, $bot_id );
837 - } else {
838 - $result = $this->inspect_wordpress_entry( $source_url, $entry_id );
839 - }
840 -
841 - if ( is_wp_error( $result ) ) {
842 - wp_send_json_error( array( 'message' => $result->get_error_message() ) );
843 - }
844 -
845 - wp_send_json_success( $result );
846 -}
847 -
848 -/**
849 - * Read-only inspector for WordPress-DB entries. Mirrors get_wordpress_entry_content()
850 - * but returns each STORED chunk's exact text + length (no implode), plus the assembled
851 - * embedded text. This shows what is actually in the index, not a re-derivation from the post.
852 - */
853 -private function inspect_wordpress_entry( $source_url, $entry_id ) {
854 - global $wpdb;
855 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
856 -
857 - $rows = array();
858 -
859 - // Group by the real stored source_url — this INCLUDES "mxchat://" manual
860 - // Direct Content entries (the spec's manual-entry case), which share one
861 - // source_url across their chunk rows. Only the synthetic "_ungrouped_<id>"
862 - // display key (invented by the table view for rows with no source_url) is
863 - // excluded; those fall through to the entry_id lookup below.
864 - if ( ! empty( $source_url ) && strpos( $source_url, '_ungrouped_' ) !== 0 ) {
865 - $rows = $wpdb->get_results( $wpdb->prepare(
866 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE source_url = %s ORDER BY id ASC",
867 - $source_url
868 - ) );
869 - }
870 -
871 - // Fallback / manual "Direct Content" entries: fetch the single row by id.
872 - if ( empty( $rows ) && $entry_id > 0 ) {
873 - $row = $wpdb->get_row( $wpdb->prepare(
874 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE id = %d",
875 - $entry_id
876 - ) );
877 - if ( $row ) {
878 - $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();
879 702 }
880 - }
881 -
882 - if ( empty( $rows ) ) {
883 - return new WP_Error( 'not_found', esc_html__('Entry not found in the local knowledge database.', 'mxchat') );
884 - }
885 -
886 - $chunks = array();
887 - $content_type = '';
888 - foreach ( $rows as $row ) {
889 - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content );
890 - $text = isset( $parsed['text'] ) ? $parsed['text'] : '';
891 - $index = isset( $parsed['metadata']['chunk_index'] ) ? intval( $parsed['metadata']['chunk_index'] ) : count( $chunks );
892 - $content_type = $row->content_type;
893 - $chunks[] = array(
894 - 'index' => $index,
895 - 'text' => $text,
896 - 'length' => function_exists('mb_strlen') ? mb_strlen( $text ) : strlen( $text ),
897 - 'row_id' => intval( $row->id ),
898 - );
899 - }
900 -
901 - usort( $chunks, function( $a, $b ) { return $a['index'] - $b['index']; } );
902 -
903 - $assembled = implode( "\n\n", wp_list_pluck( $chunks, 'text' ) );
904 -
905 - return array(
906 - 'store' => 'wordpress',
907 - 'source_url' => $source_url,
908 - 'content_type' => $content_type,
909 - 'is_chunked' => count( $chunks ) > 1,
910 - 'chunk_count' => count( $chunks ),
911 - 'assembled' => $assembled,
912 - 'assembled_length' => function_exists('mb_strlen') ? mb_strlen( $assembled ) : strlen( $assembled ),
913 - 'chunks' => array_values( $chunks ),
914 - // WP-DB storage carries no separate vector metadata; surface that fact
915 - // rather than letting the owner guess (the spec's taxonomy question).
916 - 'metadata' => array(),
917 - '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'),
918 - );
919 -}
920 -
921 -/**
922 - * Read-only inspector for Pinecone entries. Mirrors get_pinecone_entry_content()
923 - * but keeps each vector's text + metadata instead of imploding, so the owner can
924 - * confirm exactly which metadata fields (text/source_url/type/last_updated/created_at/bot_id)
925 - * are present per chunk. READ-ONLY.
926 - */
927 -private function inspect_pinecone_entry( $source_url, $entry_id, $bot_id ) {
928 - if ( ! class_exists('MxChat_Pinecone_Manager') ) {
929 - return new WP_Error( 'pinecone_unavailable', esc_html__('Pinecone manager not available.', 'mxchat') );
930 - }
931 -
932 - if ( $bot_id === 'default' || ! class_exists('MxChat_Multi_Bot_Manager') ) {
933 - $pinecone_options = get_option('mxchat_pinecone_addon_options');
934 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
935 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
936 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
937 - } else {
938 - $bot_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
939 - $api_key = $bot_config['api_key'] ?? '';
940 - $host = $bot_config['host'] ?? '';
941 - $namespace = $bot_config['namespace'] ?? '';
942 - }
943 -
944 - if ( empty($host) || empty($api_key) ) {
945 - return new WP_Error( 'pinecone_config', esc_html__('Pinecone not configured.', 'mxchat') );
946 - }
947 -
948 - $base_id = md5( $source_url );
949 - $vector_ids = array( $base_id );
950 -
951 - $list_url = "https://{$host}/vectors/list";
952 - $list_body = array( 'prefix' => $base_id . '_chunk_', 'limit' => 100 );
953 - if ( ! empty($namespace) ) {
954 - $list_body['namespace'] = $namespace;
955 - }
956 -
957 - $list_resp = wp_remote_post( $list_url, array(
958 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
959 - 'body' => wp_json_encode( $list_body ),
960 - 'timeout' => 15,
961 - ) );
962 -
963 - if ( ! is_wp_error($list_resp) ) {
964 - $list_data = json_decode( wp_remote_retrieve_body($list_resp), true );
965 - if ( ! empty($list_data['vectors']) ) {
966 - foreach ( $list_data['vectors'] as $v ) {
967 - $vector_ids[] = $v['id'];
968 - }
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);
969 708 }
970 709 }
971 -
972 - $fetch_url = "https://{$host}/vectors/fetch";
973 - $fetch_body = array( 'ids' => $vector_ids );
974 - if ( ! empty($namespace) ) {
975 - $fetch_body['namespace'] = $namespace;
976 - }
977 -
978 - $fetch_resp = wp_remote_post( $fetch_url, array(
979 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
980 - 'body' => wp_json_encode( $fetch_body ),
981 - 'timeout' => 15,
982 - ) );
983 -
984 - if ( is_wp_error($fetch_resp) ) {
985 - return new WP_Error( 'pinecone_fetch', esc_html__('Failed to fetch from Pinecone.', 'mxchat') );
986 - }
987 -
988 - $fetch_data = json_decode( wp_remote_retrieve_body($fetch_resp), true );
989 - $vectors = $fetch_data['vectors'] ?? array();
990 -
991 - if ( empty($vectors) ) {
992 - return new WP_Error( 'not_found', esc_html__('Entry not found in Pinecone.', 'mxchat') );
993 - }
994 -
995 - // Whitelisted metadata fields the spec calls out — shown so devs can confirm
996 - // what is (and is NOT) stored per vector.
997 - $meta_fields = array( 'text', 'source_url', 'type', 'last_updated', 'created_at', 'bot_id', 'chunk_index', 'total_chunks' );
998 - $chunks = array();
999 - $content_type = '';
1000 - foreach ( $vectors as $vid => $vector ) {
1001 - $meta = isset($vector['metadata']) && is_array($vector['metadata']) ? $vector['metadata'] : array();
1002 - $text = $meta['text'] ?? '';
1003 - $index = isset($meta['chunk_index']) ? intval($meta['chunk_index']) : count($chunks);
1004 - $content_type = $meta['type'] ?? $content_type;
1005 -
1006 - $clean_meta = array();
1007 - foreach ( $meta_fields as $field ) {
1008 - if ( array_key_exists( $field, $meta ) && $field !== 'text' ) {
1009 - $clean_meta[ $field ] = is_scalar( $meta[ $field ] ) ? (string) $meta[ $field ] : wp_json_encode( $meta[ $field ] );
1010 - }
1011 - }
1012 -
1013 - $chunks[] = array(
1014 - 'index' => $index,
1015 - 'text' => $text,
1016 - 'length' => function_exists('mb_strlen') ? mb_strlen( $text ) : strlen( $text ),
1017 - 'vector_id' => (string) $vid,
1018 - 'metadata' => $clean_meta,
1019 - );
1020 - }
1021 -
1022 - usort( $chunks, function( $a, $b ) { return $a['index'] - $b['index']; } );
1023 -
1024 - $assembled = implode( "\n\n", wp_list_pluck( $chunks, 'text' ) );
1025 -
1026 - return array(
1027 - 'store' => 'pinecone',
1028 - 'source_url' => $source_url,
1029 - 'content_type' => $content_type,
1030 - 'is_chunked' => count( $chunks ) > 1,
1031 - 'chunk_count' => count( $chunks ),
1032 - 'assembled' => $assembled,
1033 - 'assembled_length' => function_exists('mb_strlen') ? mb_strlen( $assembled ) : strlen( $assembled ),
1034 - 'chunks' => array_values( $chunks ),
1035 - 'metadata' => array(),
1036 - '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'),
1037 - );
1038 710 }
1039 711
1040 -/**
1041 - * AJAX: Save edited content — re-chunks and re-embeds as needed.
1042 - * Works for both WordPress DB and Pinecone entries.
1043 - */
1044 -public function ajax_mxchat_save_entry_content() {
1045 - 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');
1046 715
1047 - if ( ! current_user_can('manage_options') ) {
1048 - wp_send_json_error( array( 'message' => 'Permission denied.' ) );
1049 - }
716 + // Verify permissions
717 + if (!current_user_can('manage_options')) {
718 + wp_send_json_error(esc_html__('Permission denied.', 'mxchat'));
719 + return;
720 + }
1050 721
1051 - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : '';
1052 - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0;
1053 - $content = isset($_POST['content']) ? wp_kses_post( wp_unslash($_POST['content']) ) : '';
1054 - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress';
1055 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
1056 - $content_type = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : 'content';
722 + global $wpdb;
723 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1057 724
1058 - if ( empty($content) ) {
1059 - wp_send_json_error( array( 'message' => 'Content cannot be empty.' ) );
1060 - }
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']) : '';
1061 729
1062 - // Get the embedding API key
1063 - $options = get_option('mxchat_options', array());
1064 - $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);
1065 733
1066 - if ( $bot_id !== 'default' && class_exists('MxChat_Multi_Bot_Manager') ) {
1067 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
1068 - $api_key = $bot_options['api_key'] ?? '';
1069 - }
1070 - if ( empty($api_key) ) {
1071 - $api_key = $options['api_key'] ?? '';
1072 - }
734 + if (is_array($embedding_vector)) {
735 + // Serialize the embedding vector before storing it
736 + $embedding_vector_serialized = serialize($embedding_vector);
1073 737
1074 - global $wpdb;
1075 - $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 + );
1076 750
1077 - // If source_url is empty but we have an entry_id, look it up
1078 - if ( empty($source_url) && $entry_id > 0 && $data_source === 'wordpress' ) {
1079 - $row = $wpdb->get_row( $wpdb->prepare( "SELECT source_url FROM {$table} WHERE id = %d", $entry_id ) );
1080 - if ( $row && ! empty($row->source_url) ) {
1081 - $source_url = $row->source_url;
1082 - }
1083 - }
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 + }
1084 763
1085 - // For manual entries (no source_url or mxchat:// prefix), delete the old entry by ID first
1086 - // so submit_content_to_db creates a replacement instead of a duplicate
1087 - // Also treat legacy mxchat.ai source URLs as manual — old bug assigned the site URL to manual entries
1088 - $is_legacy_manual = !empty($source_url) && strpos($source_url, 'mxchat.ai') !== false && strpos($source_url, 'mxchat://') !== 0;
1089 - if ( $entry_id > 0 && (empty($source_url) || strpos($source_url, 'mxchat://') === 0 || $is_legacy_manual) ) {
1090 - $wpdb->delete( $table, array( 'id' => $entry_id ), array( '%d' ) );
1091 - // Clear legacy URL so submit_content_to_db generates a unique mxchat:// identifier
1092 - // instead of reusing the shared URL (which would mass-delete other entries with the same URL)
1093 - if ( $is_legacy_manual ) {
1094 - $source_url = '';
1095 - }
1096 - }
1097 764
1098 - // Use the existing submit_content_to_db which handles chunking, Pinecone, and WP DB
1099 - $vector_id = ! empty($source_url) ? md5($source_url) : md5('mxchat_manual_' . $entry_id);
1100 -
1101 - // submit_content_to_db already handles: delete old chunks → re-chunk → re-embed → store
1102 - $result = MxChat_Utils::submit_content_to_db( $content, $source_url, $api_key, $vector_id, $bot_id, $content_type );
1103 -
1104 - if ( is_wp_error($result) ) {
1105 - wp_send_json_error( array( 'message' => $result->get_error_message() ) );
1106 - }
1107 -
1108 - wp_send_json_success( array( 'message' => 'Content saved and re-embedded successfully.' ) );
1109 -}
1110 -
1111 765 public function mxchat_get_pdf_processing_status($pdf_url) {
1112 766 $pdf_url = esc_url_raw($pdf_url);
1113 767 $status = get_transient(sanitize_key('mxchat_pdf_status_' . md5($pdf_url)));
1114 768
@@ -1151,18 +805,24 @@
1151 805 }
1152 806
1153 807
1154 808 public function mxchat_handle_sitemap_submission() {
809 + // Start logging the submission process
810 + //error_log('[MXCHAT-URL] ===== Starting URL submission process =====');
811 +
1155 812 // Check if the form was submitted and verify permissions
1156 813 if (!isset($_POST['submit_sitemap']) || !current_user_can('manage_options')) {
814 + //error_log('[MXCHAT-URL] Error: Unauthorized access or form not submitted properly');
1157 815 wp_die(esc_html__('Unauthorized access', 'mxchat'));
1158 816 }
1159 817
1160 818 // Verify nonce
819 + //error_log('[MXCHAT-URL] Verifying nonce');
1161 820 check_admin_referer('mxchat_submit_sitemap_action', 'mxchat_submit_sitemap_nonce');
1162 821
1163 822 // Validate URL
1164 823 if (!isset($_POST['sitemap_url']) || empty($_POST['sitemap_url'])) {
824 + //error_log('[MXCHAT-URL] Error: Empty or missing URL');
1165 825 set_transient('mxchat_admin_notice_error',
1166 826 esc_html__('Please provide a valid URL.', 'mxchat'),
1167 827 30
1168 828 );
@@ -1170,28 +830,12 @@
1170 830 exit;
1171 831 }
1172 832
1173 833 $submitted_url = esc_url_raw($_POST['sitemap_url']);
834 + //error_log('[MXCHAT-URL] Processing URL: ' . $submitted_url);
1174 835
1175 - // Convert Google Drive sharing URLs to direct download URLs
1176 - if ( strpos($submitted_url, 'drive.google.com') !== false ) {
1177 - $file_id = '';
1178 - if ( preg_match('/[?&]id=([a-zA-Z0-9_-]+)/', $submitted_url, $m) ) {
1179 - $file_id = $m[1];
1180 - } elseif ( preg_match('#/file/d/([a-zA-Z0-9_-]+)#', $submitted_url, $m) ) {
1181 - $file_id = $m[1];
1182 - }
1183 - if ( ! empty($file_id) ) {
1184 - $submitted_url = 'https://drive.google.com/uc?export=download&id=' . $file_id;
1185 - }
1186 - }
1187 -
1188 - // Get bot_id from form submission
1189 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
1190 -
1191 - // Get bot-specific options and validate API key
1192 - $bot_options = $this->get_bot_options($bot_id);
1193 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
836 + // Validate API key first
837 + $options = get_option('mxchat_options');
1194 838 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
1195 839
1196 840 if (strpos($selected_model, 'voyage') === 0) {
1197 841 $api_key = $options['voyage_api_key'] ?? '';
@@ -1208,31 +852,21 @@
1208 852 $error_message = sprintf(
1209 853 esc_html__('%s API key is not configured. Please add your API key in the settings before submitting content.', 'mxchat'),
1210 854 $provider_name
1211 855 );
856 + //error_log('[MXCHAT-URL] Error: ' . $error_message);
1212 857 set_transient('mxchat_admin_notice_error', $error_message, 30);
858 + //error_log('[MXCHAT-URL] Set error transient: ' . $error_message);
1213 859 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1214 860 exit;
1215 861 }
1216 862
1217 - // Fetch URL — send an honest, versioned MXChat crawler UA (not a spoofed
1218 - // browser). Stale browser UAs are exactly what WAFs like SiteGround's
1219 - // ModSecurity flag as scrapers, 403-ing the fetch (including PDFs served
1220 - // from the site's own media library, which route through this same call).
1221 - // See mxchat_ingest_user_agent(). Accept is kept for content negotiation;
1222 - // the browser-only Accept-Language fingerprint is dropped so it stays
1223 - // coherent with a bot identity.
1224 - $response = wp_remote_get($submitted_url, array(
1225 - 'timeout' => 30,
1226 - 'sslverify' => false,
1227 - 'user-agent' => mxchat_ingest_user_agent(),
1228 - 'headers' => array(
1229 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
1230 - ),
1231 - ));
863 + //error_log('[MXCHAT-URL] Fetching URL content');
864 + $response = wp_remote_get($submitted_url, array('timeout' => 30));
1232 865
1233 866 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1234 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);
1235 869 set_transient('mxchat_admin_notice_error',
1236 870 sprintf(
1237 871 esc_html__('Failed to fetch the URL: %s', 'mxchat'),
1238 872 esc_html($error_message)
@@ -1243,11 +877,13 @@
1243 877 exit;
1244 878 }
1245 879
1246 880 $content_type = wp_remote_retrieve_header($response, 'content-type');
881 + //error_log('[MXCHAT-URL] Content type: ' . $content_type);
1247 882 $body_content = wp_remote_retrieve_body($response);
1248 883
1249 884 if (empty($body_content)) {
885 + //error_log('[MXCHAT-URL] Error: Empty response body');
1250 886 set_transient('mxchat_admin_notice_error',
1251 887 esc_html__('Empty response received from URL.', 'mxchat'),
1252 888 30
1253 889 );
@@ -1253,21 +889,29 @@
1253 889 );
1254 890 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1255 891 exit;
1256 892 }
893 + //error_log('[MXCHAT-URL] Retrieved body content length: ' . strlen($body_content) . ' bytes');
1257 894
1258 895 // Handle PDF URL
1259 896 if ($this->mxchat_is_pdf_url($submitted_url, $response)) {
1260 - $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);
1261 900
1262 - if ($result === 'queued') {
1263 - set_transient('mxchat_admin_notice_success',
1264 - 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'),
1265 909 30
1266 910 );
1267 911 } else {
1268 912 set_transient('mxchat_admin_notice_error',
1269 - esc_html__('Failed to queue PDF processing: ', 'mxchat') . esc_html($result),
913 + esc_html__('Failed to start PDF processing: ', 'mxchat') . esc_html($result),
1270 914 30
1271 915 );
1272 916 }
1273 917
@@ -1276,8 +920,9 @@
1276 920 }
1277 921
1278 922 // Handle Sitemap XML
1279 923 if (strpos($content_type, 'xml') !== false || strpos($body_content, '<urlset') !== false) {
924 + //error_log('[MXCHAT-URL] Detected XML content, processing as sitemap');
1280 925 libxml_use_internal_errors(true);
1281 926 $xml = simplexml_load_string($body_content);
1282 927 $xml_errors = libxml_get_errors();
1283 928 libxml_clear_errors();
@@ -1282,8 +927,15 @@
1282 927 $xml_errors = libxml_get_errors();
1283 928 libxml_clear_errors();
1284 929
1285 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 +
1286 938 set_transient('mxchat_admin_notice_error',
1287 939 esc_html__('Invalid sitemap XML. Please provide a valid sitemap.', 'mxchat'),
1288 940 30
1289 941 );
@@ -1290,18 +942,27 @@
1290 942 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1291 943 exit;
1292 944 }
1293 945
1294 - $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);
1295 949
1296 - if ($result === 'queued') {
1297 - set_transient('mxchat_admin_notice_success',
1298 - 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'),
1299 958 30
1300 959 );
1301 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
1302 963 set_transient('mxchat_admin_notice_error',
1303 - 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'),
1304 965 30
1305 966 );
1306 967 }
1307 968
@@ -1308,50 +969,127 @@
1308 969 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1309 970 exit;
1310 971 }
1311 972
1312 - // Handle Regular URL (single page)
973 + // Handle Regular URL
974 + //error_log('[MXCHAT-URL] Processing as regular webpage');
1313 975 $page_content = $this->mxchat_extract_main_content($body_content);
976 + //error_log('[MXCHAT-URL] Extracted content length: ' . strlen($page_content) . ' bytes');
977 +
1314 978 $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
979 + //error_log('[MXCHAT-URL] Sanitized content length: ' . strlen($sanitized_content) . ' bytes');
1315 980
1316 - //error_log('[MXCHAT-URL-DEBUG] URL: ' . $submitted_url);
1317 - //error_log('[MXCHAT-URL-DEBUG] Extracted page_content length: ' . strlen($page_content));
1318 - //error_log('[MXCHAT-URL-DEBUG] Sanitized content length: ' . strlen($sanitized_content));
1319 - //error_log('[MXCHAT-URL-DEBUG] Content preview: ' . substr($sanitized_content, 0, 500));
1320 -
1321 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
1322 985 set_transient('mxchat_admin_notice_error',
1323 986 esc_html__('No valid content found on the provided URL.', 'mxchat'),
1324 987 30
1325 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 +
1326 998 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1327 999 exit;
1328 1000 }
1329 1001
1330 - // For single URLs, process immediately using submit_content_to_db
1331 - // This handles chunking automatically for large content
1332 - $db_result = MxChat_Utils::submit_content_to_db(
1333 - $sanitized_content,
1334 - $submitted_url,
1335 - $api_key,
1336 - null,
1337 - $bot_id,
1338 - 'url' // content_type
1339 - );
1002 + //error_log('[MXCHAT-URL] Generating embedding for content');
1003 + $embedding_vector = $this->mxchat_generate_embedding($sanitized_content);
1340 1004
1341 - if (is_wp_error($db_result)) {
1342 - $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
1343 1012 set_transient('mxchat_admin_notice_error', $error_message, 30);
1344 - } 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');
1345 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
1346 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);
1347 1086 }
1348 1087
1088 + //error_log('[MXCHAT-URL] ===== Completed URL submission process =====');
1349 1089 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
1350 1090 exit;
1351 1091 }
1352 -
1353 -
1354 1092 public function mxchat_get_single_url_status() {
1355 1093 $status = get_transient('mxchat_single_url_status');
1356 1094 if (!$status) {
1357 1095 return null;
@@ -1363,11 +1101,13 @@
1363 1101 }
1364 1102
1365 1103 return $status;
1366 1104 }
1367 -
1368 -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');
1369 1108 if (!current_user_can('manage_options')) {
1109 + //error_log(esc_html__('Unauthorized sitemap processing attempt', 'mxchat'));
1370 1110 return false;
1371 1111 }
1372 1112
1373 1113 try {
@@ -1376,96 +1116,331 @@
1376 1116 if (!$xml || !is_object($xml)) {
1377 1117 throw new Exception(__('Invalid XML object provided', 'mxchat'));
1378 1118 }
1379 1119
1380 - // Get bot-specific embedding API for validation
1381 - $bot_options = $this->get_bot_options($bot_id);
1382 - $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.");
1383 1123
1384 - // Test the embedding API before processing
1385 - $test_phrase = "Test embedding generation for MxChat";
1386 - $test_result = $this->mxchat_generate_embedding($test_phrase, $bot_id);
1387 -
1124 + // Check if test_result is a string (error message) rather than an array (valid embedding)
1388 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 +
1389 1143 throw new Exception(__('Embedding API validation failed: ', 'mxchat') . $test_result);
1390 1144 }
1391 1145
1146 + // Make sure it's an array (valid embedding)
1392 1147 if (!is_array($test_result)) {
1148 + //error_log('[MXCHAT-URL] Embedding API returned unexpected result type: ' . gettype($test_result));
1393 1149 throw new Exception(__('Embedding API returned unexpected result type. Please check your configuration.', 'mxchat'));
1394 1150 }
1395 1151
1396 - // Extract URLs from sitemap
1397 - $urls = array();
1152 + $urls = [];
1398 1153 foreach ($xml->url as $url_element) {
1399 1154 $url = esc_url_raw((string)$url_element->loc);
1400 1155 if ($url) {
1401 - $urls[] = array('url' => $url);
1156 + $urls[] = $url;
1402 1157 }
1403 1158 }
1404 1159
1405 - $total_urls = count($urls);
1160 + $total_urls = absint(count($urls));
1406 1161
1407 1162 if ($total_urls < 1) {
1408 1163 throw new Exception(__('No valid URLs found in sitemap', 'mxchat'));
1409 1164 }
1410 1165
1411 - // Create unique queue ID
1412 - $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 + ));
1413 1173
1414 - // Add URLs to queue
1415 - $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 + );
1416 1180
1417 - if ($queued_count === 0) {
1418 - throw new Exception(__('Failed to add URLs to processing queue', 'mxchat'));
1419 - }
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 + );
1420 1186
1421 - // Store queue metadata
1422 - $this->mxchat_set_queue_meta($queue_id, 'source_url', $sitemap_url);
1423 - $this->mxchat_set_queue_meta($queue_id, 'queue_type', 'sitemap');
1424 - $this->mxchat_set_queue_meta($queue_id, 'total_items', $total_urls);
1425 - $this->mxchat_set_queue_meta($queue_id, 'bot_id', $bot_id);
1426 - $this->mxchat_set_queue_meta($queue_id, 'created_at', current_time('mysql'));
1187 + return __('scheduled', 'mxchat');
1427 1188
1428 - // Store queue ID in transient for status tracking
1429 - set_transient('mxchat_active_queue_sitemap', $queue_id, DAY_IN_SECONDS);
1430 - set_transient('mxchat_last_sitemap_url', $sitemap_url, DAY_IN_SECONDS);
1431 -
1432 - return 'queued';
1433 -
1434 - } catch (Exception $e) {
1189 + } catch (\Exception $e) {
1435 1190 $error_message = $e->getMessage();
1436 1191 //error_log(sprintf(esc_html__('Error preparing sitemap for processing: %s', 'mxchat'), esc_html($error_message)));
1437 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 +
1438 1214 return $error_message;
1439 1215 }
1440 -
1441 1216 }
1442 1217
1443 -/**
1444 - * Remove shortcode tags but preserve the content inside them
1445 - * Example: [vc_column]Hello World[/vc_column] becomes "Hello World"
1446 - *
1447 - * @param string $content The content containing shortcodes
1448 - * @return string Content with shortcode tags removed but inner content preserved
1449 - */
1450 -private function strip_shortcode_tags_preserve_content($content) {
1451 - // Single-pass regex removes all shortcode brackets: [tag], [tag attr="val"], [/tag], [tag /]
1452 - // Content between tags is inherently preserved since only brackets are targeted
1453 - $result = preg_replace('/\[\/?\w[\w-]*[^\]]*\]/', '', $content);
1454 - 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 + }
1455 1434 }
1456 -
1457 1435 public function mxchat_sanitize_content_for_api($content) {
1458 1436 //error_log('[MXCHAT-SANITIZE] Original content preview: ' . substr($content, 0, 500) . '...');
1459 -
1460 - // Remove shortcode tags but PRESERVE content inside them
1461 - $content = $this->strip_shortcode_tags_preserve_content($content);
1462 -
1437 +
1463 1438 // Remove script, style tags, and HTML comments
1464 1439 $content = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $content);
1465 1440 $content = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $content);
1466 1441 $content = preg_replace('/<!--(.|\s)*?-->/', '', $content);
1467 -
1442 +
1468 1443 // Remove all HTML tags and decode HTML entities
1469 1444 $content = wp_strip_all_tags($content);
1470 1445 $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5);
1471 1446
@@ -1523,12 +1498,12 @@
1523 1498 @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
1524 1499 $xpath = new DOMXPath($dom);
1525 1500
1526 1501 // For debugging purposes
1527 - $debugEnabled = true; // Set to true to enable debugging output
1502 + $debugEnabled = false; // Set to true to enable debugging output
1528 1503 $debug = function($message) use ($debugEnabled) {
1529 1504 if ($debugEnabled) {
1530 - //error_log('[MXCHAT-EXTRACT-DEBUG] ' . $message);
1505 + //error_log('[MXCHAT-DEBUG] ' . $message);
1531 1506 }
1532 1507 };
1533 1508
1534 1509 // Direct targeting for Gerow theme posts
@@ -1627,25 +1602,18 @@
1627 1602 '//main',
1628 1603 '//div[contains(@class, "content")]'
1629 1604 ];
1630 1605
1631 - // First handle Elementor content - get only leaf widget containers to avoid duplicates
1606 + // First handle Elementor content
1632 1607 $debug("Checking for Elementor content");
1633 - // Get widget containers that are direct children of widgets (not nested inside other widget containers)
1634 - $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")]');
1635 1609 if ($elementor_widgets && $elementor_widgets->length > 0) {
1636 1610 $debug("Found Elementor widgets");
1637 - $seen_content = array(); // Track seen content to avoid duplicates
1638 1611 $combined_content = '';
1639 1612 foreach ($elementor_widgets as $widget) {
1640 1613 $widget_content = $dom->saveHTML($widget);
1641 1614 if (!empty($widget_content)) {
1642 - // Create a hash of the content to detect duplicates
1643 - $content_hash = md5($widget_content);
1644 - if (!isset($seen_content[$content_hash])) {
1645 - $seen_content[$content_hash] = true;
1646 - $combined_content .= $widget_content;
1647 - }
1615 + $combined_content .= $widget_content;
1648 1616 }
1649 1617 }
1650 1618 if (!empty($combined_content)) {
1651 1619 $debug("Returning Elementor content");
@@ -1657,14 +1625,15 @@
1657 1625 foreach ($selectors as $selector) {
1658 1626 $debug("Trying selector: " . $selector);
1659 1627 $nodes = $xpath->query($selector);
1660 1628 if ($nodes && $nodes->length > 0) {
1661 - $debug("Found " . $nodes->length . " matches for selector: " . $selector);
1662 - // Only take the FIRST matching node to avoid duplicate content
1663 - // (pages often have nested or multiple containers with same class)
1664 - $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 + }
1665 1634 if (!empty($content)) {
1666 - $debug("Returning content from selector: " . $selector . " (first match only)");
1635 + $debug("Returning content from selector: " . $selector);
1667 1636 return $content;
1668 1637 }
1669 1638 }
1670 1639 }
@@ -1688,106 +1657,16 @@
1688 1657 $debug("Returning blog-area section content");
1689 1658 return $content;
1690 1659 }
1691 1660 }
1692 -
1693 - // Generic container selectors for non-CMS sites (like .asp pages)
1694 - $debug("Trying generic container selectors");
1695 - $generic_selectors = [
1696 - '//div[@id="main"]',
1697 - '//div[@id="wrapper"]',
1698 - '//div[@id="page"]',
1699 - '//div[@id="site-content"]',
1700 - '//div[contains(@class, "main-content")]',
1701 - '//div[contains(@class, "page-content")]',
1702 - '//div[contains(@class, "site-content")]',
1703 - ];
1704 -
1705 - foreach ($generic_selectors as $selector) {
1706 - $debug("Trying generic selector: " . $selector);
1707 - $nodes = $xpath->query($selector);
1708 - if ($nodes && $nodes->length > 0) {
1709 - $content = $dom->saveHTML($nodes->item(0));
1710 - if (!empty($content)) {
1711 - $debug("Returning content from generic selector: " . $selector);
1712 - return $content;
1713 - }
1714 - }
1715 - }
1716 -
1717 - // Paragraph-based content detection - find regions with substantial text
1718 - $debug("Trying paragraph-based content detection");
1719 - $paragraphs = $xpath->query('//p[string-length(normalize-space()) > 50]');
1720 - if ($paragraphs && $paragraphs->length >= 3) {
1721 - $debug("Found " . $paragraphs->length . " substantial paragraphs");
1722 - // Collect all substantial paragraphs and their content
1723 - $paragraph_content = '';
1724 - foreach ($paragraphs as $p) {
1725 - $paragraph_content .= $dom->saveHTML($p) . "\n";
1726 - }
1727 - if (!empty($paragraph_content)) {
1728 - $debug("Returning paragraph-based content");
1729 - return $paragraph_content;
1730 - }
1731 - }
1732 -
1733 - // Improved body fallback - strip nav/header/footer elements first
1734 - $debug("Using improved body fallback");
1661 +
1662 + // Fallback: Return the body content if no specific selector matches
1663 + $debug("Using body fallback");
1735 1664 $body = $dom->getElementsByTagName('body');
1736 1665 if ($body->length > 0) {
1737 - // Clone the body to avoid modifying the original DOM
1738 - $body_clone = $body->item(0)->cloneNode(true);
1739 -
1740 - // Remove common non-content elements by tag name
1741 - $remove_tags = ['nav', 'header', 'footer', 'aside', 'script', 'style', 'noscript'];
1742 - foreach ($remove_tags as $tag) {
1743 - $elements = $body_clone->getElementsByTagName($tag);
1744 - // Iterate backwards to safely remove elements
1745 - for ($i = $elements->length - 1; $i >= 0; $i--) {
1746 - $el = $elements->item($i);
1747 - if ($el && $el->parentNode) {
1748 - $el->parentNode->removeChild($el);
1749 - }
1750 - }
1751 - }
1752 -
1753 - // Remove elements with common non-content class names using XPath on the cloned body
1754 - $temp_dom = new DOMDocument();
1755 - @$temp_dom->appendChild($temp_dom->importNode($body_clone, true));
1756 - $temp_xpath = new DOMXPath($temp_dom);
1757 -
1758 - $remove_class_patterns = [
1759 - '//*[contains(@class, "nav")]',
1760 - '//*[contains(@class, "menu")]',
1761 - '//*[contains(@class, "sidebar")]',
1762 - '//*[contains(@class, "footer")]',
1763 - '//*[contains(@class, "header")]',
1764 - '//*[contains(@id, "nav")]',
1765 - '//*[contains(@id, "menu")]',
1766 - '//*[contains(@id, "sidebar")]',
1767 - '//*[contains(@id, "footer")]',
1768 - '//*[contains(@id, "header")]',
1769 - ];
1770 -
1771 - foreach ($remove_class_patterns as $pattern) {
1772 - $elements = $temp_xpath->query($pattern);
1773 - if ($elements) {
1774 - for ($i = $elements->length - 1; $i >= 0; $i--) {
1775 - $el = $elements->item($i);
1776 - if ($el && $el->parentNode) {
1777 - $el->parentNode->removeChild($el);
1778 - }
1779 - }
1780 - }
1781 - }
1782 -
1783 - $cleaned_content = $temp_dom->saveHTML();
1784 - if (!empty($cleaned_content)) {
1785 - $debug("Returning cleaned body content");
1786 - return $cleaned_content;
1787 - }
1666 + return $dom->saveHTML($body->item(0));
1788 1667 }
1789 -
1668 +
1790 1669 // Last resort: return the original HTML
1791 1670 $debug("Returning original HTML");
1792 1671 return $html;
1793 1672 } catch (Exception $e) {
@@ -1839,41 +1718,47 @@
1839 1718 try {
1840 1719 // Verify the request
1841 1720 check_ajax_referer('mxchat_status_nonce', 'nonce');
1842 1721
1843 - // Get active queue IDs
1844 - $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
1845 - $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');
1846 1725
1847 - $sitemap_status = false;
1848 - $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;
1849 1728
1850 - // Get sitemap queue status
1851 - if ($sitemap_queue_id) {
1852 - $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;
1853 1732 }
1854 1733
1855 - // Get PDF queue status
1856 - if ($pdf_queue_id) {
1857 - $pdf_status = $this->mxchat_get_queue_status_data($pdf_queue_id, 'pdf');
1858 - }
1734 + // Set the current PDF URL for the manual batch processing button
1735 + $current_pdf_url = $pdf_url;
1859 1736
1737 + // Check for true processing status, not just presence of status
1860 1738 $is_active_processing =
1861 - ($sitemap_status && $sitemap_status['status'] === 'processing') ||
1862 - ($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');
1863 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 +
1864 1747 // Return JSON response with the status data
1865 1748 wp_send_json(array(
1866 1749 'pdf_status' => $pdf_status,
1867 1750 'sitemap_status' => $sitemap_status,
1751 + 'single_url_status' => $single_url_status,
1868 1752 'is_processing' => $is_active_processing,
1869 - 'sitemap_queue_id' => $sitemap_queue_id,
1870 - 'pdf_queue_id' => $pdf_queue_id
1753 + 'current_pdf_url' => $current_pdf_url
1871 1754 ));
1872 1755
1873 1756 } catch (Exception $e) {
1757 + // Log the error
1874 1758 //error_log('MxChat Status Update Error: ' . $e->getMessage());
1875 1759
1760 + // Return a friendly error response
1876 1761 wp_send_json_error(array(
1877 1762 'message' => 'Error getting status updates: ' . $e->getMessage(),
1878 1763 'status' => 'error'
1879 1764 ));
@@ -1878,1371 +1763,8 @@
1878 1763 'status' => 'error'
1879 1764 ));
1880 1765 }
1881 1766 }
1882 -
1883 -/**
1884 - * Helper function to get queue status data
1885 - */
1886 -private function mxchat_get_queue_status_data($queue_id, $type = 'sitemap') {
1887 - global $wpdb;
1888 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
1889 -
1890 - // Get counts by status
1891 - $counts = $wpdb->get_results($wpdb->prepare(
1892 - "SELECT status, COUNT(*) as count
1893 - FROM $table_name
1894 - WHERE queue_id = %s
1895 - GROUP BY status",
1896 - $queue_id
1897 - ), OBJECT_K);
1898 -
1899 - $total = 0;
1900 - $completed = 0;
1901 - $failed = 0;
1902 - $processing = 0;
1903 - $pending = 0;
1904 -
1905 - foreach ($counts as $status => $data) {
1906 - $count = absint($data->count);
1907 - $total += $count;
1908 -
1909 - switch ($status) {
1910 - case 'completed':
1911 - $completed = $count;
1912 - break;
1913 - case 'failed':
1914 - $failed = $count;
1915 - break;
1916 - case 'processing':
1917 - $processing = $count;
1918 - break;
1919 - case 'pending':
1920 - $pending = $count;
1921 - break;
1922 - }
1923 - }
1924 -
1925 - if ($total === 0) {
1926 - return false;
1927 - }
1928 -
1929 - // Calculate percentage
1930 - $percentage = round((($completed + $failed) / $total) * 100);
1931 -
1932 - // Get failed items details (limit to 50)
1933 - $failed_items = array();
1934 - if ($failed > 0) {
1935 - $failed_results = $wpdb->get_results($wpdb->prepare(
1936 - "SELECT item_type, item_data, error_message, attempts, completed_at
1937 - FROM $table_name
1938 - WHERE queue_id = %s
1939 - AND status = 'failed'
1940 - AND attempts >= max_attempts
1941 - ORDER BY id DESC
1942 - LIMIT 50",
1943 - $queue_id
1944 - ));
1945 -
1946 - foreach ($failed_results as $item) {
1947 - $data = json_decode($item->item_data, true);
1948 - $url = $type === 'pdf' ? ($data['pdf_url'] ?? '') : ($data['url'] ?? '');
1949 -
1950 - $failed_items[] = array(
1951 - 'url' => $url,
1952 - 'page' => $type === 'pdf' ? ($data['page_number'] ?? 0) : 0,
1953 - 'error' => $item->error_message,
1954 - 'retries' => $item->attempts,
1955 - 'time' => strtotime($item->completed_at)
1956 - );
1957 - }
1958 - }
1959 -
1960 - // Get queue metadata
1961 - $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
1962 -
1963 - // Determine if queue is complete
1964 - $is_complete = ($pending === 0 && $processing === 0);
1965 -
1966 - // Get last update time
1967 - $last_update = $wpdb->get_var($wpdb->prepare(
1968 - "SELECT MAX(UNIX_TIMESTAMP(COALESCE(completed_at, started_at, created_at)))
1969 - FROM $table_name
1970 - WHERE queue_id = %s",
1971 - $queue_id
1972 - ));
1973 -
1974 - $last_update_text = $last_update ? human_time_diff($last_update, time()) . ' ' . __('ago', 'mxchat') : __('Just now', 'mxchat');
1975 -
1976 - // Format based on type
1977 - if ($type === 'pdf') {
1978 - return array(
1979 - 'total_pages' => $total,
1980 - 'processed_pages' => $completed + $failed,
1981 - 'failed_pages' => $failed,
1982 - 'percentage' => $percentage,
1983 - 'status' => $is_complete ? 'complete' : 'processing',
1984 - 'last_update' => $last_update_text,
1985 - 'failed_pages_list' => $failed_items,
1986 - 'pdf_url' => $source_url,
1987 - 'queue_id' => $queue_id
1988 - );
1989 - } else {
1990 - return array(
1991 - 'total_urls' => $total,
1992 - 'processed_urls' => $completed + $failed,
1993 - 'failed_urls' => $failed,
1994 - 'percentage' => $percentage,
1995 - 'status' => $is_complete ? 'complete' : 'processing',
1996 - 'last_update' => $last_update_text,
1997 - 'failed_urls_list' => $failed_items,
1998 - 'sitemap_url' => $source_url,
1999 - 'queue_id' => $queue_id
2000 - );
2001 - }
2002 -}
2003 -
2004 -/**
2005 - * Public method to get processing status for both sitemap and PDF queues
2006 - * Used by admin pages to display processing status
2007 - *
2008 - * @return array Array with 'sitemap_status', 'pdf_status', and 'is_processing' keys
2009 - */
2010 -public function mxchat_get_processing_statuses() {
2011 - $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
2012 - $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
2013 -
2014 - $sitemap_status = false;
2015 - $pdf_status = false;
2016 -
2017 - if ($sitemap_queue_id) {
2018 - $sitemap_status = $this->mxchat_get_queue_status_data($sitemap_queue_id, 'sitemap');
2019 - }
2020 -
2021 - if ($pdf_queue_id) {
2022 - $pdf_status = $this->mxchat_get_queue_status_data($pdf_queue_id, 'pdf');
2023 - }
2024 -
2025 - $is_processing =
2026 - ($sitemap_status && $sitemap_status['status'] === 'processing') ||
2027 - ($pdf_status && $pdf_status['status'] === 'processing');
2028 -
2029 - return array(
2030 - 'sitemap_status' => $sitemap_status,
2031 - 'pdf_status' => $pdf_status,
2032 - 'is_processing' => $is_processing
2033 - );
2034 -}
2035 -
2036 -/**
2037 - * AJAX handler to get recent knowledge entries for real-time table updates
2038 - * UPDATED: Now supports both WordPress DB and Pinecone data sources
2039 - */
2040 -public function ajax_mxchat_get_recent_entries() {
2041 - check_ajax_referer('mxchat_entries_nonce', 'nonce');
2042 -
2043 - if (!current_user_can('manage_options')) {
2044 - wp_send_json_error(array('message' => 'Unauthorized'));
2045 - return;
2046 - }
2047 -
2048 - global $wpdb;
2049 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2050 -
2051 - // Get parameters
2052 - $last_id = isset($_POST['last_id']) ? absint($_POST['last_id']) : 0;
2053 - $limit = isset($_POST['limit']) ? min(absint($_POST['limit']), 50) : 10;
2054 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
2055 -
2056 - // Check if Pinecone is enabled for this bot
2057 - $pinecone_manager = $this->mxchat_get_pinecone_manager();
2058 - $pinecone_options = $pinecone_manager ? $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id) : array();
2059 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2060 - $has_pinecone_api = !empty($pinecone_options['mxchat_pinecone_api_key']);
2061 -
2062 - if ($use_pinecone && $has_pinecone_api) {
2063 - // PINECONE DATA SOURCE - Get grouped entry count (not raw vector count)
2064 - // Use mxchat_fetch_pinecone_records which returns total_unique_entries
2065 - $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, '', 1, 10, $bot_id, '');
2066 - $total_count = $records['total'] ?? 0;
2067 -
2068 - // For Pinecone, we don't return individual entries during polling
2069 - // (entries are already displayed on page load via mxchat_fetch_pinecone_records)
2070 - // We just return the updated count
2071 - wp_send_json_success(array(
2072 - 'entries' => array(),
2073 - 'total_count' => absint($total_count),
2074 - 'max_id' => $last_id,
2075 - 'data_source' => 'pinecone'
2076 - ));
2077 - return;
2078 - }
2079 -
2080 - // WORDPRESS DB DATA SOURCE
2081 - // Build query to get entries newer than last_id
2082 - $where_clauses = array('1=1');
2083 - $where_values = array();
2084 -
2085 - if ($last_id > 0) {
2086 - $where_clauses[] = 'id > %d';
2087 - $where_values[] = $last_id;
2088 - }
2089 -
2090 - // Note: WordPress DB table doesn't have bot_id column
2091 - // Multi-bot filtering is handled via Pinecone namespaces
2092 -
2093 - $where_sql = implode(' AND ', $where_clauses);
2094 -
2095 - // Get recent entries
2096 - $query = "SELECT id, article_content, source_url, timestamp
2097 - FROM $table_name
2098 - WHERE $where_sql
2099 - ORDER BY id DESC
2100 - LIMIT %d";
2101 -
2102 - $where_values[] = $limit;
2103 -
2104 - $entries = $wpdb->get_results($wpdb->prepare($query, $where_values));
2105 -
2106 - // Get total count of GROUPED entries (by source_url) - matches pagination display
2107 - // Count unique source_urls (excluding mxchat:// internal refs) + count of ungrouped rows
2108 - $total_count = $wpdb->get_var(
2109 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2110 - (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')"
2111 - );
2112 -
2113 - // Format entries for response
2114 - $formatted_entries = array();
2115 - $preview_length = 150;
2116 - foreach ($entries as $entry) {
2117 - // Parse chunk metadata using the proper chunker method (same as initial page load)
2118 - if (class_exists('MxChat_Chunker')) {
2119 - $chunk_meta = MxChat_Chunker::parse_stored_chunk($entry->article_content);
2120 - $display_content = $chunk_meta['text'];
2121 - $chunk_metadata = $chunk_meta['metadata'];
2122 - } else {
2123 - $display_content = $entry->article_content;
2124 - $chunk_metadata = array();
2125 - }
2126 -
2127 - $content_preview = mb_strlen($display_content) > $preview_length
2128 - ? mb_substr($display_content, 0, $preview_length) . '...'
2129 - : $display_content;
2130 -
2131 - $formatted_entries[] = array(
2132 - 'id' => $entry->id,
2133 - 'preview' => esc_html($content_preview),
2134 - 'full_content' => wp_kses_post(wpautop($display_content)),
2135 - 'content_length' => mb_strlen($display_content),
2136 - 'preview_length' => $preview_length,
2137 - 'source_url' => $entry->source_url,
2138 - 'has_link' => !empty($entry->source_url) && strpos($entry->source_url, 'mxchat://') !== 0,
2139 - 'chunk_metadata' => $chunk_metadata,
2140 - 'bot_id' => $entry->bot_id ?? 'default',
2141 - 'edit_nonce' => wp_create_nonce('mxchat_edit_entry_nonce'),
2142 - 'delete_nonce' => wp_create_nonce('mxchat_delete_prompt_nonce')
2143 - );
2144 - }
2145 -
2146 - wp_send_json_success(array(
2147 - 'entries' => $formatted_entries,
2148 - 'total_count' => absint($total_count),
2149 - 'max_id' => !empty($entries) ? $entries[0]->id : $last_id,
2150 - 'data_source' => 'wordpress'
2151 - ));
2152 -}
2153 -
2154 -/**
2155 - * Get Pinecone total count from stats API
2156 - * Helper function for ajax_mxchat_get_recent_entries
2157 - */
2158 -private function mxchat_get_pinecone_count_from_stats($pinecone_options) {
2159 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2160 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2161 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
2162 -
2163 - if (empty($api_key) || empty($host)) {
2164 - return 0;
2165 - }
2166 -
2167 - try {
2168 - $stats_url = "https://{$host}/describe_index_stats";
2169 -
2170 - $response = wp_remote_post($stats_url, array(
2171 - 'headers' => array(
2172 - 'Api-Key' => $api_key,
2173 - 'Content-Type' => 'application/json'
2174 - ),
2175 - 'body' => '{}',
2176 - 'timeout' => 10
2177 - ));
2178 -
2179 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2180 - $body = wp_remote_retrieve_body($response);
2181 - $stats_data = json_decode($body, true);
2182 -
2183 - // If namespace is specified, get count from that specific namespace
2184 - if (!empty($namespace) && isset($stats_data['namespaces'][$namespace]['vectorCount'])) {
2185 - return intval($stats_data['namespaces'][$namespace]['vectorCount']);
2186 - }
2187 -
2188 - // If no namespace specified or namespace not found in response, use total
2189 - return intval($stats_data['totalVectorCount'] ?? 0);
2190 - }
2191 -
2192 - return 0;
2193 -
2194 - } catch (Exception $e) {
2195 - return 0;
2196 - }
2197 -}
2198 -
2199 -/**
2200 - * AJAX handler to refresh Pinecone entries table via AJAX
2201 - * Returns the table HTML for updating the UI without a full page reload
2202 - */
2203 -public function ajax_mxchat_refresh_pinecone_entries() {
2204 - check_ajax_referer('mxchat_entries_nonce', 'nonce');
2205 -
2206 - if (!current_user_can('manage_options')) {
2207 - wp_send_json_error(array('message' => 'Unauthorized'));
2208 - return;
2209 - }
2210 -
2211 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
2212 - $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
2213 - $per_page = 25;
2214 - $search_query = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
2215 - $content_type_filter = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : '';
2216 -
2217 - // Get Pinecone manager and options
2218 - $pinecone_manager = $this->mxchat_get_pinecone_manager();
2219 - if (!$pinecone_manager) {
2220 - wp_send_json_error(array('message' => 'Pinecone manager not available'));
2221 - return;
2222 - }
2223 -
2224 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
2225 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2226 - $pinecone_api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2227 -
2228 - if (!$use_pinecone || empty($pinecone_api_key)) {
2229 - wp_send_json_error(array('message' => 'Pinecone not configured'));
2230 - return;
2231 - }
2232 -
2233 - // Fetch records from Pinecone
2234 - $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, $search_query, $page, $per_page, $bot_id, $content_type_filter);
2235 - $prompts = $records['data'] ?? array();
2236 - $total_records = $records['total'] ?? 0;
2237 -
2238 - // Preprocess Pinecone records — set chunk_metadata and display_content
2239 - // (matches admin-knowledge-page.php preprocessing)
2240 - foreach ($prompts as $prompt) {
2241 - if (isset($prompt->chunk_index) && $prompt->chunk_index !== null) {
2242 - $prompt->chunk_metadata = array(
2243 - 'chunk_index' => intval($prompt->chunk_index),
2244 - 'total_chunks' => isset($prompt->total_chunks) ? intval($prompt->total_chunks) : null,
2245 - 'is_chunked' => isset($prompt->is_chunked) ? (bool) $prompt->is_chunked : true,
2246 - 'source_url' => $prompt->source_url ?? ''
2247 - );
2248 - $prompt->display_content = $prompt->article_content;
2249 - } else {
2250 - $prompt->chunk_metadata = array();
2251 - $prompt->display_content = $prompt->article_content ?? '';
2252 - }
2253 - }
2254 -
2255 - // Group prompts by source_url
2256 - $grouped_prompts = array();
2257 - foreach ($prompts as $prompt) {
2258 - $source_url = '';
2259 - if (!empty($prompt->chunk_metadata['source_url'])) {
2260 - $source_url = $prompt->chunk_metadata['source_url'];
2261 - } elseif (!empty($prompt->source_url)) {
2262 - $source_url = $prompt->source_url;
2263 - }
2264 -
2265 - if (!empty($source_url)) {
2266 - if (!isset($grouped_prompts[$source_url])) {
2267 - $grouped_prompts[$source_url] = array();
2268 - }
2269 - $grouped_prompts[$source_url][] = $prompt;
2270 - } else {
2271 - $grouped_prompts['_ungrouped_' . $prompt->id] = array($prompt);
2272 - }
2273 - }
2274 -
2275 - // Sort each group by chunk_index
2276 - foreach ($grouped_prompts as $source_url => &$group) {
2277 - usort($group, function($a, $b) {
2278 - $index_a = isset($a->chunk_metadata['chunk_index']) ? intval($a->chunk_metadata['chunk_index']) : 0;
2279 - $index_b = isset($b->chunk_metadata['chunk_index']) ? intval($b->chunk_metadata['chunk_index']) : 0;
2280 - return $index_a - $index_b;
2281 - });
2282 - }
2283 - unset($group);
2284 -
2285 - // Build HTML for the table rows - must match admin-knowledge-page.php structure exactly
2286 - ob_start();
2287 - $display_index = 0;
2288 - $current_page = $page;
2289 - $data_source = 'pinecone';
2290 - $current_bot_id = $bot_id;
2291 - $preview_length = 150;
2292 -
2293 - if (empty($grouped_prompts)) {
2294 - echo '<tr><td colspan="4" style="padding: 40px; text-align: center; color: var(--mxch-text-muted);">';
2295 - esc_html_e('No knowledge entries found in Pinecone.', 'mxchat');
2296 - echo '</td></tr>';
2297 - } else {
2298 - foreach ($grouped_prompts as $source_url => $group) {
2299 - $chunk_count = count($group);
2300 - $first_prompt = $group[0];
2301 - $display_index++;
2302 -
2303 - if ($chunk_count > 1) {
2304 - // Multiple chunks - show grouped row with expand button
2305 - $group_id = 'group-' . md5($source_url);
2306 - ?>
2307 - <tr id="prompt-<?php echo esc_attr($first_prompt->id); ?>"
2308 - class="mxchat-chunk-group-header"
2309 - data-source="<?php echo esc_attr($data_source); ?>"
2310 - data-group-id="<?php echo esc_attr($group_id); ?>"
2311 - style="border-bottom: 1px solid var(--mxch-card-border); background: rgba(33, 150, 243, 0.02);">
2312 - <td style="padding: 12px 16px; text-align: center;">
2313 - <input type="checkbox"
2314 - class="mxchat-entry-checkbox"
2315 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2316 - data-source="<?php echo esc_attr($data_source); ?>"
2317 - data-source-url="<?php echo esc_attr($source_url); ?>"
2318 - data-is-group="true"
2319 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>">
2320 - </td>
2321 - <td style="padding: 12px 16px; font-size: 13px;">
2322 - <?php echo esc_html($display_index + (($current_page - 1) * $per_page)); ?>
2323 - </td>
2324 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2325 - <div class="mxchat-chunk-group-info">
2326 - <button type="button" class="mxchat-chunk-toggle" data-group-id="<?php echo esc_attr($group_id); ?>">
2327 - <span class="dashicons dashicons-arrow-right-alt2"></span>
2328 - </button>
2329 - <span class="mxchat-chunk-badge"><?php echo esc_html($chunk_count); ?> <?php esc_html_e('chunks', 'mxchat'); ?></span>
2330 - <span class="mxchat-chunk-preview">
2331 - <?php
2332 - $parent_content = isset($first_prompt->display_content) ? $first_prompt->display_content : (isset($first_prompt->article_content) ? $first_prompt->article_content : '');
2333 - $content_preview = mb_substr($parent_content, 0, 100);
2334 - echo esc_html($content_preview . '...');
2335 - ?>
2336 - </span>
2337 - </div>
2338 - </td>
2339 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2340 - <?php if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0 && strpos($source_url, '_ungrouped_') !== 0) : ?>
2341 - <a href="<?php echo esc_url($source_url); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2342 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2343 - <?php esc_html_e('View Source', 'mxchat'); ?>
2344 - </a>
2345 - <?php else : ?>
2346 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
2347 - <?php endif; ?>
2348 - </td>
2349 - <td class="mxchat-actions-cell" style="padding: 12px 16px; white-space: nowrap;">
2350 - <?php if ($data_source !== 'pinecone') : ?>
2351 - <button type="button"
2352 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2353 - data-source-url="<?php echo esc_attr($source_url); ?>"
2354 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2355 - data-data-source="<?php echo esc_attr($data_source); ?>"
2356 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2357 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2358 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2359 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2360 - </button>
2361 - <?php endif; ?>
2362 - <button type="button"
2363 - class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
2364 - data-source-url="<?php echo esc_attr($source_url); ?>"
2365 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
2366 - data-data-source="<?php echo esc_attr($data_source); ?>"
2367 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2368 - data-nonce="<?php echo wp_create_nonce('mxchat_delete_chunks_nonce'); ?>"
2369 - style="color: var(--mxch-error);"
2370 - title="<?php esc_attr_e('Delete all chunks', 'mxchat'); ?>">
2371 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2372 - </button>
2373 - </td>
2374 - </tr>
2375 - <?php
2376 - // Render hidden chunk rows
2377 - foreach ($group as $chunk_index => $chunk) {
2378 - $meta_chunk_index = isset($chunk->chunk_metadata['chunk_index']) ? intval($chunk->chunk_metadata['chunk_index']) : $chunk_index;
2379 - $meta_total_chunks = isset($chunk->chunk_metadata['total_chunks']) ? intval($chunk->chunk_metadata['total_chunks']) : $chunk_count;
2380 - $content = isset($chunk->display_content) ? $chunk->display_content : (isset($chunk->article_content) ? $chunk->article_content : '');
2381 - $content_preview = mb_strlen($content) > $preview_length
2382 - ? mb_substr($content, 0, $preview_length) . '...'
2383 - : $content;
2384 - ?>
2385 - <tr id="prompt-<?php echo esc_attr($chunk->id); ?>"
2386 - class="mxchat-chunk-row <?php echo esc_attr($group_id); ?>"
2387 - data-source="<?php echo esc_attr($data_source); ?>"
2388 - style="display: none; background: #f8f9fa; border-bottom: 1px solid var(--mxch-card-border);">
2389 - <td style="padding: 12px 16px; text-align: center;">
2390 - <!-- Checkbox column placeholder for chunks (managed by group) -->
2391 - </td>
2392 - <td style="padding: 12px 16px 12px 30px; font-size: 13px;">
2393 - <!-- Hidden ID column for chunks -->
2394 - </td>
2395 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2396 - <div class="mxchat-accordion-wrapper">
2397 - <div class="mxchat-content-preview">
2398 - <span class="mxchat-chunk-indicator" style="margin-right: 10px; color: var(--mxch-text-secondary); font-size: 12px;">
2399 - <?php printf(esc_html__('Chunk %d of %d', 'mxchat'), $meta_chunk_index + 1, $meta_total_chunks); ?>
2400 - </span>
2401 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2402 - <?php if (mb_strlen($content) > $preview_length) : ?>
2403 - <button class="mxchat-expand-toggle" type="button">
2404 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2405 - </button>
2406 - <?php endif; ?>
2407 - </div>
2408 - <div class="mxchat-content-full" style="display: none;">
2409 - <div class="content-view">
2410 - <?php
2411 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2412 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2413 - echo wp_kses_post(wpautop($content));
2414 - echo '</div>';
2415 - } else {
2416 - echo wp_kses_post(wpautop($content));
2417 - }
2418 - ?>
2419 - </div>
2420 - </div>
2421 - </div>
2422 - </td>
2423 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2424 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Same as parent', 'mxchat'); ?></span>
2425 - </td>
2426 - <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2427 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Managed by group', 'mxchat'); ?></span>
2428 - </td>
2429 - </tr>
2430 - <?php
2431 - }
2432 - } else {
2433 - // Single entry - display normally with accordion
2434 - $prompt = $first_prompt;
2435 - $content = isset($prompt->display_content) ? $prompt->display_content : (isset($prompt->article_content) ? $prompt->article_content : '');
2436 - $content_preview = mb_strlen($content) > $preview_length
2437 - ? mb_substr($content, 0, $preview_length) . '...'
2438 - : $content;
2439 - ?>
2440 - <tr id="prompt-<?php echo esc_attr($prompt->id); ?>"
2441 - data-source="<?php echo esc_attr($data_source); ?>"
2442 - style="border-bottom: 1px solid var(--mxch-card-border); background: rgba(33, 150, 243, 0.02);">
2443 - <td style="padding: 12px 16px; text-align: center;">
2444 - <input type="checkbox"
2445 - class="mxchat-entry-checkbox"
2446 - data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2447 - data-source="<?php echo esc_attr($data_source); ?>"
2448 - data-source-url="<?php echo esc_attr($prompt->source_url ?? ''); ?>"
2449 - data-is-group="false"
2450 - data-chunk-count="1">
2451 - </td>
2452 - <td style="padding: 12px 16px; font-size: 13px;">
2453 - <?php echo esc_html($display_index + (($current_page - 1) * $per_page)); ?>
2454 - </td>
2455 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2456 - <div class="mxchat-accordion-wrapper">
2457 - <div class="mxchat-content-preview">
2458 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2459 - <?php if (mb_strlen($content) > $preview_length) : ?>
2460 - <button class="mxchat-expand-toggle" type="button">
2461 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2462 - </button>
2463 - <?php endif; ?>
2464 - </div>
2465 - <div class="mxchat-content-full" style="display: none;">
2466 - <div class="content-view">
2467 - <?php
2468 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2469 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2470 - echo wp_kses_post(wpautop($content));
2471 - echo '</div>';
2472 - } else {
2473 - echo wp_kses_post(wpautop($content));
2474 - }
2475 - ?>
2476 - </div>
2477 - </div>
2478 - </div>
2479 - </td>
2480 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2481 - <?php
2482 - $actual_source = $source_url;
2483 - if (strpos($source_url, '_ungrouped_') === 0) {
2484 - $actual_source = $prompt->source_url ?? '';
2485 - }
2486 - if (!empty($actual_source) && strpos($actual_source, 'mxchat://') !== 0) : ?>
2487 - <a href="<?php echo esc_url($actual_source); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2488 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2489 - <?php esc_html_e('View', 'mxchat'); ?>
2490 - </a>
2491 - <?php else : ?>
2492 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
2493 - <?php endif; ?>
2494 - </td>
2495 - <td style="padding: 12px 16px;">
2496 - <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);">
2497 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2498 - </button>
2499 - </td>
2500 - </tr>
2501 - <?php
2502 - }
2503 - }
2504 - }
2505 - $html = ob_get_clean();
2506 -
2507 - // Generate pagination HTML for Pinecone
2508 - $total_pages = ceil($total_records / $per_page);
2509 - $pagination_html = '';
2510 - if ($total_pages > 1) {
2511 - $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) . '">';
2512 -
2513 - // Previous button
2514 - if ($page > 1) {
2515 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page - 1) . '">' . esc_html__('&laquo; Previous', 'mxchat') . '</a> ';
2516 - }
2517 -
2518 - // Page numbers
2519 - $start_page = max(1, $page - 2);
2520 - $end_page = min($total_pages, $page + 2);
2521 -
2522 - if ($start_page > 1) {
2523 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="1">1</a> ';
2524 - if ($start_page > 2) {
2525 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2526 - }
2527 - }
2528 -
2529 - for ($i = $start_page; $i <= $end_page; $i++) {
2530 - if ($i == $page) {
2531 - $pagination_html .= '<span class="mxchat-page-current">' . $i . '</span> ';
2532 - } else {
2533 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $i . '">' . $i . '</a> ';
2534 - }
2535 - }
2536 -
2537 - if ($end_page < $total_pages) {
2538 - if ($end_page < $total_pages - 1) {
2539 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
2540 - }
2541 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $total_pages . '">' . $total_pages . '</a> ';
2542 - }
2543 -
2544 - // Next button
2545 - if ($page < $total_pages) {
2546 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page + 1) . '">' . esc_html__('Next &raquo;', 'mxchat') . '</a>';
2547 - }
2548 -
2549 - $pagination_html .= '</div>';
2550 - }
2551 -
2552 - wp_send_json_success(array(
2553 - 'html' => $html,
2554 - 'pagination_html' => $pagination_html,
2555 - 'total_count' => $total_records,
2556 - 'total_pages' => $total_pages,
2557 - 'page' => $page,
2558 - 'per_page' => $per_page,
2559 - 'data_source' => 'pinecone'
2560 - ));
2561 -}
2562 -
2563 -/**
2564 - * AJAX handler for pagination - handles both WordPress DB and Pinecone data sources
2565 - * Returns paginated entries without requiring a full page reload
2566 - */
2567 -public function ajax_mxchat_paginate_entries() {
2568 - check_ajax_referer('mxchat_entries_nonce', 'nonce');
2569 -
2570 - if (!current_user_can('manage_options')) {
2571 - wp_send_json_error(array('message' => 'Unauthorized'));
2572 - return;
2573 - }
2574 -
2575 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
2576 - $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
2577 - $search_query = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
2578 - $content_type_filter = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : '';
2579 - $per_page = 25;
2580 -
2581 - // Check if Pinecone is enabled for this bot
2582 - $pinecone_manager = $this->mxchat_get_pinecone_manager();
2583 - $pinecone_options = $pinecone_manager ? $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id) : array();
2584 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2585 - $has_pinecone_api = !empty($pinecone_options['mxchat_pinecone_api_key']);
2586 -
2587 - if ($use_pinecone && $has_pinecone_api) {
2588 - // Delegate to Pinecone pagination handler (pass search params)
2589 - $_POST['page'] = $page;
2590 - $_POST['search'] = $search_query;
2591 - $_POST['content_type'] = $content_type_filter;
2592 - $this->ajax_mxchat_refresh_pinecone_entries();
2593 - return;
2594 - }
2595 -
2596 - // WordPress DB pagination - MUST match initial page load logic exactly
2597 - global $wpdb;
2598 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2599 - $offset = ($page - 1) * $per_page;
2600 -
2601 - // Build WHERE clause for search and content type filtering
2602 - $where_clauses = array();
2603 - $where_values = array();
2604 -
2605 - if ($search_query) {
2606 - $where_clauses[] = "article_content LIKE %s";
2607 - $where_values[] = '%' . $wpdb->esc_like($search_query) . '%';
2608 - }
2609 -
2610 - if ($content_type_filter) {
2611 - switch ($content_type_filter) {
2612 - case 'manual':
2613 - $where_clauses[] = "(source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')";
2614 - break;
2615 - case 'pdf':
2616 - $where_clauses[] = "source_url LIKE '%.pdf'";
2617 - break;
2618 - case 'url':
2619 - $where_clauses[] = "source_url != '' AND source_url IS NOT NULL AND source_url NOT LIKE 'mxchat://%' AND source_url NOT LIKE '%.pdf'";
2620 - break;
2621 - }
2622 - }
2623 -
2624 - $where_sql = !empty($where_clauses) ? 'WHERE ' . implode(' AND ', $where_clauses) : '';
2625 -
2626 - // Count grouped entries with filters applied
2627 - if (!empty($where_values)) {
2628 - $count_args = array_merge($where_values, $where_values);
2629 - $count_query = $wpdb->prepare(
2630 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$where_sql} AND source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2631 - (SELECT COUNT(*) FROM {$table_name} {$where_sql} AND (source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%'))",
2632 - ...$count_args
2633 - );
2634 - $total_records = $wpdb->get_var($count_query);
2635 - } else if (!empty($where_sql)) {
2636 - // Content type filter only (no search), no prepared values needed
2637 - $total_records = $wpdb->get_var(
2638 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$where_sql} AND source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2639 - (SELECT COUNT(*) FROM {$table_name} {$where_sql} AND (source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%'))"
2640 - );
2641 - } else {
2642 - // No filters
2643 - $total_records = $wpdb->get_var(
2644 - "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '' AND source_url NOT LIKE 'mxchat://%') +
2645 - (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL OR source_url LIKE 'mxchat://%')"
2646 - );
2647 - }
2648 - $total_pages = ceil($total_records / $per_page);
2649 -
2650 - // Step 1: Get unique source_urls for this page (ordered by latest timestamp) with filters
2651 - if (!empty($where_values)) {
2652 - $query_args = array_merge($where_values, array($per_page, $offset));
2653 - $urls_query = $wpdb->prepare(
2654 - "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2655 - {$where_sql}
2656 - GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2657 - ...$query_args
2658 - );
2659 - } else if (!empty($where_sql)) {
2660 - $urls_query = $wpdb->prepare(
2661 - "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2662 - {$where_sql}
2663 - GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2664 - $per_page, $offset
2665 - );
2666 - } else {
2667 - $urls_query = $wpdb->prepare(
2668 - "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2669 - GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2670 - $per_page, $offset
2671 - );
2672 - }
2673 - $page_urls = $wpdb->get_results($urls_query);
2674 -
2675 - // Step 2: Build list of source_urls to fetch
2676 - $url_list = array();
2677 - $url_order_map = array();
2678 - $order_index = 0;
2679 - foreach ($page_urls as $url_row) {
2680 - $url = $url_row->source_url;
2681 - $url_list[] = $url;
2682 - $url_order_map[$url] = $order_index++;
2683 - }
2684 -
2685 - // Step 3: Fetch all rows for these source_urls (with search filter if applicable)
2686 - $prompts = array();
2687 - if (!empty($url_list)) {
2688 - $placeholders = implode(',', array_fill(0, count($url_list), '%s'));
2689 - if ($search_query) {
2690 - // Include search filter in the final fetch
2691 - $prompts_query = $wpdb->prepare(
2692 - "SELECT id, article_content, source_url, timestamp, role_restriction
2693 - FROM {$table_name}
2694 - WHERE source_url IN ($placeholders) AND article_content LIKE %s
2695 - ORDER BY timestamp DESC",
2696 - ...array_merge($url_list, ['%' . $wpdb->esc_like($search_query) . '%'])
2697 - );
2698 - } else {
2699 - $prompts_query = $wpdb->prepare(
2700 - "SELECT id, article_content, source_url, timestamp, role_restriction
2701 - FROM {$table_name}
2702 - WHERE source_url IN ($placeholders)
2703 - ORDER BY timestamp DESC",
2704 - $url_list
2705 - );
2706 - }
2707 - $prompts = $wpdb->get_results($prompts_query);
2708 - }
2709 -
2710 - // Group prompts by source_url for chunk display
2711 - $grouped_prompts = array();
2712 - foreach ($prompts as $prompt) {
2713 - $source_url = $prompt->source_url ?? '';
2714 -
2715 - // Parse chunk metadata using the proper chunker method (same as initial page load)
2716 - if (class_exists('MxChat_Chunker')) {
2717 - $chunk_meta = MxChat_Chunker::parse_stored_chunk($prompt->article_content);
2718 - $prompt->chunk_metadata = $chunk_meta['metadata'];
2719 - $prompt->display_content = $chunk_meta['text'];
2720 - } else {
2721 - $prompt->chunk_metadata = array();
2722 - $prompt->display_content = $prompt->article_content;
2723 - }
2724 -
2725 - if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0) {
2726 - if (!isset($grouped_prompts[$source_url])) {
2727 - $grouped_prompts[$source_url] = array();
2728 - }
2729 - $grouped_prompts[$source_url][] = $prompt;
2730 - } else {
2731 - // Ungrouped entries
2732 - $grouped_prompts['_ungrouped_' . $prompt->id] = array($prompt);
2733 - }
2734 - }
2735 -
2736 - // Sort groups by the original URL order (newest first)
2737 - uksort($grouped_prompts, function($a, $b) use ($url_order_map) {
2738 - $order_a = isset($url_order_map[$a]) ? $url_order_map[$a] : PHP_INT_MAX;
2739 - $order_b = isset($url_order_map[$b]) ? $url_order_map[$b] : PHP_INT_MAX;
2740 - return $order_a - $order_b;
2741 - });
2742 -
2743 - // Sort each group internally by chunk_index
2744 - foreach ($grouped_prompts as $source_url => &$group) {
2745 - usort($group, function($a, $b) {
2746 - $index_a = isset($a->chunk_metadata['chunk_index']) ? intval($a->chunk_metadata['chunk_index']) : 0;
2747 - $index_b = isset($b->chunk_metadata['chunk_index']) ? intval($b->chunk_metadata['chunk_index']) : 0;
2748 - return $index_a - $index_b;
2749 - });
2750 - }
2751 - unset($group);
2752 -
2753 - // Build HTML for the table rows
2754 - ob_start();
2755 - $display_index = 0;
2756 - $current_page = $page;
2757 - $data_source = 'wordpress';
2758 - $current_bot_id = $bot_id;
2759 - $preview_length = 150;
2760 -
2761 - if (empty($grouped_prompts)) {
2762 - echo '<tr><td colspan="5" style="padding: 40px; text-align: center; color: var(--mxch-text-muted);">';
2763 - esc_html_e('No knowledge entries found. Use the Import Options to add content.', 'mxchat');
2764 - echo '</td></tr>';
2765 - } else {
2766 - foreach ($grouped_prompts as $source_url => $group) {
2767 - $chunk_count = count($group);
2768 - $first_prompt = $group[0];
2769 - $display_index++;
2770 -
2771 - if ($chunk_count > 1) {
2772 - // Multiple chunks - show grouped row with expand button
2773 - $group_id = 'group-' . md5($source_url);
2774 - ?>
2775 - <tr id="prompt-<?php echo esc_attr($first_prompt->id); ?>"
2776 - class="mxchat-chunk-group-header"
2777 - data-source="<?php echo esc_attr($data_source); ?>"
2778 - data-group-id="<?php echo esc_attr($group_id); ?>"
2779 - style="border-bottom: 1px solid var(--mxch-card-border);">
2780 - <td style="padding: 12px 16px; text-align: center;">
2781 - <input type="checkbox"
2782 - class="mxchat-entry-checkbox"
2783 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2784 - data-source="<?php echo esc_attr($data_source); ?>"
2785 - data-source-url="<?php echo esc_attr($source_url); ?>"
2786 - data-is-group="true"
2787 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>">
2788 - </td>
2789 - <td style="padding: 12px 16px; font-size: 13px;">
2790 - <?php echo esc_html($first_prompt->id); ?>
2791 - </td>
2792 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2793 - <div class="mxchat-chunk-group-info">
2794 - <button type="button" class="mxchat-chunk-toggle" data-group-id="<?php echo esc_attr($group_id); ?>">
2795 - <span class="dashicons dashicons-arrow-right-alt2"></span>
2796 - </button>
2797 - <span class="mxchat-chunk-badge"><?php echo esc_html($chunk_count); ?> <?php esc_html_e('chunks', 'mxchat'); ?></span>
2798 - <span class="mxchat-chunk-preview">
2799 - <?php
2800 - $parent_content = isset($first_prompt->display_content) ? $first_prompt->display_content : $first_prompt->article_content;
2801 - $content_preview = mb_substr($parent_content, 0, 100);
2802 - echo esc_html($content_preview . '...');
2803 - ?>
2804 - </span>
2805 - </div>
2806 - </td>
2807 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2808 - <?php if (!empty($source_url) && strpos($source_url, 'mxchat://') !== 0 && strpos($source_url, '_ungrouped_') !== 0) : ?>
2809 - <a href="<?php echo esc_url($source_url); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2810 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2811 - <?php esc_html_e('View Source', 'mxchat'); ?>
2812 - </a>
2813 - <?php else : ?>
2814 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
2815 - <?php endif; ?>
2816 - </td>
2817 - <td class="mxchat-actions-cell" style="padding: 12px 16px; white-space: nowrap;">
2818 - <?php if ($data_source !== 'pinecone') : ?>
2819 - <button type="button"
2820 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2821 - data-source-url="<?php echo esc_attr($source_url); ?>"
2822 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2823 - data-data-source="<?php echo esc_attr($data_source); ?>"
2824 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2825 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2826 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2827 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2828 - </button>
2829 - <?php endif; ?>
2830 - <button type="button"
2831 - class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
2832 - data-source-url="<?php echo esc_attr($source_url); ?>"
2833 - data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
2834 - data-data-source="<?php echo esc_attr($data_source); ?>"
2835 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2836 - data-nonce="<?php echo wp_create_nonce('mxchat_delete_chunks_nonce'); ?>"
2837 - style="color: var(--mxch-error);"
2838 - title="<?php esc_attr_e('Delete all chunks', 'mxchat'); ?>">
2839 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2840 - </button>
2841 - </td>
2842 - </tr>
2843 - <?php
2844 - // Render hidden chunk rows
2845 - foreach ($group as $chunk_index => $chunk) {
2846 - $meta_chunk_index = isset($chunk->chunk_metadata['chunk_index']) ? intval($chunk->chunk_metadata['chunk_index']) : $chunk_index;
2847 - $meta_total_chunks = isset($chunk->chunk_metadata['total_chunks']) ? intval($chunk->chunk_metadata['total_chunks']) : $chunk_count;
2848 - $content = isset($chunk->display_content) ? $chunk->display_content : $chunk->article_content;
2849 - $content_preview = mb_strlen($content) > $preview_length
2850 - ? mb_substr($content, 0, $preview_length) . '...'
2851 - : $content;
2852 - ?>
2853 - <tr id="prompt-<?php echo esc_attr($chunk->id); ?>"
2854 - class="mxchat-chunk-row <?php echo esc_attr($group_id); ?>"
2855 - data-source="<?php echo esc_attr($data_source); ?>"
2856 - style="display: none; background: #f8f9fa; border-bottom: 1px solid var(--mxch-card-border);">
2857 - <td style="padding: 12px 16px; text-align: center;">
2858 - <!-- Checkbox column placeholder for chunks (managed by group) -->
2859 - </td>
2860 - <td style="padding: 12px 16px 12px 30px; font-size: 13px;">
2861 - <!-- Hidden ID column for chunks -->
2862 - </td>
2863 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2864 - <div class="mxchat-accordion-wrapper">
2865 - <div class="mxchat-content-preview">
2866 - <span class="mxchat-chunk-indicator" style="margin-right: 10px; color: var(--mxch-text-secondary); font-size: 12px;">
2867 - <?php printf(esc_html__('Chunk %d of %d', 'mxchat'), $meta_chunk_index + 1, $meta_total_chunks); ?>
2868 - </span>
2869 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2870 - <?php if (mb_strlen($content) > $preview_length) : ?>
2871 - <button class="mxchat-expand-toggle" type="button">
2872 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2873 - </button>
2874 - <?php endif; ?>
2875 - </div>
2876 - <div class="mxchat-content-full" style="display: none;">
2877 - <div class="content-view">
2878 - <?php
2879 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2880 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2881 - echo wp_kses_post(wpautop($content));
2882 - echo '</div>';
2883 - } else {
2884 - echo wp_kses_post(wpautop($content));
2885 - }
2886 - ?>
2887 - </div>
2888 - </div>
2889 - </div>
2890 - </td>
2891 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2892 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Same as parent', 'mxchat'); ?></span>
2893 - </td>
2894 - <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2895 - <span class="mxchat-chunk-label" style="color: var(--mxch-text-muted);"><?php esc_html_e('Managed by group', 'mxchat'); ?></span>
2896 - </td>
2897 - </tr>
2898 - <?php
2899 - }
2900 - } else {
2901 - // Single entry - display normally with accordion
2902 - $prompt = $first_prompt;
2903 - $content = isset($prompt->display_content) ? $prompt->display_content : $prompt->article_content;
2904 - $content_preview = mb_strlen($content) > $preview_length
2905 - ? mb_substr($content, 0, $preview_length) . '...'
2906 - : $content;
2907 - ?>
2908 - <tr id="prompt-<?php echo esc_attr($prompt->id); ?>"
2909 - data-source="<?php echo esc_attr($data_source); ?>"
2910 - style="border-bottom: 1px solid var(--mxch-card-border);">
2911 - <td style="padding: 12px 16px; text-align: center;">
2912 - <input type="checkbox"
2913 - class="mxchat-entry-checkbox"
2914 - data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2915 - data-source="<?php echo esc_attr($data_source); ?>"
2916 - data-source-url="<?php echo esc_attr($source_url); ?>"
2917 - data-is-group="false">
2918 - </td>
2919 - <td style="padding: 12px 16px; font-size: 13px;">
2920 - <?php echo esc_html($prompt->id); ?>
2921 - </td>
2922 - <td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">
2923 - <div class="mxchat-accordion-wrapper">
2924 - <div class="mxchat-content-preview">
2925 - <span class="preview-text"><?php echo esc_html($content_preview); ?></span>
2926 - <?php if (mb_strlen($content) > $preview_length) : ?>
2927 - <button class="mxchat-expand-toggle" type="button">
2928 - <span class="dashicons dashicons-arrow-down-alt2"></span>
2929 - </button>
2930 - <?php endif; ?>
2931 - </div>
2932 - <div class="mxchat-content-full" style="display: none;">
2933 - <div class="content-view">
2934 - <?php
2935 - if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
2936 - echo '<div dir="rtl" lang="he" class="rtl-content">';
2937 - echo wp_kses_post(wpautop($content));
2938 - echo '</div>';
2939 - } else {
2940 - echo wp_kses_post(wpautop($content));
2941 - }
2942 - ?>
2943 - </div>
2944 - </div>
2945 - </div>
2946 - </td>
2947 - <td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">
2948 - <?php
2949 - $actual_source = $source_url;
2950 - if (strpos($source_url, '_ungrouped_') === 0) {
2951 - $actual_source = $prompt->source_url ?? '';
2952 - }
2953 - if (!empty($actual_source) && strpos($actual_source, 'mxchat://') !== 0) : ?>
2954 - <a href="<?php echo esc_url($actual_source); ?>" target="_blank" style="color: var(--mxch-primary); text-decoration: none;">
2955 - <span class="dashicons dashicons-external" style="font-size: 14px;"></span>
2956 - <?php esc_html_e('View', 'mxchat'); ?>
2957 - </a>
2958 - <?php else : ?>
2959 - <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
2960 - <?php endif; ?>
2961 - </td>
2962 - <td style="padding: 12px 16px; white-space: nowrap;">
2963 - <button type="button"
2964 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2965 - data-source-url="<?php echo esc_attr($prompt->source_url ?? ''); ?>"
2966 - data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2967 - data-data-source="<?php echo esc_attr($data_source); ?>"
2968 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2969 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2970 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2971 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2972 - </button>
2973 - <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);">
2974 - <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2975 - </button>
2976 - </td>
2977 - </tr>
2978 - <?php
2979 - }
2980 - }
2981 - }
2982 - $html = ob_get_clean();
2983 -
2984 - // Generate pagination HTML (include search/filter data for subsequent pages)
2985 - $pagination_html = '';
2986 - if ($total_pages > 1) {
2987 - $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) . '">';
2988 -
2989 - // Previous button
2990 - if ($page > 1) {
2991 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page - 1) . '">' . esc_html__('&laquo; Previous', 'mxchat') . '</a> ';
2992 - }
2993 -
2994 - // Page numbers
2995 - $start_page = max(1, $page - 2);
2996 - $end_page = min($total_pages, $page + 2);
2997 -
2998 - if ($start_page > 1) {
2999 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="1">1</a> ';
3000 - if ($start_page > 2) {
3001 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
3002 - }
3003 - }
3004 -
3005 - for ($i = $start_page; $i <= $end_page; $i++) {
3006 - if ($i == $page) {
3007 - $pagination_html .= '<span class="mxchat-page-current">' . $i . '</span> ';
3008 - } else {
3009 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $i . '">' . $i . '</a> ';
3010 - }
3011 - }
3012 -
3013 - if ($end_page < $total_pages) {
3014 - if ($end_page < $total_pages - 1) {
3015 - $pagination_html .= '<span class="mxchat-page-dots">...</span> ';
3016 - }
3017 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . $total_pages . '">' . $total_pages . '</a> ';
3018 - }
3019 -
3020 - // Next button
3021 - if ($page < $total_pages) {
3022 - $pagination_html .= '<a href="#" class="mxchat-page-link" data-page="' . ($page + 1) . '">' . esc_html__('Next &raquo;', 'mxchat') . '</a>';
3023 - }
3024 -
3025 - $pagination_html .= '</div>';
3026 - }
3027 -
3028 - wp_send_json_success(array(
3029 - 'html' => $html,
3030 - 'pagination_html' => $pagination_html,
3031 - 'total_count' => $total_records,
3032 - 'total_pages' => $total_pages,
3033 - 'page' => $page,
3034 - 'per_page' => $per_page,
3035 - 'data_source' => 'wordpress'
3036 - ));
3037 -}
3038 -
3039 -/**
3040 - * AJAX handler to detect available sitemaps on the site
3041 - * Optimized for speed - only checks primary sitemap indexes first
3042 - */
3043 -public function ajax_mxchat_detect_sitemaps() {
3044 - check_ajax_referer('mxchat_detect_sitemaps_nonce', 'nonce');
3045 -
3046 - if (!current_user_can('manage_options')) {
3047 - wp_send_json_error(array('message' => 'Unauthorized'));
3048 - return;
3049 - }
3050 -
3051 - $site_url = get_site_url();
3052 - $sitemaps = array();
3053 - $found_index = false;
3054 -
3055 - // Only check the main sitemap index files first (much faster)
3056 - // These are the primary entry points that contain sub-sitemaps
3057 - $primary_indexes = array(
3058 - 'sitemap_index.xml' => 'Yoast SEO / Rank Math', // Yoast & Rank Math
3059 - 'wp-sitemap.xml' => 'WordPress Core', // WordPress Core
3060 - 'sitemap.xml' => 'Standard', // Generic/AIOSEO
3061 - );
3062 -
3063 - foreach ($primary_indexes as $path => $source) {
3064 - $url = trailingslashit($site_url) . $path;
3065 -
3066 - $response = wp_remote_head($url, array(
3067 - 'timeout' => 10,
3068 - 'sslverify' => false,
3069 - 'redirection' => 1,
3070 - 'user-agent' => mxchat_ingest_user_agent(),
3071 - ));
3072 -
3073 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
3074 - // Found a sitemap index - parse it to get sub-sitemaps
3075 - $sub_sitemaps = $this->parse_sitemap_index($url);
3076 - if (!empty($sub_sitemaps)) {
3077 - $sitemaps[] = array(
3078 - 'url' => $url,
3079 - 'type' => 'index',
3080 - 'source' => $source,
3081 - 'sub_sitemaps' => $sub_sitemaps
3082 - );
3083 - $found_index = true;
3084 - // Found a valid index, no need to check others
3085 - break;
3086 - }
3087 - }
3088 - }
3089 -
3090 - // If no sitemap index found, check for standalone sitemaps
3091 - if (!$found_index) {
3092 - $standalone_sitemaps = array(
3093 - 'post-sitemap.xml' => array('type' => 'content', 'source' => 'Yoast SEO'),
3094 - 'page-sitemap.xml' => array('type' => 'content', 'source' => 'Yoast SEO'),
3095 - );
3096 -
3097 - foreach ($standalone_sitemaps as $path => $info) {
3098 - $url = trailingslashit($site_url) . $path;
3099 -
3100 - $response = wp_remote_head($url, array(
3101 - 'timeout' => 2,
3102 - 'sslverify' => false
3103 - ));
3104 -
3105 - if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
3106 - $sitemaps[] = array(
3107 - 'url' => $url,
3108 - 'type' => $info['type'],
3109 - 'source' => $info['source'],
3110 - 'url_count' => 0 // Skip URL count for speed
3111 - );
3112 - }
3113 - }
3114 - }
3115 -
3116 - wp_send_json_success(array(
3117 - 'sitemaps' => $sitemaps,
3118 - 'site_url' => $site_url
3119 - ));
3120 -}
3121 -
3122 -/**
3123 - * Parse a sitemap index to get sub-sitemaps
3124 - * Optimized: doesn't fetch URL count for each sub-sitemap (too slow)
3125 - */
3126 -private function parse_sitemap_index($url) {
3127 - $sub_sitemaps = array();
3128 -
3129 - $response = wp_remote_get($url, array(
3130 - 'timeout' => 30,
3131 - 'sslverify' => false,
3132 - 'user-agent' => mxchat_ingest_user_agent(),
3133 - 'headers' => array(
3134 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
3135 - ),
3136 - ));
3137 -
3138 - if (is_wp_error($response)) {
3139 - return $sub_sitemaps;
3140 - }
3141 -
3142 - $body = wp_remote_retrieve_body($response);
3143 - if (empty($body)) {
3144 - return $sub_sitemaps;
3145 - }
3146 -
3147 - // Suppress XML errors
3148 - libxml_use_internal_errors(true);
3149 - $xml = simplexml_load_string($body);
3150 - libxml_clear_errors();
3151 -
3152 - if ($xml === false) {
3153 - return $sub_sitemaps;
3154 - }
3155 -
3156 - // Check if it's a sitemap index (contains <sitemap> elements)
3157 - if (isset($xml->sitemap)) {
3158 - foreach ($xml->sitemap as $sitemap) {
3159 - $loc = (string) $sitemap->loc;
3160 - if (!empty($loc)) {
3161 - // Try to determine the type from the URL
3162 - $type = 'content';
3163 - if (strpos($loc, 'category') !== false || strpos($loc, 'tag') !== false || strpos($loc, 'taxonomy') !== false) {
3164 - $type = 'taxonomy';
3165 - } elseif (strpos($loc, 'author') !== false || strpos($loc, 'user') !== false) {
3166 - $type = 'author';
3167 - }
3168 -
3169 - // Skip URL count - too slow to fetch for each sitemap
3170 - $sub_sitemaps[] = array(
3171 - 'url' => $loc,
3172 - 'type' => $type,
3173 - 'url_count' => 0, // Don't fetch - takes too long
3174 - 'name' => basename(parse_url($loc, PHP_URL_PATH))
3175 - );
3176 - }
3177 - }
3178 - }
3179 -
3180 - return $sub_sitemaps;
3181 -}
3182 -
3183 -/**
3184 - * Get URL count from a sitemap
3185 - */
3186 -private function get_sitemap_url_count($url) {
3187 - $response = wp_remote_get($url, array(
3188 - 'timeout' => 30,
3189 - 'sslverify' => false,
3190 - 'user-agent' => mxchat_ingest_user_agent(),
3191 - 'headers' => array(
3192 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
3193 - ),
3194 - ));
3195 -
3196 - if (is_wp_error($response)) {
3197 - return 0;
3198 - }
3199 -
3200 - $body = wp_remote_retrieve_body($response);
3201 - if (empty($body)) {
3202 - return 0;
3203 - }
3204 -
3205 - // Count <url> or <loc> elements
3206 - $count = preg_match_all('/<url>/i', $body, $matches);
3207 - return $count ?: 0;
3208 -}
3209 -
3210 -/**
3211 - * Get sitemaps declared in robots.txt
3212 - */
3213 -private function get_sitemaps_from_robots($site_url) {
3214 - $sitemaps = array();
3215 - $robots_url = trailingslashit($site_url) . 'robots.txt';
3216 -
3217 - $response = wp_remote_get($robots_url, array(
3218 - 'timeout' => 15,
3219 - 'sslverify' => false,
3220 - 'user-agent' => mxchat_ingest_user_agent(),
3221 - ));
3222 -
3223 - if (is_wp_error($response)) {
3224 - return $sitemaps;
3225 - }
3226 -
3227 - $body = wp_remote_retrieve_body($response);
3228 - if (empty($body)) {
3229 - return $sitemaps;
3230 - }
3231 -
3232 - // Find Sitemap: declarations
3233 - if (preg_match_all('/^Sitemap:\s*(.+)$/mi', $body, $matches)) {
3234 - foreach ($matches[1] as $sitemap_url) {
3235 - $sitemap_url = trim($sitemap_url);
3236 - if (filter_var($sitemap_url, FILTER_VALIDATE_URL)) {
3237 - $sitemaps[] = $sitemap_url;
3238 - }
3239 - }
3240 - }
3241 -
3242 - return $sitemaps;
3243 -}
3244 -
3245 1767 public function mxchat_stop_processing() {
3246 1768 // Verify permissions
3247 1769 if (!current_user_can('manage_options')) {
3248 1770 wp_die(esc_html__('Unauthorized access', 'mxchat'));
@@ -3250,52 +1772,28 @@
3250 1772
3251 1773 // Verify nonce
3252 1774 check_admin_referer('mxchat_stop_processing_action', 'mxchat_stop_processing_nonce');
3253 1775
3254 - global $wpdb;
3255 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
3256 -
3257 - // Get active queue IDs
3258 - $sitemap_queue_id = get_transient('mxchat_active_queue_sitemap');
3259 - $pdf_queue_id = get_transient('mxchat_active_queue_pdf');
3260 -
3261 - // Delete all pending items from active queues
3262 - if ($sitemap_queue_id) {
3263 - $wpdb->delete(
3264 - $table_name,
3265 - array(
3266 - 'queue_id' => $sitemap_queue_id,
3267 - 'status' => 'pending'
3268 - ),
3269 - array('%s', '%s')
3270 - );
3271 -
3272 - 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));
3273 1780 delete_transient('mxchat_last_sitemap_url');
3274 1781 }
3275 -
3276 - if ($pdf_queue_id) {
3277 - // Get PDF path before deleting
3278 - $pdf_path = $this->mxchat_get_queue_meta($pdf_queue_id, 'pdf_path');
3279 -
3280 - $wpdb->delete(
3281 - $table_name,
3282 - array(
3283 - 'queue_id' => $pdf_queue_id,
3284 - 'status' => 'pending'
3285 - ),
3286 - array('%s', '%s')
3287 - );
3288 -
3289 - // Delete PDF file
3290 - if ($pdf_path && file_exists($pdf_path)) {
3291 - wp_delete_file($pdf_path);
3292 - }
3293 -
3294 - 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));
3295 1787 delete_transient('mxchat_last_pdf_url');
3296 1788 }
3297 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 +
3298 1796 // Redirect back with a success message
3299 1797 set_transient('mxchat_admin_notice_success',
3300 1798 esc_html__('Processing has been stopped successfully.', 'mxchat'),
3301 1799 30
@@ -3302,12 +1800,8 @@
3302 1800 );
3303 1801 wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
3304 1802 exit;
3305 1803 }
3306 -
3307 -/**
3308 - * Get content list for processing
3309 - */
3310 1804 public function ajax_mxchat_get_content_list() {
3311 1805 // Verify the nonce
3312 1806 check_ajax_referer('mxchat_content_selector_nonce', 'nonce');
3313 1807
@@ -3315,9 +1809,9 @@
3315 1809 wp_send_json_error(__('Unauthorized access', 'mxchat'));
3316 1810 }
3317 1811
3318 1812 $page = isset($_GET['page']) ? absint($_GET['page']) : 1;
3319 - $per_page = isset($_GET['per_page']) ? absint($_GET['per_page']) : 100;
1813 + $per_page = isset($_GET['per_page']) ? absint($_GET['per_page']) : 20;
3320 1814 $search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
3321 1815 $post_type = isset($_GET['post_type']) ? sanitize_text_field($_GET['post_type']) : 'all';
3322 1816 $post_status = isset($_GET['post_status']) ? sanitize_text_field($_GET['post_status']) : 'publish';
3323 1817 $processed_filter = isset($_GET['processed_filter']) ? sanitize_text_field($_GET['processed_filter']) : 'all';
@@ -3330,47 +1824,20 @@
3330 1824 'orderby' => 'date',
3331 1825 'order' => 'DESC',
3332 1826 );
3333 1827
3334 - // Handle post types - IMPROVED VERSION
1828 + // Handle post types
3335 1829 if ($post_type !== 'all') {
3336 1830 $args['post_type'] = $post_type;
3337 1831 } else {
3338 - // Get all available post types that might contain content
3339 - $all_post_types = array();
1832 + // Default to post and page if we can't get post types
1833 + $args['post_type'] = array('post', 'page');
3340 1834
3341 - // First get all public post types
3342 - $public_types = get_post_types(array('public' => true), 'names');
3343 - $all_post_types = array_merge($all_post_types, $public_types);
3344 -
3345 - // Add common forum/community post types
3346 - $forum_types = array('topic', 'reply', 'forum', 'wpforo_topic', 'wpforo_post');
3347 - foreach ($forum_types as $forum_type) {
3348 - if (post_type_exists($forum_type)) {
3349 - $all_post_types[] = $forum_type;
3350 - }
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);
3351 1839 }
3352 -
3353 - // Add other commonly used post types
3354 - $common_types = array('product', 'job_listing', 'event', 'portfolio');
3355 - foreach ($common_types as $common_type) {
3356 - if (post_type_exists($common_type)) {
3357 - $all_post_types[] = $common_type;
3358 - }
3359 - }
3360 -
3361 - // Remove duplicates and ensure we have at least some post types
3362 - $all_post_types = array_unique($all_post_types);
3363 -
3364 - if (empty($all_post_types)) {
3365 - // Fallback to basic post types
3366 - $all_post_types = array('post', 'page');
3367 - }
3368 -
3369 - $args['post_type'] = $all_post_types;
3370 -
3371 - // Debug logging to see what post types are being queried
3372 - //error_log('MxChat Debug: Querying post types: ' . implode(', ', $all_post_types));
3373 1840 }
3374 1841
3375 1842 if (!empty($search)) {
3376 1843 $args['s'] = $search;
@@ -3375,58 +1842,36 @@
3375 1842 if (!empty($search)) {
3376 1843 $args['s'] = $search;
3377 1844 }
3378 1845
3379 - // Get processed data from storage
1846 + // ================================
1847 + // FIXED: Check only the ACTIVE storage method
1848 + // ================================
1849 +
3380 1850 $processed_data = array();
3381 1851
1852 + $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
3382 1853 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3383 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3384 -
1854 + $pinecone_manager->mxchat_refresh_after_new_content($pinecone_options);
1855 +
3385 1856 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3386 - // Get fresh data from Pinecone - no caching
1857 + // ONLY check Pinecone if it's enabled
3387 1858 $processed_data = $this->mxchat_get_pinecone_processed_content($pinecone_options);
3388 1859 } else {
3389 - // WordPress DB checking with better URL matching for all post types
1860 + // ONLY check WordPress DB if Pinecone is not enabled
3390 1861 global $wpdb;
3391 1862 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3392 - $processed_items = $wpdb->get_results("SELECT id, source_url, article_content, timestamp FROM {$table_name}");
3393 -
3394 - // Group items by source_url to count chunks
3395 - $url_chunk_counts = array();
3396 - $url_latest_timestamp = array();
3397 - $url_first_id = array();
3398 -
1863 + $processed_items = $wpdb->get_results("SELECT id, source_url, timestamp FROM {$table_name}");
1864 +
3399 1865 if (!empty($processed_items)) {
3400 1866 foreach ($processed_items as $item) {
3401 - $url = $item->source_url;
3402 - if (empty($url)) continue;
3403 -
3404 - // Count chunks per URL
3405 - if (!isset($url_chunk_counts[$url])) {
3406 - $url_chunk_counts[$url] = 0;
3407 - $url_latest_timestamp[$url] = $item->timestamp;
3408 - $url_first_id[$url] = $item->id;
3409 - }
3410 - $url_chunk_counts[$url]++;
3411 -
3412 - // Track latest timestamp
3413 - if (strtotime($item->timestamp) > strtotime($url_latest_timestamp[$url])) {
3414 - $url_latest_timestamp[$url] = $item->timestamp;
3415 - }
3416 - }
3417 -
3418 - // Now build processed_data with chunk counts
3419 - foreach ($url_chunk_counts as $url => $chunk_count) {
3420 - $post_id = $this->mxchat_url_to_post_id_improved($url);
3421 -
1867 + $post_id = url_to_postid($item->source_url);
3422 1868 if ($post_id) {
3423 1869 $processed_data[$post_id] = array(
3424 - 'db_id' => $url_first_id[$url],
3425 - 'timestamp' => $url_latest_timestamp[$url],
3426 - 'url' => $url,
3427 - 'source' => 'wordpress',
3428 - 'chunk_count' => $chunk_count
1870 + 'db_id' => $item->id,
1871 + 'timestamp' => $item->timestamp,
1872 + 'url' => $item->source_url,
1873 + 'source' => 'wordpress'
3429 1874 );
3430 1875 }
3431 1876 }
3432 1877 }
@@ -3431,8 +1876,10 @@
3431 1876 }
3432 1877 }
3433 1878 }
3434 1879
1880 + // ================================
1881 +
3435 1882 // Get processed IDs as a simple array for in_array checks
3436 1883 $processed_ids = array_keys($processed_data);
3437 1884
3438 1885 // Handle processed/unprocessed filter
@@ -3474,14 +1921,8 @@
3474 1921 $db_record_id = $item_data['db_id'];
3475 1922 }
3476 1923 }
3477 1924
3478 - // Get chunk count for this item
3479 - $chunk_count = 0;
3480 - if ($is_processed && isset($processed_data[$id]['chunk_count'])) {
3481 - $chunk_count = intval($processed_data[$id]['chunk_count']);
3482 - }
3483 -
3484 1925 $content_items[] = array(
3485 1926 'id' => $id,
3486 1927 'title' => get_the_title(),
3487 1928 'permalink' => get_permalink(),
@@ -3492,10 +1933,9 @@
3492 1933 'word_count' => $word_count,
3493 1934 'already_processed' => $is_processed,
3494 1935 'processed_date' => $processed_date,
3495 1936 'db_record_id' => $db_record_id,
3496 - 'data_source' => $data_source,
3497 - 'chunk_count' => $chunk_count
1937 + 'data_source' => $data_source
3498 1938 );
3499 1939 }
3500 1940 wp_reset_postdata();
3501 1941 }
@@ -3511,178 +1951,8 @@
3511 1951 wp_send_json_success($response);
3512 1952 exit;
3513 1953 }
3514 1954
3515 -
3516 -/**
3517 - * This function handles various WooCommerce URL formats and permalink structures
3518 - */
3519 -private function mxchat_url_to_post_id_improved($url) {
3520 - // First try the standard WordPress function
3521 - $post_id = url_to_postid($url);
3522 -
3523 - if ($post_id > 0) {
3524 - return $post_id;
3525 - }
3526 -
3527 - // If that fails, try more aggressive URL matching
3528 - // Remove trailing slashes and query parameters for better matching
3529 - $clean_url = rtrim($url, '/');
3530 - $clean_url = strtok($clean_url, '?'); // Remove query parameters
3531 -
3532 - // Try again with cleaned URL
3533 - $post_id = url_to_postid($clean_url);
3534 - if ($post_id > 0) {
3535 - return $post_id;
3536 - }
3537 -
3538 - // For bbPress forum topics, try extracting slug from URL
3539 - if (strpos($url, '/topic/') !== false || strpos($url, '/forums/') !== false) {
3540 - // Handle bbPress URLs: /forums/topic/topic-name/
3541 - if (preg_match('/\/forums\/topic\/([^\/\?]+)/', $url, $matches)) {
3542 - $topic_slug = $matches[1];
3543 -
3544 - // Look up topic by slug
3545 - $topic = get_page_by_path($topic_slug, OBJECT, 'topic');
3546 - if ($topic) {
3547 - return $topic->ID;
3548 - }
3549 -
3550 - // Alternative method: query by post_name
3551 - global $wpdb;
3552 - $post_id = $wpdb->get_var($wpdb->prepare(
3553 - "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
3554 - $topic_slug
3555 - ));
3556 -
3557 - if ($post_id) {
3558 - return intval($post_id);
3559 - }
3560 - }
3561 -
3562 - // Handle simpler topic URLs: /topic/topic-name/
3563 - if (preg_match('/\/topic\/([^\/\?]+)/', $url, $matches)) {
3564 - $topic_slug = $matches[1];
3565 -
3566 - global $wpdb;
3567 - $post_id = $wpdb->get_var($wpdb->prepare(
3568 - "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'topic' AND post_status IN ('publish', 'closed')",
3569 - $topic_slug
3570 - ));
3571 -
3572 - if ($post_id) {
3573 - return intval($post_id);
3574 - }
3575 - }
3576 - }
3577 -
3578 - // For WooCommerce products
3579 - if (strpos($url, '/product/') !== false || strpos($url, 'product=') !== false) {
3580 - // Extract product slug from various URL formats
3581 - $product_slug = '';
3582 -
3583 - // Handle pretty permalinks: /product/product-name/
3584 - if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
3585 - $product_slug = $matches[1];
3586 - }
3587 - // Handle query parameters: ?product=product-name
3588 - elseif (preg_match('/[\?&]product=([^&]+)/', $url, $matches)) {
3589 - $product_slug = $matches[1];
3590 - }
3591 -
3592 - if (!empty($product_slug)) {
3593 - // Look up product by slug
3594 - $product = get_page_by_path($product_slug, OBJECT, 'product');
3595 - if ($product) {
3596 - return $product->ID;
3597 - }
3598 -
3599 - // Alternative method: query by post_name
3600 - global $wpdb;
3601 - $post_id = $wpdb->get_var($wpdb->prepare(
3602 - "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'product' AND post_status = 'publish'",
3603 - $product_slug
3604 - ));
3605 -
3606 - if ($post_id) {
3607 - return intval($post_id);
3608 - }
3609 - }
3610 - }
3611 -
3612 - // Generic approach: try to extract slug and match against all post types
3613 - $parsed_url = wp_parse_url($clean_url);
3614 - $path = $parsed_url['path'] ?? '';
3615 -
3616 - if (!empty($path)) {
3617 - // Get the last part of the path as potential slug
3618 - $path_parts = array_filter(explode('/', trim($path, '/')));
3619 - $potential_slug = end($path_parts);
3620 -
3621 - if (!empty($potential_slug)) {
3622 - global $wpdb;
3623 -
3624 - // Try to find any post with this slug
3625 - $post_id = $wpdb->get_var($wpdb->prepare(
3626 - "SELECT ID FROM {$wpdb->posts}
3627 - WHERE post_name = %s
3628 - AND post_status IN ('publish', 'closed', 'private')
3629 - AND post_type NOT IN ('revision', 'attachment', 'nav_menu_item')
3630 - ORDER BY CASE
3631 - WHEN post_type = 'post' THEN 1
3632 - WHEN post_type = 'page' THEN 2
3633 - WHEN post_type = 'topic' THEN 3
3634 - WHEN post_type = 'product' THEN 4
3635 - ELSE 5
3636 - END
3637 - LIMIT 1",
3638 - $potential_slug
3639 - ));
3640 -
3641 - if ($post_id) {
3642 - return intval($post_id);
3643 - }
3644 - }
3645 - }
3646 -
3647 - // ADDITIONAL: Try direct database lookup by URL variations
3648 - global $wpdb;
3649 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3650 -
3651 - // Try variations of the URL (with/without trailing slash, http/https)
3652 - $url_variations = array(
3653 - $url,
3654 - rtrim($url, '/'),
3655 - $url . '/',
3656 - str_replace('http://', 'https://', $url),
3657 - str_replace('https://', 'http://', $url),
3658 - str_replace('http://', 'https://', rtrim($url, '/')),
3659 - str_replace('https://', 'http://', rtrim($url, '/'))
3660 - );
3661 -
3662 - // Remove duplicates
3663 - $url_variations = array_unique($url_variations);
3664 -
3665 - foreach ($url_variations as $variation) {
3666 - $existing_record = $wpdb->get_row($wpdb->prepare(
3667 - "SELECT id, source_url FROM $table_name WHERE source_url = %s",
3668 - $variation
3669 - ));
3670 -
3671 - if ($existing_record) {
3672 - // Try to get post ID from this stored URL
3673 - $stored_post_id = url_to_postid($existing_record->source_url);
3674 - if ($stored_post_id > 0) {
3675 - return $stored_post_id;
3676 - }
3677 - }
3678 - }
3679 -
3680 - return 0; // No match found
3681 -}
3682 -/**
3683 - * Process selected content via AJAX
3684 - */
3685 1955 public function ajax_mxchat_process_selected_content() {
3686 1956 // Basic request validation
3687 1957 if (!check_ajax_referer('mxchat_content_selector_nonce', 'nonce', false)) {
3688 1958 wp_send_json_error('Invalid nonce');
@@ -3706,24 +1976,8 @@
3706 1976 wp_send_json_error('No content selected');
3707 1977 exit;
3708 1978 }
3709 1979
3710 - // Get bot_id from request
3711 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
3712 -
3713 - // ACF→PDF extraction is opt-in per import batch. Persist the last-used value so users
3714 - // don't re-check on every batch; the default is OFF for installs that haven't set it.
3715 - $extract_acf_pdfs = !empty($_POST['extract_acf_pdfs']) && $_POST['extract_acf_pdfs'] !== 'false';
3716 - $mxchat_options = get_option('mxchat_options', array());
3717 - if (!is_array($mxchat_options)) {
3718 - $mxchat_options = array();
3719 - }
3720 - $prior_default = !empty($mxchat_options['acf_pdf_extract_default']);
3721 - if ($prior_default !== $extract_acf_pdfs) {
3722 - $mxchat_options['acf_pdf_extract_default'] = $extract_acf_pdfs ? 1 : 0;
3723 - update_option('mxchat_options', $mxchat_options);
3724 - }
3725 -
3726 1980 // Process only ONE post at a time to avoid request size issues
3727 1981 $post_id = reset($post_ids);
3728 1982 $post = get_post($post_id);
3729 1983
@@ -3730,185 +1984,15 @@
3730 1984 if (!$post) {
3731 1985 wp_send_json_error('Post not found');
3732 1986 exit;
3733 1987 }
3734 -
3735 - // Allow developers to modify post data before processing into knowledge base
3736 - $post = apply_filters('mxchat_before_process_post', $post, $bot_id);
3737 -
3738 - // Get content including title, short description (for WooCommerce), and main content
3739 - $content = $post->post_title . "\n\n";
3740 -
3741 - // Add short description if it exists (WooCommerce products use post_excerpt for short description)
3742 - if (!empty($post->post_excerpt)) {
3743 - // Remove shortcode tags but preserve content inside them
3744 - $clean_excerpt = $this->strip_shortcode_tags_preserve_content($post->post_excerpt);
3745 - $content .= "Short Description: " . wp_strip_all_tags($clean_excerpt) . "\n\n";
3746 - }
3747 -
3748 - // Add main content - remove shortcode tags but preserve content inside them
3749 - $clean_content = $this->strip_shortcode_tags_preserve_content($post->post_content);
3750 - $content .= wp_strip_all_tags($clean_content);
3751 -
3752 - // ADD WOOCOMMERCE PRODUCT DATA (pricing, stock, categories, custom tabs)
3753 - if (get_post_type($post_id) === 'product' && class_exists('WooCommerce')) {
3754 - $product = wc_get_product($post_id);
3755 -
3756 - if ($product) {
3757 - // Get pricing information
3758 - $regular_price = $product->get_regular_price();
3759 - $sale_price = $product->get_sale_price();
3760 - $price = $product->get_price();
3761 - $sku = $product->get_sku();
3762 -
3763 - // Get currency symbol
3764 - $currency_symbol = get_woocommerce_currency_symbol();
3765 -
3766 - // Add pricing information
3767 - $content .= "\n";
3768 - if (!empty($regular_price)) {
3769 - $content .= "Price: " . $currency_symbol . $regular_price . "\n";
3770 - } elseif (!empty($price)) {
3771 - $content .= "Price: " . $currency_symbol . $price . "\n";
3772 - }
3773 -
3774 - if (!empty($sale_price) && $sale_price !== $regular_price) {
3775 - $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
3776 - }
3777 -
3778 - // Handle variable products - show price range
3779 - if ($product->is_type('variable')) {
3780 - $min_price = $product->get_variation_price('min');
3781 - $max_price = $product->get_variation_price('max');
3782 - if ($min_price !== $max_price) {
3783 - $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
3784 - }
3785 - }
3786 -
3787 - if (!empty($sku)) {
3788 - $content .= "SKU: " . $sku . "\n";
3789 - }
3790 -
3791 - // Get product categories
3792 - $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
3793 - if (!empty($categories) && !is_wp_error($categories)) {
3794 - $content .= "Categories: " . implode(', ', $categories) . "\n";
3795 - }
3796 - }
3797 -
3798 - // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
3799 - $custom_tabs = get_post_meta($post_id, 'yikes_woo_products_tabs', true);
3800 - if (!empty($custom_tabs) && is_array($custom_tabs)) {
3801 - foreach ($custom_tabs as $tab) {
3802 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
3803 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
3804 -
3805 - if (!empty($tab_title) && !empty($tab_content)) {
3806 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
3807 - }
3808 - }
3809 - }
3810 -
3811 - // Also check for reusable/saved tabs applied to this product
3812 - $applied_saved_tabs = get_post_meta($post_id, 'yikes_woo_reusable_products_tabs_applied', true);
3813 - if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
3814 - $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
3815 - if (!empty($saved_tabs) && is_array($saved_tabs)) {
3816 - foreach ($applied_saved_tabs as $saved_tab_id) {
3817 - if (isset($saved_tabs[$saved_tab_id])) {
3818 - $tab = $saved_tabs[$saved_tab_id];
3819 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
3820 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
3821 -
3822 - if (!empty($tab_title) && !empty($tab_content)) {
3823 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
3824 - }
3825 - }
3826 - }
3827 - }
3828 - }
3829 - }
3830 -
3831 - // ADD ACF FIELDS SUPPORT
3832 - $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
3833 - $pdf_extracted_count = 0;
3834 - if (!empty($acf_fields)) {
3835 - $acf_content_parts = array();
3836 - $pdf_attachment_ids = array();
3837 -
3838 - foreach ($acf_fields as $field_name => $field_value) {
3839 - $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
3840 -
3841 - if (!empty($formatted_value)) {
3842 - $field_label = ucwords(str_replace('_', ' ', $field_name));
3843 - $acf_content_parts[] = $field_label . ": " . $formatted_value;
3844 - }
3845 -
3846 - // Walk this field's value tree for any PDF attachment references and queue them for extraction.
3847 - // Only when the user opted into ACF→PDF extraction for this batch; otherwise the ACF text
3848 - // still lands in the KB but the heavier PDF parsing is skipped.
3849 - if ($extract_acf_pdfs) {
3850 - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($field_value, $pdf_attachment_ids);
3851 - }
3852 - }
3853 -
3854 - if (!empty($acf_content_parts)) {
3855 - $content .= "\n\n" . implode("\n", $acf_content_parts);
3856 - }
3857 -
3858 - // Extract text from each unique PDF found in ACF fields and append as a labeled section
3859 - if ($extract_acf_pdfs && !empty($pdf_attachment_ids)) {
3860 - $pdf_attachment_ids = array_unique(array_filter(array_map('intval', $pdf_attachment_ids)));
3861 - $pdf_sections = array();
3862 - foreach ($pdf_attachment_ids as $att_id) {
3863 - $pdf_text = $this->mxchat_extract_pdf_text_by_attachment_id($att_id);
3864 - if (!empty($pdf_text)) {
3865 - $pdf_title = get_the_title($att_id);
3866 - $pdf_url = wp_get_attachment_url($att_id);
3867 - $header = 'PDF Attachment';
3868 - if (!empty($pdf_title)) {
3869 - $header .= ': ' . $pdf_title;
3870 - }
3871 - if (!empty($pdf_url)) {
3872 - $header .= ' (' . $pdf_url . ')';
3873 - }
3874 - $pdf_sections[] = $header . "\n" . $pdf_text;
3875 - $pdf_extracted_count++;
3876 - }
3877 - }
3878 - if (!empty($pdf_sections)) {
3879 - $content .= "\n\nAttached PDFs:\n" . implode("\n\n", $pdf_sections);
3880 - }
3881 - }
3882 - }
3883 -
3884 - // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
3885 - $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
3886 - if (!empty($custom_meta)) {
3887 - $meta_content_parts = array();
3888 -
3889 - foreach ($custom_meta as $meta_key => $meta_value) {
3890 - // Convert meta key to readable label
3891 - $meta_label = ucwords(str_replace(array('_', '-'), ' ', $meta_key));
3892 - $meta_content_parts[] = $meta_label . ": " . $meta_value;
3893 - }
3894 -
3895 - if (!empty($meta_content_parts)) {
3896 - $content .= "\n\n" . implode("\n", $meta_content_parts);
3897 - }
3898 - }
3899 -
3900 - // Debug logging for WordPress Import content
3901 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Post ID: ' . $post_id . ' Title: ' . $post->post_title);
3902 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Raw post_content length: ' . strlen($post->post_content));
3903 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Final content length: ' . strlen($content));
3904 - //error_log('[MXCHAT-WP-IMPORT-DEBUG] Content preview: ' . substr($content, 0, 300));
3905 -
3906 - // Note: Removed 10,000 char limit - chunking now handles large content properly
3907 -
3908 - // Get bot-specific API key
3909 - $bot_options = $this->get_bot_options($bot_id);
3910 - $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');
3911 1995 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3912 1996
3913 1997 if (strpos($selected_model, 'voyage') === 0) {
3914 1998 $api_key = $options['voyage_api_key'] ?? '';
@@ -3921,9 +2005,8 @@
3921 2005 $provider_name = 'OpenAI';
3922 2006 }
3923 2007
3924 2008 if (empty($api_key)) {
3925 - MxChat_Admin::mxchat_log_debug('api_error', $provider_name . ' API key not configured for knowledge processing');
3926 2009 wp_send_json_error($provider_name . ' API key not configured');
3927 2010 exit;
3928 2011 }
3929 2012
@@ -3929,23 +2012,26 @@
3929 2012
3930 2013 $source_url = get_permalink($post_id);
3931 2014 $vector_id = md5($source_url); // Vector ID for Pinecone
3932 2015
3933 - // Check for existing content in bot-specific storage
2016 + // ================================
2017 + // FIXED: Check for existing content in ONLY the active storage method
2018 + // ================================
2019 +
3934 2020 $is_update = false;
3935 2021
3936 - // Get bot-specific Pinecone configuration
3937 - $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
3938 - $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';
3939 2025
3940 - if ($use_pinecone && !empty($bot_pinecone_config['api_key'])) {
3941 - // Check Pinecone for this bot
3942 - $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);
3943 2029 if (isset($pinecone_data[$post_id])) {
3944 2030 $is_update = true;
3945 2031 }
3946 2032 } else {
3947 - // Check WordPress DB (same as before since it's shared)
2033 + // ONLY check WordPress DB if Pinecone is not enabled
3948 2034 global $wpdb;
3949 2035 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3950 2036 $existing_record = $wpdb->get_row($wpdb->prepare(
3951 2037 "SELECT id FROM $table_name WHERE source_url = %s",
@@ -3955,54 +2041,54 @@
3955 2041 if ($existing_record) {
3956 2042 $is_update = true;
3957 2043 }
3958 2044 }
3959 -
3960 - // UPDATED 2.5.6: Determine content type based on post_type
3961 - $post_type = $post->post_type;
3962 - $content_type = 'content'; // Default fallback
3963 -
3964 - // Map WordPress post types to content types
3965 - switch ($post_type) {
3966 - case 'post':
3967 - $content_type = 'post';
3968 - break;
3969 - case 'page':
3970 - $content_type = 'page';
3971 - break;
3972 - case 'product':
3973 - $content_type = 'product';
3974 - break;
3975 - default:
3976 - // For custom post types, use the post type name
3977 - $content_type = sanitize_key($post_type);
3978 - break;
3979 - }
3980 -
3981 - // Use the centralized utility function with bot_id and content_type
2045 +
2046 + // Use the centralized utility function for storage
3982 2047 $result = MxChat_Utils::submit_content_to_db(
3983 - $content,
3984 - $source_url,
2048 + $content,
2049 + $source_url,
3985 2050 $api_key,
3986 - $vector_id,
3987 - $bot_id,
3988 - $content_type
2051 + $vector_id
3989 2052 );
3990 -
2053 +
3991 2054 if (is_wp_error($result)) {
3992 - MxChat_Admin::mxchat_log_debug('storage_error', 'Knowledge storage failed: ' . $result->get_error_message(), array('source_url' => $source_url));
3993 2055 wp_send_json_error('Storage failed: ' . $result->get_error_message());
3994 2056 exit;
3995 2057 }
3996 -
3997 - // Automatically apply role restriction based on tags
3998 - $this->apply_role_restriction_to_post($post_id, $source_url);
3999 -
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 +
4000 2089 $operation_type = $is_update ? 'update' : 'new';
4001 2090
4002 - // Count ACF fields for debugging
4003 - $acf_field_count = count($acf_fields);
4004 -
4005 2091 // Success response with minimal data
4006 2092 wp_send_json_success(array(
4007 2093 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully',
4008 2094 'post_id' => $post_id,
@@ -4007,95 +2093,27 @@
4007 2093 'message' => $operation_type === 'update' ? 'Content updated successfully' : 'Content processed successfully',
4008 2094 'post_id' => $post_id,
4009 2095 'title' => $post->post_title,
4010 2096 'operation_type' => $operation_type,
4011 - 'vector_id' => $vector_id,
4012 - 'acf_fields_found' => $acf_field_count,
4013 - 'pdf_extracted_count' => (int) $pdf_extracted_count,
4014 - 'content_preview' => substr($content, 0, 100) . '...',
4015 - 'bot_id' => $bot_id
2097 + 'vector_id' => $vector_id, // Include vector ID for debugging
2098 + 'cache_updated' => $use_pinecone // Indicate if cache was updated
4016 2099 ));
4017 2100 exit;
4018 2101 }
4019 2102
4020 -private function apply_role_restriction_to_post($post_id, $source_url) {
4021 - // Get tag-role mappings
4022 - $mappings = get_option('mxchat_tag_role_mappings', array());
4023 -
4024 - if (empty($mappings)) {
4025 - return; // No mappings, leave as public
4026 - }
4027 -
4028 - // Get all tags for the post
4029 - $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
4030 -
4031 - if (empty($post_tags)) {
4032 - return; // No tags, leave as public
4033 - }
4034 -
4035 - // Determine the highest role restriction based on tags
4036 - $highest_role = 'public';
4037 - $role_hierarchy = array(
4038 - 'public' => 0,
4039 - 'logged_in' => 1,
4040 - 'subscriber' => 2,
4041 - 'contributor' => 3,
4042 - 'author' => 4,
4043 - 'editor' => 5,
4044 - 'administrator' => 6
4045 - );
4046 -
4047 - foreach ($post_tags as $tag_slug) {
4048 - if (isset($mappings[$tag_slug])) {
4049 - $role = $mappings[$tag_slug];
4050 - if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
4051 - $highest_role = $role;
4052 - }
4053 - }
4054 - }
4055 -
4056 - // If no restricted tags found, return (leave as public)
4057 - if ($highest_role === 'public') {
4058 - return;
4059 - }
4060 -
4061 - // Update the role restriction in the database
4062 - global $wpdb;
4063 -
4064 - // Check if using Pinecone
4065 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4066 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
4067 -
4068 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
4069 - // Update Pinecone role restriction
4070 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
4071 - $vector_id = md5($source_url);
4072 -
4073 - $wpdb->replace(
4074 - $roles_table,
4075 - array(
4076 - 'vector_id' => $vector_id,
4077 - 'role_restriction' => $highest_role,
4078 - 'updated_at' => current_time('mysql')
4079 - ),
4080 - array('%s', '%s', '%s')
4081 - );
4082 - } else {
4083 - // Update WordPress DB
4084 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4085 -
4086 - $wpdb->update(
4087 - $table_name,
4088 - array('role_restriction' => $highest_role),
4089 - array('source_url' => $source_url),
4090 - array('%s'),
4091 - array('%s')
4092 - );
4093 - }
4094 -}
4095 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 + }
4096 2115 public function mxchat_get_public_post_types() {
4097 - // Get all public post types
4098 2116 $post_types = get_post_types(array('public' => true), 'objects');
4099 2117 $post_type_options = array();
4100 2118
4101 2119 foreach ($post_types as $post_type) {
@@ -4101,45 +2119,51 @@
4101 2119 foreach ($post_types as $post_type) {
4102 2120 $post_type_options[$post_type->name] = $post_type->label;
4103 2121 }
4104 2122
4105 - // Also include common forum/community post types that might not be marked as public
4106 - $additional_types = array(
4107 - 'topic' => 'Forum Topics (bbPress)',
4108 - 'reply' => 'Forum Replies (bbPress)',
4109 - 'forum' => 'Forums (bbPress)',
4110 - 'wpforo_topic' => 'wpForo Topics',
4111 - 'wpforo_post' => 'wpForo Posts'
4112 - );
4113 -
4114 - foreach ($additional_types as $type_name => $type_label) {
4115 - if (post_type_exists($type_name) && !isset($post_type_options[$type_name])) {
4116 - $post_type_options[$type_name] = $type_label;
4117 - }
4118 - }
4119 -
4120 2123 return $post_type_options;
4121 2124 }
2125 +public function mxchat_get_pinecone_processed_content($pinecone_options) {
2126 + //error_log('=== DEBUG: Starting mxchat_get_pinecone_processed_content ===');
2127 +
2128 + // First check local cache for immediate updates
2129 + $cached_data = get_option('mxchat_pinecone_processed_cache', array());
2130 + //error_log('DEBUG: Found ' . count($cached_data) . ' items in local cache');
4122 2131
4123 -/**
4124 - * Retrieves processed content from Pinecone API
4125 - */
4126 -public function mxchat_get_pinecone_processed_content($pinecone_options) {
4127 2132 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
4128 2133 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
2134 +
2135 + //error_log('DEBUG: API key present: ' . (!empty($api_key) ? 'YES' : 'NO'));
2136 + //error_log('DEBUG: Host: ' . $host);
4129 2137
4130 2138 if (empty($api_key) || empty($host)) {
4131 - return array();
2139 + //error_log('DEBUG: Missing API credentials, returning cached data only');
2140 + return $cached_data;
4132 2141 }
4133 2142
4134 2143 $pinecone_data = array();
4135 2144
4136 2145 try {
4137 - // Always get fresh data from Pinecone
4138 - $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
2146 + // Method 1: Try to get vectors using cached vector IDs first
2147 + $cached_vector_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
2148 + //error_log('DEBUG: Found ' . count($cached_vector_ids) . ' cached vector IDs');
4139 2149
4140 - // Method 2: Final fallback - try stats endpoint (if available)
2150 + if (!empty($cached_vector_ids)) {
2151 + //error_log('DEBUG: Trying to fetch by cached vector IDs...');
2152 + $pinecone_data = $this->mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $cached_vector_ids);
2153 + //error_log('DEBUG: Fetch by IDs returned ' . count($pinecone_data) . ' items');
2154 + }
2155 +
2156 + // Method 2: If no cached IDs or fetch failed, use scanning approach
4141 2157 if (empty($pinecone_data)) {
2158 + //error_log('DEBUG: Trying scanning approach...');
2159 + $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
2160 + //error_log('DEBUG: Scanning returned ' . count($pinecone_data) . ' items');
2161 + }
2162 +
2163 + // Method 3: Final fallback - try stats endpoint
2164 + if (empty($pinecone_data)) {
2165 + //error_log('DEBUG: Trying stats endpoint...');
4142 2166 $stats_url = "https://{$host}/describe_index_stats";
4143 2167
4144 2168 $response = wp_remote_post($stats_url, array(
4145 2169 'headers' => array(
@@ -4152,17 +2176,44 @@
4152 2176
4153 2177 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
4154 2178 $body = wp_remote_retrieve_body($response);
4155 2179 $stats_data = json_decode($body, true);
2180 + //error_log('DEBUG: Pinecone stats: ' . print_r($stats_data, true));
2181 + } else {
2182 + if (is_wp_error($response)) {
2183 + //error_log('DEBUG: Stats endpoint error: ' . $response->get_error_message());
2184 + } else {
2185 + //error_log('DEBUG: Stats endpoint failed with code: ' . wp_remote_retrieve_response_code($response));
2186 + }
4156 2187 }
4157 2188 }
4158 2189
4159 2190 } catch (Exception $e) {
4160 - // Log error but return fresh data only
2191 + //error_log('DEBUG: Exception in get_pinecone_processed_content: ' . $e->getMessage());
4161 2192 }
4162 2193
4163 - return $pinecone_data;
2194 + // Merge cached data with Pinecone data
2195 + $merged_data = $pinecone_data;
2196 +
2197 + foreach ($cached_data as $post_id => $cache_item) {
2198 + $cache_timestamp = $cache_item['timestamp'] ?? 0;
2199 + $time_diff = current_time('timestamp') - $cache_timestamp;
2200 +
2201 + if ($time_diff < 300) { // 5 minutes = 300 seconds
2202 + $merged_data[$post_id] = $cache_item;
2203 + } else {
2204 + if (!isset($merged_data[$post_id])) {
2205 + $merged_data[$post_id] = $cache_item;
2206 + }
2207 + }
2208 + }
2209 +
2210 + //error_log('DEBUG: Final merged data count: ' . count($merged_data));
2211 + //error_log('=== DEBUG: End mxchat_get_pinecone_processed_content ===');
2212 +
2213 + return $merged_data;
4164 2214 }
2215 +
4165 2216 public function mxchat_fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
4166 2217 //error_log('=== DEBUG: Starting mxchat_fetch_pinecone_vectors_by_ids ===');
4167 2218
4168 2219 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
@@ -4254,46 +2305,16 @@
4254 2305 return array();
4255 2306 }
4256 2307 }
4257 2308
4258 -/**
4259 - * Get embedding dimensions based on the selected model.
4260 - */
4261 -private function mxchat_get_embedding_dimensions() {
4262 - $options = get_option('mxchat_options', array());
4263 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4264 -
4265 - $model_dimensions = array(
4266 - 'text-embedding-ada-002' => 1536,
4267 - 'text-embedding-3-small' => 1536,
4268 - 'text-embedding-3-large' => 3072,
4269 - 'voyage-2' => 1024,
4270 - 'voyage-large-2' => 1536,
4271 - 'voyage-3-large' => 2048,
4272 - 'gemini-embedding-001' => 1536,
4273 - );
4274 -
4275 - if (strpos($selected_model, 'voyage-3-large') === 0) {
4276 - $custom_dimensions = $options['voyage_output_dimension'] ?? 2048;
4277 - return intval($custom_dimensions);
4278 - }
4279 -
4280 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4281 - $custom_dimensions = $options['gemini_output_dimension'] ?? 1536;
4282 - return intval($custom_dimensions);
4283 - }
4284 -
4285 - return $model_dimensions[$selected_model] ?? 1536;
4286 -}
4287 -
4288 -/**
4289 - * Scan Pinecone for processed content
4290 - */
4291 2309 public function mxchat_scan_pinecone_for_processed_content($pinecone_options) {
2310 + //error_log('=== DEBUG: Starting mxchat_scan_pinecone_for_processed_content ===');
2311 +
4292 2312 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
4293 2313 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
4294 2314
4295 2315 if (empty($api_key) || empty($host)) {
2316 + //error_log('DEBUG: Missing API credentials for scanning');
4296 2317 return array();
4297 2318 }
4298 2319
4299 2320 try {
@@ -4300,18 +2321,17 @@
4300 2321 // Use multiple random vectors to get better coverage
4301 2322 $all_matches = array();
4302 2323 $seen_ids = array();
4303 2324
4304 - // Get correct dimensions for the configured embedding model
4305 - $dimensions = $this->mxchat_get_embedding_dimensions();
4306 -
4307 2325 // Try 3 different random vectors to get better coverage
4308 2326 for ($i = 0; $i < 3; $i++) {
2327 + //error_log('DEBUG: Scanning attempt ' . ($i + 1) . '/3');
2328 +
4309 2329 $query_url = "https://{$host}/query";
4310 2330
4311 2331 // Generate a random unit vector instead of zeros
4312 2332 $random_vector = array();
4313 - for ($j = 0; $j < $dimensions; $j++) {
2333 + for ($j = 0; $j < 1536; $j++) {
4314 2334 $random_vector[] = (rand(-1000, 1000) / 1000.0);
4315 2335 }
4316 2336
4317 2337 // Normalize the vector to unit length
@@ -4336,14 +2356,18 @@
4336 2356 'timeout' => 30
4337 2357 ));
4338 2358
4339 2359 if (is_wp_error($response)) {
2360 + //error_log('DEBUG: Query attempt ' . ($i + 1) . ' WP error: ' . $response->get_error_message());
4340 2361 continue;
4341 2362 }
4342 2363
4343 2364 $response_code = wp_remote_retrieve_response_code($response);
2365 + //error_log('DEBUG: Query attempt ' . ($i + 1) . ' response code: ' . $response_code);
4344 2366
4345 2367 if ($response_code !== 200) {
2368 + $error_body = wp_remote_retrieve_body($response);
2369 + //error_log('DEBUG: Query attempt ' . ($i + 1) . ' failed with body: ' . substr($error_body, 0, 500));
4346 2370 continue;
4347 2371 }
4348 2372
4349 2373 $body = wp_remote_retrieve_body($response);
@@ -4349,8 +2373,9 @@
4349 2373 $body = wp_remote_retrieve_body($response);
4350 2374 $data = json_decode($body, true);
4351 2375
4352 2376 if (isset($data['matches'])) {
2377 + //error_log('DEBUG: Query attempt ' . ($i + 1) . ' returned ' . count($data['matches']) . ' matches');
4353 2378 foreach ($data['matches'] as $match) {
4354 2379 $match_id = $match['id'] ?? '';
4355 2380 if (!empty($match_id) && !isset($seen_ids[$match_id])) {
4356 2381 $all_matches[] = $match;
@@ -4356,14 +2381,18 @@
4356 2381 $all_matches[] = $match;
4357 2382 $seen_ids[$match_id] = true;
4358 2383 }
4359 2384 }
2385 + } else {
2386 + //error_log('DEBUG: Query attempt ' . ($i + 1) . ' - no matches key in response');
4360 2387 }
4361 2388 }
4362 2389
4363 - // Convert matches to processed data format, grouping by URL to count chunks
2390 + //error_log('DEBUG: Total unique matches found: ' . count($all_matches));
2391 +
2392 + // Convert matches to processed data format
4364 2393 $processed_data = array();
4365 - $url_chunk_counts = array();
2394 + $vector_ids_for_cache = array();
4366 2395
4367 2396 foreach ($all_matches as $match) {
4368 2397 $metadata = $match['metadata'] ?? array();
4369 2398 $source_url = $metadata['source_url'] ?? '';
@@ -4371,14 +2400,8 @@
4371 2400
4372 2401 if (!empty($source_url) && !empty($match_id)) {
4373 2402 $post_id = url_to_postid($source_url);
4374 2403 if ($post_id) {
4375 - // Count chunks per post_id
4376 - if (!isset($url_chunk_counts[$post_id])) {
4377 - $url_chunk_counts[$post_id] = 0;
4378 - }
4379 - $url_chunk_counts[$post_id]++;
4380 -
4381 2404 $created_at = $metadata['created_at'] ?? '';
4382 2405 $processed_date = 'Recently';
4383 2406
4384 2407 if (!empty($created_at)) {
@@ -4387,322 +2410,243 @@
4387 2410 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
4388 2411 }
4389 2412 }
4390 2413
4391 - // Only store if not already set, or update with newer timestamp
4392 - if (!isset($processed_data[$post_id]) ||
4393 - ($timestamp ?? 0) > ($processed_data[$post_id]['timestamp'] ?? 0)) {
4394 - $processed_data[$post_id] = array(
4395 - 'db_id' => $match_id,
4396 - 'processed_date' => $processed_date,
4397 - 'url' => $source_url,
4398 - 'source' => 'pinecone',
4399 - 'timestamp' => $timestamp ?? current_time('timestamp')
4400 - );
4401 - }
2414 + $processed_data[$post_id] = array(
2415 + 'db_id' => $match_id,
2416 + 'processed_date' => $processed_date,
2417 + 'url' => $source_url,
2418 + 'source' => 'pinecone',
2419 + 'timestamp' => $timestamp ?? current_time('timestamp')
2420 + );
2421 +
2422 + $vector_ids_for_cache[] = $match_id;
4402 2423 }
4403 2424 }
4404 2425 }
4405 2426
4406 - // Add chunk counts to processed data
4407 - foreach ($url_chunk_counts as $post_id => $chunk_count) {
4408 - if (isset($processed_data[$post_id])) {
4409 - $processed_data[$post_id]['chunk_count'] = $chunk_count;
4410 - }
2427 + // Update the vector IDs cache for future use
2428 + if (!empty($vector_ids_for_cache)) {
2429 + update_option('mxchat_pinecone_vector_ids_cache', $vector_ids_for_cache);
2430 + //error_log('DEBUG: Updated vector IDs cache with ' . count($vector_ids_for_cache) . ' IDs');
4411 2431 }
4412 2432
2433 + //error_log('DEBUG: Returning ' . count($processed_data) . ' processed items from scanning');
4413 2434 return $processed_data;
4414 2435
4415 2436 } catch (Exception $e) {
2437 + //error_log('DEBUG: Exception in scan_pinecone_for_processed_content: ' . $e->getMessage());
4416 2438 return array();
4417 2439 }
4418 2440 }
4419 -/**
4420 - * Generate embeddings from input text for MXChat with bot support
4421 - */
4422 -private function mxchat_generate_embedding($text, $bot_id = 'default') {
4423 - // Enable detailed logging for debugging
4424 - //error_log('[MXCHAT-EMBED] Starting embedding generation for bot: ' . $bot_id . '. Text length: ' . strlen($text) . ' bytes');
4425 - //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
4426 2441
4427 - // Get bot-specific options
4428 - $bot_options = $this->get_bot_options($bot_id);
4429 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
2442 + /**
2443 + * Generates embeddings from input text for MXChat
2444 + */
2445 + private function mxchat_generate_embedding($text) {
2446 + // Enable detailed logging for debugging
2447 + //error_log('[MXCHAT-EMBED] Starting embedding generation. Text length: ' . strlen($text) . ' bytes');
2448 + //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
4430 2449
4431 - // Opt-in: when the custom provider is selected for embeddings, index through
4432 - // the same custom endpoint the query path uses so stored vectors and query
4433 - // vectors share a model. Returns the vector array on success, or an error
4434 - // string on failure (this function's existing failure contract).
4435 - if (isset($options['custom_provider_for_embeddings']) && $options['custom_provider_for_embeddings'] === 'on') {
4436 - if (!class_exists('MxChat_Utils')) {
4437 - require_once dirname(__FILE__) . '/../includes/class-mxchat-utils.php';
4438 - }
4439 - return MxChat_Utils::generate_embedding_custom($text, $options);
4440 - }
2450 + $options = get_option('mxchat_options');
2451 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
2452 + //error_log('[MXCHAT-EMBED] Selected embedding model: ' . $selected_model);
4441 2453
4442 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4443 - //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model);
2454 + // Determine provider and endpoint
2455 + if (strpos($selected_model, 'voyage') === 0) {
2456 + $api_key = $options['voyage_api_key'] ?? '';
2457 + $endpoint = 'https://api.voyageai.com/v1/embeddings';
2458 + $provider_name = 'Voyage AI';
2459 + //error_log('[MXCHAT-EMBED] Using Voyage AI API');
2460 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2461 + $api_key = $options['gemini_api_key'] ?? '';
2462 + $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
2463 + $provider_name = 'Google Gemini';
2464 + //error_log('[MXCHAT-EMBED] Using Google Gemini API');
2465 + } else {
2466 + $api_key = $options['api_key'] ?? '';
2467 + $endpoint = 'https://api.openai.com/v1/embeddings';
2468 + $provider_name = 'OpenAI';
2469 + //error_log('[MXCHAT-EMBED] Using OpenAI API');
2470 + }
4444 2471
4445 - // Determine provider and endpoint
4446 - if (strpos($selected_model, 'voyage') === 0) {
4447 - $api_key = $options['voyage_api_key'] ?? '';
4448 - $endpoint = 'https://api.voyageai.com/v1/embeddings';
4449 - $provider_name = 'Voyage AI';
4450 - //error_log('[MXCHAT-EMBED] Using Voyage AI API for bot ' . $bot_id);
4451 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
4452 - $api_key = $options['gemini_api_key'] ?? '';
4453 - $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
4454 - $provider_name = 'Google Gemini';
4455 - //error_log('[MXCHAT-EMBED] Using Google Gemini API for bot ' . $bot_id);
4456 - } else {
4457 - $api_key = $options['api_key'] ?? '';
4458 - $endpoint = 'https://api.openai.com/v1/embeddings';
4459 - $provider_name = 'OpenAI';
4460 - //error_log('[MXCHAT-EMBED] Using OpenAI API for bot ' . $bot_id);
4461 - }
2472 + //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
4462 2473
4463 - //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
2474 + if (empty($api_key)) {
2475 + $error_message = sprintf('Missing %s API key. Please configure your API key in the MxChat settings.', $provider_name);
2476 + //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
2477 + return $error_message;
2478 + }
4464 2479
4465 - if (empty($api_key)) {
4466 - $error_message = sprintf('Missing %s API key for bot %s. Please configure your API key in the bot settings.', $provider_name, $bot_id);
4467 - //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
4468 - return $error_message;
4469 - }
2480 + // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
2481 + $estimated_tokens = ceil(str_word_count($text) / 0.75);
2482 + //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
4470 2483
4471 - // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
4472 - $estimated_tokens = ceil(str_word_count($text) / 0.75);
4473 - //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
2484 + if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
2485 + //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
2486 + // Consider truncating text here
2487 + }
4474 2488
4475 - if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
4476 - //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
4477 - // Consider truncating text here
4478 - }
2489 + // Prepare request body based on provider
2490 + if (strpos($selected_model, 'gemini-embedding') === 0) {
2491 + // Gemini API format
2492 + $request_body = array(
2493 + 'model' => 'models/' . $selected_model,
2494 + 'content' => array(
2495 + 'parts' => array(
2496 + array('text' => $text)
2497 + )
2498 + )
2499 + );
4479 2500
4480 - // Prepare request body based on provider
4481 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4482 - // Gemini API format
4483 - $request_body = array(
4484 - 'model' => 'models/' . $selected_model,
4485 - 'content' => array(
4486 - 'parts' => array(
4487 - array('text' => $text)
4488 - )
4489 - )
4490 - );
2501 + // Set output dimensionality to 1536 for consistency with other models
2502 + $request_body['outputDimensionality'] = 1536;
2503 + } else {
2504 + // OpenAI/Voyage API format
2505 + $request_body = array(
2506 + 'model' => $selected_model,
2507 + 'input' => $text
2508 + );
4491 2509
4492 - // Set output dimensionality to 1536 for consistency with other models
4493 - $request_body['outputDimensionality'] = 1536;
4494 - } else {
4495 - // OpenAI/Voyage API format
4496 - $request_body = array(
4497 - 'model' => $selected_model,
4498 - 'input' => $text
4499 - );
2510 + // Add output_dimension for voyage-3-large model
2511 + if ($selected_model === 'voyage-3-large') {
2512 + $request_body['output_dimension'] = 2048;
2513 + }
2514 + }
4500 2515
4501 - // Add output_dimension for voyage-3-large model
4502 - if ($selected_model === 'voyage-3-large') {
4503 - $request_body['output_dimension'] = 2048;
4504 - }
4505 - }
2516 + //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
4506 2517
4507 - //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
2518 + // Prepare headers based on provider
2519 + if (strpos($selected_model, 'gemini-embedding') === 0) {
2520 + // Gemini uses API key as query parameter
2521 + $endpoint .= '?key=' . $api_key;
2522 + $headers = array(
2523 + 'Content-Type' => 'application/json'
2524 + );
2525 + } else {
2526 + // OpenAI/Voyage use Bearer token
2527 + $headers = array(
2528 + 'Authorization' => 'Bearer ' . $api_key,
2529 + 'Content-Type' => 'application/json'
2530 + );
2531 + }
4508 2532
4509 - // Prepare headers based on provider
4510 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4511 - // Gemini uses API key as query parameter
4512 - $endpoint .= '?key=' . $api_key;
4513 - $headers = array(
4514 - 'Content-Type' => 'application/json'
4515 - );
4516 - } else {
4517 - // OpenAI/Voyage use Bearer token
4518 - $headers = array(
4519 - 'Authorization' => 'Bearer ' . $api_key,
4520 - 'Content-Type' => 'application/json'
4521 - );
4522 - }
2533 + // Make API request
2534 + //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
2535 + $response = wp_remote_post($endpoint, array(
2536 + 'body' => wp_json_encode($request_body),
2537 + 'headers' => $headers,
2538 + 'timeout' => 60 // Increased timeout for large inputs
2539 + ));
4523 2540
4524 - // Make API request
4525 - //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
4526 - $response = wp_remote_post($endpoint, array(
4527 - 'body' => wp_json_encode($request_body),
4528 - 'headers' => $headers,
4529 - 'timeout' => 60 // Increased timeout for large inputs
4530 - ));
2541 + // Handle wp_remote_post errors
2542 + if (is_wp_error($response)) {
2543 + $error_message = $response->get_error_message();
2544 + //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
2545 + return 'Connection error: ' . $error_message;
2546 + }
4531 2547
4532 - // Handle wp_remote_post errors
4533 - if (is_wp_error($response)) {
4534 - $error_message = $response->get_error_message();
4535 - //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
4536 - return 'Connection error: ' . $error_message;
4537 - }
2548 + // Get and check HTTP response code
2549 + $http_code = wp_remote_retrieve_response_code($response);
2550 + //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
4538 2551
4539 - // Get and check HTTP response code
4540 - $http_code = wp_remote_retrieve_response_code($response);
4541 - //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
2552 + if ($http_code !== 200) {
2553 + $error_body = wp_remote_retrieve_body($response);
2554 + //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
4542 2555
4543 - if ($http_code !== 200) {
4544 - $error_body = wp_remote_retrieve_body($response);
4545 - //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
2556 + // Try to parse error for more details
2557 + $error_json = json_decode($error_body, true);
2558 + if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
2559 + $error_type = $error_json['error']['type'] ?? 'unknown';
2560 + $error_message = $error_json['error']['message'] ?? 'No message';
2561 + //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
2562 + //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
4546 2563
4547 - // Try to parse error for more details
4548 - $error_json = json_decode($error_body, true);
4549 - if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
4550 - $error_type = $error_json['error']['type'] ?? 'unknown';
4551 - $error_message = $error_json['error']['message'] ?? 'No message';
4552 - //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
4553 - //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
2564 + // Customize error message for common API errors
2565 + if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
2566 + $error_message = sprintf('Invalid %s API key. Please check your API key in the MxChat settings.', $provider_name);
2567 + } elseif ($error_type === 'authentication_error') {
2568 + $error_message = sprintf('%s authentication failed. Please verify your API key in the MxChat settings.', $provider_name);
2569 + }
4554 2570
4555 - // Customize error message for common API errors
4556 - if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
4557 - $error_message = sprintf('Invalid %s API key for bot %s. Please check your API key in the bot settings.', $provider_name, $bot_id);
4558 - } elseif ($error_type === 'authentication_error') {
4559 - $error_message = sprintf('%s authentication failed for bot %s. Please verify your API key in the bot settings.', $provider_name, $bot_id);
4560 - }
2571 + //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
2572 + return $error_message;
2573 + }
4561 2574
4562 - //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
4563 - return $error_message;
4564 - }
2575 + $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding", $http_code);
2576 + //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
2577 + return $error_message;
2578 + }
4565 2579
4566 - $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding for bot %s", $http_code, $bot_id);
4567 - //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
4568 - return $error_message;
4569 - }
2580 + // Parse response body
2581 + $response_body = wp_remote_retrieve_body($response);
2582 + //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
4570 2583
4571 - // Parse response body
4572 - $response_body = wp_remote_retrieve_body($response);
4573 - //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
2584 + $response_data = json_decode($response_body, true);
4574 2585
4575 - $response_data = json_decode($response_body, true);
2586 + if (json_last_error() !== JSON_ERROR_NONE) {
2587 + $error = json_last_error_msg();
2588 + //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
2589 + //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
2590 + return "Failed to parse API response: $error";
2591 + }
4576 2592
4577 - if (json_last_error() !== JSON_ERROR_NONE) {
4578 - $error = json_last_error_msg();
4579 - //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
4580 - //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
4581 - return "Failed to parse API response: $error";
4582 - }
2593 + // Handle different response formats based on provider
2594 + if (strpos($selected_model, 'gemini-embedding') === 0) {
2595 + // Gemini API response format
2596 + if (isset($response_data['embedding']['values'])) {
2597 + $embedding_dimensions = count($response_data['embedding']['values']);
2598 + //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
4583 2599
4584 - // Handle different response formats based on provider
4585 - if (strpos($selected_model, 'gemini-embedding') === 0) {
4586 - // Gemini API response format
4587 - if (isset($response_data['embedding']['values'])) {
4588 - $embedding_dimensions = count($response_data['embedding']['values']);
4589 - //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
2600 + // Check if embedding dimensions are as expected (should be 1536)
2601 + if ($embedding_dimensions !== 1536) {
2602 + //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
2603 + }
4590 2604
4591 - // Check if embedding dimensions are as expected (should be 1536)
4592 - if ($embedding_dimensions !== 1536) {
4593 - //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
4594 - }
2605 + return $response_data['embedding']['values'];
2606 + } else {
2607 + //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
2608 + //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
4595 2609
4596 - return $response_data['embedding']['values'];
4597 - } else {
4598 - //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
4599 - //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
2610 + if (isset($response_data['error'])) {
2611 + $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
2612 + //error_log('[MXCHAT-EMBED] ' . $error_message);
2613 + return $error_message;
2614 + }
4600 2615
4601 - if (isset($response_data['error'])) {
4602 - $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
4603 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4604 - return $error_message;
4605 - }
2616 + $error_message = "Invalid Gemini API response format: No embedding found";
2617 + //error_log('[MXCHAT-EMBED] ' . $error_message);
2618 + return $error_message;
2619 + }
2620 + } else {
2621 + // OpenAI/Voyage API response format
2622 + if (isset($response_data['data'][0]['embedding'])) {
2623 + $embedding_dimensions = count($response_data['data'][0]['embedding']);
2624 + //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
4606 2625
4607 - $error_message = "Invalid Gemini API response format: No embedding found";
4608 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4609 - return $error_message;
4610 - }
4611 - } else {
4612 - // OpenAI/Voyage API response format
4613 - if (isset($response_data['data'][0]['embedding'])) {
4614 - $embedding_dimensions = count($response_data['data'][0]['embedding']);
4615 - //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
2626 + // Check if embedding dimensions are as expected
2627 + if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
2628 + ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
2629 + //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
2630 + }
4616 2631
4617 - // Check if embedding dimensions are as expected
4618 - if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
4619 - ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
4620 - //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
4621 - }
2632 + return $response_data['data'][0]['embedding'];
2633 + } else {
2634 + //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
2635 + //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
4622 2636
4623 - return $response_data['data'][0]['embedding'];
4624 - } else {
4625 - //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
4626 - //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
2637 + if (isset($response_data['error'])) {
2638 + $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
2639 + //error_log('[MXCHAT-EMBED] ' . $error_message);
2640 + return $error_message;
2641 + }
4627 2642
4628 - if (isset($response_data['error'])) {
4629 - $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
4630 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4631 - return $error_message;
4632 - }
4633 -
4634 - $error_message = "Invalid API response format: No embedding found";
4635 - //error_log('[MXCHAT-EMBED] ' . $error_message);
4636 - return $error_message;
4637 - }
4638 - }
4639 -}
4640 -
4641 -/**
4642 - * Get bot-specific options for multi-bot functionality
4643 - * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
4644 - */
4645 -private function get_bot_options($bot_id = 'default') {
4646 - //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
4647 -
4648 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
4649 - //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
4650 - return array();
4651 - }
4652 -
4653 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
4654 -
4655 - if (!empty($bot_options)) {
4656 - //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
4657 - if (isset($bot_options['similarity_threshold'])) {
4658 - //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
4659 - }
4660 - }
4661 -
4662 - return is_array($bot_options) ? $bot_options : array();
4663 -}
4664 -
4665 -/**
4666 - * Get bot-specific Pinecone configuration
4667 - * Used in the knowledge retrieval functions
4668 - */
4669 -// Also add debugging to your get_bot_pinecone_config function
4670 -private function get_bot_pinecone_config($bot_id = 'default') {
4671 - //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
4672 -
4673 - // If default bot or multi-bot add-on not active, use default Pinecone config
4674 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
4675 - //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
4676 - $addon_options = get_option('mxchat_pinecone_addon_options', array());
4677 - $config = array(
4678 - 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
4679 - 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
4680 - 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
4681 - 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
4682 - );
4683 - //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
4684 - return $config;
4685 - }
4686 -
4687 - //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
4688 -
4689 - // Hook for multi-bot add-on to provide bot-specific Pinecone config
4690 - $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
4691 -
4692 - if (!empty($bot_pinecone_config)) {
4693 - //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
4694 - //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
4695 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
4696 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
4697 - } else {
4698 - //error_log("MXCHAT DEBUG: Filter returned empty config!");
4699 - }
4700 -
4701 - return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
4702 -}
4703 -
4704 -
2643 + $error_message = "Invalid API response format: No embedding found";
2644 + //error_log('[MXCHAT-EMBED] ' . $error_message);
2645 + return $error_message;
2646 + }
2647 + }
2648 + }
4705 2649 public function mxchat_ajax_dismiss_completed_status() {
4706 2650 try {
4707 2651 // Verify the request
4708 2652 check_ajax_referer('mxchat_status_nonce', 'nonce');
@@ -5065,564 +3009,18 @@
5065 3009
5066 3010 return $html;
5067 3011 }
5068 3012
5069 -/**
5070 - * Get all ACF fields for a specific post, excluding any fields the user has disabled
5071 - */
5072 -public function mxchat_get_acf_fields_for_post($post_id) {
5073 - if (!function_exists('get_fields')) {
5074 - return array();
5075 - }
5076 -
5077 - $fields = get_fields($post_id);
5078 - if (!$fields || !is_array($fields)) {
5079 - return array();
5080 - }
5081 -
5082 - // Get excluded fields from settings
5083 - $excluded_fields = get_option('mxchat_acf_excluded_fields', array());
5084 - if (!empty($excluded_fields) && is_array($excluded_fields)) {
5085 - foreach ($excluded_fields as $excluded_field) {
5086 - if (isset($fields[$excluded_field])) {
5087 - unset($fields[$excluded_field]);
5088 - }
5089 - }
5090 - }
5091 -
5092 - return $fields;
5093 -}
5094 -
5095 -/**
5096 - * Get all registered ACF field groups and their fields for the settings UI
5097 - */
5098 -public function mxchat_get_all_acf_fields() {
5099 - if (!function_exists('acf_get_field_groups') || !function_exists('acf_get_fields')) {
5100 - return array();
5101 - }
5102 -
5103 - $all_fields = array();
5104 - $field_groups = acf_get_field_groups();
5105 -
5106 - if (!empty($field_groups)) {
5107 - foreach ($field_groups as $group) {
5108 - $group_fields = acf_get_fields($group['key']);
5109 - if (!empty($group_fields)) {
5110 - $all_fields[$group['title']] = array();
5111 - foreach ($group_fields as $field) {
5112 - $all_fields[$group['title']][] = array(
5113 - 'name' => $field['name'],
5114 - 'label' => $field['label'],
5115 - 'type' => $field['type']
5116 - );
5117 - }
5118 - }
5119 - }
5120 - }
5121 -
5122 - return $all_fields;
5123 -}
5124 -
5125 -/**
5126 - * Get whitelisted custom post meta for a given post
5127 - * This allows non-ACF meta fields (like OptionTree, theme meta boxes) to be included in embeddings
5128 - */
5129 -public function mxchat_get_whitelisted_post_meta($post_id) {
5130 - $whitelist = get_option('mxchat_custom_meta_whitelist', '');
5131 -
5132 - if (empty($whitelist)) {
5133 - return array();
5134 - }
5135 -
5136 - // Parse the whitelist - one meta key per line
5137 - $meta_keys = array_filter(array_map('trim', explode("\n", $whitelist)));
5138 -
5139 - if (empty($meta_keys)) {
5140 - return array();
5141 - }
5142 -
5143 - $result = array();
5144 -
5145 - foreach ($meta_keys as $key) {
5146 - // Skip empty keys
5147 - if (empty($key)) {
5148 - continue;
5149 - }
5150 -
5151 - $value = get_post_meta($post_id, $key, true);
5152 -
5153 - // Only include non-empty string values
5154 - if (!empty($value) && is_string($value)) {
5155 - $result[$key] = $value;
5156 - } elseif (!empty($value) && is_array($value)) {
5157 - // Handle array values by joining them
5158 - $flat_value = $this->mxchat_flatten_meta_array($value);
5159 - if (!empty($flat_value)) {
5160 - $result[$key] = $flat_value;
5161 - }
5162 - }
5163 - }
5164 -
5165 - return $result;
5166 -}
5167 -
5168 -/**
5169 - * Flatten array meta values into a readable string
5170 - */
5171 -private function mxchat_flatten_meta_array($array, $depth = 0) {
5172 - if ($depth > 3) {
5173 - return ''; // Prevent infinite recursion
5174 - }
5175 -
5176 - $parts = array();
5177 -
5178 - foreach ($array as $key => $value) {
5179 - if (is_string($value) && !empty($value)) {
5180 - $parts[] = $value;
5181 - } elseif (is_array($value)) {
5182 - $nested = $this->mxchat_flatten_meta_array($value, $depth + 1);
5183 - if (!empty($nested)) {
5184 - $parts[] = $nested;
5185 - }
5186 - }
5187 - }
5188 -
5189 - return implode(', ', $parts);
5190 -}
5191 -
5192 -/**
5193 - * Format ACF field values for content extraction
5194 - */
5195 -public function mxchat_format_acf_field_value($value, $field_name = '', $post_id = 0) {
5196 - if (empty($value)) {
5197 - return '';
5198 - }
5199 -
5200 - // Handle WP_Post objects first (THIS IS THE KEY FIX)
5201 - if ($value instanceof WP_Post) {
5202 - return $value->post_title ?: '';
5203 - }
5204 -
5205 - // Handle other WP objects
5206 - if (is_object($value)) {
5207 - if (isset($value->post_title)) {
5208 - return $value->post_title;
5209 - } elseif (isset($value->display_name)) {
5210 - return $value->display_name;
5211 - } elseif (isset($value->name)) {
5212 - return $value->name;
5213 - } elseif (method_exists($value, '__toString')) {
5214 - try {
5215 - return (string) $value;
5216 - } catch (Exception $e) {
5217 - return '';
5218 - }
5219 - }
5220 - // For any other objects, return empty string
5221 - return '';
5222 - }
5223 -
5224 - // Handle different ACF field types
5225 - if (is_array($value)) {
5226 - // Check if it's an image/file field
5227 - if (isset($value['url'])) {
5228 - // Image field - return alt text, title, or caption
5229 - if (!empty($value['alt'])) {
5230 - return $value['alt'];
5231 - } elseif (!empty($value['title'])) {
5232 - return $value['title'];
5233 - } elseif (!empty($value['caption'])) {
5234 - return $value['caption'];
5235 - } else {
5236 - return ''; // Don't include just the URL
5237 - }
5238 - }
5239 -
5240 - // Check if it's a post object or relationship field
5241 - if (isset($value['post_title'])) {
5242 - return $value['post_title'];
5243 - }
5244 -
5245 - // Check if it's a user field
5246 - if (isset($value['display_name'])) {
5247 - return $value['display_name'];
5248 - }
5249 -
5250 - // Check if it's a taxonomy term
5251 - if (isset($value['name']) && isset($value['taxonomy'])) {
5252 - return $value['name'];
5253 - }
5254 -
5255 - // Check if it's a select field with label
5256 - if (isset($value['label'])) {
5257 - return $value['label'];
5258 - }
5259 -
5260 - // Check for repeater field or flexible content
5261 - if (is_numeric(key($value))) {
5262 - $sub_values = array();
5263 - foreach ($value as $sub_item) {
5264 - if (is_array($sub_item)) {
5265 - // For repeater/flexible content, extract text values
5266 - $sub_text = $this->mxchat_extract_text_from_acf_array($sub_item);
5267 - if (!empty($sub_text)) {
5268 - $sub_values[] = $sub_text;
5269 - }
5270 - } elseif ($sub_item instanceof WP_Post) {
5271 - // Handle WP_Post objects in arrays
5272 - $sub_values[] = $sub_item->post_title ?: '';
5273 - } else {
5274 - $sub_values[] = (string) $sub_item;
5275 - }
5276 - }
5277 - return implode(', ', array_filter($sub_values));
5278 - }
5279 -
5280 - // For other arrays, try to extract meaningful text
5281 - $text_values = array();
5282 - foreach ($value as $key => $val) {
5283 - if (is_string($val) && !empty(trim($val))) {
5284 - $text_values[] = trim($val);
5285 - } elseif ($val instanceof WP_Post) {
5286 - // Handle WP_Post objects in associative arrays
5287 - $text_values[] = $val->post_title ?: '';
5288 - } elseif (is_array($val) && isset($val['post_title'])) {
5289 - $text_values[] = $val['post_title'];
5290 - } elseif (is_array($val) && isset($val['name'])) {
5291 - $text_values[] = $val['name'];
5292 - }
5293 - }
5294 -
5295 - return implode(', ', array_filter($text_values));
5296 - }
5297 -
5298 - // Handle boolean values
5299 - if (is_bool($value)) {
5300 - return $value ? 'Yes' : 'No';
5301 - }
5302 -
5303 - // Handle numeric values
5304 - if (is_numeric($value)) {
5305 - return (string) $value;
5306 - }
5307 -
5308 - // Handle string values
5309 - if (is_string($value)) {
5310 - return trim($value);
5311 - }
5312 -
5313 - // For anything else that we can't handle, return empty string
5314 - // This prevents the "Object could not be converted to string" error
5315 - return '';
5316 -}
5317 -
5318 -/**
5319 - * Extract text from complex ACF array structures
5320 - */
5321 -private function mxchat_extract_text_from_acf_array($array) {
5322 - if (!is_array($array)) {
5323 - return '';
5324 - }
5325 -
5326 - $text_parts = array();
5327 -
5328 - foreach ($array as $key => $value) {
5329 - if (is_string($value) && !empty(trim($value))) {
5330 - // Skip keys that are likely to be IDs or technical values
5331 - if (!is_numeric($value) || strlen($value) > 10) {
5332 - $text_parts[] = trim($value);
5333 - }
5334 - } elseif ($value instanceof WP_Post) {
5335 - // Handle WP_Post objects
5336 - $text_parts[] = $value->post_title ?: '';
5337 - } elseif (is_array($value)) {
5338 - if (isset($value['post_title'])) {
5339 - $text_parts[] = $value['post_title'];
5340 - } elseif (isset($value['name'])) {
5341 - $text_parts[] = $value['name'];
5342 - } elseif (isset($value['label'])) {
5343 - $text_parts[] = $value['label'];
5344 - }
5345 - } elseif (is_object($value)) {
5346 - // Handle other objects safely
5347 - if (isset($value->post_title)) {
5348 - $text_parts[] = $value->post_title;
5349 - } elseif (isset($value->name)) {
5350 - $text_parts[] = $value->name;
5351 - } elseif (isset($value->display_name)) {
5352 - $text_parts[] = $value->display_name;
5353 - }
5354 - }
5355 - }
5356 -
5357 - return implode(', ', array_filter($text_parts));
5358 -}
5359 -
5360 -/**
5361 - * Walk an ACF field value tree and collect attachment IDs for any value that
5362 - * resolves to a PDF in the WordPress media library. Handles the three shapes
5363 - * ACF returns for File/Image/URL fields (array with ID+url, integer attachment ID,
5364 - * plain URL string), and recurses through repeater/group/flexible content.
5365 - *
5366 - * @param mixed $value The ACF field value (any depth)
5367 - * @param array $out Accumulator (passed by reference) for attachment IDs
5368 - * @param int $depth Recursion guard
5369 - */
5370 -private function mxchat_collect_pdf_attachment_ids_from_acf_value($value, &$out, $depth = 0) {
5371 - if ($depth > 6) {
5372 - return; // prevent runaway recursion on circular/very-deep structures
5373 - }
5374 -
5375 - if (empty($value)) {
5376 - return;
5377 - }
5378 -
5379 - // Array shapes: ACF File/Image return value=array; repeaters/groups are arrays of arrays
5380 - if (is_array($value)) {
5381 - // Direct File/Image-style array (has 'url' and usually 'ID' + 'mime_type')
5382 - $looks_like_attachment = isset($value['url']) || isset($value['ID']) || isset($value['id']);
5383 - if ($looks_like_attachment) {
5384 - $att_id = 0;
5385 - if (!empty($value['ID']) && is_numeric($value['ID'])) {
5386 - $att_id = (int) $value['ID'];
5387 - } elseif (!empty($value['id']) && is_numeric($value['id'])) {
5388 - $att_id = (int) $value['id'];
5389 - } elseif (!empty($value['url']) && is_string($value['url'])) {
5390 - $att_id = (int) attachment_url_to_postid($value['url']);
5391 - }
5392 -
5393 - $is_pdf = false;
5394 - if (!empty($value['mime_type']) && $value['mime_type'] === 'application/pdf') {
5395 - $is_pdf = true;
5396 - } elseif (!empty($value['subtype']) && strtolower((string) $value['subtype']) === 'pdf') {
5397 - $is_pdf = true;
5398 - } elseif (!empty($value['url']) && is_string($value['url']) && $this->mxchat_url_looks_like_pdf($value['url'])) {
5399 - $is_pdf = true;
5400 - } elseif ($att_id && get_post_mime_type($att_id) === 'application/pdf') {
5401 - $is_pdf = true;
5402 - }
5403 -
5404 - if ($is_pdf && $att_id && get_post_mime_type($att_id) === 'application/pdf') {
5405 - $out[] = $att_id;
5406 - }
5407 - // An array node that represents one attachment doesn't contain other
5408 - // attachments inside it — done with this branch.
5409 - return;
5410 - }
5411 -
5412 - // Recurse: repeater rows, flexible-content layouts, groups, etc.
5413 - foreach ($value as $sub) {
5414 - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($sub, $out, $depth + 1);
5415 - }
5416 - return;
5417 - }
5418 -
5419 - // Plain numeric attachment ID (ACF File field set to "Return: ID")
5420 - if (is_numeric($value)) {
5421 - $att_id = (int) $value;
5422 - if ($att_id > 0 && get_post_mime_type($att_id) === 'application/pdf') {
5423 - $out[] = $att_id;
5424 - }
5425 - return;
5426 - }
5427 -
5428 - // Plain string — URL pointing at a PDF (ACF File field set to "Return: URL", or a custom URL/text field)
5429 - if (is_string($value)) {
5430 - $trimmed = trim($value);
5431 - if ($trimmed !== '' && $this->mxchat_url_looks_like_pdf($trimmed)) {
5432 - $att_id = (int) attachment_url_to_postid($trimmed);
5433 - if ($att_id > 0 && get_post_mime_type($att_id) === 'application/pdf') {
5434 - $out[] = $att_id;
5435 - }
5436 - }
5437 - return;
5438 - }
5439 -}
5440 -
5441 -/**
5442 - * Heuristic: does this URL/string look like a PDF reference?
5443 - * Tolerates query strings and fragments (#page=2).
5444 - */
5445 -private function mxchat_url_looks_like_pdf($url) {
5446 - if (!is_string($url) || $url === '') {
5447 - return false;
5448 - }
5449 - // Strip query + fragment before checking extension
5450 - $path = preg_replace('/[?#].*$/', '', $url);
5451 - return (bool) preg_match('/\.pdf$/i', $path);
5452 -}
5453 -
5454 -/**
5455 - * Extract text from a PDF attachment by ID using the bundled Smalot parser.
5456 - * Reads the file directly from disk via get_attached_file (no HTTP fetch).
5457 - * Result is cached on the attachment as post_meta keyed by file mtime so we
5458 - * only parse the same PDF once unless the file changes on disk.
5459 - *
5460 - * @param int $attachment_id
5461 - * @return string Extracted plain text, or '' on failure.
5462 - */
5463 -private function mxchat_extract_pdf_text_by_attachment_id($attachment_id) {
5464 - $attachment_id = (int) $attachment_id;
5465 - if ($attachment_id <= 0) {
5466 - return '';
5467 - }
5468 - if (get_post_mime_type($attachment_id) !== 'application/pdf') {
5469 - return '';
5470 - }
5471 -
5472 - $pdf_path = get_attached_file($attachment_id);
5473 - if (empty($pdf_path) || !file_exists($pdf_path) || !is_readable($pdf_path)) {
5474 - return '';
5475 - }
5476 -
5477 - // Raw-file size cap. Parsing very large PDFs can OOM the request; skip with a log entry
5478 - // and let the rest of the ACF content land in the KB. Filterable for users who need it bigger.
5479 - $default_max_bytes = 25 * 1024 * 1024;
5480 - $max_bytes = (int) apply_filters('mxchat_acf_pdf_max_bytes', $default_max_bytes, $attachment_id, $pdf_path);
5481 - if ($max_bytes > 0) {
5482 - $file_size = @filesize($pdf_path);
5483 - if ($file_size !== false && $file_size > $max_bytes) {
5484 - error_log(sprintf(
5485 - '[mxchat] ACF PDF skipped (over size cap): attachment %d "%s" %d bytes > cap %d',
5486 - $attachment_id,
5487 - basename($pdf_path),
5488 - $file_size,
5489 - $max_bytes
5490 - ));
5491 - return '';
5492 - }
5493 - }
5494 -
5495 - $mtime = @filemtime($pdf_path);
5496 - $cache_meta_key = '_mxchat_acf_pdf_text_v1';
5497 - $cached = get_post_meta($attachment_id, $cache_meta_key, true);
5498 - if (is_array($cached) && isset($cached['mtime'], $cached['text']) && (int) $cached['mtime'] === (int) $mtime) {
5499 - return (string) $cached['text'];
5500 - }
5501 -
5502 - $text = '';
5503 - try {
5504 - if (function_exists('mxchat_load_pdf_parser')) {
5505 - mxchat_load_pdf_parser();
5506 - }
5507 - if (!class_exists('\\Smalot\\PdfParser\\Parser')) {
5508 - return '';
5509 - }
5510 - $parser = new \Smalot\PdfParser\Parser();
5511 - $pdf = $parser->parseFile($pdf_path);
5512 - $pages = $pdf->getPages();
5513 - $page_texts = array();
5514 - foreach ($pages as $page) {
5515 - $page_text = '';
5516 - try {
5517 - $page_text = $page->getText();
5518 - } catch (\Exception $e) {
5519 - $page_text = '';
5520 - }
5521 - if (!empty($page_text)) {
5522 - $page_texts[] = $page_text;
5523 - }
5524 - }
5525 - $text = trim(implode("\n\n", $page_texts));
5526 - } catch (\Exception $e) {
5527 - error_log('[mxchat] ACF PDF extraction failed for attachment ' . $attachment_id . ': ' . $e->getMessage());
5528 - return '';
5529 - } catch (\Throwable $e) {
5530 - error_log('[mxchat] ACF PDF extraction error for attachment ' . $attachment_id . ': ' . $e->getMessage());
5531 - return '';
5532 - }
5533 -
5534 - // Cap per-PDF text to avoid blowing up the embedding payload on enormous PDFs.
5535 - // The chunker downstream will still split this into multiple vectors.
5536 - $max_len = (int) apply_filters('mxchat_acf_pdf_text_max_length', 50000);
5537 - if ($max_len > 0 && strlen($text) > $max_len) {
5538 - $text = substr($text, 0, $max_len);
5539 - }
5540 -
5541 - update_post_meta($attachment_id, $cache_meta_key, array(
5542 - 'mtime' => (int) $mtime,
5543 - 'text' => $text,
5544 - ));
5545 -
5546 - return $text;
5547 -}
5548 -
5549 -/**
5550 - * Handle ACF save - fires after ACF fields are saved
5551 - * This ensures ACF field data is available when syncing to knowledge base
5552 - */
5553 -public function mxchat_handle_acf_save($post_id) {
5554 - // Skip if not a valid post
5555 - if (!$post_id || $post_id === 'options') {
5556 - return;
5557 - }
5558 -
5559 - // Skip autosaves and revisions
3013 +public function mxchat_handle_post_update($post_id, $post, $update) {
3014 + // Basic validation checks
5560 3015 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
5561 3016 return;
5562 3017 }
5563 -
5564 - $post = get_post($post_id);
5565 - if (!$post) {
5566 - return;
5567 - }
5568 -
5569 - $post_type = $post->post_type;
5570 -
5571 - // Check if sync is enabled for this post type
5572 - $should_sync = false;
5573 -
5574 - if ($post_type === 'post' && get_option('mxchat_auto_sync_posts') === '1') {
5575 - $should_sync = true;
5576 - } else if ($post_type === 'page' && get_option('mxchat_auto_sync_pages') === '1') {
5577 - $should_sync = true;
5578 - } else if ($post_type === 'product' && class_exists('WooCommerce')) {
5579 - // WooCommerce products - check if WooCommerce integration is enabled
5580 - $options = get_option('mxchat_options', array());
5581 - if (isset($options['enable_woocommerce_integration']) &&
5582 - ($options['enable_woocommerce_integration'] === '1' || $options['enable_woocommerce_integration'] === 'on')) {
5583 - $should_sync = true;
5584 - }
5585 - } else {
5586 - // Check custom post types
5587 - $option_name = 'mxchat_auto_sync_' . $post_type;
5588 - if (get_option($option_name) === '1') {
5589 - $should_sync = true;
5590 - }
5591 - }
5592 -
5593 - if (!$should_sync) {
5594 - return;
5595 - }
5596 -
5597 - // Only process published posts
3018 +
3019 + // Only process published content
5598 3020 if ($post->post_status !== 'publish') {
5599 3021 return;
5600 3022 }
5601 -
5602 - // Check if this post has any ACF fields - if not, no need to re-sync
5603 - $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
5604 - if (empty($acf_fields)) {
5605 - return;
5606 - }
5607 -
5608 - // Use a transient to prevent duplicate processing (post_updated may have already run)
5609 - $transient_key = 'mxchat_acf_synced_' . $post_id;
5610 - if (get_transient($transient_key)) {
5611 - return;
5612 - }
5613 - set_transient($transient_key, true, 60); // Prevent re-processing for 60 seconds
5614 -
5615 - // Re-run the sync with ACF data now available
5616 - // We pass $update=true since this is effectively an update with ACF data
5617 - $this->mxchat_handle_post_update($post_id, $post, true);
5618 -}
5619 -
5620 -public function mxchat_handle_post_update($post_id, $post, $update) {
5621 - // Basic validation checks
5622 - if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || wp_is_post_revision($post_id)) {
5623 - return;
5624 - }
5625 3023
5626 3024 $post_type = $post->post_type;
5627 3025
5628 3026 // Check if sync is enabled for this post type
@@ -5644,276 +3042,80 @@
5644 3042 if (!$should_sync) {
5645 3043 return;
5646 3044 }
5647 3045
5648 - // Check if we have stored the previous status and URL in our transients
5649 - $previous_status_key = 'mxchat_prev_status_' . $post_id;
5650 - $previous_status = get_transient($previous_status_key);
3046 + // Get content with proper formatting (matching ajax_mxchat_process_selected_content)
3047 + $title = get_the_title($post_id);
3048 + $content = get_post_field('post_content', $post_id);
5651 3049
5652 - $previous_url_key = 'mxchat_prev_url_' . $post_id;
5653 - $previous_url = get_transient($previous_url_key);
3050 + // Apply WordPress content filters to get properly formatted content
3051 + $content = apply_filters('the_content', $content);
5654 3052
5655 - // If the post was previously published but is now not published, remove from knowledge base
5656 - if ($previous_status === 'publish' && $post->post_status !== 'publish') {
5657 - // Use the stored URL from when it was published, or fall back to current permalink
5658 - $source_url = $previous_url ?: get_permalink($post_id);
5659 -
5660 - if ($source_url) {
5661 - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks)
5662 - MxChat_Utils::delete_chunks_for_url($source_url, 'default');
3053 + // Strip tags but preserve structure
3054 + $content = wp_strip_all_tags($content);
3055 +
3056 + // Combine title and content
3057 + $final_content = $title . "\n\n" . $content;
3058 +
3059 + // For custom post types like job_listing, include additional fields
3060 + if ($post_type === 'job_listing') {
3061 + // Add job-specific meta if available
3062 + $job_location = get_post_meta($post_id, '_job_location', true);
3063 + if (!empty($job_location)) {
3064 + $final_content .= "\n\nLocation: " . $job_location;
5663 3065 }
5664 -
5665 - // Clean up the transients and exit early
5666 - delete_transient($previous_status_key);
5667 - delete_transient($previous_url_key);
5668 - return;
5669 - }
5670 -
5671 - // Slug/permalink rename while still published: delete the old vectors before upserting new ones.
5672 - // Without this, md5(old_url) vectors (base + chunks) would be orphaned under the stale URL.
5673 - if ($post->post_status === 'publish' && !empty($previous_url)) {
5674 - $current_url = get_permalink($post_id);
5675 - if ($current_url && $current_url !== $previous_url) {
5676 - MxChat_Utils::delete_chunks_for_url($previous_url, 'default');
5677 - }
5678 - }
5679 -
5680 - // Store the current status for next time (if this is an update)
5681 - if ($update) {
5682 - set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
5683 -
5684 - // If the post is currently published, also store its URL
5685 - if ($post->post_status === 'publish') {
5686 - $current_url = get_permalink($post_id);
5687 - set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
5688 - }
5689 - }
5690 -
5691 - // Only process currently published content for adding/updating
5692 - if ($post->post_status === 'publish') {
5693 - // Get the source URL
5694 - $source_url = get_permalink($post_id);
5695 3066
5696 - // Get content with proper formatting (matching ajax_mxchat_process_selected_content)
5697 - $title = get_the_title($post_id);
5698 - $content = get_post_field('post_content', $post_id);
5699 - $excerpt = get_post_field('post_excerpt', $post_id);
5700 -
5701 - // Remove shortcode tags but preserve content inside them
5702 - $content = $this->strip_shortcode_tags_preserve_content($content);
5703 - $excerpt = $this->strip_shortcode_tags_preserve_content($excerpt);
5704 -
5705 - // Strip tags but preserve structure (don't use 'the_content' filter as it may re-add shortcodes)
5706 - $content = wp_strip_all_tags($content);
5707 -
5708 - // Combine title, short description (if exists), and content
5709 - $final_content = $title . "\n\n";
5710 -
5711 - // Add short description if it exists (WooCommerce products use post_excerpt for short description)
5712 - if (!empty($excerpt)) {
5713 - $final_content .= "Short Description: " . wp_strip_all_tags($excerpt) . "\n\n";
5714 - }
5715 -
5716 - $final_content .= $content;
5717 -
5718 - // For WooCommerce products, include pricing and product details
5719 - if ($post_type === 'product' && class_exists('WooCommerce')) {
5720 - $product = wc_get_product($post_id);
5721 -
5722 - if ($product) {
5723 - // Get pricing information
5724 - $regular_price = $product->get_regular_price();
5725 - $sale_price = $product->get_sale_price();
5726 - $price = $product->get_price();
5727 - $sku = $product->get_sku();
5728 -
5729 - // Get currency symbol
5730 - $currency_symbol = get_woocommerce_currency_symbol();
5731 -
5732 - // Add pricing information
5733 - $final_content .= "\n";
5734 - if (!empty($regular_price)) {
5735 - $final_content .= "Price: " . $currency_symbol . $regular_price . "\n";
5736 - } elseif (!empty($price)) {
5737 - $final_content .= "Price: " . $currency_symbol . $price . "\n";
5738 - }
5739 -
5740 - if (!empty($sale_price) && $sale_price !== $regular_price) {
5741 - $final_content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
5742 - }
5743 -
5744 - // Handle variable products - show price range
5745 - if ($product->is_type('variable')) {
5746 - $min_price = $product->get_variation_price('min');
5747 - $max_price = $product->get_variation_price('max');
5748 - if ($min_price !== $max_price) {
5749 - $final_content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
5750 - }
5751 - }
5752 -
5753 - if (!empty($sku)) {
5754 - $final_content .= "SKU: " . $sku . "\n";
5755 - }
5756 -
5757 - // Get product categories
5758 - $categories = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
5759 - if (!empty($categories) && !is_wp_error($categories)) {
5760 - $final_content .= "Categories: " . implode(', ', $categories) . "\n";
5761 - }
3067 + // Get job type terms
3068 + $job_types = get_the_terms($post_id, 'job_listing_type');
3069 + if (!empty($job_types) && !is_wp_error($job_types)) {
3070 + $types = array();
3071 + foreach ($job_types as $type) {
3072 + $types[] = $type->name;
5762 3073 }
3074 + $final_content .= "\n\nJob Type: " . implode(', ', $types);
5763 3075 }
5764 -
5765 - // For custom post types like job_listing, include additional fields
5766 - if ($post_type === 'job_listing') {
5767 - // Add job-specific meta if available
5768 - $job_location = get_post_meta($post_id, '_job_location', true);
5769 - if (!empty($job_location)) {
5770 - $final_content .= "\n\nLocation: " . $job_location;
5771 - }
5772 -
5773 - // Get job type terms
5774 - $job_types = get_the_terms($post_id, 'job_listing_type');
5775 - if (!empty($job_types) && !is_wp_error($job_types)) {
5776 - $types = array();
5777 - foreach ($job_types as $type) {
5778 - $types[] = $type->name;
5779 - }
5780 - $final_content .= "\n\nJob Type: " . implode(', ', $types);
5781 - }
5782 -
5783 - // Get company name if available
5784 - $company_name = get_post_meta($post_id, '_company_name', true);
5785 - if (!empty($company_name)) {
5786 - $final_content .= "\n\nCompany: " . $company_name;
5787 - }
5788 - }
5789 -
5790 - // ADD ACF FIELDS SUPPORT (matches ajax_mxchat_process_selected_content behavior)
5791 - $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
5792 - if (!empty($acf_fields)) {
5793 - $acf_content_parts = array();
5794 - $pdf_attachment_ids = array();
5795 -
5796 - foreach ($acf_fields as $field_name => $field_value) {
5797 - $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
5798 - if (!empty($formatted_value)) {
5799 - // Convert field name to readable label
5800 - $field_label = ucwords(str_replace(['_', '-'], ' ', $field_name));
5801 - $acf_content_parts[] = $field_label . ": " . $formatted_value;
5802 - }
5803 -
5804 - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($field_value, $pdf_attachment_ids);
5805 - }
5806 -
5807 - if (!empty($acf_content_parts)) {
5808 - $final_content .= "\n\n" . implode("\n", $acf_content_parts);
5809 - }
5810 -
5811 - // Gate the auto-sync PDF-extraction loop behind an opt-in option.
5812 - // Mirrors the per-batch checkbox the manual content selector has; the
5813 - // 25 MB size cap lives in the shared extractor so it applies in both
5814 - // paths regardless. Default OFF — re-parsing every ACF PDF on every
5815 - // editor save is expensive and most sites don't want it.
5816 - $autosync_extract_acf_pdfs = get_option('mxchat_auto_sync_acf_pdfs', '0') === '1';
5817 - if ($autosync_extract_acf_pdfs && !empty($pdf_attachment_ids)) {
5818 - $pdf_attachment_ids = array_unique(array_filter(array_map('intval', $pdf_attachment_ids)));
5819 - $pdf_sections = array();
5820 - foreach ($pdf_attachment_ids as $att_id) {
5821 - $pdf_text = $this->mxchat_extract_pdf_text_by_attachment_id($att_id);
5822 - if (!empty($pdf_text)) {
5823 - $pdf_title = get_the_title($att_id);
5824 - $pdf_url = wp_get_attachment_url($att_id);
5825 - $header = 'PDF Attachment';
5826 - if (!empty($pdf_title)) {
5827 - $header .= ': ' . $pdf_title;
5828 - }
5829 - if (!empty($pdf_url)) {
5830 - $header .= ' (' . $pdf_url . ')';
5831 - }
5832 - $pdf_sections[] = $header . "\n" . $pdf_text;
5833 - }
5834 - }
5835 - if (!empty($pdf_sections)) {
5836 - $final_content .= "\n\nAttached PDFs:\n" . implode("\n\n", $pdf_sections);
5837 - }
5838 - }
5839 - }
5840 -
5841 - // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
5842 - $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
5843 - if (!empty($custom_meta)) {
5844 - $meta_content_parts = array();
5845 -
5846 - foreach ($custom_meta as $meta_key => $meta_value) {
5847 - // Convert meta key to readable label
5848 - $meta_label = ucwords(str_replace(array('_', '-'), ' ', $meta_key));
5849 - $meta_content_parts[] = $meta_label . ": " . $meta_value;
5850 - }
5851 -
5852 - if (!empty($meta_content_parts)) {
5853 - $final_content .= "\n\n" . implode("\n", $meta_content_parts);
5854 - }
5855 - }
5856 -
5857 - // Get API key with proper model detection
5858 - $options = get_option('mxchat_options');
5859 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
5860 3076
5861 - if (strpos($selected_model, 'voyage') === 0) {
5862 - $api_key = $options['voyage_api_key'] ?? '';
5863 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
5864 - $api_key = $options['gemini_api_key'] ?? '';
5865 - } else {
5866 - $api_key = $options['api_key'] ?? '';
3077 + // Get company name if available
3078 + $company_name = get_post_meta($post_id, '_company_name', true);
3079 + if (!empty($company_name)) {
3080 + $final_content .= "\n\nCompany: " . $company_name;
5867 3081 }
5868 -
5869 - if (empty($api_key)) {
5870 - return;
5871 - }
5872 -
5873 - // Use the centralized utility function for storage
5874 - $result = MxChat_Utils::submit_content_to_db(
5875 - $final_content,
5876 - $source_url,
5877 - $api_key,
5878 - md5($source_url) // Vector ID for Pinecone
5879 - );
5880 -
5881 - // After successful storage, apply role restriction based on tags
5882 - if (!is_wp_error($result)) {
5883 - $this->apply_role_restriction_to_post($post_id, $source_url);
5884 - }
5885 3082 }
5886 3083
5887 - // Clean up the stored previous status if not used above
5888 - if ($previous_status !== 'publish' || $post->post_status === 'publish') {
5889 - delete_transient($previous_status_key);
5890 - delete_transient($previous_url_key);
3084 + // Get the source URL
3085 + $source_url = get_permalink($post_id);
3086 +
3087 + // Get API key with proper model detection
3088 + $options = get_option('mxchat_options');
3089 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3090 +
3091 + if (strpos($selected_model, 'voyage') === 0) {
3092 + $api_key = $options['voyage_api_key'] ?? '';
3093 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3094 + $api_key = $options['gemini_api_key'] ?? '';
3095 + } else {
3096 + $api_key = $options['api_key'] ?? '';
5891 3097 }
5892 -}
5893 -
5894 -/**
5895 - * Store the post status and URL before update to detect status transitions
5896 - * This runs before the post is actually updated in the database
5897 - */
5898 -public function mxchat_store_pre_update_status($post_id, $data) {
5899 - // Get the current post from database (before update)
5900 - $current_post = get_post($post_id);
5901 3098
5902 - if ($current_post) {
5903 - // Store the current status temporarily
5904 - $status_key = 'mxchat_prev_status_' . $post_id;
5905 - set_transient($status_key, $current_post->post_status, HOUR_IN_SECONDS);
5906 -
5907 - // If the post is currently published, also store its URL
5908 - if ($current_post->post_status === 'publish') {
5909 - $url_key = 'mxchat_prev_url_' . $post_id;
5910 - $current_url = get_permalink($post_id);
5911 - set_transient($url_key, $current_url, HOUR_IN_SECONDS);
5912 - }
3099 + if (empty($api_key)) {
3100 + //error_log('MxChat Auto-sync: No API key configured for embedding model');
3101 + return;
5913 3102 }
3103 +
3104 + // Use the centralized utility function for storage
3105 + $result = MxChat_Utils::submit_content_to_db(
3106 + $final_content,
3107 + $source_url,
3108 + $api_key,
3109 + md5($source_url) // Vector ID for Pinecone
3110 + );
3111 +
3112 + if (is_wp_error($result)) {
3113 + //error_log('MxChat Auto-sync failed for post ' . $post_id . ': ' . $result->get_error_message());
3114 + }
5914 3115 }
5915 3116
3117 +
5916 3118 public function mxchat_handle_post_delete($post_id) {
5917 3119 // Get post data before it's deleted
5918 3120 $post = get_post($post_id);
5919 3121
@@ -5943,51 +3145,85 @@
5943 3145 if (!$should_sync) {
5944 3146 return;
5945 3147 }
5946 3148
5947 - // Resolve the pre-trash URL. wp_trash_post renames the slug with "__trashed" before firing
5948 - // this hook, so get_permalink() here would return the trashed URL and md5() would miss the
5949 - // real vector IDs stored under the original URL.
5950 - $source_url = $this->mxchat_resolve_pre_trash_url($post_id);
3149 + // Get the URL before post is deleted
3150 + $source_url = get_permalink($post_id);
5951 3151 if (!$source_url) {
5952 - //error_log('MXChat: Failed to resolve source URL for post ' . $post_id);
3152 + //error_log('MXChat: Failed to get permalink for post ' . $post_id);
5953 3153 return;
5954 3154 }
5955 3155
5956 - // Use chunk-aware deletion (handles both chunked and non-chunked content)
5957 - $delete_result = MxChat_Utils::delete_chunks_for_url($source_url, 'default');
3156 + // Check if Pinecone is enabled
3157 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
3158 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5958 3159
5959 - if (is_wp_error($delete_result)) {
5960 - //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message());
3160 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3161 + // Delete from Pinecone
3162 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
3163 + } else {
3164 + // Delete from WordPress DB
3165 + global $wpdb;
3166 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3167 +
3168 + $result = $wpdb->delete(
3169 + $table_name,
3170 + array('source_url' => $source_url),
3171 + array('%s')
3172 + );
3173 +
3174 + if ($result === false) {
3175 + //error_log('MXChat: WordPress DB deletion failed for URL: ' . $source_url);
3176 + }
5961 3177 }
3178 +}
5962 3179
5963 - delete_transient('mxchat_prev_url_' . $post_id);
5964 - delete_transient('mxchat_prev_status_' . $post_id);
5965 -}
5966 3180
5967 -/**
5968 - * Resolve the source URL for a post being trashed/deleted.
5969 - *
5970 - * Why: wp_trash_post appends "__trashed" to the slug before the wp_trash_post action fires, so
5971 - * get_permalink() returns a URL whose md5() won't match the vector IDs stored in Pinecone or
5972 - * the source_url rows in the WP DB. Prefer the URL captured by mxchat_store_pre_update_status
5973 - * (runs on pre_post_update, before the rename); fall back to stripping the __trashed suffix.
5974 - */
5975 -private function mxchat_resolve_pre_trash_url($post_id) {
5976 - $previous_url = get_transient('mxchat_prev_url_' . $post_id);
5977 - if (!empty($previous_url)) {
5978 - return $previous_url;
5979 - }
3181 + /**
3182 + * Deletes data from Pinecone using a source URL
3183 + */
3184 + public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
3185 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
3186 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
5980 3187
5981 - $current = get_permalink($post_id);
5982 - if (!$current) {
5983 - return '';
5984 - }
5985 - return preg_replace('#__trashed(/?)$#', '$1', $current);
5986 -}
3188 + if (empty($host) || empty($api_key)) {
3189 + //error_log('MXChat: Pinecone deletion failed - missing configuration');
3190 + return false;
3191 + }
5987 3192
3193 + $api_endpoint = "https://{$host}/vectors/delete";
3194 + $vector_id = md5($source_url);
5988 3195
3196 + $request_body = array(
3197 + 'ids' => array($vector_id)
3198 + );
5989 3199
3200 + $response = wp_remote_post($api_endpoint, array(
3201 + 'headers' => array(
3202 + 'Api-Key' => $api_key,
3203 + 'accept' => 'application/json',
3204 + 'content-type' => 'application/json'
3205 + ),
3206 + 'body' => wp_json_encode($request_body),
3207 + 'timeout' => 30
3208 + ));
3209 +
3210 + if (is_wp_error($response)) {
3211 + //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
3212 + return false;
3213 + }
3214 +
3215 + $response_code = wp_remote_retrieve_response_code($response);
3216 + if ($response_code !== 200) {
3217 + //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
3218 + return false;
3219 + }
3220 +
3221 + return true;
3222 + }
3223 +
3224 +
3225 +
5990 3226 public function mxchat_handle_product_change($post_id, $post, $update) {
5991 3227 if ($post->post_type !== 'product') {
5992 3228 return;
5993 3229 }
@@ -6009,12 +3245,11 @@
6009 3245 if (!isset($this->options['enable_woocommerce_integration']) ||
6010 3246 !in_array($this->options['enable_woocommerce_integration'], ['1', 'on'])) {
6011 3247 return;
6012 3248 }
6013 -
3249 +
6014 3250 $source_url = get_permalink($product->get_id());
6015 - $product_id = $product->get_id();
6016 -
3251 +
6017 3252 // Build product content
6018 3253 $title = $product->get_name();
6019 3254 $description = $product->get_description();
6020 3255 $short_description = $product->get_short_description();
@@ -6019,92 +3254,35 @@
6019 3254 $description = $product->get_description();
6020 3255 $short_description = $product->get_short_description();
6021 3256 $regular_price = $product->get_regular_price();
6022 3257 $sale_price = $product->get_sale_price();
6023 - $price = $product->get_price();
6024 3258 $sku = $product->get_sku();
6025 -
6026 - // Get currency symbol
6027 - $currency_symbol = get_woocommerce_currency_symbol();
6028 -
3259 +
6029 3260 // Format content consistently
6030 3261 $content = $title . "\n\n";
6031 -
6032 - if (!empty($short_description)) {
6033 - $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
6034 - }
6035 -
3262 +
6036 3263 if (!empty($description)) {
6037 3264 $content .= wp_strip_all_tags($description) . "\n\n";
6038 3265 }
6039 -
6040 - // Add pricing information
6041 - if (!empty($regular_price)) {
6042 - $content .= "Price: " . $currency_symbol . $regular_price . "\n";
6043 - } elseif (!empty($price)) {
6044 - $content .= "Price: " . $currency_symbol . $price . "\n";
3266 +
3267 + if (!empty($short_description)) {
3268 + $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
6045 3269 }
6046 -
6047 - if (!empty($sale_price) && $sale_price !== $regular_price) {
6048 - $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
3270 +
3271 + $content .= "Price: $" . $regular_price . "\n";
3272 +
3273 + if (!empty($sale_price)) {
3274 + $content .= "Sale Price: $" . $sale_price . "\n";
6049 3275 }
6050 -
6051 - // Handle variable products - show price range
6052 - if ($product->is_type('variable')) {
6053 - $min_price = $product->get_variation_price('min');
6054 - $max_price = $product->get_variation_price('max');
6055 - if ($min_price !== $max_price) {
6056 - $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
6057 - }
6058 - }
6059 -
3276 +
6060 3277 if (!empty($sku)) {
6061 3278 $content .= "SKU: " . $sku . "\n";
6062 3279 }
6063 -
6064 - // Get product categories
6065 - $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
6066 - if (!empty($categories) && !is_wp_error($categories)) {
6067 - $content .= "Categories: " . implode(', ', $categories) . "\n";
6068 - }
6069 -
6070 - // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
6071 - $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
6072 - if (!empty($custom_tabs) && is_array($custom_tabs)) {
6073 - foreach ($custom_tabs as $tab) {
6074 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
6075 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
6076 -
6077 - if (!empty($tab_title) && !empty($tab_content)) {
6078 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
6079 - }
6080 - }
6081 - }
6082 -
6083 - // Also check for reusable/saved tabs applied to this product
6084 - $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
6085 - if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
6086 - // Get the saved tabs option
6087 - $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
6088 - if (!empty($saved_tabs) && is_array($saved_tabs)) {
6089 - foreach ($applied_saved_tabs as $saved_tab_id) {
6090 - if (isset($saved_tabs[$saved_tab_id])) {
6091 - $tab = $saved_tabs[$saved_tab_id];
6092 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
6093 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
6094 -
6095 - if (!empty($tab_title) && !empty($tab_content)) {
6096 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
6097 - }
6098 - }
6099 - }
6100 - }
6101 - }
6102 -
3280 +
6103 3281 // Get API key with proper model detection
6104 3282 $options = get_option('mxchat_options');
6105 3283 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
6106 -
3284 +
6107 3285 if (strpos($selected_model, 'voyage') === 0) {
6108 3286 $api_key = $options['voyage_api_key'] ?? '';
6109 3287 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
6110 3288 $api_key = $options['gemini_api_key'] ?? '';
@@ -6110,14 +3288,14 @@
6110 3288 $api_key = $options['gemini_api_key'] ?? '';
6111 3289 } else {
6112 3290 $api_key = $options['api_key'] ?? '';
6113 3291 }
6114 -
3292 +
6115 3293 if (empty($api_key)) {
6116 3294 //error_log('MxChat Auto-sync: No API key configured for embedding model');
6117 3295 return;
6118 3296 }
6119 -
3297 +
6120 3298 // Use the centralized utility function for storage
6121 3299 $result = MxChat_Utils::submit_content_to_db(
6122 3300 $content,
6123 3301 $source_url,
@@ -6123,16 +3301,11 @@
6123 3301 $source_url,
6124 3302 $api_key,
6125 3303 md5($source_url) // Vector ID for Pinecone
6126 3304 );
6127 -
6128 - // After successful storage, apply role restriction based on tags
6129 - if (!is_wp_error($result)) {
6130 - $this->apply_role_restriction_to_post($product_id, $source_url);
6131 - }
6132 -
3305 +
6133 3306 if (is_wp_error($result)) {
6134 - //error_log('MxChat WooCommerce sync failed for product ' . $product_id . ': ' . $result->get_error_message());
3307 + //error_log('MxChat WooCommerce sync failed for product ' . $product->get_id() . ': ' . $result->get_error_message());
6135 3308 }
6136 3309 }
6137 3310
6138 3311 public function mxchat_handle_product_delete($post_id) {
@@ -6139,1123 +3312,30 @@
6139 3312 if (get_post_type($post_id) !== 'product') {
6140 3313 return;
6141 3314 }
6142 3315
6143 - $source_url = $this->mxchat_resolve_pre_trash_url($post_id);
6144 - if (!$source_url) {
6145 - return;
6146 - }
3316 + $source_url = get_permalink($post_id);
6147 3317
6148 - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks)
6149 - MxChat_Utils::delete_chunks_for_url($source_url, 'default');
6150 -
6151 - delete_transient('mxchat_prev_url_' . $post_id);
6152 - delete_transient('mxchat_prev_status_' . $post_id);
6153 -}
6154 -
6155 -/**
6156 - * Handle individual Pinecone content deletion
6157 - */
6158 -public function mxchat_handle_pinecone_prompt_delete() {
6159 - // Check permissions
6160 - if (!current_user_can('manage_options')) {
6161 - wp_die(esc_html__('You do not have sufficient permissions.', 'mxchat'));
6162 - }
6163 -
6164 - // Verify nonce
6165 - if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'mxchat_delete_pinecone_prompt_nonce')) {
6166 - wp_die(esc_html__('Security check failed.', 'mxchat'));
6167 - }
6168 -
6169 - $vector_id = isset($_GET['vector_id']) ? sanitize_text_field($_GET['vector_id']) : '';
6170 -
6171 - if (empty($vector_id)) {
6172 - set_transient('mxchat_admin_notice_error',
6173 - esc_html__('Invalid vector ID.', 'mxchat'),
6174 - 30
6175 - );
6176 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
6177 - exit;
6178 - }
6179 -
6180 - // Get Pinecone settings
3318 + // Check if Pinecone is enabled
6181 3319 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
6182 3320 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6183 -
6184 - if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
6185 - set_transient('mxchat_admin_notice_error',
6186 - esc_html__('Pinecone is not properly configured.', 'mxchat'),
6187 - 30
6188 - );
6189 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
6190 - exit;
6191 - }
6192 -
6193 - // Delete from Pinecone
6194 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
6195 - $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
6196 - $vector_id,
6197 - $pinecone_options['mxchat_pinecone_api_key'],
6198 - $pinecone_options['mxchat_pinecone_host']
6199 - );
6200 -
6201 - if ($result['success']) {
6202 - // No cache clearing needed since we removed caching
6203 - set_transient('mxchat_admin_notice_success',
6204 - esc_html__('Entry deleted successfully from Pinecone.', 'mxchat'),
6205 - 30
6206 - );
6207 - } else {
6208 - set_transient('mxchat_admin_notice_error',
6209 - esc_html__('Failed to delete entry: ', 'mxchat') . $result['message'],
6210 - 30
6211 - );
6212 - }
6213 -
6214 - wp_safe_redirect(admin_url('admin.php?page=mxchat-prompts'));
6215 - exit;
6216 -}
6217 -/**
6218 - * Handle individual Pinecone content deletion via AJAX
6219 - */
6220 -public function ajax_mxchat_delete_pinecone_prompt() {
6221 - // Verify nonce and permissions
6222 - if (!check_ajax_referer('mxchat_delete_pinecone_prompt_nonce', 'nonce', false)) {
6223 - wp_send_json_error('Invalid nonce');
6224 - exit;
6225 - }
6226 -
6227 - if (!current_user_can('manage_options')) {
6228 - wp_send_json_error('Unauthorized access');
6229 - exit;
6230 - }
6231 -
6232 - $vector_id = isset($_POST['vector_id']) ? sanitize_text_field($_POST['vector_id']) : '';
6233 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
6234 -
6235 - if (empty($vector_id)) {
6236 - wp_send_json_error('Missing vector ID');
6237 - exit;
6238 - }
6239 -
6240 - // Get bot-specific Pinecone settings
6241 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
6242 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
6243 -
6244 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6245 -
6246 - if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
6247 - wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
6248 - exit;
6249 - }
6250 -
6251 - // Delete from the correct Pinecone index
6252 - $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
6253 - $vector_id,
6254 - $pinecone_options['mxchat_pinecone_api_key'],
6255 - $pinecone_options['mxchat_pinecone_host']
6256 - );
6257 -
6258 - if ($result['success']) {
6259 - // No cache clearing needed since we removed caching
6260 - wp_send_json_success(array(
6261 - 'message' => 'Entry deleted successfully from Pinecone',
6262 - 'vector_id' => $vector_id,
6263 - 'bot_id' => $bot_id
6264 - ));
6265 - } else {
6266 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Failed to delete from Pinecone: ' . $result['message'], array('vector_id' => $vector_id, 'bot_id' => $bot_id));
6267 - wp_send_json_error('Failed to delete from Pinecone: ' . $result['message']);
6268 - }
6269 -
6270 - exit;
6271 -}
6272 3321
6273 -/**
6274 - * Handle deletion of all chunks for a given source URL via AJAX
6275 - * Follows the same pattern as ajax_mxchat_delete_pinecone_prompt
6276 - */
6277 -public function ajax_mxchat_delete_chunks_by_url() {
6278 - // Verify nonce and permissions
6279 - if (!check_ajax_referer('mxchat_delete_chunks_nonce', 'nonce', false)) {
6280 - wp_send_json_error('Invalid nonce');
6281 - exit;
6282 - }
6283 -
6284 - if (!current_user_can('manage_options')) {
6285 - wp_send_json_error('Unauthorized access');
6286 - exit;
6287 - }
6288 -
6289 - $source_url = isset($_POST['source_url']) ? esc_url_raw($_POST['source_url']) : '';
6290 - $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
6291 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
6292 -
6293 - if (empty($source_url)) {
6294 - wp_send_json_error('Missing source URL');
6295 - exit;
6296 - }
6297 -
6298 - // Generate the base vector ID from the source URL (same as how chunks are created)
6299 - $base_vector_id = md5($source_url);
6300 -
6301 - if ($data_source === 'pinecone') {
6302 - // Get bot-specific Pinecone settings (same as working delete function)
6303 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
6304 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
6305 -
6306 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6307 -
6308 - if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) {
6309 - wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id);
6310 - exit;
6311 - }
6312 -
6313 - $api_key = $pinecone_options['mxchat_pinecone_api_key'];
6314 - $host = $pinecone_options['mxchat_pinecone_host'];
6315 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
6316 -
6317 - // Collect all vector IDs to delete
6318 - $vectors_to_delete = array();
6319 -
6320 - // Add the original single-vector ID (for non-chunked content)
6321 - $vectors_to_delete[] = $base_vector_id;
6322 -
6323 - // Use Pinecone list API to find all chunk vectors with this prefix
6324 - // NOTE: Pinecone List API is a GET request with query parameters, not POST
6325 - $prefix = $base_vector_id . '_chunk_';
6326 -
6327 - $query_params = array(
6328 - 'prefix' => $prefix,
6329 - 'limit' => 100
6330 - );
6331 -
6332 - if (!empty($namespace)) {
6333 - $query_params['namespace'] = $namespace;
6334 - }
6335 -
6336 - $list_url = "https://{$host}/vectors/list?" . http_build_query($query_params);
6337 -
6338 - $list_response = wp_remote_get($list_url, array(
6339 - 'headers' => array(
6340 - 'Api-Key' => $api_key,
6341 - 'accept' => 'application/json'
6342 - ),
6343 - 'timeout' => 30
6344 - ));
6345 -
6346 - if (!is_wp_error($list_response)) {
6347 - $list_body_response = wp_remote_retrieve_body($list_response);
6348 - $list_data = json_decode($list_body_response, true);
6349 - if (!empty($list_data['vectors'])) {
6350 - foreach ($list_data['vectors'] as $vector) {
6351 - if (isset($vector['id'])) {
6352 - $vectors_to_delete[] = $vector['id'];
6353 - }
6354 - }
6355 - }
6356 - }
6357 -
6358 - if (empty($vectors_to_delete)) {
6359 - wp_send_json_success(array(
6360 - 'message' => 'No vectors found to delete',
6361 - 'source_url' => $source_url
6362 - ));
6363 - exit;
6364 - }
6365 -
6366 - // Delete all vectors using the same endpoint as the working function
6367 - $delete_url = "https://{$host}/vectors/delete";
6368 -
6369 - $delete_body = array(
6370 - 'ids' => $vectors_to_delete
6371 - );
6372 -
6373 - if (!empty($namespace)) {
6374 - $delete_body['namespace'] = $namespace;
6375 - }
6376 -
6377 - $delete_response = wp_remote_post($delete_url, array(
6378 - 'headers' => array(
6379 - 'Api-Key' => $api_key,
6380 - 'accept' => 'application/json',
6381 - 'content-type' => 'application/json'
6382 - ),
6383 - 'body' => wp_json_encode($delete_body),
6384 - 'timeout' => 30
6385 - ));
6386 -
6387 - if (is_wp_error($delete_response)) {
6388 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Failed to delete chunks from Pinecone: ' . $delete_response->get_error_message(), array('source_url' => $source_url));
6389 - wp_send_json_error('Failed to delete from Pinecone: ' . $delete_response->get_error_message());
6390 - exit;
6391 - }
6392 -
6393 - $response_code = wp_remote_retrieve_response_code($delete_response);
6394 -
6395 - if ($response_code !== 200) {
6396 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Pinecone API error (HTTP ' . $response_code . ')', array('source_url' => $source_url));
6397 - wp_send_json_error('Pinecone API error (HTTP ' . $response_code . ')');
6398 - exit;
6399 - }
6400 -
6401 - wp_send_json_success(array(
6402 - 'message' => 'All chunks deleted successfully from Pinecone',
6403 - 'source_url' => $source_url,
6404 - 'deleted_count' => count($vectors_to_delete)
6405 - ));
6406 -
3322 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
3323 + // Delete from Pinecone
3324 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
6407 3325 } else {
6408 - // WordPress database deletion
3326 + // Delete from WordPress DB
6409 3327 global $wpdb;
6410 3328 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6411 -
6412 - $result = $wpdb->delete(
6413 - $table_name,
6414 - array('source_url' => $source_url),
6415 - array('%s')
6416 - );
6417 -
6418 - if ($result === false) {
6419 - MxChat_Admin::mxchat_log_debug('database_error', 'Failed to delete from database: ' . $wpdb->last_error, array('source_url' => $source_url));
6420 - wp_send_json_error('Failed to delete from database: ' . $wpdb->last_error);
6421 - exit;
6422 - }
6423 -
6424 - wp_send_json_success(array(
6425 - 'message' => 'All chunks deleted successfully from database',
6426 - 'source_url' => $source_url,
6427 - 'deleted_count' => $result
6428 - ));
6429 - }
6430 -
6431 - exit;
6432 -}
6433 -
6434 -/**
6435 - * Handle individual WordPress database content deletion via AJAX
6436 - * Mirrors the Pinecone delete handler but for WordPress database entries
6437 - */
6438 -public function ajax_mxchat_delete_wordpress_prompt() {
6439 - // Verify nonce and permissions
6440 - if (!check_ajax_referer('mxchat_delete_wordpress_prompt_nonce', 'nonce', false)) {
6441 - wp_send_json_error('Invalid nonce');
6442 - exit;
6443 - }
6444 -
6445 - if (!current_user_can('manage_options')) {
6446 - wp_send_json_error('Unauthorized access');
6447 - exit;
6448 - }
6449 -
6450 - $entry_id = isset($_POST['entry_id']) ? intval($_POST['entry_id']) : 0;
6451 -
6452 - if (empty($entry_id)) {
6453 - wp_send_json_error('Missing entry ID');
6454 - exit;
6455 - }
6456 -
6457 - global $wpdb;
6458 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6459 -
6460 - // Clear cache for this entry
6461 - wp_cache_delete('prompt_' . $entry_id, 'mxchat_prompts');
6462 -
6463 - // Delete from database
6464 - $result = $wpdb->delete(
6465 - $table_name,
6466 - array('id' => $entry_id),
6467 - array('%d')
6468 - );
6469 -
6470 - if ($result !== false) {
6471 - wp_send_json_success(array(
6472 - 'message' => 'Entry deleted successfully',
6473 - 'entry_id' => $entry_id
6474 - ));
6475 - } else {
6476 - wp_send_json_error('Failed to delete entry from database');
6477 - }
6478 -
6479 - exit;
6480 -}
6481 -
6482 -/**
6483 - * Handle bulk deletion of knowledge entries via AJAX
6484 - * Supports both Pinecone and WordPress database entries
6485 - */
6486 -public function ajax_mxchat_bulk_delete_knowledge() {
6487 - // Verify nonce and permissions
6488 - if (!check_ajax_referer('mxchat_bulk_delete_knowledge_nonce', 'nonce', false)) {
6489 - wp_send_json_error('Invalid nonce');
6490 - exit;
6491 - }
6492 -
6493 - if (!current_user_can('manage_options')) {
6494 - wp_send_json_error('Unauthorized access');
6495 - exit;
6496 - }
6497 -
6498 - $entries = isset($_POST['entries']) ? $_POST['entries'] : array();
6499 - $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
6500 -
6501 - if (empty($entries) || !is_array($entries)) {
6502 - wp_send_json_error('No entries provided');
6503 - exit;
6504 - }
6505 -
6506 - // Extend execution time — bulk Pinecone operations can take a while
6507 - if (function_exists('set_time_limit')) {
6508 - set_time_limit(120);
6509 - }
6510 -
6511 - $success_ids = array();
6512 - $failed_ids = array();
6513 - $errors = array();
6514 -
6515 - global $wpdb;
6516 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6517 -
6518 - // Get Pinecone manager for Pinecone deletions
6519 - $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
6520 - $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
6521 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
6522 -
6523 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
6524 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
6525 -
6526 - // =============================================
6527 - // PHASE 1: Collect all Pinecone vector IDs
6528 - // and separate WordPress entries
6529 - // =============================================
6530 - $pinecone_entry_ids = array(); // entry IDs that are pinecone-sourced
6531 - $wordpress_entries = array(); // entries for WordPress DB deletion
6532 - $all_vector_ids = array(); // all pinecone vector IDs to delete in one batch
6533 -
6534 - foreach ($entries as $entry) {
6535 - $entry_id = sanitize_text_field($entry['id'] ?? '');
6536 - $source = sanitize_text_field($entry['source'] ?? 'wordpress');
6537 - $source_url = isset($entry['sourceUrl']) ? esc_url_raw($entry['sourceUrl']) : '';
6538 - $is_group = isset($entry['isGroup']) && ($entry['isGroup'] === true || $entry['isGroup'] === 'true');
6539 -
6540 - if (empty($entry_id)) {
6541 - continue;
6542 - }
6543 -
6544 - if ($source === 'pinecone') {
6545 - if (!$use_pinecone || empty($api_key)) {
6546 - $failed_ids[] = $entry_id;
6547 - $errors[] = "Pinecone not configured for entry: $entry_id";
6548 - continue;
6549 - }
6550 -
6551 - $pinecone_entry_ids[] = $entry_id;
6552 -
6553 - if ($is_group && !empty($source_url)) {
6554 - // Grouped/chunked entry: collect base ID + chunk IDs via List API
6555 - $base_vector_id = md5($source_url);
6556 - $all_vector_ids[] = $base_vector_id;
6557 -
6558 - $list_url = 'https://' . $host . '/vectors/list?prefix=' . urlencode($base_vector_id . '_chunk_') . '&limit=100';
6559 - $list_response = wp_remote_get($list_url, array(
6560 - 'headers' => array(
6561 - 'Api-Key' => $api_key,
6562 - 'accept' => 'application/json'
6563 - ),
6564 - 'timeout' => 30
6565 - ));
6566 -
6567 - if (!is_wp_error($list_response)) {
6568 - $list_body = json_decode(wp_remote_retrieve_body($list_response), true);
6569 - if (!empty($list_body['vectors']) && is_array($list_body['vectors'])) {
6570 - foreach ($list_body['vectors'] as $vector) {
6571 - if (isset($vector['id'])) {
6572 - $all_vector_ids[] = $vector['id'];
6573 - }
6574 - }
6575 - }
6576 - }
6577 - } else {
6578 - // Single entry: the entry_id IS the vector ID
6579 - $all_vector_ids[] = $entry_id;
6580 - }
6581 - } else {
6582 - $wordpress_entries[] = $entry;
6583 - }
6584 - }
6585 -
6586 - // =============================================
6587 - // PHASE 2: Single batch delete to Pinecone
6588 - // =============================================
6589 - if (!empty($all_vector_ids)) {
6590 - $all_vector_ids = array_values(array_unique($all_vector_ids));
6591 - $pinecone_success = true;
6592 - $batches = array_chunk($all_vector_ids, 100);
6593 -
6594 - foreach ($batches as $batch) {
6595 - $delete_response = wp_remote_post("https://{$host}/vectors/delete", array(
6596 - 'headers' => array(
6597 - 'Api-Key' => $api_key,
6598 - 'accept' => 'application/json',
6599 - 'content-type' => 'application/json'
6600 - ),
6601 - 'body' => wp_json_encode(array('ids' => $batch)),
6602 - 'timeout' => 60
6603 - ));
6604 -
6605 - if (is_wp_error($delete_response)) {
6606 - $pinecone_success = false;
6607 - $errors[] = 'Pinecone batch deletion failed: ' . $delete_response->get_error_message();
6608 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Bulk delete batch failed: ' . $delete_response->get_error_message());
6609 - } else {
6610 - $response_code = wp_remote_retrieve_response_code($delete_response);
6611 - if ($response_code !== 200) {
6612 - $pinecone_success = false;
6613 - $response_body = wp_remote_retrieve_body($delete_response);
6614 - $errors[] = "Pinecone API error (HTTP $response_code)";
6615 - MxChat_Admin::mxchat_log_debug('pinecone_error', 'Bulk delete batch failed (HTTP ' . $response_code . ')', array('response' => substr($response_body, 0, 200)));
6616 - }
6617 - }
6618 - }
6619 -
6620 - // Mark all pinecone entries based on batch result
6621 - foreach ($pinecone_entry_ids as $eid) {
6622 - if ($pinecone_success) {
6623 - $success_ids[] = $eid;
6624 - } else {
6625 - $failed_ids[] = $eid;
6626 - }
6627 - }
6628 - }
6629 -
6630 - // =============================================
6631 - // PHASE 3: WordPress database deletions
6632 - // =============================================
6633 - foreach ($wordpress_entries as $entry) {
6634 - $entry_id = sanitize_text_field($entry['id'] ?? '');
6635 - $source_url = isset($entry['sourceUrl']) ? esc_url_raw($entry['sourceUrl']) : '';
6636 - $is_group = isset($entry['isGroup']) && ($entry['isGroup'] === true || $entry['isGroup'] === 'true');
6637 -
6638 - if (empty($entry_id)) {
6639 - continue;
6640 - }
6641 -
6642 - try {
6643 - if ($is_group && !empty($source_url)) {
6644 - $result = $wpdb->delete(
6645 - $table_name,
6646 - array('source_url' => $source_url),
6647 - array('%s')
6648 - );
6649 - } else {
6650 - wp_cache_delete('prompt_' . $entry_id, 'mxchat_prompts');
6651 - $result = $wpdb->delete(
6652 - $table_name,
6653 - array('id' => intval($entry_id)),
6654 - array('%d')
6655 - );
6656 - }
6657 -
6658 - if ($result !== false) {
6659 - $success_ids[] = $entry_id;
6660 - } else {
6661 - $failed_ids[] = $entry_id;
6662 - $errors[] = "Database error for entry: $entry_id";
6663 - }
6664 - } catch (Exception $e) {
6665 - $failed_ids[] = $entry_id;
6666 - $errors[] = $e->getMessage();
6667 - }
6668 - }
6669 -
6670 - wp_send_json_success(array(
6671 - 'success_ids' => $success_ids,
6672 - 'failed_ids' => $failed_ids,
6673 - 'errors' => $errors,
6674 - 'total_processed' => count($success_ids) + count($failed_ids)
6675 - ));
6676 -
6677 - exit;
6678 -}
6679 -
6680 -/**
6681 - * Get hierarchical roles for dropdown
6682 - */
6683 -public function mxchat_get_role_options() {
6684 - return array(
6685 - 'public' => __('Public (Everyone)', 'mxchat'),
6686 - 'logged_in' => __('Logged In Users', 'mxchat'),
6687 - 'subscriber' => __('Subscribers & Above', 'mxchat'),
6688 - 'contributor' => __('Contributors & Above', 'mxchat'),
6689 - 'author' => __('Authors & Above', 'mxchat'),
6690 - 'editor' => __('Editors & Above', 'mxchat'),
6691 - 'administrator' => __('Administrators Only', 'mxchat')
6692 - );
6693 -}
6694 -
6695 -/**
6696 - * Check if user has access to content based on role restriction
6697 - */
6698 -public function mxchat_user_has_content_access($role_restriction) {
6699 - // Public content is always accessible
6700 - if ($role_restriction === 'public' || empty($role_restriction)) {
6701 - return true;
6702 - }
6703 -
6704 - // Check if user is logged in for logged_in restriction
6705 - if ($role_restriction === 'logged_in') {
6706 - return is_user_logged_in();
6707 - }
6708 -
6709 - // If not logged in, no access to role-restricted content
6710 - if (!is_user_logged_in()) {
6711 - return false;
6712 - }
6713 -
6714 - $user = wp_get_current_user();
6715 - $user_roles = $user->roles;
6716 -
6717 - if (empty($user_roles)) {
6718 - return false;
6719 - }
6720 -
6721 - // Define role hierarchy (higher number = higher access)
6722 - $hierarchy = array(
6723 - 'subscriber' => 1,
6724 - 'contributor' => 2,
6725 - 'author' => 3,
6726 - 'editor' => 4,
6727 - 'administrator' => 5
6728 - );
6729 -
6730 - // Get required level
6731 - $required_level = isset($hierarchy[$role_restriction]) ? $hierarchy[$role_restriction] : 0;
6732 -
6733 - // Check if user has required level or higher
6734 - foreach ($user_roles as $user_role) {
6735 - $user_level = isset($hierarchy[$user_role]) ? $hierarchy[$user_role] : 0;
6736 - if ($user_level >= $required_level) {
6737 - return true;
6738 - }
6739 - }
6740 -
6741 - return false;
6742 -}
6743 -
6744 -/**
6745 - * Handle role restriction updates via AJAX
6746 - * Removed cache clearing call since we removed caching
6747 - */
6748 -public function ajax_mxchat_update_role_restriction() {
6749 - // Verify nonce and permissions
6750 - if (!check_ajax_referer('mxchat_update_role_nonce', 'nonce', false)) {
6751 - wp_send_json_error('Invalid nonce');
6752 - exit;
6753 - }
6754 -
6755 - if (!current_user_can('manage_options')) {
6756 - wp_send_json_error('Unauthorized access');
6757 - exit;
6758 - }
6759 -
6760 - $entry_id = isset($_POST['entry_id']) ? sanitize_text_field($_POST['entry_id']) : '';
6761 - $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
6762 - $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress';
6763 -
6764 - if (empty($entry_id)) {
6765 - wp_send_json_error('Invalid entry ID');
6766 - exit;
6767 - }
6768 -
6769 - // Get knowledge manager instance to validate role restriction
6770 - $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
6771 - $valid_roles = array_keys($knowledge_manager->mxchat_get_role_options());
6772 - if (!in_array($role_restriction, $valid_roles)) {
6773 - wp_send_json_error('Invalid role restriction');
6774 - exit;
6775 - }
6776 -
6777 - global $wpdb;
6778 -
6779 - if ($data_source === 'pinecone') {
6780 - // Handle Pinecone role restriction (stored separately in WordPress table)
6781 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6782 3329
6783 - // Use REPLACE to insert or update the role restriction
6784 - $result = $wpdb->replace(
6785 - $roles_table,
6786 - array(
6787 - 'vector_id' => $entry_id,
6788 - 'role_restriction' => $role_restriction,
6789 - 'updated_at' => current_time('mysql')
6790 - ),
6791 - array('%s', '%s', '%s')
6792 - );
6793 -
6794 - // No cache clearing needed since we removed caching
6795 -
6796 - } else {
6797 - // Handle WordPress database role restriction (existing functionality)
6798 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6799 -
6800 - $result = $wpdb->update(
3330 + $wpdb->delete(
6801 3331 $table_name,
6802 - array('role_restriction' => $role_restriction),
6803 - array('id' => absint($entry_id)),
6804 - array('%s'),
6805 - array('%d')
6806 - );
6807 - }
6808 -
6809 - if ($result === false) {
6810 - wp_send_json_error('Database update failed: ' . $wpdb->last_error);
6811 - exit;
6812 - }
6813 -
6814 - wp_send_json_success(array(
6815 - 'message' => 'Role restriction updated successfully',
6816 - 'role_restriction' => $role_restriction,
6817 - 'data_source' => $data_source,
6818 - 'entry_id' => $entry_id
6819 - ));
6820 - exit;
6821 -}
6822 -
6823 -// ========================================
6824 -// ROLE-BASED CONTENT RESTRICTIONS - NEW FUNCTIONS
6825 -// Add these to your MxChat_Knowledge_Manager class
6826 -// ========================================
6827 -
6828 -/**
6829 - * Initialize role-based content hooks
6830 - * Add this call to your __construct() or mxchat_init_hooks() method
6831 - */
6832 -private function mxchat_init_role_hooks() {
6833 - // AJAX handlers for tag-role mappings
6834 - add_action('wp_ajax_mxchat_add_tag_role_mapping', array($this, 'ajax_add_tag_role_mapping'));
6835 - add_action('wp_ajax_mxchat_delete_tag_role_mapping', array($this, 'ajax_delete_tag_role_mapping'));
6836 - add_action('wp_ajax_mxchat_get_tag_role_mappings', array($this, 'ajax_get_tag_role_mappings'));
6837 - add_action('wp_ajax_mxchat_bulk_update_tag_roles', array($this, 'ajax_bulk_update_tag_roles'));
6838 -
6839 - // Hook to automatically update role restrictions when tags are added/removed
6840 - add_action('set_object_terms', array($this, 'handle_tag_change'), 10, 6);
6841 -
6842 - // Hook to apply role restrictions on auto-sync
6843 - add_action('mxchat_content_stored', array($this, 'apply_role_restriction_after_storage'), 10, 2);
6844 -}
6845 -
6846 -/**
6847 - * Add tag-role mapping via AJAX
6848 - */
6849 -public function ajax_add_tag_role_mapping() {
6850 - // Verify nonce and permissions
6851 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6852 -
6853 - if (!current_user_can('manage_options')) {
6854 - wp_send_json_error('Unauthorized access');
6855 - exit;
6856 - }
6857 -
6858 - $tag_input = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
6859 - $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public';
6860 -
6861 - if (empty($tag_input)) {
6862 - wp_send_json_error('Please enter a tag name or slug');
6863 - exit;
6864 - }
6865 -
6866 - // Validate role restriction
6867 - $valid_roles = array_keys($this->mxchat_get_role_options());
6868 - if (!in_array($role_restriction, $valid_roles)) {
6869 - wp_send_json_error('Invalid role restriction');
6870 - exit;
6871 - }
6872 -
6873 - // Resolve the tag by slug first, then fall back to its display name, so users can
6874 - // enter either "premium-content" or "Premium Content". (plan b8bcf5 — the field is
6875 - // labeled by name but previously validated by slug only, producing the confusing
6876 - // "Tag does not exist in WordPress" error when a real tag's name was typed.)
6877 - $term = get_term_by('slug', $tag_input, 'post_tag');
6878 - if (!$term) {
6879 - $term = get_term_by('name', $tag_input, 'post_tag');
6880 - }
6881 - if (!$term) {
6882 - 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.');
6883 - exit;
6884 - }
6885 -
6886 - // Always key the mapping by the RESOLVED slug — apply_role_restriction_to_post()
6887 - // compares against each post's tag slugs, so the stored key must be a slug,
6888 - // never the raw (possibly display-name) input.
6889 - $tag_slug = $term->slug;
6890 -
6891 - // Get existing mappings
6892 - $mappings = get_option('mxchat_tag_role_mappings', array());
6893 -
6894 - // Check if mapping already exists
6895 - if (isset($mappings[$tag_slug])) {
6896 - wp_send_json_error('Mapping for this tag already exists');
6897 - exit;
6898 - }
6899 -
6900 - // Add new mapping
6901 - $mappings[$tag_slug] = $role_restriction;
6902 - update_option('mxchat_tag_role_mappings', $mappings);
6903 -
6904 - wp_send_json_success(array(
6905 - 'message' => 'Tag-role mapping added successfully',
6906 - 'tag_slug' => $tag_slug,
6907 - 'role_restriction' => $role_restriction
6908 - ));
6909 - exit;
6910 -}
6911 -
6912 -/**
6913 - * Delete tag-role mapping via AJAX
6914 - */
6915 -public function ajax_delete_tag_role_mapping() {
6916 - // Verify nonce and permissions
6917 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6918 -
6919 - if (!current_user_can('manage_options')) {
6920 - wp_send_json_error('Unauthorized access');
6921 - exit;
6922 - }
6923 -
6924 - $tag_slug = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : '';
6925 -
6926 - if (empty($tag_slug)) {
6927 - wp_send_json_error('Tag slug is required');
6928 - exit;
6929 - }
6930 -
6931 - // Get existing mappings
6932 - $mappings = get_option('mxchat_tag_role_mappings', array());
6933 -
6934 - // Check if mapping exists
6935 - if (!isset($mappings[$tag_slug])) {
6936 - wp_send_json_error('Mapping does not exist');
6937 - exit;
6938 - }
6939 -
6940 - // Remove mapping
6941 - unset($mappings[$tag_slug]);
6942 - update_option('mxchat_tag_role_mappings', $mappings);
6943 -
6944 - wp_send_json_success(array(
6945 - 'message' => 'Tag-role mapping deleted successfully',
6946 - 'tag_slug' => $tag_slug
6947 - ));
6948 - exit;
6949 -}
6950 -
6951 -/**
6952 - * Get all tag-role mappings via AJAX
6953 - */
6954 -public function ajax_get_tag_role_mappings() {
6955 - // Verify nonce and permissions
6956 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6957 -
6958 - if (!current_user_can('manage_options')) {
6959 - wp_send_json_error('Unauthorized access');
6960 - exit;
6961 - }
6962 -
6963 - // Get mappings
6964 - $mappings = get_option('mxchat_tag_role_mappings', array());
6965 - $role_options = $this->mxchat_get_role_options();
6966 -
6967 - $formatted_mappings = array();
6968 -
6969 - foreach ($mappings as $tag_slug => $role_restriction) {
6970 - // Get tag object
6971 - $term = get_term_by('slug', $tag_slug, 'post_tag');
6972 -
6973 - // Count posts with this tag
6974 - $post_count = 0;
6975 - if ($term) {
6976 - $post_count = $term->count;
6977 - }
6978 -
6979 - $formatted_mappings[] = array(
6980 - 'tag_slug' => $tag_slug,
6981 - 'role_restriction' => $role_restriction,
6982 - 'role_label' => $role_options[$role_restriction] ?? $role_restriction,
6983 - 'post_count' => $post_count
6984 - );
6985 - }
6986 -
6987 - wp_send_json_success(array(
6988 - 'mappings' => $formatted_mappings
6989 - ));
6990 - exit;
6991 -}
6992 -
6993 -/**
6994 - * Bulk update role restrictions for all existing content with mapped tags
6995 - */
6996 -public function ajax_bulk_update_tag_roles() {
6997 - // Verify nonce and permissions
6998 - check_ajax_referer('mxchat_prompts_setting_nonce', 'nonce');
6999 -
7000 - if (!current_user_can('manage_options')) {
7001 - wp_send_json_error('Unauthorized access');
7002 - exit;
7003 - }
7004 -
7005 - // Get mappings
7006 - $mappings = get_option('mxchat_tag_role_mappings', array());
7007 -
7008 - if (empty($mappings)) {
7009 - wp_send_json_error('No tag-role mappings found');
7010 - exit;
7011 - }
7012 -
7013 - global $wpdb;
7014 -
7015 - // Check if using Pinecone
7016 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
7017 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
7018 -
7019 - $updated_count = 0;
7020 - $details = array();
7021 -
7022 - foreach ($mappings as $tag_slug => $role_restriction) {
7023 - // Get all posts with this tag
7024 - $posts = get_posts(array(
7025 - 'tag' => $tag_slug,
7026 - 'post_type' => 'any',
7027 - 'posts_per_page' => -1,
7028 - 'fields' => 'ids',
7029 - 'post_status' => 'publish'
7030 - ));
7031 -
7032 - if (empty($posts)) {
7033 - continue;
7034 - }
7035 -
7036 - $tag_updated = 0;
7037 -
7038 - foreach ($posts as $post_id) {
7039 - $source_url = get_permalink($post_id);
7040 - if (!$source_url) {
7041 - continue;
7042 - }
7043 -
7044 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
7045 - // Update Pinecone role restriction
7046 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
7047 - $vector_id = md5($source_url);
7048 -
7049 - $result = $wpdb->replace(
7050 - $roles_table,
7051 - array(
7052 - 'vector_id' => $vector_id,
7053 - 'role_restriction' => $role_restriction,
7054 - 'updated_at' => current_time('mysql')
7055 - ),
7056 - array('%s', '%s', '%s')
7057 - );
7058 - } else {
7059 - // Update WordPress DB
7060 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
7061 -
7062 - $result = $wpdb->update(
7063 - $table_name,
7064 - array('role_restriction' => $role_restriction),
7065 - array('source_url' => $source_url),
7066 - array('%s'),
7067 - array('%s')
7068 - );
7069 - }
7070 -
7071 - if ($result !== false) {
7072 - $tag_updated++;
7073 - $updated_count++;
7074 - }
7075 - }
7076 -
7077 - if ($tag_updated > 0) {
7078 - $details[] = sprintf(
7079 - 'Tag "%s" (%s): %d posts updated',
7080 - $tag_slug,
7081 - $role_restriction,
7082 - $tag_updated
7083 - );
7084 - }
7085 - }
7086 -
7087 - wp_send_json_success(array(
7088 - 'message' => 'Bulk update completed',
7089 - 'updated_count' => $updated_count,
7090 - 'tags_processed' => count($mappings),
7091 - 'details' => $details
7092 - ));
7093 - exit;
7094 -}
7095 -
7096 -/**
7097 - * Handle tag changes on posts (when tags are added or removed)
7098 - */
7099 -public function handle_tag_change($object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids) {
7100 - // Only process post tags
7101 - if ($taxonomy !== 'post_tag') {
7102 - return;
7103 - }
7104 -
7105 - // Get tag-role mappings
7106 - $mappings = get_option('mxchat_tag_role_mappings', array());
7107 -
7108 - if (empty($mappings)) {
7109 - return;
7110 - }
7111 -
7112 - // Get the post's URL
7113 - $source_url = get_permalink($object_id);
7114 - if (!$source_url) {
7115 - return;
7116 - }
7117 -
7118 - // Determine the highest role restriction based on tags
7119 - $highest_role = 'public';
7120 - $role_hierarchy = array(
7121 - 'public' => 0,
7122 - 'logged_in' => 1,
7123 - 'subscriber' => 2,
7124 - 'contributor' => 3,
7125 - 'author' => 4,
7126 - 'editor' => 5,
7127 - 'administrator' => 6
7128 - );
7129 -
7130 - // Get all current tags for the post
7131 - $current_tags = wp_get_post_tags($object_id, array('fields' => 'slugs'));
7132 -
7133 - // Find the highest role restriction among the tags
7134 - foreach ($current_tags as $tag_slug) {
7135 - if (isset($mappings[$tag_slug])) {
7136 - $role = $mappings[$tag_slug];
7137 - if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
7138 - $highest_role = $role;
7139 - }
7140 - }
7141 - }
7142 -
7143 - // Update the role restriction in the database
7144 - global $wpdb;
7145 -
7146 - // Check if using Pinecone
7147 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
7148 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
7149 -
7150 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
7151 - // Update Pinecone role restriction
7152 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
7153 - $vector_id = md5($source_url);
7154 -
7155 - $wpdb->replace(
7156 - $roles_table,
7157 - array(
7158 - 'vector_id' => $vector_id,
7159 - 'role_restriction' => $highest_role,
7160 - 'updated_at' => current_time('mysql')
7161 - ),
7162 - array('%s', '%s', '%s')
7163 - );
7164 - } else {
7165 - // Update WordPress DB
7166 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
7167 -
7168 - $wpdb->update(
7169 - $table_name,
7170 - array('role_restriction' => $highest_role),
7171 3332 array('source_url' => $source_url),
7172 - array('%s'),
7173 3333 array('%s')
7174 3334 );
7175 3335 }
7176 3336 }
7177 -
7178 -/**
7179 - * Apply role restriction after content is stored (for auto-sync)
7180 - */
7181 -public function apply_role_restriction_after_storage($post_id, $source_url) {
7182 - // Get tag-role mappings
7183 - $mappings = get_option('mxchat_tag_role_mappings', array());
7184 3337
7185 - if (empty($mappings)) {
7186 - return;
7187 - }
7188 -
7189 - // Get all tags for the post
7190 - $post_tags = wp_get_post_tags($post_id, array('fields' => 'slugs'));
7191 -
7192 - if (empty($post_tags)) {
7193 - return;
7194 - }
7195 -
7196 - // Determine the highest role restriction based on tags
7197 - $highest_role = 'public';
7198 - $role_hierarchy = array(
7199 - 'public' => 0,
7200 - 'logged_in' => 1,
7201 - 'subscriber' => 2,
7202 - 'contributor' => 3,
7203 - 'author' => 4,
7204 - 'editor' => 5,
7205 - 'administrator' => 6
7206 - );
7207 -
7208 - foreach ($post_tags as $tag_slug) {
7209 - if (isset($mappings[$tag_slug])) {
7210 - $role = $mappings[$tag_slug];
7211 - if (isset($role_hierarchy[$role]) && $role_hierarchy[$role] > $role_hierarchy[$highest_role]) {
7212 - $highest_role = $role;
7213 - }
7214 - }
7215 - }
7216 -
7217 - // If no restricted tags found, return (leave as public)
7218 - if ($highest_role === 'public') {
7219 - return;
7220 - }
7221 -
7222 - // Update the role restriction
7223 - global $wpdb;
7224 -
7225 - // Check if using Pinecone
7226 - $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
7227 - $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
7228 -
7229 - if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
7230 - // Update Pinecone role restriction
7231 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
7232 - $vector_id = md5($source_url);
7233 -
7234 - $wpdb->replace(
7235 - $roles_table,
7236 - array(
7237 - 'vector_id' => $vector_id,
7238 - 'role_restriction' => $highest_role,
7239 - 'updated_at' => current_time('mysql')
7240 - ),
7241 - array('%s', '%s', '%s')
7242 - );
7243 - } else {
7244 - // Update WordPress DB
7245 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
7246 -
7247 - $wpdb->update(
7248 - $table_name,
7249 - array('role_restriction' => $highest_role),
7250 - array('source_url' => $source_url),
7251 - array('%s'),
7252 - array('%s')
7253 - );
7254 - }
7255 -}
7256 -
7257 -
7258 3338 // ========================================
7259 3339 // HELPER METHODS
7260 3340 // ========================================
7261 3341
@@ -7322,1074 +3402,8 @@
7322 3402 */
7323 3403 private function mxchat_get_pinecone_manager() {
7324 3404 return MxChat_Pinecone_Manager::get_instance();
7325 3405 }
7326 -
7327 -
7328 - // ========================================
7329 -// DATABASE QUEUE TABLE MANAGEMENT
7330 -// ========================================
7331 -
7332 -/**
7333 - * Create queue table on plugin activation
7334 - * Call this from your plugin activation hook
7335 - */
7336 -public function mxchat_create_queue_table() {
7337 - global $wpdb;
7338 -
7339 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7340 - $charset_collate = $wpdb->get_charset_collate();
7341 -
7342 - $sql = "CREATE TABLE IF NOT EXISTS $table_name (
7343 - id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
7344 - queue_id varchar(64) NOT NULL,
7345 - item_type varchar(20) NOT NULL,
7346 - item_data longtext NOT NULL,
7347 - status varchar(20) NOT NULL DEFAULT 'pending',
7348 - bot_id varchar(50) NOT NULL DEFAULT 'default',
7349 - priority int(11) NOT NULL DEFAULT 0,
7350 - attempts int(11) NOT NULL DEFAULT 0,
7351 - max_attempts int(11) NOT NULL DEFAULT 3,
7352 - error_message text DEFAULT NULL,
7353 - created_at datetime NOT NULL,
7354 - started_at datetime DEFAULT NULL,
7355 - completed_at datetime DEFAULT NULL,
7356 - PRIMARY KEY (id),
7357 - KEY queue_id (queue_id),
7358 - KEY status (status),
7359 - KEY item_type (item_type),
7360 - KEY priority (priority)
7361 - ) $charset_collate;";
7362 -
7363 - require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
7364 - dbDelta($sql);
7365 -
7366 - // Also create a meta table for queue metadata
7367 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
7368 -
7369 - $meta_sql = "CREATE TABLE IF NOT EXISTS $meta_table (
7370 - id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
7371 - queue_id varchar(64) NOT NULL,
7372 - meta_key varchar(255) NOT NULL,
7373 - meta_value longtext,
7374 - PRIMARY KEY (id),
7375 - KEY queue_id (queue_id),
7376 - KEY meta_key (meta_key)
7377 - ) $charset_collate;";
7378 -
7379 - dbDelta($meta_sql);
7380 -}
7381 -
7382 -/**
7383 - * Add items to the processing queue
7384 - *
7385 - * @param string $queue_id Unique identifier for this queue batch
7386 - * @param string $item_type Type of item (url, pdf_page)
7387 - * @param array $items Array of items to queue
7388 - * @param string $bot_id Bot ID for processing
7389 - * @return int Number of items queued
7390 - */
7391 -private function mxchat_add_to_queue($queue_id, $item_type, $items, $bot_id = 'default') {
7392 - global $wpdb;
7393 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7394 -
7395 - $queued_count = 0;
7396 - $priority = 0;
7397 -
7398 - foreach ($items as $item) {
7399 - $result = $wpdb->insert(
7400 - $table_name,
7401 - array(
7402 - 'queue_id' => $queue_id,
7403 - 'item_type' => $item_type,
7404 - 'item_data' => wp_json_encode($item),
7405 - 'status' => 'pending',
7406 - 'bot_id' => $bot_id,
7407 - 'priority' => $priority,
7408 - 'attempts' => 0,
7409 - 'max_attempts' => 3,
7410 - 'created_at' => current_time('mysql')
7411 - ),
7412 - array('%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%s')
7413 - );
7414 -
7415 - if ($result) {
7416 - $queued_count++;
7417 - }
7418 -
7419 - $priority++; // Process in order
7420 - }
7421 -
7422 - return $queued_count;
7423 -}
7424 -
7425 -/**
7426 - * Store queue metadata (total counts, source URL, etc.)
7427 - */
7428 -private function mxchat_set_queue_meta($queue_id, $meta_key, $meta_value) {
7429 - global $wpdb;
7430 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
7431 -
7432 - // Check if meta exists
7433 - $existing = $wpdb->get_var($wpdb->prepare(
7434 - "SELECT id FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
7435 - $queue_id,
7436 - $meta_key
7437 - ));
7438 -
7439 - if ($existing) {
7440 - // Update
7441 - $wpdb->update(
7442 - $meta_table,
7443 - array('meta_value' => maybe_serialize($meta_value)),
7444 - array('queue_id' => $queue_id, 'meta_key' => $meta_key),
7445 - array('%s'),
7446 - array('%s', '%s')
7447 - );
7448 - } else {
7449 - // Insert
7450 - $wpdb->insert(
7451 - $meta_table,
7452 - array(
7453 - 'queue_id' => $queue_id,
7454 - 'meta_key' => $meta_key,
7455 - 'meta_value' => maybe_serialize($meta_value)
7456 - ),
7457 - array('%s', '%s', '%s')
7458 - );
7459 - }
7460 -}
7461 -
7462 -/**
7463 - * Get queue metadata
7464 - */
7465 -private function mxchat_get_queue_meta($queue_id, $meta_key) {
7466 - global $wpdb;
7467 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
7468 -
7469 - $value = $wpdb->get_var($wpdb->prepare(
7470 - "SELECT meta_value FROM $meta_table WHERE queue_id = %s AND meta_key = %s",
7471 - $queue_id,
7472 - $meta_key
7473 - ));
7474 -
7475 - return maybe_unserialize($value);
7476 -}
7477 -
7478 -// ========================================
7479 -// AJAX QUEUE PROCESSING HANDLERS
7480 -// ========================================
7481 -
7482 -/**
7483 - * AJAX: Get next item from queue to process
7484 - */
7485 -public function ajax_mxchat_get_next_queue_item() {
7486 - // Verify nonce and permissions
7487 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7488 -
7489 - if (!current_user_can('manage_options')) {
7490 - wp_send_json_error('Unauthorized access');
7491 - }
7492 -
7493 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
7494 -
7495 - if (empty($queue_id)) {
7496 - wp_send_json_error('Missing queue ID');
7497 - }
7498 -
7499 - global $wpdb;
7500 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7501 -
7502 - // Get next pending item with retry logic for failed items
7503 - $next_item = $wpdb->get_row($wpdb->prepare(
7504 - "SELECT * FROM $table_name
7505 - WHERE queue_id = %s
7506 - AND status IN ('pending', 'failed')
7507 - AND attempts < max_attempts
7508 - ORDER BY priority ASC, id ASC
7509 - LIMIT 1",
7510 - $queue_id
7511 - ));
7512 -
7513 - if (!$next_item) {
7514 - // No more items - queue complete
7515 - wp_send_json_success(array(
7516 - 'complete' => true,
7517 - 'message' => 'Queue processing complete'
7518 - ));
7519 - }
7520 -
7521 - // Mark item as processing
7522 - $wpdb->update(
7523 - $table_name,
7524 - array(
7525 - 'status' => 'processing',
7526 - 'started_at' => current_time('mysql'),
7527 - 'attempts' => $next_item->attempts + 1
7528 - ),
7529 - array('id' => $next_item->id),
7530 - array('%s', '%s', '%d'),
7531 - array('%d')
7532 - );
7533 -
7534 - wp_send_json_success(array(
7535 - 'complete' => false,
7536 - 'item' => array(
7537 - 'id' => $next_item->id,
7538 - 'type' => $next_item->item_type,
7539 - 'data' => json_decode($next_item->item_data, true),
7540 - 'bot_id' => $next_item->bot_id,
7541 - 'attempt' => $next_item->attempts + 1
7542 - )
7543 - ));
7544 -}
7545 -
7546 -/**
7547 - * AJAX: Process a single queue item
7548 - */
7549 -public function ajax_mxchat_process_queue_item() {
7550 - // Verify nonce and permissions
7551 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
7552 -
7553 - if (!current_user_can('manage_options')) {
7554 - wp_send_json_error('Unauthorized access');
7555 - }
7556 -
7557 - $item_id = isset($_POST['item_id']) ? absint($_POST['item_id']) : 0;
7558 - $item_type = isset($_POST['item_type']) ? sanitize_text_field($_POST['item_type']) : '';
7559 - $item_data = isset($_POST['item_data']) ? $_POST['item_data'] : array();
7560 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
7561 -
7562 - if (empty($item_id) || empty($item_type)) {
7563 - wp_send_json_error('Missing item data');
7564 - }
7565 -
7566 - global $wpdb;
7567 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
7568 -
7569 - // Process based on item type
7570 - try {
7571 - set_time_limit(60); // Give processing 60 seconds
7572 -
7573 - $result = false;
7574 - $error_message = '';
7575 -
7576 - // Read item directly from DB to get queue_id and preserve special chars in item_data
7577 - // (POST round-trip through JS mangles characters like apostrophes in URLs)
7578 - $db_item = $wpdb->get_row($wpdb->prepare(
7579 - "SELECT queue_id, item_data FROM $table_name WHERE id = %d",
7580 - $item_id
7581 - ));
7582 - $item_queue_id = $db_item ? $db_item->queue_id : '';
7583 - if ($db_item && !empty($db_item->item_data)) {
7584 - $db_data = json_decode($db_item->item_data, true);
7585 - if (is_array($db_data)) {
7586 - $item_data = $db_data;
7587 - }
7588 - }
7589 -
7590 - switch ($item_type) {
7591 - case 'url':
7592 - $result = $this->mxchat_process_queue_url($item_data, $bot_id, $item_queue_id);
7593 - break;
7594 -
7595 - case 'pdf_page':
7596 - $result = $this->mxchat_process_queue_pdf_page($item_data, $bot_id);
7597 - break;
7598 -
7599 - default:
7600 - throw new Exception('Unknown item type: ' . $item_type);
7601 - }
7602 -
7603 - if (is_wp_error($result)) {
7604 - $error_code = $result->get_error_code();
7605 - // Content errors (empty page, sanitization) are permanent — retrying won't help
7606 - $permanent_codes = array('empty_page', 'empty_after_sanitization', 'no_api_key', 'page_not_found');
7607 - if (in_array($error_code, $permanent_codes)) {
7608 - // Mark as permanently failed — set attempts = max_attempts so it won't be retried
7609 - $current_item = $wpdb->get_row($wpdb->prepare(
7610 - "SELECT max_attempts FROM $table_name WHERE id = %d", $item_id
7611 - ));
7612 - $wpdb->update(
7613 - $table_name,
7614 - array(
7615 - 'status' => 'failed',
7616 - 'error_message' => $result->get_error_message(),
7617 - 'attempts' => $current_item ? $current_item->max_attempts : 3
7618 - ),
7619 - array('id' => $item_id),
7620 - array('%s', '%s', '%d'),
7621 - array('%d')
7622 - );
7623 - wp_send_json_error(array(
7624 - 'message' => $result->get_error_message(),
7625 - 'permanent_failure' => true,
7626 - 'item_id' => $item_id
7627 - ));
7628 - return;
7629 - }
7630 - throw new Exception($result->get_error_message());
7631 - }
7632 -
7633 - if ($result === false) {
7634 - throw new Exception('Processing returned false - item may be empty or invalid');
7635 - }
7636 -
7637 - // Mark as completed
7638 - $wpdb->update(
7639 - $table_name,
7640 - array(
7641 - 'status' => 'completed',
7642 - 'completed_at' => current_time('mysql'),
7643 - 'error_message' => null
7644 - ),
7645 - array('id' => $item_id),
7646 - array('%s', '%s', '%s'),
7647 - array('%d')
7648 - );
7649 -
7650 - wp_send_json_success(array(
7651 - 'processed' => true,
7652 - 'item_id' => $item_id,
7653 - 'message' => 'Item processed successfully'
7654 - ));
7655 -
7656 - } catch (Exception $e) {
7657 - $error_message = $e->getMessage();
7658 -
7659 - // Get current attempt count
7660 - $item = $wpdb->get_row($wpdb->prepare(
7661 - "SELECT attempts, max_attempts FROM $table_name WHERE id = %d",
7662 - $item_id
7663 - ));
7664 -
7665 - // Check if we've exhausted retries
7666 - if ($item && $item->attempts >= $item->max_attempts) {
7667 - // Permanently failed
7668 - $wpdb->update(
7669 - $table_name,
7670 - array(
7671 - 'status' => 'failed',
7672 - 'error_message' => $error_message
7673 - ),
7674 - array('id' => $item_id),
7675 - array('%s', '%s'),
7676 - array('%d')
7677 - );
7678 -
7679 - wp_send_json_error(array(
7680 - 'message' => 'Item failed after maximum attempts: ' . $error_message,
7681 - 'permanent_failure' => true,
7682 - 'item_id' => $item_id
7683 - ));
7684 - } else {
7685 - // Mark for retry
7686 - $wpdb->update(
7687 - $table_name,
7688 - array(
7689 - 'status' => 'failed',
7690 - 'error_message' => $error_message
7691 - ),
7692 - array('id' => $item_id),
7693 - array('%s', '%s'),
7694 - array('%d')
7695 - );
7696 -
7697 - wp_send_json_error(array(
7698 - 'message' => 'Item processing failed, will retry: ' . $error_message,
7699 - 'can_retry' => true,
7700 - 'item_id' => $item_id,
7701 - 'attempts' => $item ? $item->attempts : 0
7702 - ));
7703 - }
7704 - }
7705 -}
7706 -
7707 -/**
7708 - * Process a URL from the queue
7709 - */
7710 -private function mxchat_process_queue_url($item_data, $bot_id = 'default', $queue_id = '') {
7711 - $url = isset($item_data['url']) ? $item_data['url'] : '';
7712 -
7713 - if (empty($url)) {
7714 - return new WP_Error('invalid_url', 'URL is empty');
7715 - }
7716 -
7717 - // Get bot-specific API key early (needed for both paths)
7718 - $bot_options = $this->get_bot_options($bot_id);
7719 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
7720 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
7721 -
7722 - if (strpos($selected_model, 'voyage') === 0) {
7723 - $api_key = $options['voyage_api_key'] ?? '';
7724 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
7725 - $api_key = $options['gemini_api_key'] ?? '';
7726 - } else {
7727 - $api_key = $options['api_key'] ?? '';
7728 - }
7729 -
7730 - if (empty($api_key)) {
7731 - return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
7732 - }
7733 -
7734 - // Check if this is a WooCommerce product URL and WooCommerce is active
7735 - $is_product_url = (strpos($url, '/product/') !== false || strpos($url, '/shop/') !== false);
7736 - $content_type = $is_product_url ? 'product' : 'url';
7737 -
7738 - // Try to get WooCommerce product data if it's a product URL
7739 - if ($is_product_url && class_exists('WooCommerce')) {
7740 - $product_content = $this->mxchat_extract_woocommerce_product_content($url);
7741 -
7742 - if (!empty($product_content)) {
7743 - // Successfully extracted WooCommerce product data with pricing
7744 - $result = MxChat_Utils::submit_content_to_db(
7745 - $product_content,
7746 - $url,
7747 - $api_key,
7748 - null,
7749 - $bot_id,
7750 - 'product'
7751 - );
7752 - return $result;
7753 - }
7754 - // If WooCommerce extraction failed, fall through to HTML extraction
7755 - }
7756 -
7757 - // Fetch URL content (fallback for non-products or when WooCommerce extraction fails)
7758 - $is_likely_pdf = (strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?: '', PATHINFO_EXTENSION)) === 'pdf');
7759 - $response = wp_remote_get($url, array(
7760 - 'timeout' => $is_likely_pdf ? 120 : 30,
7761 - 'redirection' => 5,
7762 - 'user-agent' => mxchat_ingest_user_agent(),
7763 - ));
7764 -
7765 - if (is_wp_error($response)) {
7766 - return $response;
7767 - }
7768 -
7769 - $response_code = wp_remote_retrieve_response_code($response);
7770 - if ($response_code !== 200) {
7771 - return new WP_Error('http_error', 'HTTP ' . $response_code . ' error for: ' . $url);
7772 - }
7773 -
7774 - // Check if URL is a PDF — expand into per-page queue items using the standard PDF pipeline
7775 - if ($this->mxchat_is_pdf_url($url, $response)) {
7776 - return $this->mxchat_expand_pdf_to_queue($url, $response, $bot_id, $queue_id);
7777 - }
7778 -
7779 - $html = wp_remote_retrieve_body($response);
7780 -
7781 - if (empty($html)) {
7782 - return new WP_Error('empty_response', 'Empty response body');
7783 - }
7784 -
7785 - // Extract and sanitize content
7786 - $content = $this->mxchat_extract_main_content($html);
7787 - $sanitized = $this->mxchat_sanitize_content_for_api($content);
7788 -
7789 - if (empty($sanitized)) {
7790 - // Not an error - just no content found (maybe a redirect or empty page)
7791 - return false;
7792 - }
7793 -
7794 - // Submit to database with content_type
7795 - $result = MxChat_Utils::submit_content_to_db(
7796 - $sanitized,
7797 - $url,
7798 - $api_key,
7799 - null,
7800 - $bot_id,
7801 - $content_type
7802 - );
7803 -
7804 - return $result;
7805 -}
7806 -
7807 -/**
7808 - * Expand a PDF URL into per-page queue items using the standard PDF pipeline.
7809 - * Called when a sitemap URL turns out to be a PDF — downloads, parses page count,
7810 - * and adds pdf_page items to the same queue so they process with full progress tracking.
7811 - */
7812 -private function mxchat_expand_pdf_to_queue($pdf_url, $response, $bot_id = 'default', $queue_id = '') {
7813 - set_time_limit(120); // PDFs need extra time for download + parsing
7814 -
7815 - $upload_dir = wp_upload_dir();
7816 - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf');
7817 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
7818 -
7819 - $response_body = wp_remote_retrieve_body($response);
7820 - if (empty($response_body)) {
7821 - return new WP_Error('empty_pdf', 'Empty PDF response for: ' . $pdf_url);
7822 - }
7823 -
7824 - if (!wp_mkdir_p(dirname($pdf_path))) {
7825 - return new WP_Error('dir_error', 'Failed to create upload directory');
7826 - }
7827 -
7828 - file_put_contents($pdf_path, $response_body);
7829 -
7830 - if (!file_exists($pdf_path)) {
7831 - return new WP_Error('save_error', 'Failed to save PDF file');
7832 - }
7833 -
7834 - try {
7835 - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
7836 -
7837 - if ($total_pages === false || $total_pages < 1) {
7838 - wp_delete_file($pdf_path);
7839 - return new WP_Error('no_pages', 'PDF has no pages: ' . $pdf_url);
7840 - }
7841 -
7842 - // Build per-page items identical to mxchat_handle_pdf_for_knowledge_base
7843 - $pages = array();
7844 - for ($i = 1; $i <= $total_pages; $i++) {
7845 - $pages[] = array(
7846 - 'pdf_path' => $pdf_path,
7847 - 'pdf_url' => $pdf_url,
7848 - 'page_number' => $i,
7849 - 'total_pages' => $total_pages
7850 - );
7851 - }
7852 -
7853 - // Add pdf_page items to the SAME queue so the JS picks them up automatically
7854 - if (!empty($queue_id)) {
7855 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
7856 - } else {
7857 - // Fallback: create a new PDF queue (shouldn't happen in sitemap flow)
7858 - $new_queue_id = 'pdf_' . md5($pdf_url . time());
7859 - $queued_count = $this->mxchat_add_to_queue($new_queue_id, 'pdf_page', $pages, $bot_id);
7860 - $this->mxchat_set_queue_meta($new_queue_id, 'source_url', $pdf_url);
7861 - $this->mxchat_set_queue_meta($new_queue_id, 'queue_type', 'pdf');
7862 - $this->mxchat_set_queue_meta($new_queue_id, 'total_items', $total_pages);
7863 - $this->mxchat_set_queue_meta($new_queue_id, 'bot_id', $bot_id);
7864 - $this->mxchat_set_queue_meta($new_queue_id, 'pdf_path', $pdf_path);
7865 - $this->mxchat_set_queue_meta($new_queue_id, 'created_at', current_time('mysql'));
7866 - }
7867 -
7868 - if ($queued_count === 0) {
7869 - wp_delete_file($pdf_path);
7870 - return new WP_Error('queue_error', 'Failed to add PDF pages to queue');
7871 - }
7872 -
7873 - // Return true so the original URL item is marked complete
7874 - // The new pdf_page items will be processed in subsequent batches
7875 - return true;
7876 -
7877 - } catch (Exception $e) {
7878 - if (file_exists($pdf_path)) {
7879 - wp_delete_file($pdf_path);
7880 - }
7881 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
7882 - }
7883 -}
7884 -
7885 -/**
7886 - * Legacy: Process a PDF URL inline during sitemap queue processing.
7887 - * @deprecated Use mxchat_expand_pdf_to_queue instead — kept for reference only.
7888 - */
7889 -private function mxchat_process_pdf_url_inline($pdf_url, $response, $api_key, $bot_id = 'default') {
7890 - set_time_limit(120); // PDFs need more time — downloading + parsing all pages
7891 -
7892 - $upload_dir = wp_upload_dir();
7893 - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf');
7894 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
7895 -
7896 - $response_body = wp_remote_retrieve_body($response);
7897 - if (empty($response_body)) {
7898 - return new WP_Error('empty_pdf', 'Empty PDF response');
7899 - }
7900 -
7901 - if (!wp_mkdir_p(dirname($pdf_path))) {
7902 - return new WP_Error('dir_error', 'Failed to create upload directory');
7903 - }
7904 -
7905 - file_put_contents($pdf_path, $response_body);
7906 -
7907 - if (!file_exists($pdf_path)) {
7908 - return new WP_Error('save_error', 'Failed to save PDF file');
7909 - }
7910 -
7911 - try {
7912 - mxchat_load_pdf_parser();
7913 - $parser = new \Smalot\PdfParser\Parser();
7914 - $pdf = $parser->parseFile($pdf_path);
7915 - $pages = $pdf->getPages();
7916 - $total_pages = count($pages);
7917 -
7918 - if ($total_pages < 1) {
7919 - wp_delete_file($pdf_path);
7920 - return new WP_Error('no_pages', 'PDF has no pages');
7921 - }
7922 -
7923 - $processed = 0;
7924 - $skipped_pages = array();
7925 -
7926 - for ($i = 0; $i < $total_pages; $i++) {
7927 - $page_num = $i + 1;
7928 - $text = $pages[$i]->getText();
7929 - if (empty($text)) {
7930 - $skipped_pages[] = 'Page ' . $page_num . ': No text could be extracted — page may contain only images, links, or non-standard encoding';
7931 - continue;
7932 - }
7933 -
7934 - $sanitized = $this->mxchat_sanitize_content_for_api($text);
7935 - if (empty($sanitized)) {
7936 - $skipped_pages[] = 'Page ' . $page_num . ': Text was extracted but contained only special characters, control codes, or unsupported content';
7937 - continue;
7938 - }
7939 -
7940 - $metadata = array(
7941 - 'document_type' => 'pdf',
7942 - 'total_pages' => $total_pages,
7943 - 'current_page' => $page_num,
7944 - 'source_url' => $pdf_url,
7945 - );
7946 -
7947 - $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized;
7948 - $page_url = esc_url($pdf_url . '#page=' . $page_num);
7949 -
7950 - MxChat_Utils::submit_content_to_db(
7951 - $content_with_metadata,
7952 - $page_url,
7953 - $api_key,
7954 - null,
7955 - $bot_id,
7956 - 'pdf'
7957 - );
7958 -
7959 - $processed++;
7960 - }
7961 -
7962 - // Clean up the temp PDF file
7963 - wp_delete_file($pdf_path);
7964 -
7965 - if (!empty($skipped_pages)) {
7966 - error_log('MxChat PDF: Skipped ' . count($skipped_pages) . ' of ' . $total_pages . ' pages: ' . implode('; ', $skipped_pages));
7967 - }
7968 -
7969 - return $processed > 0 ? true : false;
7970 -
7971 - } catch (Exception $e) {
7972 - if (file_exists($pdf_path)) {
7973 - wp_delete_file($pdf_path);
7974 - }
7975 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
7976 - }
7977 -}
7978 -
7979 -/**
7980 - * Extract WooCommerce product content including pricing
7981 - *
7982 - * @param string $url The product URL
7983 - * @return string|false Product content with pricing, or false if not found
7984 - */
7985 -private function mxchat_extract_woocommerce_product_content($url) {
7986 - // Try to get product ID from URL
7987 - $product_id = url_to_postid($url);
7988 -
7989 - // If url_to_postid fails, try to extract from URL pattern
7990 - if (!$product_id) {
7991 - $product_slug = '';
7992 -
7993 - // Handle pretty permalinks: /product/product-name/
7994 - if (preg_match('/\/product\/([^\/\?]+)/', $url, $matches)) {
7995 - $product_slug = $matches[1];
7996 - }
7997 -
7998 - if (!empty($product_slug)) {
7999 - $product_post = get_page_by_path($product_slug, OBJECT, 'product');
8000 - if ($product_post) {
8001 - $product_id = $product_post->ID;
8002 - }
8003 - }
8004 - }
8005 -
8006 - if (!$product_id) {
8007 - return false;
8008 - }
8009 -
8010 - // Get WooCommerce product object
8011 - $product = wc_get_product($product_id);
8012 -
8013 - if (!$product) {
8014 - return false;
8015 - }
8016 -
8017 - // Build product content with pricing (similar to mxchat_store_product_embedding)
8018 - $title = $product->get_name();
8019 - $description = $product->get_description();
8020 - $short_description = $product->get_short_description();
8021 - $sku = $product->get_sku();
8022 -
8023 - // Get pricing information
8024 - $regular_price = $product->get_regular_price();
8025 - $sale_price = $product->get_sale_price();
8026 - $price = $product->get_price(); // Current active price
8027 -
8028 - // Get currency symbol
8029 - $currency_symbol = get_woocommerce_currency_symbol();
8030 -
8031 - // Format content
8032 - $content = $title . "\n\n";
8033 -
8034 - if (!empty($short_description)) {
8035 - $content .= "Short Description: " . wp_strip_all_tags($short_description) . "\n\n";
8036 - }
8037 -
8038 - if (!empty($description)) {
8039 - $content .= wp_strip_all_tags($description) . "\n\n";
8040 - }
8041 -
8042 - // Add pricing information
8043 - if (!empty($regular_price)) {
8044 - $content .= "Price: " . $currency_symbol . $regular_price . "\n";
8045 - } elseif (!empty($price)) {
8046 - $content .= "Price: " . $currency_symbol . $price . "\n";
8047 - }
8048 -
8049 - if (!empty($sale_price) && $sale_price !== $regular_price) {
8050 - $content .= "Sale Price: " . $currency_symbol . $sale_price . "\n";
8051 - }
8052 -
8053 - // Handle variable products - show price range
8054 - if ($product->is_type('variable')) {
8055 - $min_price = $product->get_variation_price('min');
8056 - $max_price = $product->get_variation_price('max');
8057 - if ($min_price !== $max_price) {
8058 - $content .= "Price Range: " . $currency_symbol . $min_price . " - " . $currency_symbol . $max_price . "\n";
8059 - }
8060 - }
8061 -
8062 - if (!empty($sku)) {
8063 - $content .= "SKU: " . $sku . "\n";
8064 - }
8065 -
8066 - // Get product categories
8067 - $categories = wp_get_post_terms($product_id, 'product_cat', array('fields' => 'names'));
8068 - if (!empty($categories) && !is_wp_error($categories)) {
8069 - $content .= "Categories: " . implode(', ', $categories) . "\n";
8070 - }
8071 -
8072 - // Get Custom Product Tabs (supports "Custom Product Tabs for WooCommerce" by Code Parrots)
8073 - $custom_tabs = get_post_meta($product_id, 'yikes_woo_products_tabs', true);
8074 - if (!empty($custom_tabs) && is_array($custom_tabs)) {
8075 - foreach ($custom_tabs as $tab) {
8076 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
8077 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
8078 -
8079 - if (!empty($tab_title) && !empty($tab_content)) {
8080 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
8081 - }
8082 - }
8083 - }
8084 -
8085 - // Also check for reusable/saved tabs applied to this product
8086 - $applied_saved_tabs = get_post_meta($product_id, 'yikes_woo_reusable_products_tabs_applied', true);
8087 - if (!empty($applied_saved_tabs) && is_array($applied_saved_tabs)) {
8088 - $saved_tabs = get_option('yikes_woo_reusable_products_tabs', array());
8089 - if (!empty($saved_tabs) && is_array($saved_tabs)) {
8090 - foreach ($applied_saved_tabs as $saved_tab_id) {
8091 - if (isset($saved_tabs[$saved_tab_id])) {
8092 - $tab = $saved_tabs[$saved_tab_id];
8093 - $tab_title = isset($tab['title']) ? $tab['title'] : (isset($tab['tab_title']) ? $tab['tab_title'] : '');
8094 - $tab_content = isset($tab['content']) ? $tab['content'] : '';
8095 -
8096 - if (!empty($tab_title) && !empty($tab_content)) {
8097 - $content .= "\n" . $tab_title . ": " . wp_strip_all_tags($tab_content) . "\n";
8098 - }
8099 - }
8100 - }
8101 - }
8102 - }
8103 -
8104 - return $this->mxchat_sanitize_content_for_api($content);
8105 -}
8106 -
8107 -/**
8108 - * Process a PDF page from the queue
8109 - */
8110 -private function mxchat_process_queue_pdf_page($item_data, $bot_id = 'default') {
8111 - $pdf_path = isset($item_data['pdf_path']) ? $item_data['pdf_path'] : '';
8112 - $pdf_url = isset($item_data['pdf_url']) ? $item_data['pdf_url'] : '';
8113 - $page_number = isset($item_data['page_number']) ? absint($item_data['page_number']) : 0;
8114 - $total_pages = isset($item_data['total_pages']) ? absint($item_data['total_pages']) : 0;
8115 -
8116 - if (empty($pdf_path) || !file_exists($pdf_path)) {
8117 - return new WP_Error('pdf_not_found', 'PDF file not found: ' . $pdf_path);
8118 - }
8119 -
8120 - if ($page_number < 1) {
8121 - return new WP_Error('invalid_page', 'Invalid page number');
8122 - }
8123 -
8124 - try {
8125 - mxchat_load_pdf_parser();
8126 - $parser = new \Smalot\PdfParser\Parser();
8127 - $pdf = $parser->parseFile($pdf_path);
8128 - $pages = $pdf->getPages();
8129 -
8130 - if (!isset($pages[$page_number - 1])) {
8131 - return new WP_Error('page_not_found', 'Page ' . $page_number . ' not found in PDF');
8132 - }
8133 -
8134 - $text = $pages[$page_number - 1]->getText();
8135 -
8136 - if (empty($text)) {
8137 - return new WP_Error('empty_page', 'Page ' . $page_number . ': No text could be extracted — page may contain only images, links, or non-standard encoding');
8138 - }
8139 -
8140 - $sanitized = $this->mxchat_sanitize_content_for_api($text);
8141 -
8142 - if (empty($sanitized)) {
8143 - 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');
8144 - }
8145 -
8146 - // Create metadata
8147 - $metadata = array(
8148 - 'document_type' => 'pdf',
8149 - 'total_pages' => $total_pages,
8150 - 'current_page' => $page_number,
8151 - 'source_url' => $pdf_url
8152 - );
8153 -
8154 - $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized;
8155 - $page_url = esc_url($pdf_url . "#page=" . $page_number);
8156 -
8157 - // Get bot-specific API key
8158 - $bot_options = $this->get_bot_options($bot_id);
8159 - $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
8160 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
8161 -
8162 - if (strpos($selected_model, 'voyage') === 0) {
8163 - $api_key = $options['voyage_api_key'] ?? '';
8164 - } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
8165 - $api_key = $options['gemini_api_key'] ?? '';
8166 - } else {
8167 - $api_key = $options['api_key'] ?? '';
8168 - }
8169 -
8170 - if (empty($api_key)) {
8171 - return new WP_Error('no_api_key', 'API key not configured for bot: ' . $bot_id);
8172 - }
8173 -
8174 - // Submit to database - UPDATED 2.5.6: Added content_type 'pdf'
8175 - $result = MxChat_Utils::submit_content_to_db(
8176 - $content_with_metadata,
8177 - $page_url,
8178 - $api_key,
8179 - null,
8180 - $bot_id,
8181 - 'pdf'
8182 - );
8183 -
8184 - return $result;
8185 -
8186 - } catch (Exception $e) {
8187 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
8188 - }
8189 -}
8190 -
8191 -/**
8192 - * AJAX: Get queue processing status
8193 - */
8194 -public function ajax_mxchat_get_queue_status() {
8195 - // Verify nonce and permissions
8196 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
8197 -
8198 - if (!current_user_can('manage_options')) {
8199 - wp_send_json_error('Unauthorized access');
8200 - }
8201 -
8202 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
8203 -
8204 - if (empty($queue_id)) {
8205 - wp_send_json_error('Missing queue ID');
8206 - }
8207 -
8208 - global $wpdb;
8209 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
8210 -
8211 - // Get counts by status
8212 - $counts = $wpdb->get_results($wpdb->prepare(
8213 - "SELECT status, COUNT(*) as count
8214 - FROM $table_name
8215 - WHERE queue_id = %s
8216 - GROUP BY status",
8217 - $queue_id
8218 - ), OBJECT_K);
8219 -
8220 - $total = 0;
8221 - $completed = 0;
8222 - $failed = 0;
8223 - $processing = 0;
8224 - $pending = 0;
8225 -
8226 - foreach ($counts as $status => $data) {
8227 - $count = absint($data->count);
8228 - $total += $count;
8229 -
8230 - switch ($status) {
8231 - case 'completed':
8232 - $completed = $count;
8233 - break;
8234 - case 'failed':
8235 - $failed = $count;
8236 - break;
8237 - case 'processing':
8238 - $processing = $count;
8239 - break;
8240 - case 'pending':
8241 - $pending = $count;
8242 - break;
8243 - }
8244 - }
8245 -
8246 - // Calculate percentage
8247 - $percentage = $total > 0 ? round((($completed + $failed) / $total) * 100) : 0;
8248 -
8249 - // Get failed items details (include all failed items, not just those that exhausted retries)
8250 - $failed_items = array();
8251 - if ($failed > 0) {
8252 - $failed_items = $wpdb->get_results($wpdb->prepare(
8253 - "SELECT item_type, item_data, error_message, attempts
8254 - FROM $table_name
8255 - WHERE queue_id = %s
8256 - AND status = 'failed'
8257 - ORDER BY id DESC
8258 - LIMIT 50",
8259 - $queue_id
8260 - ));
8261 - }
8262 -
8263 - // Get queue metadata
8264 - $source_url = $this->mxchat_get_queue_meta($queue_id, 'source_url');
8265 - $queue_type = $this->mxchat_get_queue_meta($queue_id, 'queue_type');
8266 -
8267 - // Determine if queue is complete
8268 - $is_complete = ($pending === 0 && $processing === 0);
8269 -
8270 - wp_send_json_success(array(
8271 - 'queue_id' => $queue_id,
8272 - 'queue_type' => $queue_type,
8273 - 'source_url' => $source_url,
8274 - 'total' => $total,
8275 - 'completed' => $completed,
8276 - 'failed' => $failed,
8277 - 'processing' => $processing,
8278 - 'pending' => $pending,
8279 - 'percentage' => $percentage,
8280 - 'is_complete' => $is_complete,
8281 - 'failed_items' => $failed_items,
8282 - 'status' => $is_complete ? 'complete' : 'processing'
8283 - ));
8284 -}
8285 -
8286 -/**
8287 - * AJAX: Clear completed queue
8288 - */
8289 -public function ajax_mxchat_clear_queue() {
8290 - // Verify nonce and permissions
8291 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
8292 -
8293 - if (!current_user_can('manage_options')) {
8294 - wp_send_json_error('Unauthorized access');
8295 - }
8296 -
8297 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
8298 -
8299 - if (empty($queue_id)) {
8300 - wp_send_json_error('Missing queue ID');
8301 - }
8302 -
8303 - global $wpdb;
8304 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
8305 - $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
8306 -
8307 - // Delete queue items
8308 - $wpdb->delete(
8309 - $table_name,
8310 - array('queue_id' => $queue_id),
8311 - array('%s')
8312 - );
8313 -
8314 - // Delete queue metadata
8315 - $wpdb->delete(
8316 - $meta_table,
8317 - array('queue_id' => $queue_id),
8318 - array('%s')
8319 - );
8320 -
8321 - wp_send_json_success(array(
8322 - 'message' => 'Queue cleared successfully'
8323 - ));
8324 -}
8325 -
8326 -/**
8327 - * AJAX: Retry failed items in queue
8328 - */
8329 -public function ajax_mxchat_retry_failed() {
8330 - // Verify nonce and permissions
8331 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
8332 -
8333 - if (!current_user_can('manage_options')) {
8334 - wp_send_json_error('Unauthorized access');
8335 - }
8336 -
8337 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
8338 -
8339 - if (empty($queue_id)) {
8340 - wp_send_json_error('Missing queue ID');
8341 - }
8342 -
8343 - global $wpdb;
8344 - $table_name = $wpdb->prefix . 'mxchat_processing_queue';
8345 -
8346 - // Reset failed items to pending and reset attempt count
8347 - $updated = $wpdb->update(
8348 - $table_name,
8349 - array(
8350 - 'status' => 'pending',
8351 - 'attempts' => 0,
8352 - 'error_message' => null
8353 - ),
8354 - array(
8355 - 'queue_id' => $queue_id,
8356 - 'status' => 'failed'
8357 - ),
8358 - array('%s', '%d', '%s'),
8359 - array('%s', '%s')
8360 - );
8361 -
8362 - wp_send_json_success(array(
8363 - 'message' => 'Reset ' . $updated . ' failed items for retry',
8364 - 'reset_count' => $updated
8365 - ));
8366 -}
8367 -
8368 -
8369 -public function ajax_mxchat_mark_queue_complete() {
8370 - check_ajax_referer('mxchat_queue_nonce', 'nonce');
8371 -
8372 - if (!current_user_can('manage_options')) {
8373 - wp_send_json_error('Unauthorized access');
8374 - }
8375 -
8376 - $queue_id = isset($_POST['queue_id']) ? sanitize_text_field($_POST['queue_id']) : '';
8377 -
8378 - if (empty($queue_id)) {
8379 - wp_send_json_error('Missing queue ID');
8380 - }
8381 -
8382 - // Clear active queue transients
8383 - if (strpos($queue_id, 'sitemap_') === 0) {
8384 - delete_transient('mxchat_active_queue_sitemap');
8385 - } else if (strpos($queue_id, 'pdf_') === 0) {
8386 - delete_transient('mxchat_active_queue_pdf');
8387 - }
8388 -
8389 - wp_send_json_success(array('message' => 'Queue marked as complete'));
8390 -}
8391 -
8392 3406
8393 3407 // ========================================
8394 3408 // STATIC ACCESS METHODS
8395 3409 // ========================================