Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/browser/components/preferences/config/   (Firefox Browser Version 153.0.1©)  Datei vom 27.6.2026 mit Größe 48 kB image not shown  

Quelle  aiFeatures.mjs  Sprache: unbekannt

 
Spracherkennung für: .mjs vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

/* 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, nothing } 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",
  ChatStore:
    "moz-src:///browser/components/aiwindow/ui/modules/ChatStore.sys.mjs",
  GenAI: "resource:///modules/GenAI.sys.mjs",
  MemoryStore:
    "moz-src:///browser/components/aiwindow/services/MemoryStore.sys.mjs",
  getCachedModelsData:
    "moz-src:///browser/components/aiwindow/models/Utils.sys.mjs",
});

let previousAssistantModel = "No model";

Preferences.addAll([
  // browser.ai.control.* prefs defined in main.js
  { id: "browser.ml.chat.provider", type: "string" },
  { id: "browser.ml.chat.shortcuts.smartwindow", type: "bool" },
  { 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.generateFromConversation", type: "bool" },
  { id: "browser.smartwindow.memories.generateFromHistory", type: "bool" },
  { id: "browser.smartwindow.model", type: "string" },
  { id: "browser.smartwindow.customEndpoint", type: "string" },
  { id: "browser.smartwindow.isDefaultWindow", type: "bool" },
  { id: "browser.smartwindow.sidebar.openByDefault", type: "bool" },
  { 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 => {
    return 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.block(feature);
    } else if (!isBlocked && !OnDeviceModelManager.isEnabled(feature)) {
      // Reset to default (available) state unless it was manually enabled.
      OnDeviceModelManager.makeAvailable(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 {
  static properties = {
    headingL10nId: { type: String },
    descriptionL10nId: { type: String },
    isGlobal: { type: Boolean },
  };

  #resolvers = Promise.withResolvers();
  #confirmed = false;

  constructor() {
    super();
    this.isGlobal = true;
  }

  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"])');
  }

  /**
   * @param {object} options
   * @param {boolean} options.all - Show the global block dialog with all features listed
   * @param {string} [options.headingL10nId] - Custom heading l10n ID for feature-specific dialogs
   * @param {string} [options.descriptionL10nId] - Custom description l10n ID for feature-specific dialogs
   * @returns {Promise<boolean>} - Resolves true if the user confirmed, false if cancelled
   */
  showModal({ all, headingL10nId, descriptionL10nId }) {
    this.#resolvers = Promise.withResolvers();
    this.#confirmed = false;
    this.isGlobal = !!all;
    this.headingL10nId = headingL10nId;
    this.descriptionL10nId = descriptionL10nId;
    this.updateComplete.then(() => this.dialog.showModal());
    return this.#resolvers.promise;
  }

  handleCancel() {
    this.#confirmed = false;
    this.dialog.close();
  }

  handleConfirm() {
    this.#confirmed = true;
    this.dialog.close();
  }

  globalTemplate() {
    return html`
      <p
        data-l10n-id="preferences-ai-controls-block-confirmation-description"
      ></p>
      <p
        class="ul-prefix-p"
        data-l10n-id="preferences-ai-controls-block-confirmation-features-start"
      ></p>
      <ul>
        <li
          data-l10n-id="preferences-ai-controls-block-confirmation-translations"
        ></li>
        <li
          data-l10n-id="preferences-ai-controls-block-confirmation-pdfjs"
        ></li>
        <li
          data-l10n-id="preferences-ai-controls-block-confirmation-tab-group-suggestions"
        ></li>
        <li
          data-l10n-id="preferences-ai-controls-block-confirmation-key-points"
        ></li>
        <li
          data-l10n-id="preferences-ai-controls-block-confirmation-smart-window"
        ></li>
        <li
          data-l10n-id="preferences-ai-controls-block-confirmation-sidebar-chatbot"
        ></li>
      </ul>
      <p
        data-l10n-id="preferences-ai-controls-block-confirmation-features-after"
      ></p>
      <a is="moz-support-link" support-page="firefox-ai-controls"></a>
    `;
  }

  descriptionTemplate() {
    return html`<p data-l10n-id=${this.descriptionL10nId}></p>`;
  }

  onToggle() {
    if (!this.dialog.open) {
      this.#resolvers.resolve(this.#confirmed);
    }
  }

  render() {
    return html`
      <link
        rel="stylesheet"
        href="chrome://global/skin/in-content/common.css"
      />
      <link
        rel="stylesheet"
        href="chrome://browser/skin/preferences/preferences.css"
      />
      <link
        rel="stylesheet"
        href="chrome://browser/content/preferences/config/block-ai-confirmation-dialog.css"
      />
      <dialog
        aria-labelledby="heading"
        aria-describedby="content"
        @toggle=${this.onToggle}
      >
        <div class="dialog-header">
          ${this.isGlobal
            ? html`<img
                class="dialog-header-icon"
                src="chrome://global/skin/icons/block.svg"
                alt=""
              />`
            : nothing}
          <h2
            id="heading"
            class="text-box-trim-start"
            data-l10n-id=${this.isGlobal
              ? "preferences-ai-controls-block-confirmation-heading"
              : this.headingL10nId}
          ></h2>
        </div>
        <div id="content" class="dialog-body">
          ${this.isGlobal ? this.globalTemplate() : this.descriptionTemplate()}
        </div>
        <moz-button-group>
          <moz-button
            data-l10n-id="preferences-ai-controls-block-confirmation-cancel"
            @click=${this.handleCancel}
          ></moz-button>
          <moz-button
            autofocus
            type="primary"
            data-l10n-id="preferences-ai-controls-block-confirmation-confirm"
            @click=${this.handleConfirm}
          ></moz-button>
        </moz-button-group>
      </dialog>
    `;
  }
}
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",
  },
];

