Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 32 additions & 24 deletions hlx_statics/blocks/ai-assistant/ai-assistant.css
Original file line number Diff line number Diff line change
Expand Up @@ -238,20 +238,20 @@
* MARK: Input Section
*/
.chat-window .chat-window-input-section {
display: grid;
grid-template-areas:
"textarea textarea"
"disclaimer-text send-button";
grid-template-columns: 1fr 72px;
grid-template-rows: auto auto;
gap: 4px 12px;
display: flex;
flex-direction: column;
gap: 4px;
padding: 0 24px 24px 24px;

.chat-textarea-wrapper {
position: relative;
}

textarea {
grid-area: textarea;
width: 100%;
min-height: 96px;
max-height: 120px;
padding: 12px;
padding: 12px 12px 48px 12px;
border: 2px solid #dadada;
border-radius: 8px;
font-family: "adobe-clean", "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Trebuchet MS", "Lucida Grande", sans-serif;
Expand All @@ -261,6 +261,7 @@
background: #ffffff;
resize: none;
overflow-y: auto;
box-sizing: border-box;

&:focus {
outline: none;
Expand All @@ -273,8 +274,6 @@
}

.chat-disclaimer-text {
grid-area: disclaimer-text;
align-self: center;
font-size: 14px;
line-height: 21px;
color: #222222;
Expand All @@ -291,35 +290,44 @@
}

.chat-send-button {
grid-area: send-button;
align-self: center;
width: 100%;
position: absolute;
bottom: 8px;
right: 8px;
width: 72px;
height: 32px;
padding: 0;
border: none;
border-radius: 16px;
background: #3b63fb;
background: #e9e9e9;
font-family: "adobe-clean", "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Trebuchet MS", "Lucida Grande", sans-serif;
font-weight: 700;
color: #292929;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;

&:hover {
background: #2952e0;
&:hover:not(:disabled) {
background: #d0d0d0;
}

&:active {
background: #1f42c7;
&:active:not(:disabled) {
background: #b8b8b8;
}

&:disabled {
background: #e1e1e1;
opacity: 0.5;
cursor: not-allowed;
}

img {
width: 20px;
height: 20px;
filter: brightness(0) invert(1);
&.stop-mode {
width: auto;
padding: 0 12px;
gap: 6px;

span {
height: 20px;
}
}
}
}
Expand Down
98 changes: 86 additions & 12 deletions hlx_statics/blocks/ai-assistant/ai-assistant.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const SUGGESTED_QUESTIONS = [
];
const GENERIC_ERROR_MESSAGE =
"Sorry, I encountered an error. Please try again later.";
const SEND_ICON_SRC = "/hlx_statics/icons/send-message.svg";
const STOP_ICON_SRC = "/hlx_statics/icons/stop-response.svg";
// #endregion

// #region ChatBubble
Expand Down Expand Up @@ -470,6 +472,7 @@ class AiApiClient {
}
this.baseUrl = baseUrl;
this.apiKey = apiKey;
this.abortController = null;
}

/**
Expand All @@ -493,6 +496,8 @@ class AiApiClient {
onComplete = () => {},
onError = () => {},
}) {
this.abortController = new AbortController();
const { signal } = this.abortController;
try {
const response = await fetch(
`${this.baseUrl}${AiApiClient.STREAMING_ENDPOINT}`,
Expand All @@ -503,6 +508,7 @@ class AiApiClient {
"X-Api-Key": this.apiKey,
},
body: JSON.stringify(body),
signal,
},
);

Expand Down Expand Up @@ -566,8 +572,21 @@ class AiApiClient {
}
}
} catch (error) {
if (error.name === "AbortError") {
onComplete();
return;
}
console.error("[AiApiClient] Stream request error:", error);
onError(error);
} finally {
this.abortController = null;
}
}

abort() {
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
}

Expand Down Expand Up @@ -658,6 +677,24 @@ const createChatWindowHeader = () => {
return chatWindowHeader;
};

const showStopButton = () => {
const btn = ELEMENTS.CHAT_SEND_BUTTON;
btn.querySelector("img").src = STOP_ICON_SRC;
btn.querySelector("span").textContent = "Stop response";
btn.classList.add("stop-mode");
btn.setAttribute("aria-label", "Stop response");
btn.disabled = false;
};

const hideStopButton = () => {
const btn = ELEMENTS.CHAT_SEND_BUTTON;
btn.querySelector("img").src = SEND_ICON_SRC;
btn.querySelector("span").textContent = "";
btn.classList.remove("stop-mode");
btn.setAttribute("aria-label", "Send message");
btn.disabled = ELEMENTS.CHAT_TEXTAREA.value.trim() === "";
};

/**
* Creates the input section
*/
Expand All @@ -675,13 +712,44 @@ const createInputSection = () => {
type: "button",
"aria-label": "Send message",
});
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="#ffffff"/></svg>`;
const sendButtonIcon = createTag("img", {
src: SEND_ICON_SRC,
alt: "",
"aria-hidden": true,
width: "20",
height: "20",
});
const sendButtonLabel = createTag("span");
sendButton.appendChild(sendButtonIcon);
sendButton.appendChild(sendButtonLabel);
sendButton.disabled = true;

inputSection.appendChild(textarea);
const textareaWrapper = createTag("div", { class: "chat-textarea-wrapper" });
textareaWrapper.appendChild(textarea);
textareaWrapper.appendChild(sendButton);

inputSection.appendChild(textareaWrapper);
inputSection.appendChild(disclaimerText);
inputSection.appendChild(sendButton);
ELEMENTS.CHAT_SEND_BUTTON = sendButton;
ELEMENTS.CHAT_TEXTAREA = textarea;

sendButton.addEventListener("click", () => {
if (sendButton.classList.contains("stop-mode")) {
aiApiClient.abort();
} else {
handleUserQuery();
}
});
textarea.addEventListener("input", () => {
sendButton.disabled = textarea.value.trim() === "";
});
textarea.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleUserQuery();
}
});

return inputSection;
};

Expand Down Expand Up @@ -737,6 +805,7 @@ const showSuggestedQuestions = () => {
el.classList.remove("animate-fade-in");
requestAnimationFrame(() => {
el.classList.add("animate-fade-in");
el.scrollIntoView({ behavior: "smooth" });
});
}
};
Expand Down Expand Up @@ -820,6 +889,7 @@ const handleUserQuery = async (messageContentOverride) => {
if (!messageContentOverride) {
messageContent = ELEMENTS.CHAT_TEXTAREA.value.trim();
ELEMENTS.CHAT_TEXTAREA.value = "";
ELEMENTS.CHAT_TEXTAREA.dispatchEvent(new Event("input"));
}

if (!messageContent) {
Expand Down Expand Up @@ -850,6 +920,8 @@ const handleUserQuery = async (messageContentOverride) => {
let responseContent = "";
let accumulatedReferences = [];

showStopButton();

await aiApiClient.query({
query: messageContent,
context: queryContext,
Expand Down Expand Up @@ -894,15 +966,24 @@ const handleUserQuery = async (messageContentOverride) => {
}
},
onComplete: () => {
hideStopButton();
if (!responseContent) {
targetBubble.hideThinking();
responseContent = "_Response stopped by user._";
targetBubble.updateContent(responseContent);
} else {
targetBubble.hideStreamingCursor();
targetBubble.showCopyButton();
}
chatHistory.updateLast({
content: responseContent,
references: accumulatedReferences,
});
targetBubble.showCopyButton();
targetBubble.scrollIntoView();
window.setTimeout(showSuggestedQuestions, suggestedQuestionsDelayMs);
},
onError: (error) => {
hideStopButton();
// TODO: Log error somehow somewhere?
console.error("[AI Assistant] Error:", error);
showErrorMessage();
Expand Down Expand Up @@ -1056,12 +1137,5 @@ export default async function decorate(block) {
minimizeChatWindow,
);
ELEMENTS.CHAT_WINDOW_CLOSE_BUTTON.addEventListener("click", closeChatWindow);
ELEMENTS.CHAT_SEND_BUTTON.addEventListener("click", handleUserQuery);
ELEMENTS.CHAT_TEXTAREA.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleUserQuery();
}
});
}
// #endregion
// #endregion
3 changes: 3 additions & 0 deletions hlx_statics/icons/send-message.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions hlx_statics/icons/stop-response.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading