Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  link-preview-card.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 {
  createRef,
  html,
  ref,
} from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
  FaviconUtils: "moz-src:///toolkit/modules/FaviconUtils.sys.mjs",
});

ChromeUtils.defineLazyGetter(
  lazy,
  "numberFormat",
  () => new Services.intl.NumberFormat()
);

ChromeUtils.defineLazyGetter(
  lazy,
  "pluralRules",
  () => new Services.intl.PluralRules()
);

ChromeUtils.importESModule(
  "chrome://browser/content/genai/content/model-optin.mjs",
  {
    global: "current",
  }
);

window.MozXULElement.insertFTLIfNeeded("browser/genai.ftl");

/**
 * Class representing a link preview element.
 *
 * @augments MozLitElement
 */
class LinkPreviewCard extends MozLitElement {
  static AI_ICON = "chrome://global/skin/icons/highlights.svg";
  // Number of placeholder rows to show when loading
  static PLACEHOLDER_COUNT = 3;

  static properties = {
    collapsed: { type: Boolean },
    generating: { type: Number }, // 0 = off, 1-4 = generating & dots state
    isMissingDataErrorState: { type: Boolean },
    generationError: { type: Object }, // null = no error, otherwise contains error info
    keyPoints: { type: Array },
    canShowKeyPoints: { type: Boolean },
    optin: { type: Boolean },
    pageData: { type: Object },
    progress: { type: Number }, // -1 = off, 0-100 = download progress
  };

  constructor() {
    super();
    this.collapsed = false;
    this.generationError = null;
    this.isMissingDataErrorState = false;
    this.keyPoints = [];
    this.canShowKeyPoints = true;
    this.optin = false;
    this.optinRef = createRef();
    this.firstTimeModalRef = createRef();
    this.progress = -1;
  }

  /**
   * Handles click events on the settings button.
   *
   * Prevents the default event behavior and opens Firefox's preferences
   * page with the link preview settings section focused.
   *
   * @param {MouseEvent} _event - The click event from the settings button.
   */
  handleSettingsClick(_event) {
    const win = this.documentGlobal;
    win.openPreferences("general-link-preview");
    this.dispatchEvent(
      new CustomEvent("LinkPreviewCard:dismiss", {
        detail: "settings",
      })
    );
  }

  addKeyPoint(text) {
    this.keyPoints.push(text);
    this.requestUpdate();
  }

  /**
   * Handles click events on the <a> element.
   *
   * @param {MouseEvent} event - The click event.
   */
  handleLink(event) {
    event.preventDefault();

    const anchor = event.target.closest("a");
    const url = anchor.href;

    const win = this.documentGlobal;
    const params = {
      triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
        {}
      ),
    };

    // Determine where to open the link based on the event (e.g., new tab,
    // current tab)
    const where = lazy.BrowserUtils.whereToOpenLink(event, false, true);
    win.openLinkIn(url, where, params);