const modelL10nArgs = key => ({
  model: lazy.getCachedModelsData()[key].model,
  ownerName: lazy.getCachedModelsData()[key].ownerName,
});

/**
 * 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({ all: true }).then(confirmed => {
        if (confirmed) {
          updateAiControlDefault(AiControlGlobalStates.blocked);
        }
      });
    } 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 {SettingConfig['getControlConfig']} [options.getControlConfig] A getControlConfig implementation.
 * @param {() => Promise<boolean>} [options.onBeforeBlock] Optional async callback to show a modal before blocking
 */
function makeAiControlSetting({
  id,
  pref,
  feature,
  getControlConfig,
  onBeforeBlock,
}) {
  function recordTelemetry(selection) {
    Glean.browser.aiControlChanged.record({ feature, selection });
  }

  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) {
      const aiControlState = OnDeviceModelManager.getAiControlState(feature);

      if (
        prefVal == AiControlStates.blocked ||
        (prefVal == AiControlStates.default &&
          deps.aiControlDefault.value == AiControlGlobalStates.blocked) ||
        aiControlState == AiControlStates.blocked
      ) {
        return AiControlStates.blocked;
      }

      if (
        OnDeviceModelManager.hasDistinctEnabledState(feature) &&
        (prefVal == AiControlStates.enabled ||
          aiControlState == AiControlStates.enabled)
      ) {
        return AiControlStates.enabled;
      }

      return AiControlStates.available;
    },
    set(prefVal, _, setting) {
      if (prefVal == AiControlStates.blocked && onBeforeBlock) {
        setting.onChange();
        onBeforeBlock().then(confirmed => {
          if (confirmed) {
            OnDeviceModelManager.block(feature);
            recordTelemetry(AiControlStates.blocked);
          }
        });

        return setting.value;
      }

      if (prefVal == AiControlStates.available) {
        OnDeviceModelManager.makeAvailable(feature);
      } else if (prefVal == AiControlStates.enabled) {
        OnDeviceModelManager.enable(feature);
      } else if (prefVal == AiControlStates.blocked) {
        OnDeviceModelManager.block(feature);
      }
      return prefVal;
    },
    disabled() {
      return OnDeviceModelManager.isManagedByPolicy(feature);
    },
    visible(deps) {
      return (
        OnDeviceModelManager.isAllowed(feature) ||
        deps.aiControlsShowUnavailable.value
      );
    },
    onUserChange(selection, _, setting) {
      // Only record telemetry if the selection was actually saved
      // since selecting "blocked" shows a block confirmation dialog that the user may cancel
      if (selection === setting.value) {
        recordTelemetry(selection);
      }
    },
    getControlConfig(config, deps, setting) {
      if (!OnDeviceModelManager.hasDistinctEnabledState(feature)) {
        config.options = config.options.filter(
          option => option.value != AiControlStates.enabled
        );
      }

      return getControlConfig
        ? getControlConfig(config, deps, setting)
        : config;
    },
  });
}
makeAiControlSetting({
  id: "aiControlTranslationsSelect",
  pref: "browser.ai.control.translations",
  feature: OnDeviceModelManager.features.Translations,
  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) {
      const aiControlState = OnDeviceModelManager.getAiControlState(
        this.feature
      );

      if (
        prefVal == AiControlStates.blocked ||
        (prefVal == AiControlStates.default &&
          deps.aiControlDefault.value == AiControlGlobalStates.blocked) ||
        aiControlState == AiControlStates.blocked
      ) {
        return AiControlStates.blocked;
      }

      return aiControlState == AiControlStates.enabled
        ? deps.chatbotProvider.value
        : aiControlState;
    },
    set(inputVal, deps) {
      if (inputVal == AiControlStates.blocked) {
        OnDeviceModelManager.block(this.feature);
        return inputVal;
      }
      if (inputVal == AiControlStates.available) {
        OnDeviceModelManager.makeAvailable(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
      );
    },
    onUserChange(selection) {
      Glean.browser.aiControlChanged.record({
        feature: OnDeviceModelManager.features.SidebarChatbot,
        selection:
          String(selection) in AiControlStates
            ? selection
            : AiControlStates.enabled,
      });
    },
    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: "smartWindowToConsentTime",
  pref: "browser.smartwindow.tos.consentTime",
});

