diff --git a/browser/components/genai/LinkPreview.sys.mjs b/browser/components/genai/LinkPreview.sys.mjs index 04bec567f57d..6a7d76821064 100644 --- a/browser/components/genai/LinkPreview.sys.mjs +++ b/browser/components/genai/LinkPreview.sys.mjs @@ -24,6 +24,8 @@ XPCOMUtils.defineLazyPreferenceGetter( ); export const LinkPreview = { + // Shared downloading state to use across multiple previews + downloadingModel: false, keyboardComboActive: false, _windowStates: new Map(), linkPreviewPanelId: "link-preview-panel", @@ -189,6 +191,8 @@ export const LinkPreview = { createOGCard(doc, pageData) { const ogCard = doc.createElement("link-preview-card"); ogCard.pageData = pageData; + // Assume we need to wait if another generate is downloading. + ogCard.showWait = this.downloadingModel; if (pageData.article.textContent) { this.generateKeyPoints(ogCard); @@ -222,8 +226,14 @@ export const LinkPreview = { await lazy.LinkPreviewModel.generateTextAI( ogCard.pageData.article.textContent, { + onDownload: state => { + ogCard.showWait = state; + this.downloadingModel = state; + }, onError: console.error, - onText(text) { + onText: text => { + // Clear waiting in case a different generate handled download. + ogCard.showWait = false; ogCard.addKeyPoint(text); if (--expected == 0) { ogCard.generating = false; diff --git a/browser/components/genai/LinkPreviewModel.sys.mjs b/browser/components/genai/LinkPreviewModel.sys.mjs index 39b97a7224b3..415b64541d09 100644 --- a/browser/components/genai/LinkPreviewModel.sys.mjs +++ b/browser/components/genai/LinkPreviewModel.sys.mjs @@ -19,6 +19,7 @@ const MIN_WORD_COUNT = 5; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { createEngine: "chrome://global/content/ml/EngineProcess.sys.mjs", + Progress: "chrome://global/content/ml/Utils.sys.mjs", }); XPCOMUtils.defineLazyPreferenceGetter( lazy, @@ -233,10 +234,11 @@ export const LinkPreviewModel = { * * @param {string} inputText * @param {object} callbacks for progress and error + * @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, { onText, onError } = {}) { + async generateTextAI(inputText, { onDownload, onText, onError } = {}) { const processedInput = this.preprocessText(inputText); // Asssume generated text is approximately the same length as the input. const nPredict = Math.ceil(processedInput.length / CHARACTERS_PER_TOKEN); @@ -251,25 +253,34 @@ export const LinkPreviewModel = { let engine; try { - engine = await lazy.createEngine({ - backend: "wllama", - engineId: "wllamapreview", - kvCacheDtype: "q8_0", - modelFile: "smollm2-360m-instruct-q8_0.gguf", - modelHubRootUrl: "https://model-hub.mozilla.org", - modelHubUrlTemplate: "{model}/{revision}", - modelId: "HuggingFaceTB/SmolLM2-360M-Instruct-GGUF", - modelRevision: "main", - numBatch: numContext, - numContext, - numUbatch: numContext, - runtimeFilename: "wllama.wasm", - taskName: "wllama-text-generation", - timeoutMS: -1, - useMlock: false, - useMmap: true, - ...JSON.parse(lazy.config), - }); + engine = await lazy.createEngine( + { + backend: "wllama", + engineId: "wllamapreview", + kvCacheDtype: "q8_0", + modelFile: "smollm2-360m-instruct-q8_0.gguf", + modelHubRootUrl: "https://model-hub.mozilla.org", + modelHubUrlTemplate: "{model}/{revision}", + modelId: "HuggingFaceTB/SmolLM2-360M-Instruct-GGUF", + modelRevision: "main", + numBatch: numContext, + numContext, + numUbatch: numContext, + runtimeFilename: "wllama.wasm", + taskName: "wllama-text-generation", + timeoutMS: -1, + useMlock: false, + useMmap: true, + ...JSON.parse(lazy.config), + }, + data => { + if (data.type == lazy.Progress.ProgressType.DOWNLOAD) { + onDownload?.( + data.statusText != lazy.Progress.ProgressStatusText.DONE + ); + } + } + ); const postProcessor = new SentencePostProcessor(); for await (const val of engine.runWithGenerator({ diff --git a/browser/components/genai/content/link-preview-card.mjs b/browser/components/genai/content/link-preview-card.mjs index 27f291ae6ded..f44f274c6e0c 100644 --- a/browser/components/genai/content/link-preview-card.mjs +++ b/browser/components/genai/content/link-preview-card.mjs @@ -12,6 +12,10 @@ ChromeUtils.defineESModuleGetters(lazy, { BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", }); +// TODO put in actual link probably same as labs bug 1951144 +const FEEDBACK_LINK = + "https://docs.google.com/spreadsheets/d/1hsG7UXGJRN8D4ViaETICDyA0gbBArzmib1qTylmIu8M"; + /** * Class representing a link preview element. * @@ -19,9 +23,10 @@ ChromeUtils.defineESModuleGetters(lazy, { */ class LinkPreviewCard extends MozLitElement { static properties = { - generating: { type: Number }, + generating: { type: Number }, // 0 = off, 1-4 = generating & dots state keyPoints: { type: Array }, pageData: { type: Object }, + showWait: { type: Boolean }, }; constructor() { @@ -61,11 +66,13 @@ class LinkPreviewCard extends MozLitElement { updated(properties) { 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 % 3) + 1), + () => (this.generating = (this.generating % 4) + 1), 500 ); } else { + // Setting to false or 0 means we're done generating. clearTimeout(this.dotsTimeout); } } @@ -87,15 +94,14 @@ class LinkPreviewCard extends MozLitElement { metaData["og:title"] || metaData["twitter:title"] || metaData["html:title"] || - pageUrl || - ""; + "This link can’t be previewed"; const description = articleData.excerpt || metaData["og:description"] || metaData["twitter:description"] || metaData.description || - ""; + "No Reason. Just ’cause. (better error handling incoming)"; const imageUrl = metaData["og:image"] || metaData["twitter:image:src"] || ""; @@ -136,14 +142,31 @@ class LinkPreviewCard extends MozLitElement { ? html`

- Generat${this.generating ? "ing" : "ed"} key - points${".".repeat(this.generating)} + ${this.generating + ? "Generating key points" + ".".repeat(this.generating - 1) + : "Key points"}


-

AI-generated content may be inaccurate

+ ${this.showWait + ? html`

+ This may take a moment the first time you preview a link. + Key points should appear more quickly next time. +

` + : ""} +

+ Key points are AI-generated and may be wrong. + + Foxfooding feedback + +

+

+ + Visit original page + +

` : ""} diff --git a/browser/components/genai/tests/browser/browser_link_preview.js b/browser/components/genai/tests/browser/browser_link_preview.js index 4b8e0c34912f..84e69a971259 100644 --- a/browser/components/genai/tests/browser/browser_link_preview.js +++ b/browser/components/genai/tests/browser/browser_link_preview.js @@ -4,6 +4,9 @@ const { LinkPreview } = ChromeUtils.importESModule( "moz-src:///browser/components/genai/LinkPreview.sys.mjs" ); +const { LinkPreviewModel } = ChromeUtils.importESModule( + "moz-src:///browser/components/genai/LinkPreviewModel.sys.mjs" +); const { sinon } = ChromeUtils.importESModule( "resource://testing-common/Sinon.sys.mjs" ); @@ -110,7 +113,15 @@ add_task(async function test_link_preview_panel_shown() { set: [["browser.ml.linkPreview.enabled", true]], }); - const stub = sinon.stub(LinkPreview, "generateKeyPoints"); + let onDownload, toResolve; + const stub = sinon + .stub(LinkPreviewModel, "generateTextAI") + .callsFake(async (text, options) => { + onDownload = options.onDownload; + toResolve = Promise.withResolvers(); + return toResolve.promise; + }); + window.dispatchEvent( new KeyboardEvent("keydown", { bubbles: true, @@ -129,6 +140,28 @@ add_task(async function test_link_preview_panel_shown() { is(stub.callCount, 1, "would have generated key points"); + const card = panel.querySelector("link-preview-card"); + ok(card, "card created for link preview"); + ok(card.generating, "initially marked as generating"); + ok(!card.showWait, "initially assume not waiting"); + ok(!LinkPreview.downloadingModel, "initially assume not downloading"); + + onDownload(true); + + ok(card.showWait, "switched to waiting when download initiates"); + ok(LinkPreview.downloadingModel, "shared waiting for download"); + + onDownload(false); + + ok(!card.showWait, "no longer waiting after download complete"); + ok(!LinkPreview.downloadingModel, "downloading updated"); + ok(card.generating, "still generating"); + + toResolve.resolve(); + await LinkPreview.lastRequest; + + ok(!card.generating, "done generating"); + panel.remove(); stub.restore(); });