Bug 1958926 - Error state update for Link Preview - r=Mardak,firefox-ai-ml-reviewers,fluent-reviewers,bolsson

- retry for generation error
- update error message for when page is not supported
- Revert "error state update for eu region check"
- change from --text-color-disabled to --text-color-deemphasized
- update generation error message to "Something went wrong."
- only show "Visit Link" when generation is finished, same as footer
- move region check out of data check conditions
- refactor for fluent strings
- minor refactor logic flow for renderKeyPointsSection
- comment change for link-preview-generation-error-missing-data fluent string

Differential Revision: https://phabricator.services.mozilla.com/D246485
This commit is contained in:
Tim Xia
2025-05-08 21:05:21 +00:00
committed by txia@mozilla.com
parent 75cbd03398
commit b3598a91d0
5 changed files with 320 additions and 97 deletions

View File

@@ -225,10 +225,14 @@ export const LinkPreview = {
};
updateProgress();
if (!this._isRegionSupported()) {
// Region not supported, just don't show key points section
return ogCard;
}
// Generate key points if we have content, language and configured for any
// language or restricted.
if (
this._isRegionSupported() &&
pageData.article.textContent &&
pageData.article.detectedLanguage &&
(!lazy.allowedLanguages ||
@@ -237,7 +241,10 @@ export const LinkPreview = {
.includes(pageData.article.detectedLanguage))
) {
this.generateKeyPoints(ogCard);
} else {
ogCard.isMissingDataErrorState = true;
}
return ogCard;
},
@@ -245,8 +252,9 @@ export const LinkPreview = {
* Generate AI key points for card.
*
* @param {LinkPreviewCard} ogCard to add key points
* @param {boolean} _retry Indicates whether to retry the operation.
*/
async generateKeyPoints(ogCard) {
async generateKeyPoints(ogCard, _retry = false) {
// Support prefetching without a card by mocking expected properties.
let outcome = ogCard ? "success" : "prefetch";
if (!ogCard) {
@@ -290,6 +298,7 @@ export const LinkPreview = {
onError: error => {
console.error(error);
outcome = error;
ogCard.isGenerationErrorState = true;
},
onText: text => {
// Clear waiting in case a different generate handled download.
@@ -396,6 +405,16 @@ 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" });
});
openPopup();
},

View File

@@ -50,6 +50,18 @@
.ai-content {
padding: var(--og-padding);
.og-error-message-container {
margin:0;
font-size: var(--og-main-font-size);
.og-error-message {
color: var(--text-color-deemphasized);
}
a {
color: var(--text-color-deemphasized);
}
}
h3 {
align-items: center;
@@ -67,23 +79,22 @@
margin-inline-start: var(--space-xlarge);
pointer-events: none;
width: var(--icon-size-default);
}
> ul {
font-size: var(--og-main-font-size);
line-height: 1.15; /* Design requires 18px line-height */
list-style-type: square;
padding-inline-start: var(--space-large);
}
li {
margin-block: var(--space-medium);
padding-inline-start: 5px;
&::marker {
color: var(--border-color-deemphasized);
}
}
> ul {
font-size: var(--og-main-font-size);
line-height: 1.15; /* Design requires 18px line-height */
list-style-type: square;
padding-inline-start: var(--space-large);
}
li {
margin-block: var(--space-medium);
padding-inline-start: 5px;
&::marker {
color: var(--border-color-deemphasized);
}
}
.visit-link-container {
align-items: center;

View File

@@ -43,12 +43,16 @@ class LinkPreviewCard extends MozLitElement {
keyPoints: { type: Array },
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.isGenerationErrorState = false;
}
addKeyPoint(text) {
@@ -86,6 +90,17 @@ class LinkPreviewCard extends MozLitElement {
);
}
/**
* Handles retry request for key points generation.
*
* @param {MouseEvent} event - The click event.
*/
handleRetry(event) {
event.preventDefault();
// Dispatch retry event to be handled by LinkPreview.sys.mjs
this.dispatchEvent(new CustomEvent("LinkPreviewCard:retry"));
}
updated(properties) {
if (properties.has("generating")) {
if (this.generating > 0) {
@@ -101,6 +116,180 @@ class LinkPreviewCard extends MozLitElement {
}
}
/**
* Get the appropriate Fluent ID for the error message based on the error state.
*
* @returns {string} The Fluent ID for the error message.
*/
get errorMessageL10nId() {
if (this.isMissingDataErrorState) {
return "link-preview-generation-error-missing-data";
} else if (this.isGenerationErrorState) {
return "link-preview-generation-error-unexpected";
}
return "";
}
/**
* Renders the error generation card for when we have a generation error.
*
* @returns {import('lit').TemplateResult} The error generation card HTML
*/
renderErrorGenerationCard() {
return html`
<div class="ai-content">
<p class="og-error-message-container">
<span
class="og-error-message"
data-l10n-id=${this.errorMessageL10nId}
></span>
${this.isGenerationErrorState
? html`
<span class="retry-link">
<a
href="#"
@click=${this.handleRetry}
data-l10n-id="link-preview-generation-retry"
></a>
</span>
`
: ""}
</p>
</div>
`;
}
/**
* Renders the normal generation card for displaying key points.
*
* @param {string} pageUrl - URL of the page being previewed
* @returns {import('lit').TemplateResult} The normal generation card HTML
*/
/**
* Renders the normal generation card for displaying key points.
*
* @param {string} pageUrl - URL of the page being previewed
* @returns {import('lit').TemplateResult} The normal generation card HTML
*/
renderNormalGenerationCard(pageUrl) {
// Extract the links section into its own variable
const linksSection = html`
<p>
Key points are AI-generated and may have mistakes.
<a
@click=${this.handleLink}
data-source="feedback"
href=${FEEDBACK_LINK}
>
Share feedback
</a>
</p>
<p>
<a @click=${this.handleLink} data-source="visit" href=${pageUrl}>
Visit original page
</a>
</p>
`;
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">
${
/* All populated content items */
this.keyPoints.map(
item => html`<li class="content-item">${item}</li>`
)
}
${
/* Loading placeholders with three divs each */
this.generating
? Array(
Math.max(
0,
LinkPreviewCard.PLACEHOLDER_COUNT - this.keyPoints.length
)
)
.fill()
.map(
() =>
html` <li class="content-item loading">
<div></div>
<div></div>
<div></div>
</li>`
)
: []
}
</ul>
${!this.generating
? html`
<div class="visit-link-container">
<a
@click=${this.handleLink}
data-source="visit"
href=${pageUrl}
class="visit-link"
>
Visit page
<img
class="icon"
xmlns="http://www.w3.org/1999/xhtml"
role="presentation"
src="chrome://global/skin/icons/open-in-new.svg"
/>
</a>
</div>
`
: ""}
${this.progress >= 0
? html`
<p>First-time setup • <strong>${this.progress}%</strong></p>
<p>You'll see key points more quickly next time.</p>
`
: ""}
${!this.generating
? html`
<hr />
${linksSection}
`
: ""}
</div>
`;
}
/**
* Renders the appropriate content card based on state.
*
* @param {string} pageUrl - URL of the page being previewed
* @returns {import('lit').TemplateResult} The content card HTML
*/
renderKeyPointsSection(pageUrl) {
// Determine if there's any generation error state
const isGenerationError =
this.isMissingDataErrorState || this.isGenerationErrorState;
if (isGenerationError) {
return this.renderErrorGenerationCard(pageUrl);
}
// Show key points section only if generating or we have key points
if (this.generating || this.keyPoints.length) {
return this.renderNormalGenerationCard(pageUrl);
}
// Otherwise, don't show the keypoints section
return "";
}
/**
* Renders the link preview element.
*
@@ -196,85 +385,7 @@ class LinkPreviewCard extends MozLitElement {
`
: ""}
</div>
${this.generating || this.keyPoints.length
? 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">
${
/* All populated content items */
this.keyPoints.map(
item => html`<li class="content-item">${item}</li>`
)
}
${
/* Loading placeholders with three divs each */
this.generating
? Array(
Math.max(
0,
LinkPreviewCard.PLACEHOLDER_COUNT -
this.keyPoints.length
)
)
.fill()
.map(
() =>
html` <li class="content-item loading">
<div></div>
<div></div>
<div></div>
</li>`
)
: []
}
</ul>
<div class="visit-link-container">
<a
@click=${this.handleLink}
data-source="visit"
href=${pageUrl}
class="visit-link"
>
Visit page
<img
class="icon"
xmlns="http://www.w3.org/1999/xhtml"
role="presentation"
src="chrome://global/skin/icons/open-in-new.svg"
/>
</a>
</div>
${this.progress >= 0
? html`
<p>
First-time setup • <strong>${this.progress}%</strong>
</p>
<p>You'll see key points more quickly next time.</p>
`
: ""}
<hr />
<p>
Key points are AI-generated and may have mistakes.
<a
@click=${this.handleLink}
data-source="feedback"
href=${FEEDBACK_LINK}
>
Share feedback
</a>
</p>
</div>
`
: ""}
${this.renderKeyPointsSection(pageUrl)}
</div>
`;

View File

@@ -455,3 +455,76 @@ add_task(async function test_no_key_points_in_disallowed_region() {
Services.prefs.clearUserPref("browser.ml.linkPreview.noKeyPointsRegions");
});
/**
* Test that .og-error-message element is rendered with correct error messages
* given different props set on the card
*/
add_task(async function test_link_preview_error_rendered() {
await SpecialPowers.pushPrefEnv({
set: [["browser.ml.linkPreview.enabled", true]],
});
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, "Found link-preview-card in panel");
// Check that errors are off by default.
ok(
!card.isMissingDataErrorState,
"Should not be missing data error initially"
);
ok(!card.isGenerationErrorState, "Should not be generation error initially");
// Force a "missing data" error and confirm the card updates.
card.isMissingDataErrorState = true;
await TestUtils.waitForCondition(() =>
card.shadowRoot.querySelector(".og-error-message")
);
let ogErrorEl1 = card.shadowRoot.querySelector(".og-error-message");
ok(ogErrorEl1, "og-error-message shown with isMissingDataErrorState = true");
is(
ogErrorEl1.getAttribute("data-l10n-id"),
"link-preview-generation-error-missing-data",
"Correct fluent ID for missing data error"
);
is(
ogErrorEl1.textContent.trim(),
"We cant generate key points for this webpage.",
"Correct localized message for missing data error"
);
// Switch to a "generation error"
card.isMissingDataErrorState = false;
card.isGenerationErrorState = true;
await TestUtils.waitForCondition(() =>
card.shadowRoot.querySelector(".og-error-message")
);
let ogErrorEl2 = card.shadowRoot.querySelector(".og-error-message");
ok(ogErrorEl2, "og-error-message shown with isGenerationErrorState = true");
is(
ogErrorEl2.getAttribute("data-l10n-id"),
"link-preview-generation-error-unexpected",
"Correct fluent ID for generation error"
);
is(
ogErrorEl2.textContent.trim(),
"Something went wrong.",
"Correct localized message for generation error"
);
// Cleanup
panel.remove();
LinkPreview.keyboardComboActive = false;
});

View File

@@ -7,3 +7,12 @@ link-preview-error-message = We cant preview this link
# Text for the link to visit the original URL when in error state
link-preview-visit-link = Visit link
# Error message when we can't generate key points (summary highlights or main ideas of page content) for a page
link-preview-generation-error-missing-data = We cant generate key points for this webpage.
# Error message when something went wrong during key point generation
link-preview-generation-error-unexpected = Something went wrong.
# Text for the retry link when generation fails
link-preview-generation-retry = Try again