commit 99e34a1b2e8b244ff32e078a15c3b6568e0e1cdf
parent 5c2a4610234f1d3a2d53be9f86324b4ab0859f84
Author: Tim Xia <txia@mozilla.com>
Date: Thu, 30 Oct 2025 19:16:02 +0000
Bug 1985666 - Add the model-download progress bar as shown in the Link Preview designs. - r=fluent-reviewers,firefox-ai-ml-reviewers,bolsson,atossou
- remove the unused block for dots
- add _abortController to interrupt and stop downloading model
Differential Revision: https://phabricator.services.mozilla.com/D262873
Diffstat:
4 files changed, 93 insertions(+), 30 deletions(-)
diff --git a/browser/components/genai/LinkPreview.sys.mjs b/browser/components/genai/LinkPreview.sys.mjs
@@ -138,6 +138,7 @@ XPCOMUtils.defineLazyPreferenceGetter(
export const LinkPreview = {
// Shared downloading state to use across multiple previews
progress: -1, // -1 = off, 0-100 = download progress
+ _abortController: null,
cancelLongPress: null,
keyboardComboActive: false,
@@ -313,6 +314,12 @@ export const LinkPreview = {
expand: !collapsed,
});
Glean.genaiLinkpreview.keyPoints.set(!collapsed);
+
+ // If user collapses while a model download is in progress, stop showing the progress bar.
+ if (collapsed && this.progress >= 0) {
+ this.progress = -1;
+ this.updateCardProperty("progress", this.progress);
+ }
},
/**
@@ -787,6 +794,11 @@ export const LinkPreview = {
ogCard.style.width = "100%";
ogCard.pageData = pageData;
+ ogCard.addEventListener("LinkPreviewCard:cancelDownload", () => {
+ this._abortController?.abort();
+ Services.prefs.setBoolPref("browser.ml.linkPreview.collapsed", true);
+ });
+
ogCard.optin = lazy.optin;
ogCard.collapsed = lazy.collapsed;
ogCard.canShowKeyPoints = this.canShowKeyPoints;
@@ -834,6 +846,7 @@ export const LinkPreview = {
if (!lazy.optin || lazy.collapsed) {
return;
}
+ this._abortController = new AbortController();
// Support prefetching without a card by mocking expected properties.
let outcome = ogCard ? "success" : "prefetch";
@@ -867,6 +880,7 @@ export const LinkPreview = {
await lazy.LinkPreviewModel.generateTextAI(
ogCard.pageData?.article.textContent ?? "",
{
+ abortSignal: this._abortController.signal,
onDownload: (downloading, percentage) => {
// Initial percentage is NaN, so set to 0.
percentage = isNaN(percentage) ? 0 : percentage;
@@ -876,6 +890,16 @@ export const LinkPreview = {
download = Date.now() - startTime;
},
onError: error => {
+ if (
+ error.name === "AbortError" ||
+ error.message?.includes("AbortError")
+ ) {
+ // This is an expected error when the user cancels the download.
+ // We don't need to show an error state.
+ outcome = "aborted";
+ this.lastRequest = Promise.resolve();
+ return;
+ }
console.error(error);
outcome = error;
ogCard.generationError = error;
diff --git a/browser/components/genai/LinkPreviewModel.sys.mjs b/browser/components/genai/LinkPreviewModel.sys.mjs
@@ -335,10 +335,11 @@ export const LinkPreviewModel = {
*
* @param {object} options - Configuration options for the ML engine.
* @param {?function(ProgressAndStatusCallbackParams):void} notificationsCallback A function to call to indicate notifications.
+ * @param {AbortSignal} abortSignal - The signal to abort the download.
* @returns {Promise<MLEngine>} - A promise that resolves to the ML engine instance.
*/
- async createEngine(options, notificationsCallback = null) {
- return lazy.createEngine(options, notificationsCallback);
+ async createEngine(options, notificationsCallback = null, abortSignal) {
+ return lazy.createEngine(options, notificationsCallback, abortSignal);
},
/**
@@ -346,11 +347,15 @@ export const LinkPreviewModel = {
*
* @param {string} inputText
* @param {object} callbacks for progress and error
+ * @param {AbortSignal} callbacks.abortSignal - The signal to abort the download.
* @param {Function} callbacks.onDownload optional for download active
* @param {Function} callbacks.onText optional for text chunks
* @param {Function} callbacks.onError optional for error
*/
- async generateTextAI(inputText, { onDownload, onText, onError } = {}) {
+ async generateTextAI(
+ inputText,
+ { onDownload, onText, onError, abortSignal } = {}
+ ) {
// Get updated options from remote settings. No failure if no record exists
const remoteRequestRecord = await lazy.RemoteSettingsManager.getRemoteData({
collectionName: "ml-inference-request-options",
@@ -423,7 +428,8 @@ export const LinkPreviewModel = {
Math.round((100 * data.totalLoaded) / data.total)
);
}
- }
+ },
+ abortSignal
);
const postProcessor = await SentencePostProcessor.initialize();
diff --git a/browser/components/genai/content/link-preview-card.mjs b/browser/components/genai/content/link-preview-card.mjs
@@ -68,6 +68,7 @@ class LinkPreviewCard extends MozLitElement {
this.canShowKeyPoints = true;
this.optin = false;
this.optinRef = createRef();
+ this.firstTimeModalRef = createRef();
this.progress = -1;
}
@@ -141,6 +142,10 @@ class LinkPreviewCard extends MozLitElement {
* @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
@@ -153,21 +158,19 @@ class LinkPreviewCard extends MozLitElement {
}
}
- updated(properties) {
+ updated(_properties) {
if (this.optinRef.value) {
this.optinRef.value.headingIcon = LinkPreviewCard.AI_ICON;
}
- if (properties.has("generating")) {
- if (this.generating > 0) {
- // Count up to 4 so that we can show 0 to 3 dots.
- this.dotsTimeout = setTimeout(
- () => (this.generating = (this.generating % 4) + 1),
- 500
- );
- } else {
- // Setting to false or 0 means we're done generating.
- clearTimeout(this.dotsTimeout);
+ 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;
}
}
}
@@ -307,7 +310,7 @@ class LinkPreviewCard extends MozLitElement {
}
${
/* Loading placeholders with three divs each */
- this.generating
+ this.generating || this.progress >= 0
? Array(
Math.max(
0,
@@ -317,7 +320,11 @@ class LinkPreviewCard extends MozLitElement {
.fill()
.map(
() =>
- html` <li class="content-item loading">
+ html` <li
+ class="content-item loading ${this.progress >= 0
+ ? "static"
+ : ""}"
+ >
<div></div>
<div></div>
<div></div>
@@ -326,7 +333,7 @@ class LinkPreviewCard extends MozLitElement {
: []
}
</ul>
- ${!this.generating
+ ${!(this.generating || this.progress >= 0)
? html`
<div class="visit-link-container">
<a
@@ -346,18 +353,8 @@ class LinkPreviewCard extends MozLitElement {
</div>
`
: ""}
- ${this.progress >= 0
- ? html`
- <p
- data-l10n-id="link-preview-setup"
- data-l10n-args=${JSON.stringify({
- progress: this.progress,
- })}
- ></p>
- <p data-l10n-id="link-preview-setup-faster-next-time"></p>
- `
- : ""}
- ${!this.generating
+ ${this.renderModalFirstTime()}
+ ${!(this.generating || this.progress >= 0)
? html`
<hr />
${linksSection}
@@ -391,6 +388,29 @@ class LinkPreviewCard extends MozLitElement {
}
/**
+ * 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.
@@ -402,6 +422,13 @@ class LinkPreviewCard extends MozLitElement {
}
/**
+ * 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.
diff --git a/browser/locales/en-US/browser/genai.ftl b/browser/locales/en-US/browser/genai.ftl
@@ -250,3 +250,9 @@ link-preview-onboarding-button = See a preview
# Onboarding card Close button
link-preview-onboarding-close = Close
+
+# Title for the first-time setup modal
+link-preview-first-time-setup-title = First-time setup
+
+# Message for the first-time setup modal
+link-preview-first-time-setup-message = This may take a moment. You’ll see key points more quickly next time.