1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 const { topChromeWindow
} = window
.browsingContext
;
8 ChromeUtils
.defineESModuleGetters(lazy
, {
9 GenAI
: "resource:///modules/GenAI.sys.mjs",
10 LightweightThemeConsumer
:
11 "resource://gre/modules/LightweightThemeConsumer.sys.mjs",
12 SpecialMessageActions
:
13 "resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
15 const { XPCOMUtils
} = ChromeUtils
.importESModule(
16 "resource://gre/modules/XPCOMUtils.sys.mjs"
19 XPCOMUtils
.defineLazyPreferenceGetter(
22 "browser.ml.chat.provider",
26 XPCOMUtils
.defineLazyPreferenceGetter(
29 "browser.ml.chat.shortcuts"
31 XPCOMUtils
.defineLazyPreferenceGetter(
37 ChromeUtils
.defineLazyGetter(
41 Services
.urlFormatter
.formatURLPref("app.support.baseURL") + "ai-chatbot"
46 function closeSidebar() {
47 topChromeWindow
.SidebarController
.hide();
50 function openLink(url
) {
51 topChromeWindow
.openLinkIn(url
, "tabshifted", {
52 triggeringPrincipal
: Services
.scriptSecurityManager
.createNullPrincipal({}),
56 function request(url
= lazy
.providerPref
) {
58 node
.chat
.fixupAndLoadURIString(url
, {
59 triggeringPrincipal
: Services
.scriptSecurityManager
.createNullPrincipal(
64 console
.error("Failed to request chat provider", ex
);
68 function renderChat() {
69 const browser
= document
.createXULElement("browser");
70 browser
.setAttribute("disableglobalhistory", "true");
71 browser
.setAttribute("maychangeremoteness", "true");
72 browser
.setAttribute("nodefaultsrc", "true");
73 browser
.setAttribute("remote", "true");
74 browser
.setAttribute("type", "content");
75 return document
.body
.appendChild(browser
);
78 async
function renderProviders() {
79 // Skip potential pref change callback when unloading
80 if ((await document
.visibilityState
) == "hidden") {
84 const select
= document
.getElementById("provider");
85 select
.innerHTML
= "";
88 const addOption
= (text
= "", val
= "") => {
89 const option
= select
.appendChild(document
.createElement("option"));
90 option
.textContent
= text
;
95 // Add the known providers in order while looking for current selection
96 lazy
.GenAI
.chatProviders
.forEach((data
, url
) => {
97 const option
= addOption(data
.name
, url
);
98 if (lazy
.providerPref
== url
) {
99 option
.selected
= true;
101 } else if (data
.hidden
) {
102 option
.hidden
= true;
106 // Must be a custom preference if provider wasn't found
108 const option
= addOption(lazy
.providerPref
, lazy
.providerPref
);
109 option
.selected
= true;
110 if (!lazy
.providerPref
) {
115 // Add extra controls after the providers
116 select
.appendChild(document
.createElement("hr"));
117 document
.l10n
.setAttributes(addOption(), "genai-provider-view-details");
119 // Update provider telemetry
120 const providerId
= lazy
.GenAI
.getProviderId(lazy
.providerPref
);
121 Glean
.genaiChatbot
.provider
.set(providerId
);
122 if (renderProviders
.lastId
&& document
.hasFocus()) {
123 Glean
.genaiChatbot
.providerChange
.record({
125 previous
: renderProviders
.lastId
,
129 renderProviders
.lastId
= providerId
;
131 // Load the requested provider
136 function renderMore() {
137 const button
= document
.getElementById("header-more");
138 button
.addEventListener("click", () => {
139 const topDoc
= topChromeWindow
.document
;
140 let menu
= topDoc
.getElementById("chatbot-menupopup");
143 .getElementById("mainPopupSet")
144 .appendChild(topDoc
.createXULElement("menupopup"));
145 menu
.id
= "chatbot-menupopup";
147 menu
.addEventListener("popuphidden", () => {
148 button
.setAttribute("aria-expanded", false);
153 const provider
= lazy
.GenAI
.chatProviders
.get(lazy
.providerPref
)?.name
;
154 const providerId
= lazy
.GenAI
.getProviderId();
160 ? "genai-options-reload-provider"
161 : "genai-options-reload-generic",
171 ["genai-options-show-shortcut"],
172 function show_shortcuts() {
173 Services
.prefs
.setBoolPref("browser.ml.chat.shortcuts", true);
179 ["genai-options-hide-shortcut"],
180 function hide_shortcuts() {
181 Services
.prefs
.setBoolPref("browser.ml.chat.shortcuts", false);
188 ["genai-options-about-chatbot"],
190 openLink(lazy
.supportLink
);
193 ].forEach(([type
, l10n
, command
, checked
]) => {
194 const item
= menu
.appendChild(topDoc
.createXULElement(type
));
195 if (type
== "menuitem") {
196 document
.l10n
.setAttributes(item
, ...l10n
);
197 item
.addEventListener("command", () => {
199 Glean
.genaiChatbot
.sidebarMoreMenuClick
.record({
200 action
: command
.name
,
201 provider
: providerId
,
205 item
.setAttribute("checked", true);
209 menu
.openPopup(button
, "after_start");
210 button
.setAttribute("aria-expanded", true);
211 Glean
.genaiChatbot
.sidebarMoreMenuDisplay
.record({ provider
: providerId
});
215 function handleChange({ target
}) {
216 const { value
} = target
;
219 // Special behavior to show first screen of onboarding
221 target
.value
= lazy
.providerPref
;
223 Glean
.genaiChatbot
.sidebarProviderMenuClick
.record({
225 provider
: lazy
.GenAI
.getProviderId(),
228 Services
.prefs
.setStringPref("browser.ml.chat.provider", value
);
233 addEventListener("change", handleChange
);
235 // Expose a promise for loading and rendering the chat browser element
236 var browserPromise
= new Promise((resolve
, reject
) => {
237 addEventListener("load", async () => {
238 new lazy
.LightweightThemeConsumer(document
);
240 node
.chat
= renderChat();
241 node
.provider
= await
renderProviders();
244 document
.getElementById("header-close").addEventListener("click", () => {
246 Glean
.genaiChatbot
.sidebarCloseClick
.record({
247 provider
: lazy
.GenAI
.getProviderId(),
251 console
.error("Failed to render on load", ex
);
255 Glean
.genaiChatbot
.sidebarToggle
.record({
257 provider
: lazy
.GenAI
.getProviderId(),
259 version
: lazy
.sidebarRevampPref
? "new" : "old",
264 addEventListener("unload", () => {
266 Glean
.genaiChatbot
.sidebarToggle
.record({
268 provider
: lazy
.GenAI
.getProviderId(),
270 version
: lazy
.sidebarRevampPref
? "new" : "old",
275 * Show onboarding screens
277 * @param {number} length optional show fewer screens
279 function showOnboarding(length
) {
280 const ACTIONS
= Object
.freeze({
281 OPEN_URL
: "OPEN_URL",
282 CHATBOT_SELECT
: "chatbot:select",
283 CHATBOT_PERSIST
: "chatbot:persist",
284 CHATBOT_REVERT
: "chatbot:revert",
287 // Insert onboarding container and render with script
288 const root
= document
.createElement("div");
289 root
.id
= "multi-stage-message-root";
290 document
.getElementById(root
.id
)?.remove();
291 document
.body
.prepend(root
);
292 history
.replaceState("", "");
293 const script
= document
.head
.appendChild(document
.createElement("script"));
294 script
.src
= "chrome://browser/content/aboutwelcome/aboutwelcome.bundle.js";
296 // Convert provider data for lookup by id
297 const providerConfigs
= new Map();
298 lazy
.GenAI
.chatProviders
.forEach((data
, url
) => {
300 providerConfigs
.set(data
.id
, { ...data
, url
});
304 // Define various AW* functions to control aboutwelcome bundle behavior
305 Object
.assign(window
, {
306 AWEvaluateScreenTargeting(screens
) {
310 if (lazy
.providerPref
== "") {
315 AWGetFeatureConfig() {
318 template
: "multistage",
325 hide_secondary_section
: "responsive",
331 string_id
: "genai-onboarding-header",
335 string_id
: "genai-onboarding-description",
336 string_name
: "learn-more",
340 args
: lazy
.supportLink
,
343 type
: ACTIONS
.OPEN_URL
,
347 action
: { picker
: "<event>" },
348 data
: [...providerConfigs
.values()].map(config
=> ({
349 action
: { type
: ACTIONS
.CHATBOT_SELECT
, config
},
352 tooltip
: { string_id
: config
.tooltipId
},
354 // Default to nothing selected
356 type
: "single-select",
358 above_button_content
: [
359 // Placeholder to inject on provider change
368 type
: ACTIONS
.CHATBOT_PERSIST
,
370 label
: { string_id
: "genai-onboarding-primary" },
373 action
: { dismiss
: true, type
: ACTIONS
.CHATBOT_REVERT
},
374 label
: { string_id
: "genai-onboarding-secondary" },
384 hide_secondary_section
: "responsive",
390 string_id
: "genai-onboarding-select-header",
392 subtitle
: { string_id
: "genai-onboarding-select-description" },
393 above_button_content
: [
405 action
: { navigate
: true },
406 label
: { string_id
: "genai-onboarding-select-primary" },
414 AWGetInstalledAddons() {},
415 AWGetSelectedTheme() {
416 document
.querySelector(".primary").disabled
= true;
418 // Specially handle links to open out of the sidebar
419 const handleLink
= ev
=> {
420 const { href
} = ev
.target
;
426 const links
= document
.querySelector(".link-paragraph");
427 links
.addEventListener("click", handleLink
);
429 [...document
.querySelectorAll("fieldset label")].forEach(label
=> {
430 // Add content that is hidden with 0 height until selected
432 .querySelector(".text")
433 .appendChild(document
.createElement("div"));
434 div
.style
.maxHeight
= 0;
436 const ul
= div
.appendChild(document
.createElement("ul"));
437 const config
= providerConfigs
.get(label
.querySelector("input").value
);
438 config
.choiceIds
?.forEach(id
=> {
439 const li
= ul
.appendChild(document
.createElement("li"));
440 document
.l10n
.setAttributes(li
, id
);
442 if (config
.learnLink
&& config
.learnId
) {
443 const a
= div
.appendChild(document
.createElement("a"));
444 a
.href
= config
.learnLink
;
446 a
.addEventListener("click", ev
=> {
448 Glean
.genaiChatbot
.onboardingProviderLearn
.record({
453 document
.l10n
.setAttributes(a
, config
.learnId
);
457 AWSendEventTelemetry({ event
, event_context
: { source
}, message_id
}) {
458 const { provider
} = window
.AWSendEventTelemetry
;
459 const step
= message_id
.match(/chat_pick/) ? 1 : 2;
461 case step
== 1 && event
== "IMPRESSION":
462 Glean
.genaiChatbot
.onboardingProviderChoiceDisplayed
.record({
463 provider
: lazy
.GenAI
.getProviderId(lazy
.providerPref
),
467 case step
== 1 && source
== "cta_paragraph":
468 Glean
.genaiChatbot
.onboardingLearnMore
.record({ provider
, step
});
470 case step
== 1 && source
== "primary_button":
471 Glean
.genaiChatbot
.onboardingContinue
.record({ provider
, step
});
473 case step
== 1 && source
== "additional_button":
474 Glean
.genaiChatbot
.onboardingClose
.record({ provider
, step
});
476 case step
== 1 && source
.startsWith("link"):
477 Glean
.genaiChatbot
.onboardingProviderTerms
.record({
483 // Assume generic click not yet handled above single select of provider
484 case step
== 1 && event
== "CLICK_BUTTON":
485 window
.AWSendEventTelemetry
.provider
= source
;
486 Glean
.genaiChatbot
.onboardingProviderSelection
.record({
491 case step
== 2 && event
== "IMPRESSION":
492 Glean
.genaiChatbot
.onboardingTextHighlightDisplayed
.record({
497 case step
== 2 && source
== "primary_button":
498 Glean
.genaiChatbot
.onboardingFinish
.record({ provider
, step
});
502 AWSendToParent(_message
, action
) {
503 switch (action
.type
) {
504 case ACTIONS
.OPEN_URL
:
505 lazy
.SpecialMessageActions
.handleAction(action
, topChromeWindow
);
507 case ACTIONS
.CHATBOT_PERSIST
: {
508 const { value
} = document
.querySelector(
509 "label:has(.selected) input"
511 Services
.prefs
.setStringPref(
512 "browser.ml.chat.provider",
513 providerConfigs
.get(value
).url
517 case ACTIONS
.CHATBOT_REVERT
: {
521 // Handle single select provider choice
522 case ACTIONS
.CHATBOT_SELECT
: {
523 const { config
} = action
;
529 document
.querySelector(".primary").disabled
= false;
531 // Set max-height to trigger transition
532 document
.querySelectorAll("label .text div").forEach(div
=> {
534 div
.closest("label").querySelector("input").value
== config
.id
;
535 div
.style
.maxHeight
= selected
? div
.scrollHeight
+ "px" : 0;
536 const a
= div
.querySelector("a");
538 a
.tabIndex
= selected
? 0 : -1;
542 // Update potentially multiple links for the provider
543 const links
= document
.querySelector(".link-paragraph");
544 if (links
.dataset
.l10nId
!= config
.linksId
) {
545 links
.innerHTML
= "";
546 for (let i
= 1; i
<= 3; i
++) {
547 const link
= links
.appendChild(document
.createElement("a"));
548 const name
= (link
.dataset
.l10nName
= `link${i}`);
549 link
.href
= config
[name
];
550 link
.setAttribute("value", name
);
552 document
.l10n
.setAttributes(links
, config
.linksId
);