Changeset 3417995
- Timestamp:
- 12/12/2025 08:00:18 AM (2 months ago)
- Location:
- bitbot
- Files:
-
- 17 added
- 7 edited
-
tags/1.1.0 (added)
-
tags/1.1.0/assets (added)
-
tags/1.1.0/assets/css (added)
-
tags/1.1.0/assets/css/admin.css (added)
-
tags/1.1.0/assets/css/widget.css (added)
-
tags/1.1.0/assets/js (added)
-
tags/1.1.0/assets/js/admin.js (added)
-
tags/1.1.0/assets/js/widget.js (added)
-
tags/1.1.0/bitbot.php (added)
-
tags/1.1.0/includes (added)
-
tags/1.1.0/includes/class-admin.php (added)
-
tags/1.1.0/includes/class-api-client.php (added)
-
tags/1.1.0/includes/class-widget.php (added)
-
tags/1.1.0/languages (added)
-
tags/1.1.0/languages/.gitkeep (added)
-
tags/1.1.0/readme.txt (added)
-
tags/1.1.0/uninstall.php (added)
-
trunk/assets/js/admin.js (modified) (4 diffs)
-
trunk/assets/js/widget.js (modified) (1 diff)
-
trunk/bitbot.php (modified) (6 diffs)
-
trunk/includes/class-admin.php (modified) (5 diffs)
-
trunk/includes/class-api-client.php (modified) (6 diffs)
-
trunk/includes/class-widget.php (modified) (4 diffs)
-
trunk/readme.txt (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
bitbot/trunk/assets/js/admin.js
r3417246 r3417995 18 18 bindSubscriptionActions(); 19 19 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 }); 20 62 } 21 63 … … 30 72 const originalText = $btn.text(); 31 73 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) 37 84 data: { 38 85 action: 'bitbot_sync', … … 41 88 success: function(response) { 42 89 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(); 44 94 } else { 45 95 alert('Sync failed: ' + (response.data?.error || 'Unknown error')); 46 96 } 47 97 }, 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 } 50 104 }, 51 105 complete: function() { 52 $btn.text(originalText).removeClass('syncing'); 106 $btn.text(originalText).removeClass('syncing').prop('disabled', false); 107 $('#sync-progress').remove(); 53 108 } 54 109 }); … … 779 834 } 780 835 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 781 1081 // Initialize when ready 782 1082 $(document).ready(function() { 783 1083 init(); 784 1084 bindConversationsActions(); 1085 bindPagesActions(); 785 1086 }); 786 1087 -
bitbot/trunk/assets/js/widget.js
r3417246 r3417995 142 142 message: message, 143 143 history: chatHistory.slice(-10), // Send last 10 messages for context 144 sessionId: getSessionId() 144 sessionId: getSessionId(), 145 siteLanguage: config.languageCode || 'en' // Pass site language 145 146 }) 146 147 }); -
bitbot/trunk/bitbot.php
r3417246 r3417995 11 11 * Plugin Name: BitBot 12 12 * 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.013 * Description: AI-powered multilingual chatbot that helps visitors navigate your website in any language, plus AI content generation. 14 * Version: 1.1.0 15 15 * Requires at least: 5.8 16 16 * Requires PHP: 7.4 … … 50 50 define( 'BITBOT_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); 51 51 define( 'BITBOT_API_URL', 'https://youami.ai/api' ); 52 // define( 'BITBOT_API_URL', 'https://dev.youami.ai/api' ); 52 53 53 54 // Include required files … … 161 162 private function ensure_api_key() { 162 163 if ( ! get_option( 'bitbot_api_key' ) ) { 163 $api_key = wp_generate_uuid4();164 $api_key = 'bb_' . str_replace( '-', '', wp_generate_uuid4() ); 164 165 update_option( 'bitbot_api_key', $api_key ); 165 166 } … … 196 197 // Generate unique API key for this site 197 198 if ( ! get_option( 'bitbot_api_key' ) ) { 198 $api_key = wp_generate_uuid4();199 $api_key = 'bb_' . str_replace( '-', '', wp_generate_uuid4() ); 199 200 update_option( 'bitbot_api_key', $api_key ); 200 201 } … … 257 258 ); 258 259 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 259 264 wp_localize_script( 'bitbot-widget', 'bitbotConfig', array( 260 265 'restUrl' => esc_url_raw( rest_url( 'bitbot/v1' ) ), … … 263 268 'primaryColor' => sanitize_hex_color( get_option( 'bitbot_primary_color', '#0073aa' ) ), 264 269 'welcomeMessage' => wp_kses_post( get_option( 'bitbot_welcome_message', __( 'Hi! How can I help you?', 'bitbot' ) ) ), 270 'languageCode' => sanitize_text_field( $language_code ), 265 271 ) ); 266 272 } -
bitbot/trunk/includes/class-admin.php
r3417246 r3417995 48 48 add_action('wp_ajax_bitbot_get_conversations', array($this, 'ajax_get_conversations')); 49 49 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')); 50 60 } 51 61 … … 168 178 */ 169 179 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(); 177 192 } 178 193 } … … 180 195 $sync_status = $this->api_client->get_sync_status(); 181 196 $usage = $this->api_client->get_usage(); 182 $subscription = $this->api_client->get_subscription();183 197 $api_key = get_option('bitbot_api_key', ''); 184 185 // Check if site needs registration186 $needs_registration = !$subscription['success'] && strpos($subscription['error'] ?? '', 'Invalid API key') !== false;187 198 ?> 188 199 <div class="wrap"> 189 200 <h1><?php esc_html_e('BitBot Dashboard', 'bitbot'); ?></h1> 190 201 191 <!-- Always show API key for debugging -->192 202 <p style="color: #666; font-size: 12px;"> 193 203 <?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> 194 207 </p> 195 208 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> 205 212 </div> 206 213 <?php endif; ?> … … 355 362 <p class="error"><?php echo esc_html($subscription['error'] ?? 'Unable to fetch subscription'); ?></p> 356 363 <?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> 357 411 </div> 358 412 … … 930 984 } 931 985 } 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 } 932 1126 } -
bitbot/trunk/includes/class-api-client.php
r3417246 r3417995 54 54 * @param string $request_tag The RPC request tag. 55 55 * @param array $payload Request payload data. 56 * @param int $timeout Request timeout in seconds (default 60, max 900 for sync). 56 57 * @return array Response with 'success' and 'data' or 'error' keys. 57 58 */ 58 private function rpc( $request_tag, $payload = array() ) {59 private function rpc( $request_tag, $payload = array(), $timeout = 60 ) { 59 60 // Add API key to all payloads 60 61 $payload['apiKey'] = $this->api_key; … … 68 69 $args = array( 69 70 'method' => 'POST', 70 'timeout' => 60,71 'timeout' => $timeout, 71 72 'headers' => array( 72 73 'Content-Type' => 'application/json', … … 158 159 159 160 /** 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. 164 223 */ 165 224 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 ); 180 303 } 181 304 … … 200 323 * @return array API response. 201 324 */ 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' ) { 203 326 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 ) { 206 329 return array( 207 330 'role' => sanitize_text_field( $msg['role'] ), … … 209 332 ); 210 333 }, $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 ), 213 337 ) ); 214 338 } … … 420 544 'conversationId' => sanitize_text_field( $conversation_id ), 421 545 ) ); 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 422 634 } 423 635 -
bitbot/trunk/includes/class-widget.php
r3417246 r3417995 45 45 46 46 /** 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 /** 47 77 * Render the chat widget HTML 48 78 * … … 56 86 $position = sanitize_text_field( get_option( 'bitbot_widget_position', 'bottom-right' ) ); 57 87 $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(); 59 92 ?> 60 93 <div id="bitbot-widget" class="bitbot-widget bitbot-<?php echo esc_attr( $position ); ?>" style="--bitbot-primary: <?php echo esc_attr( $primary_color ); ?>"> … … 149 182 */ 150 183 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(); 155 189 156 190 if ( empty( $message ) ) { … … 175 209 } 176 210 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 ); 178 212 179 213 if ( $result['success'] ) { -
bitbot/trunk/readme.txt
r3417246 r3417995 2 2 Contributors: youamibot 3 3 Donate link: https://youami.ai 4 Tags: chatbot, ai, content, seo, assistant 4 Tags: chatbot, ai, content, seo, assistant, multilingual 5 5 Requires at least: 5.8 6 6 Tested up to: 6.9 7 7 Requires PHP: 7.4 8 Stable tag: 1. 0.08 Stable tag: 1.1.0 9 9 License: GPLv2 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html 11 11 12 AI-powered chatbot that helps visitors navigate your website using your own content, plus AI content generation.12 AI-powered multilingual chatbot that helps visitors navigate your website in any language, plus AI content generation. 13 13 14 14 == Description == 15 15 16 BitBot by youami.ai transforms your WordPress site with an intelligent AI assistant that knows your content .16 BitBot by youami.ai transforms your WordPress site with an intelligent AI assistant that knows your content and speaks your visitors' language. 17 17 18 = SmartChat Assistant =18 = Multilingual Chat Assistant = 19 19 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 20 24 * Answers visitor questions based on your actual site content 21 25 * Provides clickable links to relevant pages and sections … … 23 27 * Customizable appearance and widget position 24 28 * 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. 25 31 26 32 = AI Content Generator = … … 34 40 = Key Features = 35 41 42 * **Global language support** - Works with any WordPress site language 36 43 * Automatic sitemap crawling and content indexing 37 44 * Deep linking to page sections via anchor tags … … 79 86 80 87 == Frequently Asked Questions == 88 89 = Does BitBot support my language? = 90 91 Yes! 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. 81 92 82 93 = How does the chatbot learn about my site? = … … 134 145 == Changelog == 135 146 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 136 152 = 1.0.0 = 137 153 * Initial release … … 145 161 146 162 == Upgrade Notice == 163 164 = 1.1.0 = 165 Major update! BitBot now supports 100+ languages. Visitors can ask questions in their native language and get answers in the same language. 147 166 148 167 = 1.0.0 =
Note: See TracChangeset
for help on using the changeset viewer.