Preferences.addSetting({
  id: "smartWindowFieldset",
  deps: ["smartWindowEnabled"],
  visible: deps => deps.smartWindowEnabled.value,
});

Preferences.addSetting({
  id: "aiFeaturesSmartWindowGroup",
});

Preferences.addSetting({ id: "smartWindowControlItem" });
makeAiControlSetting({
  id: "aiControlSmartWindowSelect",
  pref: "browser.ai.control.smartWindow",
  feature: OnDeviceModelManager.features.SmartWindow,
  async onBeforeBlock() {
    const hasChats = !!(await lazy.ChatStore.findRecentConversations(1)).length;
    const hasMemories = !!(await lazy.MemoryStore.getMemories()).length;

    // if no data, skip modal
    if (!hasChats && !hasMemories) {
      return true;
    }

    const dialog = /** @type {BlockAiConfirmationDialog} */ (
      document.querySelector("block-ai-confirmation-dialog")
    );
    let descriptionL10nId;

    if (hasChats && hasMemories) {
      descriptionL10nId = "smart-window-block-description-both";
    } else if (hasChats) {
      descriptionL10nId = "smart-window-block-description-chats";
    } else {
      descriptionL10nId = "smart-window-block-description-memories";
    }
    return dialog.showModal({
      all: false,
      headingL10nId: "smart-window-block-title",
      descriptionL10nId,
    });
  },
  getControlConfig(config) {
    let isEnabled = OnDeviceModelManager.isEnabled(
      OnDeviceModelManager.features.SmartWindow
    );

    config.options = AI_CONTROL_OPTIONS.filter(option => {
      if (option.value == AiControlStates.available) {
        return !isEnabled;
      } else if (option.value == AiControlStates.enabled) {
        return isEnabled;
      }
      return true;
    });
    return config;
  },
});
Preferences.addSetting({
  id: "activateSmartWindowLink",
  deps: ["aiControlSmartWindowSelect"],
  visible: deps =>
    deps.aiControlSmartWindowSelect.value === AiControlStates.available,
  onUserClick(e) {
    e.preventDefault();
    const browser = window.browsingContext.embedderElement;
    lazy.AIWindow.launchWindow(browser, true);
  },
});

