Plugin Directory

Changeset 3417995


Ignore:
Timestamp:
12/12/2025 08:00:18 AM (2 months ago)
Author:
youamibot
Message:

v1.1.0 - Multilingual support: 100+ languages with automatic detection, cross-language search

Location:
bitbot
Files:
17 added
7 edited

Legend:

Unmodified
Added
Removed
  • bitbot/trunk/assets/js/admin.js

    r3417246 r3417995  
    1818        bindSubscriptionActions();
    1919        bindReactivateButton();
     20        bindReconnectButton();
     21    }
     22
     23    /**
     24     * Reconnect button functionality
     25     */
     26    function bindReconnectButton() {
     27        const $reconnectBtn = $('#bitbot-reconnect-btn');
     28        console.log('[BitBot] Binding reconnect button, found:', $reconnectBtn.length);
     29
     30        $reconnectBtn.on('click', function(e) {
     31            e.preventDefault();
     32            console.log('[BitBot] Reconnect clicked');
     33
     34            const $btn = $(this);
     35            const originalText = $btn.text();
     36
     37            $btn.text('Reconnecting...').prop('disabled', true);
     38
     39            $.ajax({
     40                url: config.ajaxUrl,
     41                method: 'POST',
     42                data: {
     43                    action: 'bitbot_reconnect',
     44                    nonce: config.nonce
     45                },
     46                success: function(response) {
     47                    if (response.success) {
     48                        alert(response.data.message || 'Site reconnected successfully!');
     49                        location.reload();
     50                    } else {
     51                        alert('Reconnect failed: ' + (response.data?.error || 'Unknown error'));
     52                    }
     53                },
     54                error: function(xhr, status, error) {
     55                    alert('Failed to reconnect: ' + error);
     56                },
     57                complete: function() {
     58                    $btn.text(originalText).prop('disabled', false);
     59                }
     60            });
     61        });
    2062    }
    2163
     
    3072            const originalText = $btn.text();
    3173
    32             $btn.text('Syncing...').addClass('syncing');
    33 
    34             $.ajax({
    35                 url: config.ajaxUrl,
    36                 method: 'POST',
     74            $btn.text('Syncing... please wait').addClass('syncing').prop('disabled', true);
     75
     76            // Show progress indicator
     77            const $status = $('<div id="sync-progress" style="margin-top: 10px; padding: 10px; background: #f0f0f1; border-radius: 4px;"><span class="spinner is-active" style="float: none; margin: 0 8px 0 0;"></span>Syncing content... This may take a few minutes for large sites.</div>');
     78            $btn.after($status);
     79
     80            $.ajax({
     81                url: config.ajaxUrl,
     82                method: 'POST',
     83                timeout: 600000, // 10 minute timeout (batches are 5 min each)
    3784                data: {
    3885                    action: 'bitbot_sync',
     
    4188                success: function(response) {
    4289                    if (response.success) {
    43                         alert('Sync started! It runs in background and may take 1-2 minutes. Refresh the page to see updated stats.');
     90                        const data = response.data;
     91                        const msg = data.message || `Sync complete! ${data.pagesSaved || 0} pages synced.`;
     92                        alert(msg);
     93                        location.reload();
    4494                    } else {
    4595                        alert('Sync failed: ' + (response.data?.error || 'Unknown error'));
    4696                    }
    4797                },
    48                 error: function() {
    49                     alert('Failed to connect to server');
     98                error: function(xhr, status, error) {
     99                    if (status === 'timeout') {
     100                        alert('Sync timed out. Please try again or check if you have a large number of posts.');
     101                    } else {
     102                        alert('Failed to connect to server: ' + error);
     103                    }
    50104                },
    51105                complete: function() {
    52                     $btn.text(originalText).removeClass('syncing');
     106                    $btn.text(originalText).removeClass('syncing').prop('disabled', false);
     107                    $('#sync-progress').remove();
    53108                }
    54109            });
     
    779834    }
    780835
     836    /**
     837     * Pages management functionality
     838     */
     839    let allPages = [];
     840
     841    function loadPages() {
     842        const $container = $('#bitbot-pages-container');
     843        const $list = $('#bitbot-pages-list');
     844        const $tbody = $('#bitbot-pages-tbody');
     845        const $btn = $('#bitbot-load-pages-btn');
     846
     847        $container.find('.bitbot-loading').remove();
     848        $container.prepend('<p class="bitbot-loading" style="color: #666;"><span class="spinner is-active" style="float: none; margin: 0 5px 0 0;"></span> Loading pages...</p>');
     849        $btn.hide();
     850
     851        $.ajax({
     852            url: config.ajaxUrl,
     853            method: 'POST',
     854            data: {
     855                action: 'bitbot_get_pages',
     856                nonce: config.nonce
     857            },
     858            success: function(response) {
     859                $container.find('.bitbot-loading').remove();
     860
     861                if (response.success) {
     862                    allPages = response.data.pages || [];
     863                    renderPagesTable();
     864                    $list.show();
     865                    $('#bitbot-pages-total').text('Total: ' + response.data.total + ' pages indexed');
     866                } else {
     867                    $container.append('<p class="error" style="color: #dc3232;">Failed to load: ' + (response.data?.error || 'Unknown error') + '</p>');
     868                    $btn.show();
     869                }
     870            },
     871            error: function() {
     872                $container.find('.bitbot-loading').remove();
     873                $container.append('<p class="error" style="color: #dc3232;">Failed to connect to server</p>');
     874                $btn.show();
     875            }
     876        });
     877    }
     878
     879    function renderPagesTable() {
     880        const $tbody = $('#bitbot-pages-tbody');
     881        $tbody.empty();
     882
     883        if (allPages.length === 0) {
     884            $tbody.append('<tr><td colspan="5" style="text-align: center; color: #666;">No pages indexed yet. Click "Sync Now" to crawl your site.</td></tr>');
     885            return;
     886        }
     887
     888        allPages.forEach(function(page) {
     889            const enabledChecked = page.enabled ? 'checked' : '';
     890            const urlShort = page.url.length > 50 ? page.url.substring(0, 50) + '...' : page.url;
     891            const title = page.title || '-';
     892            const summary = page.summary ? (page.summary.length > 80 ? page.summary.substring(0, 80) + '...' : page.summary) : '-';
     893            const disabledStyle = page.enabled ? '' : 'opacity: 0.5; background: #f9f9f9;';
     894            const disabledTextStyle = page.enabled ? '' : 'text-decoration: line-through; color: #999;';
     895            // Only show remove button for enabled pages
     896            const removeBtn = page.enabled
     897                ? `<button class="button button-small remove-page" data-id="${page.id}" style="color: #a00;" title="Disable this page">×</button>`
     898                : '';
     899
     900            $tbody.append(`
     901                <tr data-page-id="${page.id}" style="${disabledStyle}">
     902                    <td style="text-align: center;">
     903                        <input type="checkbox" class="toggle-page-enabled" data-id="${page.id}" ${enabledChecked} title="${page.enabled ? 'Click to disable' : 'Click to enable'}">
     904                    </td>
     905                    <td>
     906                        <a href="${escapeHtml(page.url)}" target="_blank" title="${escapeHtml(page.url)}" style="font-size: 12px; ${disabledTextStyle}">
     907                            ${escapeHtml(urlShort)}
     908                        </a>
     909                    </td>
     910                    <td style="font-size: 12px; ${disabledTextStyle}">${escapeHtml(title)}</td>
     911                    <td style="font-size: 11px; color: #666; ${disabledTextStyle}" title="${escapeHtml(page.summary || '')}">${escapeHtml(summary)}</td>
     912                    <td>
     913                        <button class="button button-small resync-page" data-id="${page.id}" data-url="${escapeHtml(page.url)}" title="${page.enabled ? 'Re-sync this page' : 'Re-sync and re-enable'}">↻</button>
     914                        ${removeBtn}
     915                    </td>
     916                </tr>
     917            `);
     918        });
     919    }
     920
     921    function bindPagesActions() {
     922        // Auto-load pages on page load if the container exists
     923        if ($('#bitbot-pages-container').length) {
     924            loadPages();
     925        }
     926
     927        // Manual refresh button
     928        $('#bitbot-load-pages-btn').on('click', function(e) {
     929            e.preventDefault();
     930            loadPages();
     931        });
     932
     933        // Add page form
     934        $('#bitbot-add-page-form').on('submit', function(e) {
     935            e.preventDefault();
     936
     937            const $form = $(this);
     938            const $input = $('#bitbot-new-url');
     939            const url = $input.val().trim();
     940
     941            if (!url) {
     942                alert('Please enter a URL');
     943                return;
     944            }
     945
     946            const $btn = $form.find('button[type="submit"]');
     947            const originalText = $btn.text();
     948            $btn.text('Adding...').prop('disabled', true);
     949
     950            $.ajax({
     951                url: config.ajaxUrl,
     952                method: 'POST',
     953                data: {
     954                    action: 'bitbot_add_page',
     955                    nonce: config.nonce,
     956                    url: url
     957                },
     958                success: function(response) {
     959                    if (response.success) {
     960                        $input.val('');
     961                        alert(response.data.message || 'Page added!');
     962                        loadPages(); // Refresh the list
     963                    } else {
     964                        alert('Failed: ' + (response.data?.error || 'Unknown error'));
     965                    }
     966                },
     967                error: function() {
     968                    alert('Failed to connect to server');
     969                },
     970                complete: function() {
     971                    $btn.text(originalText).prop('disabled', false);
     972                }
     973            });
     974        });
     975
     976        // Toggle page enabled/disabled
     977        $(document).on('change', '.toggle-page-enabled', function() {
     978            const $checkbox = $(this);
     979            const pageId = $checkbox.data('id');
     980            const enabled = $checkbox.is(':checked');
     981
     982            $.ajax({
     983                url: config.ajaxUrl,
     984                method: 'POST',
     985                data: {
     986                    action: 'bitbot_toggle_page',
     987                    nonce: config.nonce,
     988                    page_id: pageId,
     989                    enabled: enabled
     990                },
     991                success: function(response) {
     992                    if (!response.success) {
     993                        // Revert checkbox on error
     994                        $checkbox.prop('checked', !enabled);
     995                        alert('Failed: ' + (response.data?.error || 'Unknown error'));
     996                    }
     997                },
     998                error: function() {
     999                    $checkbox.prop('checked', !enabled);
     1000                    alert('Failed to connect to server');
     1001                }
     1002            });
     1003        });
     1004
     1005        // Resync page
     1006        $(document).on('click', '.resync-page', function(e) {
     1007            e.preventDefault();
     1008
     1009            const $btn = $(this);
     1010            const pageUrl = $btn.data('url');
     1011
     1012            $btn.text('...').prop('disabled', true);
     1013
     1014            $.ajax({
     1015                url: config.ajaxUrl,
     1016                method: 'POST',
     1017                data: {
     1018                    action: 'bitbot_resync_page',
     1019                    nonce: config.nonce,
     1020                    page_url: pageUrl
     1021                },
     1022                success: function(response) {
     1023                    if (response.success) {
     1024                        alert(response.data.message || 'Re-syncing page...');
     1025                    } else {
     1026                        alert('Failed: ' + (response.data?.error || 'Unknown error'));
     1027                    }
     1028                },
     1029                error: function() {
     1030                    alert('Failed to connect to server');
     1031                },
     1032                complete: function() {
     1033                    $btn.text('↻').prop('disabled', false);
     1034                }
     1035            });
     1036        });
     1037
     1038        // Remove page
     1039        $(document).on('click', '.remove-page', function(e) {
     1040            e.preventDefault();
     1041
     1042            if (!confirm('Remove this page from the index? The chatbot will no longer reference it.')) {
     1043                return;
     1044            }
     1045
     1046            const $btn = $(this);
     1047            const pageId = $btn.data('id');
     1048            const $row = $btn.closest('tr');
     1049
     1050            $btn.text('...').prop('disabled', true);
     1051
     1052            $.ajax({
     1053                url: config.ajaxUrl,
     1054                method: 'POST',
     1055                data: {
     1056                    action: 'bitbot_remove_page',
     1057                    nonce: config.nonce,
     1058                    page_id: pageId
     1059                },
     1060                success: function(response) {
     1061                    if (response.success) {
     1062                        $row.fadeOut(300, function() {
     1063                            $(this).remove();
     1064                            // Update total count
     1065                            allPages = allPages.filter(p => p.id !== pageId);
     1066                            $('#bitbot-pages-total').text('Total: ' + allPages.length + ' pages indexed');
     1067                        });
     1068                    } else {
     1069                        alert('Failed: ' + (response.data?.error || 'Unknown error'));
     1070                        $btn.text('×').prop('disabled', false);
     1071                    }
     1072                },
     1073                error: function() {
     1074                    alert('Failed to connect to server');
     1075                    $btn.text('×').prop('disabled', false);
     1076                }
     1077            });
     1078        });
     1079    }
     1080
    7811081    // Initialize when ready
    7821082    $(document).ready(function() {
    7831083        init();
    7841084        bindConversationsActions();
     1085        bindPagesActions();
    7851086    });
    7861087
  • bitbot/trunk/assets/js/widget.js

    r3417246 r3417995  
    142142                    message: message,
    143143                    history: chatHistory.slice(-10), // Send last 10 messages for context
    144                     sessionId: getSessionId()
     144                    sessionId: getSessionId(),
     145                    siteLanguage: config.languageCode || 'en'  // Pass site language
    145146                })
    146147            });
  • bitbot/trunk/bitbot.php

    r3417246 r3417995  
    1111 * Plugin Name:       BitBot
    1212 * Plugin URI:        https://youami.ai
    13  * Description:       AI-powered chatbot that helps visitors navigate your website using your own content, plus AI content generation.
    14  * Version:           1.0.0
     13 * Description:       AI-powered multilingual chatbot that helps visitors navigate your website in any language, plus AI content generation.
     14 * Version:           1.1.0
    1515 * Requires at least: 5.8
    1616 * Requires PHP:      7.4
     
    5050define( 'BITBOT_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
    5151define( 'BITBOT_API_URL', 'https://youami.ai/api' );
     52// define( 'BITBOT_API_URL', 'https://dev.youami.ai/api' );
    5253
    5354// Include required files
     
    161162    private function ensure_api_key() {
    162163        if ( ! get_option( 'bitbot_api_key' ) ) {
    163             $api_key = wp_generate_uuid4();
     164            $api_key = 'bb_' . str_replace( '-', '', wp_generate_uuid4() );
    164165            update_option( 'bitbot_api_key', $api_key );
    165166        }
     
    196197        // Generate unique API key for this site
    197198        if ( ! get_option( 'bitbot_api_key' ) ) {
    198             $api_key = wp_generate_uuid4();
     199            $api_key = 'bb_' . str_replace( '-', '', wp_generate_uuid4() );
    199200            update_option( 'bitbot_api_key', $api_key );
    200201        }
     
    257258        );
    258259
     260        // Get site language code (e.g., 'en', 'hi', 'es')
     261        $locale = get_locale();
     262        $language_code = substr( $locale, 0, 2 ); // Extract first 2 chars (e.g., 'en' from 'en_US')
     263
    259264        wp_localize_script( 'bitbot-widget', 'bitbotConfig', array(
    260265            'restUrl'        => esc_url_raw( rest_url( 'bitbot/v1' ) ),
     
    263268            'primaryColor'   => sanitize_hex_color( get_option( 'bitbot_primary_color', '#0073aa' ) ),
    264269            'welcomeMessage' => wp_kses_post( get_option( 'bitbot_welcome_message', __( 'Hi! How can I help you?', 'bitbot' ) ) ),
     270            'languageCode'   => sanitize_text_field( $language_code ),
    265271        ) );
    266272    }
  • bitbot/trunk/includes/class-admin.php

    r3417246 r3417995  
    4848        add_action('wp_ajax_bitbot_get_conversations', array($this, 'ajax_get_conversations'));
    4949        add_action('wp_ajax_bitbot_get_conversation_messages', array($this, 'ajax_get_conversation_messages'));
     50
     51        // Page management AJAX handlers
     52        add_action('wp_ajax_bitbot_get_pages', array($this, 'ajax_get_pages'));
     53        add_action('wp_ajax_bitbot_add_page', array($this, 'ajax_add_page'));
     54        add_action('wp_ajax_bitbot_remove_page', array($this, 'ajax_remove_page'));
     55        add_action('wp_ajax_bitbot_toggle_page', array($this, 'ajax_toggle_page'));
     56        add_action('wp_ajax_bitbot_resync_page', array($this, 'ajax_resync_page'));
     57
     58        // Reconnect handler
     59        add_action('wp_ajax_bitbot_reconnect', array($this, 'ajax_reconnect'));
    5060    }
    5161
     
    168178     */
    169179    public function render_dashboard_page() {
    170         // Handle registration if form submitted
    171         if (isset($_POST['bitbot_register']) && check_admin_referer('bitbot_register_nonce')) {
    172             $register_result = $this->api_client->register_site();
    173             if ($register_result['success']) {
    174                 echo '<div class="notice notice-success"><p>' . esc_html__('Site registered successfully!', 'bitbot') . '</p></div>';
    175             } else {
    176                 echo '<div class="notice notice-error"><p>' . esc_html($register_result['error']) . '</p></div>';
     180        $reconnected = false;
     181
     182        // Check if API key is valid by testing subscription endpoint
     183        $subscription = $this->api_client->get_subscription();
     184
     185        // Auto-reconnect if API key is invalid (just re-register with existing key)
     186        if ( ! $subscription['success'] && strpos( $subscription['error'] ?? '', 'Invalid API key' ) !== false ) {
     187            $reconnect_result = $this->api_client->register_site();
     188            if ( $reconnect_result['success'] ) {
     189                $reconnected = true;
     190                // Re-fetch subscription with updated key in DB
     191                $subscription = $this->api_client->get_subscription();
    177192            }
    178193        }
     
    180195        $sync_status = $this->api_client->get_sync_status();
    181196        $usage = $this->api_client->get_usage();
    182         $subscription = $this->api_client->get_subscription();
    183197        $api_key = get_option('bitbot_api_key', '');
    184 
    185         // Check if site needs registration
    186         $needs_registration = !$subscription['success'] && strpos($subscription['error'] ?? '', 'Invalid API key') !== false;
    187198        ?>
    188199        <div class="wrap">
    189200            <h1><?php esc_html_e('BitBot Dashboard', 'bitbot'); ?></h1>
    190201
    191             <!-- Always show API key for debugging -->
    192202            <p style="color: #666; font-size: 12px;">
    193203                <?php esc_html_e('API Key:', 'bitbot'); ?> <code><?php echo esc_html($api_key ?: 'NOT SET'); ?></code>
     204                <button id="bitbot-reconnect-btn" class="button button-small" style="margin-left: 10px;">
     205                    <?php esc_html_e('Reconnect', 'bitbot'); ?>
     206                </button>
    194207            </p>
    195208
    196             <?php if ($needs_registration): ?>
    197             <div class="notice notice-warning">
    198                 <p><strong><?php esc_html_e('Site not registered!', 'bitbot'); ?></strong> <?php esc_html_e('Click the button below to register your site with BitBot.', 'bitbot'); ?></p>
    199                 <form method="post" style="margin-bottom: 10px;">
    200                     <?php wp_nonce_field('bitbot_register_nonce'); ?>
    201                     <input type="hidden" name="bitbot_register" value="1">
    202                     <button type="submit" class="button button-primary"><?php esc_html_e('Register Site Now', 'bitbot'); ?></button>
    203                 </form>
    204                 <p><small><?php esc_html_e('Your API Key:', 'bitbot'); ?> <code><?php echo esc_html($api_key); ?></code></small></p>
     209            <?php if ($reconnected): ?>
     210            <div class="notice notice-success is-dismissible">
     211                <p><?php esc_html_e('Site reconnected successfully!', 'bitbot'); ?></p>
    205212            </div>
    206213            <?php endif; ?>
     
    355362                        <p class="error"><?php echo esc_html($subscription['error'] ?? 'Unable to fetch subscription'); ?></p>
    356363                    <?php endif; ?>
     364                </div>
     365
     366                <!-- Indexed Pages Card -->
     367                <div class="bitbot-card bitbot-card-full">
     368                    <h2><?php esc_html_e('Indexed Pages', 'bitbot'); ?></h2>
     369                    <p class="description" style="margin-bottom: 15px;">
     370                        <?php esc_html_e('Manage which pages are indexed and available for the chatbot. You can add, remove, or toggle pages.', 'bitbot'); ?>
     371                    </p>
     372
     373                    <!-- Add URL Form -->
     374                    <div style="margin-bottom: 20px; padding: 15px; background: #f9f9f9; border-radius: 4px;">
     375                        <form id="bitbot-add-page-form" style="display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap;">
     376                            <div>
     377                                <label for="bitbot-new-url" style="display: block; margin-bottom: 5px; font-weight: 500;">
     378                                    <?php esc_html_e('Add URL to Index', 'bitbot'); ?>
     379                                </label>
     380                                <input type="url" id="bitbot-new-url" placeholder="https://yoursite.com/page" style="width: 350px;" class="regular-text">
     381                            </div>
     382                            <button type="submit" class="button button-secondary">
     383                                <?php esc_html_e('Add Page', 'bitbot'); ?>
     384                            </button>
     385                        </form>
     386                    </div>
     387
     388                    <div id="bitbot-pages-container">
     389                        <button id="bitbot-load-pages-btn" class="button button-secondary">
     390                            <?php esc_html_e('Load Pages', 'bitbot'); ?>
     391                        </button>
     392                        <div id="bitbot-pages-list" style="display: none; margin-top: 15px;">
     393                            <div style="max-height: 350px; overflow-y: auto; border: 1px solid #c3c4c7; border-radius: 4px;">
     394                                <table class="wp-list-table widefat fixed striped bitbot-pages-table" style="margin: 0; border: none;">
     395                                    <thead style="position: sticky; top: 0; background: #f6f7f7; z-index: 1;">
     396                                        <tr>
     397                                            <th style="width: 5%;"><?php esc_html_e('Enabled', 'bitbot'); ?></th>
     398                                            <th style="width: 35%;"><?php esc_html_e('URL', 'bitbot'); ?></th>
     399                                            <th style="width: 20%;"><?php esc_html_e('Title', 'bitbot'); ?></th>
     400                                            <th style="width: 25%;"><?php esc_html_e('Summary', 'bitbot'); ?></th>
     401                                            <th style="width: 15%;"><?php esc_html_e('Actions', 'bitbot'); ?></th>
     402                                        </tr>
     403                                    </thead>
     404                                    <tbody id="bitbot-pages-tbody">
     405                                    </tbody>
     406                                </table>
     407                            </div>
     408                            <p id="bitbot-pages-total" style="margin-top: 10px; color: #666;"></p>
     409                        </div>
     410                    </div>
    357411                </div>
    358412
     
    930984        }
    931985    }
     986
     987    /**
     988     * AJAX handler for get pages
     989     */
     990    public function ajax_get_pages() {
     991        check_ajax_referer('bitbot_admin_nonce', 'nonce');
     992
     993        if (!current_user_can('manage_options')) {
     994            wp_send_json_error(array('error' => 'Permission denied'));
     995        }
     996
     997        $result = $this->api_client->get_pages();
     998
     999        if ($result['success']) {
     1000            wp_send_json_success($result['data']);
     1001        } else {
     1002            wp_send_json_error(array('error' => $result['error']));
     1003        }
     1004    }
     1005
     1006    /**
     1007     * AJAX handler for add page
     1008     */
     1009    public function ajax_add_page() {
     1010        check_ajax_referer('bitbot_admin_nonce', 'nonce');
     1011
     1012        if (!current_user_can('manage_options')) {
     1013            wp_send_json_error(array('error' => 'Permission denied'));
     1014        }
     1015
     1016        $url = isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : '';
     1017
     1018        if (empty($url)) {
     1019            wp_send_json_error(array('error' => 'URL is required'));
     1020        }
     1021
     1022        $result = $this->api_client->add_page($url);
     1023
     1024        if ($result['success']) {
     1025            wp_send_json_success($result['data']);
     1026        } else {
     1027            wp_send_json_error(array('error' => $result['error']));
     1028        }
     1029    }
     1030
     1031    /**
     1032     * AJAX handler for remove page
     1033     */
     1034    public function ajax_remove_page() {
     1035        check_ajax_referer('bitbot_admin_nonce', 'nonce');
     1036
     1037        if (!current_user_can('manage_options')) {
     1038            wp_send_json_error(array('error' => 'Permission denied'));
     1039        }
     1040
     1041        $page_id = isset( $_POST['page_id'] ) ? sanitize_text_field( wp_unslash( $_POST['page_id'] ) ) : '';
     1042
     1043        if (empty($page_id)) {
     1044            wp_send_json_error(array('error' => 'Page ID is required'));
     1045        }
     1046
     1047        $result = $this->api_client->remove_page($page_id);
     1048
     1049        if ($result['success']) {
     1050            wp_send_json_success($result['data']);
     1051        } else {
     1052            wp_send_json_error(array('error' => $result['error']));
     1053        }
     1054    }
     1055
     1056    /**
     1057     * AJAX handler for toggle page
     1058     */
     1059    public function ajax_toggle_page() {
     1060        check_ajax_referer('bitbot_admin_nonce', 'nonce');
     1061
     1062        if (!current_user_can('manage_options')) {
     1063            wp_send_json_error(array('error' => 'Permission denied'));
     1064        }
     1065
     1066        $page_id = isset( $_POST['page_id'] ) ? sanitize_text_field( wp_unslash( $_POST['page_id'] ) ) : '';
     1067        $enabled = isset( $_POST['enabled'] ) ? rest_sanitize_boolean( wp_unslash( $_POST['enabled'] ) ) : true;
     1068
     1069        if (empty($page_id)) {
     1070            wp_send_json_error(array('error' => 'Page ID is required'));
     1071        }
     1072
     1073        $result = $this->api_client->toggle_page($page_id, $enabled);
     1074
     1075        if ($result['success']) {
     1076            wp_send_json_success($result['data']);
     1077        } else {
     1078            wp_send_json_error(array('error' => $result['error']));
     1079        }
     1080    }
     1081
     1082    /**
     1083     * AJAX handler for resync page
     1084     */
     1085    public function ajax_resync_page() {
     1086        check_ajax_referer('bitbot_admin_nonce', 'nonce');
     1087
     1088        if (!current_user_can('manage_options')) {
     1089            wp_send_json_error(array('error' => 'Permission denied'));
     1090        }
     1091
     1092        $page_url = isset( $_POST['page_url'] ) ? esc_url_raw( wp_unslash( $_POST['page_url'] ) ) : '';
     1093
     1094        if (empty($page_url)) {
     1095            wp_send_json_error(array('error' => 'Page URL is required'));
     1096        }
     1097
     1098        $result = $this->api_client->resync_page($page_url);
     1099
     1100        if ($result['success']) {
     1101            wp_send_json_success($result['data']);
     1102        } else {
     1103            wp_send_json_error(array('error' => $result['error']));
     1104        }
     1105    }
     1106
     1107    /**
     1108     * AJAX handler for manual reconnect
     1109     */
     1110    public function ajax_reconnect() {
     1111        check_ajax_referer('bitbot_admin_nonce', 'nonce');
     1112
     1113        if (!current_user_can('manage_options')) {
     1114            wp_send_json_error(array('error' => 'Permission denied'));
     1115        }
     1116
     1117        // Regenerate API key with new bb_ format and reconnect
     1118        $result = $this->api_client->regenerate_and_reconnect();
     1119
     1120        if ($result['success']) {
     1121            wp_send_json_success($result['data']);
     1122        } else {
     1123            wp_send_json_error(array('error' => $result['error']));
     1124        }
     1125    }
    9321126}
  • bitbot/trunk/includes/class-api-client.php

    r3417246 r3417995  
    5454     * @param string $request_tag The RPC request tag.
    5555     * @param array  $payload     Request payload data.
     56     * @param int    $timeout     Request timeout in seconds (default 60, max 900 for sync).
    5657     * @return array Response with 'success' and 'data' or 'error' keys.
    5758     */
    58     private function rpc( $request_tag, $payload = array() ) {
     59    private function rpc( $request_tag, $payload = array(), $timeout = 60 ) {
    5960        // Add API key to all payloads
    6061        $payload['apiKey'] = $this->api_key;
     
    6869        $args = array(
    6970            'method'  => 'POST',
    70             'timeout' => 60,
     71            'timeout' => $timeout,
    7172            'headers' => array(
    7273                'Content-Type' => 'application/json',
     
    158159
    159160    /**
    160      * Trigger content sync
    161      *
    162      * @since 1.0.0
    163      * @return array API response.
     161     * Regenerate API key and re-register site
     162     *
     163     * @since 1.0.0
     164     * @return array Result with success/error.
     165     */
     166    public function regenerate_and_reconnect() {
     167        // Generate new API key
     168        $new_api_key = 'bb_' . str_replace( '-', '', wp_generate_uuid4() );
     169        update_option( 'bitbot_api_key', $new_api_key );
     170
     171        // Update this instance's key so RPC calls use the new key
     172        $this->api_key = $new_api_key;
     173
     174        // Re-register site with new key
     175        return $this->register_site();
     176    }
     177
     178    /**
     179     * Gather all published content from WordPress
     180     *
     181     * @since 1.0.0
     182     * @return array Array of pages with url, title, and content.
     183     */
     184    private function gather_content() {
     185        $pages = array();
     186
     187        // Get all published posts and pages
     188        $post_types = array( 'post', 'page' );
     189        $args = array(
     190            'post_type'      => $post_types,
     191            'post_status'    => 'publish',
     192            'posts_per_page' => -1,
     193            'orderby'        => 'modified',
     194            'order'          => 'DESC',
     195        );
     196
     197        $query = new WP_Query( $args );
     198
     199        if ( $query->have_posts() ) {
     200            while ( $query->have_posts() ) {
     201                $query->the_post();
     202
     203                // Get the full rendered content (with shortcodes processed)
     204                $content = apply_filters( 'the_content', get_the_content() );
     205
     206                $pages[] = array(
     207                    'url'     => get_permalink(),
     208                    'title'   => get_the_title(),
     209                    'content' => $content,
     210                );
     211            }
     212            wp_reset_postdata();
     213        }
     214
     215        return $pages;
     216    }
     217
     218    /**
     219     * Trigger content sync by gathering local content and sending to API
     220     *
     221     * @since 1.0.0
     222     * @return array API response with sync results.
    164223     */
    165224    public function trigger_sync() {
    166         return $this->rpc( 'SyncSite' );
    167     }
    168 
    169     /**
    170      * Sync a single URL
    171      *
    172      * @since 1.0.0
    173      * @param string $url URL to sync.
    174      * @return array API response.
    175      */
    176     public function sync_single_url( $url ) {
    177         return $this->rpc( 'SyncSingleUrl', array(
    178             'url' => esc_url_raw( $url ),
    179         ) );
     225        // Gather all content locally
     226        $all_pages = $this->gather_content();
     227
     228        if ( empty( $all_pages ) ) {
     229            return array(
     230                'success' => true,
     231                'data'    => array(
     232                    'message' => 'No published content found to sync.',
     233                    'pagesSaved' => 0,
     234                ),
     235            );
     236        }
     237
     238        // Batch pages to avoid hitting payload limits (~50 pages per batch)
     239        $batch_size    = 50;
     240        $batches       = array_chunk( $all_pages, $batch_size );
     241        $total_batches = count( $batches );
     242
     243        // Collect all URLs for cleanup of deleted pages
     244        $all_urls = array_map( function( $page ) {
     245            return $page['url'];
     246        }, $all_pages );
     247
     248        $total_saved   = 0;
     249        $total_skipped = 0;
     250        $total_deleted = 0;
     251        $chunks_embedded = 0;
     252
     253        // Send each batch to the API
     254        foreach ( $batches as $index => $batch ) {
     255            $payload = array(
     256                'pages'        => $batch,
     257                'batchIndex'   => $index,
     258                'totalBatches' => $total_batches,
     259            );
     260
     261            // Include all URLs in the final batch for cleanup of deleted pages
     262            if ( $index === $total_batches - 1 ) {
     263                $payload['allUrls'] = $all_urls;
     264            }
     265
     266            $result = $this->rpc( 'SyncContent', $payload, 300 ); // 5 min timeout per batch
     267
     268            if ( ! $result['success'] ) {
     269                return $result;
     270            }
     271
     272            $total_saved   += $result['data']['pagesSaved'] ?? 0;
     273            $total_skipped += $result['data']['pagesSkipped'] ?? 0;
     274            $total_deleted += $result['data']['pagesDeleted'] ?? 0;
     275
     276            if ( isset( $result['data']['chunksEmbedded'] ) ) {
     277                $chunks_embedded = $result['data']['chunksEmbedded'];
     278            }
     279        }
     280
     281        // Build message
     282        $message_parts = array();
     283        if ( $total_saved > 0 ) {
     284            $message_parts[] = sprintf( '%d pages synced', $total_saved );
     285        }
     286        if ( $total_skipped > 0 ) {
     287            $message_parts[] = sprintf( '%d unchanged', $total_skipped );
     288        }
     289        if ( $total_deleted > 0 ) {
     290            $message_parts[] = sprintf( '%d removed', $total_deleted );
     291        }
     292        return array(
     293            'success' => true,
     294            'data'    => array(
     295                'status'          => 'completed',
     296                'pagesSaved'      => $total_saved,
     297                'pagesSkipped'    => $total_skipped,
     298                'pagesDeleted'    => $total_deleted,
     299                'chunksEmbedded'  => $chunks_embedded,
     300                'message'         => 'Sync complete! ' . implode( ', ', $message_parts ) . '.',
     301            ),
     302        );
    180303    }
    181304
     
    200323     * @return array API response.
    201324     */
    202     public function chat( $message, $history = array(), $session_id = '', $visitor_ip = '' ) {
     325    public function chat( $message, $history = array(), $session_id = '', $visitor_ip = '', $site_language = 'en' ) {
    203326        return $this->rpc( 'Chat', array(
    204             'message'   => sanitize_text_field( $message ),
    205             'history'   => array_map( function( $msg ) {
     327            'message'      => sanitize_text_field( $message ),
     328            'history'      => array_map( function( $msg ) {
    206329                return array(
    207330                    'role'    => sanitize_text_field( $msg['role'] ),
     
    209332                );
    210333            }, $history ),
    211             'sessionId' => sanitize_text_field( $session_id ),
    212             'visitorIp' => sanitize_text_field( $visitor_ip ),
     334            'sessionId'    => sanitize_text_field( $session_id ),
     335            'visitorIp'    => sanitize_text_field( $visitor_ip ),
     336            'siteLanguage' => sanitize_text_field( $site_language ),
    213337        ) );
    214338    }
     
    420544            'conversationId' => sanitize_text_field( $conversation_id ),
    421545        ) );
     546    }
     547
     548    /**
     549     * Get indexed pages
     550     *
     551     * @since 1.0.0
     552     * @return array API response with pages list.
     553     */
     554    public function get_pages() {
     555        return $this->rpc( 'GetPages' );
     556    }
     557
     558    /**
     559     * Add a page to index
     560     *
     561     * @since 1.0.0
     562     * @param string $url URL to add.
     563     * @return array API response.
     564     */
     565    public function add_page( $url ) {
     566        return $this->rpc( 'AddPage', array(
     567            'url' => esc_url_raw( $url ),
     568        ) );
     569    }
     570
     571    /**
     572     * Remove a page from index
     573     *
     574     * @since 1.0.0
     575     * @param string $page_id Page ID.
     576     * @return array API response.
     577     */
     578    public function remove_page( $page_id ) {
     579        return $this->rpc( 'RemovePage', array(
     580            'pageId' => sanitize_text_field( $page_id ),
     581        ) );
     582    }
     583
     584    /**
     585     * Enable/disable a page for chat
     586     *
     587     * @since 1.0.0
     588     * @param string $page_id Page ID.
     589     * @param bool   $enabled Whether to enable or disable.
     590     * @return array API response.
     591     */
     592    public function toggle_page( $page_id, $enabled ) {
     593        return $this->rpc( 'TogglePage', array(
     594            'pageId'  => sanitize_text_field( $page_id ),
     595            'enabled' => (bool) $enabled,
     596        ) );
     597    }
     598
     599    /**
     600     * Resync a specific page by gathering its content from WordPress
     601     *
     602     * @since 1.0.0
     603     * @param string $page_url URL of the page to resync.
     604     * @return array API response.
     605     */
     606    public function resync_page( $page_url ) {
     607        // Find the WordPress post by URL
     608        $post_id = url_to_postid( $page_url );
     609
     610        if ( ! $post_id ) {
     611            return array(
     612                'success' => false,
     613                'error'   => __( 'Could not find WordPress post for this URL', 'bitbot' ),
     614            );
     615        }
     616
     617        $post = get_post( $post_id );
     618
     619        if ( ! $post || $post->post_status !== 'publish' ) {
     620            return array(
     621                'success' => false,
     622                'error'   => __( 'Post not found or not published', 'bitbot' ),
     623            );
     624        }
     625
     626        // Get the rendered content
     627        $content = apply_filters( 'the_content', $post->post_content );
     628
     629        return $this->rpc( 'ResyncPage', array(
     630            'url'     => esc_url_raw( $page_url ),
     631            'title'   => get_the_title( $post_id ),
     632            'content' => $content,
     633        ), 120 ); // 2 min timeout for resync
    422634    }
    423635
  • bitbot/trunk/includes/class-widget.php

    r3417246 r3417995  
    4545
    4646    /**
     47     * Get localized greeting based on site language
     48     *
     49     * @since 1.0.0
     50     * @return string Greeting message in the site's language.
     51     */
     52    private function get_localized_greeting() {
     53        // Default greetings for common languages
     54        $greetings = array(
     55            'en' => 'Hi! How can I help you navigate our website today?',
     56            'hi' => 'नमस्ते! आज मैं आपकी वेबसाइट नेविगेट करने में कैसे मदद कर सकता हूँ?',
     57            'es' => '¡Hola! ¿Cómo puedo ayudarte a navegar por nuestro sitio web hoy?',
     58            'fr' => 'Bonjour ! Comment puis-je vous aider à naviguer sur notre site aujourd\'hui ?',
     59            'de' => 'Hallo! Wie kann ich Ihnen heute bei der Navigation auf unserer Website helfen?',
     60            'pt' => 'Olá! Como posso ajudá-lo a navegar em nosso site hoje?',
     61            'zh' => '您好!今天我能如何帮助您浏览我们的网站?',
     62            'ja' => 'こんにちは!本日はウェブサイトのナビゲーションをどのようにお手伝いできますか?',
     63            'ko' => '안녕하세요! 오늘 웹사이트 탐색에 어떻게 도움을 드릴까요?',
     64            'ar' => 'مرحباً! كيف يمكنني مساعدتك في تصفح موقعنا اليوم؟',
     65            'ru' => 'Привет! Как я могу помочь вам сегодня с навигацией по нашему сайту?',
     66        );
     67
     68        // Get language code from WordPress locale
     69        $locale        = get_locale();
     70        $language_code = substr( $locale, 0, 2 );
     71
     72        // Return greeting for the language, or default to English
     73        return isset( $greetings[ $language_code ] ) ? $greetings[ $language_code ] : $greetings['en'];
     74    }
     75
     76    /**
    4777     * Render the chat widget HTML
    4878     *
     
    5686        $position        = sanitize_text_field( get_option( 'bitbot_widget_position', 'bottom-right' ) );
    5787        $primary_color   = sanitize_hex_color( get_option( 'bitbot_primary_color', '#0073aa' ) );
    58         $welcome_message = wp_kses_post( get_option( 'bitbot_welcome_message', __( 'Hi! How can I help you navigate our website today?', 'bitbot' ) ) );
     88
     89        // Check if user has set a custom welcome message, otherwise use localized default
     90        $custom_message  = get_option( 'bitbot_welcome_message', '' );
     91        $welcome_message = ! empty( $custom_message ) ? wp_kses_post( $custom_message ) : $this->get_localized_greeting();
    5992        ?>
    6093        <div id="bitbot-widget" class="bitbot-widget bitbot-<?php echo esc_attr( $position ); ?>" style="--bitbot-primary: <?php echo esc_attr( $primary_color ); ?>">
     
    149182     */
    150183    public function handle_chat_request( $request ) {
    151         $message    = sanitize_text_field( $request->get_param( 'message' ) );
    152         $history    = $request->get_param( 'history' ) ?? array();
    153         $session_id = sanitize_text_field( $request->get_param( 'sessionId' ) ?? '' );
    154         $visitor_ip = $this->get_visitor_ip();
     184        $message       = sanitize_text_field( $request->get_param( 'message' ) );
     185        $history       = $request->get_param( 'history' ) ?? array();
     186        $session_id    = sanitize_text_field( $request->get_param( 'sessionId' ) ?? '' );
     187        $site_language = sanitize_text_field( $request->get_param( 'siteLanguage' ) ?? substr( get_locale(), 0, 2 ) );
     188        $visitor_ip    = $this->get_visitor_ip();
    155189
    156190        if ( empty( $message ) ) {
     
    175209        }
    176210
    177         $result = $this->api_client->chat( $message, $sanitized_history, $session_id, $visitor_ip );
     211        $result = $this->api_client->chat( $message, $sanitized_history, $session_id, $visitor_ip, $site_language );
    178212
    179213        if ( $result['success'] ) {
  • bitbot/trunk/readme.txt

    r3417246 r3417995  
    22Contributors: youamibot
    33Donate link: https://youami.ai
    4 Tags: chatbot, ai, content, seo, assistant
     4Tags: chatbot, ai, content, seo, assistant, multilingual
    55Requires at least: 5.8
    66Tested up to: 6.9
    77Requires PHP: 7.4
    8 Stable tag: 1.0.0
     8Stable tag: 1.1.0
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1111
    12 AI-powered chatbot that helps visitors navigate your website using your own content, plus AI content generation.
     12AI-powered multilingual chatbot that helps visitors navigate your website in any language, plus AI content generation.
    1313
    1414== Description ==
    1515
    16 BitBot by youami.ai transforms your WordPress site with an intelligent AI assistant that knows your content.
     16BitBot by youami.ai transforms your WordPress site with an intelligent AI assistant that knows your content and speaks your visitors' language.
    1717
    18 = Smart Chat Assistant =
     18= Multilingual Chat Assistant =
    1919
     20* **Supports 100+ languages** - Visitors can ask questions in their native language
     21* **Automatic language detection** - No configuration needed, just works
     22* **Cross-language search** - Finds relevant content regardless of query language
     23* **Responds in visitor's language** - Natural conversation in any language
    2024* Answers visitor questions based on your actual site content
    2125* Provides clickable links to relevant pages and sections
     
    2327* Customizable appearance and widget position
    2428* Mobile-responsive chat widget
     29
     30**Reach a global audience** - Whether your site is in English, Spanish, Hindi, Chinese, Arabic, or any other language, BitBot helps visitors find what they need in the language they prefer.
    2531
    2632= AI Content Generator =
     
    3440= Key Features =
    3541
     42* **Global language support** - Works with any WordPress site language
    3643* Automatic sitemap crawling and content indexing
    3744* Deep linking to page sections via anchor tags
     
    7986
    8087== Frequently Asked Questions ==
     88
     89= Does BitBot support my language? =
     90
     91Yes! BitBot supports 100+ languages out of the box. It automatically detects the visitor's language and responds accordingly. Your site can be in any language - English, Spanish, French, German, Hindi, Chinese, Arabic, Japanese, Korean, Portuguese, Russian, and many more.
    8192
    8293= How does the chatbot learn about my site? =
     
    134145== Changelog ==
    135146
     147= 1.1.0 =
     148* **Multilingual support** - 100+ languages with automatic detection
     149* Cross-language search - query in any language, find content in site's language
     150* Responds in visitor's language automatically
     151
    136152= 1.0.0 =
    137153* Initial release
     
    145161
    146162== Upgrade Notice ==
     163
     164= 1.1.0 =
     165Major update! BitBot now supports 100+ languages. Visitors can ask questions in their native language and get answers in the same language.
    147166
    148167= 1.0.0 =
Note: See TracChangeset for help on using the changeset viewer.