    this.dispatchEvent(
      new CustomEvent("LinkPreviewCard:dismiss", {
        detail: event.target.dataset.source ?? "error",
      })
    );
  }

  /**
   * Handles retry request for key points generation.
   *
   * @param {MouseEvent} event - The click event.
   */
  handleRetry(event) {
    event.preventDefault();
    // Dispatch retry event to be handled by LinkPreview.sys.mjs
    this.dispatchEvent(new CustomEvent("LinkPreviewCard:retry"));
  }

  /**
   * Toggles the expanded state of the key points section
   *
   * @param {MouseEvent} _event - The click event
   */
  toggleKeyPoints(_event) {
    // Do not allow collapsing while a download is in progress.
    if (this.progress >= 0) {
      return;
    }
    Services.prefs.setBoolPref(
      "browser.ml.linkPreview.collapsed",
      !this.collapsed
    );

    // When expanded, if there are existing key points, we won't trigger
    // another generation
    if (!this.collapsed) {
      this.dispatchEvent(new CustomEvent("LinkPreviewCard:generate"));
    }
  }

  updated(_properties) {
    if (this.optinRef.value) {
      this.optinRef.value.headingIcon = LinkPreviewCard.AI_ICON;
    }

    if (this.firstTimeModalRef.value) {
      this.firstTimeModalRef.value.headingIcon = LinkPreviewCard.AI_ICON;
      this.firstTimeModalRef.value.iconAtEnd = true;
      this.firstTimeModalRef.value.footerMessageL10nId = "";

      if (this.progress >= 0) {
        this.firstTimeModalRef.value.isLoading = true;
        this.firstTimeModalRef.value.progressStatus = this.progress;
      }
    }
  }

  /**
   * Get the appropriate Fluent ID for the error message based on the error state.
   *
   * @returns {string} The Fluent ID for the error message.
   */
  get errorMessageL10nId() {
    if (this.isMissingDataErrorState) {
      return "link-preview-generation-error-missing-data-v2";
    } else if (this.generationError) {
      return "link-preview-generation-error-unexpected";
    }
    return "";
  }

  /**
   * Renders the error generation card for when we have a generation error.
   *
   * @returns {import('lit').TemplateResult} The error generation card HTML
   */
  renderErrorGenerationCard() {
    // Only show the retry link if we have a generation error that's not a memory error
    const showRetryLink =
      this.generationError &&
      this.generationError.name !== "NotEnoughMemoryError";

    return html`
      <div class="ai-content">
        <p class="og-error-message-container">
          <span
            class="og-error-message"
            data-l10n-id=${this.errorMessageL10nId}
          ></span>
          ${showRetryLink
            ? html`
                <span class="retry-link">
                  <a
                    href="#"
                    @click=${this.handleRetry}
                    data-l10n-id="link-preview-generation-retry"
                  ></a>
                </span>
              `
            : ""}
        </p>
      </div>
    `;
  }

  /**
   * Renders a placeholder generation card for the opt-in mode,
   * showing only loading animations without real content.
   *
   * @returns {import('lit').TemplateResult} The opt-in placeholder card HTML
   */
  renderOptInPlaceholderCard() {
    return html`
      <div class="ai-content">
        <h3
          class="keypoints-header"
          @click=${this._handleOptinDeny}
          tabindex="0"
          role="button"
          aria-expanded=${!this.collapsed}
        >
          <div class="chevron-icon"></div>
          <span data-l10n-id="link-preview-key-points-header"></span>
          <img
            class="icon"
            xmlns="http://www.w3.org/1999/xhtml"
            role="presentation"
            src="chrome://global/skin/icons/highlights.svg"
          />
        </h3>
        <div class="keypoints-content ${this.collapsed ? "hidden" : ""}">
          <ul class="keypoints-list">
            ${
              /* Always show 3 placeholder loading items */
              Array(LinkPreviewCard.PLACEHOLDER_COUNT)
                .fill()
                .map(
                  () =>
                    html` <li class="content-item loading static">
                      <div></div>
                      <div></div>
                      <div></div>
                    </li>`
                )
            }
          </ul>
          ${this.renderModelOptIn()}
        </div>
      </div>
    `;
  }

  /**
   * Renders the normal generation card for displaying key points.
   *
   * @param {string} pageUrl - URL of the page being previewed
   * @returns {import('lit').TemplateResult} The normal generation card HTML
   */
  renderNormalGenerationCard(pageUrl) {
    // Extract the links section into its own variable
    const linksSection = html`
      <p data-l10n-id="link-preview-key-points-disclaimer"></p>
    `;

    return html`
      <div class="ai-content">
        <h3
          class="keypoints-header"
          @click=${this.toggleKeyPoints}
          tabindex="0"
          role="button"
          aria-expanded=${!this.collapsed}
        >
          <span data-l10n-id="link-preview-key-points-header"></span>
          <img
            class="icon"
            xmlns="http://www.w3.org/1999/xhtml"
            role="presentation"
            src="chrome://global/skin/icons/highlights.svg"
          />
        </h3>
        <div class="keypoints-content ${this.collapsed ? "hidden" : ""}">
          <ul class="keypoints-list">
            ${
              /* All populated content items */
              this.keyPoints.map(
                item => html`<li class="content-item">${item}</li>`
              )
            }
            ${
              /* Loading placeholders with three divs each */
              this.generating || this.progress >= 0
                ? Array(
                    Math.max(
                      0,
                      LinkPreviewCard.PLACEHOLDER_COUNT - this.keyPoints.length
                    )
                  )
                    .fill()
                    .map(
                      () =>
                        html` <li
                          class="content-item loading ${this.progress >= 0
                            ? "static"
                            : ""}"
                        >
                          <div></div>
                          <div></div>
                          <div></div>
                        </li>`
                    )
                : []
            }
          </ul>
          ${!(this.generating || this.progress >= 0)
            ? html`
                <div class="visit-link-container">
                  <a
                    @click=${this.handleLink}
                    data-source="visit"
                    href=${pageUrl}
                    class="visit-link"
                  >
                    <span data-l10n-id="link-preview-visit-link"></span>
                  </a>
                </div>
              `
            : ""}
          ${this.renderModalFirstTime()}
          ${!(this.generating || this.progress >= 0)
            ? html`
                <hr />
                ${linksSection}
              `
            : ""}
        </div>
      </div>
    `;
  }

  /**
   * Renders the model opt-in component that prompts users to optin to AI features.
   * This component allows users to opt in or out of the link preview AI functionality
   * and includes a support link for more information.
   *
   * @returns {import('lit').TemplateResult} The model opt-in component HTML
   */
  renderModelOptIn() {
    return html`
      <model-optin
        ${ref(this.optinRef)}
        headingIcon=${LinkPreviewCard.AI_ICON}
        headingL10nId="link-preview-optin-title"
        iconAtEnd
        messageL10nId="link-preview-optin-message"
        @MlModelOptinConfirm=${this._handleOptinConfirm}
        @MlModelOptinDeny=${this._handleOptinDeny}
      >
      </model-optin>
    `;
  }

  /**
   * Renders the first-time setup modal with progress bar.
   * Shows a modal-style component when progress is being tracked (this.progress >= 0).
   *
   * @returns {import('lit').TemplateResult} The first-time setup modal HTML
   */
  renderModalFirstTime() {
    if (this.progress < 0) {
      return "";
    }

    return html`
      <model-optin
        ${ref(this.firstTimeModalRef)}
        headingL10nId="link-preview-first-time-setup-title"
        messageL10nId="link-preview-first-time-setup-message"
        progressStatus=${this.progress}
        @MlModelOptinCancelDownload=${this._handleCancelDownload}
      >
      </model-optin>
    `;
  }

  /**
   * Handles the user confirming the opt-in prompt for link preview.
   * Sets preference values to enable the feature, hides the prompt for future sessions,
   * and triggers a retry to generate the preview.
   */
  _handleOptinConfirm() {
    Services.prefs.setBoolPref("browser.ml.linkPreview.optin", true);

    this.dispatchEvent(new CustomEvent("LinkPreviewCard:generate"));
  }

  /**
   * Handles the user canceling the first-time model download.
   */
  _handleCancelDownload() {
    this.dispatchEvent(new CustomEvent("LinkPreviewCard:cancelDownload"));
  }

  /**
   * Handles the user denying the opt-in prompt for link preview.
   * Sets preference values to disable the feature and hides
   * the prompt for future sessions.
   */
  _handleOptinDeny() {
    Services.prefs.setBoolPref("browser.ml.linkPreview.optin", false);
    Services.prefs.setBoolPref("browser.ml.linkPreview.collapsed", true);

    Glean.genaiLinkpreview.cardAiConsent.record({ option: "cancel" });
  }

  /**
   * Renders the appropriate content card based on state.
   *
   * @param {string} pageUrl - URL of the page being previewed
   * @returns {import('lit').TemplateResult} The content card HTML
   */
  renderKeyPointsSection(pageUrl) {
    if (!this.canShowKeyPoints) {
      return "";
    }

    // Determine if there's any generation error state
    const isGenerationError =
      this.isMissingDataErrorState || this.generationError;

    // If we should show the opt-in prompt, show our special placeholder card
    if (!this.optin && !this.collapsed) {
      return this.renderOptInPlaceholderCard();
    }

    if (isGenerationError) {
      return this.renderErrorGenerationCard(pageUrl);
    }

    // Always render the ai-content, otherwise we won't have header to expand/collapse
    return this.renderNormalGenerationCard(pageUrl);
  }

  /**
   * Renders the link preview element.
   *
   * @returns {import('lit').TemplateResult} The rendered HTML template.
   */
  render() {
    const articleData = this.pageData?.article || {};
    const pageUrl = this.pageData?.url || "about:blank";
    const siteName =
      articleData.siteName || this.pageData?.urlComponents?.domain || "";

    const { title, description, imageUrl } = this.pageData.meta;

    const inDarkMode = window.matchMedia(
      "(prefers-color-scheme: dark)"
    ).matches;
    const colorScheme = inDarkMode ? "dark" : "light";
    const ogImageUrl = lazy.FaviconUtils.getMozRemoteImageURL(imageUrl, {
      colorScheme,
    });

    const readingTimeMinsFast = articleData.readingTimeMinsFast || "";
    const readingTimeMinsSlow = articleData.readingTimeMinsSlow || "";
    const readingTimeMinsFastStr =
      lazy.numberFormat.format(readingTimeMinsFast);
    const readingTimeRange = lazy.numberFormat.formatRange(
      readingTimeMinsFast,
      readingTimeMinsSlow
    );

    // Check if both metadata and article text content are missing
    const isMissingAllContent = !description && !articleData.textContent;

    const filename = this.pageData?.urlComponents?.filename;

    // Error Link Preview card UI: A simplified version of the preview card showing only an error message
    // and a link to visit the URL. This is a fallback UI for cases when we don't have
    // enough metadata to generate a useful preview.
    const errorCard = html`
      <div class="og-card">
        <div class="og-card-content">
          <div class="og-error-content">
            <p
              class="og-error-message"
              data-l10n-id="link-preview-error-message-v2"
            ></p>
            <a
              class="og-card-title"
              @click=${this.handleLink}
              data-l10n-id="link-preview-visit-link"
              href=${pageUrl}
            ></a>
          </div>
        </div>
      </div>
    `;

    // Normal Link Preview card UI: Shown when we have sufficient metadata (at least title and description)
    // Displays rich preview information including optional elements like site name, image,
    // reading time, and AI-generated key points if available
    const normalCard = html`
      <div class="og-card">
        <div class="og-card-content">
          ${imageUrl.startsWith("https://")
            ? html` <img class="og-card-img" src=${ogImageUrl} alt=${title} /> `
            : ""}
          ${siteName
            ? html`
                <div class="page-info-and-card-setting-container">
                  <span class="site-name">${siteName}</span>
                </div>
              `
            : ""}
          <h2 class="og-card-title">
            <a @click=${this.handleLink} data-source="title" href=${pageUrl}
              >${title || filename}</a
            >
          </h2>
          ${description
            ? html`<p class="og-card-description">${description}</p>`
            : ""}
          <div class="reading-time-settings-container">
            ${readingTimeMinsFast && readingTimeMinsSlow
              ? html`
                  <div
                    class="og-card-reading-time"
                    data-l10n-id="link-preview-reading-time"
                    data-l10n-args=${JSON.stringify({
                      range:
                        readingTimeMinsFast === readingTimeMinsSlow
                          ? `~${readingTimeMinsFastStr}`
                          : `${readingTimeRange}`,
                      rangePlural:
                        readingTimeMinsFast === readingTimeMinsSlow
                          ? lazy.pluralRules.select(readingTimeMinsFast)
                          : lazy.pluralRules.selectRange(
                              readingTimeMinsFast,
                              readingTimeMinsSlow
                            ),
                    })}
                  ></div>
                `
              : html`<div></div>`}
            <moz-button
              type="icon ghost"
              iconSrc="chrome://global/skin/icons/settings.svg"
              data-l10n-id="link-preview-settings-button"
              data-l10n-attrs="title"
              @click=${this.handleSettingsClick}
            >
            </moz-button>
          </div>
        </div>
        ${this.renderKeyPointsSection(pageUrl)}
      </div>
    `;

    return html`
      <link
        rel="stylesheet"
        href="chrome://browser/content/genai/content/link-preview-card.css"
      />
      ${isMissingAllContent ? errorCard : normalCard}
    `;
  }
}

customElements.define("link-preview-card", LinkPreviewCard);

[Dauer der Verarbeitung: 0.27 Sekunden, vorverarbeitet 2026-08-26]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Statistik
#Sources=277311
#Domains=752002