Preferences.addSetting({
  id: "personalizeSmartWindowButton",
  deps: ["aiControlSmartWindowSelect"],
  visible: deps =>
    deps.aiControlSmartWindowSelect.value == AiControlStates.enabled,
  onUserClick(e) {
    e.preventDefault();
    window.gotoPref("panePersonalizeSmartWindow");
  },
});

Preferences.addSetting({
  id: "openSidebarByDefault",
  pref: "browser.smartwindow.sidebar.openByDefault",
});

Preferences.addSetting({
  id: "smartCursorInSmartWindow",
  pref: "browser.ml.chat.shortcuts.smartwindow",
});

Preferences.addSetting({
  id: "smartWindowIsDefaultWindow",
  pref: "browser.smartwindow.isDefaultWindow",
});

Preferences.addSetting({
  id: "smartWindowModel",
  pref: "browser.smartwindow.model",
});

Preferences.addSetting({
  id: "smartWindowApiKey",
  pref: "browser.smartwindow.apiKey",
});

Preferences.addSetting({
  id: "smartWindowCustomEndpoint",
  pref: "browser.smartwindow.customEndpoint",
});

Preferences.addSetting({
  id: "smartWindowFirstRunModelChoice",
  pref: "browser.smartwindow.firstrun.modelChoice",
});

{
  // Track when the custom radio is selected but not yet saved
  // Defer writing modelChoice = "0" until Save button is clicked
  let customRadioSelected = false;
  Preferences.addSetting({
    id: "modelSelection",
    deps: [
      "smartWindowModel",
      "smartWindowFirstRunModelChoice",
      "smartWindowCustomEndpoint",
    ],
    get(_, deps) {
      if (customRadioSelected) {
        return "0";
      }

      const modelChoice = deps.smartWindowFirstRunModelChoice.value;
      if (modelChoice) {
        return modelChoice;
      }

      // Fall back to no selection
      return null;
    },
    set(value, deps, setting) {
      const prev = deps.smartWindowFirstRunModelChoice.value;
      previousAssistantModel = prev
        ? lazy.getCachedModelsData()[String(prev)].model
        : "No model";

      customRadioSelected = value === "0";
      if (customRadioSelected) {
        // If the user has previously saved a custom model, switching back to
        // the custom radio re-activates that saved configuration so the form
        // reflects the active state and Save stays disabled until edited.
        if (deps.smartWindowCustomEndpoint.value && prev !== "0") {
          deps.smartWindowFirstRunModelChoice.value = "0";
        }
        setting.onChange();
        return;
      }
      // Switching to preset
      deps.smartWindowFirstRunModelChoice.value = value;
    },
    onUserChange(value, _) {
      // sending telemetry only for the preset models
      // custom model telemetry is sent after user hits the save button
      if (value !== "0") {
        const new_model = lazy.getCachedModelsData()[String(value)].model;
        Glean.smartWindow.settingsModel.record({
          previous_model: previousAssistantModel,
          new_model,
        });
        previousAssistantModel = new_model;
      }
    },
  });
}

const CUSTOM_MODEL_FIELD_IDS = new Set([
  "customModelName",
  "customModelEndpoint",
  "customModelAuthToken",
]);

// Tracks which fields the user has actually edited. An input may show a
// fallback value that doesn't match what's saved, so without this the form
// would look unsaved before anyone has typed anything.
const editedCustomModelFields = new WeakSet();

