Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion head.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>


<script src="/hlx_statics/scripts/scripts.js" type="module" crossorigin="use-credentials"></script>

<link rel="stylesheet" href="/hlx_statics/styles/styles.css"/>
Expand Down
35 changes: 33 additions & 2 deletions hlx_statics/blocks/ai-assistant/ai-assistant.css
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
*/
.ai-assistant-panel .chat-button {
--background-color: #ffffff;
--transition-delay: calc(var(--opening-transition-duration) * 0.9);

position: relative;
width: var(--button-size);
Expand All @@ -54,9 +55,11 @@
var(--ai-border-linear-gradient) border-box;
align-self: flex-end;
pointer-events: auto;
transform: scale(1);
visibility: visible;
transition: visibility 0s linear
calc(var(--opening-transition-duration) * 0.9);
transition:
visibility 0s linear var(--transition-delay),
transform 0.15s cubic-bezier(0, 0, 0.4, 1) var(--transition-delay);

&:hover {
--background-color: #f0f0f0;
Expand All @@ -65,9 +68,36 @@
&.hidden {
visibility: hidden;
transition: visibility 0s linear;
transform: scale(0.9);
}
}

.ai-assistant-panel .chat-button .chat-button-badge {
position: absolute;
top: -10px;
right: -10px;
background: var(--ai-border-linear-gradient);
color: #fff;
font-size: 9px;
font-weight: 800;
letter-spacing: 0.3px;
padding: 2px 6px;
border-radius: 8px;
border: 2px solid #fff;
pointer-events: none;
line-height: 1.4;
opacity: 1;
transform: scale(1);
transition:
opacity 0.15s cubic-bezier(0, 0, 0.4, 1),
transform 0.15s cubic-bezier(0, 0, 0.4, 1);
}

.ai-assistant-panel .chat-button .chat-button-badge.hidden {
opacity: 0;
transform: scale(0.6);
}

/*
* MARK: Chat Window
*/
Expand Down Expand Up @@ -422,6 +452,7 @@
justify-content: center;
padding: 24px;
box-sizing: border-box;
z-index: 11;
}

.ai-assistant-panel .chat-window .chat-window-dialog-card {
Expand Down
2 changes: 1 addition & 1 deletion hlx_statics/blocks/ai-assistant/ai-assistant_api-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ export class AiApiClient {
callbacks = {},
}) {
const defaultSystemPrompt = `
Use markdown formatting for the response.
Use markdown formatting and codeblocks for the response.
`;

/** @type {RequestBody} */
Expand Down
54 changes: 53 additions & 1 deletion hlx_statics/blocks/ai-assistant/ai-assistant_chat-bubble.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// @ts-check
import { createTag } from "../../scripts/lib-adobeio.js";
import decoratePreformattedCode from "../../components/code.js";
import { ensurePrismLoaded } from "../../scripts/prism-loader.js";
import { aiApiClient } from "./ai-assistant_api-client.js";
import { chatHistory } from "./ai-assistant_chat-history.js";
import { createAiAvatar } from "./ai-assistant_chat-ui.js";
Expand Down Expand Up @@ -74,6 +76,8 @@ export class ChatBubble {
bubble.dataset.messageId = this.id;
// If we have an ID already (e.g., restored from history), append the actions
contentElement.appendChild(this._actionsContainer);
// Restored-history bubbles never call completeBubble(), so decorate here.
this.#_decorateCodeBlocks(contentElement);
}
// Otherwise, the actions will be appended when completeBubble is called
}
Expand Down Expand Up @@ -301,7 +305,7 @@ export class ChatBubble {
}

