Plugin Directory

Changeset 3430202


Ignore:
Timestamp:
12/31/2025 04:28:36 PM (7 weeks ago)
Author:
mxchat
Message:

Version 2.6.3: Add semantic text chunking for RAG, enhance Knowledge Base UI with chunk grouping

  • Implement automatic text chunking for large content with configurable chunk size
  • Add chunk count badges and collapsible chunk groups in Knowledge Base table
  • Improve retrieval by finding best chunks then reassembling full content
  • Disable chat input during bot response to prevent duplicates
  • Add frontend debug panel toggle in settings
  • Fix markdown formatting preservation when streaming disabled
  • Auto-check streaming compatibility and disable if unsupported
Location:
mxchat-basic
Files:
105 added
20 edited

Legend:

Unmodified
Added
Removed
  • mxchat-basic/trunk/admin/class-ajax-handler.php

    r3428255 r3430202  
    281281                'contextual_awareness_toggle',
    282282                'enable_email_block',
    283                 'enable_name_field'
     283                'enable_name_field',
     284                'show_frontend_debugger'
    284285            ])) {
    285286                //error_log('MXChat Save: Processing toggle: ' . $name);
     
    503504         }
    504505
     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
    505571         // Handle other prompts options
    506572         $options = get_option('mxchat_prompts_options', []);
  • mxchat-basic/trunk/admin/class-knowledge-manager.php

    r3413311 r3430202  
    4242    add_action('admin_post_mxchat_delete_pinecone_prompt', array($this, 'mxchat_handle_pinecone_prompt_delete'));
    4343    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'));
    4445    add_action('wp_ajax_mxchat_update_role_restriction', array($this, 'ajax_mxchat_update_role_restriction'));
    4546   
     
    632633    $sanitized_content = $this->mxchat_sanitize_content_for_api($page_content);
    633634
     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
    634640    if (empty($sanitized_content)) {
    635641        set_transient('mxchat_admin_notice_error',
     
    641647    }
    642648
    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());
    648662        set_transient('mxchat_admin_notice_error', $error_message, 30);
    649         wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
    650         exit;
    651     }
    652 
    653     if (is_array($embedding_vector)) {
    654         $db_result = MxChat_Utils::submit_content_to_db(
    655             $sanitized_content,
    656             $submitted_url,
    657             $api_key,
    658             null,
    659             $bot_id
    660         );
    661 
    662         if (is_wp_error($db_result)) {
    663             $error_message = esc_html__('Failed to store content in database: ', 'mxchat') . esc_html($db_result->get_error_message());
    664             set_transient('mxchat_admin_notice_error', $error_message, 30);
    665         } else {
    666             $success_message = esc_html__('URL content successfully submitted!', 'mxchat');
    667             set_transient('mxchat_admin_notice_success', $success_message, 30);
    668         }
    669663    } 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);
    672666    }
    673667
     
    862856       
    863857        // For debugging purposes
    864         $debugEnabled = false; // Set to true to enable debugging output
     858        $debugEnabled = true; // Set to true to enable debugging output
    865859        $debug = function($message) use ($debugEnabled) {
    866860            if ($debugEnabled) {
    867                 //error_log('[MXCHAT-DEBUG] ' . $message);
     861                error_log('[MXCHAT-EXTRACT-DEBUG] ' . $message);
    868862            }
    869863        };
     
    966960        ];
    967961       
    968         // First handle Elementor content
     962        // First handle Elementor content - get only leaf widget containers to avoid duplicates
    969963        $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")]');
    971966        if ($elementor_widgets && $elementor_widgets->length > 0) {
    972967            $debug("Found Elementor widgets");
     968            $seen_content = array(); // Track seen content to avoid duplicates
    973969            $combined_content = '';
    974970            foreach ($elementor_widgets as $widget) {
    975971                $widget_content = $dom->saveHTML($widget);
    976972                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                    }
    978979                }
    979980            }
     
    989990            $nodes = $xpath->query($selector);
    990991            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));
    996996                if (!empty($content)) {
    997                     $debug("Returning content from selector: " . $selector);
     997                    $debug("Returning content from selector: " . $selector . " (first match only)");
    998998                    return $content;
    999999                }
     
    13901390        global $wpdb;
    13911391        $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
    13941399        if (!empty($processed_items)) {
    13951400            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
    13991422                if ($post_id) {
    14001423                    $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
    14051429                    );
    14061430                }
     
    14521476            }
    14531477           
     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
    14541484            $content_items[] = array(
    14551485                'id' => $id,
     
    14641494                'processed_date' => $processed_date,
    14651495                'db_record_id' => $db_record_id,
    1466                 'data_source' => $data_source
     1496                'data_source' => $data_source,
     1497                'chunk_count' => $chunk_count
    14671498            );
    14681499        }
     
    18011832    }
    18021833   
    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
    18051842    //  Get bot-specific API key
    18061843    $bot_options = $this->get_bot_options($bot_id);
     
    22222259        }
    22232260
    2224         // Convert matches to processed data format
     2261        // Convert matches to processed data format, grouping by URL to count chunks
    22252262        $processed_data = array();
     2263        $url_chunk_counts = array();
    22262264
    22272265        foreach ($all_matches as $match) {
     
    22332271                $post_id = url_to_postid($source_url);
    22342272                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
    22352279                    $created_at = $metadata['created_at'] ?? '';
    22362280                    $processed_date = 'Recently';
     
    22432287                    }
    22442288
    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                    }
    22522300                }
     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;
    22532308            }
    22542309        }
     
    33763431    }
    33773432
    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());
    33993438    }
    34003439}
     
    37353774    }
    37363775   
     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 */
     3783public 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
    37373934    exit;
    37383935}
  • mxchat-basic/trunk/admin/class-pinecone-manager.php

    r3428255 r3430202  
    7373        }
    7474
    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
    7796        $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        }
    79119
    80120        return array(
    81121            'data' => $paged_records,
    82             'total' => $total,
     122            'total' => $total_unique_entries,
    83123            'total_in_database' => $total_in_database,
    84124            'showing_recent_only' => ($total_in_database > 500)
     
    192232                'created_at' => $created_at,
    193233                '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
    195238            );
    196239        }
     
    415458                'bot_id' => $bot_id,
    416459                '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
    418464            );
    419465        }
     
    583629                'bot_id' => $bot_id,
    584630                '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
    586635            );
    587636        }
     
    666715                'bot_id' => $bot_id,
    667716                '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
    669721            );
    670722        }
     
    841893                                'bot_id' => $bot_id,
    842894                                '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
    844899                            );
    845900
  • mxchat-basic/trunk/css/admin-style.css

    r3428255 r3430202  
    46924692
    46934693.mxchat-stat-card {
    4694     background: #f6f7f7;
    46954694    padding: 20px;
    46964695    border-radius: 12px;
     
    47494748
    47504749.mxchat-stat-accent {
    4751     background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);
    47524750    border: 1px solid #e9d5ff;
    47534751}
     
    60696067}
    60706068
     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
    60716158/* No Matches */
    60726159.mxchat-rag-no-matches {
     
    62106297.mxchat-radio-label input[type="radio"] {
    62116298    margin: 0;
    6212     width: 18px;
    6213     height: 18px;
     6299    width: 16px;
     6300    height: 16px;
    62146301    accent-color: #2271b1;
    62156302}
  • mxchat-basic/trunk/css/chat-transcripts.css

    r3408040 r3430202  
    297297}
    298298
     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
    299373.mxchat-timestamp {
    300374    font-size: 12px;
  • mxchat-basic/trunk/css/knowledge-style.css

    r3406402 r3430202  
    30373037    margin-left: 10px;
    30383038}
     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  
    329329}
    330330
     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
    331439.similarity-score {
    332440    font-size: 16px;
  • mxchat-basic/trunk/includes/class-mxchat-admin.php

    r3428255 r3430202  
    109109        'post_type_visibility_list' => array(), // Array of post type slugs
    110110        'contextual_awareness_toggle' => 'off',
     111        'show_frontend_debugger' => 'on',
    111112        'close_button_color' => esc_html__('#fff', 'mxchat'),
    112113        'chatbot_bg_color' => esc_html__('#fff', 'mxchat'),
     
    18171818                ]
    18181819            );
    1819             $message_content = nl2br($message_content);
     1820            // Note: Don't use nl2br() here - format_transcript_message() handles paragraph formatting
    18201821
    18211822            // Check if this bot message has RAG context data
     
    21152116        }
    21162117
    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
    21182121        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            );
    21202128        } 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)";
    21222131        }
    21232132        $total_records = $wpdb->get_var($count_query);
    21242133        $total_pages = ceil($total_records / $per_page);
    21252134
    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        }
    21342200    }
    21352201
     
    26922758
    26932759                        <!-- 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                        ?>
    26942807                        <div class="mxchat-table-wrapper">
    26952808                            <table class="mxchat-records-table">
     
    27072820                                <tbody>
    27082821                                    <?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                                            ?>
    27102979                                            <tr id="prompt-<?php echo esc_attr($prompt->id); ?>"
    27112980                                                data-source="<?php echo esc_attr($data_source); ?>"
     
    27242993                                                        <div class="mxchat-content-preview">
    27252994                                                            <?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;
    27272997                                                            $preview_length = 150;
    27282998                                                            $content_preview = mb_strlen($content) > $preview_length
     
    27533023                                                            <?php if ($data_source === 'wordpress') : ?>
    27543024                                                                <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'); ?>
    27573027                                                                </textarea>
    27583028                                                            <?php endif; ?>
     
    28643134                                                </td>
    28653135
     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>
    28663216                                            </tr>
    28673217                                        <?php endforeach; ?>
     
    29843334                            </div>
    29853335                        </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
    29873395                        <!-- Role-Based Content Restrictions Card -->
    29883396                        <div class="mxchat-card" style="margin-top: 20px;">
     
    34583866    }
    34593867
     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
    34603879    // Process markdown headers (# Header)
    34613880    $text = preg_replace_callback('/^(#{1,6})\s+(.+)$/m', function($matches) {
    34623881        $level = strlen($matches[1]);
    3463         $content = esc_html($matches[2]);
     3882        $content = esc_html(trim($matches[2]));
    34643883        return "<h{$level}>{$content}</h{$level}>";
    34653884    }, $text);
     
    35153934    }, $text);
    35163935
    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    }
    35193958
    35203959    return $text;
     
    50215460    );
    50225461
     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
    50235471    // Similarity Threshold Slider
    50245472    add_settings_field(
     
    69737421}
    69747422
     7423public 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
    69757438public function mxchat_contextual_awareness_callback() {
    69767439    // Get value from options array, default to 'off'
     
    75868049            wp_localize_script('mxchat-test-streaming-js', 'mxchatTestStreamingAjax', [
    75878050                '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')
    75898053            ]);
    75908054            break;
     
    77968260    if (isset($input['contextual_awareness_toggle'])) {
    77978261    $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';
    77988266}
    77998267
  • mxchat-basic/trunk/includes/class-mxchat-integrator.php

    r3418641 r3430202  
    1414    private $current_valid_urls = [];
    1515    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 */
     22private 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}
    1644
    1745/**
     
    970998
    971999    //   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');
    9721002    $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'));
    9741004
    9751005    // ADDED: Store streaming state in class property for use in private methods
    9761006    $this->is_streaming = $is_streaming;
    9771007
    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
    9951010
    9961011    // Check if MX Chat Moderation is active
     
    14861501                    $response_data['testing_data'] = $testing_data;
    14871502                }
    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
    14981504                wp_send_json($response_data);
    14991505                wp_die();
    15001506            } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
    15011507                // Intent returned true and set fallbackResponse
    1502                
     1508
    15031509                // SAVE TO TRANSCRIPT FIRST
    15041510                if (!empty($this->fallbackResponse['text'])) {
     
    15081514                    $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
    15091515                }
    1510                
     1516
    15111517                $response_data = [
    15121518                    'text' => $this->fallbackResponse['text'] ?? '',
     
    15141520                    'session_id' => $session_id
    15151521                ];
    1516                
     1522
    15171523                if (isset($this->fallbackResponse['chat_mode'])) {
    15181524                    $response_data['chat_mode'] = $this->fallbackResponse['chat_mode'];
    15191525                }
    1520                
     1526
    15211527                if ($testing_data !== null) {
    15221528                    $response_data['testing_data'] = $testing_data;
    15231529                }
    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
    15341531                wp_send_json($response_data);
    15351532                wp_die();
     
    18011798
    18021799        // 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
    18031805        $response_data = [
    18041806            'text' => $response,
     
    40974099    // Calculate similarities and build results array
    40984100    $all_similarities = [];
    4099     $relevant_results = [];
    4100    
     4101    $url_groups = array(); // NEW: Group by source_url for chunk reassembly
     4102
    41014103    foreach ($embeddings as $embedding) {
    41024104        $database_embedding = $embedding->embedding_vector
    41034105            ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
    41044106            : null;
    4105            
     4107
    41064108        if (is_array($database_embedding) && is_array($user_embedding)) {
    41074109            $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
    4108            
     4110
    41094111            // Check role access
    41104112            $role_restriction = $embedding->role_restriction ?? 'public';
    41114113            $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
    4112            
     4114
    41134115            // Store ALL similarities for testing (top 10)
    41144116            $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;
    41174120            } else {
    41184121                $content_preview = strip_tags($embedding->article_content ?? '');
     
    41204123                $source_display = substr(trim($content_preview), 0, 50) . '...';
    41214124            }
    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
    41234132            $all_similarities[] = [
    41244133                'document_id' => $embedding->id,
     
    41274136                'above_threshold' => $similarity >= $similarity_threshold,
    41284137                '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 later
     4138                'content_preview' => substr(strip_tags($parsed_for_display['text'] ?? ''), 0, 100) . '...',
     4139                'used_for_context' => false,
    41314140                'role_restriction' => $role_restriction,
    41324141                '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
    41344146            ];
    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
    41454190        unset($database_embedding);
    41464191    }
     
    41504195        return $b['similarity'] <=> $a['similarity'];
    41514196    });
    4152    
    4153     // Sort relevant results by similarity (highest first)
    4154     usort($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'];
    41564201    });
    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 actually used for context
     4202
     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
    41624207    $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
    41674218    // Update the all_similarities array to mark which were actually used
    41684219    foreach ($all_similarities as &$similarity_item) {
    41694220        $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
    41704221    }
    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
    41734224    $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
    41744225    $this->last_similarity_analysis['total_checked'] = count($embeddings);
    4175    
     4226
    41764227    // Initialize final content
    41774228    $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'];
    42444249                }
    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++;
    42654276        }
    42664277    }
     
    42684279    // NEW: Store unique valid URLs for validation
    42694280    $this->current_valid_urls = array_unique($valid_urls);
    4270    
     4281
    42714282    // Add response guidelines
    4272     if (empty($top_results)) {
     4283    if (empty($top_urls)) {
    42734284        $content = "No reference information was found for this query.\n\n";
    42744285    } else {
     
    42864297}
    42874298
     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 */
     4305private 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
    42884342private function find_relevant_content_pinecone($user_embedding, $bot_id = 'default', $bot_config = null) {
    42894343    global $wpdb;
     
    43484402    $request_body = array(
    43494403        'vector' => $user_embedding,
    4350         'topK' => 20, // Request more to get good testing data
     4404        'topK' => 50, // Increased for chunked content grouping - need more candidates to find top 3 unique URLs
    43514405        'includeMetadata' => true,
    43524406        'includeValues' => true
     
    44334487    $matches_used = 0;
    44344488    $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
    44374493    foreach ($results['matches'] as $index => $match) {
    44384494        // Skip if similarity is below threshold
     
    44404496            continue;
    44414497        }
    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)) {
    44604592            $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
    44704601            preg_match_all(
    4471                 '#\bhttps?://[^\s<>"\']+#i', 
    4472                 $match['metadata']['text'],
     4602                '#\bhttps?://[^\s<>"\']+#i',
     4603                $full_text,
    44734604                $content_urls
    44744605            );
     
    44764607                $valid_urls = array_merge($valid_urls, $content_urls[0]);
    44774608            }
    4478            
    4479             $matches_used_for_context[] = $match['id'] ?? $index;
     4609
    44804610            $matches_used++;
    44814611        }
     
    45034633       
    45044634        $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
    45064646        $all_matches[] = [
    45074647            'document_id' => $match_id_for_display,
     
    45144654            'role_restriction' => $role_restriction,
    45154655            '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
    45174660        ];
    45184661    }
     
    45804723    // Cache individual role for 1 hour
    45814724    wp_cache_set($cache_key, $role_restriction, 'mxchat_vector_roles', 3600);
    4582    
     4725
    45834726    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 */
     4736private 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);
    45844841}
    45854842
     
    51585415        ]);
    51595416
     5417        // Setup streaming headers now that we know we're actually streaming
     5418        $this->setup_streaming_headers();
     5419
    51605420        $ch = curl_init();
    51615421        curl_setopt($ch, CURLOPT_URL, 'https://openrouter.ai/api/v1/chat/completions');
     
    53825642
    53835643        $body = json_encode($request_body);
     5644
     5645        // Setup streaming headers now that we know we're actually streaming
     5646        $this->setup_streaming_headers();
    53845647
    53855648        // Use cURL for streaming support
     
    56425905        }
    56435906
     5907        // Setup streaming headers now that we know we're actually streaming
     5908        $this->setup_streaming_headers();
     5909
    56445910        // Use cURL for streaming support
    56455911        $ch = curl_init();
     
    59166182            'stream' => true
    59176183        ]);
     6184
     6185        // Setup streaming headers now that we know we're actually streaming
     6186        $this->setup_streaming_headers();
    59186187
    59196188        // Use cURL for streaming support
     
    61416410            'stream' => true
    61426411        ]);
     6412
     6413        // Setup streaming headers now that we know we're actually streaming
     6414        $this->setup_streaming_headers();
    61436415
    61446416        // Use cURL for streaming support
     
    81858457    delete_transient("mxchat_include_pdf_in_context_{$session_id}");
    81868458    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
    81888464    //error_log("MxChat: Cleared all data for session: {$session_id}");
    81898465}
     
    85158791   
    85168792    // 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
    85198796   
    85208797    error_log("Final cleaned response: " . $cleaned_response);
  • mxchat-basic/trunk/includes/class-mxchat-public.php

    r3428255 r3430202  
    2424     */
    2525    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
    2933            // Get plugin URL for assets
    3034            $plugin_url = plugin_dir_url(dirname(__FILE__));
  • mxchat-basic/trunk/includes/class-mxchat-utils.php

    r3406402 r3430202  
    3838    // Remove only null bytes and other control characters, but preserve newlines (\n = \x0A) and carriage returns (\r = \x0D)
    3939    $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    }
    4047
    4148    // UPDATED: Generate the embedding using bot-specific configuration
     
    525532    }
    526533}
    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 */
     549private 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 */
     646private 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 */
     726private 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 */
     758public 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 */
     771private 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 */
     878private 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  
    3131    function getChatSession() {
    3232        var sessionId = getCookie('mxchat_session_id');
    33         //console.log("Session ID retrieved from cookie: ", sessionId);
    34    
     33
    3534        if (!sessionId) {
    3635            sessionId = generateSessionId();
    37             //console.log("Generated new session ID: ", sessionId);
    3836            setChatSession(sessionId);
    3937        }
    40    
    41         //console.log("Final session ID: ", sessionId);
     38
    4239        return sessionId;
    4340    }
     
    215212// CORE CHAT FUNCTIONALITY
    216213// ====================================
     214
     215// Helper functions to disable/enable chat input while waiting for response
     216function 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
     230function 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
    217245// Update your existing sendMessage function
    218246function sendMessage() {
    219247    var message = $('#chat-input').val();
    220    
     248
    221249    // 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
    226254    if (message) {
     255        // Disable input while waiting for response
     256        disableChatInput();
     257
    227258        appendMessage("user", message);
    228259        $('#chat-input').val('');
     
    256287        message = customMxChatFilter(message, "prompt");
    257288    }
    258    
     289
     290    // Disable input while waiting for response
     291    disableChatInput();
     292
    259293    var sessionId = getChatSession();
    260294
     
    370404        data: ajaxData,
    371405        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
    388407            if (response.chat_mode) {
    389                 //console.log("Updating chat mode immediately to:", response.chat_mode);
    390408                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);
    391414            }
    392415
     
    442465                }
    443466
    444                 // Log the error with code for debugging
    445                 //console.log("Response data:", response.data);
    446                 //console.error("API Error:", errorMessage, "Code:", errorCode);
    447 
    448467                // Format user-friendly error message
    449468                let displayMessage = errorMessage;
     
    524543                    }
    525544                } else {
    526                     // This should rarely happen now that errors are handled above
    527                     ////console.error("Unexpected response format:", response);
    528545                    replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.");
    529546                }
     
    540557        },
    541558        error: function(xhr, status, error) {
    542             //console.error("AJAX Error:", status, error);
    543             //console.log("Response Text:", xhr.responseText);
    544 
    545559            let errorMessage = "An unexpected error occurred.";
    546560
     
    548562            try {
    549563                const responseJson = JSON.parse(xhr.responseText);
    550                 //console.log("Parsed error response:", responseJson);
    551564
    552565                if (responseJson.data && responseJson.data.error_message) {
     
    579592    $('.mxchat-input-holder textarea').data('pending-message', message);
    580593
    581     //console.log("Using streaming for message:", message);
    582 
    583594    const currentModel = mxchatChat.model || 'gpt-4o';
    584595    if (!isStreamingSupported(currentModel)) {
    585         //console.log("Streaming not supported, falling back to regular call");
    586596        callMxChat(message, callback);
    587597        return;
     
    629639    })
    630640    .then(response => {
    631         //console.log("Streaming response received:", response);
    632        
    633641        // Store the response for potential fallback handling
    634642        const responseClone = response.clone();
    635        
     643
    636644        if (!response.ok) {
    637645            // Try to get error details from response
    638646            return responseClone.json().then(errorData => {
    639                 //console.log("Server returned error response:", errorData);
    640647                throw { isServerError: true, data: errorData };
    641648            }).catch(() => {
     
    647654    const contentType = response.headers.get('content-type');
    648655    if (contentType && contentType.includes('application/json')) {
    649         //console.log("Received JSON response instead of stream");
    650656        return responseClone.json().then(data => {
    651             //console.log("Processing JSON response:", data);
    652            
    653657            // IMMEDIATE CHAT MODE UPDATE for JSON response
    654658            if (data.chat_mode) {
    655                 //console.log("JSON Response: Updating chat mode to:", data.chat_mode);
    656659                updateChatModeIndicator(data.chat_mode);
    657660            }
    658    
     661
    659662            // Check for testing panel
    660663            if (window.mxchatTestPanelInstance && data.testing_data) {
    661                 //console.log('Testing data found in streaming JSON response:', data.testing_data);
    662664                window.mxchatTestPanelInstance.handleTestingData(data.testing_data);
    663665            }
    664            
     666
    665667            // Handle the JSON response directly
    666668            handleNonStreamResponse(data, callback);
     
    677679            reader.read().then(({ done, value }) => {
    678680                if (done) {
    679                     //console.log("Streaming completed, final content:", accumulatedContent);
    680                    
    681681                    // If streaming completed but no content was received, try to get response as fallback
    682682                    if (!streamingStarted || !accumulatedContent) {
    683                         //console.log("Stream completed with no content, checking for fallback data");
    684                        
    685683                        // Try to read the response as JSON
    686684                        responseClone.text().then(text => {
    687685                            try {
    688686                                const data = JSON.parse(text);
    689                                 if (data.text || data.message) {
     687                                if (data.text || data.message || data.html) {
    690688                                    handleNonStreamResponse(data, callback);
    691689                                } else {
     
    721719
    722720                        if (data === '[DONE]') {
    723                             //console.log("Received [DONE] signal");
    724                            
    725721                            if (!accumulatedContent) {
    726                                 //console.log("No content received before [DONE]");
    727722                                $('.bot-message.temporary-message').remove();
    728723                                callMxChat(message, callback);
    729724                                return;
    730725                            }
    731                            
     726
     727                            // Re-enable chat input after streaming completes
     728                            enableChatInput();
     729
    732730                            if (callback) {
    733731                                callback(accumulatedContent);
     
    738736                        try {
    739737                            const json = JSON.parse(data);
    740                            
     738
    741739                            // IMMEDIATE CHAT MODE UPDATE FOR STREAMING
    742740                            if (json.chat_mode) {
    743                                 //console.log("Stream: Updating chat mode immediately to:", json.chat_mode);
    744741                                updateChatModeIndicator(json.chat_mode);
    745742                            }
    746                            
     743
    747744                            // Handle testing data
    748745                            if (json.testing_data && !testingDataReceived) {
    749                                 //console.log('Testing data received in stream:', json.testing_data);
    750746                                if (window.mxchatTestPanelInstance) {
    751747                                    window.mxchatTestPanelInstance.handleTestingData(json.testing_data);
     
    758754                                accumulatedContent += json.content;
    759755                                updateStreamingMessage(accumulatedContent);
    760                             } 
     756                            }
    761757                            // 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) {
    764759                                handleNonStreamResponse(json, callback);
    765760                                return;
    766761                            }
    767                             // Handle errors - UPDATED to properly display API errors
     762                            // Handle errors
    768763                            else if (json.error) {
    769                                 //console.error("Streaming error:", json.error);
    770764
    771765                                // Get error message from various possible fields
    772766                                let errorMessage = json.error_message || json.message || json.text ||
    773767                                    (typeof json.error === 'string' ? json.error : 'An error occurred. Please try again.');
     768
     769                                // Re-enable chat input on error
     770                                enableChatInput();
    774771
    775772                                // Display the error directly in the chat
     
    782779                            }
    783780                        } catch (e) {
    784                             //console.error('Error parsing SSE data:', e, 'Data:', data);
     781                            // SSE data parsing error - silently continue
    785782                        }
    786783                    }
     
    789786                processStream();
    790787            }).catch(streamError => {
    791                 //console.error('Error reading stream:', streamError);
    792788                $('.bot-message.temporary-message').remove();
    793789                callMxChat(message, callback);
     
    798794    })
    799795        .catch(error => {
    800             //console.log('Streaming failed:', error);
    801            
    802796            // Check if we have server error data with chat mode
    803797            if (error && error.isServerError && error.data) {
    804                 //console.log('Using server error response data');
    805                
    806798                // Check for chat mode in error data
    807799                if (error.data.chat_mode) {
    808                     //console.log("Error Response: Updating chat mode to:", error.data.chat_mode);
    809800                    updateChatModeIndicator(error.data.chat_mode);
    810801                }
    811                
     802
    812803                handleNonStreamResponse(error.data, callback);
    813804            } else {
    814805                // 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');
    816806                $('.bot-message.temporary-message').remove();
    817807                callMxChat(message, callback);
     
    820810}
    821811
    822 // Helper function to handle non-streaming responses (updated to include immediate chat mode handling)
     812// Helper function to handle non-streaming responses
    823813function handleNonStreamResponse(data, callback) {
    824     //console.log("Handling non-stream response:", data);
    825 
    826814    // IMMEDIATE CHAT MODE UPDATE FOR NON-STREAMING RESPONSES
    827815    if (data.chat_mode) {
    828         //console.log("Non-Stream: Updating chat mode immediately to:", data.chat_mode);
    829816        updateChatModeIndicator(data.chat_mode);
    830817    }
     
    832819    // Also check in data property if response is wrapped
    833820    if (data.data && data.data.chat_mode) {
    834         //console.log("Non-Stream: Updating chat mode from data to:", data.data.chat_mode);
    835821        updateChatModeIndicator(data.data.chat_mode);
    836822    }
    837823
    838     // Remove temporary message
    839     $('.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
    840826
    841827    // SECURITY FIX: Check for errors FIRST
     
    931917    }
    932918
     919    // Ensure chat input is re-enabled (safety net for edge cases)
     920    enableChatInput();
     921
    933922    if (callback) {
    934923        callback(data.text || data.message || '');
    935924    }
    936925}
     926
    937927// Enhanced updateChatModeIndicator function for immediate DOM updates
    938928function updateChatModeIndicator(mode) {
    939     //console.log("updateChatModeIndicator called with mode:", mode);
    940929    const indicator = document.getElementById('chat-mode-indicator');
    941930    if (indicator) {
    942931        const oldText = indicator.textContent;
    943        
     932
    944933        if (mode === 'agent') {
    945934            indicator.textContent = 'Live Agent';
    946935            startPolling();
    947             //console.log("Set indicator to Live Agent");
    948936        } else {
    949937            // Everything else is AI mode
     
    951939            indicator.textContent = customAiText;
    952940            stopPolling();
    953             //console.log("Set indicator to AI Agent:", customAiText);
    954         }
    955        
     941        }
     942
    956943        // Force immediate DOM update and reflow
    957944        if (oldText !== indicator.textContent) {
     
    960947            indicator.offsetHeight; // Trigger reflow
    961948            indicator.style.display = '';
    962            
     949
    963950            // Double-check after a brief moment to ensure the change stuck
    964951            setTimeout(() => {
    965                 //console.log("Final indicator text:", indicator.textContent);
    966952                if (mode === 'agent' && indicator.textContent !== 'Live Agent') {
    967                     //console.warn("Mode indicator update failed, forcing update");
    968953                    indicator.textContent = 'Live Agent';
    969954                } else if (mode !== 'agent' && indicator.textContent === 'Live Agent') {
    970                     //console.warn("Mode indicator stuck on Live Agent, forcing AI update");
    971955                    const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
    972956                    indicator.textContent = customAiText;
     
    974958            }, 50);
    975959        }
    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 message
    986     $('.bot-message.temporary-message').remove();
    987    
    988     // Handle different response formats
    989     if (data.text || data.html || data.message) {
    990        
    991         // Apply response hooks
    992         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 too
    1000         handleChatModeUpdates(data, data.text || data.message);
    1001        
    1002         // Display the response
    1003         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 properties
    1015     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 || '');
    1028960    }
    1029961}
     
    1066998}
    1067999
    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();
    10711004});
    10721005
    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) {
    10751008    if (e.which == 13 && !e.shiftKey) {
    10761009        e.preventDefault();
    1077         sendMessage(); // Use the updated sendMessage function
     1010        disableChatInput();
     1011        sendMessage();
    10781012    }
    10791013});
     
    11411075        // Append HTML content if provided
    11421076        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            }
    11441083        }
    11451084
     
    11691108        }
    11701109    } catch (error) {
    1171         //console.error("Error rendering message:", error);
     1110        // Error rendering message - silently continue
    11721111    }
    11731112}
     
    12881227
    12891228    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();
    13191256    } else {
    13201257        appendMessage(sender, responseText, responseHtml, images);
     1258        // Re-enable chat input after response is displayed
     1259        enableChatInput();
    13211260    }
    13221261}
     
    16541593        // Force visibility
    16551594        $('#floating-chatbot-button').removeClass('hidden');
    1656         //console.log('Showing widget');
    1657     }
    1658    
     1595    }
     1596
    16591597    function hideChatWidget() {
    16601598        $('#floating-chatbot-button').css('display', 'none');
    16611599        $('#floating-chatbot-button').addClass('hidden');
    1662         //console.log('Hiding widget');
    16631600    }
    16641601   
     
    16911628   
    16921629    function createNotificationBadge() {
    1693         //console.log("Creating notification badge...");
    16941630        const chatButton = document.getElementById('floating-chatbot-button');
    1695         //console.log("Chat button found:", !!chatButton);
    1696        
     1631
    16971632        if (!chatButton) return;
    1698    
     1633
    16991634        // Remove any existing badge first
    17001635        const existingBadge = chatButton.querySelector('.chat-notification-badge');
    17011636        if (existingBadge) {
    1702             //console.log("Removing existing badge");
    17031637            existingBadge.remove();
    17041638        }
     
    17801714    // LIVE AGENT FUNCTIONALITY
    17811715    // ====================================
    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 mode
    1795             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 update
    1802         if (oldText !== indicator.textContent) {
    1803             // Force a reflow to ensure the change is visible immediately
    1804             indicator.offsetHeight;
    1805            
    1806             // Double-check after a brief moment
    1807             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 }
    18151716
    18161717    function startPolling() {
     
    18191720        // Start new polling interval
    18201721        pollingInterval = setInterval(checkForAgentMessages, 5000);
    1821         //console.log("Started agent message polling");
    1822     }
    1823    
     1722    }
     1723
    18241724    function stopPolling() {
    18251725        if (pollingInterval) {
    18261726            clearInterval(pollingInterval);
    18271727            pollingInterval = null;
    1828             //console.log("Stopped agent message polling");
    18291728        }
    18301729    }
     
    18651764        },
    18661765        error: function (xhr, status, error) {
    1867             //console.error("Polling error:", xhr, status, error);
     1766            // Polling error - silently continue
    18681767        }
    18691768    });
     
    18731772    // CHAT HISTORY & PERSISTENCE
    18741773    // ====================================
    1875 // Update the loadChatHistory function:
     1774
    18761775function loadChatHistory() {
    18771776    // Prevent duplicate loading
    18781777    if (chatHistoryLoaded) {
    1879        // console.log('Chat history already loaded, skipping...');
    18801778        return;
    18811779    }
     
    19991897            },
    20001898            error: function(xhr, status, error) {
    2001                 //console.error("AJAX error loading chat history:", status, error);
     1899                // Error loading chat history - silently continue
    20021900            }
    20031901        });
     
    20201918        const container = document.getElementById('active-pdf-container');
    20211919        const nameElement = document.getElementById('active-pdf-name');
    2022        
     1920
    20231921        if (!container || !nameElement) {
    2024             //console.error('PDF container elements not found');
    20251922            return;
    20261923        }
    2027    
     1924
    20281925        nameElement.textContent = filename;
    20291926        container.style.display = 'flex';
    20301927    }
    2031    
     1928
    20321929    function showActiveWord(filename) {
    20331930        const container = document.getElementById('active-word-container');
    20341931        const nameElement = document.getElementById('active-word-name');
    2035        
     1932
    20361933        if (!container || !nameElement) {
    2037             //console.error('Word document container elements not found');
    20381934            return;
    20391935        }
    2040    
     1936
    20411937        nameElement.textContent = filename;
    20421938        container.style.display = 'flex';
     
    20701966        })
    20711967        .catch(error => {
    2072             //console.error('Error removing PDF:', error);
     1968            // Error removing PDF - silently continue
    20731969        });
    20741970    }
    2075    
     1971
    20761972    function removeActiveWord() {
    20771973        const container = document.getElementById('active-word-container');
     
    21011997        })
    21021998        .catch(error => {
    2103             //console.error('Error removing Word document:', error);
     1999            // Error removing Word document - silently continue
    21042000        });
    21052001    }
    2106    
     2002
    21072003    // ====================================
    21082004    // CONSENT & COMPLIANCE (GDPR)
    21092005    // ====================================
    2110    
     2006
    21112007    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' ||
    21152010                                mxchatChat.complianz_toggle === 1;
    2116    
     2011
    21172012        if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
    21182013            // Initial check
    21192014            checkConsentAndShowChat();
    2120    
     2015
    21212016            // Listen for consent changes
    21222017            $(document).on('cmplz_status_change', function(event) {
    2123                 //console.log('Status change detected');
    21242018                checkConsentAndShowChat();
    21252019            });
     
    21402034        var consentStatus = cmplz_has_consent('marketing');
    21412035        var consentType = complianz.consenttype;
    2142        
    2143         //console.log('Checking consent:', {status: consentStatus,type: consentType});
    2144    
     2036
    21452037        let $widget = $('#floating-chatbot-button');
    21462038        let $chatbot = $('#floating-chatbot');
    21472039        let $preChat = $('#pre-chat-message');
    2148        
     2040
    21492041        if (consentStatus === true) {
    2150             //console.log('Consent granted - showing widget');
    21512042            $widget
    21522043                .removeClass('no-consent')
     
    21552046                .fadeTo(500, 1);
    21562047            $chatbot.removeClass('no-consent');
    2157            
     2048
    21582049            // Show pre-chat message if not dismissed
    21592050            checkPreChatDismissal();
    21602051        } else {
    2161             //console.log('No consent - hiding widget');
    21622052            $widget
    21632053                .addClass('no-consent')
     
    21682058                });
    21692059            $chatbot.addClass('no-consent');
    2170            
     2060
    21712061            // Hide pre-chat message when no consent
    21722062            $preChat.hide();
     
    21952085            },
    21962086            error: function() {
    2197                 //console.error('Failed to check pre-chat message dismissal status.');
     2087                // Error checking pre-chat dismissal - silently continue
    21982088            }
    21992089        });
    22002090    }
    2201    
     2091
    22022092    function handlePreChatDismissal() {
    22032093        $('#pre-chat-message').fadeOut(200);
     
    22132103            },
    22142104            error: function() {
    2215                 //console.error('Failed to dismiss pre-chat message.');
     2105                // Error dismissing pre-chat message - silently continue
    22162106            }
    22172107        });
     
    23232213   
    23242214        if (!sessionId) {
    2325             //console.error('No session ID found');
    23262215            alert('Error: No session ID found');
    23272216            return;
    23282217        }
    2329    
     2218
    23302219        if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
    2331             //console.error('mxchatChat not properly configured:', mxchatChat);
    23322220            alert('Error: Ajax configuration missing');
    23332221            return;
     
    23732261                activePdfFile = data.data.filename;
    23742262            } else {
    2375                 //console.error('Upload failed:', data.data);
    23762263                alert('Failed to upload PDF. Please try again.');
    23772264            }
    23782265        } catch (error) {
    2379             //console.error('Upload error:', error);
    23802266            alert('Error uploading file. Please try again.');
    23812267        } finally {
     
    23972283   
    23982284        if (!sessionId) {
    2399             //console.error('No session ID found');
    24002285            alert('Error: No session ID found');
    24012286            return;
    24022287        }
    2403    
     2288
    24042289        // Disable buttons and show loading state
    24052290        const uploadBtn = document.getElementById('word-upload-btn');
     
    24412326                activeWordFile = data.data.filename;
    24422327            } else {
    2443                 //console.error('Upload failed:', data.data);
    24442328                alert('Failed to upload Word document. Please try again.');
    24452329            }
    24462330        } catch (error) {
    2447             //console.error('Upload error:', error);
    24482331            alert('Error uploading file. Please try again.');
    24492332        } finally {
     
    25092392// Only run email collection setup if it's enabled
    25102393if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
    2511     //console.log('Email collection is enabled, setting up handlers...');
    2512    
    25132394    // Email collection form setup and handlers
    25142395    const emailForm = document.getElementById('email-collection-form');
     
    26662547        }
    26672548
    2668         // MAIN FORM SUBMIT HANDLER - This is the critical fix
    2669         //console.log('Setting up form submit handler...');
    2670        
     2549        // MAIN FORM SUBMIT HANDLER
    26712550        // Remove any existing event listeners first
    26722551        emailForm.removeEventListener('submit', handleFormSubmit);
    2673        
     2552
    26742553        // Add the form submit handler
    26752554        emailForm.addEventListener('submit', handleFormSubmit);
    2676        
     2555
    26772556        function handleFormSubmit(event) {
    2678             //console.log('Form submit handler triggered!');
    26792557            event.preventDefault();
    26802558            event.stopPropagation();
    2681            
     2559
    26822560            // Prevent double submission
    26832561            if (isSubmitting) {
    2684                 //console.log('Already submitting, ignoring...');
    26852562                return false;
    26862563            }
     
    26912568            const sessionId = getChatSession();
    26922569
    2693             //console.log('Form data:', { userEmail, userName, sessionId });
    2694 
    26952570            // Validate email before submission
    26962571            if (!userEmail) {
     
    27132588            clearEmailError();
    27142589            setSubmissionState(true);
    2715 
    2716             //console.log('Sending AJAX request...');
    27172590
    27182591            // Prepare form data with optional name
     
    27372610            })
    27382611            .then((response) => {
    2739                 //console.log('Response received:', response);
    27402612                if (!response.ok) {
    27412613                    throw new Error(`HTTP error! status: ${response.status}`);
     
    27442616            })
    27452617            .then((data) => {
    2746                 //console.log('Response data:', data);
    27472618                setSubmissionState(false);
    2748                
     2619
    27492620                if (data.success) {
    27502621                    // Show chat immediately
     
    27652636            })
    27662637            .catch((error) => {
    2767                 //console.error('Email submission error:', error);
    27682638                setSubmissionState(false);
    27692639                showEmailError('An error occurred. Please try again.');
     
    28372707        // Initial state check
    28382708        if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
    2839             //console.log('Using server-provided email state:', mxchatChat.initial_email_state);
    2840            
    28412709            const emailState = mxchatChat.initial_email_state;
    28422710            if (emailState.show_email_form) {
     
    28842752            })
    28852753            .catch((error) => {
    2886                 //console.warn('Email check failed, defaulting to email form:', error);
     2754                // Email check failed - default to email form
    28872755                showEmailForm();
    28882756            });
    28892757        }
    2890        
     2758
    28912759    } 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    }
    29002762}
    29012763
     
    29252787                },
    29262788                success: function() {
    2927                     //console.log('Pre-chat message dismissed for 24 hours.');
    2928 
    29292789                    // Ensure the message is hidden after dismissal
    29302790                    $('#pre-chat-message').hide();
    29312791                },
    29322792                error: function() {
    2933                     //console.error('Failed to dismiss pre-chat message.');
     2793                    // Error dismissing pre-chat message - silently continue
    29342794                }
    29352795            });
  • mxchat-basic/trunk/js/content-selector.js

    r3364278 r3430202  
    186186    function renderContentItems(items) {
    187187        let html = '';
    188        
     188
    189189        items.forEach(function(item) {
    190190            const isSelected = selectedItems.has(item.id);
    191191            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            }
    195199            const badgeClass = isProcessed ? 'mxchat-kb-processed-badge' : 'mxchat-kb-unprocessed-badge';
    196            
    197            
     200
     201
    198202            html += `
    199203                <div class="mxchat-kb-content-item ${isProcessed ? 'processed' : ''}" data-id="${item.id}">
     
    217221            `;
    218222        });
    219        
     223
    220224        $contentList.html(html);
    221225       
  • mxchat-basic/trunk/js/knowledge-processing.js

    r3394595 r3430202  
    11521152        }
    11531153    });
     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    });
    11541241});
  • mxchat-basic/trunk/js/mxchat-admin.js

    r3428255 r3430202  
    449449                    // Determine which AJAX action and nonce to use:
    450450                    var ajaxAction, nonce;
    451                     // *** UPDATED: Add Pinecone fields to prompts action ***
     451                    // *** UPDATED: Add Pinecone fields and chunking fields to prompts action ***
    452452                    if (name.indexOf('mxchat_prompts_options') !== -1 ||
    453                         name === 'mxchat_auto_sync_posts' || 
     453                        name === 'mxchat_auto_sync_posts' ||
    454454                        name === 'mxchat_auto_sync_pages' ||
    455455                        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
    457458                        ajaxAction = 'mxchat_save_prompts_setting';
    458459                        nonce = mxchatPromptsAdmin.prompts_setting_nonce;
  • mxchat-basic/trunk/js/mxchat-test-streaming.js

    r3340699 r3430202  
    11jQuery(document).ready(function($) {
     2    // Manual test button click
    23    $('#mxchat-test-streaming-btn').on('click', function() {
    34        const $button = $(this);
    45        const $result = $('#mxchat-test-streaming-result');
    5        
     6
    67        $button.prop('disabled', true).text('Testing...');
    78        $result.text('Testing streaming environment...');
    8        
     9
    910        // Test both backend capability AND frontend environment
    1011        testCompleteStreamingEnvironment()
     
    2021            })
    2122            .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');
    2324            });
    2425    });
    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
    2685    function testCompleteStreamingEnvironment() {
    2786        return new Promise((resolve, reject) => {
     
    53112            formData.append('session_id', 'streaming_test_' + Date.now());
    54113            formData.append('nonce', mxchatTestStreamingAjax.nonce);
     114            formData.append('force_streaming_test', '1'); // Force streaming mode for testing
    55115           
    56116            fetch(mxchatTestStreamingAjax.ajax_url, {
  • mxchat-basic/trunk/js/mxchat_transcripts.js

    r3418641 r3430202  
    323323    }
    324324
    325     // Render RAG context data in the modal
     325    // Render RAG context data in the modal - grouped by URL
    326326    function renderRagContext(data, $container) {
    327327        let html = '';
     
    343343        html += '</div>';
    344344
    345         // Top matches section
     345        // Top matches section - grouped by URL
    346346        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
    347392            html += '<div class="mxchat-rag-matches">';
    348393            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                }
    356408
    357409                html += '<div class="mxchat-rag-match-card ' + cardClass + '">';
    358410                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') + '">';
    362414                html += statusIcon + ' ' + statusLabel;
    363415                html += '</span>';
     
    365417
    366418                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);
    370422                    html += '</a>';
    371423                } else {
    372                     html += '📄 ' + escapeHtml(match.source_display || 'Content snippet');
     424                    html += '📄 ' + escapeHtml(group.url);
    373425                }
    374426                html += '</div>';
    375427
    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>';
    379432                    html += '</div>';
    380433                }
    381434
    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
    386457                    html += '</div>';
    387458                }
     
    408479
    409480        $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        });
    410496    }
    411497
  • mxchat-basic/trunk/js/test-panel.js

    r3389316 r3430202  
    395395    updateTopMatches(topMatches, threshold) {
    396396        const scoresEl = this.panel.querySelector('#similarity-scores');
    397        
     397
    398398        if (!topMatches || topMatches.length === 0) {
    399399            scoresEl.innerHTML = '<div class="no-data-message">No similarity data available</div>';
    400400            return;
    401401        }
    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
    403444        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>
    405447        </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 PHP
    410             const statusIcon = isAboveThreshold ? '✓' : '✗';
    411            
    412             // Determine the correct label based on actual usage
    413             let contextLabel;
    414             if (isUsedForContext) {
    415                 contextLabel = '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 used
    423             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
    425467            html += `
    426468                <div class="match-card ${cardClass}">
     
    428470                        <div class="match-title">
    429471                            <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}
    431474                        </div>
    432475                        <span class="context-label">${contextLabel}</span>
    433476                    </div>
    434477                    <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}`
    438481                        }
    439482                    </div>
     483                    ${expandToggle}
     484                    ${hasMultipleChunks ? this.renderChunkDetails(group.matchedChunks, groupIndex) : ''}
    440485                </div>
    441486            `;
    442487        });
    443        
     488
    444489        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;
    445528    }
    446529
  • mxchat-basic/trunk/mxchat-basic.php

    r3428255 r3430202  
    44 * Plugin URI: https://mxchat.ai/
    55 * Description: AI chatbot for WordPress with OpenAI, Claude, xAI, DeepSeek, live agent, PDF uploads, WooCommerce, and training on website data.
    6  * Version: 2.6.2
     6 * Version: 2.6.3
    77 * Author: MxChat
    88 * Author URI: https://mxchat.ai
     
    4949        'includes/class-mxchat-user.php',
    5050        'includes/class-mxchat-meta-box.php',
     51        'includes/class-mxchat-chunker.php',
    5152        'includes/pdf-parser/alt_autoload.php',
    5253        'includes/class-mxchat-word-handler.php',
  • mxchat-basic/trunk/readme.txt

    r3428255 r3430202  
    66Tested up to: 6.9
    77Requires PHP: 7.2
    8 Stable tag: 2.6.2
     8Stable tag: 2.6.3
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    1616[Documentation](https://mxchat.ai/documentation/)
    1717
    18 ## Why Choose MxChat AI Chatbot for Your WordPress Website?
     18## What makes MxChat the best free AI Chatbot plugin for WordPress?
    1919
    2020✅ **6 Major AI Providers in One Plugin**: OpenRouter, OpenAI GPT, Claude, Gemini, xAI Grok, and DeepSeek - switch between 100+ models instantly
     
    3939[Explore all product videos on YouTube](https://www.youtube.com/@MxChat/videos) 
    4040
    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.
    5258
    5359## Core Features That Set MxChat Apart
     
    191197== Changelog ==
    192198
     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
    193212= 2.6.2 - December 22, 2025 =
    194213- Fixed: Critical error when viewing Knowledge Database with large Pinecone datasets (10K+ products)
     
    653672== Upgrade Notice ==
    654673
    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 =
     675New 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.
    657676
    658677== License & Warranty ==
Note: See TracChangeset for help on using the changeset viewer.