tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mbrowser/components/genai/LinkPreview.sys.mjs | 24++++++++++++++++++++++++
Mbrowser/components/genai/LinkPreviewModel.sys.mjs | 14++++++++++----
Mbrowser/components/genai/content/link-preview-card.mjs | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mbrowser/locales/en-US/browser/genai.ftl | 6++++++
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.