Changeset 3801388
- Timestamp:
- 11/14/2026 04:05:17 AM (less than one hour ago)
- Location:
- chat-assist-for-woocommerce
- Files:
-
- 10 edited
- 1 copied
-
tags/1.1.2 (copied) (copied from chat-assist-for-woocommerce/trunk )
-
tags/1.1.2/assets/js/frontend-widget.js (modified) (12 diffs)
-
tags/1.1.2/blocks/chat-assist/block.json (modified) (1 diff)
-
tags/1.1.2/includes/class-rest.php (modified) (3 diffs)
-
tags/1.1.2/readme.txt (modified) (3 diffs)
-
tags/1.1.2/woo-chat-assist.php (modified) (2 diffs)
-
trunk/assets/js/frontend-widget.js (modified) (12 diffs)
-
trunk/blocks/chat-assist/block.json (modified) (1 diff)
-
trunk/includes/class-rest.php (modified) (3 diffs)
-
trunk/readme.txt (modified) (3 diffs)
-
trunk/woo-chat-assist.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
chat-assist-for-woocommerce/tags/1.1.2/assets/js/frontend-widget.js
r3800390 r3801388 22 22 // bounded pre-roll (about 3 s of 16 kHz PCM) and sent in order when listening opens. 23 23 const VOICE_PREROLL_MAX_BYTES = 96 * 1024; 24 // How long an ended Talk session's socket stays open for the hand-over answer (language switch). 25 const VOICE_HANDOFF_WAIT_MS = 2500; 24 26 /** Microphone gate: 'closed' while the assistant talks, 'preroll' until the backend listens, then 'open'. */ 25 27 function createVoiceCaptureGate( send, maxBytes = VOICE_PREROLL_MAX_BYTES ) { … … 369 371 this.preloadToken = null; 370 372 this.preloadWs = null; 373 // Talk hand-over to the next session on this page: the handle the backend gave, and a hand-over on its way. 374 this.voiceContinuationHandle = null; 375 this.voiceHandoff = null; 371 376 this.preloadWsReady = false; 372 377 this.preloadReadyPromise = null; … … 618 623 } 619 624 620 document.addEventListener( 'keydown', ( e ) => { 625 // Leaving the page: a Talk hand-over handle that waits for a next start is given back. 626 if ( typeof window.addEventListener === 'function' ) { 627 window.addEventListener( 'pagehide', ( e ) => { 628 if ( ! e.persisted ) this.revokeVoiceContinuation( true ); 629 } ); 630 } 631 632 document.addEventListener( 'keydown', ( e ) => { 621 633 if ( e.key === 'Escape' && ! this.panel.classList.contains( 'chafwc-hidden' ) ) { 622 634 this.togglePanel(); … … 899 911 if ( this.currentTab === 'voice' ) { 900 912 if ( this.websocket || this.isConnecting ) { 901 this.stopVoice(); 913 // The live call ends, and the next one goes on with the conversation (merchant direction 2026-11-12 914 // and live check 2026-11-14: Talk forgot the product after a language switch; same as Shopify). 915 this.stopVoice( { continues: true } ); 902 916 } 903 917 this.cleanupPreload(); … … 1047 1061 } else { 1048 1062 // Cleanup voice if active 1063 this.revokeVoiceContinuation(); 1049 1064 if ( this.state === STATE.LISTENING || this.state === STATE.SPEAKING || this.state === STATE.THINKING || this.isConnecting ) { 1050 1065 this.stopVoice(); … … 1077 1092 this.voiceMode.hidden = true; 1078 1093 this.voiceMode.classList.remove( 'chafwc-mode-active' ); 1094 this.revokeVoiceContinuation(); 1079 1095 this.stopVoice(); 1080 1096 // Cancel any preload … … 1175 1191 }); 1176 1192 1193 // A handle waits for the next Talk start, or is on its way: a session prepared ahead could not use it. 1194 if ( this.voiceContinuationHandle || this.voiceHandoff ) { 1195 debug( 'Preload skipped - voice continuation pending' ); 1196 return; 1197 } 1198 1177 1199 // Don't preload if voice not enabled or already connected/preloading 1178 1200 if ( ! hasVoice || this.mode === 'free' || this.websocket || this.isPreloading || this.preloadWs ) { … … 2617 2639 this.startCapturePreroll(); 2618 2640 2641 // A session ended to go on is still handing over: wait for its handle. A session prepared ahead 2642 // knows nothing of that conversation, so it is dropped and a fresh one starts with the handle. 2643 if ( this.voiceHandoff ) await this.voiceHandoff.promise; 2644 if ( this.voiceContinuationHandle ) this.cleanupPreload(); 2645 2619 2646 if ( this.preloadWs && this.preloadWs.readyState === WebSocket.OPEN && ! this.preloadWsReady ) { 2620 2647 await this.waitForPreloadReady( 1500 ); … … 2665 2692 debug( 'No preload available, creating new connection...' ); 2666 2693 this.cleanupPreload(); // Clear any partial preload 2694 this.discardBufferedAudio(); 2667 2695 2668 2696 const tokenResponse = await this.apiRequest( 'session/token', { 2669 2697 method: 'POST', 2670 body: JSON.stringify( { context: this.getContext() })2698 body: JSON.stringify( this.voiceTokenRequestBody() ) 2671 2699 }); 2672 2700 … … 2793 2821 } 2794 2822 2795 stopVoice() { 2823 /** 2824 * Drops audio that was received outside a reply stream and not played. Such audio (the greeting, late 2825 * frames of a reply) belongs to the session it came in: left in the buffer, the tail of the last answer was 2826 * played as the next session's greeting and the real greeting never was (merchant QA 2026-11-14, Woo Talk 2827 * after a language switch: 0.12 s played of a 2.3 s German greeting). Every end and every fresh start of a 2828 * Talk session runs this; a session prepared ahead starts with an empty buffer of its own. 2829 */ 2830 discardBufferedAudio() { 2831 this.audioChunks = []; 2832 this.pendingAudioPlay = false; 2833 if ( this.pendingAudioTimeout ) { 2834 clearTimeout( this.pendingAudioTimeout ); 2835 this.pendingAudioTimeout = null; 2836 } 2837 } 2838 2839 /** The Talk token request: the page context and, once, the hand-over handle of the session before. */ 2840 voiceTokenRequestBody() { 2841 const body = { context: this.getContext() }; 2842 if ( this.voiceContinuationHandle ) body.continuationHandle = this.voiceContinuationHandle; 2843 this.voiceContinuationHandle = null; 2844 return body; 2845 } 2846 2847 storeVoiceContinuation( handle ) { 2848 if ( typeof handle === 'string' && /^[A-Za-z0-9_-]{43}$/.test( handle ) ) this.voiceContinuationHandle = handle; 2849 } 2850 2851 /** Waits on an ended session's socket for the hand-over answer, keeps the handle and closes the socket. */ 2852 awaitVoiceHandoff( ws ) { 2853 const handoff = { cancelled: false, promise: null }; 2854 handoff.promise = new Promise( resolve => { 2855 let done = false; 2856 let timer = null; 2857 const finish = ( handle = null ) => { 2858 if ( done ) return; 2859 done = true; 2860 clearTimeout( timer ); 2861 ws.onmessage = () => {}; 2862 ws.onerror = () => {}; 2863 ws.onclose = () => {}; 2864 try { 2865 ws.close( 1000, 'User ended session' ); 2866 } catch ( error ) { 2867 debug( 'Hand-over socket close failed', error ); 2868 } 2869 if ( this.voiceHandoff === handoff ) this.voiceHandoff = null; 2870 // Talk was left while the answer was on its way: the handle is given back instead of kept. 2871 if ( handoff.cancelled ) this.sendVoiceContinuationRevoke( handle ); 2872 else this.storeVoiceContinuation( handle ); 2873 resolve(); 2874 }; 2875 timer = setTimeout( () => finish(), VOICE_HANDOFF_WAIT_MS ); 2876 ws.onmessage = event => { 2877 if ( typeof event.data !== 'string' ) return; 2878 let message = null; 2879 try { 2880 message = JSON.parse( event.data ); 2881 } catch ( error ) { 2882 return; 2883 } 2884 if ( message?.type === 'session.continuation' ) finish( message.payload?.continuationHandle ?? null ); 2885 }; 2886 ws.onerror = () => finish(); 2887 ws.onclose = () => finish(); 2888 }); 2889 this.voiceHandoff = handoff; 2890 // Nothing to carry over: Talk is prepared ahead as after any stop. 2891 void handoff.promise.then( () => { 2892 if ( ! this.voiceContinuationHandle && ! handoff.cancelled && this.currentTab === 'voice' && 2893 ! this.panel.classList.contains( 'chafwc-hidden' ) ) this.preloadVoiceConnection(); 2894 }); 2895 } 2896 2897 /** Leaving Talk (panel closed, Chat tab, page left): a handle that will not be used is given back. */ 2898 revokeVoiceContinuation( unloading = false ) { 2899 const handle = this.voiceContinuationHandle; 2900 this.voiceContinuationHandle = null; 2901 if ( this.voiceHandoff ) this.voiceHandoff.cancelled = true; 2902 this.sendVoiceContinuationRevoke( handle, unloading ); 2903 } 2904 2905 sendVoiceContinuationRevoke( handle, unloading = false ) { 2906 if ( typeof handle !== 'string' || ! handle ) return; 2907 const body = JSON.stringify({ continuationHandle: handle }); 2908 if ( unloading && typeof navigator !== 'undefined' && navigator.sendBeacon ) { 2909 // A beacon cannot carry the nonce header, so it goes in the address, which the route also reads. 2910 const url = `${config.restUrl}session/continuation/revoke?_wpnonce=${encodeURIComponent( config.nonce || '' )}`; 2911 if ( navigator.sendBeacon( url, new Blob( [ body ], { type: 'application/json' } ) ) ) return; 2912 } 2913 void this.apiRequest( 'session/continuation/revoke', { method: 'POST', body } ) 2914 .then( () => {}, () => debug( 'Voice continuation revoke request failed' ) ); 2915 } 2916 2917 /** 2918 * Ends Talk. With `continues` (language switch) the next Talk session goes on with this conversation: the 2919 * end message asks the backend for a handle, and only the socket stays open, for at most 2920 * VOICE_HANDOFF_WAIT_MS, to receive it. The stop button and every other end are as before. 2921 */ 2922 stopVoice( { continues = false } = {} ) { 2796 2923 debug( 'stopVoice called' ); 2797 2924 this.revealPendingVoiceAssistantTurnText(); … … 2806 2933 if ( this.websocket ) { 2807 2934 // Send end message first, then close gracefully 2808 if ( this.websocket.readyState === WebSocket.OPEN ) { 2935 const ws = this.websocket; 2936 const open = ws.readyState === WebSocket.OPEN; 2937 if ( open ) { 2809 2938 this.sendWsMessage( 'audio.stop', {} ); 2810 this.sendWsMessage( 'session.end', { reason: 'user_end' } ); 2811 } 2939 this.sendWsMessage( 'session.end', continues ? { reason: 'user_end', continuation: true } : { reason: 'user_end' } ); 2940 } 2941 this.websocket = null; 2812 2942 // Use code 1000 (normal closure) to avoid 1005 error 2813 this.websocket.close( 1000, 'User ended session');2814 this.websocket = null;2943 if ( continues && open ) this.awaitVoiceHandoff( ws ); 2944 else ws.close( 1000, 'User ended session' ); 2815 2945 } 2816 2946 this.stopAudioCapture(); 2817 2947 this.isConnecting = false; 2818 this.pendingAudioPlay = false; 2819 if ( this.pendingAudioTimeout ) { 2820 clearTimeout( this.pendingAudioTimeout ); 2821 this.pendingAudioTimeout = null; 2822 } 2948 this.discardBufferedAudio(); 2823 2949 this.setState( STATE.IDLE ); 2824 2950 … … 3316 3442 // Backend ended session due to inactivity — clean up and go idle 3317 3443 debug( 'Session timed out due to inactivity' ); 3444 this.storeVoiceContinuation( message.payload?.continuationHandle ); 3445 this.discardBufferedAudio(); 3318 3446 this.stopAudioCapture(); 3319 3447 if ( this.websocket ) { -
chat-assist-for-woocommerce/tags/1.1.2/blocks/chat-assist/block.json
r3800390 r3801388 3 3 "apiVersion": 3, 4 4 "name": "chat-assist/chat-widget", 5 "version": "1.1. 1",5 "version": "1.1.2", 6 6 "title": "Chat Assist Widget", 7 7 "category": "widgets", -
chat-assist-for-woocommerce/tags/1.1.2/includes/class-rest.php
r3800390 r3801388 166 166 'permission_callback' => array( $this, 'public_permission_check' ), 167 167 'args' => array( 168 'context' => array(168 'context' => array( 169 169 'type' => 'object', 170 170 'default' => array(), 171 ), 172 'continuationHandle' => array( 173 'type' => 'string', 174 'default' => '', 175 ), 176 ), 177 ) 178 ); 179 180 // Give back a Talk hand-over handle that will not be used (panel closed, Chat tab, page left). 181 register_rest_route( 182 $this->namespace, 183 '/session/continuation/revoke', 184 array( 185 'methods' => 'POST', 186 'callback' => array( $this, 'revoke_voice_continuation' ), 187 'permission_callback' => array( $this, 'public_permission_check' ), 188 'args' => array( 189 'continuationHandle' => array( 190 'type' => 'string', 191 'default' => '', 171 192 ), 172 193 ), … … 1162 1183 1163 1184 // Request short-lived session token from backend. 1164 $response = CHAFWC_Helpers::backend_request( 1165 'v1/sessions/token', 1166 array( 1167 'method' => 'POST', 1168 'body' => array( 1169 'origin' => home_url(), 1170 'userAgent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', 1171 'ipHash' => CHAFWC_Helpers::hash_ip( CHAFWC_Helpers::get_client_ip() ), 1172 'context' => $context, 1173 'shopContext' => $shop_context, 1174 'greetings' => $greeting_messages, 1175 ), 1176 ) 1185 $response = $this->request_voice_token( 1186 array( 1187 'origin' => home_url(), 1188 'userAgent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', 1189 'ipHash' => CHAFWC_Helpers::hash_ip( CHAFWC_Helpers::get_client_ip() ), 1190 'context' => $context, 1191 'shopContext' => $shop_context, 1192 'greetings' => $greeting_messages, 1193 ), 1194 $this->voice_continuation_handle( $request ) 1177 1195 ); 1178 1196 … … 1200 1218 ) 1201 1219 ); 1220 } 1221 1222 /** 1223 * The Talk hand-over handle of a request, or an empty string when it is missing or malformed. 1224 * 1225 * @param WP_REST_Request $request Request object. 1226 * @return string 1227 */ 1228 public function voice_continuation_handle( $request ) { 1229 $handle = $request->get_param( 'continuationHandle' ); 1230 return is_string( $handle ) && 1 === preg_match( '/^[A-Za-z0-9_-]{43}$/', $handle ) ? $handle : ''; 1231 } 1232 1233 /** 1234 * Ask the backend for a Talk token. With a hand-over handle the new session goes on with the conversation of 1235 * the one before it (after a language switch, inactivity or the time limit). A handle the backend no longer 1236 * accepts must not cost the shopper the call: the token is then requested once more without it. 1237 * 1238 * @param array $body Token request body. 1239 * @param string $handle Hand-over handle, or an empty string. 1240 * @return array|WP_Error 1241 */ 1242 public function request_voice_token( $body, $handle = '' ) { 1243 if ( '' !== $handle ) { 1244 $response = CHAFWC_Helpers::backend_request( 1245 'v1/sessions/token', 1246 array( 1247 'method' => 'POST', 1248 'body' => array_merge( $body, array( 'continuationHandle' => $handle ) ), 1249 ) 1250 ); 1251 $data = is_wp_error( $response ) ? $response->get_error_data() : null; 1252 if ( ! is_array( $data ) || ! isset( $data['status'] ) || 400 !== (int) $data['status'] ) { 1253 return $response; 1254 } 1255 } 1256 1257 return CHAFWC_Helpers::backend_request( 1258 'v1/sessions/token', 1259 array( 1260 'method' => 'POST', 1261 'body' => $body, 1262 ) 1263 ); 1264 } 1265 1266 /** 1267 * Give a Talk hand-over handle back to the backend. Always answers success: the widget has nothing to do 1268 * with the outcome, and an unknown handle is not an error. 1269 * 1270 * @param WP_REST_Request $request Request object. 1271 * @return WP_REST_Response 1272 */ 1273 public function revoke_voice_continuation( $request ) { 1274 $handle = $this->voice_continuation_handle( $request ); 1275 if ( '' !== $handle && CHAFWC_Helpers::has_backend() ) { 1276 CHAFWC_Helpers::backend_request( 1277 'v1/sessions/continuation/revoke', 1278 array( 1279 'method' => 'POST', 1280 'body' => array( 'continuationHandle' => $handle ), 1281 ) 1282 ); 1283 } 1284 1285 return rest_ensure_response( array( 'success' => true ) ); 1202 1286 } 1203 1287 -
chat-assist-for-woocommerce/tags/1.1.2/readme.txt
r3800390 r3801388 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1.1. 17 Stable tag: 1.1.2 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 115 115 == Changelog == 116 116 117 = 1.1.2 = 118 * Talk continues the conversation after a language switch and after the inactivity notice: the assistant still knows the products the shopper asked about. 119 117 120 = 1.1.1 = 118 121 * Keep paid chat sessions out of the free fallback: the request limit is now a true per-minute limit, and AI chat streaming counts separately. … … 158 161 == Upgrade Notice == 159 162 163 = 1.1.2 = 164 Talk keeps the conversation after a language switch and after a pause. 165 160 166 = 1.1.1 = 161 167 Keeps paid chat sessions from falling back to free mode, fixes Talk audio and first words, adds an AI notice and shows more of the widget in the shopper's language. -
chat-assist-for-woocommerce/tags/1.1.2/woo-chat-assist.php
r3800390 r3801388 4 4 * Plugin URI: https://qonvor.com/docs/ 5 5 * Description: Help shoppers find products, track orders, and get answers in your WooCommerce store. Free chat included, with paid text and voice options. 6 * Version: 1.1. 16 * Version: 1.1.2 7 7 * Author: Space Lab Apps Pty Ltd 8 8 * Author URI: https://qonvor.com … … 22 22 defined( 'ABSPATH' ) || exit; 23 23 24 define( 'CHAFWC_VERSION', '1.1. 1' );24 define( 'CHAFWC_VERSION', '1.1.2' ); 25 25 define( 'CHAFWC_PLUGIN_FILE', __FILE__ ); 26 26 define( 'CHAFWC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); -
chat-assist-for-woocommerce/trunk/assets/js/frontend-widget.js
r3800390 r3801388 22 22 // bounded pre-roll (about 3 s of 16 kHz PCM) and sent in order when listening opens. 23 23 const VOICE_PREROLL_MAX_BYTES = 96 * 1024; 24 // How long an ended Talk session's socket stays open for the hand-over answer (language switch). 25 const VOICE_HANDOFF_WAIT_MS = 2500; 24 26 /** Microphone gate: 'closed' while the assistant talks, 'preroll' until the backend listens, then 'open'. */ 25 27 function createVoiceCaptureGate( send, maxBytes = VOICE_PREROLL_MAX_BYTES ) { … … 369 371 this.preloadToken = null; 370 372 this.preloadWs = null; 373 // Talk hand-over to the next session on this page: the handle the backend gave, and a hand-over on its way. 374 this.voiceContinuationHandle = null; 375 this.voiceHandoff = null; 371 376 this.preloadWsReady = false; 372 377 this.preloadReadyPromise = null; … … 618 623 } 619 624 620 document.addEventListener( 'keydown', ( e ) => { 625 // Leaving the page: a Talk hand-over handle that waits for a next start is given back. 626 if ( typeof window.addEventListener === 'function' ) { 627 window.addEventListener( 'pagehide', ( e ) => { 628 if ( ! e.persisted ) this.revokeVoiceContinuation( true ); 629 } ); 630 } 631 632 document.addEventListener( 'keydown', ( e ) => { 621 633 if ( e.key === 'Escape' && ! this.panel.classList.contains( 'chafwc-hidden' ) ) { 622 634 this.togglePanel(); … … 899 911 if ( this.currentTab === 'voice' ) { 900 912 if ( this.websocket || this.isConnecting ) { 901 this.stopVoice(); 913 // The live call ends, and the next one goes on with the conversation (merchant direction 2026-11-12 914 // and live check 2026-11-14: Talk forgot the product after a language switch; same as Shopify). 915 this.stopVoice( { continues: true } ); 902 916 } 903 917 this.cleanupPreload(); … … 1047 1061 } else { 1048 1062 // Cleanup voice if active 1063 this.revokeVoiceContinuation(); 1049 1064 if ( this.state === STATE.LISTENING || this.state === STATE.SPEAKING || this.state === STATE.THINKING || this.isConnecting ) { 1050 1065 this.stopVoice(); … … 1077 1092 this.voiceMode.hidden = true; 1078 1093 this.voiceMode.classList.remove( 'chafwc-mode-active' ); 1094 this.revokeVoiceContinuation(); 1079 1095 this.stopVoice(); 1080 1096 // Cancel any preload … … 1175 1191 }); 1176 1192 1193 // A handle waits for the next Talk start, or is on its way: a session prepared ahead could not use it. 1194 if ( this.voiceContinuationHandle || this.voiceHandoff ) { 1195 debug( 'Preload skipped - voice continuation pending' ); 1196 return; 1197 } 1198 1177 1199 // Don't preload if voice not enabled or already connected/preloading 1178 1200 if ( ! hasVoice || this.mode === 'free' || this.websocket || this.isPreloading || this.preloadWs ) { … … 2617 2639 this.startCapturePreroll(); 2618 2640 2641 // A session ended to go on is still handing over: wait for its handle. A session prepared ahead 2642 // knows nothing of that conversation, so it is dropped and a fresh one starts with the handle. 2643 if ( this.voiceHandoff ) await this.voiceHandoff.promise; 2644 if ( this.voiceContinuationHandle ) this.cleanupPreload(); 2645 2619 2646 if ( this.preloadWs && this.preloadWs.readyState === WebSocket.OPEN && ! this.preloadWsReady ) { 2620 2647 await this.waitForPreloadReady( 1500 ); … … 2665 2692 debug( 'No preload available, creating new connection...' ); 2666 2693 this.cleanupPreload(); // Clear any partial preload 2694 this.discardBufferedAudio(); 2667 2695 2668 2696 const tokenResponse = await this.apiRequest( 'session/token', { 2669 2697 method: 'POST', 2670 body: JSON.stringify( { context: this.getContext() })2698 body: JSON.stringify( this.voiceTokenRequestBody() ) 2671 2699 }); 2672 2700 … … 2793 2821 } 2794 2822 2795 stopVoice() { 2823 /** 2824 * Drops audio that was received outside a reply stream and not played. Such audio (the greeting, late 2825 * frames of a reply) belongs to the session it came in: left in the buffer, the tail of the last answer was 2826 * played as the next session's greeting and the real greeting never was (merchant QA 2026-11-14, Woo Talk 2827 * after a language switch: 0.12 s played of a 2.3 s German greeting). Every end and every fresh start of a 2828 * Talk session runs this; a session prepared ahead starts with an empty buffer of its own. 2829 */ 2830 discardBufferedAudio() { 2831 this.audioChunks = []; 2832 this.pendingAudioPlay = false; 2833 if ( this.pendingAudioTimeout ) { 2834 clearTimeout( this.pendingAudioTimeout ); 2835 this.pendingAudioTimeout = null; 2836 } 2837 } 2838 2839 /** The Talk token request: the page context and, once, the hand-over handle of the session before. */ 2840 voiceTokenRequestBody() { 2841 const body = { context: this.getContext() }; 2842 if ( this.voiceContinuationHandle ) body.continuationHandle = this.voiceContinuationHandle; 2843 this.voiceContinuationHandle = null; 2844 return body; 2845 } 2846 2847 storeVoiceContinuation( handle ) { 2848 if ( typeof handle === 'string' && /^[A-Za-z0-9_-]{43}$/.test( handle ) ) this.voiceContinuationHandle = handle; 2849 } 2850 2851 /** Waits on an ended session's socket for the hand-over answer, keeps the handle and closes the socket. */ 2852 awaitVoiceHandoff( ws ) { 2853 const handoff = { cancelled: false, promise: null }; 2854 handoff.promise = new Promise( resolve => { 2855 let done = false; 2856 let timer = null; 2857 const finish = ( handle = null ) => { 2858 if ( done ) return; 2859 done = true; 2860 clearTimeout( timer ); 2861 ws.onmessage = () => {}; 2862 ws.onerror = () => {}; 2863 ws.onclose = () => {}; 2864 try { 2865 ws.close( 1000, 'User ended session' ); 2866 } catch ( error ) { 2867 debug( 'Hand-over socket close failed', error ); 2868 } 2869 if ( this.voiceHandoff === handoff ) this.voiceHandoff = null; 2870 // Talk was left while the answer was on its way: the handle is given back instead of kept. 2871 if ( handoff.cancelled ) this.sendVoiceContinuationRevoke( handle ); 2872 else this.storeVoiceContinuation( handle ); 2873 resolve(); 2874 }; 2875 timer = setTimeout( () => finish(), VOICE_HANDOFF_WAIT_MS ); 2876 ws.onmessage = event => { 2877 if ( typeof event.data !== 'string' ) return; 2878 let message = null; 2879 try { 2880 message = JSON.parse( event.data ); 2881 } catch ( error ) { 2882 return; 2883 } 2884 if ( message?.type === 'session.continuation' ) finish( message.payload?.continuationHandle ?? null ); 2885 }; 2886 ws.onerror = () => finish(); 2887 ws.onclose = () => finish(); 2888 }); 2889 this.voiceHandoff = handoff; 2890 // Nothing to carry over: Talk is prepared ahead as after any stop. 2891 void handoff.promise.then( () => { 2892 if ( ! this.voiceContinuationHandle && ! handoff.cancelled && this.currentTab === 'voice' && 2893 ! this.panel.classList.contains( 'chafwc-hidden' ) ) this.preloadVoiceConnection(); 2894 }); 2895 } 2896 2897 /** Leaving Talk (panel closed, Chat tab, page left): a handle that will not be used is given back. */ 2898 revokeVoiceContinuation( unloading = false ) { 2899 const handle = this.voiceContinuationHandle; 2900 this.voiceContinuationHandle = null; 2901 if ( this.voiceHandoff ) this.voiceHandoff.cancelled = true; 2902 this.sendVoiceContinuationRevoke( handle, unloading ); 2903 } 2904 2905 sendVoiceContinuationRevoke( handle, unloading = false ) { 2906 if ( typeof handle !== 'string' || ! handle ) return; 2907 const body = JSON.stringify({ continuationHandle: handle }); 2908 if ( unloading && typeof navigator !== 'undefined' && navigator.sendBeacon ) { 2909 // A beacon cannot carry the nonce header, so it goes in the address, which the route also reads. 2910 const url = `${config.restUrl}session/continuation/revoke?_wpnonce=${encodeURIComponent( config.nonce || '' )}`; 2911 if ( navigator.sendBeacon( url, new Blob( [ body ], { type: 'application/json' } ) ) ) return; 2912 } 2913 void this.apiRequest( 'session/continuation/revoke', { method: 'POST', body } ) 2914 .then( () => {}, () => debug( 'Voice continuation revoke request failed' ) ); 2915 } 2916 2917 /** 2918 * Ends Talk. With `continues` (language switch) the next Talk session goes on with this conversation: the 2919 * end message asks the backend for a handle, and only the socket stays open, for at most 2920 * VOICE_HANDOFF_WAIT_MS, to receive it. The stop button and every other end are as before. 2921 */ 2922 stopVoice( { continues = false } = {} ) { 2796 2923 debug( 'stopVoice called' ); 2797 2924 this.revealPendingVoiceAssistantTurnText(); … … 2806 2933 if ( this.websocket ) { 2807 2934 // Send end message first, then close gracefully 2808 if ( this.websocket.readyState === WebSocket.OPEN ) { 2935 const ws = this.websocket; 2936 const open = ws.readyState === WebSocket.OPEN; 2937 if ( open ) { 2809 2938 this.sendWsMessage( 'audio.stop', {} ); 2810 this.sendWsMessage( 'session.end', { reason: 'user_end' } ); 2811 } 2939 this.sendWsMessage( 'session.end', continues ? { reason: 'user_end', continuation: true } : { reason: 'user_end' } ); 2940 } 2941 this.websocket = null; 2812 2942 // Use code 1000 (normal closure) to avoid 1005 error 2813 this.websocket.close( 1000, 'User ended session');2814 this.websocket = null;2943 if ( continues && open ) this.awaitVoiceHandoff( ws ); 2944 else ws.close( 1000, 'User ended session' ); 2815 2945 } 2816 2946 this.stopAudioCapture(); 2817 2947 this.isConnecting = false; 2818 this.pendingAudioPlay = false; 2819 if ( this.pendingAudioTimeout ) { 2820 clearTimeout( this.pendingAudioTimeout ); 2821 this.pendingAudioTimeout = null; 2822 } 2948 this.discardBufferedAudio(); 2823 2949 this.setState( STATE.IDLE ); 2824 2950 … … 3316 3442 // Backend ended session due to inactivity — clean up and go idle 3317 3443 debug( 'Session timed out due to inactivity' ); 3444 this.storeVoiceContinuation( message.payload?.continuationHandle ); 3445 this.discardBufferedAudio(); 3318 3446 this.stopAudioCapture(); 3319 3447 if ( this.websocket ) { -
chat-assist-for-woocommerce/trunk/blocks/chat-assist/block.json
r3800390 r3801388 3 3 "apiVersion": 3, 4 4 "name": "chat-assist/chat-widget", 5 "version": "1.1. 1",5 "version": "1.1.2", 6 6 "title": "Chat Assist Widget", 7 7 "category": "widgets", -
chat-assist-for-woocommerce/trunk/includes/class-rest.php
r3800390 r3801388 166 166 'permission_callback' => array( $this, 'public_permission_check' ), 167 167 'args' => array( 168 'context' => array(168 'context' => array( 169 169 'type' => 'object', 170 170 'default' => array(), 171 ), 172 'continuationHandle' => array( 173 'type' => 'string', 174 'default' => '', 175 ), 176 ), 177 ) 178 ); 179 180 // Give back a Talk hand-over handle that will not be used (panel closed, Chat tab, page left). 181 register_rest_route( 182 $this->namespace, 183 '/session/continuation/revoke', 184 array( 185 'methods' => 'POST', 186 'callback' => array( $this, 'revoke_voice_continuation' ), 187 'permission_callback' => array( $this, 'public_permission_check' ), 188 'args' => array( 189 'continuationHandle' => array( 190 'type' => 'string', 191 'default' => '', 171 192 ), 172 193 ), … … 1162 1183 1163 1184 // Request short-lived session token from backend. 1164 $response = CHAFWC_Helpers::backend_request( 1165 'v1/sessions/token', 1166 array( 1167 'method' => 'POST', 1168 'body' => array( 1169 'origin' => home_url(), 1170 'userAgent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', 1171 'ipHash' => CHAFWC_Helpers::hash_ip( CHAFWC_Helpers::get_client_ip() ), 1172 'context' => $context, 1173 'shopContext' => $shop_context, 1174 'greetings' => $greeting_messages, 1175 ), 1176 ) 1185 $response = $this->request_voice_token( 1186 array( 1187 'origin' => home_url(), 1188 'userAgent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', 1189 'ipHash' => CHAFWC_Helpers::hash_ip( CHAFWC_Helpers::get_client_ip() ), 1190 'context' => $context, 1191 'shopContext' => $shop_context, 1192 'greetings' => $greeting_messages, 1193 ), 1194 $this->voice_continuation_handle( $request ) 1177 1195 ); 1178 1196 … … 1200 1218 ) 1201 1219 ); 1220 } 1221 1222 /** 1223 * The Talk hand-over handle of a request, or an empty string when it is missing or malformed. 1224 * 1225 * @param WP_REST_Request $request Request object. 1226 * @return string 1227 */ 1228 public function voice_continuation_handle( $request ) { 1229 $handle = $request->get_param( 'continuationHandle' ); 1230 return is_string( $handle ) && 1 === preg_match( '/^[A-Za-z0-9_-]{43}$/', $handle ) ? $handle : ''; 1231 } 1232 1233 /** 1234 * Ask the backend for a Talk token. With a hand-over handle the new session goes on with the conversation of 1235 * the one before it (after a language switch, inactivity or the time limit). A handle the backend no longer 1236 * accepts must not cost the shopper the call: the token is then requested once more without it. 1237 * 1238 * @param array $body Token request body. 1239 * @param string $handle Hand-over handle, or an empty string. 1240 * @return array|WP_Error 1241 */ 1242 public function request_voice_token( $body, $handle = '' ) { 1243 if ( '' !== $handle ) { 1244 $response = CHAFWC_Helpers::backend_request( 1245 'v1/sessions/token', 1246 array( 1247 'method' => 'POST', 1248 'body' => array_merge( $body, array( 'continuationHandle' => $handle ) ), 1249 ) 1250 ); 1251 $data = is_wp_error( $response ) ? $response->get_error_data() : null; 1252 if ( ! is_array( $data ) || ! isset( $data['status'] ) || 400 !== (int) $data['status'] ) { 1253 return $response; 1254 } 1255 } 1256 1257 return CHAFWC_Helpers::backend_request( 1258 'v1/sessions/token', 1259 array( 1260 'method' => 'POST', 1261 'body' => $body, 1262 ) 1263 ); 1264 } 1265 1266 /** 1267 * Give a Talk hand-over handle back to the backend. Always answers success: the widget has nothing to do 1268 * with the outcome, and an unknown handle is not an error. 1269 * 1270 * @param WP_REST_Request $request Request object. 1271 * @return WP_REST_Response 1272 */ 1273 public function revoke_voice_continuation( $request ) { 1274 $handle = $this->voice_continuation_handle( $request ); 1275 if ( '' !== $handle && CHAFWC_Helpers::has_backend() ) { 1276 CHAFWC_Helpers::backend_request( 1277 'v1/sessions/continuation/revoke', 1278 array( 1279 'method' => 'POST', 1280 'body' => array( 'continuationHandle' => $handle ), 1281 ) 1282 ); 1283 } 1284 1285 return rest_ensure_response( array( 'success' => true ) ); 1202 1286 } 1203 1287 -
chat-assist-for-woocommerce/trunk/readme.txt
r3800390 r3801388 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1.1. 17 Stable tag: 1.1.2 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 115 115 == Changelog == 116 116 117 = 1.1.2 = 118 * Talk continues the conversation after a language switch and after the inactivity notice: the assistant still knows the products the shopper asked about. 119 117 120 = 1.1.1 = 118 121 * Keep paid chat sessions out of the free fallback: the request limit is now a true per-minute limit, and AI chat streaming counts separately. … … 158 161 == Upgrade Notice == 159 162 163 = 1.1.2 = 164 Talk keeps the conversation after a language switch and after a pause. 165 160 166 = 1.1.1 = 161 167 Keeps paid chat sessions from falling back to free mode, fixes Talk audio and first words, adds an AI notice and shows more of the widget in the shopper's language. -
chat-assist-for-woocommerce/trunk/woo-chat-assist.php
r3800390 r3801388 4 4 * Plugin URI: https://qonvor.com/docs/ 5 5 * Description: Help shoppers find products, track orders, and get answers in your WooCommerce store. Free chat included, with paid text and voice options. 6 * Version: 1.1. 16 * Version: 1.1.2 7 7 * Author: Space Lab Apps Pty Ltd 8 8 * Author URI: https://qonvor.com … … 22 22 defined( 'ABSPATH' ) || exit; 23 23 24 define( 'CHAFWC_VERSION', '1.1. 1' );24 define( 'CHAFWC_VERSION', '1.1.2' ); 25 25 define( 'CHAFWC_PLUGIN_FILE', __FILE__ ); 26 26 define( 'CHAFWC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
Note:
See TracChangeset
for help on using the changeset viewer.