/**
* Adds the missing actions to an AI bubble
* Final processing that needs to be done after streaming finishes.
*/
completeBubble() {
if (this.source !== "ai" || !this._actionsContainer) return;
Expand All @@ -310,6 +314,54 @@ export class ChatBubble {

const contentElement = this.element.querySelector(".chat-bubble-content");
contentElement?.appendChild(this._actionsContainer);
if (contentElement) {
this.#_decorateCodeBlocks(contentElement);
}
}

/**
* @param {Element} contentElement - The `.chat-bubble-content` element
*/
#_decorateCodeBlocks(contentElement) {
const preBlocks = contentElement.querySelectorAll("pre");
if (!preBlocks.length) return;

let decoratedAny = false;
preBlocks.forEach((pre) => {
// decoratePreformattedCode dereferences a <code> child unconditionally.
if (!pre.querySelector("code")) return;
// The chat panel is narrow, so use the icon-only copy button.
pre.classList.add("copy-condensed");
decoratePreformattedCode(pre);
decoratedAny = true;
});

if (!decoratedAny) return;

ensurePrismLoaded().then(() => {
// @ts-expect-error - Prism is not on the Window type
window.Prism?.highlightAllUnder?.(contentElement);
});
}

/**
* Recomputes Prism's line-number row heights for every decorated code block
* inside a container.
* This is required to correctly align line numbers after restoring history.
* @param {Element | null | undefined} container
*/
static resizeCodeBlockLineNumbers(container) {
const preBlocks = container?.querySelectorAll("pre.line-numbers");
if (!preBlocks?.length) return;

ensurePrismLoaded().then(() => {
// @ts-expect-error - Prism is not on the Window type
const resize = window.Prism?.plugins?.lineNumbers?.resize;
if (typeof resize !== "function") return;
preBlocks.forEach((pre) => {
resize(pre);
});
});
}

