commit cd2cdbf11b76e330be4d619c8a85de4af8350cf0 parent e8d015b620ce2a5f6bbce9e68f2e9e45202b8572 Author: Nick Grato <ngrato@gmail.com> Date: Thu, 18 Dec 2025 17:59:24 +0000 Bug 2005637 - Create sidebar to LLM and back pipeline for AI window r=pdahiya,ai-frontend-reviewers,ai-models-reviewers,tzhang Create a end to end implementation of a user input to LLM inference and display response via streaming in new ai window sidebar. Differential Revision: https://phabricator.services.mozilla.com/D276091 Diffstat:
19 files changed, 552 insertions(+), 73 deletions(-)
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js @@ -2249,10 +2249,13 @@ pref("browser.ml.smartAssist.model", ""); pref("browser.ml.smartAssist.overrideNewTab", false); // AI Window Feature -pref("browser.aiwindow.enabled", false); +pref("browser.aiwindow.apiKey", ''); pref("browser.aiwindow.chatStore.loglevel", "Error"); +pref("browser.aiwindow.enabled", false); +pref("browser.aiwindow.endpoint", "https://mlpa-prod-prod-mozilla.global.ssl.fastly.net/v1"); pref("browser.aiwindow.insights", false); pref("browser.aiwindow.insightsLogLevel", "Warn"); +pref("browser.aiwindow.model", ""); // Block insecure active content on https pages pref("security.mixed_content.block_active_content", true); diff --git a/browser/base/content/test/static/browser_all_files_referenced.js b/browser/base/content/test/static/browser_all_files_referenced.js @@ -339,19 +339,10 @@ var allowlist = [ { file: "moz-src:///browser/components/aiwindow/ui/modules/ChatStore.sys.mjs", }, - // Bug 2003598 - Add Chat service with fetch with history (backed out due to unused file) - { - file: "moz-src:///browser/components/aiwindow/models/Chat.sys.mjs", - }, // Bug 2002840 - add function to return real time info injection message & tests (backed out due to unused file) { file: "moz-src:///browser/components/aiwindow/models/ChatUtils.sys.mjs", }, - // Bug 2003328 - Implement createOpenAIEngine and prompt rendering (backed out due to unused file) - // Bug 2003832 - Change .mjs files to .sys.mjs (backed out due to unused file) - { - file: "moz-src:///browser/components/aiwindow/models/Utils.sys.mjs", - }, // Bug 2003623 - Add assistant system prompt { file: "moz-src:///browser/components/aiwindow/models/prompts/AssistantPrompts.sys.mjs", diff --git a/browser/components/aiwindow/models/Chat.sys.mjs b/browser/components/aiwindow/models/Chat.sys.mjs @@ -46,7 +46,7 @@ export const Chat = { const engineInstance = await openAIEngine.build(); // Note FXA token fetching disabled for now - this is still in progress // We can flip this switch on when more realiable - // const fxAccountToken = await this._getFxAccountToken(); + const fxAccountToken = await this._getFxAccountToken(); // We'll mutate a local copy of the thread as we loop let convo = Array.isArray(messages) ? [...messages] : []; @@ -55,7 +55,7 @@ export const Chat = { const streamModelResponse = () => engineInstance.runWithGenerator({ streamOptions: { enabled: true }, - // fxAccountToken, + fxAccountToken, tool_choice: "auto", // tools: Add your tools configuration here, args: convo, diff --git a/browser/components/aiwindow/ui/actors/AIChatContentChild.sys.mjs b/browser/components/aiwindow/ui/actors/AIChatContentChild.sys.mjs @@ -9,25 +9,46 @@ ChromeUtils.defineESModuleGetters(lazy, {}); * Represents a child actor for getting page data from the browser. */ export class AIChatContentChild extends JSWindowActorChild { + static #EVENT_MAPPINGS = { + "AIChatContent:DispatchMessage": { + event: "aiChatContentActor:message", + }, + }; + async receiveMessage(message) { - switch (message.name) { - case "AIChatContent:DispatchAIResponse": - return this.dispatchAIResponseToChatContent(message.data.response); + const mapping = AIChatContentChild.#EVENT_MAPPINGS[message.name]; + + if (!mapping) { + console.warn( + `AIChatContentChild received unknown message: ${message.name}` + ); + return undefined; } - return undefined; + + const payload = message.data; + return this.#dispatchToChatContent(mapping.event, payload); } - async dispatchAIResponseToChatContent(response) { + #dispatchToChatContent(eventName, payload) { try { const chatContent = this.document.querySelector("ai-chat-content"); - const event = new this.contentWindow.CustomEvent("ai-response", { - detail: response, + + if (!chatContent) { + console.error(`No ai-chat-content element found for ${eventName}`); + return false; + } + + const clonedPayload = Cu.cloneInto(payload, this.contentWindow); + + const event = new this.contentWindow.CustomEvent(eventName, { + detail: clonedPayload, bubbles: true, }); + chatContent.dispatchEvent(event); - return false; + return true; } catch (error) { - console.error("Error dispatching AI response to chat content:", error); + console.error(`Error dispatching ${eventName} to chat content:`, error); return false; } } diff --git a/browser/components/aiwindow/ui/actors/AIChatContentParent.sys.mjs b/browser/components/aiwindow/ui/actors/AIChatContentParent.sys.mjs @@ -6,9 +6,7 @@ * JSWindowActor to pass data between AIChatContent singleton and content pages. */ export class AIChatContentParent extends JSWindowActorParent { - async dispatchAIResponse(response) { - return this.sendQuery("AIChatContent:DispatchAIResponse", { - response, - }); + async dispatchMessageToChatContent(response) { + return this.sendQuery("AIChatContent:DispatchMessage", response); } } diff --git a/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.css b/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.css @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.chat-content-wrapper { + display: flex; + flex-direction: column; +} diff --git a/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.mjs b/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.mjs @@ -10,28 +10,51 @@ import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; */ export class AIChatContent extends MozLitElement { static properties = { - messages: { type: Array }, + conversationState: { type: Array }, }; constructor() { super(); - this.messages = []; + this.conversationState = []; } connectedCallback() { super.connectedCallback(); + this.#initEventListeners(); + } - // Listen for ai-response events as fallback - this.addEventListener("ai-response", this.handleAIResponseEvent.bind(this)); + /** + * Initialize event listeners for AI chat content events + */ + + #initEventListeners() { + this.addEventListener( + "aiChatContentActor:message", + this.messageEvent.bind(this) + ); + } + + messageEvent(event) { + const message = event.detail; + if (message.role === "assistant") { + this.handleAIResponseEvent(event); + return; + } + this.handleUserPromptEvent(event); } /** - * Add an AI response to the chat + * Handle user prompt events * - * @param {string} response - The AI response text + * @param {CustomeEvent} event - The custom event containing the user prompt */ - addAIResponse(response) { - this.messages = [...this.messages, { type: "ai", content: response }]; + + handleUserPromptEvent(event) { + const { content } = event.detail; + this.conversationState = [ + ...this.conversationState, + { role: "user", content }, + ]; this.requestUpdate(); } @@ -40,28 +63,32 @@ export class AIChatContent extends MozLitElement { * * @param {CustomEvent} event - The custom event containing the response */ + handleAIResponseEvent(event) { - console.warn("Received AI response event:", event); - // TODO - Use Markdown to render rich text responses - this.addAIResponse(event.detail); + const { content, latestAssistantMessageIndex } = event.detail; + if (!this.conversationState[latestAssistantMessageIndex]) { + this.conversationState[latestAssistantMessageIndex] = { + role: "assistant", + content: "", + }; + } + this.conversationState[latestAssistantMessageIndex] = { + ...this.conversationState[latestAssistantMessageIndex], + content, + }; + this.requestUpdate(); } render() { return html` - <div> - <div> - ${this.messages.map( - (message, index) => html` - <div key=${index}> - <strong>${message.type === "ai" ? "AI" : "User"}:</strong> - ${message.content} - </div> - ` - )} - ${this.messages.length === 0 - ? html`<div>Chat will appear here...</div>` - : ""} - </div> + <link + rel="stylesheet" + href="chrome://browser/content/aiwindow/components/ai-chat-content.css" + /> + <div class="chat-content-wrapper"> + ${this.conversationState.map( + msg => html`<ai-chat-message .message=${msg}></ai-chat-message>` + )} </div> `; } diff --git a/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.stories.mjs b/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.stories.mjs @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { html } from "chrome://global/content/vendor/lit.all.mjs"; +import "chrome://browser/content/aiwindow/components/ai-chat-content.mjs"; + +export default { + title: "Domain-specific UI Widgets/AI Window/AI Chat Content", + component: "ai-chat-content", + argTypes: { + conversationState: { + control: { type: "object" }, + }, + }, +}; + +const Template = ({ conversationState }) => html` + <ai-chat-content .conversationState=${conversationState}></ai-chat-content> +`; + +export const Empty = Template.bind({}); +Empty.args = { + conversationState: [], +}; + +export const SingleUserMessage = Template.bind({}); +SingleUserMessage.args = { + conversationState: [ + { role: "user", content: "What is the weather like today?" }, + ], +}; + +export const Conversation = Template.bind({}); +Conversation.args = { + conversationState: [ + { role: "user", content: "Test: What is the weather like today?" }, + { + role: "assistant", + content: + "Test: I don't have access to real-time weather data, but I can help you with other tasks!", + }, + { role: "user", content: "Test: Can you help me with coding?" }, + { + role: "assistant", + content: + "Test: Yes, I can help you with coding! What programming language or problem are you working on?", + }, + ], +}; diff --git a/browser/components/aiwindow/ui/components/ai-chat-message/ai-chat-message.css b/browser/components/aiwindow/ui/components/ai-chat-message/ai-chat-message.css @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.message-user { + border-radius: var(--border-radius-small); + border: var(--border-width) solid white; +} diff --git a/browser/components/aiwindow/ui/components/ai-chat-message/ai-chat-message.mjs b/browser/components/aiwindow/ui/components/ai-chat-message/ai-chat-message.mjs @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { html } from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +/** + * A custom element for managing AI Chat Content + */ +export class AIChatMessage extends MozLitElement { + /** + * @member {object} message - {role:"user"|"assistant" , content: string} + */ + + static properties = { + message: { type: Object }, + }; + + constructor() { + super(); + } + + connectedCallback() { + super.connectedCallback(); + } + + render() { + return html` + <link + rel="stylesheet" + href="chrome://browser/content/aiwindow/components/ai-chat-message.css" + /> + + <article> + <div class=${"message-" + this.message.role}> + <!-- TODO: Add markdown parsing here --> + ${this.message.content} + </div> + </article> + `; + } +} + +customElements.define("ai-chat-message", AIChatMessage); diff --git a/browser/components/aiwindow/ui/components/ai-chat-message/ai-chat-message.stories.mjs b/browser/components/aiwindow/ui/components/ai-chat-message/ai-chat-message.stories.mjs @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { html } from "chrome://global/content/vendor/lit.all.mjs"; +import "chrome://browser/content/aiwindow/components/ai-chat-message.mjs"; + +export default { + title: "Domain-specific UI Widgets/AI Window/AI Chat Message", + component: "ai-chat-message", + argTypes: { + role: { + options: ["user", "assistant"], + control: { type: "select" }, + }, + content: { + control: { type: "text" }, + }, + }, +}; + +const Template = ({ role, content }) => html` + <ai-chat-message .message=${{ role, content }}></ai-chat-message> +`; + +export const UserMessage = Template.bind({}); +UserMessage.args = { + role: "user", + content: "Test: What is the weather like today?", +}; + +export const AssistantMessage = Template.bind({}); +AssistantMessage.args = { + role: "assistant", + content: + "Test: I don't have access to real-time weather data, but I can help you with other tasks!", +}; diff --git a/browser/components/aiwindow/ui/components/ai-window/ai-window.css b/browser/components/aiwindow/ui/components/ai-window/ai-window.css @@ -7,9 +7,11 @@ flex-direction: column; } +/* TODO update height styles - this is a place holder size. */ #browser-container, #aichat-browser { display: flex; flex: 1; - min-height: var(--size-item-xlarge); + /* stylelint-disable-next-line stylelint-plugin-mozilla/use-size-tokens */ + min-height: 400px; } diff --git a/browser/components/aiwindow/ui/components/ai-window/ai-window.mjs b/browser/components/aiwindow/ui/components/ai-window/ai-window.mjs @@ -5,15 +5,34 @@ import { html } from "chrome://global/content/vendor/lit.all.mjs"; import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + Chat: "moz-src:///browser/components/aiwindow/models/Chat.sys.mjs", +}); + +/** + * State Management Strategy: + * + * - On initialization, this component will call `.renderState()` from ChatStore to hydrate the UI + * - Currently using temporary local state (`this.conversationState`) that only tracks the current conversation turn + * - When ChatStore is integrated, rely on it as the source of truth for full conversation history + * - When calling Chat.sys.mjs to fetch responses, supplement the request with complete history from ChatStore + */ + /** * A custom element for managing AI Window */ export class AIWindow extends MozLitElement { - static properties = {}; + static properties = { + userPrompt: { type: String }, + conversationState: { type: Array }, + }; constructor() { super(); this._browser = null; + this.userPrompt = ""; + this.conversationState = []; } connectedCallback() { @@ -37,25 +56,85 @@ export class AIWindow extends MozLitElement { this._browser = browser; } - async _submitUserPrompt() { - const mockPrompt = "This is a test prompt, how are you?"; + /** + * Adds a new message to the conversation history. + * + * @param {object} chatEntry - A message object to add to the conversation + * @param {("system"|"user"|"assistant")} chatEntry.role - The role of the message sender + * @param {string} chatEntry.content - The text content of the message + */ + + // TODO - can remove this method after ChatStore is integrated + #updateConversationState = chatEntry => { + this.conversationState = [...this.conversationState, chatEntry]; + }; + + /** + * Fetches an AI response based on the current user prompt. + * Validates the prompt, updates conversation state, streams the response, + * and dispatches updates to the browser actor. + * + * @private + */ + + #fetchAIResponse = async () => { + const formattedPrompt = (this.userPrompt || "").trim(); + if (!formattedPrompt) { + return; + } - // Call AI service directly from this instance - const response = this._fetchAIResponse(mockPrompt); + // Handle User Prompt + await this.#dispatchMessageToChatContent({ + role: "user", + content: this.userPrompt, + }); - // Dispatch directly to our browser's actor - await this._dispatchAIResponseToBrowser(response); - } + // TODO - can remove this call after ChatStore is integrated + this.#updateConversationState({ role: "user", content: formattedPrompt }); + this.userPrompt = ""; - _fetchAIResponse(userPrompt) { - // TODO - Add actual call to LLM service here - const mockResponse = `this is a response to ${userPrompt}`; - return mockResponse; - } + // Create an empty assistant placeholder. + // TODO - can remove this call after ChatStore is integrated + this.#updateConversationState({ role: "assistant", content: "" }); + const latestAssistantMessageIndex = this.conversationState.length - 1; - async _dispatchAIResponseToBrowser(response) { + let acc = ""; + try { + // TODO - replace with ChatStore integration IE pass chatstore.getConversationState(this.userPrompt) + const stream = lazy.Chat.fetchWithHistory(this.conversationState); + for await (const chunk of stream) { + acc += chunk; + + // TODO - can remove this after ChatStore is integrated + this.conversationState[latestAssistantMessageIndex] = { + ...this.conversationState[latestAssistantMessageIndex], + content: acc, + }; + + // TODO - can pass chatstore.getLastturnIndex() instead of latestAssistantMessageIndex after ChatStore is integrated + await this.#dispatchMessageToChatContent({ + role: "assistant", + content: acc, + latestAssistantMessageIndex, + }); + this.requestUpdate?.(); + } + } catch (e) { + // TODO - handle error properly + this.requestUpdate?.(); + } + }; + + /** + * Retrieves the AIChatContent actor from the browser's window global. + * + * @returns {Promise<object|null>} The AIChatContent actor, or null if unavailable. + * @private + */ + + async #getAIChatContentActor() { if (!this._browser) { - console.warn("AI browser not set, cannot dispatch response"); + console.warn("AI browser not set, cannot get AIChatContent actor"); return null; } @@ -67,16 +146,45 @@ export class AIWindow extends MozLitElement { } try { - const actor = windowGlobal.getActor("AIChatContent"); - return await actor.dispatchAIResponse(response); + return windowGlobal.getActor("AIChatContent"); } catch (error) { - console.error("Failed to dispatch AI response:", error); + console.error("Failed to get AIChatContent actor:", error); return null; } } - _handleSubmit() { - this._submitUserPrompt(); + /** + * Dispatches a message to the AIChatContent actor. + * + * @param {object} message - message to dispatch to chat content actor + * @returns + */ + + async #dispatchMessageToChatContent(message) { + const actor = await this.#getAIChatContentActor(); + return await actor.dispatchMessageToChatContent(message); + } + + /** + * Handles input events from the prompt textarea. + * Updates the userPrompt property with the current input value. + * + * @param {Event} e - The input event. + * @private + */ + + #handlePromptInput = async e => { + const value = e.target.value; + this.userPrompt = value; + }; + + /** + * Handles the submit action for the user prompt. + * Triggers the AI response fetch process. + */ + + #handleSubmit() { + this.#fetchAIResponse(); } render() { @@ -88,7 +196,11 @@ export class AIWindow extends MozLitElement { <div> <div id="browser-container"></div> <!-- TODO : Remove place holder submit button, prompt will come from ai-input --> - <moz-button type="primary" size="small" @click=${this._handleSubmit}> + <textarea + .value=${this.userPrompt} + @input=${e => this.#handlePromptInput(e)} + ></textarea> + <moz-button type="primary" size="small" @click=${this.#handleSubmit}> Submit mock prompt </moz-button> </div> diff --git a/browser/components/aiwindow/ui/content/aiChatContent.html b/browser/components/aiwindow/ui/content/aiChatContent.html @@ -20,9 +20,12 @@ type="module" src="chrome://browser/content/aiwindow/components/ai-chat-content.mjs" ></script> + <script + type="module" + src="chrome://browser/content/aiwindow/components/ai-chat-message.mjs" + ></script> </head> <body id="ai-window-wrapper"> - Placeholder for AI Chat Content <ai-chat-content></ai-chat-content> </body> </html> diff --git a/browser/components/aiwindow/ui/jar.mn b/browser/components/aiwindow/ui/jar.mn @@ -7,6 +7,9 @@ browser.jar: content/browser/aiwindow/aiWindow.html (content/aiWindow.html) content/browser/aiwindow/assets/input-cta-arrow-icon.svg (assets/input-cta-arrow-icon.svg) content/browser/aiwindow/components/ai-chat-content.mjs (components/ai-chat-content/ai-chat-content.mjs) + content/browser/aiwindow/components/ai-chat-content.css (components/ai-chat-content/ai-chat-content.css) + content/browser/aiwindow/components/ai-chat-message.mjs (components/ai-chat-message/ai-chat-message.mjs) + content/browser/aiwindow/components/ai-chat-message.css (components/ai-chat-message/ai-chat-message.css) content/browser/aiwindow/components/ai-window.mjs (components/ai-window/ai-window.mjs) content/browser/aiwindow/components/ai-window.css (components/ai-window/ai-window.css) content/browser/aiwindow/components/input-cta.css (components/input-cta/input-cta.css) diff --git a/browser/components/aiwindow/ui/test/browser/browser.toml b/browser/components/aiwindow/ui/test/browser/browser.toml @@ -1,7 +1,13 @@ [DEFAULT] +["browser_actor_user_prompt.js"] + ["browser_aichat_content_actors.js"] +["browser_aichat_message.js"] + ["browser_aiwindow_firstrun.js"] +["browser_aiwindow_integration.js"] + ["browser_open_aiwindow.js"] diff --git a/browser/components/aiwindow/ui/test/browser/browser_actor_user_prompt.js b/browser/components/aiwindow/ui/test/browser/browser_actor_user_prompt.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test user prompt dispatch through updated actor system + */ +add_task(async function test_user_prompt_dispatch() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.aiwindow.enabled", true]], + }); + + await BrowserTestUtils.withNewTab("about:aichatcontent", async browser => { + const actor = + browser.browsingContext.currentWindowGlobal.getActor("AIChatContent"); + + // Test that dispatchUserPrompt method exists and can be called + const testPrompt = { + role: "user", + content: "Hello, AI!", + }; + const result = await actor.dispatchMessageToChatContent(testPrompt); + + // The method should return true for successful dispatch + Assert.equal( + result, + true, + "dispatchUserPrompt should complete successfully" + ); + }); +}); + +/** + * Test updated AI response dispatch method works correctly + */ +add_task(async function test_streaming_ai_response() { + await BrowserTestUtils.withNewTab("about:aichatcontent", async browser => { + const actor = + browser.browsingContext.currentWindowGlobal.getActor("AIChatContent"); + + // Test streaming response format + const streamingResponse = { + role: "assistant", + content: "Partial AI response...", + latestAssistantMessageIndex: 0, + }; + + const result = await actor.dispatchMessageToChatContent(streamingResponse); + Assert.equal( + result, + true, + "Streaming AI response should be dispatched successfully" + ); + }); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/aiwindow/ui/test/browser/browser_aichat_message.js b/browser/components/aiwindow/ui/test/browser/browser_aichat_message.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test ai-chat-message custom element basic rendering + */ +add_task(async function test_ai_chat_message_rendering() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.aiwindow.enabled", true]], + }); + + // Use about:aichatcontent which already loads the components properly + await BrowserTestUtils.withNewTab("about:aichatcontent", async browser => { + await SpecialPowers.spawn(browser, [], async () => { + await content.customElements.whenDefined("ai-chat-message"); + + // Create a test ai-chat-message element + const messageElement = content.document.createElement("ai-chat-message"); + content.document.body.appendChild(messageElement); + + Assert.ok(messageElement, "ai-chat-message element should be created"); + + // Test setting a user message + messageElement.message = { role: "user", content: "Test user message" }; + await messageElement.updateComplete; + + const messageDiv = + messageElement.renderRoot?.querySelector(".message-user"); + if (messageDiv) { + Assert.ok(messageDiv, "User message div should be rendered"); + Assert.ok( + messageDiv.textContent.includes("Test user message"), + "User message content should be present" + ); + } + + // Test setting an assistant message + messageElement.message = { + role: "assistant", + content: "Test AI response", + }; + await messageElement.updateComplete; + + const assistantDiv = + messageElement.renderRoot?.querySelector(".message-assistant"); + if (assistantDiv) { + Assert.ok(assistantDiv, "Assistant message div should be rendered"); + Assert.ok( + assistantDiv.textContent.includes("Test AI response"), + "Assistant message content should be present" + ); + } + + // Clean up + messageElement.remove(); + }); + }); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/aiwindow/ui/test/browser/browser_aiwindow_integration.js b/browser/components/aiwindow/ui/test/browser/browser_aiwindow_integration.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that actor message dispatching works with updated event names + */ +add_task(async function test_actor_event_mapping() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.aiwindow.enabled", true]], + }); + await BrowserTestUtils.withNewTab("about:aichatcontent", async browser => { + await SpecialPowers.spawn(browser, [], async () => { + await content.customElements.whenDefined("ai-chat-content"); + + const chatElement = content.document.querySelector("ai-chat-content"); + Assert.ok(chatElement, "ai-chat-content element should be present"); + + // Test that event listeners are properly set up for new event names + let messageReceived = false; + + chatElement.addEventListener("aiChatContentActor:message", () => { + messageReceived = true; + }); + + const messageEvent = new content.CustomEvent( + "aiChatContentActor:message", + { + detail: { + content: "Test response", + latestAssistantMessageIndex: 0, + role: "assistant", + }, + bubbles: true, + } + ); + chatElement.dispatchEvent(messageEvent); + + Assert.ok(messageReceived, "AI response event should be received"); + }); + }); + + await SpecialPowers.popPrefEnv(); +});