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:
committed by
txia@mozilla.com
parent
75cbd03398
commit
b3598a91d0
@@ -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();
|
||||
},
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -69,7 +81,6 @@
|
||||
width: var(--icon-size-default);
|
||||
}
|
||||
|
||||
|
||||
> ul {
|
||||
font-size: var(--og-main-font-size);
|
||||
line-height: 1.15; /* Design requires 18px line-height */
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
|
||||
@@ -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 can’t 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;
|
||||
});
|
||||
|
||||
@@ -7,3 +7,12 @@ link-preview-error-message = We can’t 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 can’t 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
|
||||
|
||||
Reference in New Issue
Block a user