ationState: { type: Array }, logState: { type: Array }, mode: { type: String }, // "tab" | "sidebar" overrideNewTab: { type: Boolean }, showLog: { type: Boolean }, actionKey: { type: String }, // "chat" | "search" }; constructor() { super(); this.userPrompt = ""; // TODO the conversation state will evenually need to be stored in a "higher" location // then just the state of this lit component. This is a Stub to get the convo started for now this.conversationState = [ { role: "system", content: "You are a helpful assistant" }, ]; this.logState = []; this.showLog = false; this.mode = "sidebar"; this.overrideNewTab = Services.prefs.getBoolPref( "browser.ml.smartAssist.overrideNewTab" ); this.actionKey = ACTION_CHAT; this._actions = { [ACTION_CHAT]: { label: "Submit", icon: "chrome://global/skin/icons/arrow-right.svg", run: this._actionChat, }, [ACTION_SEARCH]: { label: "Search", icon: "chrome://global/skin/icons/search-glass.svg", run: this._actionSearch, }, }; } connectedCallback() { super.connectedCallback(); if (this.mode === "sidebar" && this.overrideNewTab) { this._applyNewTabOverride(true); } } /** * 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 */ _updateConversationState = chatEntry => { this.conversationState = [...this.conversationState, chatEntry]; }; _updatelogState = chatEntry => { const entryWithDate = { ...chatEntry, date: new Date().toLocaleString() }; this.logState = [...this.logState, entryWithDate]; }; _handlePromptInput = async e => { try { const value = e.target.value; this.userPrompt = value; const intent = await lazy.SmartAssistEngine.getPromptIntent(value); this.actionKey = [ACTION_CHAT, ACTION_SEARCH].includes(intent) ? intent : ACTION_CHAT; } catch (error) { // Default to chat on error this.actionKey = ACTION_CHAT; console.error("Error determining prompt intent:", error); } }; /** * Returns the current action object based on the actionKey */ get inputAction() { return this._actions[this.actionKey]; } _actionSearch = async () => { const searchTerms = (this.userPrompt || "").trim(); if (!searchTerms) { return; } const isPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(window); const engine = isPrivate ? await Services.search.getDefaultPrivate() : await Services.search.getDefault(); const submission = engine.getSubmission(searchTerms); // default to SEARCH (text/html) // getSubmission can return null if the engine doesn't have a URL // with a text/html response type. This is unlikely (since // SearchService._addEngineToStore() should fail for such an engine), // but let's be on the safe side. if (!submission) { return; } const triggeringPrincipal = Services.scriptSecurityManager.createNullPrincipal({}); window.browsingContext.topChromeWindow.openLinkIn( submission.uri.spec, "current", { private: isPrivate, postData: submission.postData, inBackground: false, relatedToCurrent: true, triggeringPrincipal, policyContainer: null, targetBrowser: null, globalHistoryOptions: { triggeringSearchEngine: engine.name, }, } ); }; _actionChat = async () => { const formattedPrompt = (this.userPrompt || "").trim(); if (!formattedPrompt) { return; } // Push user prompt this._updateConversationState({ role: "user", content: formattedPrompt }); this.userPrompt = ""; // Create an empty assistant placeholder. this._updateConversationState({ role: "assistant", content: "" }); const latestAssistantMessageIndex = this.conversationState.length - 1; let acc = ""; try { const stream = lazy.SmartAssistEngine.fetchWithHistory( this.conversationState ); for await (const chunk of stream) { // Check to see if chunk is special tool calling log and add to logState if (chunk.type === "tool_call_log") { this._updatelogState({ content: chunk.content, result: chunk.result || "No result", }); continue; } acc += chunk; // append to the latest assistant message this.conversationState[latestAssistantMessageIndex] = { ...this.conversationState[latestAssistantMessageIndex], content: acc, }; this.requestUpdate?.(); } } catch (e) { this.conversationState[latestAssistantMessageIndex] = { role: "assistant", content: `There was an error`, }; this.requestUpdate?.(); } }; /** * Mock Functionality to open full page UX * * @param {boolean} enable * Whether or not to override the new tab page. */ _applyNewTabOverride(enable) { try { enable ? (lazy.AboutNewTab.newTabURL = FULL_PAGE_URL) : lazy.AboutNewTab.resetNewTabURL(); } catch (e) { console.error("Failed to toggle new tab override:", e); } } _onToggleFullPage(e) { const isChecked = e.target.checked; Services.prefs.setBoolPref( "browser.ml.smartAssist.overrideNewTab", isChecked ); this.overrideNewTab = isChecked; this._applyNewTabOverride(isChecked); } /** * Initiates the Firefox Account sign-in flow for MLPA authentication. */ _signIn() { lazy.SpecialMessageActions.handleAction( { type: "FXA_SIGNIN_FLOW", data: { entrypoint: "aiwindow", extraParams: { service: "aiwindow", }, }, }, window.browsingContext.topChromeWindow.gBrowser.selectedBrowser ); } _toggleAIWindowSidebar() { lazy.AIWindowUI.toggleSidebar(window.browsingContext.topChromeWindow); } render() { const iconSrc = this.showLog ? "chrome://global/skin/icons/arrow-down.svg" : "chrome://global/skin/icons/arrow-up.svg"; return html`
Sign in for MLPA authentication.