function getCustomModelFieldValue(id, fallback = "") {
  const field = document.getElementById(id);
  if (!field || !editedCustomModelFields.has(field)) {
    return fallback;
  }
  return field.value?.trim() ?? "";
}

function getCustomModelFormValues(deps) {
  return {
    modelName: getCustomModelFieldValue(
      "customModelName",
      deps.smartWindowModel.value || ""
    ),
    endpoint: getCustomModelFieldValue(
      "customModelEndpoint",
      deps.smartWindowCustomEndpoint.value || ""
    ),
    authToken: getCustomModelFieldValue(
      "customModelAuthToken",
      deps.smartWindowApiKey.value || ""
    ),
  };
}

function hasUnsavedCustomModelChanges(deps) {
  // Compare each form value to what is actually saved.
  const { modelName, endpoint, authToken } = getCustomModelFormValues(deps);
  return (
    modelName !== (deps.smartWindowModel.value || "") ||
    endpoint !== (deps.smartWindowCustomEndpoint.value || "") ||
    authToken !== (deps.smartWindowApiKey.value || "")
  );
}

function isCustomModelSaveButtonDisabled(deps) {
  const { endpoint } = getCustomModelFormValues(deps);
  return !validateEndpointUrl(endpoint) || !hasUnsavedCustomModelChanges(deps);
}

// Any edit to a custom-model field re-emits change on the form-row setting;
// the Save button and confirmation depend on it and re-evaluate their
// enabled/disabled states from the live form values.
function setupCustomModelFormChangeListener(emitChange) {
  const handler = e => {
    if (CUSTOM_MODEL_FIELD_IDS.has(e.target?.id)) {
      editedCustomModelFields.add(e.target);
      emitChange();
    }
  };
  document.addEventListener("input", handler);
  document.addEventListener("change", handler);
  return () => {
    document.removeEventListener("input", handler);
    document.removeEventListener("change", handler);
  };
}

Preferences.addSetting({
  id: "customModelName",
  deps: ["smartWindowModel", "modelSelection"],
  visible: deps => deps.modelSelection.value === "0",
  get(_, deps) {
    return deps.smartWindowModel.value || "";
  },
});

Preferences.addSetting({
  id: "customModelEndpoint",
  deps: ["smartWindowCustomEndpoint", "modelSelection"],
  visible: deps => deps.modelSelection.value === "0",
  get(_, deps) {
    return deps.smartWindowCustomEndpoint.value || "";
  },
});

Preferences.addSetting({
  id: "customModelAuthToken",
  deps: ["smartWindowApiKey", "modelSelection"],
  visible: deps => deps.modelSelection.value === "0",
  get(_, deps) {
    if (deps.smartWindowApiKey.value) {
      return deps.smartWindowApiKey.value;
    }
    return "";
  },
});

Preferences.addSetting({
  id: "customModelHelpLink",
  deps: ["modelSelection"],
  visible: deps => deps.modelSelection.value === "0",
});

Preferences.addSetting({
  id: "customModelSaveRow",
  deps: ["modelSelection"],
  visible: deps => deps.modelSelection.value === "0",
  setup: setupCustomModelFormChangeListener,
});

Preferences.addSetting({
  id: "customModelSaveConfirmation",
  deps: [
    "smartWindowFirstRunModelChoice",
    "smartWindowModel",
    "smartWindowApiKey",
    "smartWindowCustomEndpoint",
    "modelSelection",
    "customModelSaveRow",
  ],
  visible: deps =>
    deps.smartWindowFirstRunModelChoice.value === "0" &&
    deps.modelSelection.value === "0" &&
    !hasUnsavedCustomModelChanges(deps),
});

