Skip to content

Commit dcf300f

Browse files
feat: enhance AI assistant functionality with stop response feature
- Updated CSS for the send button to include a stop mode with new styles. - Added JavaScript logic to handle aborting ongoing requests and toggling button states. - Introduced new SVG icons for send and stop actions. - Improved user experience by showing/hiding the stop button during interactions.
1 parent 4befb4a commit dcf300f

4 files changed

Lines changed: 87 additions & 5 deletions

File tree

hlx_statics/blocks/ai-assistant/ai-assistant.css

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,9 @@
298298
padding: 0;
299299
border: none;
300300
border-radius: 16px;
301-
background: #e8e8e8;
301+
background: #e9e9e9;
302+
font-family: "adobe-clean", "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Trebuchet MS", "Lucida Grande", sans-serif;
303+
font-weight: 700;
302304
color: #292929;
303305
display: flex;
304306
align-items: center;
@@ -317,6 +319,16 @@
317319
opacity: 0.5;
318320
cursor: not-allowed;
319321
}
322+
323+
&.stop-mode {
324+
width: auto;
325+
padding: 0 12px;
326+
gap: 6px;
327+
328+
span {
329+
height: 20px;
330+
}
331+
}
320332
}
321333
}
322334

hlx_statics/blocks/ai-assistant/ai-assistant.js

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const SUGGESTED_QUESTIONS = [
3838
];
3939
const 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

hlx_statics/icons/send-message.svg

Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)