/* 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/. */ const { topChromeWindow } = window.browsingContext; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { GenAI: "resource:///modules/GenAI.sys.mjs", LightweightThemeConsumer: "resource://gre/modules/LightweightThemeConsumer.sys.mjs", SpecialMessageActions: "resource://messaging-system/lib/SpecialMessageActions.sys.mjs", }); const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); // Define actions for onboarding and chatbot const ACTIONS = Object.freeze({ CHATBOT_PERSIST: "chatbot:persist", CHATBOT_REVERT: "chatbot:revert", CHATBOT_SELECT: "chatbot:select", CHATBOT_SUPPORT: "chatbot:support", OPEN_URL: "OPEN_URL", }); XPCOMUtils.defineLazyPreferenceGetter( lazy, "providerPref", "browser.ml.chat.provider", null, renderProviders ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "shortcutsPref", "browser.ml.chat.shortcuts" ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "sidebarRevampPref", "sidebar.revamp" ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "onboardingConfig", "browser.ml.chat.onboarding.config", JSON.stringify({ id: "chatbot", template: "multistage", transitions: true, screens: [ { id: "chat_pick", content: { fullscreen: true, hide_secondary_section: "responsive", narrow: true, position: "split", title: { fontWeight: 400, string_id: "genai-onboarding-header", }, cta_paragraph: { text: { string_id: "genai-onboarding-description", string_name: "learn-more", }, action: { type: ACTIONS.CHATBOT_SUPPORT, }, }, above_button_content: [ // Placeholder to inject on provider change { text: " ", type: "text", }, ], primary_button: { action: { navigate: true, type: ACTIONS.CHATBOT_PERSIST, }, label: { string_id: "genai-onboarding-primary" }, }, additional_button: { action: { dismiss: true, type: ACTIONS.CHATBOT_REVERT }, label: { string_id: "genai-onboarding-secondary" }, style: "link", }, progress_bar: true, }, }, { id: "chat_suggest", content: { fullscreen: true, hide_secondary_section: "responsive", narrow: true, position: "split", title: { fontWeight: 400, string_id: "genai-onboarding-select-header", }, subtitle: { string_id: "genai-onboarding-select-description" }, above_button_content: [ { height: "172px", type: "image", width: "307px", }, { text: " ", type: "text", }, ], primary_button: { action: { navigate: true }, label: { string_id: "genai-onboarding-select-primary" }, }, progress_bar: true, }, }, ], }) ); ChromeUtils.defineLazyGetter( lazy, "supportLink", () => Services.urlFormatter.formatURLPref("app.support.baseURL") + "ai-chatbot" ); const node = {}; function closeSidebar() { topChromeWindow.SidebarController.hide(); } function openLink(url) { topChromeWindow.openLinkIn(url, "tabshifted", { triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}), }); } function request(url = lazy.providerPref) { try { node.chat.fixupAndLoadURIString(url, { triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( {} ), }); } catch (ex) { console.error("Failed to request chat provider", ex); } } function renderChat() { const browser = document.createXULElement("browser"); browser.setAttribute("disableglobalhistory", "true"); browser.setAttribute("maychangeremoteness", "true"); browser.setAttribute("nodefaultsrc", "true"); browser.setAttribute("remote", "true"); browser.setAttribute("type", "content"); return document.body.appendChild(browser); } async function renderProviders() { // Skip potential pref change callback when unloading if ((await document.visibilityState) == "hidden") { return null; } const select = document.getElementById("provider"); select.innerHTML = ""; let selected = false; const addOption = (text = "", val = "") => { const option = select.appendChild(document.createElement("option")); option.textContent = text; option.value = val; return option; }; // Add the known providers in order while looking for current selection lazy.GenAI.chatProviders.forEach((data, url) => { const option = addOption(data.name, url); if (lazy.providerPref == url) { option.selected = true; selected = true; } else if (data.hidden) { option.hidden = true; } }); // Must be a custom preference if provider wasn't found if (!selected) { const option = addOption(lazy.providerPref, lazy.providerPref); option.selected = true; if (!lazy.providerPref) { showOnboarding(); } } // Add extra controls after the providers select.appendChild(document.createElement("hr")); document.l10n.setAttributes(addOption(), "genai-provider-view-details"); // Update provider telemetry const providerId = lazy.GenAI.getProviderId(lazy.providerPref); Glean.genaiChatbot.provider.set(providerId); if (renderProviders.lastId && document.hasFocus()) { Glean.genaiChatbot.providerChange.record({ current: providerId, previous: renderProviders.lastId, surface: "panel", }); } renderProviders.lastId = providerId; // Load the requested provider request(); return select; } function renderMore() { const button = document.getElementById("header-more"); button.addEventListener("click", () => { const topDoc = topChromeWindow.document; let menu = topDoc.getElementById("chatbot-menupopup"); if (!menu) { menu = topDoc .getElementById("mainPopupSet") .appendChild(topDoc.createXULElement("menupopup")); menu.id = "chatbot-menupopup"; node.menu = menu; menu.addEventListener("popuphidden", () => { button.setAttribute("aria-expanded", false); }); } menu.innerHTML = ""; const provider = lazy.GenAI.chatProviders.get(lazy.providerPref)?.name; const providerId = lazy.GenAI.getProviderId(); [ [ "menuitem", [ provider ? "genai-options-reload-provider" : "genai-options-reload-generic", { provider }, ], function reload() { request(); }, ], ["menuseparator"], [ "menuitem", ["genai-options-show-shortcut"], function show_shortcuts() { Services.prefs.setBoolPref("browser.ml.chat.shortcuts", true); }, lazy.shortcutsPref, ], [ "menuitem", ["genai-options-hide-shortcut"], function hide_shortcuts() { Services.prefs.setBoolPref("browser.ml.chat.shortcuts", false); }, !lazy.shortcutsPref, ], ["menuseparator"], [ "menuitem", ["genai-options-about-chatbot"], function about() { openLink(lazy.supportLink); }, ], ].forEach(([type, l10n, command, checked]) => { const item = menu.appendChild(topDoc.createXULElement(type)); if (type != "menuitem") { return; } document.l10n.setAttributes(item, ...l10n); item.addEventListener("command", () => { command(); Glean.genaiChatbot.sidebarMoreMenuClick.record({ action: command.name, provider: providerId, }); }); if (checked !== undefined) { item.setAttribute("type", "checkbox"); if (checked) { item.setAttribute("checked", true); } } }); menu.openPopup(button, "after_start"); button.setAttribute("aria-expanded", true); Glean.genaiChatbot.sidebarMoreMenuDisplay.record({ provider: providerId }); }); } function handleChange({ target }) { const { value } = target; switch (target) { case node.provider: // Special behavior to show first screen of onboarding if (value == "") { target.value = lazy.providerPref; showOnboarding(1); Glean.genaiChatbot.sidebarProviderMenuClick.record({ action: "details", provider: lazy.GenAI.getProviderId(), }); } else { Services.prefs.setStringPref("browser.ml.chat.provider", value); } break; } } addEventListener("change", handleChange); // Expose a promise for loading and rendering the chat browser element var browserPromise = new Promise((resolve, reject) => { addEventListener("load", async () => { new lazy.LightweightThemeConsumer(document); try { node.chat = renderChat(); node.provider = await renderProviders(); renderMore(); resolve(node.chat); document.getElementById("header-close").addEventListener("click", () => { closeSidebar(); Glean.genaiChatbot.sidebarCloseClick.record({ provider: lazy.GenAI.getProviderId(), }); }); } catch (ex) { console.error("Failed to render on load", ex); reject(ex); } Glean.genaiChatbot.sidebarToggle.record({ opened: true, provider: lazy.GenAI.getProviderId(), reason: "load", version: lazy.sidebarRevampPref ? "new" : "old", }); }); }); addEventListener("unload", () => { node.menu?.remove(); Glean.genaiChatbot.sidebarToggle.record({ opened: false, provider: lazy.GenAI.getProviderId(), reason: "unload", version: lazy.sidebarRevampPref ? "new" : "old", }); }); /** * Show onboarding screens * * @param {number} length optional show fewer screens */ function showOnboarding(length) { // Insert onboarding container and render with script const root = document.createElement("div"); root.id = "multi-stage-message-root"; document.getElementById(root.id)?.remove(); document.body.prepend(root); history.replaceState("", ""); const script = document.head.appendChild(document.createElement("script")); script.src = "chrome://browser/content/aboutwelcome/aboutwelcome.bundle.js"; // Convert provider data for lookup by id const providerConfigs = new Map(); lazy.GenAI.chatProviders.forEach((data, url) => { if (!data.hidden) { providerConfigs.set(data.id, { ...data, url }); } }); // Define various AW* functions to control aboutwelcome bundle behavior Object.assign(window, { AWEvaluateScreenTargeting(screens) { return screens; }, AWFinish() { if (lazy.providerPref == "") { closeSidebar(); } root.remove(); }, AWGetFeatureConfig() { const onboarding = JSON.parse(lazy.onboardingConfig); const providerTiles = { action: { picker: "" }, data: [...providerConfigs.values()].map(config => ({ action: { type: ACTIONS.CHATBOT_SELECT, config }, id: config.id, label: config.name, tooltip: { string_id: config.tooltipId }, })), // Default to nothing selected selected: " ", type: "single-select", }; // Insert provider tiles on the first screen onboarding.screens[0].content.tiles = providerTiles; // Remove extra screens if any onboarding.screens = onboarding.screens.slice(0, length); return onboarding; }, AWGetInstalledAddons() {}, AWGetSelectedTheme() { const primary = document.querySelector(".primary"); if (primary) { primary.disabled = true; } // Specially handle links to open out of the sidebar const handleLink = ev => { const { href } = ev.target; if (href) { ev.preventDefault(); openLink(href); } }; const links = document.querySelector(".link-paragraph"); links?.addEventListener("click", handleLink); [...document.querySelectorAll("fieldset label")].forEach(label => { // Add content that is hidden with 0 height until selected const div = label .querySelector(".text") .appendChild(document.createElement("div")); div.style.maxHeight = 0; div.tabIndex = -1; const ul = div.appendChild(document.createElement("ul")); const config = providerConfigs.get(label.querySelector("input").value); config.choiceIds?.forEach(id => { const li = ul.appendChild(document.createElement("li")); document.l10n.setAttributes(li, id); }); if (config.learnLink && config.learnId) { const a = div.appendChild(document.createElement("a")); a.href = config.learnLink; a.tabIndex = -1; a.addEventListener("click", ev => { handleLink(ev); Glean.genaiChatbot.onboardingProviderLearn.record({ provider: config.id, step: 1, }); }); document.l10n.setAttributes(a, config.learnId); } }); }, AWSendEventTelemetry({ event, event_context: { source }, message_id }) { const { provider } = window.AWSendEventTelemetry; const step = message_id.match(/chat_pick/) ? 1 : 2; switch (true) { case step == 1 && event == "IMPRESSION": Glean.genaiChatbot.onboardingProviderChoiceDisplayed.record({ provider: lazy.GenAI.getProviderId(lazy.providerPref), step, }); break; case step == 1 && source == "cta_paragraph": Glean.genaiChatbot.onboardingLearnMore.record({ provider, step }); break; case step == 1 && source == "primary_button": Glean.genaiChatbot.onboardingContinue.record({ provider, step }); break; case step == 1 && source == "additional_button": Glean.genaiChatbot.onboardingClose.record({ provider, step }); break; case step == 1 && source.startsWith("link"): Glean.genaiChatbot.onboardingProviderTerms.record({ provider, step, text: source, }); break; // Assume generic click not yet handled above single select of provider case step == 1 && event == "CLICK_BUTTON": window.AWSendEventTelemetry.provider = source; Glean.genaiChatbot.onboardingProviderSelection.record({ provider: source, step, }); break; case step == 2 && event == "IMPRESSION": Glean.genaiChatbot.onboardingTextHighlightDisplayed.record({ provider, step, }); break; case step == 2 && source == "primary_button": Glean.genaiChatbot.onboardingFinish.record({ provider, step }); break; } }, AWSendToParent(_message, action) { switch (action.type) { case ACTIONS.OPEN_URL: lazy.SpecialMessageActions.handleAction(action, topChromeWindow); return; case ACTIONS.CHATBOT_PERSIST: { const { value } = document.querySelector( "label:has(.selected) input" ); Services.prefs.setStringPref( "browser.ml.chat.provider", providerConfigs.get(value).url ); break; } case ACTIONS.CHATBOT_REVERT: { request(); break; } // Handle single select provider choice case ACTIONS.CHATBOT_SELECT: { const { config } = action; if (!config) { break; } request(config.url); document.querySelector(".primary").disabled = false; // Set max-height to trigger transition document.querySelectorAll("label .text div").forEach(div => { const selected = div.closest("label").querySelector("input").value == config.id; div.style.maxHeight = selected ? div.scrollHeight + "px" : 0; const a = div.querySelector("a"); if (a) { a.tabIndex = selected ? 0 : -1; } }); // Update potentially multiple links for the provider const links = document.querySelector(".link-paragraph"); if (links && links.dataset.l10nId != config.linksId) { links.innerHTML = ""; for (let i = 1; i <= 3; i++) { const link = links.appendChild(document.createElement("a")); const name = (link.dataset.l10nName = `link${i}`); link.href = config[name]; link.setAttribute("value", name); } document.l10n.setAttributes(links, config.linksId); } break; } case ACTIONS.CHATBOT_SUPPORT: openLink(lazy.supportLink); break; } }, }); }