Preferences.addSetting({
  id: "customModelSaveButton",
  deps: [
    "smartWindowFirstRunModelChoice",
    "smartWindowModel",
    "smartWindowApiKey",
    "smartWindowCustomEndpoint",
    "modelSelection",
    "customModelSaveRow",
  ],
  visible: deps => deps.modelSelection.value === "0",
  disabled: isCustomModelSaveButtonDisabled,
  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)) {
      return;
    }

    const new_model = lazy.getCachedModelsData()["0"].model;
    Glean.smartWindow.settingsModel.record({
      previous_model: previousAssistantModel,
      new_model,
    });
    previousAssistantModel = new_model;

    // Save custom selection pref
    deps.smartWindowFirstRunModelChoice.value = "0";
    deps.smartWindowModel.value = modelName;
    deps.smartWindowApiKey.value = modelAuthToken;
    deps.smartWindowCustomEndpoint.value = modelEndpoint;
  },
});

Preferences.addSetting({ id: "learnFromChatActivityWrapper" });
Preferences.addSetting({ id: "learnFromBrowsingActivityWrapper" });
Preferences.addSetting({
  id: "learnFromChatActivity",
  pref: "browser.smartwindow.memories.generateFromConversation",
  onUserChange(val) {
    Glean.smartWindow.settingsMemories.record({
      type: "chat",
      enabled: val,
    });
  },
});
Preferences.addSetting({
  id: "learnFromBrowsingActivity",
  pref: "browser.smartwindow.memories.generateFromHistory",
  onUserChange(val) {
    Glean.smartWindow.settingsMemories.record({
      type: "browsing",
      enabled: val,
    });
  },
});

