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.chat.sidebar", true);
|
||||||
|
|
||||||
pref("browser.ml.linkPreview.allowedLanguages", "en");
|
pref("browser.ml.linkPreview.allowedLanguages", "en");
|
||||||
pref("browser.ml.linkPreview.enabled", false);
|
|
||||||
pref("browser.ml.linkPreview.blockListEnabled", true);
|
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.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.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);
|
pref("browser.ml.linkPreview.optin", false);
|
||||||
|
|||||||
@@ -16,11 +16,18 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
|||||||
"allowedLanguages",
|
"allowedLanguages",
|
||||||
"browser.ml.linkPreview.allowedLanguages"
|
"browser.ml.linkPreview.allowedLanguages"
|
||||||
);
|
);
|
||||||
|
XPCOMUtils.defineLazyPreferenceGetter(
|
||||||
|
lazy,
|
||||||
|
"collapsed",
|
||||||
|
"browser.ml.linkPreview.collapsed",
|
||||||
|
null,
|
||||||
|
(_pref, _old, val) => LinkPreview.onCollapsedPref(val)
|
||||||
|
);
|
||||||
XPCOMUtils.defineLazyPreferenceGetter(
|
XPCOMUtils.defineLazyPreferenceGetter(
|
||||||
lazy,
|
lazy,
|
||||||
"enabled",
|
"enabled",
|
||||||
"browser.ml.linkPreview.enabled",
|
"browser.ml.linkPreview.enabled",
|
||||||
false,
|
null,
|
||||||
(_pref, _old, val) => LinkPreview.onEnabledPref(val)
|
(_pref, _old, val) => LinkPreview.onEnabledPref(val)
|
||||||
);
|
);
|
||||||
XPCOMUtils.defineLazyPreferenceGetter(
|
XPCOMUtils.defineLazyPreferenceGetter(
|
||||||
@@ -28,6 +35,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
|||||||
"noKeyPointsRegions",
|
"noKeyPointsRegions",
|
||||||
"browser.ml.linkPreview.noKeyPointsRegions"
|
"browser.ml.linkPreview.noKeyPointsRegions"
|
||||||
);
|
);
|
||||||
|
XPCOMUtils.defineLazyPreferenceGetter(
|
||||||
|
lazy,
|
||||||
|
"optin",
|
||||||
|
"browser.ml.linkPreview.optin",
|
||||||
|
null,
|
||||||
|
(_pref, _old, val) => LinkPreview.onOptinPref(val)
|
||||||
|
);
|
||||||
XPCOMUtils.defineLazyPreferenceGetter(
|
XPCOMUtils.defineLazyPreferenceGetter(
|
||||||
lazy,
|
lazy,
|
||||||
"prefetchOnEnable",
|
"prefetchOnEnable",
|
||||||
@@ -102,12 +116,60 @@ export const LinkPreview = {
|
|||||||
Glean.genaiLinkpreview.labsCheckbox.record({ enabled });
|
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.
|
* Handles startup tasks such as telemetry and adding listeners.
|
||||||
*
|
*
|
||||||
* @param {Window} win - The window context used to add event listeners.
|
* @param {Window} win - The window context used to add event listeners.
|
||||||
*/
|
*/
|
||||||
init(win) {
|
init(win) {
|
||||||
|
// Access getters for side effects of observing pref changes
|
||||||
|
lazy.collapsed;
|
||||||
|
lazy.enabled;
|
||||||
|
lazy.optin;
|
||||||
|
|
||||||
this._windowStates.set(win, {});
|
this._windowStates.set(win, {});
|
||||||
if (!win.customElements.get("link-preview-card")) {
|
if (!win.customElements.get("link-preview-card")) {
|
||||||
win.ChromeUtils.importESModule(
|
win.ChromeUtils.importESModule(
|
||||||
@@ -260,6 +322,9 @@ export const LinkPreview = {
|
|||||||
ogCard.style.width = "100%";
|
ogCard.style.width = "100%";
|
||||||
ogCard.pageData = pageData;
|
ogCard.pageData = pageData;
|
||||||
|
|
||||||
|
ogCard.optin = lazy.optin;
|
||||||
|
ogCard.collapsed = lazy.collapsed;
|
||||||
|
|
||||||
// Reflect the shared download progress to this preview.
|
// Reflect the shared download progress to this preview.
|
||||||
const updateProgress = () => {
|
const updateProgress = () => {
|
||||||
ogCard.progress = this.progress;
|
ogCard.progress = this.progress;
|
||||||
@@ -303,6 +368,12 @@ export const LinkPreview = {
|
|||||||
* @param {boolean} _retry Indicates whether to retry the operation.
|
* @param {boolean} _retry Indicates whether to retry the operation.
|
||||||
*/
|
*/
|
||||||
async generateKeyPoints(ogCard, _retry = false) {
|
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.
|
// Support prefetching without a card by mocking expected properties.
|
||||||
let outcome = ogCard ? "success" : "prefetch";
|
let outcome = ogCard ? "success" : "prefetch";
|
||||||
if (!ogCard) {
|
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.
|
* Renders the link preview panel at the specified coordinates.
|
||||||
*
|
*
|
||||||
@@ -453,16 +540,15 @@ export const LinkPreview = {
|
|||||||
Glean.genaiLinkpreview.cardLink.record({ source: event.detail });
|
Glean.genaiLinkpreview.cardLink.record({ source: event.detail });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add event listener for the retry event
|
|
||||||
ogCard.addEventListener("LinkPreviewCard:retry", _event => {
|
ogCard.addEventListener("LinkPreviewCard:retry", _event => {
|
||||||
// Reset error states
|
this._handleKeyPointsGenerationEvent(ogCard, "retry");
|
||||||
ogCard.isMissingDataErrorState = false;
|
Glean.genaiLinkpreview.cardLink.record({ source: "retry" });
|
||||||
ogCard.isGenerationErrorState = false;
|
|
||||||
|
|
||||||
this.generateKeyPoints(ogCard, true);
|
|
||||||
//TODO: review if glean record is correct
|
|
||||||
// Glean.genaiLinkpreview.cardLink.record({ source: url, op: "retry" });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ogCard.addEventListener("LinkPreviewCard:generate", _event => {
|
||||||
|
this._handleKeyPointsGenerationEvent(ogCard, "generate");
|
||||||
|
});
|
||||||
|
|
||||||
openPopup();
|
openPopup();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ai-content {
|
.ai-content {
|
||||||
|
position: relative;
|
||||||
padding: var(--og-padding);
|
padding: var(--og-padding);
|
||||||
.og-error-message-container {
|
.og-error-message-container {
|
||||||
margin:0;
|
margin:0;
|
||||||
@@ -162,6 +163,11 @@
|
|||||||
border-color: var(--skeleton-loader-motion-element-color);
|
border-color: var(--skeleton-loader-motion-element-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.static div {
|
||||||
|
animation: none;
|
||||||
|
background: var(--skeleton-loader-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
div:nth-of-type(1) {
|
div:nth-of-type(1) {
|
||||||
max-width: 95%;
|
max-width: 95%;
|
||||||
}
|
}
|
||||||
@@ -182,3 +188,13 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
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/.
|
* 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";
|
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
|
||||||
|
|
||||||
const lazy = {};
|
const lazy = {};
|
||||||
@@ -24,6 +28,13 @@ ChromeUtils.defineLazyGetter(
|
|||||||
() => new Services.intl.PluralRules()
|
() => new Services.intl.PluralRules()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ChromeUtils.importESModule(
|
||||||
|
"chrome://browser/content/genai/content/model-optin.mjs",
|
||||||
|
{
|
||||||
|
global: "current",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const FEEDBACK_LINK =
|
const FEEDBACK_LINK =
|
||||||
"https://connect.mozilla.org/t5/discussions/try-out-link-previews-on-firefox-labs/td-p/92012";
|
"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
|
* @augments MozLitElement
|
||||||
*/
|
*/
|
||||||
class LinkPreviewCard extends MozLitElement {
|
class LinkPreviewCard extends MozLitElement {
|
||||||
|
static AI_ICON = "chrome://global/skin/icons/highlights.svg";
|
||||||
// Number of placeholder rows to show when loading
|
// Number of placeholder rows to show when loading
|
||||||
static PLACEHOLDER_COUNT = 3;
|
static PLACEHOLDER_COUNT = 3;
|
||||||
|
|
||||||
static properties = {
|
static properties = {
|
||||||
|
collapsed: { type: Boolean },
|
||||||
generating: { type: Number }, // 0 = off, 1-4 = generating & dots state
|
generating: { type: Number }, // 0 = off, 1-4 = generating & dots state
|
||||||
|
isGenerationErrorState: { type: Boolean },
|
||||||
|
isMissingDataErrorState: { type: Boolean },
|
||||||
keyPoints: { type: Array },
|
keyPoints: { type: Array },
|
||||||
|
optin: { type: Boolean },
|
||||||
pageData: { type: Object },
|
pageData: { type: Object },
|
||||||
progress: { type: Number }, // -1 = off, 0-100 = download progress
|
progress: { type: Number }, // -1 = off, 0-100 = download progress
|
||||||
isMissingDataErrorState: { type: Boolean },
|
|
||||||
isGenerationErrorState: { type: Boolean },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.keyPoints = [];
|
this.collapsed = false;
|
||||||
this.progress = -1;
|
|
||||||
this.isMissingDataErrorState = false;
|
|
||||||
this.isGenerationErrorState = 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) {
|
updated(properties) {
|
||||||
|
if (this.optinRef.value) {
|
||||||
|
this.optinRef.value.headingIcon = LinkPreviewCard.AI_ICON;
|
||||||
|
}
|
||||||
|
|
||||||
if (properties.has("generating")) {
|
if (properties.has("generating")) {
|
||||||
if (this.generating > 0) {
|
if (this.generating > 0) {
|
||||||
// Count up to 4 so that we can show 0 to 3 dots.
|
// 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.
|
* 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.
|
* Renders the appropriate content card based on state.
|
||||||
*
|
*
|
||||||
@@ -295,6 +396,16 @@ class LinkPreviewCard extends MozLitElement {
|
|||||||
const isGenerationError =
|
const isGenerationError =
|
||||||
this.isMissingDataErrorState || this.isGenerationErrorState;
|
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) {
|
if (isGenerationError) {
|
||||||
return this.renderErrorGenerationCard(pageUrl);
|
return this.renderErrorGenerationCard(pageUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
.optin-wrapper {
|
.optin-wrapper {
|
||||||
|
background-color: var(--background-color-canvas);
|
||||||
border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
|
border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: var(--space-large);
|
padding: var(--space-large);
|
||||||
@@ -41,6 +42,10 @@
|
|||||||
height: 12px;
|
height: 12px;
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
-moz-context-properties: fill, fill-opacity, stroke;
|
-moz-context-properties: fill, fill-opacity, stroke;
|
||||||
|
margin-inline-start: var(--space-small);
|
||||||
|
&.icon-at-end {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.optin-progress-bar-wrapper {
|
.optin-progress-bar-wrapper {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class ModelOptin extends MozLitElement {
|
|||||||
static properties = {
|
static properties = {
|
||||||
headingL10nId: { type: String, fluent: true },
|
headingL10nId: { type: String, fluent: true },
|
||||||
headingIcon: { type: String },
|
headingIcon: { type: String },
|
||||||
|
iconAtEnd: { type: Boolean },
|
||||||
messageL10nId: { type: String, fluent: true },
|
messageL10nId: { type: String, fluent: true },
|
||||||
optinButtonL10nId: { type: String, fluent: true },
|
optinButtonL10nId: { type: String, fluent: true },
|
||||||
optoutButtonL10nId: { type: String, fluent: true },
|
optoutButtonL10nId: { type: String, fluent: true },
|
||||||
@@ -42,9 +43,11 @@ class ModelOptin extends MozLitElement {
|
|||||||
super();
|
super();
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.isHidden = false;
|
this.isHidden = false;
|
||||||
|
this.iconAtEnd = false;
|
||||||
this.optinButtonL10nId = "genai-model-optin-continue";
|
this.optinButtonL10nId = "genai-model-optin-continue";
|
||||||
this.optoutButtonL10nId = "genai-model-optin-optout";
|
this.optoutButtonL10nId = "genai-model-optin-optout";
|
||||||
this.cancelDownloadButtonL10nId = "genai-model-optin-cancel";
|
this.cancelDownloadButtonL10nId = "genai-model-optin-cancel";
|
||||||
|
this.footerMessageL10nId = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(event) {
|
dispatch(event) {
|
||||||
@@ -109,7 +112,9 @@ class ModelOptin extends MozLitElement {
|
|||||||
? html`<img
|
? html`<img
|
||||||
src=${this.headingIcon}
|
src=${this.headingIcon}
|
||||||
alt=${this.headingL10nId}
|
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>
|
<h3 class="optin-heading" data-l10n-id=${this.headingL10nId}></h3>
|
||||||
|
|||||||
@@ -682,6 +682,26 @@ genai.linkpreview:
|
|||||||
|
|
||||||
# events
|
# 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:
|
card_close:
|
||||||
type: event
|
type: event
|
||||||
description: >
|
description: >
|
||||||
@@ -720,7 +740,7 @@ genai.linkpreview:
|
|||||||
extra_keys:
|
extra_keys:
|
||||||
source:
|
source:
|
||||||
type: string
|
type: string
|
||||||
description: Which link, e.g., title, feedback, visit
|
description: Which link, e.g., error, feedback, retry, title, visit
|
||||||
|
|
||||||
fetch:
|
fetch:
|
||||||
type: event
|
type: event
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
prefs = [
|
prefs = [
|
||||||
"browser.ml.chat.enabled=true",
|
"browser.ml.chat.enabled=true",
|
||||||
"browser.ml.chat.openSidebarOnProviderChange=false",
|
"browser.ml.chat.openSidebarOnProviderChange=false",
|
||||||
|
"browser.ml.linkPreview.collapsed=false",
|
||||||
|
"browser.ml.linkPreview.optin=true",
|
||||||
"browser.ml.linkPreview.prefetchOnEnable=false",
|
"browser.ml.linkPreview.prefetchOnEnable=false",
|
||||||
"browser.ml.linkPreview.shiftAlt=true",
|
"browser.ml.linkPreview.shiftAlt=true",
|
||||||
]
|
]
|
||||||
@@ -27,3 +29,11 @@ support-files = [
|
|||||||
"data/readableFr.html",
|
"data/readableFr.html",
|
||||||
"data/readableEn.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 =
|
link-preview-settings-long-press =
|
||||||
.label = Shortcut: Click and hold the link for 1 second (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