/* 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";
import { Preferences } from "chrome://global/content/preferences/Preferences.mjs";
import { SettingGroupManager } from "chrome://browser/content/preferences/config/SettingGroupManager.mjs";
import { OnDeviceModelManager } from "chrome://browser/content/preferences/OnDeviceModelManager.mjs";
/**
* @import { OnDeviceModelFeaturesEnum } from "chrome://browser/content/preferences/OnDeviceModelManager.mjs"
* @typedef {typeof AiControlGlobalStates[keyof typeof AiControlGlobalStates]} AiControlGlobalStatesEnum
*/
const { CommonDialog } = ChromeUtils.importESModule(
"resource://gre/modules/CommonDialog.sys.mjs"
);
const XPCOMUtils = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
).XPCOMUtils;
const lazy = XPCOMUtils.declareLazy({
AIWindow:
"moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs",
GenAI: "resource:///modules/GenAI.sys.mjs",
MemoryStore:
"moz-src:///browser/components/aiwindow/services/MemoryStore.sys.mjs",
});
Preferences.addAll([
// browser.ai.control.* prefs defined in main.js
{ id: "browser.ml.chat.provider", type: "string" },
{ id: "browser.smartwindow.apiKey", type: "string" },
{ id: "browser.smartwindow.enabled", type: "bool" },
{ id: "browser.smartwindow.endpoint", type: "string" },
{ id: "browser.smartwindow.firstrun.modelChoice", type: "string" },
{ id: "browser.smartwindow.memories", type: "bool" },
{ id: "browser.smartwindow.model", type: "string" },
{ id: "browser.smartwindow.preferences.endpoint", type: "string" },
{ id: "browser.smartwindow.tos.consentTime", type: "int" },
{ id: "browser.preferences.aiControls.showUnavailable", type: "bool" },
]);
Preferences.addSetting({
id: "aiControlsShowUnavailable",
pref: "browser.preferences.aiControls.showUnavailable",
});
Preferences.addSetting({ id: "aiControlsDescription" });
Preferences.addSetting({ id: "blockAiGroup" });
Preferences.addSetting({ id: "blockAiDescription" });
Preferences.addSetting({ id: "onDeviceFieldset" });
Preferences.addSetting({
id: "onDeviceGroup",
deps: [
"aiControlTranslationsSelect",
"aiControlPdfjsAltTextSelect",
"aiControlSmartTabGroupsSelect",
"aiControlLinkPreviewKeyPointsSelect",
],
getControlConfig(config, deps) {
for (let option of config.options) {
let control = option.items[0];
if (control.id in deps) {
option.controlAttrs = option.controlAttrs || {};
option.controlAttrs.class = deps[control.id].visible
? ""
: "setting-hidden";
}
}
return config;
},
});
Preferences.addSetting({ id: "aiStatesDescription" });
Preferences.addSetting({ id: "sidebarChatbotFieldset" });
Preferences.addSetting({
id: "aiBlockedMessage",
deps: ["aiControlDefaultToggle"],
visible: deps => deps.aiControlDefaultToggle.value,
});
const AiControlStates = Object.freeze({
default: "default",
enabled: "enabled",
blocked: "blocked",
available: "available",
});
const AiControlGlobalStates = Object.freeze({
available: "available",
blocked: "blocked",
});
/**
* @param {AiControlGlobalStatesEnum} state
*/
function updateAiControlDefault(state) {
let isBlocked = state == AiControlGlobalStates.blocked;
for (let feature of Object.values(OnDeviceModelManager.features)) {
if (isBlocked) {
// Reset to default (blocked) state unless it was already blocked.
OnDeviceModelManager.disable(feature);
} else if (!isBlocked && !OnDeviceModelManager.isEnabled(feature)) {
// Reset to default (available) state unless it was manually enabled.
OnDeviceModelManager.reset(feature);
}
}
if (isBlocked) {
Services.prefs.setStringPref(
"browser.ai.control.default",
AiControlGlobalStates.blocked
);
}
// There's no feature-specific dropdown for extensions since it's still a
// trial feature, so just turn it off/on based on the global switch.
Services.prefs.setBoolPref("extensions.ml.enabled", !isBlocked);
Glean.browser.globalAiControlToggled.record({ blocked: isBlocked });
}
class BlockAiConfirmationDialog extends MozLitElement {
get dialog() {
return this.renderRoot.querySelector("dialog");
}
get confirmButton() {
return this.renderRoot.querySelector('moz-button[type="primary"]');
}
get cancelButton() {
return this.renderRoot.querySelector('moz-button:not([type="primary"])');
}
async showModal() {
await this.updateComplete;
this.dialog.showModal();
}
handleCancel() {
this.dialog.close();
}
handleConfirm() {
this.dialog.close();
updateAiControlDefault(AiControlGlobalStates.blocked);
}
render() {
return html`
`;
}
}
customElements.define(
"block-ai-confirmation-dialog",
BlockAiConfirmationDialog
);
const AI_CONTROL_OPTIONS = [
{
value: AiControlStates.available,
l10nId: "preferences-ai-controls-state-available",
},
{
value: AiControlStates.enabled,
l10nId: "preferences-ai-controls-state-enabled",
},
{
value: AiControlStates.blocked,
l10nId: "preferences-ai-controls-state-blocked",
},
];
/**
* Validates that a URL is trustworthy (HTTPS or localhost).
*
* @param {string} url - The URL to validate.
* @returns {boolean} True if URL is HTTPS or localhost, otherwise false.
*/
function validateEndpointUrl(url) {
if (!url) {
return false;
}
try {
const uri = Services.io.newURI(url);
const principal = Services.scriptSecurityManager.createContentPrincipal(
uri,
{}
);
return principal.isOriginPotentiallyTrustworthy;
} catch {
return false;
}
}
Preferences.addSetting({
id: "aiControlDefaultToggle",
pref: "browser.ai.control.default",
setup() {
document.body.append(
document.createElement("block-ai-confirmation-dialog")
);
},
get: prefVal =>
prefVal in AiControlGlobalStates
? prefVal == AiControlGlobalStates.blocked
: AiControlGlobalStates.available,
set(inputVal, _, setting) {
if (inputVal) {
// Restore the toggle to not pressed, we're opening a dialog
setting.onChange();
let dialog = /** @type {BlockAiConfirmationDialog} */ (
document.querySelector("block-ai-confirmation-dialog")
);
dialog.showModal();
} else {
updateAiControlDefault(AiControlGlobalStates.available);
}
return AiControlGlobalStates.available;
},
});
/**
* @param {object} options
* @param {string} options.id Setting id to create
* @param {string} options.pref Pref id for the state
* @param {OnDeviceModelFeaturesEnum} options.feature Feature id for removing models
* @param {boolean} [options.supportsEnabled] If the feature supports the "enabled" state
* @param {SettingConfig['getControlConfig']} [options.getControlConfig] A getControlConfig implementation.
*/
function makeAiControlSetting({
id,
pref,
feature,
supportsEnabled = true,
getControlConfig,
}) {
Preferences.addSetting({
id,
pref,
deps: ["aiControlDefault", "aiControlsShowUnavailable"],
setup(emitChange) {
/**
* @param {nsISupports} _
* @param {string} __
* @param {string} changedFeature
*/
const featureChange = (_, __, changedFeature) => {
if (changedFeature == feature) {
emitChange();
}
};
Services.obs.addObserver(featureChange, "OnDeviceModelManagerChange");
return () =>
Services.obs.removeObserver(
featureChange,
"OnDeviceModelManagerChange"
);
},
get(prefVal, deps) {
if (
prefVal == AiControlStates.blocked ||
(prefVal == AiControlStates.default &&
deps.aiControlDefault.value == AiControlGlobalStates.blocked) ||
OnDeviceModelManager.isBlocked(feature)
) {
return AiControlStates.blocked;
}
if (
supportsEnabled &&
(prefVal == AiControlStates.enabled ||
OnDeviceModelManager.isEnabled(feature))
) {
return AiControlStates.enabled;
}
return AiControlStates.available;
},
set(prefVal) {
if (prefVal == AiControlStates.available) {
OnDeviceModelManager.reset(feature);
} else if (prefVal == AiControlStates.enabled) {
OnDeviceModelManager.enable(feature);
} else if (prefVal == AiControlStates.blocked) {
OnDeviceModelManager.disable(feature);
}
return prefVal;
},
disabled() {
return OnDeviceModelManager.isManagedByPolicy(feature);
},
visible(deps) {
return (
OnDeviceModelManager.isAllowed(feature) ||
deps.aiControlsShowUnavailable.value
);
},
getControlConfig,
});
}
makeAiControlSetting({
id: "aiControlTranslationsSelect",
pref: "browser.ai.control.translations",
feature: OnDeviceModelManager.features.Translations,
supportsEnabled: false,
getControlConfig(config, _, setting) {
let isBlocked = setting.value == AiControlStates.blocked;
let moreSettingsLink = config.options.at(-1);
moreSettingsLink.hidden = isBlocked;
config.supportPage = isBlocked ? "website-translation" : null;
return config;
},
});
makeAiControlSetting({
id: "aiControlPdfjsAltTextSelect",
pref: "browser.ai.control.pdfjsAltText",
feature: OnDeviceModelManager.features.PdfAltText,
});
makeAiControlSetting({
id: "aiControlSmartTabGroupsSelect",
pref: "browser.ai.control.smartTabGroups",
feature: OnDeviceModelManager.features.TabGroups,
});
makeAiControlSetting({
id: "aiControlLinkPreviewKeyPointsSelect",
pref: "browser.ai.control.linkPreviewKeyPoints",
feature: OnDeviceModelManager.features.KeyPoints,
});
// sidebar chatbot
Preferences.addSetting({ id: "chatbotProviderItem" });
Preferences.addSetting({
id: "chatbotProvider",
pref: "browser.ml.chat.provider",
});
Preferences.addSetting(
/** @type {{ feature: OnDeviceModelFeaturesEnum } & SettingConfig } */ ({
id: "aiControlSidebarChatbotSelect",
pref: "browser.ai.control.sidebarChatbot",
deps: ["aiControlDefault", "chatbotProvider", "aiControlsShowUnavailable"],
feature: OnDeviceModelManager.features.SidebarChatbot,
setup(emitChange) {
lazy.GenAI.init();
/**
* @param {nsISupports} _
* @param {string} __
* @param {string} changedFeature
*/
const featureChange = (_, __, changedFeature) => {
if (changedFeature == this.feature) {
emitChange();
}
};
Services.obs.addObserver(featureChange, "OnDeviceModelManagerChange");
return () =>
Services.obs.removeObserver(
featureChange,
"OnDeviceModelManagerChange"
);
},
get(prefVal, deps) {
if (
prefVal == AiControlStates.blocked ||
(prefVal == AiControlStates.default &&
deps.aiControlDefault.value == AiControlGlobalStates.blocked) ||
OnDeviceModelManager.isBlocked(this.feature)
) {
return AiControlStates.blocked;
}
return deps.chatbotProvider.value || AiControlStates.available;
},
set(inputVal, deps) {
if (inputVal == AiControlStates.blocked) {
OnDeviceModelManager.disable(this.feature);
return inputVal;
}
if (inputVal == AiControlStates.available) {
OnDeviceModelManager.reset(this.feature);
return inputVal;
}
if (inputVal) {
// Enable the chatbot sidebar so it can be used with this provider.
OnDeviceModelManager.enable(this.feature);
deps.chatbotProvider.value = inputVal;
}
return AiControlStates.enabled;
},
disabled() {
return OnDeviceModelManager.isManagedByPolicy(this.feature);
},
visible(deps) {
return (
OnDeviceModelManager.isAllowed(this.feature) ||
deps.aiControlsShowUnavailable.value
);
},
getControlConfig(config, _, setting) {
let providerUrl = setting.value;
let options = config.options.slice(0, 3);
lazy.GenAI.chatProviders.forEach((provider, url) => {
let isSelected = url == providerUrl;
// @ts-expect-error provider.hidden isn't in the typing
if (!isSelected && provider.hidden) {
return;
}
options.push({
value: url,
controlAttrs: { label: provider.name },
});
});
if (!options.some(opt => opt.value == providerUrl)) {
options.push({
value: providerUrl,
controlAttrs: { label: providerUrl },
});
}
return {
...config,
options,
};
},
})
);
Preferences.addSetting({
id: "smartWindowEnabled",
pref: "browser.smartwindow.enabled",
});
Preferences.addSetting({
id: "smartWindowFieldset",
deps: ["smartWindowEnabled"],
visible: deps => {
return deps.smartWindowEnabled.value;
},
});
Preferences.addSetting({
id: "aiFeaturesSmartWindowGroup",
});
Preferences.addSetting({
id: "smartWindowToConsentTime",
pref: "browser.smartwindow.tos.consentTime",
});
Preferences.addSetting({
id: "activateSmartWindowLink",
deps: ["smartWindowEnabled", "smartWindowToConsentTime"],
visible: deps => {
return (
deps.smartWindowEnabled.value && !deps.smartWindowToConsentTime.value
);
},
onUserClick(e) {
e.preventDefault();
const browser = window.browsingContext.embedderElement;
lazy.AIWindow.launchWindow(browser, true);
},
});
Preferences.addSetting({
id: "personalizeSmartWindowButton",
deps: ["smartWindowEnabled", "smartWindowToConsentTime"],
visible: deps => {
return deps.smartWindowEnabled.value && deps.smartWindowToConsentTime.value;
},
onUserClick(e) {
e.preventDefault();
window.gotoPref("panePersonalizeSmartWindow");
},
});
Preferences.addSetting({
id: "smartWindowEndpoint",
pref: "browser.smartwindow.endpoint",
});
Preferences.addSetting({
id: "smartWindowModel",
pref: "browser.smartwindow.model",
});
Preferences.addSetting({
id: "smartWindowApiKey",
pref: "browser.smartwindow.apiKey",
});
Preferences.addSetting({
id: "smartWindowPreferencesEndpoint",
pref: "browser.smartwindow.preferences.endpoint",
});
Preferences.addSetting({
id: "smartWindowFirstRunModelChoice",
pref: "browser.smartwindow.firstrun.modelChoice",
});
Preferences.addSetting({
id: "modelSelection",
deps: [
"smartWindowModel",
"smartWindowFirstRunModelChoice",
"smartWindowEndpoint",
"smartWindowPreferencesEndpoint",
],
get(_, deps) {
const modelChoice = deps.smartWindowFirstRunModelChoice.value;
if (modelChoice) {
return modelChoice;
}
// Fall back to no selection
return null;
},
set(value, deps) {
// Save model selection
// Preset models save pref immediately, "Custom" waits for clicking the Save button
if (value !== "0") {
// Switching to preset
const endpointEl = document.getElementById("customModelEndpoint");
const currentEndpoint = endpointEl?.value?.trim();
if (currentEndpoint) {
deps.smartWindowPreferencesEndpoint.value = currentEndpoint;
}
Services.prefs.clearUserPref("browser.smartwindow.endpoint");
}
// Write index to firstrun.modelChoice
deps.smartWindowFirstRunModelChoice.value = value;
},
});
Preferences.addSetting({
id: "customModelName",
deps: [
"smartWindowFirstRunModelChoice",
"smartWindowModel",
"smartWindowEndpoint",
"smartWindowPreferencesEndpoint",
],
visible: deps => deps.smartWindowFirstRunModelChoice.value === "0",
get(_, deps) {
return deps.smartWindowModel.value || "";
},
});
Preferences.addSetting({
id: "customModelEndpoint",
deps: [
"smartWindowFirstRunModelChoice",
"smartWindowEndpoint",
"smartWindowPreferencesEndpoint",
],
visible: deps => deps.smartWindowFirstRunModelChoice.value === "0",
get(_, deps) {
const defaultEndpoint = Services.prefs
.getDefaultBranch("")
.getStringPref("browser.smartwindow.endpoint", "");
// Show saved endpoint if user has set a custom value if its different from default
if (
deps.smartWindowEndpoint.value &&
deps.smartWindowEndpoint.value !== defaultEndpoint
) {
return deps.smartWindowEndpoint.value;
}
// Show backup endpoint when switching back to custom
if (deps.smartWindowPreferencesEndpoint.value) {
return deps.smartWindowPreferencesEndpoint.value;
}
return "";
},
onUserChange(value) {
const saveButton = document.getElementById("customModelSaveButton");
if (saveButton) {
saveButton.disabled = !validateEndpointUrl(value?.trim());
}
},
});
Preferences.addSetting({
id: "customModelAuthToken",
deps: ["smartWindowFirstRunModelChoice", "smartWindowApiKey"],
visible: deps => deps.smartWindowFirstRunModelChoice.value === "0",
get(_, deps) {
if (deps.smartWindowApiKey.value) {
return deps.smartWindowApiKey.value;
}
return "";
},
});
Preferences.addSetting({
id: "customModelHelpLink",
deps: ["smartWindowFirstRunModelChoice"],
visible: deps => deps.smartWindowFirstRunModelChoice.value === "0",
});
Preferences.addSetting({
id: "customModelSaveButton",
deps: [
"smartWindowFirstRunModelChoice",
"smartWindowModel",
"smartWindowEndpoint",
"smartWindowApiKey",
"smartWindowPreferencesEndpoint",
],
visible: deps => deps.smartWindowFirstRunModelChoice.value === "0",
disabled() {
// Read from input element since setting only updates on Save button
const endpoint = document
.getElementById("customModelEndpoint")
?.value?.trim();
return !validateEndpointUrl(endpoint);
},
onUserClick(e, deps) {
const doc = e.target.ownerDocument;
// TODO: (bug 2014287) Utilize ways of handling the input changes instead of using document.getElementById()
const modelName =
doc.getElementById("customModelName")?.value?.trim() || "";
const modelEndpoint =
doc.getElementById("customModelEndpoint")?.value?.trim() || "";
const modelAuthToken =
doc.getElementById("customModelAuthToken")?.value?.trim() || "";
if (!validateEndpointUrl(modelEndpoint)) {
console.warn("For custom setting URL must be HTTPS or localhost");
e.target.disabled = true;
return;
}
// custom uses .model pref
deps.smartWindowModel.value = modelName;
deps.smartWindowEndpoint.value = modelEndpoint;
deps.smartWindowApiKey.value = modelAuthToken;
// Update backup custom endpoint when saving
deps.smartWindowPreferencesEndpoint.value = modelEndpoint;
},
});
Preferences.addSetting({ id: "learnFromActivityWrapper" });
Preferences.addSetting({
id: "learnFromActivity",
pref: "browser.smartwindow.memories",
});
Preferences.addSetting({
id: "manageMemoriesButton",
onUserClick(e) {
e.preventDefault();
window.gotoPref("manageMemories");
},
});
Preferences.addSetting({ id: "memories" });
Preferences.addSetting({
id: "memory-item",
onUserClick(e) {
const action = e.target.getAttribute("action");
const memoryId = e.target.getAttribute("memoryId");
if (action === "delete") {
lazy.MemoryStore.hardDeleteMemory(memoryId);
}
},
});
Preferences.addSetting({
id: "deleteAllMemoriesButton",
async onUserClick() {
const memories = await lazy.MemoryStore.getMemories();
if (!memories.length) {
return;
}
const [title, message, deleteButton, cancelButton] =
await document.l10n.formatValues([
{ id: "ai-window-delete-all-memories-title" },
{ id: "ai-window-delete-all-memories-message" },
{ id: "ai-window-delete-all-memories-confirm" },
{ id: "ai-window-delete-all-memories-cancel" },
]);
const buttonFlags =
Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_IS_STRING +
Services.prompt.BUTTON_POS_0_DEFAULT;
const result = await Services.prompt.asyncConfirmEx(
window.browsingContext,
Services.prompt.MODAL_TYPE_CONTENT,
title,
message,
buttonFlags,
deleteButton,
cancelButton,
null,
null,
false,
{
useTitle: true,
headerIconCSSValue: CommonDialog.DEFAULT_APP_ICON_CSS,
}
);
if (result.get("buttonNumClicked") === 0) {
for (const memory of memories) {
try {
await lazy.MemoryStore.hardDeleteMemory(memory.id);
} catch (err) {
console.error("Failed to delete memory:", memory.id, err);
}
}
}
},
});
Preferences.addSetting({ id: "no-memories-stored" });
Preferences.addSetting({ id: "memories-list-header" });
Preferences.addSetting(
class extends Preferences.AsyncSetting {
static id = "memoriesList";
setup() {
Services.obs.addObserver(this.emitChange, "memory-store-changed");
Services.prefs.addObserver(
"browser.smartwindow.memories",
this.emitChange
);
return () => {
Services.obs.removeObserver(this.emitChange, "memory-store-changed");
Services.prefs.removeObserver(
"browser.smartwindow.memories",
this.emitChange
);
};
}
async getMemories() {
return lazy.MemoryStore.getMemories();
}
async getControlConfig() {
const memories = await this.getMemories();
const isLearningEnabled = Services.prefs.getBoolPref(
"browser.smartwindow.memories",
false
);
if (!memories.length) {
return {
items: [
{
id: "no-memories-stored",
l10nId: isLearningEnabled
? "ai-window-no-memories"
: "ai-window-no-memories-learning-off",
control: "placeholder-message",
},
],
};
}
return {
items: [
{
id: "memories-list-header",
control: "moz-box-item",
items: [
{
id: "deleteAllMemoriesButton",
control: "moz-button",
l10nId: "ai-window-delete-all-memories-button",
iconSrc: "chrome://global/skin/icons/delete.svg",
},
],
},
...memories.map((memory, index) => ({
id: `memory-item`,
key: `memory-${index}`,
control: "moz-box-item",
controlAttrs: {
".label": memory.memory_summary,
},
options: [
{
control: "moz-button",
iconSrc: "chrome://global/skin/icons/delete.svg",
l10nId: "ai-window-memory-delete-button",
l10nArgs: { label: memory.memory_summary },
controlAttrs: {
slot: "actions-start",
action: "delete",
memoryId: memory.id,
},
},
],
})),
],
};
}
}
);
SettingGroupManager.registerGroups({
aiControlsDescription: {
card: "never",
items: [
{
id: "aiControlsDescription",
control: "moz-card",
controlAttrs: {
class: "ai-controls-description",
},
options: [
{
control: "p",
options: [
{
control: "span",
l10nId: "preferences-ai-controls-description",
},
{
control: "span",
controlAttrs: {
".textContent": " ",
},
},
{
control: "a",
controlAttrs: {
is: "moz-support-link",
"support-page": "firefox-ai-controls",
},
},
],
},
{
control: "img",
controlAttrs: {
src: "chrome://browser/skin/preferences/fox-ai.svg",
},
},
],
},
],
},
aiStatesDescription: {
card: "never",
items: [
{
id: "aiStatesDescription",
control: "footer",
controlAttrs: {
class: "text-deemphasized",
},
options: [
{
control: "span",
l10nId: "preferences-ai-controls-state-description-before",
},
{
control: "ul",
options: [
{
control: "li",
l10nId: "preferences-ai-controls-state-description-available",
},
{
control: "li",
l10nId: "preferences-ai-controls-state-description-enabled",
},
{
control: "li",
l10nId: "preferences-ai-controls-state-description-blocked",
},
],
},
],
},
],
},
aiFeatures: {
card: "always",
items: [
{
id: "blockAiGroup",
control: "moz-box-item",
items: [
{
id: "aiControlDefaultToggle",
l10nId: "preferences-ai-controls-block-ai",
control: "moz-toggle",
controlAttrs: {
headinglevel: 2,
inputlayout: "inline-end",
},
options: [
{
l10nId: "preferences-ai-controls-block-ai-description",
control: "span",
slot: "description",
options: [
{
control: "a",
controlAttrs: {
"data-l10n-name": "link",
"support-page": "firefox-ai-controls",
is: "moz-support-link",
},
},
],
},
],
},
{
id: "aiBlockedMessage",
control: "moz-message-bar",
l10nId: "preferences-ai-controls-blocked-message",
},
],
},
{
id: "onDeviceFieldset",
l10nId: "preferences-ai-controls-on-device-group",
supportPage: "on-device-models",
control: "moz-fieldset",
controlAttrs: {
headinglevel: 2,
iconsrc: "chrome://browser/skin/device-desktop.svg",
},
items: [
{
id: "onDeviceGroup",
control: "moz-box-group",
options: [
{
control: "moz-box-item",
items: [
{
id: "aiControlTranslationsSelect",
l10nId: "preferences-ai-controls-translations-control",
control: "moz-select",
controlAttrs: {
inputlayout: "inline-end",
},
options: [
...AI_CONTROL_OPTIONS.filter(
opt => opt.value != AiControlStates.enabled
),
{
control: "a",
l10nId:
"preferences-ai-controls-translations-more-link",
slot: "support-link",
controlAttrs: {
href: "#general-translations",
},
},
],
},
],
},
{
control: "moz-box-item",
items: [
{
id: "aiControlPdfjsAltTextSelect",
l10nId: "preferences-ai-controls-pdfjs-control",
control: "moz-select",
controlAttrs: {
inputlayout: "inline-end",
},
supportPage: "pdf-alt-text",
options: [...AI_CONTROL_OPTIONS],
},
],
},
{
control: "moz-box-item",
items: [
{
id: "aiControlSmartTabGroupsSelect",
l10nId:
"preferences-ai-controls-tab-group-suggestions-control",
control: "moz-select",
controlAttrs: {
inputlayout: "inline-end",
},
supportPage: "how-use-ai-enhanced-tab-groups",
options: [...AI_CONTROL_OPTIONS],
},
],
},
{
control: "moz-box-item",
items: [
{
id: "aiControlLinkPreviewKeyPointsSelect",
l10nId: "preferences-ai-controls-key-points-control",
control: "moz-select",
controlAttrs: {
inputlayout: "inline-end",
},
supportPage: "use-link-previews-firefox",
options: [...AI_CONTROL_OPTIONS],
},
],
},
],
},
],
},
{
id: "smartWindowFieldset",
l10nId: "ai-window-features-group",
control: "moz-fieldset",
controlAttrs: {
headinglevel: 2,
},
items: [
{
id: "aiFeaturesSmartWindowGroup",
control: "moz-box-group",
items: [
{
id: "activateSmartWindowLink",
l10nId: "ai-window-activate-link",
control: "moz-box-link",
},
{
id: "personalizeSmartWindowButton",
l10nId: "ai-window-personalize-button",
control: "moz-box-button",
},
],
},
],
},
{
id: "sidebarChatbotFieldset",
control: "moz-fieldset",
l10nId: "preferences-ai-controls-sidebar-chatbot-group",
supportPage: "ai-chatbot",
controlAttrs: {
headinglevel: 2,
iconsrc: "chrome://browser/skin/sidebars.svg",
},
items: [
{
id: "chatbotProviderItem",
control: "moz-box-item",
items: [
{
id: "aiControlSidebarChatbotSelect",
l10nId: "preferences-ai-controls-sidebar-chatbot-control",
control: "moz-select",
controlAttrs: {
inputlayout: "inline-end",
},
options: [
{
l10nId: "preferences-ai-controls-state-available",
value: AiControlStates.available,
},
{
l10nId: "preferences-ai-controls-state-blocked",
value: AiControlStates.blocked,
},
{ control: "hr" },
],
},
],
},
],
},
],
},
assistantModelGroup: {
l10nId: "smart-window-model-section",
headingLevel: 2,
supportPage: "smart-window-model",
items: [
{
id: "modelSelection",
control: "moz-radio-group",
options: [
{
value: "1",
l10nId: "smart-window-model-fast",
l10nArgs: { modelName: "gemini-flash-lite" },
},
{
value: "2",
l10nId: "smart-window-model-flexible",
l10nArgs: { modelName: "Qwen3-235B-A22B-throughput" },
},
{
value: "3",
l10nId: "smart-window-model-personal",
l10nArgs: { modelName: "gpt-oss-120b" },
},
{
value: "0",
l10nId: "smart-window-model-custom",
items: [
{
id: "customModelName",
l10nId: "smart-window-model-custom-name",
control: "moz-input-text",
},
{
id: "customModelEndpoint",
l10nId: "smart-window-model-custom-url",
control: "moz-input-url",
},
{
id: "customModelAuthToken",
l10nId: "smart-window-model-custom-token",
control: "moz-input-password",
},
{
id: "customModelHelpLink",
control: "moz-message-bar",
l10nId: "smart-window-model-custom-help",
controlAttrs: {
type: "info",
},
options: [
{
control: "a",
l10nId: "smart-window-model-custom-more-link",
slot: "support-link",
controlAttrs: {
href: "",
},
},
],
},
{
id: "customModelSaveButton",
control: "moz-button",
l10nId: "smart-window-model-custom-save",
controlAttrs: {
type: "primary",
},
},
],
},
],
},
],
},
memoriesGroup: {
l10nId: "ai-window-memories-section",
headingLevel: 2,
// TODO: Finalize SUMO support page slug (GENAI-3016)
supportPage: "smart-window-memories",
items: [
{
id: "memories",
control: "moz-box-group",
items: [
{
id: "learnFromActivityWrapper",
control: "moz-box-item",
items: [
{
id: "learnFromActivity",
l10nId: "ai-window-learn-from-activity",
control: "moz-checkbox",
},
],
},
{
id: "manageMemoriesButton",
l10nId: "ai-window-manage-memories-button",
control: "moz-box-button",
},
],
},
],
},
manageMemories: {
items: [
{
id: "memoriesList",
control: "moz-box-group",
controlAttrs: {
type: "list",
},
},
],
},
});