Bug 1951146 - Provide button to generate ai preview with first time opt-in - r=Mardak,fluent-reviewers,firefox-ai-ml-reviewers,bolsson
- static skeleton animation - start download / inference only after opt-in - update model-optin to allow placing icon on either right or left depending on iconAtEnd - update background-color of optin-wrapper - add new prefs browser.ml.linkPreview.optin and browser.ml.linkPreview.collapse - use css order attribute for placement of icon - update to final text for opt-in modal - don't generate key points if collapsed is true - add optin test suite in browser_link_preview_optin.js - add telemetry to link review optin - remove learn more link - refactor updateCardProperty Differential Revision: https://phabricator.services.mozilla.com/D249104
This commit is contained in:
committed by
elee@mozilla.com
parent
6a2624c304
commit
842057e5ca
@@ -2142,8 +2142,9 @@ pref("browser.ml.chat.shortcuts.longPress", 60000);
|
||||
pref("browser.ml.chat.sidebar", true);
|
||||
|
||||
pref("browser.ml.linkPreview.allowedLanguages", "en");
|
||||
pref("browser.ml.linkPreview.enabled", false);
|
||||
pref("browser.ml.linkPreview.blockListEnabled", true);
|
||||
pref("browser.ml.linkPreview.collapsed", false);
|
||||
pref("browser.ml.linkPreview.enabled", false);
|
||||
pref("browser.ml.linkPreview.longPress", true);
|
||||
pref("browser.ml.linkPreview.noKeyPointsRegions", "AD,AT,BE,BG,CH,CY,CZ,DE,DK,EE,ES,FI,FR,GR,HR,HU,IE,IS,IT,LI,LT,LU,LV,MT,NL,NO,PL,PT,RO,SE,SI,SK");
|
||||
pref("browser.ml.linkPreview.optin", false);
|
||||
|
||||
@@ -16,11 +16,18 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
||||
"allowedLanguages",
|
||||
"browser.ml.linkPreview.allowedLanguages"
|
||||
);
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
lazy,
|
||||
"collapsed",
|
||||
"browser.ml.linkPreview.collapsed",
|
||||
null,
|
||||
(_pref, _old, val) => LinkPreview.onCollapsedPref(val)
|
||||
);
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
lazy,
|
||||
"enabled",
|
||||
"browser.ml.linkPreview.enabled",
|
||||
false,
|
||||
null,
|
||||
(_pref, _old, val) => LinkPreview.onEnabledPref(val)
|
||||
);
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
@@ -28,6 +35,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
||||
"noKeyPointsRegions",
|
||||
"browser.ml.linkPreview.noKeyPointsRegions"
|
||||
);
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
lazy,
|
||||
"optin",
|
||||
"browser.ml.linkPreview.optin",
|
||||
null,
|
||||
(_pref, _old, val) => LinkPreview.onOptinPref(val)
|
||||
);
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
lazy,
|
||||
"prefetchOnEnable",
|
||||
@@ -102,12 +116,60 @@ export const LinkPreview = {
|
||||
Glean.genaiLinkpreview.labsCheckbox.record({ enabled });
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates a property on the link-preview-card element for all window states.
|
||||
*
|
||||
* @param {string} prop - The property to update.
|
||||
* @param {*} value - The value to set for the property.
|
||||
*/
|
||||
updateCardProperty(prop, value) {
|
||||
for (const [win] of this._windowStates) {
|
||||
const panel = win.document.getElementById(this.linkPreviewPanelId);
|
||||
if (!panel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const card = panel.querySelector("link-preview-card");
|
||||
if (card) {
|
||||
card[prop] = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the preference change for opt-in state.
|
||||
* Updates all link preview cards with the new opt-in state.
|
||||
*
|
||||
* @param {boolean} optin - The new state of the opt-in preference.
|
||||
*/
|
||||
onOptinPref(optin) {
|
||||
this.updateCardProperty("optin", optin);
|
||||
Glean.genaiLinkpreview.cardAiConsent.record({
|
||||
option: optin ? "continue" : "cancel",
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the preference change for collapsed state.
|
||||
* Updates all link preview cards with the new collapsed state.
|
||||
*
|
||||
* @param {boolean} collapsed - The new state of the collapsed preference.
|
||||
*/
|
||||
onCollapsedPref(collapsed) {
|
||||
this.updateCardProperty("collapsed", collapsed);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles startup tasks such as telemetry and adding listeners.
|
||||
*
|
||||
* @param {Window} win - The window context used to add event listeners.
|
||||
*/
|
||||
init(win) {
|
||||
// Access getters for side effects of observing pref changes
|
||||
lazy.collapsed;
|
||||
lazy.enabled;
|
||||
lazy.optin;
|
||||
|
||||
this._windowStates.set(win, {});
|
||||
if (!win.customElements.get("link-preview-card")) {
|
||||
win.ChromeUtils.importESModule(
|
||||
@@ -260,6 +322,9 @@ export const LinkPreview = {
|
||||
ogCard.style.width = "100%";
|
||||
ogCard.pageData = pageData;
|
||||
|
||||
ogCard.optin = lazy.optin;
|
||||
ogCard.collapsed = lazy.collapsed;
|
||||
|
||||
// Reflect the shared download progress to this preview.
|
||||
const updateProgress = () => {
|
||||
ogCard.progress = this.progress;
|
||||
@@ -303,6 +368,12 @@ export const LinkPreview = {
|
||||
* @param {boolean} _retry Indicates whether to retry the operation.
|
||||
*/
|
||||
async generateKeyPoints(ogCard, _retry = false) {
|
||||
// Prevent keypoints if user not opt-in to link preview or user is set
|
||||
// keypoints to be collapsed.
|
||||
if (!lazy.optin || lazy.collapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Support prefetching without a card by mocking expected properties.
|
||||
let outcome = ogCard ? "success" : "prefetch";
|
||||
if (!ogCard) {
|
||||
@@ -370,6 +441,22 @@ export const LinkPreview = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles key points generation requests from different user actions.
|
||||
* This is a shared handler for both retry and initial generation events.
|
||||
* Resets error states and triggers key points generation.
|
||||
*
|
||||
* @param {LinkPreviewCard} ogCard - The card element to generate key points for
|
||||
* @private
|
||||
*/
|
||||
_handleKeyPointsGenerationEvent(ogCard) {
|
||||
// Reset error states
|
||||
ogCard.isMissingDataErrorState = false;
|
||||
ogCard.isGenerationErrorState = false;
|
||||
|
||||
this.generateKeyPoints(ogCard, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the link preview panel at the specified coordinates.
|
||||
*
|
||||
@@ -453,16 +540,15 @@ export const LinkPreview = {
|
||||
Glean.genaiLinkpreview.cardLink.record({ source: event.detail });
|
||||
});
|
||||
|
||||
// Add event listener for the retry event
|
||||
ogCard.addEventListener("LinkPreviewCard:retry", _event => {
|
||||
// Reset error states
|
||||
ogCard.isMissingDataErrorState = false;
|
||||
ogCard.isGenerationErrorState = false;
|
||||
|
||||
this.generateKeyPoints(ogCard, true);
|
||||
//TODO: review if glean record is correct
|
||||
// Glean.genaiLinkpreview.cardLink.record({ source: url, op: "retry" });
|
||||
this._handleKeyPointsGenerationEvent(ogCard, "retry");
|
||||
Glean.genaiLinkpreview.cardLink.record({ source: "retry" });
|
||||
});
|
||||
|
||||
ogCard.addEventListener("LinkPreviewCard:generate", _event => {
|
||||
this._handleKeyPointsGenerationEvent(ogCard, "generate");
|
||||
});
|
||||
|
||||
openPopup();
|
||||
},
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
}
|
||||
|
||||
.ai-content {
|
||||
position: relative;
|
||||
padding: var(--og-padding);
|
||||
.og-error-message-container {
|
||||
margin:0;
|
||||
@@ -162,6 +163,11 @@
|
||||
border-color: var(--skeleton-loader-motion-element-color);
|
||||
}
|
||||
|
||||
&.static div {
|
||||
animation: none;
|
||||
background: var(--skeleton-loader-background-color);
|
||||
}
|
||||
|
||||
div:nth-of-type(1) {
|
||||
max-width: 95%;
|
||||
}
|
||||
@@ -182,3 +188,13 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
model-optin {
|
||||
--font-size-xxlarge: var(--og-main-font-size);
|
||||
inset-inline: 0;
|
||||
margin-inline: auto;
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
width: 75%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { html } from "chrome://global/content/vendor/lit.all.mjs";
|
||||
import {
|
||||
createRef,
|
||||
html,
|
||||
ref,
|
||||
} from "chrome://global/content/vendor/lit.all.mjs";
|
||||
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
|
||||
|
||||
const lazy = {};
|
||||
@@ -24,6 +28,13 @@ ChromeUtils.defineLazyGetter(
|
||||
() => new Services.intl.PluralRules()
|
||||
);
|
||||
|
||||
ChromeUtils.importESModule(
|
||||
"chrome://browser/content/genai/content/model-optin.mjs",
|
||||
{
|
||||
global: "current",
|
||||
}
|
||||
);
|
||||
|
||||
const FEEDBACK_LINK =
|
||||
"https://connect.mozilla.org/t5/discussions/try-out-link-previews-on-firefox-labs/td-p/92012";
|
||||
|
||||
@@ -35,24 +46,30 @@ window.MozXULElement.insertFTLIfNeeded("preview/linkPreview.ftl");
|
||||
* @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
|
||||
isGenerationErrorState: { type: Boolean },
|
||||
isMissingDataErrorState: { type: Boolean },
|
||||
keyPoints: { type: Array },
|
||||
optin: { type: Boolean },
|
||||
pageData: { type: Object },
|
||||
progress: { type: Number }, // -1 = off, 0-100 = download progress
|
||||
isMissingDataErrorState: { type: Boolean },
|
||||
isGenerationErrorState: { type: Boolean },
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.keyPoints = [];
|
||||
this.progress = -1;
|
||||
this.isMissingDataErrorState = false;
|
||||
this.collapsed = false;
|
||||
this.isGenerationErrorState = false;
|
||||
this.isMissingDataErrorState = false;
|
||||
this.keyPoints = [];
|
||||
this.optin = false;
|
||||
this.optinRef = createRef();
|
||||
this.progress = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,6 +137,10 @@ class LinkPreviewCard extends MozLitElement {
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -177,6 +198,44 @@ class LinkPreviewCard extends MozLitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>
|
||||
Key points
|
||||
<img
|
||||
class="icon"
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
role="presentation"
|
||||
src="chrome://global/skin/icons/highlights.svg"
|
||||
/>
|
||||
</h3>
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the normal generation card for displaying key points.
|
||||
*
|
||||
@@ -284,6 +343,48 @@ class LinkPreviewCard extends MozLitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the appropriate content card based on state.
|
||||
*
|
||||
@@ -295,6 +396,16 @@ class LinkPreviewCard extends MozLitElement {
|
||||
const isGenerationError =
|
||||
this.isMissingDataErrorState || this.isGenerationErrorState;
|
||||
|
||||
// If we should show the opt-in prompt, show our special placeholder card
|
||||
if (!this.optin && !this.collapsed) {
|
||||
return this.renderOptInPlaceholderCard();
|
||||
}
|
||||
|
||||
// If user has opted out, don't show any AI content
|
||||
if (!this.optin) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (isGenerationError) {
|
||||
return this.renderErrorGenerationCard(pageUrl);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
.optin-wrapper {
|
||||
background-color: var(--background-color-canvas);
|
||||
border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
|
||||
border-radius: 8px;
|
||||
padding: var(--space-large);
|
||||
@@ -41,6 +42,10 @@
|
||||
height: 12px;
|
||||
fill: currentColor;
|
||||
-moz-context-properties: fill, fill-opacity, stroke;
|
||||
margin-inline-start: var(--space-small);
|
||||
&.icon-at-end {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.optin-progress-bar-wrapper {
|
||||
|
||||
@@ -15,6 +15,7 @@ class ModelOptin extends MozLitElement {
|
||||
static properties = {
|
||||
headingL10nId: { type: String, fluent: true },
|
||||
headingIcon: { type: String },
|
||||
iconAtEnd: { type: Boolean },
|
||||
messageL10nId: { type: String, fluent: true },
|
||||
optinButtonL10nId: { type: String, fluent: true },
|
||||
optoutButtonL10nId: { type: String, fluent: true },
|
||||
@@ -42,9 +43,11 @@ class ModelOptin extends MozLitElement {
|
||||
super();
|
||||
this.isLoading = false;
|
||||
this.isHidden = false;
|
||||
this.iconAtEnd = false;
|
||||
this.optinButtonL10nId = "genai-model-optin-continue";
|
||||
this.optoutButtonL10nId = "genai-model-optin-optout";
|
||||
this.cancelDownloadButtonL10nId = "genai-model-optin-cancel";
|
||||
this.footerMessageL10nId = "";
|
||||
}
|
||||
|
||||
dispatch(event) {
|
||||
@@ -109,7 +112,9 @@ class ModelOptin extends MozLitElement {
|
||||
? html`<img
|
||||
src=${this.headingIcon}
|
||||
alt=${this.headingL10nId}
|
||||
class="optin-heading-icon"
|
||||
class="optin-heading-icon ${this.iconAtEnd
|
||||
? "icon-at-end"
|
||||
: ""}"
|
||||
/>`
|
||||
: ""}
|
||||
<h3 class="optin-heading" data-l10n-id=${this.headingL10nId}></h3>
|
||||
|
||||
@@ -682,6 +682,26 @@ genai.linkpreview:
|
||||
|
||||
# events
|
||||
|
||||
card_ai_consent:
|
||||
type: event
|
||||
description: >
|
||||
Recorded when the user interacts with the AI consent dialog
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1951146
|
||||
data_reviews:
|
||||
- https://phabricator.services.mozilla.com/D249104
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
expires: 150
|
||||
notification_emails:
|
||||
- elee@mozilla.com
|
||||
send_in_pings:
|
||||
- events
|
||||
extra_keys:
|
||||
option:
|
||||
type: string
|
||||
description: User's response - cancel, continue, or learn
|
||||
|
||||
card_close:
|
||||
type: event
|
||||
description: >
|
||||
@@ -720,7 +740,7 @@ genai.linkpreview:
|
||||
extra_keys:
|
||||
source:
|
||||
type: string
|
||||
description: Which link, e.g., title, feedback, visit
|
||||
description: Which link, e.g., error, feedback, retry, title, visit
|
||||
|
||||
fetch:
|
||||
type: event
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
prefs = [
|
||||
"browser.ml.chat.enabled=true",
|
||||
"browser.ml.chat.openSidebarOnProviderChange=false",
|
||||
"browser.ml.linkPreview.collapsed=false",
|
||||
"browser.ml.linkPreview.optin=true",
|
||||
"browser.ml.linkPreview.prefetchOnEnable=false",
|
||||
"browser.ml.linkPreview.shiftAlt=true",
|
||||
]
|
||||
@@ -27,3 +29,11 @@ support-files = [
|
||||
"data/readableFr.html",
|
||||
"data/readableEn.html"
|
||||
]
|
||||
|
||||
["browser_link_preview_optin.js"]
|
||||
support-files = [
|
||||
"data/readableFr.html",
|
||||
"data/readableEn.html"
|
||||
]
|
||||
|
||||
["browser_link_preview_telemetry.js"]
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
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"
|
||||
);
|
||||
const TEST_LINK_URL = "https://example.com";
|
||||
|
||||
/**
|
||||
* Test that optin and collapsed preferences properly update existing cards.
|
||||
*
|
||||
* This test ensures that when preference values change, they properly
|
||||
* update the properties of already created link preview cards.
|
||||
*/
|
||||
add_task(async function test_pref_updates_existing_cards() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["browser.ml.linkPreview.enabled", true],
|
||||
["browser.ml.linkPreview.optin", true],
|
||||
["browser.ml.linkPreview.collapsed", false],
|
||||
],
|
||||
});
|
||||
|
||||
const generateStub = sinon.stub(LinkPreviewModel, "generateTextAI");
|
||||
|
||||
const READABLE_PAGE_URL =
|
||||
"https://example.com/browser/browser/components/genai/tests/browser/data/readableEn.html";
|
||||
|
||||
LinkPreview.keyboardComboActive = true;
|
||||
XULBrowserWindow.setOverLink(READABLE_PAGE_URL, {});
|
||||
|
||||
const panel = await TestUtils.waitForCondition(() =>
|
||||
document.getElementById("link-preview-panel")
|
||||
);
|
||||
await BrowserTestUtils.waitForEvent(panel, "popupshown");
|
||||
|
||||
const card = panel.querySelector("link-preview-card");
|
||||
ok(card, "card created for link preview");
|
||||
is(card.optin, true, "card has optin=true initially");
|
||||
is(card.collapsed, false, "card has collapsed=false initially");
|
||||
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["browser.ml.linkPreview.optin", false]],
|
||||
});
|
||||
is(card.optin, false, "card optin updated to false");
|
||||
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["browser.ml.linkPreview.collapsed", true]],
|
||||
});
|
||||
is(card.collapsed, true, "card collapsed updated to true");
|
||||
|
||||
panel.remove();
|
||||
generateStub.restore();
|
||||
LinkPreview.keyboardComboActive = false;
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that link preview doesn't generate key points when optin is false.
|
||||
*
|
||||
* This test verifies that when the optin preference is set to false,
|
||||
* the link preview feature will not attempt to generate key points.
|
||||
*/
|
||||
add_task(async function test_no_keypoints_when_optin_false() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["browser.ml.linkPreview.enabled", true],
|
||||
["browser.ml.linkPreview.optin", false],
|
||||
["browser.ml.linkPreview.collapsed", false],
|
||||
],
|
||||
});
|
||||
const generateStub = sinon.stub(LinkPreviewModel, "generateTextAI");
|
||||
|
||||
LinkPreview.keyboardComboActive = true;
|
||||
XULBrowserWindow.setOverLink(
|
||||
"https://example.com/browser/browser/components/genai/tests/browser/data/readableEn.html",
|
||||
{}
|
||||
);
|
||||
|
||||
let panel = await TestUtils.waitForCondition(() =>
|
||||
document.getElementById("link-preview-panel")
|
||||
);
|
||||
await BrowserTestUtils.waitForEvent(panel, "popupshown");
|
||||
|
||||
is(
|
||||
generateStub.callCount,
|
||||
0,
|
||||
"generateTextAI should not be called when optin is false"
|
||||
);
|
||||
|
||||
panel.remove();
|
||||
LinkPreview.keyboardComboActive = false;
|
||||
generateStub.restore();
|
||||
|
||||
Services.prefs.clearUserPref("browser.ml.linkPreview.optin");
|
||||
Services.prefs.clearUserPref("browser.ml.linkPreview.collapsed");
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that link preview doesn't generate key points when collapsed is true.
|
||||
*
|
||||
* This test verifies that when the collapsed preference is set to true,
|
||||
* the link preview feature will not attempt to generate key points even if
|
||||
* optin is true.
|
||||
*/
|
||||
add_task(async function test_no_keypoints_when_collapsed_true() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["browser.ml.linkPreview.enabled", true],
|
||||
["browser.ml.linkPreview.optin", true],
|
||||
["browser.ml.linkPreview.collapsed", true],
|
||||
],
|
||||
});
|
||||
|
||||
const generateStub = sinon.stub(LinkPreviewModel, "generateTextAI");
|
||||
|
||||
const READABLE_PAGE_URL =
|
||||
"https://example.com/browser/browser/components/genai/tests/browser/data/readableEn.html";
|
||||
|
||||
LinkPreview.keyboardComboActive = true;
|
||||
XULBrowserWindow.setOverLink(READABLE_PAGE_URL, {});
|
||||
|
||||
const panel = await TestUtils.waitForCondition(() =>
|
||||
document.getElementById("link-preview-panel")
|
||||
);
|
||||
await BrowserTestUtils.waitForEvent(panel, "popupshown");
|
||||
|
||||
is(
|
||||
generateStub.callCount,
|
||||
0,
|
||||
"generateTextAI should not be called when collapsed is true"
|
||||
);
|
||||
|
||||
const card = panel.querySelector("link-preview-card");
|
||||
ok(card, "card created for link preview");
|
||||
ok(!card.generating, "card should not be in generating state");
|
||||
is(card.optin, true, "card has optin=true");
|
||||
is(card.collapsed, true, "card has collapsed=true");
|
||||
|
||||
panel.remove();
|
||||
generateStub.restore();
|
||||
LinkPreview.keyboardComboActive = false;
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that link preview does generate key points when optin is true and collapsed is false.
|
||||
*
|
||||
* This test verifies that when both the optin preference is true and collapsed
|
||||
* preference is false, the link preview feature will attempt to generate key points.
|
||||
*/
|
||||
add_task(async function test_generate_keypoints_when_opted_in() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["browser.ml.linkPreview.enabled", true],
|
||||
["browser.ml.linkPreview.optin", true],
|
||||
["browser.ml.linkPreview.collapsed", false],
|
||||
],
|
||||
});
|
||||
|
||||
let onDownload, toResolve;
|
||||
const stub = sinon
|
||||
.stub(LinkPreviewModel, "generateTextAI")
|
||||
.callsFake(async (text, options) => {
|
||||
onDownload = options.onDownload;
|
||||
toResolve = Promise.withResolvers();
|
||||
return toResolve.promise;
|
||||
});
|
||||
|
||||
const READABLE_PAGE_URL =
|
||||
"https://example.com/browser/browser/components/genai/tests/browser/data/readableEn.html";
|
||||
|
||||
LinkPreview.keyboardComboActive = true;
|
||||
XULBrowserWindow.setOverLink(READABLE_PAGE_URL, {});
|
||||
|
||||
const panel = await TestUtils.waitForCondition(() =>
|
||||
document.getElementById("link-preview-panel")
|
||||
);
|
||||
await BrowserTestUtils.waitForEvent(panel, "popupshown");
|
||||
|
||||
is(
|
||||
stub.callCount,
|
||||
1,
|
||||
"generateTextAI should be called when optin=true and collapsed=false"
|
||||
);
|
||||
|
||||
const card = panel.querySelector("link-preview-card");
|
||||
|
||||
onDownload(false);
|
||||
ok(card, "card created for link preview");
|
||||
ok(card.generating, "card should be in generating state");
|
||||
is(card.optin, true, "card has optin=true");
|
||||
is(card.collapsed, false, "card has collapsed=false");
|
||||
|
||||
if (toResolve) {
|
||||
toResolve.resolve();
|
||||
await LinkPreview.lastRequest;
|
||||
}
|
||||
panel.remove();
|
||||
stub.restore();
|
||||
LinkPreview.keyboardComboActive = false;
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that when optin=true and collapsed=true, keypoints are not generated.
|
||||
*
|
||||
* This test represents a user who has opted in but doesn't want to see key points.
|
||||
* It verifies that even with optin=true, if collapsed=true then key points are not generated.
|
||||
*/
|
||||
add_task(async function test_no_keypoints_when_opted_in_but_collapsed() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["browser.ml.linkPreview.enabled", true],
|
||||
["browser.ml.linkPreview.optin", true], // Opted in
|
||||
["browser.ml.linkPreview.collapsed", true], // But collapsed (don't show keypoints)
|
||||
],
|
||||
});
|
||||
|
||||
const generateStub = sinon.stub(LinkPreviewModel, "generateTextAI");
|
||||
|
||||
const READABLE_PAGE_URL =
|
||||
"https://example.com/browser/browser/components/genai/tests/browser/data/readableEn.html";
|
||||
|
||||
LinkPreview.keyboardComboActive = true;
|
||||
XULBrowserWindow.setOverLink(READABLE_PAGE_URL, {});
|
||||
|
||||
const panel = await TestUtils.waitForCondition(() =>
|
||||
document.getElementById("link-preview-panel")
|
||||
);
|
||||
await BrowserTestUtils.waitForEvent(panel, "popupshown");
|
||||
|
||||
is(
|
||||
generateStub.callCount,
|
||||
0,
|
||||
"generateTextAI should not be called when collapsed is true"
|
||||
);
|
||||
|
||||
const card = panel.querySelector("link-preview-card");
|
||||
ok(card, "card created for link preview");
|
||||
ok(!card.generating, "card should not be in generating state");
|
||||
is(card.optin, true, "card has optin=true");
|
||||
is(card.collapsed, true, "card has collapsed=true");
|
||||
|
||||
panel.remove();
|
||||
generateStub.restore();
|
||||
LinkPreview.keyboardComboActive = false;
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that opt-out action in initial state works correctly.
|
||||
*
|
||||
* This test verifies that when a user denies the opt-in prompt,
|
||||
* the optin preference stays false and the collapsed preference is set to true.
|
||||
*/
|
||||
add_task(async function test_model_optin_deny_action() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["browser.ml.linkPreview.enabled", true],
|
||||
["browser.ml.linkPreview.optin", false], // Initial state - not opted in
|
||||
["browser.ml.linkPreview.collapsed", false], // Not collapsed
|
||||
],
|
||||
});
|
||||
|
||||
const generateStub = sinon.stub(LinkPreviewModel, "generateTextAI");
|
||||
|
||||
const READABLE_PAGE_URL =
|
||||
"https://example.com/browser/browser/components/genai/tests/browser/data/readableEn.html";
|
||||
|
||||
LinkPreview.keyboardComboActive = true;
|
||||
XULBrowserWindow.setOverLink(READABLE_PAGE_URL, {});
|
||||
|
||||
const panel = await TestUtils.waitForCondition(() =>
|
||||
document.getElementById("link-preview-panel")
|
||||
);
|
||||
await BrowserTestUtils.waitForEvent(panel, "popupshown");
|
||||
|
||||
const card = panel.querySelector("link-preview-card");
|
||||
ok(card, "card created for link preview");
|
||||
|
||||
const modelOptinElement = await TestUtils.waitForCondition(() => {
|
||||
if (card.shadowRoot) {
|
||||
return card.shadowRoot.querySelector("model-optin");
|
||||
}
|
||||
return null;
|
||||
}, "Waiting for model-optin element");
|
||||
|
||||
ok(modelOptinElement, "model-optin element is present");
|
||||
|
||||
const optinDenyEvent = new CustomEvent("MlModelOptinDeny", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
});
|
||||
modelOptinElement.dispatchEvent(optinDenyEvent);
|
||||
|
||||
is(
|
||||
Services.prefs.getBoolPref("browser.ml.linkPreview.optin"),
|
||||
false,
|
||||
"optin preference should remain false after denying"
|
||||
);
|
||||
is(
|
||||
Services.prefs.getBoolPref("browser.ml.linkPreview.collapsed"),
|
||||
true,
|
||||
"collapsed preference should be true after denying"
|
||||
);
|
||||
|
||||
is(
|
||||
generateStub.callCount,
|
||||
0,
|
||||
"generateTextAI should not be called after user denies opt-in"
|
||||
);
|
||||
|
||||
panel.remove();
|
||||
generateStub.restore();
|
||||
LinkPreview.keyboardComboActive = false;
|
||||
Services.prefs.setBoolPref("browser.ml.linkPreview.optin", false);
|
||||
Services.prefs.setBoolPref("browser.ml.linkPreview.collapsed", false);
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that opt-in confirm action in initial state works correctly.
|
||||
*
|
||||
* This test verifies that when a user confirms the opt-in prompt,
|
||||
* the optin preference is set to true and key points generation is triggered.
|
||||
*/
|
||||
add_task(async function test_model_optin_confirm_action() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["browser.ml.linkPreview.enabled", true],
|
||||
["browser.ml.linkPreview.optin", false], // Initial state - not opted in
|
||||
["browser.ml.linkPreview.collapsed", false], // Not collapsed
|
||||
],
|
||||
});
|
||||
|
||||
const generateStub = sinon.stub(LinkPreviewModel, "generateTextAI");
|
||||
|
||||
const READABLE_PAGE_URL =
|
||||
"https://example.com/browser/browser/components/genai/tests/browser/data/readableEn.html";
|
||||
|
||||
LinkPreview.keyboardComboActive = true;
|
||||
XULBrowserWindow.setOverLink(READABLE_PAGE_URL, {});
|
||||
|
||||
const panel = await TestUtils.waitForCondition(() =>
|
||||
document.getElementById("link-preview-panel")
|
||||
);
|
||||
await BrowserTestUtils.waitForEvent(panel, "popupshown");
|
||||
|
||||
const card = panel.querySelector("link-preview-card");
|
||||
ok(card, "card created for link preview");
|
||||
|
||||
const modelOptinElement = await TestUtils.waitForCondition(() => {
|
||||
if (card.shadowRoot) {
|
||||
return card.shadowRoot.querySelector("model-optin");
|
||||
}
|
||||
return null;
|
||||
}, "Waiting for model-optin element");
|
||||
|
||||
ok(modelOptinElement, "model-optin element is present");
|
||||
|
||||
const optinConfirmEvent = new CustomEvent("MlModelOptinConfirm", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
});
|
||||
modelOptinElement.dispatchEvent(optinConfirmEvent);
|
||||
|
||||
is(
|
||||
Services.prefs.getBoolPref("browser.ml.linkPreview.optin"),
|
||||
true,
|
||||
"optin preference should be set to true after confirming"
|
||||
);
|
||||
is(
|
||||
Services.prefs.getBoolPref("browser.ml.linkPreview.collapsed"),
|
||||
false,
|
||||
"collapsed preference should remain false after confirming"
|
||||
);
|
||||
|
||||
await TestUtils.waitForCondition(
|
||||
() => generateStub.callCount > 0,
|
||||
"Waiting for generateTextAI to be called"
|
||||
);
|
||||
Assert.greater(
|
||||
generateStub.callCount,
|
||||
0,
|
||||
"generateTextAI should be called after user confirms opt-in"
|
||||
);
|
||||
|
||||
panel.remove();
|
||||
generateStub.restore();
|
||||
LinkPreview.keyboardComboActive = false;
|
||||
Services.prefs.setBoolPref("browser.ml.linkPreview.optin", false);
|
||||
Services.prefs.setBoolPref("browser.ml.linkPreview.collapsed", false);
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const { LinkPreview } = ChromeUtils.importESModule(
|
||||
"moz-src:///browser/components/genai/LinkPreview.sys.mjs"
|
||||
);
|
||||
|
||||
add_setup(async () => {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
clear: [["browser.ml.linkPreview.optin"]],
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that AI consent telemetry records the "continue" (accept) option
|
||||
*/
|
||||
add_task(async function test_link_preview_ai_consent_continue() {
|
||||
Services.fog.testResetFOG();
|
||||
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["browser.ml.linkPreview.optin", true]],
|
||||
});
|
||||
|
||||
let events = Glean.genaiLinkpreview.cardAiConsent.testGetValue();
|
||||
Assert.equal(events.length, 1, "One consent event recorded");
|
||||
Assert.equal(events[0].extra.option, "continue", "Continue option recorded");
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that AI consent telemetry records the "cancel" (deny) option
|
||||
*/
|
||||
add_task(async function test_link_preview_ai_consent_cancel() {
|
||||
Services.fog.testResetFOG();
|
||||
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["browser.ml.linkPreview.optin", false]],
|
||||
});
|
||||
|
||||
let events = Glean.genaiLinkpreview.cardAiConsent.testGetValue();
|
||||
Assert.equal(events.length, 1, "One consent event recorded");
|
||||
Assert.equal(events[0].extra.option, "cancel", "Cancel option recorded");
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that AI consent telemetry records the "learn" option
|
||||
*/
|
||||
add_task(async function test_link_preview_ai_consent_learn() {
|
||||
Services.fog.testResetFOG();
|
||||
|
||||
Glean.genaiLinkpreview.cardAiConsent.record({ option: "learn" });
|
||||
|
||||
let events = Glean.genaiLinkpreview.cardAiConsent.testGetValue();
|
||||
Assert.equal(events.length, 1, "One consent event recorded");
|
||||
Assert.equal(events[0].extra.option, "learn", "Learn option recorded");
|
||||
});
|
||||
@@ -35,3 +35,15 @@ link-preview-settings-shift-alt =
|
||||
}
|
||||
link-preview-settings-long-press =
|
||||
.label = Shortcut: Click and hold the link for 1 second (long press)
|
||||
|
||||
# Title that appears when user is shown the opt-in flow for link previews
|
||||
link-preview-optin-title = See more with AI?
|
||||
|
||||
# Message that appears when user is shown the opt-in flow for link previews
|
||||
link-preview-optin-message = { -brand-short-name } uses AI to read the beginning of the page and generate a few key points. To prioritize your privacy, this happens on your device.
|
||||
|
||||
# Title displayed during first-time download of AI model
|
||||
link-preview-optin-title-download = Setting up link previews…
|
||||
|
||||
# Message displayed during first-time download of AI model
|
||||
link-preview-optin-message-download = This may take a moment.
|
||||
|
||||
Reference in New Issue
Block a user