Preferences.addSetting({
  id: "manageMemoriesButton",
  async onUserClick(e) {
    e.preventDefault();
    window.gotoPref("manageMemories");

    const memories = await lazy.MemoryStore.getMemories();
    Glean.smartWindow.memoriesPanelDisplayed.record({
      source: "settings",
      memories: memories?.length ?? 0,
    });
  },
});

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, "settings");
    }
  },
});

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) {
      Glean.smartWindow.memoriesNuke.record();
      for (const memory of memories) {
        try {
          await lazy.MemoryStore.hardDeleteMemory(memory.id, "settings");
        } 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.generateFromConversation",
        this.emitChange
      );
      Services.prefs.addObserver(
        "browser.smartwindow.memories.generateFromHistory",
        this.emitChange
      );
      return () => {
        Services.obs.removeObserver(this.emitChange, "memory-store-changed");
        Services.prefs.removeObserver(
          "browser.smartwindow.memories.generateFromConversation",
          this.emitChange
        );
        Services.prefs.removeObserver(
          "browser.smartwindow.memories.generateFromHistory",
          this.emitChange
        );
      };
    }

    async getMemories() {
      return lazy.MemoryStore.getMemories();
    }

    async getControlConfig() {
      const memories = await this.getMemories();
      const isLearningEnabled =
        Services.prefs.getBoolPref(
          "browser.smartwindow.memories.generateFromConversation",
          false
        ) ||
        Services.prefs.getBoolPref(
          "browser.smartwindow.memories.generateFromHistory",
          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: 3,
              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",
            controlAttrs: {
              role: "status",
            },
          },
        ],
      },
      {
        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",
        supportPage: "smart-window",
        controlAttrs: {
          headinglevel: 2,
          iconsrc: "chrome://browser/skin/smart-window-mono.svg",
          badge: "beta",
        },
        items: [
          {
            id: "aiFeaturesSmartWindowGroup",
            control: "moz-box-group",
            items: [
              {
                id: "smartWindowControlItem",
                control: "moz-box-item",
                items: [
                  {
                    id: "aiControlSmartWindowSelect",
                    l10nId: "smart-window-select-label",
                    control: "moz-select",
                    controlAttrs: {
                      inputlayout: "inline-end",
                    },
                    options: [...AI_CONTROL_OPTIONS],
                  },
                ],
              },
              {
                id: "activateSmartWindowLink",
                l10nId: "ai-window-activate-link",
                control: "moz-box-link",
              },
              {
                id: "personalizeSmartWindowButton",
                loadPane: "personalizeSmartWindow",
                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/sidebar-collapsed.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" },
                ],
              },
            ],
          },
        ],
      },
    ],
  },
  assistantDefaultGroup: {
    l10nId: "ai-window-default-section",
    headingLevel: 2,
    items: [
      {
        id: "smartWindowIsDefaultWindow",
        l10nId: "ai-window-is-default-window",
        control: "moz-checkbox",
      },
      {
        id: "openSidebarByDefault",
        l10nId: "ai-window-open-sidebar",
        control: "moz-checkbox",
      },
      {
        id: "smartCursorInSmartWindow",
        l10nId: "ai-window-smart-cursor-in-smart-window",
        control: "moz-checkbox",
      },
    ],
  },
  assistantModelGroup: {
    l10nId: "smart-window-model-section",
    headingLevel: 2,
    supportPage: "smart-window-models",
    items: [
      {
        id: "modelSelection",
        control: "moz-radio-group",
        options: [
          {
            value: "1",
            l10nId: "smart-window-model-fast",
            get l10nArgs() {
              return modelL10nArgs("1");
            },
          },
          {
            value: "2",
            l10nId: "smart-window-model-flexible",
            get l10nArgs() {
              return modelL10nArgs("2");
            },
          },
          {
            value: "3",
            l10nId: "smart-window-model-personal",
            get l10nArgs() {
              return modelL10nArgs("3");
            },
          },
          {
            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-info",
                controlAttrs: {
                  type: "info",
                  role: "status",
                },
                options: [
                  {
                    control: "a",
                    l10nId: "smart-window-model-custom-more-link",
                    slot: "support-link",
                    controlAttrs: {
                      is: "moz-support-link",
                      "support-page": "smart-window-byom",
                    },
                  },
                ],
              },
              {
                id: "customModelSaveRow",
                control: "div",
                controlAttrs: {
                  class: "custom-model-save-row",
                },
                items: [
                  {
                    id: "customModelSaveButton",
                    control: "moz-button",
                    l10nId: "smart-window-model-custom-save",
                    controlAttrs: {
                      type: "primary",
                    },
                  },
                  {
                    id: "customModelSaveConfirmation",
                    control: "span",
                    controlAttrs: {
                      class: "custom-model-save-confirmation",
                      role: "status",
                    },
                    options: [
                      {
                        control: "img",
                        controlAttrs: {
                          class: "custom-model-save-confirmation-icon",
                          src: "chrome://global/skin/icons/check-filled.svg",
                          alt: "",
                        },
                      },
                      {
                        control: "span",
                        l10nId: "smart-window-model-custom-save-confirmation",
                      },
                    ],
                  },
                ],
              },
            ],
          },
        ],
      },
    ],
  },
  memoriesGroup: {
    l10nId: "ai-window-memories-section",
    headingLevel: 2,
    supportPage: "smart-window-memories",
    items: [
      {
        id: "memories",
        control: "moz-box-group",
        items: [
          {
            id: "learnFromChatActivityWrapper",
            control: "moz-box-item",
            items: [
              {
                id: "learnFromChatActivity",
                l10nId: "ai-window-learn-from-chat-activity",
                control: "moz-checkbox",
              },
            ],
          },
          {
            id: "learnFromBrowsingActivityWrapper",
            control: "moz-box-item",
            items: [
              {
                id: "learnFromBrowsingActivity",
                l10nId: "ai-window-learn-from-browsing-activity",
                control: "moz-checkbox",
              },
            ],
          },
          {
            id: "manageMemoriesButton",
            loadPane: "manageMemories",
            l10nId: "ai-window-manage-memories-button",
            control: "moz-box-button",
          },
        ],
      },
    ],
  },
  manageMemories: {
    items: [
      {
        id: "memoriesList",
        control: "moz-box-group",
        controlAttrs: {
          type: "list",
        },
      },
    ],
  },
});

[Dauer der Verarbeitung: 0.46 Sekunden, vorverarbeitet 2026-09-28]