@@ -38,6 +38,8 @@ const SUGGESTED_QUESTIONS = [
3838] ;
3939const GENERIC_ERROR_MESSAGE =
4040 "Sorry, I encountered an error. Please try again later." ;
41+ const SEND_ICON_SRC = "/hlx_statics/icons/send-message.svg" ;
42+ const STOP_ICON_SRC = "/hlx_statics/icons/stop-response.svg" ;
4143// #endregion
4244
4345// #region ChatBubble
@@ -470,6 +472,7 @@ class AiApiClient {
470472 }
471473 this . baseUrl = baseUrl ;
472474 this . apiKey = apiKey ;
475+ this . abortController = null ;
473476 }
474477
475478 /**
@@ -493,6 +496,8 @@ class AiApiClient {
493496 onComplete = ( ) => { } ,
494497 onError = ( ) => { } ,
495498 } ) {
499+ this . abortController = new AbortController ( ) ;
500+ const { signal } = this . abortController ;
496501 try {
497502 const response = await fetch (
498503 `${ this . baseUrl } ${ AiApiClient . STREAMING_ENDPOINT } ` ,
@@ -503,6 +508,7 @@ class AiApiClient {
503508 "X-Api-Key" : this . apiKey ,
504509 } ,
505510 body : JSON . stringify ( body ) ,
511+ signal,
506512 } ,
507513 ) ;
508514
@@ -566,8 +572,21 @@ class AiApiClient {
566572 }
567573 }
568574 } catch ( error ) {
575+ if ( error . name === "AbortError" ) {
576+ onComplete ( ) ;
577+ return ;
578+ }
569579 console . error ( "[AiApiClient] Stream request error:" , error ) ;
570580 onError ( error ) ;
581+ } finally {
582+ this . abortController = null ;
583+ }
584+ }
585+
586+ abort ( ) {
587+ if ( this . abortController ) {
588+ this . abortController . abort ( ) ;
589+ this . abortController = null ;
571590 }
572591 }
573592
@@ -658,6 +677,24 @@ const createChatWindowHeader = () => {
658677 return chatWindowHeader ;
659678} ;
660679
680+ const showStopButton = ( ) => {
681+ const btn = ELEMENTS . CHAT_SEND_BUTTON ;
682+ btn . querySelector ( "img" ) . src = STOP_ICON_SRC ;
683+ btn . querySelector ( "span" ) . textContent = "Stop response" ;
684+ btn . classList . add ( "stop-mode" ) ;
685+ btn . setAttribute ( "aria-label" , "Stop response" ) ;
686+ btn . disabled = false ;
687+ } ;
688+
689+ const hideStopButton = ( ) => {
690+ const btn = ELEMENTS . CHAT_SEND_BUTTON ;
691+ btn . querySelector ( "img" ) . src = SEND_ICON_SRC ;
692+ btn . querySelector ( "span" ) . textContent = "" ;
693+ btn . classList . remove ( "stop-mode" ) ;
694+ btn . setAttribute ( "aria-label" , "Send message" ) ;
695+ btn . disabled = ELEMENTS . CHAT_TEXTAREA . value . trim ( ) === "" ;
696+ } ;
697+
661698/**
662699 * Creates the input section
663700 */
@@ -675,7 +712,16 @@ const createInputSection = () => {
675712 type : "button" ,
676713 "aria-label" : "Send message" ,
677714 } ) ;
678- sendButton . innerHTML = `<svg width="20" height="20" viewBox="0 0 20 20" focusable="false" aria-hidden="true" role="img" class="spectrum-Icon spectrum-Icon--sizeXL"><path d="M18.6485 9.97369C18.6482 9.67918 18.4769 9.41125 18.2059 9.29075L4.05752 2.93301C3.80133 2.81769 3.50129 2.85602 3.28171 3.03141C3.06178 3.20784 2.95889 3.49165 3.01516 3.76752L4.28678 10.0082L3.06488 16.2386C3.0162 16.4854 3.09492 16.7382 3.27031 16.9136C3.29068 16.9339 3.31278 16.9533 3.33522 16.9716C3.55619 17.1456 3.85519 17.1822 4.11069 17.0662L18.2086 10.658C18.4773 10.5358 18.6489 10.2682 18.6485 9.97369ZM14.406 9.22735L5.66439 9.25398L4.77705 4.90103L14.406 9.22735ZM4.81711 15.0974L5.6694 10.7531L14.4323 10.7265L4.81711 15.0974Z" fill="currentColor"/></svg>` ;
715+ const sendButtonIcon = createTag ( "img" , {
716+ src : SEND_ICON_SRC ,
717+ alt : "" ,
718+ "aria-hidden" : true ,
719+ width : "20" ,
720+ height : "20" ,
721+ } ) ;
722+ const sendButtonLabel = createTag ( "span" ) ;
723+ sendButton . appendChild ( sendButtonIcon ) ;
724+ sendButton . appendChild ( sendButtonLabel ) ;
679725 sendButton . disabled = true ;
680726
681727 const textareaWrapper = createTag ( "div" , { class : "chat-textarea-wrapper" } ) ;
@@ -687,7 +733,13 @@ const createInputSection = () => {
687733 ELEMENTS . CHAT_SEND_BUTTON = sendButton ;
688734 ELEMENTS . CHAT_TEXTAREA = textarea ;
689735
690- sendButton . addEventListener ( "click" , handleUserQuery ) ;
736+ sendButton . addEventListener ( "click" , ( ) => {
737+ if ( sendButton . classList . contains ( "stop-mode" ) ) {
738+ aiApiClient . abort ( ) ;
739+ } else {
740+ handleUserQuery ( ) ;
741+ }
742+ } ) ;
691743 textarea . addEventListener ( "input" , ( ) => {
692744 sendButton . disabled = textarea . value . trim ( ) === "" ;
693745 } ) ;
@@ -753,6 +805,7 @@ const showSuggestedQuestions = () => {
753805 el . classList . remove ( "animate-fade-in" ) ;
754806 requestAnimationFrame ( ( ) => {
755807 el . classList . add ( "animate-fade-in" ) ;
808+ el . scrollIntoView ( { behavior : "smooth" } ) ;
756809 } ) ;
757810 }
758811} ;
@@ -867,6 +920,8 @@ const handleUserQuery = async (messageContentOverride) => {
867920 let responseContent = "" ;
868921 let accumulatedReferences = [ ] ;
869922
923+ showStopButton ( ) ;
924+
870925 await aiApiClient . query ( {
871926 query : messageContent ,
872927 context : queryContext ,
@@ -911,15 +966,24 @@ const handleUserQuery = async (messageContentOverride) => {
911966 }
912967 } ,
913968 onComplete : ( ) => {
969+ hideStopButton ( ) ;
970+ if ( ! responseContent ) {
971+ targetBubble . hideThinking ( ) ;
972+ responseContent = "_Response stopped by user._" ;
973+ targetBubble . updateContent ( responseContent ) ;
974+ } else {
975+ targetBubble . hideStreamingCursor ( ) ;
976+ targetBubble . showCopyButton ( ) ;
977+ }
914978 chatHistory . updateLast ( {
915979 content : responseContent ,
916980 references : accumulatedReferences ,
917981 } ) ;
918- targetBubble . showCopyButton ( ) ;
919982 targetBubble . scrollIntoView ( ) ;
920983 window . setTimeout ( showSuggestedQuestions , suggestedQuestionsDelayMs ) ;
921984 } ,
922985 onError : ( error ) => {
986+ hideStopButton ( ) ;
923987 // TODO: Log error somehow somewhere?
924988 console . error ( "[AI Assistant] Error:" , error ) ;
925989 showErrorMessage ( ) ;
@@ -1074,4 +1138,4 @@ export default async function decorate(block) {
10741138 ) ;
10751139 ELEMENTS . CHAT_WINDOW_CLOSE_BUTTON . addEventListener ( "click" , closeChatWindow ) ;
10761140}
1077- // #endregion
1141+ // #endregion
0 commit comments