/**
Expand Down
77 changes: 69 additions & 8 deletions hlx_statics/blocks/ai-assistant/ai-assistant_chat-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import {
CHAT_BUTTON_LABEL_OPEN,
ELEMENTS,
GENERIC_ERROR_MESSAGE,
INITIAL_SUGGESTED_QUESTIONS,
SEND_ICON_SRC,
STOP_ICON_SRC,
} from "./ai-assistant_constants.js";
import {
getCollectionsQuestions,
hideSuggestedQuestions,
parseAiSuggestedQuestions,
showSuggestedQuestions,
Expand Down Expand Up @@ -51,6 +51,7 @@ export const onUserScroll = (event) => {
*/
const sendInitialMessages = ({ delay = 250 } = {}) => {
hideSuggestedQuestions();
updateSuggestedQuestions(INITIAL_SUGGESTED_QUESTIONS);
window.setTimeout(() => {
sendMessage({
content: "Hello, welcome to Adobe Developer Website!",
Expand Down Expand Up @@ -81,6 +82,26 @@ export const openChatWindow = () => {
ELEMENTS.CHAT_BUTTON.ariaLabel = CHAT_BUTTON_LABEL_MINIMIZE;
ELEMENTS.CHAT_WINDOW.classList.add("show");
ELEMENTS.CHAT_BUTTON.classList.add("hidden");
ELEMENTS.CHAT_BUTTON_BADGE?.classList.add("hidden");

// Code blocks in restored-history bubbles are decorated while the window is
// hidden (scaled to 0), so Prism's line-number rows collapse onto line 1.
// Recompute them once the open transition settles and the window has real
// layout. transitionend is the precise signal; the timeout is a fallback in
// case the transition is skipped (e.g. reduced motion / 0s duration).
const chatWindow = ELEMENTS.CHAT_WINDOW;
let fixedLineNumbers = false;
const fixLineNumbers = (event) => {
// transitionend bubbles, so ignore descendant transitions and only react to
// the window's own transform settling (or the timeout, which has no event).
if (event && (event.target !== chatWindow || event.propertyName !== "transform")) return;
if (fixedLineNumbers) return;
fixedLineNumbers = true;
chatWindow.removeEventListener("transitionend", fixLineNumbers);
ChatBubble.resizeCodeBlockLineNumbers(ELEMENTS.CHAT_WINDOW_CONTENT);
};
chatWindow.addEventListener("transitionend", fixLineNumbers);
window.setTimeout(fixLineNumbers, 400);

// Initial messages
if (chatHistory.isEmpty()) {
Expand All @@ -96,6 +117,44 @@ export const minimizeChatWindow = () => {
ELEMENTS.CHAT_BUTTON.ariaLabel = CHAT_BUTTON_LABEL_OPEN;
ELEMENTS.CHAT_BUTTON?.classList.remove("hidden");
ELEMENTS.CHAT_WINDOW?.classList.remove("show");

restoreBadgeAfterClose();
};

/**
* Restores the beta badge after the chat window's collapse (transform) transition
* completes.
*/
const restoreBadgeAfterClose = () => {
const chatWindow = ELEMENTS.CHAT_WINDOW;
if (!chatWindow) return;

// With reduced motion the window collapses instantly (no transform transition),
// so `transitionend` may never fire — restore the badge right away instead.
const prefersReducedMotion = window.matchMedia?.(
"(prefers-reduced-motion: reduce)",
)?.matches;
if (prefersReducedMotion) {
if (!chatWindow.classList.contains("show")) {
ELEMENTS.CHAT_BUTTON_BADGE?.classList.remove("hidden");
}
return;
}

/** @param {TransitionEvent} event */
const onTransitionEnd = (event) => {
// The window animates both `transform` and `visibility`; only react to the
// transform transition, which is the visible collapse.
if (event.target !== chatWindow || event.propertyName !== "transform") {
return;
}
chatWindow.removeEventListener("transitionend", onTransitionEnd);
if (!chatWindow.classList.contains("show")) {
ELEMENTS.CHAT_BUTTON_BADGE?.classList.remove("hidden");
}
};

chatWindow.addEventListener("transitionend", onTransitionEnd);
};

export const clearConversation = () => {
Expand All @@ -122,7 +181,7 @@ export const toggleChatWindow = () => {
* Falls back to static questions on any error or parse failure.
*/
export const fetchAiSuggestedQuestions = async () => {
const query = `Please suggest 2 follow-up questions based on our conversation to make the users.`;
const query = `Please suggest 2 follow-up questions based on our conversation to make the users happy.`;
const systemPrompt = `
Structured questions format:
---question---
Expand All @@ -142,14 +201,14 @@ export const fetchAiSuggestedQuestions = async () => {
if (parsed.length > 0) {
updateSuggestedQuestions(parsed);
} else {
updateSuggestedQuestions(await getCollectionsQuestions());
updateSuggestedQuestions(INITIAL_SUGGESTED_QUESTIONS);
}
} catch (error) {
console.warn(
"[AI Assistant] Failed to fetch AI suggested questions:",
error,
);
updateSuggestedQuestions(await getCollectionsQuestions());
updateSuggestedQuestions(INITIAL_SUGGESTED_QUESTIONS);
}
};

Expand Down Expand Up @@ -291,7 +350,7 @@ export const handleUserQuery = async (
targetBubble.hideThinking();
responseContent = "_Response stopped by user._";
targetBubble.updateContent(responseContent);
updateSuggestedQuestions(await getCollectionsQuestions());
updateSuggestedQuestions(INITIAL_SUGGESTED_QUESTIONS);
window.setTimeout(
() =>
showSuggestedQuestions({ shouldScrollIntoView: !userScrolledUp }),
Expand Down Expand Up @@ -322,7 +381,7 @@ export const handleUserQuery = async (
// TODO: Log error somehow somewhere?
console.error("[AI Assistant] Error:", error);
showErrorMessage();
getCollectionsQuestions().then(updateSuggestedQuestions);
updateSuggestedQuestions(INITIAL_SUGGESTED_QUESTIONS);
window.setTimeout(
() =>
showSuggestedQuestions({ shouldScrollIntoView: !userScrolledUp }),
Expand Down Expand Up @@ -415,9 +474,11 @@ export const restoreChatHistory = async () => {
}
const lastMessage = chatHistory.getAll().pop();
if (lastMessage?.source === "ai") {
updateSuggestedQuestions(await getCollectionsQuestions());
updateSuggestedQuestions(
chatHistory.getSuggestedQuestions() ?? INITIAL_SUGGESTED_QUESTIONS,
);
showSuggestedQuestions();
} else {
hideSuggestedQuestions();
}
};
};
Loading
Loading