Changeset 3430202
- Timestamp:
- 12/31/2025 04:28:36 PM (7 weeks ago)
- Location:
- mxchat-basic
- Files:
-
- 105 added
- 20 edited
-
tags/2.6.3 (added)
-
tags/2.6.3/admin (added)
-
tags/2.6.3/admin/class-ajax-handler.php (added)
-
tags/2.6.3/admin/class-knowledge-manager.php (added)
-
tags/2.6.3/admin/class-pinecone-manager.php (added)
-
tags/2.6.3/css (added)
-
tags/2.6.3/css/admin-add-ons.css (added)
-
tags/2.6.3/css/admin-style.css (added)
-
tags/2.6.3/css/chat-style.css (added)
-
tags/2.6.3/css/chat-transcripts.css (added)
-
tags/2.6.3/css/content-selector.css (added)
-
tags/2.6.3/css/intent-style.css (added)
-
tags/2.6.3/css/knowledge-style.css (added)
-
tags/2.6.3/css/test-panel.css (added)
-
tags/2.6.3/images (added)
-
tags/2.6.3/images/Icon-01.svg (added)
-
tags/2.6.3/images/Icon-02.svg (added)
-
tags/2.6.3/images/Icon-03.svg (added)
-
tags/2.6.3/images/Icon-04.svg (added)
-
tags/2.6.3/images/pro-only-dark.png (added)
-
tags/2.6.3/includes (added)
-
tags/2.6.3/includes/class-mxchat-addons.php (added)
-
tags/2.6.3/includes/class-mxchat-admin.php (added)
-
tags/2.6.3/includes/class-mxchat-chunker.php (added)
-
tags/2.6.3/includes/class-mxchat-integrator.php (added)
-
tags/2.6.3/includes/class-mxchat-meta-box.php (added)
-
tags/2.6.3/includes/class-mxchat-public.php (added)
-
tags/2.6.3/includes/class-mxchat-user.php (added)
-
tags/2.6.3/includes/class-mxchat-utils.php (added)
-
tags/2.6.3/includes/class-mxchat-woocommerce.php (added)
-
tags/2.6.3/includes/class-mxchat-word-handler.php (added)
-
tags/2.6.3/includes/pdf-parser (added)
-
tags/2.6.3/includes/pdf-parser/alt_autoload.php (added)
-
tags/2.6.3/includes/pdf-parser/src (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Config.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Document.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Element (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Element.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementArray.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementBoolean.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementDate.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementHexa.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementMissing.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementName.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementNull.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementNumeric.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementString.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementStruct.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementXRef.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Encoding (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Encoding.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Encoding/AbstractEncoding.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Encoding/EncodingLocator.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Encoding/ISOLatin1Encoding.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Encoding/ISOLatin9Encoding.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Encoding/MacRomanEncoding.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Encoding/PDFDocEncoding.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Encoding/PostScriptGlyphs.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Encoding/StandardEncoding.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Encoding/WinAnsiEncoding.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Exception (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Exception/EmptyPdfException.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Exception/EncodingNotFoundException.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Exception/MissingPdfHeaderException.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Exception/NotImplementedException.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Font (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Font.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Font/FontCIDFontType0.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Font/FontCIDFontType2.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Font/FontTrueType.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Font/FontType0.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Font/FontType1.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Font/FontType3.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Header.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/PDFObject.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Page.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Pages.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/Parser.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/RawData (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/RawData/FilterHelper.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/RawData/RawDataParser.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/XObject (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/XObject/Form.php (added)
-
tags/2.6.3/includes/pdf-parser/src/Smalot/PdfParser/XObject/Image.php (added)
-
tags/2.6.3/js (added)
-
tags/2.6.3/js/activation-script.js (added)
-
tags/2.6.3/js/admin-status.js (added)
-
tags/2.6.3/js/chat-script.js (added)
-
tags/2.6.3/js/content-selector.js (added)
-
tags/2.6.3/js/embedding-check.js (added)
-
tags/2.6.3/js/floating-script.js (added)
-
tags/2.6.3/js/knowledge-processing.js (added)
-
tags/2.6.3/js/meta-box.js (added)
-
tags/2.6.3/js/mxchat-admin.js (added)
-
tags/2.6.3/js/mxchat-test-streaming.js (added)
-
tags/2.6.3/js/mxchat_transcripts.js (added)
-
tags/2.6.3/js/my-color-picker.js (added)
-
tags/2.6.3/js/test-panel.js (added)
-
tags/2.6.3/languages (added)
-
tags/2.6.3/languages/mxchat.pot (added)
-
tags/2.6.3/mxchat-basic.php (added)
-
tags/2.6.3/readme.txt (added)
-
trunk/admin/class-ajax-handler.php (modified) (2 diffs)
-
trunk/admin/class-knowledge-manager.php (modified) (15 diffs)
-
trunk/admin/class-pinecone-manager.php (modified) (6 diffs)
-
trunk/css/admin-style.css (modified) (4 diffs)
-
trunk/css/chat-transcripts.css (modified) (1 diff)
-
trunk/css/knowledge-style.css (modified) (1 diff)
-
trunk/css/test-panel.css (modified) (1 diff)
-
trunk/includes/class-mxchat-admin.php (modified) (15 diffs)
-
trunk/includes/class-mxchat-chunker.php (added)
-
trunk/includes/class-mxchat-integrator.php (modified) (26 diffs)
-
trunk/includes/class-mxchat-public.php (modified) (1 diff)
-
trunk/includes/class-mxchat-utils.php (modified) (2 diffs)
-
trunk/js/chat-script.js (modified) (57 diffs)
-
trunk/js/content-selector.js (modified) (2 diffs)
-
trunk/js/knowledge-processing.js (modified) (1 diff)
-
trunk/js/mxchat-admin.js (modified) (1 diff)
-
trunk/js/mxchat-test-streaming.js (modified) (3 diffs)
-
trunk/js/mxchat_transcripts.js (modified) (4 diffs)
-
trunk/js/test-panel.js (modified) (2 diffs)
-
trunk/mxchat-basic.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
mxchat-basic/trunk/admin/class-ajax-handler.php
r3428255 r3430202 281 281 'contextual_awareness_toggle', 282 282 'enable_email_block', 283 'enable_name_field' 283 'enable_name_field', 284 'show_frontend_debugger' 284 285 ])) { 285 286 //error_log('MXChat Save: Processing toggle: ' . $name); … … 503 504 } 504 505 506 // Handle chunking settings - use direct DB access to bypass WordPress filters 507 if (strpos($name, 'mxchat_chunk') === 0 || $name === 'mxchat_chunking_enabled') { 508 global $wpdb; 509 510 // Get current options directly from database 511 $current_options_raw = $wpdb->get_var( 512 $wpdb->prepare( 513 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", 514 'mxchat_options' 515 ) 516 ); 517 518 $options = $current_options_raw !== null ? maybe_unserialize($current_options_raw) : array(); 519 if (!is_array($options)) { 520 $options = array(); 521 } 522 523 // Update the specific chunking field 524 if ($name === 'mxchat_chunking_enabled') { 525 $options['chunking_enabled'] = in_array($value, array('on', '1', 'true', true), true); 526 } elseif ($name === 'mxchat_chunk_size') { 527 $options['chunk_size'] = max(1000, min(10000, intval($value))); 528 } 529 530 // Save directly to database 531 $serialized_options = maybe_serialize($options); 532 533 $option_exists = $wpdb->get_var( 534 $wpdb->prepare( 535 "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name = %s", 536 'mxchat_options' 537 ) 538 ); 539 540 if ($option_exists > 0) { 541 $save_result = $wpdb->update( 542 $wpdb->options, 543 array('option_value' => $serialized_options), 544 array('option_name' => 'mxchat_options'), 545 array('%s'), 546 array('%s') 547 ); 548 } else { 549 $save_result = $wpdb->insert( 550 $wpdb->options, 551 array( 552 'option_name' => 'mxchat_options', 553 'option_value' => $serialized_options, 554 'autoload' => 'yes' 555 ), 556 array('%s', '%s', '%s') 557 ); 558 } 559 560 // Clear object cache for this option 561 wp_cache_delete('mxchat_options', 'options'); 562 563 if ($save_result !== false) { 564 wp_send_json_success(['message' => esc_html__('Chunking setting saved', 'mxchat')]); 565 } else { 566 wp_send_json_error(['message' => esc_html__('Failed to save chunking setting', 'mxchat')]); 567 } 568 return; 569 } 570 505 571 // Handle other prompts options 506 572 $options = get_option('mxchat_prompts_options', []); -
mxchat-basic/trunk/admin/class-knowledge-manager.php
r3413311 r3430202 42 42 add_action('admin_post_mxchat_delete_pinecone_prompt', array($this, 'mxchat_handle_pinecone_prompt_delete')); 43 43 add_action('wp_ajax_mxchat_delete_pinecone_prompt', array($this, 'ajax_mxchat_delete_pinecone_prompt')); 44 add_action('wp_ajax_mxchat_delete_chunks_by_url', array($this, 'ajax_mxchat_delete_chunks_by_url')); 44 45 add_action('wp_ajax_mxchat_update_role_restriction', array($this, 'ajax_mxchat_update_role_restriction')); 45 46 … … 632 633 $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content); 633 634 635 error_log('[MXCHAT-URL-DEBUG] URL: ' . $submitted_url); 636 error_log('[MXCHAT-URL-DEBUG] Extracted page_content length: ' . strlen($page_content)); 637 error_log('[MXCHAT-URL-DEBUG] Sanitized content length: ' . strlen($sanitized_content)); 638 error_log('[MXCHAT-URL-DEBUG] Content preview: ' . substr($sanitized_content, 0, 500)); 639 634 640 if (empty($sanitized_content)) { 635 641 set_transient('mxchat_admin_notice_error', … … 641 647 } 642 648 643 // For single URLs, process immediately (not queued) 644 $embedding_vector = $this->mxchat_generate_embedding($sanitized_content, $bot_id); 645 646 if (is_string($embedding_vector)) { 647 $error_message = esc_html__('Failed to generate embedding: ', 'mxchat') . esc_html($embedding_vector); 649 // For single URLs, process immediately using submit_content_to_db 650 // This handles chunking automatically for large content 651 $db_result = MxChat_Utils::submit_content_to_db( 652 $sanitized_content, 653 $submitted_url, 654 $api_key, 655 null, 656 $bot_id, 657 'url' // content_type 658 ); 659 660 if (is_wp_error($db_result)) { 661 $error_message = esc_html__('Failed to store content in database: ', 'mxchat') . esc_html($db_result->get_error_message()); 648 662 set_transient('mxchat_admin_notice_error', $error_message, 30); 649 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));650 exit;651 }652 653 if (is_array($embedding_vector)) {654 $db_result = MxChat_Utils::submit_content_to_db(655 $sanitized_content,656 $submitted_url,657 $api_key,658 null,659 $bot_id660 );661 662 if (is_wp_error($db_result)) {663 $error_message = esc_html__('Failed to store content in database: ', 'mxchat') . esc_html($db_result->get_error_message());664 set_transient('mxchat_admin_notice_error', $error_message, 30);665 } else {666 $success_message = esc_html__('URL content successfully submitted!', 'mxchat');667 set_transient('mxchat_admin_notice_success', $success_message, 30);668 }669 663 } else { 670 $ error_message = esc_html__('Failed to generate embedding: Unexpected result type. Please check your API key and try again.', 'mxchat');671 set_transient('mxchat_admin_notice_ error', $error_message, 30);664 $success_message = esc_html__('URL content successfully submitted!', 'mxchat'); 665 set_transient('mxchat_admin_notice_success', $success_message, 30); 672 666 } 673 667 … … 862 856 863 857 // For debugging purposes 864 $debugEnabled = false; // Set to true to enable debugging output858 $debugEnabled = true; // Set to true to enable debugging output 865 859 $debug = function($message) use ($debugEnabled) { 866 860 if ($debugEnabled) { 867 //error_log('[MXCHAT-DEBUG] ' . $message);861 error_log('[MXCHAT-EXTRACT-DEBUG] ' . $message); 868 862 } 869 863 }; … … 966 960 ]; 967 961 968 // First handle Elementor content 962 // First handle Elementor content - get only leaf widget containers to avoid duplicates 969 963 $debug("Checking for Elementor content"); 970 $elementor_widgets = $xpath->query('//div[contains(@class, "elementor-element")]//div[contains(@class, "elementor-widget-container")]'); 964 // Get widget containers that are direct children of widgets (not nested inside other widget containers) 965 $elementor_widgets = $xpath->query('//div[contains(@class, "elementor-widget")]//div[contains(@class, "elementor-widget-container")]'); 971 966 if ($elementor_widgets && $elementor_widgets->length > 0) { 972 967 $debug("Found Elementor widgets"); 968 $seen_content = array(); // Track seen content to avoid duplicates 973 969 $combined_content = ''; 974 970 foreach ($elementor_widgets as $widget) { 975 971 $widget_content = $dom->saveHTML($widget); 976 972 if (!empty($widget_content)) { 977 $combined_content .= $widget_content; 973 // Create a hash of the content to detect duplicates 974 $content_hash = md5($widget_content); 975 if (!isset($seen_content[$content_hash])) { 976 $seen_content[$content_hash] = true; 977 $combined_content .= $widget_content; 978 } 978 979 } 979 980 } … … 989 990 $nodes = $xpath->query($selector); 990 991 if ($nodes && $nodes->length > 0) { 991 $debug("Found matches for selector: " . $selector); 992 $content = ''; 993 foreach ($nodes as $node) { 994 $content .= $dom->saveHTML($node); 995 } 992 $debug("Found " . $nodes->length . " matches for selector: " . $selector); 993 // Only take the FIRST matching node to avoid duplicate content 994 // (pages often have nested or multiple containers with same class) 995 $content = $dom->saveHTML($nodes->item(0)); 996 996 if (!empty($content)) { 997 $debug("Returning content from selector: " . $selector );997 $debug("Returning content from selector: " . $selector . " (first match only)"); 998 998 return $content; 999 999 } … … 1390 1390 global $wpdb; 1391 1391 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; 1392 $processed_items = $wpdb->get_results("SELECT id, source_url, timestamp FROM {$table_name}"); 1393 1392 $processed_items = $wpdb->get_results("SELECT id, source_url, article_content, timestamp FROM {$table_name}"); 1393 1394 // Group items by source_url to count chunks 1395 $url_chunk_counts = array(); 1396 $url_latest_timestamp = array(); 1397 $url_first_id = array(); 1398 1394 1399 if (!empty($processed_items)) { 1395 1400 foreach ($processed_items as $item) { 1396 // Use improved URL matching that works for all post types 1397 $post_id = $this->mxchat_url_to_post_id_improved($item->source_url); 1398 1401 $url = $item->source_url; 1402 if (empty($url)) continue; 1403 1404 // Count chunks per URL 1405 if (!isset($url_chunk_counts[$url])) { 1406 $url_chunk_counts[$url] = 0; 1407 $url_latest_timestamp[$url] = $item->timestamp; 1408 $url_first_id[$url] = $item->id; 1409 } 1410 $url_chunk_counts[$url]++; 1411 1412 // Track latest timestamp 1413 if (strtotime($item->timestamp) > strtotime($url_latest_timestamp[$url])) { 1414 $url_latest_timestamp[$url] = $item->timestamp; 1415 } 1416 } 1417 1418 // Now build processed_data with chunk counts 1419 foreach ($url_chunk_counts as $url => $chunk_count) { 1420 $post_id = $this->mxchat_url_to_post_id_improved($url); 1421 1399 1422 if ($post_id) { 1400 1423 $processed_data[$post_id] = array( 1401 'db_id' => $item->id, 1402 'timestamp' => $item->timestamp, 1403 'url' => $item->source_url, 1404 'source' => 'wordpress' 1424 'db_id' => $url_first_id[$url], 1425 'timestamp' => $url_latest_timestamp[$url], 1426 'url' => $url, 1427 'source' => 'wordpress', 1428 'chunk_count' => $chunk_count 1405 1429 ); 1406 1430 } … … 1452 1476 } 1453 1477 1478 // Get chunk count for this item 1479 $chunk_count = 0; 1480 if ($is_processed && isset($processed_data[$id]['chunk_count'])) { 1481 $chunk_count = intval($processed_data[$id]['chunk_count']); 1482 } 1483 1454 1484 $content_items[] = array( 1455 1485 'id' => $id, … … 1464 1494 'processed_date' => $processed_date, 1465 1495 'db_record_id' => $db_record_id, 1466 'data_source' => $data_source 1496 'data_source' => $data_source, 1497 'chunk_count' => $chunk_count 1467 1498 ); 1468 1499 } … … 1801 1832 } 1802 1833 1803 $content = substr($content, 0, 10000); // Limit content size 1804 1834 // Debug logging for WordPress Import content 1835 error_log('[MXCHAT-WP-IMPORT-DEBUG] Post ID: ' . $post_id . ' Title: ' . $post->post_title); 1836 error_log('[MXCHAT-WP-IMPORT-DEBUG] Raw post_content length: ' . strlen($post->post_content)); 1837 error_log('[MXCHAT-WP-IMPORT-DEBUG] Final content length: ' . strlen($content)); 1838 error_log('[MXCHAT-WP-IMPORT-DEBUG] Content preview: ' . substr($content, 0, 300)); 1839 1840 // Note: Removed 10,000 char limit - chunking now handles large content properly 1841 1805 1842 // Get bot-specific API key 1806 1843 $bot_options = $this->get_bot_options($bot_id); … … 2222 2259 } 2223 2260 2224 // Convert matches to processed data format 2261 // Convert matches to processed data format, grouping by URL to count chunks 2225 2262 $processed_data = array(); 2263 $url_chunk_counts = array(); 2226 2264 2227 2265 foreach ($all_matches as $match) { … … 2233 2271 $post_id = url_to_postid($source_url); 2234 2272 if ($post_id) { 2273 // Count chunks per post_id 2274 if (!isset($url_chunk_counts[$post_id])) { 2275 $url_chunk_counts[$post_id] = 0; 2276 } 2277 $url_chunk_counts[$post_id]++; 2278 2235 2279 $created_at = $metadata['created_at'] ?? ''; 2236 2280 $processed_date = 'Recently'; … … 2243 2287 } 2244 2288 2245 $processed_data[$post_id] = array( 2246 'db_id' => $match_id, 2247 'processed_date' => $processed_date, 2248 'url' => $source_url, 2249 'source' => 'pinecone', 2250 'timestamp' => $timestamp ?? current_time('timestamp') 2251 ); 2289 // Only store if not already set, or update with newer timestamp 2290 if (!isset($processed_data[$post_id]) || 2291 ($timestamp ?? 0) > ($processed_data[$post_id]['timestamp'] ?? 0)) { 2292 $processed_data[$post_id] = array( 2293 'db_id' => $match_id, 2294 'processed_date' => $processed_date, 2295 'url' => $source_url, 2296 'source' => 'pinecone', 2297 'timestamp' => $timestamp ?? current_time('timestamp') 2298 ); 2299 } 2252 2300 } 2301 } 2302 } 2303 2304 // Add chunk counts to processed data 2305 foreach ($url_chunk_counts as $post_id => $chunk_count) { 2306 if (isset($processed_data[$post_id])) { 2307 $processed_data[$post_id]['chunk_count'] = $chunk_count; 2253 2308 } 2254 2309 } … … 3376 3431 } 3377 3432 3378 // Check if Pinecone is enabled 3379 $pinecone_options = get_option('mxchat_pinecone_addon_options', array()); 3380 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1'; 3381 3382 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) { 3383 // Delete from Pinecone 3384 $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options); 3385 } else { 3386 // Delete from WordPress DB 3387 global $wpdb; 3388 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; 3389 3390 $result = $wpdb->delete( 3391 $table_name, 3392 array('source_url' => $source_url), 3393 array('%s') 3394 ); 3395 3396 if ($result === false) { 3397 //error_log('MXChat: WordPress DB deletion failed for URL: ' . $source_url); 3398 } 3433 // Use chunk-aware deletion (handles both chunked and non-chunked content) 3434 $delete_result = MxChat_Utils::delete_chunks_for_url($source_url, 'default'); 3435 3436 if (is_wp_error($delete_result)) { 3437 //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message()); 3399 3438 } 3400 3439 } … … 3735 3774 } 3736 3775 3776 exit; 3777 } 3778 3779 /** 3780 * Handle deletion of all chunks for a given source URL via AJAX 3781 * Follows the same pattern as ajax_mxchat_delete_pinecone_prompt 3782 */ 3783 public function ajax_mxchat_delete_chunks_by_url() { 3784 // Verify nonce and permissions 3785 if (!check_ajax_referer('mxchat_delete_chunks_nonce', 'nonce', false)) { 3786 wp_send_json_error('Invalid nonce'); 3787 exit; 3788 } 3789 3790 if (!current_user_can('manage_options')) { 3791 wp_send_json_error('Unauthorized access'); 3792 exit; 3793 } 3794 3795 $source_url = isset($_POST['source_url']) ? esc_url_raw($_POST['source_url']) : ''; 3796 $data_source = isset($_POST['data_source']) ? sanitize_text_field($_POST['data_source']) : 'wordpress'; 3797 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default'; 3798 3799 if (empty($source_url)) { 3800 wp_send_json_error('Missing source URL'); 3801 exit; 3802 } 3803 3804 // Generate the base vector ID from the source URL (same as how chunks are created) 3805 $base_vector_id = md5($source_url); 3806 3807 if ($data_source === 'pinecone') { 3808 // Get bot-specific Pinecone settings (same as working delete function) 3809 $pinecone_manager = MxChat_Pinecone_Manager::get_instance(); 3810 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id); 3811 3812 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1'; 3813 3814 if (!$use_pinecone || empty($pinecone_options['mxchat_pinecone_api_key'])) { 3815 wp_send_json_error('Pinecone is not properly configured for bot: ' . $bot_id); 3816 exit; 3817 } 3818 3819 $api_key = $pinecone_options['mxchat_pinecone_api_key']; 3820 $host = $pinecone_options['mxchat_pinecone_host']; 3821 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? ''; 3822 3823 // Collect all vector IDs to delete 3824 $vectors_to_delete = array(); 3825 3826 // Add the original single-vector ID (for non-chunked content) 3827 $vectors_to_delete[] = $base_vector_id; 3828 3829 // Use Pinecone list API to find all chunk vectors with this prefix 3830 // NOTE: Pinecone List API is a GET request with query parameters, not POST 3831 $prefix = $base_vector_id . '_chunk_'; 3832 3833 $query_params = array( 3834 'prefix' => $prefix, 3835 'limit' => 100 3836 ); 3837 3838 if (!empty($namespace)) { 3839 $query_params['namespace'] = $namespace; 3840 } 3841 3842 $list_url = "https://{$host}/vectors/list?" . http_build_query($query_params); 3843 3844 $list_response = wp_remote_get($list_url, array( 3845 'headers' => array( 3846 'Api-Key' => $api_key, 3847 'accept' => 'application/json' 3848 ), 3849 'timeout' => 30 3850 )); 3851 3852 if (!is_wp_error($list_response)) { 3853 $list_body_response = wp_remote_retrieve_body($list_response); 3854 $list_data = json_decode($list_body_response, true); 3855 if (!empty($list_data['vectors'])) { 3856 foreach ($list_data['vectors'] as $vector) { 3857 if (isset($vector['id'])) { 3858 $vectors_to_delete[] = $vector['id']; 3859 } 3860 } 3861 } 3862 } 3863 3864 if (empty($vectors_to_delete)) { 3865 wp_send_json_success(array( 3866 'message' => 'No vectors found to delete', 3867 'source_url' => $source_url 3868 )); 3869 exit; 3870 } 3871 3872 // Delete all vectors using the same endpoint as the working function 3873 $delete_url = "https://{$host}/vectors/delete"; 3874 3875 $delete_body = array( 3876 'ids' => $vectors_to_delete 3877 ); 3878 3879 if (!empty($namespace)) { 3880 $delete_body['namespace'] = $namespace; 3881 } 3882 3883 $delete_response = wp_remote_post($delete_url, array( 3884 'headers' => array( 3885 'Api-Key' => $api_key, 3886 'accept' => 'application/json', 3887 'content-type' => 'application/json' 3888 ), 3889 'body' => wp_json_encode($delete_body), 3890 'timeout' => 30 3891 )); 3892 3893 if (is_wp_error($delete_response)) { 3894 wp_send_json_error('Failed to delete from Pinecone: ' . $delete_response->get_error_message()); 3895 exit; 3896 } 3897 3898 $response_code = wp_remote_retrieve_response_code($delete_response); 3899 3900 if ($response_code !== 200) { 3901 wp_send_json_error('Pinecone API error (HTTP ' . $response_code . ')'); 3902 exit; 3903 } 3904 3905 wp_send_json_success(array( 3906 'message' => 'All chunks deleted successfully from Pinecone', 3907 'source_url' => $source_url, 3908 'deleted_count' => count($vectors_to_delete) 3909 )); 3910 3911 } else { 3912 // WordPress database deletion 3913 global $wpdb; 3914 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; 3915 3916 $result = $wpdb->delete( 3917 $table_name, 3918 array('source_url' => $source_url), 3919 array('%s') 3920 ); 3921 3922 if ($result === false) { 3923 wp_send_json_error('Failed to delete from database: ' . $wpdb->last_error); 3924 exit; 3925 } 3926 3927 wp_send_json_success(array( 3928 'message' => 'All chunks deleted successfully from database', 3929 'source_url' => $source_url, 3930 'deleted_count' => $result 3931 )); 3932 } 3933 3737 3934 exit; 3738 3935 } -
mxchat-basic/trunk/admin/class-pinecone-manager.php
r3428255 r3430202 73 73 } 74 74 75 // Handle pagination 76 $total = count($all_records); 75 // UPDATED 2.6.3: Group records by source_url for chunk-aware pagination 76 // This ensures pagination shows X entries per page, not X chunks 77 $grouped_by_url = array(); 78 $empty_url_records = array(); 79 80 foreach ($all_records as $record) { 81 $source_url = $record->source_url ?? ''; 82 if (empty($source_url)) { 83 $empty_url_records[] = $record; 84 } else { 85 if (!isset($grouped_by_url[$source_url])) { 86 $grouped_by_url[$source_url] = array(); 87 } 88 $grouped_by_url[$source_url][] = $record; 89 } 90 } 91 92 // Count unique entries (unique URLs + individual empty-URL records) 93 $total_unique_entries = count($grouped_by_url) + count($empty_url_records); 94 95 // Paginate by unique entries 77 96 $offset = ($page - 1) * $per_page; 78 $paged_records = array_slice($all_records, $offset, $per_page); 97 98 // Build ordered list of URL groups (newest first based on first record) 99 $url_groups_ordered = array_keys($grouped_by_url); 100 101 // Get the URLs for this page 102 $page_urls = array_slice($url_groups_ordered, $offset, $per_page); 103 104 // Collect all records for this page's URLs 105 $paged_records = array(); 106 foreach ($page_urls as $url) { 107 foreach ($grouped_by_url[$url] as $record) { 108 $paged_records[] = $record; 109 } 110 } 111 112 // Add empty-URL records if they fall within this page's range 113 $remaining_slots = $per_page - count($page_urls); 114 $empty_offset = max(0, $offset - count($grouped_by_url)); 115 if ($remaining_slots > 0 && $empty_offset < count($empty_url_records)) { 116 $empty_page_records = array_slice($empty_url_records, $empty_offset, $remaining_slots); 117 $paged_records = array_merge($paged_records, $empty_page_records); 118 } 79 119 80 120 return array( 81 121 'data' => $paged_records, 82 'total' => $total ,122 'total' => $total_unique_entries, 83 123 'total_in_database' => $total_in_database, 84 124 'showing_recent_only' => ($total_in_database > 500) … … 192 232 'created_at' => $created_at, 193 233 'data_source' => 'pinecone', 194 'relevance_score' => $match['score'] ?? 0 234 'relevance_score' => $match['score'] ?? 0, 235 'chunk_index' => isset($metadata['chunk_index']) ? intval($metadata['chunk_index']) : null, 236 'total_chunks' => isset($metadata['total_chunks']) ? intval($metadata['total_chunks']) : null, 237 'is_chunked' => isset($metadata['is_chunked']) ? (bool) $metadata['is_chunked'] : false 195 238 ); 196 239 } … … 415 458 'bot_id' => $bot_id, 416 459 'created_at' => $created_at, 417 'data_source' => 'pinecone' 460 'data_source' => 'pinecone', 461 'chunk_index' => isset($metadata['chunk_index']) ? intval($metadata['chunk_index']) : null, 462 'total_chunks' => isset($metadata['total_chunks']) ? intval($metadata['total_chunks']) : null, 463 'is_chunked' => isset($metadata['is_chunked']) ? (bool) $metadata['is_chunked'] : false 418 464 ); 419 465 } … … 583 629 'bot_id' => $bot_id, 584 630 'created_at' => $created_at, 585 'data_source' => 'pinecone' 631 'data_source' => 'pinecone', 632 'chunk_index' => isset($metadata['chunk_index']) ? intval($metadata['chunk_index']) : null, 633 'total_chunks' => isset($metadata['total_chunks']) ? intval($metadata['total_chunks']) : null, 634 'is_chunked' => isset($metadata['is_chunked']) ? (bool) $metadata['is_chunked'] : false 586 635 ); 587 636 } … … 666 715 'bot_id' => $bot_id, 667 716 'created_at' => $created_at, 668 'data_source' => 'pinecone' 717 'data_source' => 'pinecone', 718 'chunk_index' => isset($metadata['chunk_index']) ? intval($metadata['chunk_index']) : null, 719 'total_chunks' => isset($metadata['total_chunks']) ? intval($metadata['total_chunks']) : null, 720 'is_chunked' => isset($metadata['is_chunked']) ? (bool) $metadata['is_chunked'] : false 669 721 ); 670 722 } … … 841 893 'bot_id' => $bot_id, 842 894 'created_at' => $created_at, 843 'data_source' => 'pinecone' 895 'data_source' => 'pinecone', 896 'chunk_index' => isset($metadata['chunk_index']) ? intval($metadata['chunk_index']) : null, 897 'total_chunks' => isset($metadata['total_chunks']) ? intval($metadata['total_chunks']) : null, 898 'is_chunked' => isset($metadata['is_chunked']) ? (bool) $metadata['is_chunked'] : false 844 899 ); 845 900 -
mxchat-basic/trunk/css/admin-style.css
r3428255 r3430202 4692 4692 4693 4693 .mxchat-stat-card { 4694 background: #f6f7f7;4695 4694 padding: 20px; 4696 4695 border-radius: 12px; … … 4749 4748 4750 4749 .mxchat-stat-accent { 4751 background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);4752 4750 border: 1px solid #e9d5ff; 4753 4751 } … … 6069 6067 } 6070 6068 6069 /* Matches Summary */ 6070 .mxchat-rag-matches-summary { 6071 margin: 0 0 16px 0; 6072 font-size: 0.875rem; 6073 color: #334155; 6074 font-weight: 500; 6075 } 6076 6077 /* Chunk Badge */ 6078 .mxchat-rag-chunk-badge { 6079 display: inline-block; 6080 padding: 3px 8px; 6081 font-size: 0.6875rem; 6082 font-weight: 600; 6083 background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); 6084 color: #fff; 6085 border-radius: 10px; 6086 text-transform: uppercase; 6087 letter-spacing: 0.02em; 6088 } 6089 6090 /* Chunk Toggle */ 6091 .mxchat-rag-chunk-toggle { 6092 display: block; 6093 padding: 10px 0; 6094 font-size: 0.75rem; 6095 font-weight: 600; 6096 color: #6366f1; 6097 cursor: pointer; 6098 border-top: 1px solid #e2e8f0; 6099 margin-top: 12px; 6100 transition: color 0.15s ease; 6101 } 6102 6103 .mxchat-rag-chunk-toggle:hover { 6104 color: #4f46e5; 6105 } 6106 6107 /* Chunk Details */ 6108 .mxchat-rag-chunk-details { 6109 display: none; 6110 padding: 8px 0 0 0; 6111 } 6112 6113 /* Chunk Row */ 6114 .mxchat-rag-chunk-row { 6115 display: flex; 6116 align-items: center; 6117 padding: 8px 10px; 6118 margin: 4px 0; 6119 border-radius: 6px; 6120 font-size: 0.8125rem; 6121 gap: 10px; 6122 } 6123 6124 .mxchat-rag-chunk-row.chunk-used { 6125 background: rgba(16, 185, 129, 0.1); 6126 } 6127 6128 .mxchat-rag-chunk-row.chunk-not-used { 6129 background: rgba(100, 116, 139, 0.08); 6130 } 6131 6132 .mxchat-rag-chunk-icon { 6133 font-size: 0.875rem; 6134 width: 18px; 6135 text-align: center; 6136 } 6137 6138 .chunk-used .mxchat-rag-chunk-icon { 6139 color: #10b981; 6140 } 6141 6142 .chunk-not-used .mxchat-rag-chunk-icon { 6143 color: #94a3b8; 6144 } 6145 6146 .mxchat-rag-chunk-num { 6147 font-weight: 600; 6148 color: #334155; 6149 flex: 1; 6150 } 6151 6152 .mxchat-rag-chunk-score { 6153 font-weight: 600; 6154 color: #64748b; 6155 font-variant-numeric: tabular-nums; 6156 } 6157 6071 6158 /* No Matches */ 6072 6159 .mxchat-rag-no-matches { … … 6210 6297 .mxchat-radio-label input[type="radio"] { 6211 6298 margin: 0; 6212 width: 1 8px;6213 height: 1 8px;6299 width: 16px; 6300 height: 16px; 6214 6301 accent-color: #2271b1; 6215 6302 } -
mxchat-basic/trunk/css/chat-transcripts.css
r3408040 r3430202 297 297 } 298 298 299 /* Message content markdown styling */ 300 .mxchat-message-content a { 301 color: #3b82f6; 302 text-decoration: none; 303 } 304 305 .mxchat-message-content a:hover { 306 text-decoration: underline; 307 } 308 309 .mxchat-message-content code { 310 background: rgba(0, 0, 0, 0.06); 311 padding: 2px 5px; 312 border-radius: 3px; 313 font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; 314 font-size: 0.9em; 315 } 316 317 .mxchat-message-content pre { 318 background: #1e293b; 319 color: #e2e8f0; 320 padding: 12px 16px; 321 border-radius: 6px; 322 overflow-x: auto; 323 margin: 8px 0; 324 } 325 326 .mxchat-message-content pre code { 327 background: none; 328 padding: 0; 329 color: inherit; 330 } 331 332 .mxchat-message-content strong { 333 font-weight: 600; 334 } 335 336 .mxchat-message-content em { 337 font-style: italic; 338 } 339 340 .mxchat-message-content h1, 341 .mxchat-message-content h2, 342 .mxchat-message-content h3, 343 .mxchat-message-content h4, 344 .mxchat-message-content h5, 345 .mxchat-message-content h6 { 346 margin: 12px 0 8px 0; 347 font-weight: 600; 348 line-height: 1.3; 349 color: #1f2937; 350 } 351 352 .mxchat-message-content h1 { font-size: 1.4em; } 353 .mxchat-message-content h2 { font-size: 1.3em; } 354 .mxchat-message-content h3 { font-size: 1.2em; } 355 .mxchat-message-content h4 { font-size: 1.1em; } 356 .mxchat-message-content h5 { font-size: 1em; } 357 .mxchat-message-content h6 { font-size: 0.95em; } 358 359 .mxchat-message-content ul, 360 .mxchat-message-content ol { 361 margin: 8px 0; 362 padding-left: 24px; 363 } 364 365 .mxchat-message-content li { 366 margin: 4px 0; 367 } 368 369 .mxchat-message-content p { 370 margin: 8px 0; 371 } 372 299 373 .mxchat-timestamp { 300 374 font-size: 12px; -
mxchat-basic/trunk/css/knowledge-style.css
r3406402 r3430202 3037 3037 margin-left: 10px; 3038 3038 } 3039 3040 /* ===== Chunk Grouping Styles ===== */ 3041 .mxchat-chunk-group-header { 3042 background: rgba(120, 115, 245, 0.04) !important; 3043 border-left: 3px solid #7873f5; 3044 } 3045 3046 .mxchat-chunk-group-header:hover { 3047 background: rgba(120, 115, 245, 0.08) !important; 3048 } 3049 3050 .mxchat-chunk-group-info { 3051 display: flex; 3052 align-items: center; 3053 gap: 12px; 3054 } 3055 3056 .mxchat-chunk-toggle { 3057 flex-shrink: 0; 3058 width: 28px; 3059 height: 28px; 3060 border: none; 3061 background: linear-gradient(135deg, rgba(250, 115, 230, 0.1), rgba(120, 115, 245, 0.1)); 3062 border-radius: 6px; 3063 cursor: pointer; 3064 display: inline-flex; 3065 align-items: center; 3066 justify-content: center; 3067 transition: all 0.3s ease; 3068 padding: 0; 3069 } 3070 3071 .mxchat-chunk-toggle:hover { 3072 background: linear-gradient(135deg, rgba(250, 115, 230, 0.2), rgba(120, 115, 245, 0.2)); 3073 transform: scale(1.05); 3074 } 3075 3076 .mxchat-chunk-toggle .dashicons { 3077 font-size: 16px; 3078 color: #7873f5; 3079 transition: transform 0.3s ease; 3080 } 3081 3082 .mxchat-chunk-toggle.expanded .dashicons { 3083 transform: rotate(90deg); 3084 } 3085 3086 .mxchat-chunk-badge { 3087 display: inline-flex; 3088 align-items: center; 3089 padding: 4px 10px; 3090 background: linear-gradient(135deg, #7873f5, #3ac9d1); 3091 color: white; 3092 border-radius: 12px; 3093 font-size: 12px; 3094 font-weight: 600; 3095 white-space: nowrap; 3096 } 3097 3098 .mxchat-chunk-preview { 3099 flex: 1; 3100 color: #666; 3101 font-size: 13px; 3102 overflow: hidden; 3103 text-overflow: ellipsis; 3104 white-space: nowrap; 3105 } 3106 3107 .mxchat-chunk-row { 3108 border-left: 3px solid #e0e0e0; 3109 } 3110 3111 .mxchat-chunk-row td:first-child { 3112 padding-left: 30px !important; 3113 } 3114 3115 .mxchat-chunk-indicator { 3116 display: inline-flex; 3117 align-items: center; 3118 padding: 2px 8px; 3119 background: #f0f0f0; 3120 color: #666; 3121 border-radius: 4px; 3122 font-size: 11px; 3123 font-weight: 500; 3124 } 3125 3126 .mxchat-chunk-label { 3127 color: #999; 3128 font-size: 12px; 3129 font-style: italic; 3130 } 3131 3132 /* Delete button for chunk groups */ 3133 .delete-button-group { 3134 background: none; 3135 border: none; 3136 cursor: pointer; 3137 padding: 4px; 3138 border-radius: 4px; 3139 transition: all 0.2s ease; 3140 } 3141 3142 .delete-button-group:hover { 3143 background: rgba(255, 0, 0, 0.1); 3144 } 3145 3146 .delete-button-group .dashicons { 3147 color: #dc3545; 3148 } 3149 3150 .delete-button-group .dashicons.spin { 3151 animation: mxchat-spin 1s linear infinite; 3152 } 3153 3154 @keyframes mxchat-spin { 3155 from { transform: rotate(0deg); } 3156 to { transform: rotate(360deg); } 3157 } -
mxchat-basic/trunk/css/test-panel.css
r3315188 r3430202 329 329 } 330 330 331 /* Chunk Badge (legacy) */ 332 .chunk-badge { 333 display: inline-flex; 334 align-items: center; 335 padding: 2px 8px; 336 font-size: 11px; 337 font-weight: 600; 338 background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); 339 color: #fff; 340 border-radius: 10px; 341 margin-left: 4px; 342 text-transform: uppercase; 343 letter-spacing: 0.3px; 344 } 345 346 /* Matches Header Subheader */ 347 .matches-subheader { 348 font-size: 11px; 349 font-weight: 400; 350 color: #64748b; 351 margin-left: 6px; 352 } 353 354 /* Chunk Summary Badge */ 355 .chunk-summary { 356 display: inline-flex; 357 align-items: center; 358 padding: 2px 8px; 359 font-size: 11px; 360 font-weight: 600; 361 background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); 362 color: #fff; 363 border-radius: 10px; 364 margin-left: 8px; 365 } 366 367 /* Chunk Expand Toggle */ 368 .chunk-expand-toggle { 369 display: block; 370 padding: 8px 16px; 371 font-size: 11px; 372 font-weight: 600; 373 color: #6366f1; 374 cursor: pointer; 375 border-top: 1px solid rgba(0, 0, 0, 0.05); 376 transition: background 0.15s ease; 377 } 378 379 .chunk-expand-toggle:hover { 380 background: rgba(99, 102, 241, 0.08); 381 } 382 383 /* Chunk Details Container */ 384 .chunk-details { 385 display: none; 386 padding: 0 16px 12px 16px; 387 background: rgba(0, 0, 0, 0.02); 388 border-top: 1px solid rgba(0, 0, 0, 0.05); 389 } 390 391 .chunk-details.expanded { 392 display: block; 393 } 394 395 /* Chunk Detail Row */ 396 .chunk-detail-row { 397 display: flex; 398 align-items: center; 399 padding: 6px 8px; 400 margin: 4px 0; 401 border-radius: 6px; 402 font-size: 12px; 403 gap: 10px; 404 } 405 406 .chunk-detail-row.chunk-used { 407 background: rgba(16, 185, 129, 0.1); 408 } 409 410 .chunk-detail-row.chunk-not-used { 411 background: rgba(100, 116, 139, 0.08); 412 } 413 414 .chunk-detail-icon { 415 font-size: 12px; 416 width: 16px; 417 text-align: center; 418 } 419 420 .chunk-used .chunk-detail-icon { 421 color: #10b981; 422 } 423 424 .chunk-not-used .chunk-detail-icon { 425 color: #94a3b8; 426 } 427 428 .chunk-detail-num { 429 font-weight: 600; 430 color: #334155; 431 flex: 1; 432 } 433 434 .chunk-detail-score { 435 font-weight: 600; 436 color: #64748b; 437 } 438 331 439 .similarity-score { 332 440 font-size: 16px; -
mxchat-basic/trunk/includes/class-mxchat-admin.php
r3428255 r3430202 109 109 'post_type_visibility_list' => array(), // Array of post type slugs 110 110 'contextual_awareness_toggle' => 'off', 111 'show_frontend_debugger' => 'on', 111 112 'close_button_color' => esc_html__('#fff', 'mxchat'), 112 113 'chatbot_bg_color' => esc_html__('#fff', 'mxchat'), … … 1817 1818 ] 1818 1819 ); 1819 $message_content = nl2br($message_content);1820 // Note: Don't use nl2br() here - format_transcript_message() handles paragraph formatting 1820 1821 1821 1822 // Check if this bot message has RAG context data … … 2115 2116 } 2116 2117 2117 // Retrieve total number of prompts, considering filters 2118 // UPDATED 2.6.3: Count unique entries (by source_url) instead of individual rows 2119 // This ensures pagination shows X entries per page, not X chunks 2120 // Entries with empty source_url are counted individually 2118 2121 if (!empty($where_values)) { 2119 $count_query = $wpdb->prepare("SELECT COUNT(*) FROM {$table_name} {$sql_where}", $where_values); 2122 // Count unique source_urls + count of rows with empty source_url 2123 $count_query = $wpdb->prepare( 2124 "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$sql_where} AND source_url != '') + 2125 (SELECT COUNT(*) FROM {$table_name} {$sql_where} AND (source_url = '' OR source_url IS NULL))", 2126 array_merge($where_values, $where_values) 2127 ); 2120 2128 } else { 2121 $count_query = "SELECT COUNT(*) FROM {$table_name} {$sql_where}"; 2129 $count_query = "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '') + 2130 (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL)"; 2122 2131 } 2123 2132 $total_records = $wpdb->get_var($count_query); 2124 2133 $total_pages = ceil($total_records / $per_page); 2125 2134 2126 // Retrieve prompts from the database 2127 $limit_offset_values = array_merge($where_values, array($per_page, $offset)); 2128 $prompts_query = $wpdb->prepare( 2129 "SELECT * FROM {$table_name} {$sql_where} ORDER BY timestamp DESC LIMIT %d OFFSET %d", 2130 ...$limit_offset_values 2131 ); 2132 2133 $prompts = $wpdb->get_results($prompts_query); 2135 // UPDATED 2.6.3: Get unique source_urls for pagination, then fetch all their rows 2136 // Step 1: Get the source_urls for this page (distinct URLs ordered by latest timestamp) 2137 if (!empty($where_values)) { 2138 $urls_query = $wpdb->prepare( 2139 "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name} {$sql_where} 2140 GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d", 2141 array_merge($where_values, array($per_page, $offset)) 2142 ); 2143 } else { 2144 $urls_query = $wpdb->prepare( 2145 "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name} 2146 GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d", 2147 $per_page, $offset 2148 ); 2149 } 2150 $page_urls = $wpdb->get_results($urls_query); 2151 2152 // Step 2: Fetch all rows for these source_urls 2153 $prompts = array(); 2154 if (!empty($page_urls)) { 2155 $url_list = array(); 2156 $has_empty_url = false; 2157 foreach ($page_urls as $url_row) { 2158 if (empty($url_row->source_url)) { 2159 $has_empty_url = true; 2160 } else { 2161 $url_list[] = $url_row->source_url; 2162 } 2163 } 2164 2165 // Build query to fetch all rows for these URLs 2166 $url_conditions = array(); 2167 $url_values = array(); 2168 2169 if (!empty($url_list)) { 2170 $placeholders = implode(',', array_fill(0, count($url_list), '%s')); 2171 $url_conditions[] = "source_url IN ($placeholders)"; 2172 $url_values = array_merge($url_values, $url_list); 2173 } 2174 2175 if ($has_empty_url) { 2176 $url_conditions[] = "(source_url = '' OR source_url IS NULL)"; 2177 } 2178 2179 if (!empty($url_conditions)) { 2180 $url_where = "WHERE (" . implode(" OR ", $url_conditions) . ")"; 2181 2182 // Add original filters back 2183 if (!empty($where_clauses)) { 2184 $url_where .= " AND " . implode(" AND ", $where_clauses); 2185 $url_values = array_merge($url_values, $where_values); 2186 } 2187 2188 if (!empty($url_values)) { 2189 $prompts_query = $wpdb->prepare( 2190 "SELECT * FROM {$table_name} {$url_where} ORDER BY source_url, timestamp DESC", 2191 $url_values 2192 ); 2193 } else { 2194 $prompts_query = "SELECT * FROM {$table_name} {$url_where} ORDER BY source_url, timestamp DESC"; 2195 } 2196 2197 $prompts = $wpdb->get_results($prompts_query); 2198 } 2199 } 2134 2200 } 2135 2201 … … 2692 2758 2693 2759 <!-- Table --> 2760 <?php 2761 // Group prompts by source_url for chunked content display 2762 $grouped_prompts = array(); 2763 $ungrouped_prompts = array(); 2764 2765 if ($prompts) { 2766 foreach ($prompts as $prompt) { 2767 $source_url = $prompt->source_url ?? ''; 2768 2769 // Check if chunk metadata exists on the record itself (Pinecone) 2770 if (isset($prompt->chunk_index) && $prompt->chunk_index !== null) { 2771 // Pinecone: metadata is already on the record object 2772 $prompt->chunk_metadata = array( 2773 'chunk_index' => intval($prompt->chunk_index), 2774 'total_chunks' => isset($prompt->total_chunks) ? intval($prompt->total_chunks) : null, 2775 'is_chunked' => isset($prompt->is_chunked) ? (bool) $prompt->is_chunked : true 2776 ); 2777 $prompt->display_content = $prompt->article_content; // Pinecone content is already clean 2778 } else { 2779 // WordPress: Parse chunk metadata from JSON prefix in article_content 2780 $chunk_meta = MxChat_Chunker::parse_stored_chunk($prompt->article_content); 2781 $prompt->chunk_metadata = $chunk_meta['metadata']; 2782 $prompt->display_content = $chunk_meta['text']; // Store clean content without JSON prefix 2783 } 2784 2785 if (!empty($source_url)) { 2786 if (!isset($grouped_prompts[$source_url])) { 2787 $grouped_prompts[$source_url] = array(); 2788 } 2789 $grouped_prompts[$source_url][] = $prompt; 2790 } else { 2791 $ungrouped_prompts[] = $prompt; 2792 } 2793 } 2794 2795 // Sort each group by chunk_index from metadata 2796 foreach ($grouped_prompts as $source_url => &$group) { 2797 usort($group, function($a, $b) { 2798 $index_a = isset($a->chunk_metadata['chunk_index']) ? intval($a->chunk_metadata['chunk_index']) : 0; 2799 $index_b = isset($b->chunk_metadata['chunk_index']) ? intval($b->chunk_metadata['chunk_index']) : 0; 2800 return $index_a - $index_b; 2801 }); 2802 } 2803 unset($group); // Break reference 2804 } 2805 $display_index = 0; 2806 ?> 2694 2807 <div class="mxchat-table-wrapper"> 2695 2808 <table class="mxchat-records-table"> … … 2707 2820 <tbody> 2708 2821 <?php if ($prompts) : ?> 2709 <?php foreach ($prompts as $index => $prompt) : ?> 2822 <?php 2823 // Display grouped prompts (content with same source_url) 2824 foreach ($grouped_prompts as $source_url => $group) : 2825 $chunk_count = count($group); 2826 $first_prompt = $group[0]; 2827 $display_index++; 2828 2829 if ($chunk_count > 1) : 2830 // Multiple chunks - show grouped row with expand button 2831 $group_id = 'group-' . md5($source_url); 2832 ?> 2833 <tr id="prompt-<?php echo esc_attr($first_prompt->id); ?>" 2834 class="mxchat-chunk-group-header" 2835 data-source="<?php echo esc_attr($data_source); ?>" 2836 data-group-id="<?php echo esc_attr($group_id); ?>" 2837 <?php if ($data_source === 'pinecone') : ?> 2838 style="background: rgba(33, 150, 243, 0.02);" 2839 <?php endif; ?>> 2840 <td> 2841 <?php if ($data_source === 'pinecone') : ?> 2842 <?php echo esc_html($display_index + (($current_page - 1) * $per_page)); ?> 2843 <?php else : ?> 2844 <?php echo esc_html($first_prompt->id); ?> 2845 <?php endif; ?> 2846 </td> 2847 <td class="mxchat-content-cell"> 2848 <div class="mxchat-chunk-group-info"> 2849 <button type="button" class="mxchat-chunk-toggle" data-group-id="<?php echo esc_attr($group_id); ?>"> 2850 <span class="dashicons dashicons-arrow-right-alt2"></span> 2851 </button> 2852 <span class="mxchat-chunk-badge"><?php echo esc_html($chunk_count); ?> <?php esc_html_e('chunks', 'mxchat'); ?></span> 2853 <span class="mxchat-chunk-preview"> 2854 <?php 2855 // Use display_content (without JSON prefix) if available 2856 $parent_content = isset($first_prompt->display_content) ? $first_prompt->display_content : $first_prompt->article_content; 2857 $content_preview = mb_substr($parent_content, 0, 100); 2858 echo esc_html($content_preview . '...'); 2859 ?> 2860 </span> 2861 </div> 2862 </td> 2863 <td class="mxchat-url-cell"> 2864 <div class="url-view"> 2865 <a href="<?php echo esc_url($source_url); ?>" target="_blank"> 2866 <span class="dashicons dashicons-external"></span> 2867 <?php esc_html_e('View Source', 'mxchat'); ?> 2868 </a> 2869 </div> 2870 </td> 2871 <?php if ($data_source === 'pinecone') : ?> 2872 <td class="mxchat-vector-id-cell"> 2873 <code style="background: #f5f5f5; padding: 2px 6px; border-radius: 3px; font-size: 11px;"> 2874 <?php echo esc_html(substr($first_prompt->id, 0, 8) . '..._chunk_*'); ?> 2875 </code> 2876 </td> 2877 <?php endif; ?> 2878 <td class="mxchat-actions-cell"> 2879 <div class="mxchat-role-restriction-container" style="margin-bottom: 8px;"> 2880 <select class="mxchat-role-select mxchat-group-role-select" 2881 data-group-url="<?php echo esc_attr($source_url); ?>" 2882 data-data-source="<?php echo esc_attr($data_source); ?>" 2883 data-nonce="<?php echo wp_create_nonce('mxchat_update_role_nonce'); ?>"> 2884 <?php 2885 $current_role = $first_prompt->role_restriction ?? 'public'; 2886 $role_options = $knowledge_manager->mxchat_get_role_options(); 2887 foreach ($role_options as $role_key => $role_label) : ?> 2888 <option value="<?php echo esc_attr($role_key); ?>" 2889 <?php selected($current_role, $role_key); ?>> 2890 <?php echo esc_html($role_label); ?> 2891 </option> 2892 <?php endforeach; ?> 2893 </select> 2894 </div> 2895 <div class="mxchat-delete-container"> 2896 <button type="button" 2897 class="mxchat-button-icon delete-button-group" 2898 data-source-url="<?php echo esc_attr($source_url); ?>" 2899 data-chunk-count="<?php echo esc_attr($chunk_count); ?>" 2900 data-data-source="<?php echo esc_attr($data_source); ?>" 2901 data-bot-id="<?php echo esc_attr($current_bot_id); ?>" 2902 data-nonce="<?php echo wp_create_nonce('mxchat_delete_chunks_nonce'); ?>" 2903 title="<?php esc_attr_e('Delete all chunks', 'mxchat'); ?>"> 2904 <span class="dashicons dashicons-trash"></span> 2905 </button> 2906 </div> 2907 </td> 2908 </tr> 2909 <?php 2910 // Render hidden chunk rows 2911 foreach ($group as $prompt) : 2912 // Get chunk index from metadata (0-based), default to array position 2913 $meta_chunk_index = isset($prompt->chunk_metadata['chunk_index']) ? intval($prompt->chunk_metadata['chunk_index']) : 0; 2914 $meta_total_chunks = isset($prompt->chunk_metadata['total_chunks']) ? intval($prompt->chunk_metadata['total_chunks']) : $chunk_count; 2915 ?> 2916 <tr id="prompt-<?php echo esc_attr($prompt->id); ?>" 2917 class="mxchat-chunk-row <?php echo esc_attr($group_id); ?>" 2918 data-source="<?php echo esc_attr($data_source); ?>" 2919 style="display: none; background: #f8f9fa;"> 2920 <td style="padding-left: 30px;"> 2921 <!-- Hidden ID column --> 2922 </td> 2923 <td class="mxchat-content-cell"> 2924 <div class="mxchat-accordion-wrapper"> 2925 <div class="mxchat-content-preview"> 2926 <span class="mxchat-chunk-indicator" style="margin-right: 10px;"> 2927 <?php printf(esc_html__('Chunk %d of %d', 'mxchat'), $meta_chunk_index + 1, $meta_total_chunks); ?> 2928 </span> 2929 <?php 2930 // Use display_content (without JSON prefix) if available 2931 $content = isset($prompt->display_content) ? $prompt->display_content : $prompt->article_content; 2932 $preview_length = 150; 2933 $content_preview = mb_strlen($content) > $preview_length 2934 ? mb_substr($content, 0, $preview_length) . '...' 2935 : $content; 2936 ?> 2937 <span class="preview-text"><?php echo esc_html($content_preview); ?></span> 2938 <?php if (mb_strlen($content) > $preview_length) : ?> 2939 <button class="mxchat-expand-toggle" type="button"> 2940 <span class="dashicons dashicons-arrow-down-alt2"></span> 2941 </button> 2942 <?php endif; ?> 2943 </div> 2944 <div class="mxchat-content-full" style="display: none;"> 2945 <div class="content-view"> 2946 <?php 2947 if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) { 2948 echo '<div dir="rtl" lang="he" class="rtl-content">'; 2949 echo wp_kses_post(wpautop($content)); 2950 echo '</div>'; 2951 } else { 2952 echo wp_kses_post(wpautop($content)); 2953 } 2954 ?> 2955 </div> 2956 </div> 2957 </div> 2958 </td> 2959 <td class="mxchat-url-cell"> 2960 <span class="mxchat-chunk-label"><?php esc_html_e('Same as parent', 'mxchat'); ?></span> 2961 </td> 2962 <?php if ($data_source === 'pinecone') : ?> 2963 <td class="mxchat-vector-id-cell"> 2964 <code style="background: #f5f5f5; padding: 2px 6px; border-radius: 3px; font-size: 11px;"> 2965 <?php echo esc_html(substr($prompt->id, 0, 12) . '...'); ?> 2966 </code> 2967 </td> 2968 <?php endif; ?> 2969 <td class="mxchat-actions-cell"> 2970 <span class="mxchat-chunk-label"><?php esc_html_e('Managed by group', 'mxchat'); ?></span> 2971 </td> 2972 </tr> 2973 <?php endforeach; ?> 2974 <?php else : 2975 // Single entry - display normally 2976 $prompt = $first_prompt; 2977 $index = $display_index - 1; 2978 ?> 2710 2979 <tr id="prompt-<?php echo esc_attr($prompt->id); ?>" 2711 2980 data-source="<?php echo esc_attr($data_source); ?>" … … 2724 2993 <div class="mxchat-content-preview"> 2725 2994 <?php 2726 $content = $prompt->article_content; 2995 // Use display_content (without JSON prefix) if available 2996 $content = isset($prompt->display_content) ? $prompt->display_content : $prompt->article_content; 2727 2997 $preview_length = 150; 2728 2998 $content_preview = mb_strlen($content) > $preview_length … … 2753 3023 <?php if ($data_source === 'wordpress') : ?> 2754 3024 <textarea class="content-edit" style="display:none;" 2755 <?php if (preg_match('/[\x{0590}-\x{05FF}]/u', $ prompt->article_content)) echo 'dir="rtl" lang="he"'; ?>>2756 <?php echo htmlspecialchars($ prompt->article_content, ENT_QUOTES, 'UTF-8'); ?>3025 <?php if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) echo 'dir="rtl" lang="he"'; ?>> 3026 <?php echo htmlspecialchars($content, ENT_QUOTES, 'UTF-8'); ?> 2757 3027 </textarea> 2758 3028 <?php endif; ?> … … 2864 3134 </td> 2865 3135 3136 </tr> 3137 <?php endif; ?> 3138 <?php endforeach; ?> 3139 3140 <?php 3141 // Display ungrouped prompts (content without source_url) 3142 foreach ($ungrouped_prompts as $prompt) : 3143 $display_index++; 3144 $index = $display_index - 1; 3145 ?> 3146 <tr id="prompt-<?php echo esc_attr($prompt->id); ?>" 3147 data-source="<?php echo esc_attr($data_source); ?>" 3148 <?php if ($data_source === 'pinecone') : ?> 3149 style="background: rgba(33, 150, 243, 0.02);" 3150 <?php endif; ?>> 3151 <td> 3152 <?php if ($data_source === 'pinecone') : ?> 3153 <?php echo esc_html($index + 1 + (($current_page - 1) * $per_page)); ?> 3154 <?php else : ?> 3155 <?php echo esc_html($prompt->id); ?> 3156 <?php endif; ?> 3157 </td> 3158 <td class="mxchat-content-cell"> 3159 <div class="mxchat-accordion-wrapper"> 3160 <div class="mxchat-content-preview"> 3161 <?php 3162 // Use display_content (without JSON prefix) if available 3163 $content = isset($prompt->display_content) ? $prompt->display_content : $prompt->article_content; 3164 $preview_length = 150; 3165 $content_preview = mb_strlen($content) > $preview_length 3166 ? mb_substr($content, 0, $preview_length) . '...' 3167 : $content; 3168 ?> 3169 <span class="preview-text"><?php echo esc_html($content_preview); ?></span> 3170 <?php if (mb_strlen($content) > $preview_length) : ?> 3171 <button class="mxchat-expand-toggle" type="button"> 3172 <span class="dashicons dashicons-arrow-down-alt2"></span> 3173 </button> 3174 <?php endif; ?> 3175 </div> 3176 <div class="mxchat-content-full" style="display: none;"> 3177 <div class="content-view"> 3178 <?php echo wp_kses_post(wpautop($content)); ?> 3179 </div> 3180 </div> 3181 </div> 3182 </td> 3183 <td class="mxchat-url-cell"> 3184 <span class="mxchat-na"><?php esc_html_e('N/A', 'mxchat'); ?></span> 3185 </td> 3186 <?php if ($data_source === 'pinecone') : ?> 3187 <td class="mxchat-vector-id-cell"> 3188 <code style="background: #f5f5f5; padding: 2px 6px; border-radius: 3px; font-size: 11px;"> 3189 <?php echo esc_html(substr($prompt->id, 0, 12) . '...'); ?> 3190 </code> 3191 </td> 3192 <?php endif; ?> 3193 <td class="mxchat-actions-cell"> 3194 <div class="mxchat-delete-container"> 3195 <?php if ($data_source === 'pinecone') : ?> 3196 <button type="button" 3197 class="mxchat-button-icon delete-button-ajax" 3198 data-vector-id="<?php echo esc_attr($prompt->id); ?>" 3199 data-bot-id="<?php echo esc_attr($current_bot_id); ?>" 3200 data-nonce="<?php echo wp_create_nonce('mxchat_delete_pinecone_prompt_nonce'); ?>" 3201 title="<?php esc_attr_e('Delete entry', 'mxchat'); ?>"> 3202 <span class="dashicons dashicons-trash"></span> 3203 </button> 3204 <?php else : ?> 3205 <a href="<?php echo esc_url(admin_url( 3206 'admin-post.php?action=mxchat_delete_prompt&id=' . esc_attr($prompt->id) 3207 . '&_wpnonce=' . wp_create_nonce('mxchat_delete_prompt_nonce') 3208 )); ?>" 3209 class="mxchat-button-icon delete-button" 3210 onclick="return confirm('<?php esc_attr_e('Are you sure you want to delete this entry?', 'mxchat'); ?>');"> 3211 <span class="dashicons dashicons-trash"></span> 3212 </a> 3213 <?php endif; ?> 3214 </div> 3215 </td> 2866 3216 </tr> 2867 3217 <?php endforeach; ?> … … 2984 3334 </div> 2985 3335 </div> 2986 3336 3337 <!-- Content Chunking Settings Card --> 3338 <div class="mxchat-card" style="margin-top: 20px;"> 3339 <div class="mxchat-settings-section"> 3340 <h3><?php esc_html_e('Content Chunking Settings', 'mxchat'); ?></h3> 3341 <p class="mxchat-description"> 3342 <?php esc_html_e('Chunking splits large content into smaller segments for more accurate semantic search. This improves retrieval quality for long documents. All chunks for a URL are reassembled before sending to the AI. Only applies to new submissions - entries added before v2.6.3 must be resubmitted to enable chunking.', 'mxchat'); ?> 3343 </p> 3344 3345 <?php 3346 // Get current chunking settings 3347 $chunking_settings = MxChat_Chunker::get_settings(); 3348 $chunking_enabled = $chunking_settings['chunking_enabled']; 3349 $chunk_size = $chunking_settings['chunk_size']; 3350 ?> 3351 3352 <div class="mxchat-chunking-settings mxchat-autosave-section"> 3353 <!-- Enable Chunking Toggle --> 3354 <div class="mxchat-toggle-container" style="margin-bottom: 20px;"> 3355 <label class="mxchat-toggle-switch"> 3356 <input type="checkbox" 3357 name="mxchat_chunking_enabled" 3358 id="mxchat_chunking_enabled" 3359 class="mxchat-autosave-field" 3360 value="1" 3361 data-option-name="mxchat_chunking_enabled" 3362 data-nonce="<?php echo wp_create_nonce('mxchat_prompts_setting_nonce'); ?>" 3363 <?php checked($chunking_enabled, true); ?>> 3364 <span class="mxchat-toggle-slider"></span> 3365 </label> 3366 <span class="mxchat-toggle-label"> 3367 <?php esc_html_e('Enable Content Chunking', 'mxchat'); ?> 3368 </span> 3369 </div> 3370 3371 <!-- Chunk Size Setting --> 3372 <div class="mxchat-form-group" style="margin-bottom: 20px;"> 3373 <label for="mxchat_chunk_size"> 3374 <?php esc_html_e('Chunk Size (characters)', 'mxchat'); ?> 3375 </label> 3376 <input type="number" 3377 name="mxchat_chunk_size" 3378 id="mxchat_chunk_size" 3379 class="mxchat-autosave-field regular-text" 3380 value="<?php echo esc_attr($chunk_size); ?>" 3381 min="1000" 3382 max="10000" 3383 step="500" 3384 data-option-name="mxchat_chunk_size" 3385 data-nonce="<?php echo wp_create_nonce('mxchat_prompts_setting_nonce'); ?>" 3386 style="width: 150px;"> 3387 <p class="description"> 3388 <?php esc_html_e('Recommended: 4000 characters (~1000 tokens). Range: 1000-10000. Larger chunks preserve more context but are less precise for retrieval.', 'mxchat'); ?> 3389 </p> 3390 </div> 3391 </div> 3392 </div> 3393 </div> 3394 2987 3395 <!-- Role-Based Content Restrictions Card --> 2988 3396 <div class="mxchat-card" style="margin-top: 20px;"> … … 3458 3866 } 3459 3867 3868 // Normalize line endings and clean up excessive whitespace 3869 $text = str_replace("\r\n", "\n", $text); 3870 $text = str_replace("\r", "\n", $text); 3871 3872 // Clean up existing <br> tags that may have been saved (legacy data) 3873 // Convert <br>, <br/>, <br /> back to newlines for consistent processing 3874 $text = preg_replace('/<br\s*\/?>\s*/i', "\n", $text); 3875 3876 // Collapse 3+ consecutive newlines to just 2 (paragraph break) 3877 $text = preg_replace('/\n{3,}/', "\n\n", $text); 3878 3460 3879 // Process markdown headers (# Header) 3461 3880 $text = preg_replace_callback('/^(#{1,6})\s+(.+)$/m', function($matches) { 3462 3881 $level = strlen($matches[1]); 3463 $content = esc_html( $matches[2]);3882 $content = esc_html(trim($matches[2])); 3464 3883 return "<h{$level}>{$content}</h{$level}>"; 3465 3884 }, $text); … … 3515 3934 }, $text); 3516 3935 3517 // Convert newlines to <br> tags (but preserve existing block elements) 3518 $text = preg_replace('/\n/', '<br>', $text); 3936 // Convert paragraphs: split by double newlines, wrap in <p> tags 3937 // This creates proper paragraph structure instead of excessive <br> tags 3938 $paragraphs = preg_split('/\n\n+/', $text); 3939 3940 // Filter out empty paragraphs but preserve content like "0" 3941 $paragraphs = array_values(array_filter(array_map('trim', $paragraphs), function($p) { 3942 return $p !== ''; 3943 })); 3944 3945 if (empty($paragraphs)) { 3946 // No content after filtering 3947 return ''; 3948 } elseif (count($paragraphs) > 1) { 3949 // Multiple paragraphs - wrap each in <p> tags, convert single newlines to <br> 3950 $formatted_paragraphs = array_map(function($p) { 3951 return nl2br($p); 3952 }, $paragraphs); 3953 $text = '<p>' . implode('</p><p>', $formatted_paragraphs) . '</p>'; 3954 } else { 3955 // Single paragraph - just convert newlines to <br> 3956 $text = nl2br($paragraphs[0]); 3957 } 3519 3958 3520 3959 return $text; … … 5021 5460 ); 5022 5461 5462 // Frontend Debugger Toggle 5463 add_settings_field( 5464 'show_frontend_debugger', 5465 esc_html__('Frontend Debugger', 'mxchat'), 5466 array($this, 'mxchat_frontend_debugger_callback'), 5467 'mxchat-chatbot', 5468 'mxchat_chatbot_section' 5469 ); 5470 5023 5471 // Similarity Threshold Slider 5024 5472 add_settings_field( … … 6973 7421 } 6974 7422 7423 public function mxchat_frontend_debugger_callback() { 7424 $show_debugger = isset($this->options['show_frontend_debugger']) ? $this->options['show_frontend_debugger'] : 'on'; 7425 $checked = ($show_debugger === 'on') ? 'checked' : ''; 7426 echo '<label class="toggle-switch">'; 7427 echo sprintf( 7428 '<input type="checkbox" id="show_frontend_debugger" name="show_frontend_debugger" value="on" %s />', 7429 esc_attr($checked) 7430 ); 7431 echo '<span class="slider"></span>'; 7432 echo '</label>'; 7433 echo '<p class="description">' . 7434 esc_html__('Show the debug panel on the frontend for administrators. Displays similarity scores, document matches, and query analysis in real-time.', 'mxchat') . 7435 '</p>'; 7436 } 7437 6975 7438 public function mxchat_contextual_awareness_callback() { 6976 7439 // Get value from options array, default to 'off' … … 7586 8049 wp_localize_script('mxchat-test-streaming-js', 'mxchatTestStreamingAjax', [ 7587 8050 'ajax_url' => admin_url('admin-ajax.php'), 7588 'nonce' => wp_create_nonce('mxchat_test_streaming_nonce') 8051 'nonce' => wp_create_nonce('mxchat_test_streaming_nonce'), 8052 'settings_nonce' => wp_create_nonce('mxchat_save_setting_nonce') 7589 8053 ]); 7590 8054 break; … … 7796 8260 if (isset($input['contextual_awareness_toggle'])) { 7797 8261 $new_input['contextual_awareness_toggle'] = $input['contextual_awareness_toggle'] === 'on' ? 'on' : 'off'; 8262 } 8263 8264 if (isset($input['show_frontend_debugger'])) { 8265 $new_input['show_frontend_debugger'] = $input['show_frontend_debugger'] === 'on' ? 'on' : 'off'; 7798 8266 } 7799 8267 -
mxchat-basic/trunk/includes/class-mxchat-integrator.php
r3418641 r3430202 14 14 private $current_valid_urls = []; 15 15 private $is_streaming = false; // ADDED: Track if current request is streaming 16 private $streaming_headers_sent = false; // Track if streaming headers have been sent 17 18 /** 19 * Setup streaming headers - call this right before actually streaming 20 * This delays header setup to allow actions/forms to return JSON responses 21 */ 22 private function setup_streaming_headers() { 23 if ($this->streaming_headers_sent || headers_sent()) { 24 return false; 25 } 26 27 // Disable output buffering 28 while (ob_get_level()) { 29 ob_end_flush(); 30 } 31 32 // Set headers for SSE 33 header('Content-Type: text/event-stream'); 34 header('Cache-Control: no-cache'); 35 header('Connection: keep-alive'); 36 header('X-Accel-Buffering: no'); 37 38 ob_implicit_flush(true); 39 flush(); 40 41 $this->streaming_headers_sent = true; 42 return true; 43 } 16 44 17 45 /** … … 970 998 971 999 // Check if this is a streaming request 1000 // Allow force_streaming_test parameter to bypass the setting check (for admin compatibility testing) 1001 $force_streaming_test = isset($_POST['force_streaming_test']) && $_POST['force_streaming_test'] === '1' && current_user_can('administrator'); 972 1002 $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat' && 973 isset($current_options['enable_streaming_toggle']) && $current_options['enable_streaming_toggle'] === 'on';1003 ($force_streaming_test || (isset($current_options['enable_streaming_toggle']) && $current_options['enable_streaming_toggle'] === 'on')); 974 1004 975 1005 // ADDED: Store streaming state in class property for use in private methods 976 1006 $this->is_streaming = $is_streaming; 977 1007 978 // Set streaming headers if needed 979 if ($is_streaming) { 980 // Disable output buffering 981 while (ob_get_level()) { 982 ob_end_flush(); // Changed from ob_end_clean() 983 } 984 985 // Set headers for SSE 986 header('Content-Type: text/event-stream'); 987 header('Cache-Control: no-cache'); 988 header('Connection: keep-alive'); 989 header('X-Accel-Buffering: no'); 990 991 // Add these new lines: 992 ob_implicit_flush(true); 993 flush(); 994 } 1008 // NOTE: Streaming headers are now set later via setup_streaming_headers() 1009 // This allows actions/forms to return JSON responses without header conflicts 995 1010 996 1011 // Check if MX Chat Moderation is active … … 1486 1501 $response_data['testing_data'] = $testing_data; 1487 1502 } 1488 1489 // Clear streaming headers if they were set 1490 if ($is_streaming) { 1491 header_remove('Content-Type'); 1492 header_remove('Cache-Control'); 1493 header_remove('Connection'); 1494 header_remove('X-Accel-Buffering'); 1495 header('Content-Type: application/json'); 1496 } 1497 1503 1498 1504 wp_send_json($response_data); 1499 1505 wp_die(); 1500 1506 } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) { 1501 1507 // Intent returned true and set fallbackResponse 1502 1508 1503 1509 // SAVE TO TRANSCRIPT FIRST 1504 1510 if (!empty($this->fallbackResponse['text'])) { … … 1508 1514 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']); 1509 1515 } 1510 1516 1511 1517 $response_data = [ 1512 1518 'text' => $this->fallbackResponse['text'] ?? '', … … 1514 1520 'session_id' => $session_id 1515 1521 ]; 1516 1522 1517 1523 if (isset($this->fallbackResponse['chat_mode'])) { 1518 1524 $response_data['chat_mode'] = $this->fallbackResponse['chat_mode']; 1519 1525 } 1520 1526 1521 1527 if ($testing_data !== null) { 1522 1528 $response_data['testing_data'] = $testing_data; 1523 1529 } 1524 1525 // Clear streaming headers if they were set 1526 if ($is_streaming) { 1527 header_remove('Content-Type'); 1528 header_remove('Cache-Control'); 1529 header_remove('Connection'); 1530 header_remove('X-Accel-Buffering'); 1531 header('Content-Type: application/json'); 1532 } 1533 1530 1534 1531 wp_send_json($response_data); 1535 1532 wp_die(); … … 1801 1798 1802 1799 // Step 6: Return the response 1800 // DEBUG: Check if newlines exist in the response 1801 error_log("=== MXCHAT NON-STREAMING RESPONSE DEBUG ==="); 1802 error_log("Response has newlines: " . (strpos($response, "\n") !== false ? 'YES' : 'NO')); 1803 error_log("Response first 500 chars: " . substr($response, 0, 500)); 1804 1803 1805 $response_data = [ 1804 1806 'text' => $response, … … 4097 4099 // Calculate similarities and build results array 4098 4100 $all_similarities = []; 4099 $ relevant_results = [];4100 4101 $url_groups = array(); // NEW: Group by source_url for chunk reassembly 4102 4101 4103 foreach ($embeddings as $embedding) { 4102 4104 $database_embedding = $embedding->embedding_vector 4103 4105 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false]) 4104 4106 : null; 4105 4107 4106 4108 if (is_array($database_embedding) && is_array($user_embedding)) { 4107 4109 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding); 4108 4110 4109 4111 // Check role access 4110 4112 $role_restriction = $embedding->role_restriction ?? 'public'; 4111 4113 $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction); 4112 4114 4113 4115 // Store ALL similarities for testing (top 10) 4114 4116 $source_display = ''; 4115 if (!empty($embedding->source_url) && $embedding->source_url !== '#') { 4116 $source_display = $embedding->source_url; 4117 $source_url = $embedding->source_url ?? ''; 4118 if (!empty($source_url) && $source_url !== '#') { 4119 $source_display = $source_url; 4117 4120 } else { 4118 4121 $content_preview = strip_tags($embedding->article_content ?? ''); … … 4120 4123 $source_display = substr(trim($content_preview), 0, 50) . '...'; 4121 4124 } 4122 4125 4126 // Parse chunk metadata for display 4127 $article_content_for_parse = $embedding->article_content ?? ''; 4128 $parsed_for_display = MxChat_Chunker::parse_stored_chunk($article_content_for_parse); 4129 $is_chunk = $parsed_for_display['is_chunked']; 4130 $chunk_meta = $parsed_for_display['metadata']; 4131 4123 4132 $all_similarities[] = [ 4124 4133 'document_id' => $embedding->id, … … 4127 4136 'above_threshold' => $similarity >= $similarity_threshold, 4128 4137 'source_display' => $source_display, 4129 'content_preview' => substr(strip_tags($ embedding->article_content?? ''), 0, 100) . '...',4130 'used_for_context' => false, // Initialize as false, we'll update this later4138 'content_preview' => substr(strip_tags($parsed_for_display['text'] ?? ''), 0, 100) . '...', 4139 'used_for_context' => false, 4131 4140 'role_restriction' => $role_restriction, 4132 4141 'has_access' => $has_access, 4133 'filtered_out' => !$has_access 4142 'filtered_out' => !$has_access, 4143 'is_chunk' => $is_chunk, 4144 'chunk_index' => $is_chunk ? ($chunk_meta['chunk_index'] ?? 0) : null, 4145 'total_chunks' => $is_chunk ? ($chunk_meta['total_chunks'] ?? 1) : null 4134 4146 ]; 4135 4136 // Only consider results above threshold AND with access for actual content retrieval 4137 if ($similarity >= $similarity_threshold && $has_access) { 4138 $relevant_results[] = [ 4139 'id' => $embedding->id, 4140 'similarity' => $similarity 4141 ]; 4142 } 4143 } 4144 4147 4148 // Only consider results above threshold AND with access for content retrieval 4149 if ($similarity >= $similarity_threshold && $has_access && !empty($source_url)) { 4150 // Parse chunk metadata if present 4151 $article_content = $embedding->article_content ?? ''; 4152 $parsed = MxChat_Chunker::parse_stored_chunk($article_content); 4153 $is_chunked = $parsed['is_chunked']; 4154 $chunk_index = $parsed['metadata']['chunk_index'] ?? 0; 4155 $text_content = $parsed['text']; 4156 4157 // Group by source URL 4158 if (!isset($url_groups[$source_url])) { 4159 $url_groups[$source_url] = array( 4160 'source_url' => $source_url, 4161 'best_score' => 0, 4162 'is_chunked' => $is_chunked, 4163 'chunks' => array(), 4164 'single_text' => '', 4165 'single_id' => null 4166 ); 4167 } 4168 4169 // Track best score for this URL 4170 if ($similarity > $url_groups[$source_url]['best_score']) { 4171 $url_groups[$source_url]['best_score'] = $similarity; 4172 } 4173 4174 // Store chunk info or single text 4175 if ($is_chunked) { 4176 $url_groups[$source_url]['is_chunked'] = true; 4177 $url_groups[$source_url]['chunks'][] = array( 4178 'id' => $embedding->id, 4179 'score' => $similarity, 4180 'chunk_index' => $chunk_index, 4181 'text' => $text_content 4182 ); 4183 } else { 4184 $url_groups[$source_url]['single_text'] = $text_content; 4185 $url_groups[$source_url]['single_id'] = $embedding->id; 4186 } 4187 } 4188 } 4189 4145 4190 unset($database_embedding); 4146 4191 } … … 4150 4195 return $b['similarity'] <=> $a['similarity']; 4151 4196 }); 4152 4153 // Sort relevant results by similarity(highest first)4154 u sort($relevant_results, function($a, $b) {4155 return $b[' similarity'] <=> $a['similarity'];4197 4198 // Sort URL groups by best score (highest first) 4199 uasort($url_groups, function($a, $b) { 4200 return $b['best_score'] <=> $a['best_score']; 4156 4201 }); 4157 4158 // Get top 5 results for actual content (standard approach)4159 $top_ results = array_slice($relevant_results, 0, 3);4160 4161 // NOW mark which documents are actuallyused for context4202 4203 // Take top 3 unique URLs 4204 $top_urls = array_slice($url_groups, 0, 3, true); 4205 4206 // Track which document IDs are used for context 4162 4207 $used_document_ids = []; 4163 foreach ($top_results as $result) { 4164 $used_document_ids[] = $result['id']; 4165 } 4166 4208 foreach ($top_urls as $group) { 4209 if ($group['is_chunked']) { 4210 foreach ($group['chunks'] as $chunk) { 4211 $used_document_ids[] = $chunk['id']; 4212 } 4213 } elseif ($group['single_id']) { 4214 $used_document_ids[] = $group['single_id']; 4215 } 4216 } 4217 4167 4218 // Update the all_similarities array to mark which were actually used 4168 4219 foreach ($all_similarities as &$similarity_item) { 4169 4220 $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids); 4170 4221 } 4171 4172 // Store top 10 for testing panel (now with correct used_for_context flags and role info)4222 4223 // Store top 10 for testing panel 4173 4224 $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10); 4174 4225 $this->last_similarity_analysis['total_checked'] = count($embeddings); 4175 4226 4176 4227 // Initialize final content 4177 4228 $content = ''; 4178 4179 // Track document IDs to avoid duplicates 4180 $added_document_ids = []; 4181 4182 // Fetch and format content for each selected result 4183 foreach ($top_results as $index => $result) { 4184 if (in_array($result['id'], $added_document_ids)) { 4185 continue; 4186 } 4187 4188 $chunk_content = $this->fetch_content_with_product_links($result['id']); 4189 $added_document_ids[] = $result['id']; 4190 4191 // NEW: Extract source_url from database for this result 4192 $source_url = $wpdb->get_var($wpdb->prepare( 4193 "SELECT source_url FROM {$system_prompt_table} WHERE id = %d", 4194 $result['id'] 4195 )); 4196 4197 // NEW: Add source_url to valid URLs list if it exists and is not empty/placeholder 4198 if (!empty($source_url) && $source_url !== '#') { 4199 $valid_urls[] = $source_url; 4200 } 4201 4202 // NEW: Extract any URLs from the article content itself 4203 preg_match_all( 4204 '#\bhttps?://[^\s<>"\']+#i', 4205 $chunk_content, 4206 $content_urls 4207 ); 4208 if (!empty($content_urls[0])) { 4209 $valid_urls = array_merge($valid_urls, $content_urls[0]); 4210 } 4211 4212 $content .= "## Reference " . ($index + 1) . " ##\n"; 4213 $content .= $chunk_content . "\n\n"; 4214 4215 // PDF surrounding pages logic (unchanged) 4216 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) { 4217 $surrounding_content = $wpdb->get_results($wpdb->prepare( 4218 "SELECT id, article_content, role_restriction FROM {$system_prompt_table} 4219 WHERE id IN ( 4220 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1), 4221 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1) 4222 )", 4223 $result['id'], 4224 $result['id'] 4225 )); 4226 4227 // Check role access for surrounding content too 4228 if (!empty($surrounding_content[0])) { 4229 $surrounding_role = $surrounding_content[0]->role_restriction ?? 'public'; 4230 if ($knowledge_manager->mxchat_user_has_content_access($surrounding_role)) { 4231 // NEW: Extract URLs from surrounding content too 4232 preg_match_all( 4233 '#\bhttps?://[^\s<>"\']+#i', 4234 $surrounding_content[0]->article_content, 4235 $surrounding_urls 4236 ); 4237 if (!empty($surrounding_urls[0])) { 4238 $valid_urls = array_merge($valid_urls, $surrounding_urls[0]); 4239 } 4240 4241 $content .= "## Related Content ##\n"; 4242 $content .= $surrounding_content[0]->article_content . "\n\n"; 4243 $added_document_ids[] = $surrounding_content[0]->id; 4229 $matches_used = 0; 4230 4231 // Build content from top URLs 4232 foreach ($top_urls as $source_url => $group) { 4233 $full_text = ''; 4234 4235 if ($group['is_chunked']) { 4236 // Fetch all chunks for this URL and reassemble 4237 $full_text = $this->reassemble_chunks_from_wordpress($source_url); 4238 4239 // If fetching all chunks fails, fall back to matched chunks 4240 if (empty($full_text)) { 4241 // Sort matched chunks by index and concatenate 4242 usort($group['chunks'], function($a, $b) { 4243 return $a['chunk_index'] <=> $b['chunk_index']; 4244 }); 4245 4246 $chunk_texts = array(); 4247 foreach ($group['chunks'] as $chunk) { 4248 $chunk_texts[] = $chunk['text']; 4244 4249 } 4245 } 4246 4247 if (!empty($surrounding_content[1])) { 4248 $surrounding_role = $surrounding_content[1]->role_restriction ?? 'public'; 4249 if ($knowledge_manager->mxchat_user_has_content_access($surrounding_role)) { 4250 // NEW: Extract URLs from surrounding content too 4251 preg_match_all( 4252 '#\bhttps?://[^\s<>"\']+#i', 4253 $surrounding_content[1]->article_content, 4254 $surrounding_urls 4255 ); 4256 if (!empty($surrounding_urls[0])) { 4257 $valid_urls = array_merge($valid_urls, $surrounding_urls[0]); 4258 } 4259 4260 $content .= "## Related Content ##\n"; 4261 $content .= $surrounding_content[1]->article_content . "\n\n"; 4262 $added_document_ids[] = $surrounding_content[1]->id; 4263 } 4264 } 4250 $full_text = implode("\n\n", $chunk_texts); 4251 } 4252 } else { 4253 $full_text = $group['single_text']; 4254 } 4255 4256 if (!empty($full_text)) { 4257 $content .= "## Reference " . ($matches_used + 1) . " ##\n"; 4258 $content .= $full_text . "\n\n"; 4259 4260 if (!empty($source_url) && $source_url !== '#') { 4261 $valid_urls[] = $source_url; 4262 $content .= "URL: " . $source_url . "\n\n"; 4263 } 4264 4265 // Extract any URLs from the text content itself 4266 preg_match_all( 4267 '#\bhttps?://[^\s<>"\']+#i', 4268 $full_text, 4269 $content_urls 4270 ); 4271 if (!empty($content_urls[0])) { 4272 $valid_urls = array_merge($valid_urls, $content_urls[0]); 4273 } 4274 4275 $matches_used++; 4265 4276 } 4266 4277 } … … 4268 4279 // NEW: Store unique valid URLs for validation 4269 4280 $this->current_valid_urls = array_unique($valid_urls); 4270 4281 4271 4282 // Add response guidelines 4272 if (empty($top_ results)) {4283 if (empty($top_urls)) { 4273 4284 $content = "No reference information was found for this query.\n\n"; 4274 4285 } else { … … 4286 4297 } 4287 4298 4299 /** 4300 * Fetch and reassemble all chunks for a URL from WordPress database 4301 * 4302 * @param string $source_url The source URL to fetch chunks for 4303 * @return string Reassembled content from all chunks 4304 */ 4305 private function reassemble_chunks_from_wordpress($source_url) { 4306 global $wpdb; 4307 $table = $wpdb->prefix . 'mxchat_system_prompt_content'; 4308 4309 // Fetch all rows with this source_url 4310 $rows = $wpdb->get_results($wpdb->prepare( 4311 "SELECT article_content FROM {$table} 4312 WHERE source_url = %s 4313 ORDER BY id ASC", 4314 $source_url 4315 )); 4316 4317 if (empty($rows)) { 4318 return ''; 4319 } 4320 4321 // Parse and sort chunks by index 4322 $chunks = array(); 4323 foreach ($rows as $row) { 4324 $parsed = MxChat_Chunker::parse_stored_chunk($row->article_content); 4325 4326 if ($parsed['is_chunked']) { 4327 $chunk_index = $parsed['metadata']['chunk_index'] ?? 0; 4328 $chunks[$chunk_index] = $parsed['text']; 4329 } else { 4330 // Non-chunked content - just return it 4331 $chunks[] = $parsed['text']; 4332 } 4333 } 4334 4335 // Sort by chunk index 4336 ksort($chunks); 4337 4338 // Reassemble content 4339 return implode("\n\n", $chunks); 4340 } 4341 4288 4342 private function find_relevant_content_pinecone($user_embedding, $bot_id = 'default', $bot_config = null) { 4289 4343 global $wpdb; … … 4348 4402 $request_body = array( 4349 4403 'vector' => $user_embedding, 4350 'topK' => 20, // Request more to get good testing data4404 'topK' => 50, // Increased for chunked content grouping - need more candidates to find top 3 unique URLs 4351 4405 'includeMetadata' => true, 4352 4406 'includeValues' => true … … 4433 4487 $matches_used = 0; 4434 4488 $matches_used_for_context = []; 4435 4436 // Process each match for actual content generation (lazy role checking) 4489 4490 // NEW CHUNKING LOGIC: Group results by source_url for chunk reassembly 4491 $url_groups = array(); 4492 4437 4493 foreach ($results['matches'] as $index => $match) { 4438 4494 // Skip if similarity is below threshold … … 4440 4496 continue; 4441 4497 } 4442 4443 // Limit to top 3 matches above threshold 4444 if ($matches_used >= 3) { 4445 break; 4446 } 4447 4448 if (!empty($match['metadata']['text'])) { 4449 // LAZY ROLE CHECK: Only check role for content we're actually considering 4450 $match_id = $match['id'] ?? ''; 4451 $role_restriction = $this->get_single_vector_role($match_id, $match['metadata']); 4452 $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction); 4453 4454 // Skip if user doesn't have access 4455 if (!$has_access) { 4456 continue; 4457 } 4458 4459 // User has access - add to content 4498 4499 $metadata = $match['metadata'] ?? array(); 4500 $source_url = $metadata['source_url'] ?? ''; 4501 4502 if (empty($source_url)) { 4503 continue; 4504 } 4505 4506 // LAZY ROLE CHECK: Only check role for content we're actually considering 4507 $match_id = $match['id'] ?? ''; 4508 $role_restriction = $this->get_single_vector_role($match_id, $metadata); 4509 $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction); 4510 4511 // Skip if user doesn't have access 4512 if (!$has_access) { 4513 continue; 4514 } 4515 4516 // Group by source URL 4517 if (!isset($url_groups[$source_url])) { 4518 $url_groups[$source_url] = array( 4519 'source_url' => $source_url, 4520 'best_score' => 0, 4521 'is_chunked' => isset($metadata['is_chunked']) && $metadata['is_chunked'], 4522 'chunks' => array(), 4523 'single_text' => '' 4524 ); 4525 } 4526 4527 // Track best score for this URL 4528 if ($match['score'] > $url_groups[$source_url]['best_score']) { 4529 $url_groups[$source_url]['best_score'] = $match['score']; 4530 } 4531 4532 // Store chunk info or single text 4533 if ($url_groups[$source_url]['is_chunked']) { 4534 $url_groups[$source_url]['chunks'][] = array( 4535 'id' => $match_id, 4536 'score' => $match['score'], 4537 'chunk_index' => $metadata['chunk_index'] ?? 0, 4538 'text' => $metadata['text'] ?? '' 4539 ); 4540 } else { 4541 // Non-chunked content - just store the text 4542 $url_groups[$source_url]['single_text'] = $metadata['text'] ?? ''; 4543 $url_groups[$source_url]['single_id'] = $match_id; 4544 } 4545 } 4546 4547 // Sort URL groups by best score (highest first) 4548 uasort($url_groups, function($a, $b) { 4549 return $b['best_score'] <=> $a['best_score']; 4550 }); 4551 4552 // Take top 3 unique URLs 4553 $top_urls = array_slice($url_groups, 0, 3, true); 4554 4555 // Track which match IDs are actually used for context (only from top 3 URLs) 4556 foreach ($top_urls as $group) { 4557 if ($group['is_chunked']) { 4558 foreach ($group['chunks'] as $chunk) { 4559 $matches_used_for_context[] = $chunk['id']; 4560 } 4561 } elseif (!empty($group['single_id'])) { 4562 $matches_used_for_context[] = $group['single_id']; 4563 } 4564 } 4565 4566 // Build content from top URLs 4567 foreach ($top_urls as $source_url => $group) { 4568 $full_text = ''; 4569 4570 if ($group['is_chunked']) { 4571 // Fetch all chunks for this URL and reassemble 4572 $full_text = $this->reassemble_chunks_from_pinecone($source_url, $bot_config); 4573 4574 // If fetching all chunks fails, fall back to matched chunks 4575 if (empty($full_text)) { 4576 // Sort matched chunks by index and concatenate 4577 usort($group['chunks'], function($a, $b) { 4578 return $a['chunk_index'] <=> $b['chunk_index']; 4579 }); 4580 4581 $chunk_texts = array(); 4582 foreach ($group['chunks'] as $chunk) { 4583 $chunk_texts[] = $chunk['text']; 4584 } 4585 $full_text = implode("\n\n", $chunk_texts); 4586 } 4587 } else { 4588 $full_text = $group['single_text']; 4589 } 4590 4591 if (!empty($full_text)) { 4460 4592 $content .= "## Reference " . ($matches_used + 1) . " ##\n"; 4461 $content .= $match['metadata']['text'] . "\n\n"; 4462 4463 // NEW: Extract source_url from metadata if it exists 4464 if (!empty($match['metadata']['source_url']) && $match['metadata']['source_url'] !== '#') { 4465 $valid_urls[] = $match['metadata']['source_url']; 4466 $content .= "URL: " . $match['metadata']['source_url'] . "\n\n"; 4467 } 4468 4469 // NEW: Extract any URLs from the text content itself 4593 $content .= $full_text . "\n\n"; 4594 4595 if (!empty($source_url) && $source_url !== '#') { 4596 $valid_urls[] = $source_url; 4597 $content .= "URL: " . $source_url . "\n\n"; 4598 } 4599 4600 // Extract any URLs from the text content itself 4470 4601 preg_match_all( 4471 '#\bhttps?://[^\s<>"\']+#i', 4472 $ match['metadata']['text'],4602 '#\bhttps?://[^\s<>"\']+#i', 4603 $full_text, 4473 4604 $content_urls 4474 4605 ); … … 4476 4607 $valid_urls = array_merge($valid_urls, $content_urls[0]); 4477 4608 } 4478 4479 $matches_used_for_context[] = $match['id'] ?? $index; 4609 4480 4610 $matches_used++; 4481 4611 } … … 4503 4633 4504 4634 $match_id_for_display = $match['id'] ?? $index; 4505 4635 4636 // Check for chunk metadata in Pinecone 4637 $is_chunk = isset($match['metadata']['is_chunked']) && $match['metadata']['is_chunked']; 4638 $chunk_index = isset($match['metadata']['chunk_index']) ? intval($match['metadata']['chunk_index']) : null; 4639 $total_chunks = isset($match['metadata']['total_chunks']) ? intval($match['metadata']['total_chunks']) : null; 4640 4641 // Also detect chunk from vector ID pattern: {hash}_chunk_{index} 4642 if (!$is_chunk && MxChat_Chunker::is_chunk_vector_id($match_id_for_display)) { 4643 $is_chunk = true; 4644 } 4645 4506 4646 $all_matches[] = [ 4507 4647 'document_id' => $match_id_for_display, … … 4514 4654 'role_restriction' => $role_restriction, 4515 4655 'has_access' => $has_access, 4516 'filtered_out' => !$has_access 4656 'filtered_out' => !$has_access, 4657 'is_chunk' => $is_chunk, 4658 'chunk_index' => $chunk_index, 4659 'total_chunks' => $total_chunks 4517 4660 ]; 4518 4661 } … … 4580 4723 // Cache individual role for 1 hour 4581 4724 wp_cache_set($cache_key, $role_restriction, 'mxchat_vector_roles', 3600); 4582 4725 4583 4726 return $role_restriction; 4727 } 4728 4729 /** 4730 * Fetch and reassemble all chunks for a URL from Pinecone 4731 * 4732 * @param string $source_url The source URL to fetch chunks for 4733 * @param array $bot_config Bot-specific Pinecone configuration 4734 * @return string Reassembled content from all chunks 4735 */ 4736 private function reassemble_chunks_from_pinecone($source_url, $bot_config) { 4737 $api_key = $bot_config['api_key'] ?? ''; 4738 $host = $bot_config['host'] ?? ''; 4739 $namespace = $bot_config['namespace'] ?? ''; 4740 4741 if (empty($host) || empty($api_key)) { 4742 return ''; 4743 } 4744 4745 $base_hash = md5($source_url); 4746 4747 // Use Pinecone list API to find all chunk vectors with this prefix 4748 $list_url = "https://{$host}/vectors/list"; 4749 4750 $list_body = array( 4751 'prefix' => $base_hash . '_chunk_', 4752 'limit' => 100 4753 ); 4754 4755 if (!empty($namespace)) { 4756 $list_body['namespace'] = $namespace; 4757 } 4758 4759 $list_response = wp_remote_post($list_url, array( 4760 'headers' => array( 4761 'Api-Key' => $api_key, 4762 'accept' => 'application/json', 4763 'content-type' => 'application/json' 4764 ), 4765 'body' => wp_json_encode($list_body), 4766 'timeout' => 30 4767 )); 4768 4769 if (is_wp_error($list_response)) { 4770 //error_log('[MXCHAT-CHUNK] List API error: ' . $list_response->get_error_message()); 4771 return ''; 4772 } 4773 4774 $list_data = json_decode(wp_remote_retrieve_body($list_response), true); 4775 4776 if (empty($list_data['vectors'])) { 4777 //error_log('[MXCHAT-CHUNK] No chunk vectors found for URL: ' . $source_url); 4778 return ''; 4779 } 4780 4781 // Extract vector IDs 4782 $vector_ids = array(); 4783 foreach ($list_data['vectors'] as $vector) { 4784 if (isset($vector['id'])) { 4785 $vector_ids[] = $vector['id']; 4786 } 4787 } 4788 4789 if (empty($vector_ids)) { 4790 return ''; 4791 } 4792 4793 // Fetch all chunk content 4794 $fetch_url = "https://{$host}/vectors/fetch"; 4795 4796 $fetch_body = array( 4797 'ids' => $vector_ids 4798 ); 4799 4800 if (!empty($namespace)) { 4801 $fetch_body['namespace'] = $namespace; 4802 } 4803 4804 $fetch_response = wp_remote_post($fetch_url, array( 4805 'headers' => array( 4806 'Api-Key' => $api_key, 4807 'accept' => 'application/json', 4808 'content-type' => 'application/json' 4809 ), 4810 'body' => wp_json_encode($fetch_body), 4811 'timeout' => 30 4812 )); 4813 4814 if (is_wp_error($fetch_response)) { 4815 //error_log('[MXCHAT-CHUNK] Fetch API error: ' . $fetch_response->get_error_message()); 4816 return ''; 4817 } 4818 4819 $fetch_data = json_decode(wp_remote_retrieve_body($fetch_response), true); 4820 4821 if (empty($fetch_data['vectors'])) { 4822 return ''; 4823 } 4824 4825 // Sort chunks by index and reassemble 4826 $chunks = array(); 4827 foreach ($fetch_data['vectors'] as $id => $vector) { 4828 $metadata = $vector['metadata'] ?? array(); 4829 $chunk_index = $metadata['chunk_index'] ?? 0; 4830 $text = $metadata['text'] ?? ''; 4831 4832 // Store chunk with its index 4833 $chunks[$chunk_index] = $text; 4834 } 4835 4836 // Sort by chunk index 4837 ksort($chunks); 4838 4839 // Reassemble content 4840 return implode("\n\n", $chunks); 4584 4841 } 4585 4842 … … 5158 5415 ]); 5159 5416 5417 // Setup streaming headers now that we know we're actually streaming 5418 $this->setup_streaming_headers(); 5419 5160 5420 $ch = curl_init(); 5161 5421 curl_setopt($ch, CURLOPT_URL, 'https://openrouter.ai/api/v1/chat/completions'); … … 5382 5642 5383 5643 $body = json_encode($request_body); 5644 5645 // Setup streaming headers now that we know we're actually streaming 5646 $this->setup_streaming_headers(); 5384 5647 5385 5648 // Use cURL for streaming support … … 5642 5905 } 5643 5906 5907 // Setup streaming headers now that we know we're actually streaming 5908 $this->setup_streaming_headers(); 5909 5644 5910 // Use cURL for streaming support 5645 5911 $ch = curl_init(); … … 5916 6182 'stream' => true 5917 6183 ]); 6184 6185 // Setup streaming headers now that we know we're actually streaming 6186 $this->setup_streaming_headers(); 5918 6187 5919 6188 // Use cURL for streaming support … … 6141 6410 'stream' => true 6142 6411 ]); 6412 6413 // Setup streaming headers now that we know we're actually streaming 6414 $this->setup_streaming_headers(); 6143 6415 6144 6416 // Use cURL for streaming support … … 8185 8457 delete_transient("mxchat_include_pdf_in_context_{$session_id}"); 8186 8458 delete_transient("mxchat_include_word_in_context_{$session_id}"); 8187 8459 8460 // Clear form addon state (pending forms and submitted forms) 8461 delete_option("mxchat_pending_form_{$session_id}"); 8462 delete_option("mxchat_submitted_forms_{$session_id}"); 8463 8188 8464 //error_log("MxChat: Cleared all data for session: {$session_id}"); 8189 8465 } … … 8515 8791 8516 8792 // Clean up any double spaces or awkward punctuation left behind 8517 $cleaned_response = preg_replace('/\s+/', ' ', $cleaned_response); 8518 $cleaned_response = preg_replace('/\s+([.,;:!?])/', '$1', $cleaned_response); 8793 // IMPORTANT: Only collapse horizontal whitespace (spaces/tabs), preserve newlines for markdown formatting 8794 $cleaned_response = preg_replace('/[^\S\n]+/', ' ', $cleaned_response); // Collapse spaces/tabs but NOT newlines 8795 $cleaned_response = preg_replace('/[^\S\n]+([.,;:!?])/', '$1', $cleaned_response); // Same for punctuation cleanup 8519 8796 8520 8797 error_log("Final cleaned response: " . $cleaned_response); -
mxchat-basic/trunk/includes/class-mxchat-public.php
r3428255 r3430202 24 24 */ 25 25 public function enqueue_testing_assets() { 26 // Only load for admin users or if testing parameter is present 27 if (current_user_can('administrator') || isset($_GET['mxchat_test'])) { 28 26 // Check if frontend debugger is enabled in settings 27 $options = get_option('mxchat_options', array()); 28 $show_debugger = isset($options['show_frontend_debugger']) ? $options['show_frontend_debugger'] : 'on'; 29 30 // Only load if setting is enabled AND (user is admin or testing parameter is present) 31 if ($show_debugger === 'on' && (current_user_can('administrator') || isset($_GET['mxchat_test']))) { 32 29 33 // Get plugin URL for assets 30 34 $plugin_url = plugin_dir_url(dirname(__FILE__)); -
mxchat-basic/trunk/includes/class-mxchat-utils.php
r3406402 r3430202 38 38 // Remove only null bytes and other control characters, but preserve newlines (\n = \x0A) and carriage returns (\r = \x0D) 39 39 $safe_content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $safe_content); 40 41 // Check if chunking should be applied 42 $chunker = MxChat_Chunker::from_settings(); 43 if ($chunker->should_chunk($safe_content)) { 44 //error_log('[MXCHAT-DB] Content exceeds chunk threshold, using chunked submission'); 45 return self::submit_chunked_content($safe_content, $source_url, $api_key, $bot_id, $content_type, $chunker); 46 } 40 47 41 48 // UPDATED: Generate the embedding using bot-specific configuration … … 525 532 } 526 533 } 527 } 534 535 /** 536 * Submit content as multiple chunks 537 * 538 * Splits large content into chunks, generates embeddings for each, 539 * and stores them with chunk metadata for later reassembly. 540 * 541 * @param string $content The content to chunk and store 542 * @param string $source_url The source URL 543 * @param string $api_key The API key for embeddings 544 * @param string $bot_id The bot ID 545 * @param string $content_type The content type 546 * @param MxChat_Chunker $chunker The chunker instance 547 * @return bool|WP_Error True on success, WP_Error on failure 548 */ 549 private static function submit_chunked_content($content, $source_url, $api_key, $bot_id, $content_type, $chunker) { 550 global $wpdb; 551 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; 552 553 error_log('[MXCHAT-CHUNK-DEBUG] Starting chunked submission for: ' . $source_url); 554 error_log('[MXCHAT-CHUNK-DEBUG] Content length: ' . strlen($content) . ' chars'); 555 556 // First, delete any existing chunks for this URL (clean slate) 557 $delete_result = self::delete_chunks_for_url($source_url, $bot_id); 558 if (is_wp_error($delete_result)) { 559 error_log('[MXCHAT-CHUNK-DEBUG] Warning: Failed to delete existing chunks: ' . $delete_result->get_error_message()); 560 // Continue anyway - we'll overwrite with upsert 561 } 562 563 // Split content into chunks 564 $chunks = $chunker->chunk_text($content); 565 $total_chunks = count($chunks); 566 567 error_log('[MXCHAT-CHUNK-DEBUG] Created ' . $total_chunks . ' chunks'); 568 foreach ($chunks as $i => $chunk) { 569 error_log('[MXCHAT-CHUNK-DEBUG] Chunk ' . $i . ' length: ' . strlen($chunk) . ' chars, preview: ' . substr($chunk, 0, 100)); 570 } 571 572 //error_log('[MXCHAT-CHUNK] Split content into ' . $total_chunks . ' chunks'); 573 574 if ($total_chunks === 0) { 575 return new WP_Error('chunking_failed', 'Content could not be split into chunks'); 576 } 577 578 $errors = array(); 579 $is_pinecone = self::is_pinecone_enabled_for_bot($bot_id); 580 581 foreach ($chunks as $index => $chunk_text) { 582 // Generate chunk metadata 583 $chunk_metadata = MxChat_Chunker::create_chunk_metadata($index, $total_chunks, $source_url); 584 $chunk_vector_id = MxChat_Chunker::generate_chunk_vector_id($source_url, $index); 585 586 //error_log('[MXCHAT-CHUNK] Processing chunk ' . ($index + 1) . '/' . $total_chunks . ' (ID: ' . $chunk_vector_id . ')'); 587 588 // Generate embedding for this chunk 589 $embedding_vector = self::generate_embedding($chunk_text, $api_key, $bot_id); 590 591 if (!is_array($embedding_vector)) { 592 $errors[] = new WP_Error('embedding_failed', 'Failed to generate embedding for chunk ' . $index); 593 //error_log('[MXCHAT-CHUNK] Failed to generate embedding for chunk ' . $index); 594 continue; 595 } 596 597 if ($is_pinecone) { 598 // Store in Pinecone with chunk metadata 599 $result = self::store_chunk_in_pinecone( 600 $embedding_vector, 601 $chunk_text, 602 $source_url, 603 $chunk_vector_id, 604 $bot_id, 605 $content_type, 606 $chunk_metadata 607 ); 608 } else { 609 // Store in WordPress DB with chunk metadata 610 $content_with_metadata = MxChat_Chunker::format_chunk_for_storage($chunk_text, $chunk_metadata); 611 $embedding_vector_serialized = maybe_serialize($embedding_vector); 612 613 $result = self::store_chunk_in_wordpress_db( 614 $content_with_metadata, 615 $source_url, 616 $embedding_vector_serialized, 617 $table_name, 618 $content_type, 619 $chunk_metadata 620 ); 621 } 622 623 if (is_wp_error($result)) { 624 $errors[] = $result; 625 //error_log('[MXCHAT-CHUNK] Failed to store chunk ' . $index . ': ' . $result->get_error_message()); 626 } 627 } 628 629 if (count($errors) === $total_chunks) { 630 return new WP_Error('chunking_failed', 'Failed to store any chunks'); 631 } 632 633 if (!empty($errors)) { 634 //error_log('[MXCHAT-CHUNK] Completed with ' . count($errors) . ' errors out of ' . $total_chunks . ' chunks'); 635 return new WP_Error('chunking_partial_failure', 636 sprintf('Failed to store %d of %d chunks', count($errors), $total_chunks)); 637 } 638 639 //error_log('[MXCHAT-CHUNK] Successfully stored all ' . $total_chunks . ' chunks'); 640 return true; 641 } 642 643 /** 644 * Store a single chunk in Pinecone with chunk-specific metadata 645 */ 646 private static function store_chunk_in_pinecone($embedding_vector, $chunk_text, $source_url, $vector_id, $bot_id, $content_type, $chunk_metadata) { 647 // Get Pinecone configuration 648 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) { 649 $pinecone_options = get_option('mxchat_pinecone_addon_options'); 650 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; 651 $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; 652 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? ''; 653 } else { 654 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id); 655 if (empty($bot_pinecone_config)) { 656 $pinecone_options = get_option('mxchat_pinecone_addon_options'); 657 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; 658 $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; 659 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? ''; 660 } else { 661 $api_key = $bot_pinecone_config['api_key'] ?? ''; 662 $host = $bot_pinecone_config['host'] ?? ''; 663 $namespace = $bot_pinecone_config['namespace'] ?? ''; 664 } 665 } 666 667 if (empty($host) || empty($api_key)) { 668 return new WP_Error('pinecone_config', 'Pinecone is not properly configured'); 669 } 670 671 $api_endpoint = "https://{$host}/vectors/upsert"; 672 673 // Build metadata with chunk information 674 $metadata = array( 675 'text' => $chunk_text, 676 'source_url' => $source_url, 677 'type' => $content_type, 678 'is_chunked' => true, 679 'chunk_index' => $chunk_metadata['chunk_index'], 680 'total_chunks' => $chunk_metadata['total_chunks'], 681 'parent_url_hash' => $chunk_metadata['parent_url_hash'], 682 'last_updated' => time(), 683 'created_at' => time(), 684 'bot_id' => $bot_id 685 ); 686 687 $vector_data = array( 688 'id' => $vector_id, 689 'values' => $embedding_vector, 690 'metadata' => $metadata 691 ); 692 693 $request_body = array( 694 'vectors' => array($vector_data) 695 ); 696 697 if (!empty($namespace)) { 698 $request_body['namespace'] = $namespace; 699 } 700 701 $response = wp_remote_post($api_endpoint, array( 702 'headers' => array( 703 'Api-Key' => $api_key, 704 'accept' => 'application/json', 705 'content-type' => 'application/json' 706 ), 707 'body' => wp_json_encode($request_body), 708 'timeout' => 30 709 )); 710 711 if (is_wp_error($response)) { 712 return $response; 713 } 714 715 $response_code = wp_remote_retrieve_response_code($response); 716 if ($response_code !== 200) { 717 return new WP_Error('pinecone_api', 'Pinecone API error: HTTP ' . $response_code); 718 } 719 720 return true; 721 } 722 723 /** 724 * Store a single chunk in WordPress database 725 */ 726 private static function store_chunk_in_wordpress_db($content_with_metadata, $source_url, $embedding_vector_serialized, $table_name, $content_type, $chunk_metadata) { 727 global $wpdb; 728 729 // For chunks, we always insert new rows (no duplicate checking) 730 // The URL includes chunk info in the metadata, but source_url stays the same for grouping 731 $result = $wpdb->insert( 732 $table_name, 733 array( 734 'url' => $source_url, 735 'article_content' => $content_with_metadata, 736 'embedding_vector' => $embedding_vector_serialized, 737 'source_url' => $source_url, 738 'content_type' => $content_type, 739 'timestamp' => current_time('mysql') 740 ), 741 array('%s', '%s', '%s', '%s', '%s', '%s') 742 ); 743 744 if ($result === false) { 745 return new WP_Error('database_failed', 'Failed to insert chunk: ' . $wpdb->last_error); 746 } 747 748 return true; 749 } 750 751 /** 752 * Delete all chunks for a given URL 753 * 754 * @param string $source_url The source URL 755 * @param string $bot_id The bot ID 756 * @return bool|WP_Error True on success, WP_Error on failure 757 */ 758 public static function delete_chunks_for_url($source_url, $bot_id = 'default') { 759 //error_log('[MXCHAT-CHUNK-DELETE] Deleting chunks for URL: ' . $source_url); 760 761 if (self::is_pinecone_enabled_for_bot($bot_id)) { 762 return self::delete_pinecone_chunks_by_url($source_url, $bot_id); 763 } else { 764 return self::delete_wordpress_chunks_by_url($source_url); 765 } 766 } 767 768 /** 769 * Delete all chunks for a URL from Pinecone 770 */ 771 private static function delete_pinecone_chunks_by_url($source_url, $bot_id) { 772 // Get Pinecone configuration 773 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) { 774 $pinecone_options = get_option('mxchat_pinecone_addon_options'); 775 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; 776 $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; 777 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? ''; 778 } else { 779 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id); 780 if (empty($bot_pinecone_config)) { 781 $pinecone_options = get_option('mxchat_pinecone_addon_options'); 782 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; 783 $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; 784 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? ''; 785 } else { 786 $api_key = $bot_pinecone_config['api_key'] ?? ''; 787 $host = $bot_pinecone_config['host'] ?? ''; 788 $namespace = $bot_pinecone_config['namespace'] ?? ''; 789 } 790 } 791 792 if (empty($host) || empty($api_key)) { 793 return new WP_Error('pinecone_config', 'Pinecone is not properly configured'); 794 } 795 796 $base_vector_id = md5($source_url); 797 $vectors_to_delete = array(); 798 799 // Add the original single-vector ID (for non-chunked content) 800 $vectors_to_delete[] = $base_vector_id; 801 802 // Use Pinecone list API to find all chunk vectors with this prefix 803 $list_url = "https://{$host}/vectors/list"; 804 805 $list_body = array( 806 'prefix' => $base_vector_id . '_chunk_', 807 'limit' => 100 808 ); 809 810 if (!empty($namespace)) { 811 $list_body['namespace'] = $namespace; 812 } 813 814 $list_response = wp_remote_post($list_url, array( 815 'headers' => array( 816 'Api-Key' => $api_key, 817 'accept' => 'application/json', 818 'content-type' => 'application/json' 819 ), 820 'body' => wp_json_encode($list_body), 821 'timeout' => 30 822 )); 823 824 if (!is_wp_error($list_response)) { 825 $list_data = json_decode(wp_remote_retrieve_body($list_response), true); 826 if (!empty($list_data['vectors'])) { 827 foreach ($list_data['vectors'] as $vector) { 828 if (isset($vector['id'])) { 829 $vectors_to_delete[] = $vector['id']; 830 } 831 } 832 } 833 } 834 835 if (empty($vectors_to_delete)) { 836 //error_log('[MXCHAT-CHUNK-DELETE] No vectors found to delete'); 837 return true; 838 } 839 840 //error_log('[MXCHAT-CHUNK-DELETE] Deleting ' . count($vectors_to_delete) . ' vectors from Pinecone'); 841 842 // Delete vectors 843 $delete_url = "https://{$host}/vectors/delete"; 844 845 $delete_body = array( 846 'ids' => $vectors_to_delete 847 ); 848 849 if (!empty($namespace)) { 850 $delete_body['namespace'] = $namespace; 851 } 852 853 $delete_response = wp_remote_post($delete_url, array( 854 'headers' => array( 855 'Api-Key' => $api_key, 856 'accept' => 'application/json', 857 'content-type' => 'application/json' 858 ), 859 'body' => wp_json_encode($delete_body), 860 'timeout' => 30 861 )); 862 863 if (is_wp_error($delete_response)) { 864 return $delete_response; 865 } 866 867 $response_code = wp_remote_retrieve_response_code($delete_response); 868 if ($response_code !== 200) { 869 return new WP_Error('pinecone_delete', 'Failed to delete vectors: HTTP ' . $response_code); 870 } 871 872 return true; 873 } 874 875 /** 876 * Delete all chunks for a URL from WordPress database 877 */ 878 private static function delete_wordpress_chunks_by_url($source_url) { 879 global $wpdb; 880 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; 881 882 // Delete all rows with this source_url (handles both chunked and non-chunked) 883 $result = $wpdb->delete( 884 $table_name, 885 array('source_url' => $source_url), 886 array('%s') 887 ); 888 889 if ($result === false) { 890 return new WP_Error('database_delete', 'Failed to delete chunks: ' . $wpdb->last_error); 891 } 892 893 //error_log('[MXCHAT-CHUNK-DELETE] Deleted ' . $result . ' rows from WordPress DB'); 894 return true; 895 } 896 } -
mxchat-basic/trunk/js/chat-script.js
r3418641 r3430202 31 31 function getChatSession() { 32 32 var sessionId = getCookie('mxchat_session_id'); 33 //console.log("Session ID retrieved from cookie: ", sessionId); 34 33 35 34 if (!sessionId) { 36 35 sessionId = generateSessionId(); 37 //console.log("Generated new session ID: ", sessionId);38 36 setChatSession(sessionId); 39 37 } 40 41 //console.log("Final session ID: ", sessionId); 38 42 39 return sessionId; 43 40 } … … 215 212 // CORE CHAT FUNCTIONALITY 216 213 // ==================================== 214 215 // Helper functions to disable/enable chat input while waiting for response 216 function disableChatInput() { 217 var chatInput = document.getElementById('chat-input'); 218 var sendButton = document.getElementById('send-button'); 219 if (chatInput) { 220 chatInput.disabled = true; 221 chatInput.style.opacity = '0.6'; 222 } 223 if (sendButton) { 224 sendButton.disabled = true; 225 sendButton.style.opacity = '0.5'; 226 sendButton.style.pointerEvents = 'none'; 227 } 228 } 229 230 function enableChatInput() { 231 var chatInput = document.getElementById('chat-input'); 232 var sendButton = document.getElementById('send-button'); 233 if (chatInput) { 234 chatInput.disabled = false; 235 chatInput.style.opacity = '1'; 236 chatInput.focus(); 237 } 238 if (sendButton) { 239 sendButton.disabled = false; 240 sendButton.style.opacity = '1'; 241 sendButton.style.pointerEvents = 'auto'; 242 } 243 } 244 217 245 // Update your existing sendMessage function 218 246 function sendMessage() { 219 247 var message = $('#chat-input').val(); 220 248 221 249 // ADD PROMPT HOOK HERE 222 if (typeof customMxChatFilter === 'function') { 223 message = customMxChatFilter(message, "prompt"); 224 } 225 250 if (typeof customMxChatFilter === 'function') { 251 message = customMxChatFilter(message, "prompt"); 252 } 253 226 254 if (message) { 255 // Disable input while waiting for response 256 disableChatInput(); 257 227 258 appendMessage("user", message); 228 259 $('#chat-input').val(''); … … 256 287 message = customMxChatFilter(message, "prompt"); 257 288 } 258 289 290 // Disable input while waiting for response 291 disableChatInput(); 292 259 293 var sessionId = getChatSession(); 260 294 … … 370 404 data: ajaxData, 371 405 success: function(response) { 372 373 // IMMEDIATE CHAT MODE UPDATE - This should be FIRST 374 if (response.chat_mode) { 375 //console.log("Updating chat mode to:", response.chat_mode); 376 updateChatModeIndicator(response.chat_mode); 377 } 378 379 // Also check in data property if response is wrapped 380 if (response.data && response.data.chat_mode) { 381 //console.log("Updating chat mode from data to:", response.data.chat_mode); 382 updateChatModeIndicator(response.data.chat_mode); 383 } 384 // Log the full response for debugging 385 //console.log("API Response:", response); 386 387 // IMMEDIATE CHAT MODE UPDATE - Handle this first, before any other processing 406 // IMMEDIATE CHAT MODE UPDATE - This should be FIRST 388 407 if (response.chat_mode) { 389 //console.log("Updating chat mode immediately to:", response.chat_mode);390 408 updateChatModeIndicator(response.chat_mode); 409 } 410 411 // Also check in data property if response is wrapped 412 if (response.data && response.data.chat_mode) { 413 updateChatModeIndicator(response.data.chat_mode); 391 414 } 392 415 … … 442 465 } 443 466 444 // Log the error with code for debugging445 //console.log("Response data:", response.data);446 //console.error("API Error:", errorMessage, "Code:", errorCode);447 448 467 // Format user-friendly error message 449 468 let displayMessage = errorMessage; … … 524 543 } 525 544 } else { 526 // This should rarely happen now that errors are handled above527 ////console.error("Unexpected response format:", response);528 545 replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists."); 529 546 } … … 540 557 }, 541 558 error: function(xhr, status, error) { 542 //console.error("AJAX Error:", status, error);543 //console.log("Response Text:", xhr.responseText);544 545 559 let errorMessage = "An unexpected error occurred."; 546 560 … … 548 562 try { 549 563 const responseJson = JSON.parse(xhr.responseText); 550 //console.log("Parsed error response:", responseJson);551 564 552 565 if (responseJson.data && responseJson.data.error_message) { … … 579 592 $('.mxchat-input-holder textarea').data('pending-message', message); 580 593 581 //console.log("Using streaming for message:", message);582 583 594 const currentModel = mxchatChat.model || 'gpt-4o'; 584 595 if (!isStreamingSupported(currentModel)) { 585 //console.log("Streaming not supported, falling back to regular call");586 596 callMxChat(message, callback); 587 597 return; … … 629 639 }) 630 640 .then(response => { 631 //console.log("Streaming response received:", response);632 633 641 // Store the response for potential fallback handling 634 642 const responseClone = response.clone(); 635 643 636 644 if (!response.ok) { 637 645 // Try to get error details from response 638 646 return responseClone.json().then(errorData => { 639 //console.log("Server returned error response:", errorData);640 647 throw { isServerError: true, data: errorData }; 641 648 }).catch(() => { … … 647 654 const contentType = response.headers.get('content-type'); 648 655 if (contentType && contentType.includes('application/json')) { 649 //console.log("Received JSON response instead of stream");650 656 return responseClone.json().then(data => { 651 //console.log("Processing JSON response:", data);652 653 657 // IMMEDIATE CHAT MODE UPDATE for JSON response 654 658 if (data.chat_mode) { 655 //console.log("JSON Response: Updating chat mode to:", data.chat_mode);656 659 updateChatModeIndicator(data.chat_mode); 657 660 } 658 661 659 662 // Check for testing panel 660 663 if (window.mxchatTestPanelInstance && data.testing_data) { 661 //console.log('Testing data found in streaming JSON response:', data.testing_data);662 664 window.mxchatTestPanelInstance.handleTestingData(data.testing_data); 663 665 } 664 666 665 667 // Handle the JSON response directly 666 668 handleNonStreamResponse(data, callback); … … 677 679 reader.read().then(({ done, value }) => { 678 680 if (done) { 679 //console.log("Streaming completed, final content:", accumulatedContent);680 681 681 // If streaming completed but no content was received, try to get response as fallback 682 682 if (!streamingStarted || !accumulatedContent) { 683 //console.log("Stream completed with no content, checking for fallback data");684 685 683 // Try to read the response as JSON 686 684 responseClone.text().then(text => { 687 685 try { 688 686 const data = JSON.parse(text); 689 if (data.text || data.message ) {687 if (data.text || data.message || data.html) { 690 688 handleNonStreamResponse(data, callback); 691 689 } else { … … 721 719 722 720 if (data === '[DONE]') { 723 //console.log("Received [DONE] signal");724 725 721 if (!accumulatedContent) { 726 //console.log("No content received before [DONE]");727 722 $('.bot-message.temporary-message').remove(); 728 723 callMxChat(message, callback); 729 724 return; 730 725 } 731 726 727 // Re-enable chat input after streaming completes 728 enableChatInput(); 729 732 730 if (callback) { 733 731 callback(accumulatedContent); … … 738 736 try { 739 737 const json = JSON.parse(data); 740 738 741 739 // IMMEDIATE CHAT MODE UPDATE FOR STREAMING 742 740 if (json.chat_mode) { 743 //console.log("Stream: Updating chat mode immediately to:", json.chat_mode);744 741 updateChatModeIndicator(json.chat_mode); 745 742 } 746 743 747 744 // Handle testing data 748 745 if (json.testing_data && !testingDataReceived) { 749 //console.log('Testing data received in stream:', json.testing_data);750 746 if (window.mxchatTestPanelInstance) { 751 747 window.mxchatTestPanelInstance.handleTestingData(json.testing_data); … … 758 754 accumulatedContent += json.content; 759 755 updateStreamingMessage(accumulatedContent); 760 } 756 } 761 757 // Handle complete response in stream (fallback response) 762 else if (json.text || json.message) { 763 //console.log("Received complete response in stream:", json); 758 else if (json.text || json.message || json.html) { 764 759 handleNonStreamResponse(json, callback); 765 760 return; 766 761 } 767 // Handle errors - UPDATED to properly display API errors762 // Handle errors 768 763 else if (json.error) { 769 //console.error("Streaming error:", json.error);770 764 771 765 // Get error message from various possible fields 772 766 let errorMessage = json.error_message || json.message || json.text || 773 767 (typeof json.error === 'string' ? json.error : 'An error occurred. Please try again.'); 768 769 // Re-enable chat input on error 770 enableChatInput(); 774 771 775 772 // Display the error directly in the chat … … 782 779 } 783 780 } catch (e) { 784 // console.error('Error parsing SSE data:', e, 'Data:', data);781 // SSE data parsing error - silently continue 785 782 } 786 783 } … … 789 786 processStream(); 790 787 }).catch(streamError => { 791 //console.error('Error reading stream:', streamError);792 788 $('.bot-message.temporary-message').remove(); 793 789 callMxChat(message, callback); … … 798 794 }) 799 795 .catch(error => { 800 //console.log('Streaming failed:', error);801 802 796 // Check if we have server error data with chat mode 803 797 if (error && error.isServerError && error.data) { 804 //console.log('Using server error response data');805 806 798 // Check for chat mode in error data 807 799 if (error.data.chat_mode) { 808 //console.log("Error Response: Updating chat mode to:", error.data.chat_mode);809 800 updateChatModeIndicator(error.data.chat_mode); 810 801 } 811 802 812 803 handleNonStreamResponse(error.data, callback); 813 804 } else { 814 805 // Only fall back to regular call if we don't have any response data 815 //console.log('No response data available, falling back to regular call');816 806 $('.bot-message.temporary-message').remove(); 817 807 callMxChat(message, callback); … … 820 810 } 821 811 822 // Helper function to handle non-streaming responses (updated to include immediate chat mode handling)812 // Helper function to handle non-streaming responses 823 813 function handleNonStreamResponse(data, callback) { 824 //console.log("Handling non-stream response:", data);825 826 814 // IMMEDIATE CHAT MODE UPDATE FOR NON-STREAMING RESPONSES 827 815 if (data.chat_mode) { 828 //console.log("Non-Stream: Updating chat mode immediately to:", data.chat_mode);829 816 updateChatModeIndicator(data.chat_mode); 830 817 } … … 832 819 // Also check in data property if response is wrapped 833 820 if (data.data && data.data.chat_mode) { 834 //console.log("Non-Stream: Updating chat mode from data to:", data.data.chat_mode);835 821 updateChatModeIndicator(data.data.chat_mode); 836 822 } 837 823 838 // Remove temporary message839 $('.bot-message.temporary-message').remove();824 // NOTE: Don't remove temporary message here - let replaceLastMessage handle it 825 // This prevents a visual gap between thinking dots disappearing and content appearing 840 826 841 827 // SECURITY FIX: Check for errors FIRST … … 931 917 } 932 918 919 // Ensure chat input is re-enabled (safety net for edge cases) 920 enableChatInput(); 921 933 922 if (callback) { 934 923 callback(data.text || data.message || ''); 935 924 } 936 925 } 926 937 927 // Enhanced updateChatModeIndicator function for immediate DOM updates 938 928 function updateChatModeIndicator(mode) { 939 //console.log("updateChatModeIndicator called with mode:", mode);940 929 const indicator = document.getElementById('chat-mode-indicator'); 941 930 if (indicator) { 942 931 const oldText = indicator.textContent; 943 932 944 933 if (mode === 'agent') { 945 934 indicator.textContent = 'Live Agent'; 946 935 startPolling(); 947 //console.log("Set indicator to Live Agent");948 936 } else { 949 937 // Everything else is AI mode … … 951 939 indicator.textContent = customAiText; 952 940 stopPolling(); 953 //console.log("Set indicator to AI Agent:", customAiText); 954 } 955 941 } 942 956 943 // Force immediate DOM update and reflow 957 944 if (oldText !== indicator.textContent) { … … 960 947 indicator.offsetHeight; // Trigger reflow 961 948 indicator.style.display = ''; 962 949 963 950 // Double-check after a brief moment to ensure the change stuck 964 951 setTimeout(() => { 965 //console.log("Final indicator text:", indicator.textContent);966 952 if (mode === 'agent' && indicator.textContent !== 'Live Agent') { 967 //console.warn("Mode indicator update failed, forcing update");968 953 indicator.textContent = 'Live Agent'; 969 954 } else if (mode !== 'agent' && indicator.textContent === 'Live Agent') { 970 //console.warn("Mode indicator stuck on Live Agent, forcing AI update");971 955 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent'; 972 956 indicator.textContent = customAiText; … … 974 958 }, 50); 975 959 } 976 } else {977 //console.error("Chat mode indicator element not found!");978 }979 }980 981 // Helper function to handle non-streaming responses (updated to include chat mode handling)982 function handleNonStreamResponse(data, callback) {983 //console.log("Handling non-stream response:", data);984 985 // Remove temporary message986 $('.bot-message.temporary-message').remove();987 988 // Handle different response formats989 if (data.text || data.html || data.message) {990 991 // Apply response hooks992 if (data.text && typeof customMxChatFilter === 'function') {993 data.text = customMxChatFilter(data.text, "response");994 }995 if (data.message && typeof customMxChatFilter === 'function') {996 data.message = customMxChatFilter(data.message, "response");997 }998 999 // Handle chat mode updates for streaming responses too1000 handleChatModeUpdates(data, data.text || data.message);1001 1002 // Display the response1003 if (data.text && data.html) {1004 replaceLastMessage("bot", data.text, data.html);1005 } else if (data.text) {1006 replaceLastMessage("bot", data.text);1007 } else if (data.html) {1008 replaceLastMessage("bot", "", data.html);1009 } else if (data.message) {1010 replaceLastMessage("bot", data.message);1011 }1012 }1013 1014 // Handle other response properties1015 if (data.data && data.data.filename) {1016 showActivePdf(data.data.filename);1017 activePdfFile = data.data.filename;1018 }1019 1020 if (data.redirect_url) {1021 setTimeout(() => {1022 window.location.href = data.redirect_url;1023 }, 1500);1024 }1025 1026 if (callback) {1027 callback(data.text || data.message || '');1028 960 } 1029 961 } … … 1066 998 } 1067 999 1068 // Update the event handlers to use the correct function names 1069 $('#send-button').off('click').on('click', function() { 1070 sendMessage(); // Use the updated sendMessage function 1000 // Update the event handlers to use the correct function names (using event delegation) 1001 $(document).on('click', '#send-button', function() { 1002 disableChatInput(); 1003 sendMessage(); 1071 1004 }); 1072 1005 1073 // Override enter key handler 1074 $( '#chat-input').off('keypress').on('keypress', function(e) {1006 // Override enter key handler (using event delegation) 1007 $(document).on('keypress', '#chat-input', function(e) { 1075 1008 if (e.which == 13 && !e.shiftKey) { 1076 1009 e.preventDefault(); 1077 sendMessage(); // Use the updated sendMessage function 1010 disableChatInput(); 1011 sendMessage(); 1078 1012 } 1079 1013 }); … … 1141 1075 // Append HTML content if provided 1142 1076 if (messageHtml && sender !== "user") { 1143 fullMessage += '<br><br>' + messageHtml; 1077 // Only add line breaks if there's actual text content before the HTML 1078 if (fullMessage && fullMessage.trim()) { 1079 fullMessage += '<br><br>' + messageHtml; 1080 } else { 1081 fullMessage = messageHtml; 1082 } 1144 1083 } 1145 1084 … … 1169 1108 } 1170 1109 } catch (error) { 1171 // console.error("Error rendering message:", error);1110 // Error rendering message - silently continue 1172 1111 } 1173 1112 } … … 1288 1227 1289 1228 if (lastMessageDiv.length) { 1290 lastMessageDiv.fadeOut(200, function() { 1291 $(this) 1292 .html(fullMessage) 1293 .removeClass('bot-message user-message') 1294 .addClass(messageClass) 1295 .attr('dir', 'auto') 1296 .css({ 1297 'background-color': bgColor, 1298 'color': fontColor, 1299 }) 1300 .removeClass('temporary-message') 1301 .fadeIn(200, function() { 1302 // FIXED: Use the helper function for link tracking 1303 if (sender === "bot" || sender === "agent") { 1304 attachLinkTracking($(this), responseText); 1305 } 1306 1307 if (sender === "bot" || sender === "agent") { 1308 const lastUserMessage = $('#chat-box').find('.user-message').last(); 1309 if (lastUserMessage.length) { 1310 scrollElementToTop(lastUserMessage); 1311 } 1312 // Show notification if chat is hidden 1313 if ($('#floating-chatbot').hasClass('hidden')) { 1314 showNotification(); 1315 } 1316 } 1317 }); 1318 }); 1229 // Replace content immediately to prevent visual gap between thinking dots and response 1230 lastMessageDiv 1231 .html(fullMessage) 1232 .removeClass('bot-message user-message temporary-message') 1233 .addClass(messageClass) 1234 .attr('dir', 'auto') 1235 .css({ 1236 'background-color': bgColor, 1237 'color': fontColor, 1238 }); 1239 1240 // Handle link tracking and scroll 1241 if (sender === "bot" || sender === "agent") { 1242 attachLinkTracking(lastMessageDiv, responseText); 1243 1244 const lastUserMessage = $('#chat-box').find('.user-message').last(); 1245 if (lastUserMessage.length) { 1246 scrollElementToTop(lastUserMessage); 1247 } 1248 // Show notification if chat is hidden 1249 if ($('#floating-chatbot').hasClass('hidden')) { 1250 showNotification(); 1251 } 1252 } 1253 1254 // Re-enable chat input after response is displayed 1255 enableChatInput(); 1319 1256 } else { 1320 1257 appendMessage(sender, responseText, responseHtml, images); 1258 // Re-enable chat input after response is displayed 1259 enableChatInput(); 1321 1260 } 1322 1261 } … … 1654 1593 // Force visibility 1655 1594 $('#floating-chatbot-button').removeClass('hidden'); 1656 //console.log('Showing widget'); 1657 } 1658 1595 } 1596 1659 1597 function hideChatWidget() { 1660 1598 $('#floating-chatbot-button').css('display', 'none'); 1661 1599 $('#floating-chatbot-button').addClass('hidden'); 1662 //console.log('Hiding widget');1663 1600 } 1664 1601 … … 1691 1628 1692 1629 function createNotificationBadge() { 1693 //console.log("Creating notification badge...");1694 1630 const chatButton = document.getElementById('floating-chatbot-button'); 1695 //console.log("Chat button found:", !!chatButton); 1696 1631 1697 1632 if (!chatButton) return; 1698 1633 1699 1634 // Remove any existing badge first 1700 1635 const existingBadge = chatButton.querySelector('.chat-notification-badge'); 1701 1636 if (existingBadge) { 1702 //console.log("Removing existing badge");1703 1637 existingBadge.remove(); 1704 1638 } … … 1780 1714 // LIVE AGENT FUNCTIONALITY 1781 1715 // ==================================== 1782 1783 function updateChatModeIndicator(mode) {1784 //console.log("updateChatModeIndicator called with mode:", mode);1785 const indicator = document.getElementById('chat-mode-indicator');1786 if (indicator) {1787 const oldText = indicator.textContent;1788 1789 if (mode === 'agent') {1790 indicator.textContent = 'Live Agent';1791 startPolling();1792 //console.log("Set indicator to Live Agent");1793 } else {1794 // Everything else is AI mode1795 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';1796 indicator.textContent = customAiText;1797 stopPolling();1798 //console.log("Set indicator to AI Agent:", customAiText);1799 }1800 1801 // Force immediate DOM update1802 if (oldText !== indicator.textContent) {1803 // Force a reflow to ensure the change is visible immediately1804 indicator.offsetHeight;1805 1806 // Double-check after a brief moment1807 setTimeout(() => {1808 //console.log("Final indicator text:", indicator.textContent);1809 }, 50);1810 }1811 } else {1812 //console.error("Chat mode indicator element not found!");1813 }1814 }1815 1716 1816 1717 function startPolling() { … … 1819 1720 // Start new polling interval 1820 1721 pollingInterval = setInterval(checkForAgentMessages, 5000); 1821 //console.log("Started agent message polling"); 1822 } 1823 1722 } 1723 1824 1724 function stopPolling() { 1825 1725 if (pollingInterval) { 1826 1726 clearInterval(pollingInterval); 1827 1727 pollingInterval = null; 1828 //console.log("Stopped agent message polling");1829 1728 } 1830 1729 } … … 1865 1764 }, 1866 1765 error: function (xhr, status, error) { 1867 // console.error("Polling error:", xhr, status, error);1766 // Polling error - silently continue 1868 1767 } 1869 1768 }); … … 1873 1772 // CHAT HISTORY & PERSISTENCE 1874 1773 // ==================================== 1875 // Update the loadChatHistory function: 1774 1876 1775 function loadChatHistory() { 1877 1776 // Prevent duplicate loading 1878 1777 if (chatHistoryLoaded) { 1879 // console.log('Chat history already loaded, skipping...');1880 1778 return; 1881 1779 } … … 1999 1897 }, 2000 1898 error: function(xhr, status, error) { 2001 // console.error("AJAX error loading chat history:", status, error);1899 // Error loading chat history - silently continue 2002 1900 } 2003 1901 }); … … 2020 1918 const container = document.getElementById('active-pdf-container'); 2021 1919 const nameElement = document.getElementById('active-pdf-name'); 2022 1920 2023 1921 if (!container || !nameElement) { 2024 //console.error('PDF container elements not found');2025 1922 return; 2026 1923 } 2027 1924 2028 1925 nameElement.textContent = filename; 2029 1926 container.style.display = 'flex'; 2030 1927 } 2031 1928 2032 1929 function showActiveWord(filename) { 2033 1930 const container = document.getElementById('active-word-container'); 2034 1931 const nameElement = document.getElementById('active-word-name'); 2035 1932 2036 1933 if (!container || !nameElement) { 2037 //console.error('Word document container elements not found');2038 1934 return; 2039 1935 } 2040 1936 2041 1937 nameElement.textContent = filename; 2042 1938 container.style.display = 'flex'; … … 2070 1966 }) 2071 1967 .catch(error => { 2072 // console.error('Error removing PDF:', error);1968 // Error removing PDF - silently continue 2073 1969 }); 2074 1970 } 2075 1971 2076 1972 function removeActiveWord() { 2077 1973 const container = document.getElementById('active-word-container'); … … 2101 1997 }) 2102 1998 .catch(error => { 2103 // console.error('Error removing Word document:', error);1999 // Error removing Word document - silently continue 2104 2000 }); 2105 2001 } 2106 2002 2107 2003 // ==================================== 2108 2004 // CONSENT & COMPLIANCE (GDPR) 2109 2005 // ==================================== 2110 2006 2111 2007 function initializeChatVisibility() { 2112 //console.log('Initializing chat visibility'); 2113 const complianzEnabled = mxchatChat.complianz_toggle === 'on' || 2114 mxchatChat.complianz_toggle === '1' || 2008 const complianzEnabled = mxchatChat.complianz_toggle === 'on' || 2009 mxchatChat.complianz_toggle === '1' || 2115 2010 mxchatChat.complianz_toggle === 1; 2116 2011 2117 2012 if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') { 2118 2013 // Initial check 2119 2014 checkConsentAndShowChat(); 2120 2015 2121 2016 // Listen for consent changes 2122 2017 $(document).on('cmplz_status_change', function(event) { 2123 //console.log('Status change detected');2124 2018 checkConsentAndShowChat(); 2125 2019 }); … … 2140 2034 var consentStatus = cmplz_has_consent('marketing'); 2141 2035 var consentType = complianz.consenttype; 2142 2143 //console.log('Checking consent:', {status: consentStatus,type: consentType}); 2144 2036 2145 2037 let $widget = $('#floating-chatbot-button'); 2146 2038 let $chatbot = $('#floating-chatbot'); 2147 2039 let $preChat = $('#pre-chat-message'); 2148 2040 2149 2041 if (consentStatus === true) { 2150 //console.log('Consent granted - showing widget');2151 2042 $widget 2152 2043 .removeClass('no-consent') … … 2155 2046 .fadeTo(500, 1); 2156 2047 $chatbot.removeClass('no-consent'); 2157 2048 2158 2049 // Show pre-chat message if not dismissed 2159 2050 checkPreChatDismissal(); 2160 2051 } else { 2161 //console.log('No consent - hiding widget');2162 2052 $widget 2163 2053 .addClass('no-consent') … … 2168 2058 }); 2169 2059 $chatbot.addClass('no-consent'); 2170 2060 2171 2061 // Hide pre-chat message when no consent 2172 2062 $preChat.hide(); … … 2195 2085 }, 2196 2086 error: function() { 2197 // console.error('Failed to check pre-chat message dismissal status.');2087 // Error checking pre-chat dismissal - silently continue 2198 2088 } 2199 2089 }); 2200 2090 } 2201 2091 2202 2092 function handlePreChatDismissal() { 2203 2093 $('#pre-chat-message').fadeOut(200); … … 2213 2103 }, 2214 2104 error: function() { 2215 // console.error('Failed to dismiss pre-chat message.');2105 // Error dismissing pre-chat message - silently continue 2216 2106 } 2217 2107 }); … … 2323 2213 2324 2214 if (!sessionId) { 2325 //console.error('No session ID found');2326 2215 alert('Error: No session ID found'); 2327 2216 return; 2328 2217 } 2329 2218 2330 2219 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) { 2331 //console.error('mxchatChat not properly configured:', mxchatChat);2332 2220 alert('Error: Ajax configuration missing'); 2333 2221 return; … … 2373 2261 activePdfFile = data.data.filename; 2374 2262 } else { 2375 //console.error('Upload failed:', data.data);2376 2263 alert('Failed to upload PDF. Please try again.'); 2377 2264 } 2378 2265 } catch (error) { 2379 //console.error('Upload error:', error);2380 2266 alert('Error uploading file. Please try again.'); 2381 2267 } finally { … … 2397 2283 2398 2284 if (!sessionId) { 2399 //console.error('No session ID found');2400 2285 alert('Error: No session ID found'); 2401 2286 return; 2402 2287 } 2403 2288 2404 2289 // Disable buttons and show loading state 2405 2290 const uploadBtn = document.getElementById('word-upload-btn'); … … 2441 2326 activeWordFile = data.data.filename; 2442 2327 } else { 2443 //console.error('Upload failed:', data.data);2444 2328 alert('Failed to upload Word document. Please try again.'); 2445 2329 } 2446 2330 } catch (error) { 2447 //console.error('Upload error:', error);2448 2331 alert('Error uploading file. Please try again.'); 2449 2332 } finally { … … 2509 2392 // Only run email collection setup if it's enabled 2510 2393 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') { 2511 //console.log('Email collection is enabled, setting up handlers...');2512 2513 2394 // Email collection form setup and handlers 2514 2395 const emailForm = document.getElementById('email-collection-form'); … … 2666 2547 } 2667 2548 2668 // MAIN FORM SUBMIT HANDLER - This is the critical fix 2669 //console.log('Setting up form submit handler...'); 2670 2549 // MAIN FORM SUBMIT HANDLER 2671 2550 // Remove any existing event listeners first 2672 2551 emailForm.removeEventListener('submit', handleFormSubmit); 2673 2552 2674 2553 // Add the form submit handler 2675 2554 emailForm.addEventListener('submit', handleFormSubmit); 2676 2555 2677 2556 function handleFormSubmit(event) { 2678 //console.log('Form submit handler triggered!');2679 2557 event.preventDefault(); 2680 2558 event.stopPropagation(); 2681 2559 2682 2560 // Prevent double submission 2683 2561 if (isSubmitting) { 2684 //console.log('Already submitting, ignoring...');2685 2562 return false; 2686 2563 } … … 2691 2568 const sessionId = getChatSession(); 2692 2569 2693 //console.log('Form data:', { userEmail, userName, sessionId });2694 2695 2570 // Validate email before submission 2696 2571 if (!userEmail) { … … 2713 2588 clearEmailError(); 2714 2589 setSubmissionState(true); 2715 2716 //console.log('Sending AJAX request...');2717 2590 2718 2591 // Prepare form data with optional name … … 2737 2610 }) 2738 2611 .then((response) => { 2739 //console.log('Response received:', response);2740 2612 if (!response.ok) { 2741 2613 throw new Error(`HTTP error! status: ${response.status}`); … … 2744 2616 }) 2745 2617 .then((data) => { 2746 //console.log('Response data:', data);2747 2618 setSubmissionState(false); 2748 2619 2749 2620 if (data.success) { 2750 2621 // Show chat immediately … … 2765 2636 }) 2766 2637 .catch((error) => { 2767 //console.error('Email submission error:', error);2768 2638 setSubmissionState(false); 2769 2639 showEmailError('An error occurred. Please try again.'); … … 2837 2707 // Initial state check 2838 2708 if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) { 2839 //console.log('Using server-provided email state:', mxchatChat.initial_email_state);2840 2841 2709 const emailState = mxchatChat.initial_email_state; 2842 2710 if (emailState.show_email_form) { … … 2884 2752 }) 2885 2753 .catch((error) => { 2886 // console.warn('Email check failed, defaulting to email form:', error);2754 // Email check failed - default to email form 2887 2755 showEmailForm(); 2888 2756 }); 2889 2757 } 2890 2758 2891 2759 } else { 2892 console.error('Email collection is enabled but essential elements are missing:', { 2893 emailForm: !!emailForm, 2894 emailBlocker: !!emailBlocker, 2895 chatbotWrapper: !!chatbotWrapper 2896 }); 2897 } 2898 } else { 2899 //console.log('Email collection is disabled'); 2760 // Email collection is enabled but essential elements are missing - silently continue 2761 } 2900 2762 } 2901 2763 … … 2925 2787 }, 2926 2788 success: function() { 2927 //console.log('Pre-chat message dismissed for 24 hours.');2928 2929 2789 // Ensure the message is hidden after dismissal 2930 2790 $('#pre-chat-message').hide(); 2931 2791 }, 2932 2792 error: function() { 2933 // console.error('Failed to dismiss pre-chat message.');2793 // Error dismissing pre-chat message - silently continue 2934 2794 } 2935 2795 }); -
mxchat-basic/trunk/js/content-selector.js
r3364278 r3430202 186 186 function renderContentItems(items) { 187 187 let html = ''; 188 188 189 189 items.forEach(function(item) { 190 190 const isSelected = selectedItems.has(item.id); 191 191 const isProcessed = item.already_processed; 192 193 // Updated badge text 194 const badgeText = isProcessed ? 'In Knowledge Base' : 'Not In Knowledge Base'; 192 const chunkCount = item.chunk_count || 0; 193 194 // Updated badge text - include chunk count if > 1 195 let badgeText = 'Not In Knowledge Base'; 196 if (isProcessed) { 197 badgeText = chunkCount > 1 ? `In Knowledge Base (${chunkCount} chunks)` : 'In Knowledge Base'; 198 } 195 199 const badgeClass = isProcessed ? 'mxchat-kb-processed-badge' : 'mxchat-kb-unprocessed-badge'; 196 197 200 201 198 202 html += ` 199 203 <div class="mxchat-kb-content-item ${isProcessed ? 'processed' : ''}" data-id="${item.id}"> … … 217 221 `; 218 222 }); 219 223 220 224 $contentList.html(html); 221 225 -
mxchat-basic/trunk/js/knowledge-processing.js
r3394595 r3430202 1152 1152 } 1153 1153 }); 1154 1155 // ======================================== 1156 // CHUNK GROUP TOGGLE FUNCTIONALITY 1157 // ======================================== 1158 1159 // Handle chunk group expand/collapse toggle 1160 $(document).on('click', '.mxchat-chunk-toggle', function(e) { 1161 e.preventDefault(); 1162 e.stopPropagation(); 1163 1164 const $button = $(this); 1165 const groupId = $button.data('group-id'); 1166 const $chunkRows = $('.mxchat-chunk-row.' + groupId); 1167 1168 // Toggle expanded state 1169 if ($button.hasClass('expanded')) { 1170 // Collapse 1171 $chunkRows.slideUp(300); 1172 $button.removeClass('expanded'); 1173 } else { 1174 // Expand 1175 $chunkRows.slideDown(300); 1176 $button.addClass('expanded'); 1177 } 1178 }); 1179 1180 // Handle delete button for chunk groups 1181 $(document).on('click', '.delete-button-group', function(e) { 1182 e.preventDefault(); 1183 e.stopPropagation(); 1184 1185 const $button = $(this); 1186 const sourceUrl = $button.data('source-url'); 1187 const chunkCount = $button.data('chunk-count'); 1188 const dataSource = $button.data('data-source'); 1189 const botId = $button.data('bot-id'); 1190 const nonce = $button.data('nonce'); 1191 1192 // Confirm deletion 1193 if (!confirm('Are you sure you want to delete all ' + chunkCount + ' chunks for this URL? This action cannot be undone.')) { 1194 return; 1195 } 1196 1197 // Show loading state 1198 $button.prop('disabled', true); 1199 $button.find('.dashicons').removeClass('dashicons-trash').addClass('dashicons-update spin'); 1200 1201 // Make AJAX request to delete all chunks for this URL 1202 $.ajax({ 1203 url: ajaxurl, 1204 type: 'POST', 1205 data: { 1206 action: 'mxchat_delete_chunks_by_url', 1207 source_url: sourceUrl, 1208 data_source: dataSource, 1209 bot_id: botId, 1210 nonce: nonce 1211 }, 1212 success: function(response) { 1213 if (response.success) { 1214 // Remove the group header row and all chunk rows 1215 const $headerRow = $button.closest('tr'); 1216 const groupId = $headerRow.data('group-id'); 1217 $('.mxchat-chunk-row.' + groupId).fadeOut(300, function() { 1218 $(this).remove(); 1219 }); 1220 $headerRow.fadeOut(300, function() { 1221 $(this).remove(); 1222 }); 1223 } else { 1224 var errorMsg = response.data || 'Unknown error'; 1225 if (typeof response.data === 'object' && response.data.message) { 1226 errorMsg = response.data.message; 1227 } 1228 alert('Error deleting chunks: ' + errorMsg); 1229 $button.prop('disabled', false); 1230 $button.find('.dashicons').removeClass('dashicons-update spin').addClass('dashicons-trash'); 1231 } 1232 }, 1233 error: function(xhr, status, error) { 1234 console.error('AJAX error:', xhr.responseText); 1235 alert('Error deleting chunks: ' + (error || 'Server error')); 1236 $button.prop('disabled', false); 1237 $button.find('.dashicons').removeClass('dashicons-update spin').addClass('dashicons-trash'); 1238 } 1239 }); 1240 }); 1154 1241 }); -
mxchat-basic/trunk/js/mxchat-admin.js
r3428255 r3430202 449 449 // Determine which AJAX action and nonce to use: 450 450 var ajaxAction, nonce; 451 // *** UPDATED: Add Pinecone fields to prompts action ***451 // *** UPDATED: Add Pinecone fields and chunking fields to prompts action *** 452 452 if (name.indexOf('mxchat_prompts_options') !== -1 || 453 name === 'mxchat_auto_sync_posts' || 453 name === 'mxchat_auto_sync_posts' || 454 454 name === 'mxchat_auto_sync_pages' || 455 455 name.indexOf('mxchat_auto_sync_') === 0 || 456 name.indexOf('mxchat_pinecone_addon_options') !== -1) { // *** ADD THIS LINE *** 456 name.indexOf('mxchat_pinecone_addon_options') !== -1 || 457 name.indexOf('mxchat_chunk') === 0) { // Chunking settings 457 458 ajaxAction = 'mxchat_save_prompts_setting'; 458 459 nonce = mxchatPromptsAdmin.prompts_setting_nonce; -
mxchat-basic/trunk/js/mxchat-test-streaming.js
r3340699 r3430202 1 1 jQuery(document).ready(function($) { 2 // Manual test button click 2 3 $('#mxchat-test-streaming-btn').on('click', function() { 3 4 const $button = $(this); 4 5 const $result = $('#mxchat-test-streaming-result'); 5 6 6 7 $button.prop('disabled', true).text('Testing...'); 7 8 $result.text('Testing streaming environment...'); 8 9 9 10 // Test both backend capability AND frontend environment 10 11 testCompleteStreamingEnvironment() … … 20 21 }) 21 22 .finally(() => { 22 $button.prop('disabled', false). text('Test Streaming Compatibility');23 $button.prop('disabled', false).html('<span class="dashicons dashicons-admin-tools" style="vertical-align: middle; margin-right: 5px;"></span>Test Streaming Compatibility'); 23 24 }); 24 25 }); 25 26 27 // Intercept the streaming toggle BEFORE autosave fires 28 // We use a capturing event handler to run first 29 const streamingToggle = document.getElementById('enable_streaming_toggle'); 30 if (streamingToggle) { 31 streamingToggle.addEventListener('change', function(e) { 32 if (this.checked) { 33 // Prevent the default autosave from firing immediately 34 e.stopImmediatePropagation(); 35 36 const $toggle = $(this); 37 const $result = $('#mxchat-test-streaming-result'); 38 const $button = $('#mxchat-test-streaming-btn'); 39 40 // User is enabling streaming - run automatic compatibility test first 41 $button.prop('disabled', true); 42 $toggle.prop('disabled', true); 43 $result.css('color', '#666').html('🔄 Testing streaming compatibility before enabling...'); 44 45 testCompleteStreamingEnvironment() 46 .then(result => { 47 if (result.success) { 48 // Test passed - now save the setting 49 saveStreamingSetting('on'); 50 $result.css('color', 'green').html('✅ Streaming enabled - compatibility verified!'); 51 } else { 52 // Streaming failed - turn it back off (don't save 'on') 53 $toggle.prop('checked', false); 54 $result.css('color', 'red').html(result.message); 55 } 56 }) 57 .catch(error => { 58 // Error during test - turn it back off 59 $toggle.prop('checked', false); 60 $result.css('color', 'red').html('❌ Streaming test failed: ' + error.message + '<br>Streaming has been disabled.'); 61 }) 62 .finally(() => { 63 $button.prop('disabled', false); 64 $toggle.prop('disabled', false); 65 }); 66 } 67 // If turning OFF, let the normal autosave handle it 68 }, true); // 'true' for capturing phase - runs before jQuery handlers 69 } 70 71 // Helper function to save the streaming setting via AJAX 72 function saveStreamingSetting(value) { 73 $.ajax({ 74 url: mxchatTestStreamingAjax.ajax_url, 75 type: 'POST', 76 data: { 77 action: 'mxchat_save_setting', 78 name: 'enable_streaming_toggle', 79 value: value, 80 _ajax_nonce: mxchatTestStreamingAjax.settings_nonce 81 } 82 }); 83 } 84 26 85 function testCompleteStreamingEnvironment() { 27 86 return new Promise((resolve, reject) => { … … 53 112 formData.append('session_id', 'streaming_test_' + Date.now()); 54 113 formData.append('nonce', mxchatTestStreamingAjax.nonce); 114 formData.append('force_streaming_test', '1'); // Force streaming mode for testing 55 115 56 116 fetch(mxchatTestStreamingAjax.ajax_url, { -
mxchat-basic/trunk/js/mxchat_transcripts.js
r3418641 r3430202 323 323 } 324 324 325 // Render RAG context data in the modal 325 // Render RAG context data in the modal - grouped by URL 326 326 function renderRagContext(data, $container) { 327 327 let html = ''; … … 343 343 html += '</div>'; 344 344 345 // Top matches section 345 // Top matches section - grouped by URL 346 346 if (data.top_matches && data.top_matches.length > 0) { 347 // Group matches by source URL 348 const groupedByUrl = {}; 349 data.top_matches.forEach(function(match) { 350 const url = match.source_display || 'Unknown'; 351 if (!groupedByUrl[url]) { 352 groupedByUrl[url] = { 353 url: url, 354 isUrl: url.startsWith('http'), 355 bestScore: 0, 356 usedForContext: false, 357 totalChunks: match.total_chunks || 1, 358 matchedChunks: [], 359 isChunked: match.is_chunk || false, 360 roleRestriction: match.role_restriction 361 }; 362 } 363 364 // Track best score 365 if (match.similarity_percentage > groupedByUrl[url].bestScore) { 366 groupedByUrl[url].bestScore = match.similarity_percentage; 367 } 368 369 // Track if any chunk was used 370 if (match.used_for_context) { 371 groupedByUrl[url].usedForContext = true; 372 } 373 374 // Add chunk info 375 groupedByUrl[url].matchedChunks.push({ 376 chunkIndex: match.chunk_index, 377 score: match.similarity_percentage, 378 usedForContext: match.used_for_context, 379 aboveThreshold: match.above_threshold, 380 contentPreview: match.content_preview 381 }); 382 }); 383 384 // Convert to array and sort by best score 385 const urlGroups = Object.values(groupedByUrl).sort(function(a, b) { 386 return b.bestScore - a.bestScore; 387 }); 388 389 // Count unique URLs used 390 const usedUrlCount = urlGroups.filter(function(g) { return g.usedForContext; }).length; 391 347 392 html += '<div class="mxchat-rag-matches">'; 348 393 html += '<h3>Retrieved Documents</h3>'; 349 350 data.top_matches.forEach(function(match, index) { 351 const isUsed = match.used_for_context; 352 const isAboveThreshold = match.above_threshold; 353 const cardClass = isUsed ? 'mxchat-rag-match-used' : (isAboveThreshold ? 'mxchat-rag-match-above' : 'mxchat-rag-match-below'); 354 const statusIcon = isUsed ? '✅' : (isAboveThreshold ? '⚠️' : '❌'); 355 const statusLabel = isUsed ? 'Used for Response' : (isAboveThreshold ? 'Above Threshold' : 'Below Threshold'); 394 html += '<p class="mxchat-rag-matches-summary">' + usedUrlCount + ' entr' + (usedUrlCount === 1 ? 'y' : 'ies') + ' used for response'; 395 html += ' <span style="color: #64748b; font-size: 12px;">(from ' + data.top_matches.length + ' chunk matches)</span></p>'; 396 397 urlGroups.forEach(function(group, groupIndex) { 398 const cardClass = group.usedForContext ? 'mxchat-rag-match-used' : 'mxchat-rag-match-below'; 399 const statusIcon = group.usedForContext ? '✅' : '❌'; 400 const statusLabel = group.usedForContext ? 'Used for Response' : 'Not Used'; 401 402 // Build chunk summary badge 403 let chunkBadge = ''; 404 if (group.isChunked && group.totalChunks > 1) { 405 const usedChunkCount = group.matchedChunks.filter(function(c) { return c.usedForContext; }).length; 406 chunkBadge = '<span class="mxchat-rag-chunk-badge">' + usedChunkCount + '/' + group.totalChunks + ' chunks</span>'; 407 } 356 408 357 409 html += '<div class="mxchat-rag-match-card ' + cardClass + '">'; 358 410 html += '<div class="mxchat-rag-match-header">'; 359 html += '<span class="mxchat-rag-match- rank">#' + (index + 1) + '</span>';360 html += '<span class="mxchat-rag-match-score">' + match.similarity_percentage + '%</span>';361 html += '<span class="mxchat-rag-match-status ' + ( isUsed ? 'status-used' : (isAboveThreshold ? 'status-above' : 'status-below')) + '">';411 html += '<span class="mxchat-rag-match-score">' + group.bestScore + '%</span>'; 412 html += chunkBadge; 413 html += '<span class="mxchat-rag-match-status ' + (group.usedForContext ? 'status-used' : 'status-below') + '">'; 362 414 html += statusIcon + ' ' + statusLabel; 363 415 html += '</span>'; … … 365 417 366 418 html += '<div class="mxchat-rag-match-source">'; 367 if ( match.source_display && match.source_display.startsWith('http')) {368 html += '<a href="' + escapeHtml( match.source_display) + '" target="_blank" rel="noopener noreferrer">';369 html += '🔗 ' + escapeHtml( match.source_display);419 if (group.isUrl) { 420 html += '<a href="' + escapeHtml(group.url) + '" target="_blank" rel="noopener noreferrer">'; 421 html += '🔗 ' + escapeHtml(group.url); 370 422 html += '</a>'; 371 423 } else { 372 html += '📄 ' + escapeHtml( match.source_display || 'Content snippet');424 html += '📄 ' + escapeHtml(group.url); 373 425 } 374 426 html += '</div>'; 375 427 376 if (match.content_preview) { 377 html += '<div class="mxchat-rag-match-preview">'; 378 html += escapeHtml(match.content_preview); 428 // Show role restriction if not public 429 if (group.roleRestriction && group.roleRestriction !== 'public') { 430 html += '<div class="mxchat-rag-match-meta">'; 431 html += '<span class="mxchat-rag-role-badge">🔒 ' + escapeHtml(group.roleRestriction) + '</span>'; 379 432 html += '</div>'; 380 433 } 381 434 382 // Show role restriction if not public 383 if (match.role_restriction && match.role_restriction !== 'public') { 384 html += '<div class="mxchat-rag-match-meta">'; 385 html += '<span class="mxchat-rag-role-badge">🔒 ' + escapeHtml(match.role_restriction) + '</span>'; 435 // Expandable chunk details if multiple chunks 436 if (group.matchedChunks.length > 1) { 437 html += '<div class="mxchat-rag-chunk-toggle" data-group="' + groupIndex + '">▶ Show ' + group.matchedChunks.length + ' matched chunks</div>'; 438 html += '<div class="mxchat-rag-chunk-details" data-group="' + groupIndex + '">'; 439 440 // Sort chunks by index 441 const sortedChunks = group.matchedChunks.slice().sort(function(a, b) { 442 return (a.chunkIndex || 0) - (b.chunkIndex || 0); 443 }); 444 445 sortedChunks.forEach(function(chunk) { 446 const chunkNum = (chunk.chunkIndex !== null && chunk.chunkIndex !== undefined) ? chunk.chunkIndex + 1 : '?'; 447 const chunkClass = chunk.usedForContext ? 'chunk-used' : 'chunk-not-used'; 448 const chunkIcon = chunk.usedForContext ? '✓' : '○'; 449 450 html += '<div class="mxchat-rag-chunk-row ' + chunkClass + '">'; 451 html += '<span class="mxchat-rag-chunk-icon">' + chunkIcon + '</span>'; 452 html += '<span class="mxchat-rag-chunk-num">Chunk ' + chunkNum + '</span>'; 453 html += '<span class="mxchat-rag-chunk-score">' + chunk.score + '%</span>'; 454 html += '</div>'; 455 }); 456 386 457 html += '</div>'; 387 458 } … … 408 479 409 480 $container.html(html); 481 482 // Add click handlers for chunk toggles 483 $container.find('.mxchat-rag-chunk-toggle').on('click', function() { 484 const groupId = $(this).data('group'); 485 const $details = $container.find('.mxchat-rag-chunk-details[data-group="' + groupId + '"]'); 486 const isExpanded = $details.is(':visible'); 487 488 if (isExpanded) { 489 $details.slideUp(200); 490 $(this).text('▶ Show ' + $details.find('.mxchat-rag-chunk-row').length + ' matched chunks'); 491 } else { 492 $details.slideDown(200); 493 $(this).text('▼ Hide chunks'); 494 } 495 }); 410 496 } 411 497 -
mxchat-basic/trunk/js/test-panel.js
r3389316 r3430202 395 395 updateTopMatches(topMatches, threshold) { 396 396 const scoresEl = this.panel.querySelector('#similarity-scores'); 397 397 398 398 if (!topMatches || topMatches.length === 0) { 399 399 scoresEl.innerHTML = '<div class="no-data-message">No similarity data available</div>'; 400 400 return; 401 401 } 402 402 403 // Group matches by source URL 404 const groupedByUrl = {}; 405 topMatches.forEach((match) => { 406 const url = match.source_display || 'Unknown'; 407 if (!groupedByUrl[url]) { 408 groupedByUrl[url] = { 409 url: url, 410 isUrl: url.startsWith('http'), 411 bestScore: 0, 412 usedForContext: false, 413 totalChunks: match.total_chunks || 1, 414 matchedChunks: [], 415 isChunked: match.is_chunk || false 416 }; 417 } 418 419 // Track best score 420 if (match.similarity_percentage > groupedByUrl[url].bestScore) { 421 groupedByUrl[url].bestScore = match.similarity_percentage; 422 } 423 424 // Track if any chunk was used for context 425 if (match.used_for_context) { 426 groupedByUrl[url].usedForContext = true; 427 } 428 429 // Add chunk info 430 groupedByUrl[url].matchedChunks.push({ 431 chunkIndex: match.chunk_index, 432 score: match.similarity_percentage, 433 usedForContext: match.used_for_context, 434 aboveThreshold: match.above_threshold 435 }); 436 }); 437 438 // Convert to array and sort by best score 439 const urlGroups = Object.values(groupedByUrl).sort((a, b) => b.bestScore - a.bestScore); 440 441 // Count unique URLs used for context 442 const usedUrlCount = urlGroups.filter(g => g.usedForContext).length; 443 403 444 let html = `<div class="matches-header"> 404 <strong>Top ${topMatches.length} matches</strong> 445 <strong>${usedUrlCount} entr${usedUrlCount === 1 ? 'y' : 'ies'} used for AI context</strong> 446 <span class="matches-subheader">(from ${topMatches.length} chunk matches)</span> 405 447 </div>`; 406 407 topMatches.forEach((match, index) => {408 const isAboveThreshold = match.above_threshold;409 const isUsedForContext = match.used_for_context; // Use the actual flag from PHP410 const statusIcon = isAboveThreshold ? '✓' : '✗';411 412 // Determine the correct label based on actual usage413 let c ontextLabel;414 if ( isUsedForContext) {415 con textLabel = 'Used for AI context';416 } else if (isAboveThreshold) {417 contextLabel = 'Above threshold (not used)';418 } else { 419 contextLabel = 'Below threshold';420 }421 422 // Determine card styling - should be based on whether it was actually used423 const cardClass = isUsedForContext ? 'above-threshold' : 'below-threshold';424 448 449 urlGroups.forEach((group, groupIndex) => { 450 const cardClass = group.usedForContext ? 'above-threshold' : 'below-threshold'; 451 const statusIcon = group.usedForContext ? '✓' : '✗'; 452 const contextLabel = group.usedForContext ? 'Used for AI context' : 'Not used'; 453 454 // Build chunk summary 455 let chunkSummary = ''; 456 if (group.isChunked && group.totalChunks > 1) { 457 const usedChunkCount = group.matchedChunks.filter(c => c.usedForContext).length; 458 chunkSummary = `<span class="chunk-summary">${usedChunkCount}/${group.totalChunks} chunks matched</span>`; 459 } 460 461 // Check if this entry has multiple matched chunks to show expand toggle 462 const hasMultipleChunks = group.matchedChunks.length > 1; 463 const expandToggle = hasMultipleChunks 464 ? `<span class="chunk-expand-toggle" data-group="${groupIndex}">▶ Show chunks</span>` 465 : ''; 466 425 467 html += ` 426 468 <div class="match-card ${cardClass}"> … … 428 470 <div class="match-title"> 429 471 <span class="status-icon">${statusIcon}</span> 430 <span class="similarity-score">${match.similarity_percentage}%</span> 472 <span class="similarity-score">${group.bestScore}%</span> 473 ${chunkSummary} 431 474 </div> 432 475 <span class="context-label">${contextLabel}</span> 433 476 </div> 434 477 <div class="match-source"> 435 ${ match.source_display.startsWith('http') ?436 `<span class="source-icon link-icon">🔗</span> ${ match.source_display}` :437 `<span class="source-icon doc-icon">📄</span> ${ match.source_display}`478 ${group.isUrl ? 479 `<span class="source-icon link-icon">🔗</span> ${group.url}` : 480 `<span class="source-icon doc-icon">📄</span> ${group.url}` 438 481 } 439 482 </div> 483 ${expandToggle} 484 ${hasMultipleChunks ? this.renderChunkDetails(group.matchedChunks, groupIndex) : ''} 440 485 </div> 441 486 `; 442 487 }); 443 488 444 489 scoresEl.innerHTML = html; 490 491 // Add click handlers for expand toggles 492 scoresEl.querySelectorAll('.chunk-expand-toggle').forEach(toggle => { 493 toggle.addEventListener('click', (e) => { 494 const groupId = e.target.dataset.group; 495 const details = scoresEl.querySelector(`.chunk-details[data-group="${groupId}"]`); 496 if (details) { 497 const isExpanded = details.classList.toggle('expanded'); 498 e.target.textContent = isExpanded ? '▼ Hide chunks' : '▶ Show chunks'; 499 } 500 }); 501 }); 502 } 503 504 renderChunkDetails(chunks, groupIndex) { 505 // Sort chunks by chunk index 506 const sortedChunks = [...chunks].sort((a, b) => (a.chunkIndex || 0) - (b.chunkIndex || 0)); 507 508 let html = `<div class="chunk-details" data-group="${groupIndex}">`; 509 510 sortedChunks.forEach(chunk => { 511 const chunkNum = (chunk.chunkIndex !== null && chunk.chunkIndex !== undefined) 512 ? chunk.chunkIndex + 1 513 : '?'; 514 const statusClass = chunk.usedForContext ? 'chunk-used' : 'chunk-not-used'; 515 const statusIcon = chunk.usedForContext ? '✓' : '○'; 516 517 html += ` 518 <div class="chunk-detail-row ${statusClass}"> 519 <span class="chunk-detail-icon">${statusIcon}</span> 520 <span class="chunk-detail-num">Chunk ${chunkNum}</span> 521 <span class="chunk-detail-score">${chunk.score}%</span> 522 </div> 523 `; 524 }); 525 526 html += '</div>'; 527 return html; 445 528 } 446 529 -
mxchat-basic/trunk/mxchat-basic.php
r3428255 r3430202 4 4 * Plugin URI: https://mxchat.ai/ 5 5 * Description: AI chatbot for WordPress with OpenAI, Claude, xAI, DeepSeek, live agent, PDF uploads, WooCommerce, and training on website data. 6 * Version: 2.6. 26 * Version: 2.6.3 7 7 * Author: MxChat 8 8 * Author URI: https://mxchat.ai … … 49 49 'includes/class-mxchat-user.php', 50 50 'includes/class-mxchat-meta-box.php', 51 'includes/class-mxchat-chunker.php', 51 52 'includes/pdf-parser/alt_autoload.php', 52 53 'includes/class-mxchat-word-handler.php', -
mxchat-basic/trunk/readme.txt
r3428255 r3430202 6 6 Tested up to: 6.9 7 7 Requires PHP: 7.2 8 Stable tag: 2.6. 28 Stable tag: 2.6.3 9 9 License: GPLv2 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 16 16 [Documentation](https://mxchat.ai/documentation/) 17 17 18 ## Wh y Choose MxChat AI Chatbot for Your WordPress Website?18 ## What makes MxChat the best free AI Chatbot plugin for WordPress? 19 19 20 20 ✅ **6 Major AI Providers in One Plugin**: OpenRouter, OpenAI GPT, Claude, Gemini, xAI Grok, and DeepSeek - switch between 100+ models instantly … … 39 39 [Explore all product videos on YouTube](https://www.youtube.com/@MxChat/videos) 40 40 41 ## 🔥 What's New in Version 2.6.2 42 43 🚀 **Large Pinecone Database Fix** 44 - Fixed: Critical error when viewing Knowledge Database with large datasets (10K+ products) 45 - Fixed: Optimized Pinecone queries to prevent memory exhaustion and timeouts 46 47 📐 **Viewport-Aware Chatbot Sizing** 48 - Fixed: Chatbot no longer gets cut off when browser has bookmarks bar or is zoomed above 100% 49 50 🎯 **Post Type Visibility Control** 51 - New: Control which post types display the auto-display chatbot (include or exclude specific post types) 41 ## 🔥 What's New in Version 2.6.3 42 43 🧩 **Smart Text Chunking for RAG** 44 - New: Large content is automatically split into semantic chunks for improved retrieval accuracy 45 - New: Configurable chunk size (default 4000 chars) in Knowledge Database Settings 46 - Enhancement: Retrieval now finds best matching chunks, fetches all chunks for top URLs, and reassembles them 47 48 📊 **Enhanced Knowledge Base UI** 49 - New: Chunk count badges in WordPress Import modal - see "(X chunks)" for multi-chunk content 50 - New: Collapsible chunk groups in Knowledge Base table - entries with multiple chunks grouped under expandable rows 51 - New: Chunk labels show "Chunk 1 of 5", "Chunk 2 of 5" etc. when expanded 52 53 **Other Enhancements** 54 - New: Option to show/hide frontend debug panel in Chatbot Settings 55 - Fixed: Markdown formatting now preserved correctly when streaming is disabled (newlines were being stripped) 56 - Enhancement: Chat input is now disabled while waiting for bot response, preventing duplicate message submissions 57 - Enhancement: When streaming is turned on it automatically checks if compatible and disables if not. 52 58 53 59 ## Core Features That Set MxChat Apart … … 191 197 == Changelog == 192 198 199 = 2.6.3 - December 31, 2025 = 200 - New: Text chunking for RAG - large content is automatically split into semantic chunks for improved retrieval accuracy 201 - New: Chunk count display in WordPress Import modal - shows "(X chunks)" badge for multi-chunk content 202 - New: Collapsible chunk groups in Knowledge Base table - entries with multiple chunks are grouped under a single expandable row 203 - New: Chunk labels show "Chunk 1 of 5", "Chunk 2 of 5" etc. when chunks are expanded 204 - New: Chunking settings in Knowledge Database Settings tab - configure chunk size (default 4000 chars) 205 - New: Option to show/hide frontend debug panel in Chatbot Settings 206 - Enhancement: Improved retrieval - finds best matching chunks, then fetches all chunks for top URLs and reassembles 207 - Enhancement: Chat input is now disabled while waiting for bot response, preventing duplicate message submissions 208 - Enhancement: When streaming is turned on it automatically checks if compatible and disables if not. 209 - Fixed: Markdown formatting now preserved correctly when streaming is disabled (newlines were being stripped) 210 211 193 212 = 2.6.2 - December 22, 2025 = 194 213 - Fixed: Critical error when viewing Knowledge Database with large Pinecone datasets (10K+ products) … … 653 672 == Upgrade Notice == 654 673 655 = 2.6. 2=656 Critical fix for large Pinecone databases and viewport cutoff issues when browser is zoomed or has bookmarks bar.674 = 2.6.3 = 675 New smart text chunking for RAG improves semantic retrieval accuracy. Enhanced Knowledge Base UI with chunk grouping and collapsible display. Chat input now disabled during responses to prevent duplicate submissions and more. 657 676 658 677 == License & Warranty ==
Note: See TracChangeset
for help on using the changeset viewer.