0

recorder: Render model load error in playback page

When model fails to download, expand the accordion to display the error
message and provide the download button for users to try again.

Bug: b:395788668
Test: Chromevox
Test: cra.py dev ${BOARD}
Change-Id: I067fe3afebfb84966b974cdc115c69ae50f161be
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6310261
Reviewed-by: Kam Kwankajornkiet <kamchonlathorn@chromium.org>
Commit-Queue: Jennifer Ling <hsuanling@google.com>
Cr-Commit-Position: refs/heads/main@{#1427133}
This commit is contained in:
hsuanling
2025-03-03 07:54:08 -08:00
committed by Chromium LUCI CQ
parent 916a71d808
commit b103b726b6
10 changed files with 153 additions and 54 deletions

@ -28,6 +28,12 @@ const webui::LocalizedString kLocalizedStrings[] = {
IDS_RECORDER_EXPORT_DIALOG_TRANSCRIPTION_HEADER},
{"genAiDisclaimerText", IDS_RECORDER_GEN_AI_DISCLAIMER_TEXT},
{"genAiErrorGeneralLabel", IDS_RECORDER_GEN_AI_ERROR_GENERAL_LABEL},
{"genAiErrorModelDownloadButton",
IDS_RECORDER_GEN_AI_ERROR_MODEL_DOWNLOAD_BUTTON},
{"genAiErrorModelDownloadButtonAriaLabel",
IDS_RECORDER_GEN_AI_ERROR_MODEL_DOWNLOAD_BUTTON_ARIA_LABEL},
{"genAiErrorModelLoadFailureLabel",
IDS_RECORDER_GEN_AI_ERROR_MODEL_LOAD_FAILURE_LABEL},
{"genAiErrorSummaryLanguageUnsupportedLabel",
IDS_RECORDER_GEN_AI_ERROR_SUMMARY_LANGUAGE_UNSUPPORTED_LABEL},
{"genAiErrorSummaryTranscriptTooLongLabel",

@ -26,10 +26,27 @@ export class GenaiError extends ReactiveLitElement {
align-items: center;
display: flex;
flex-flow: column;
font: var(--cros-button-1-font);
gap: 16px;
padding: 32px;
}
#description {
align-items: center;
display: flex;
flex-flow: column;
font: var(--cros-button-1-font);
gap: 8px;
text-align: center;
& > button {
font: var(--cros-button-2-font);
background: none;
border: none;
color: var(--cros-sys-primary);
cursor: pointer;
margin: 0;
padding: 0;
}
}
`;
@ -41,6 +58,10 @@ export class GenaiError extends ReactiveLitElement {
resultType: GenaiResultType|null = null;
private onDownloadClick() {
this.dispatchEvent(new CustomEvent('download-clicked'));
}
override render(): RenderResult {
if (this.error === null) {
return nothing;
@ -48,11 +69,23 @@ export class GenaiError extends ReactiveLitElement {
let imageName: string;
let message: string;
let action: RenderResult = nothing;
switch (this.error) {
case ModelResponseError.GENERAL:
imageName = 'genai_error_general';
message = i18n.genAiErrorGeneralLabel;
break;
case ModelResponseError.LOAD_FAILURE:
imageName = 'genai_error_general';
message = i18n.genAiErrorModelLoadFailureLabel;
// Use native button element to make text clickable.
action = html`
<button
aria-label=${i18n.genAiErrorModelDownloadButtonAriaLabel}
@click=${this.onDownloadClick}
>${i18n.genAiErrorModelDownloadButton}</button>
`;
break;
case ModelResponseError.UNSUPPORTED_LANGUAGE: {
imageName = 'genai_error_unsafe';
const resultType = assertExists(this.resultType);
@ -119,10 +152,12 @@ export class GenaiError extends ReactiveLitElement {
assertExhaustive(this.error);
}
// TODO(pihsun): Add a "try again" button.
return html`
<cra-image .name=${imageName}></cra-image>
<span>${message}</span>
<div id="description">
<span>${message}</span>
${action}
</div>
`;
}
}

@ -6,7 +6,6 @@ import 'chrome://resources/cros_components/accordion/accordion.js';
import 'chrome://resources/cros_components/accordion/accordion_item.js';
import 'chrome://resources/cros_components/badge/badge.js';
import './cra/cra-icon.js';
import './cra/cra-icon-button.js';
import './genai-error.js';
import './genai-feedback-buttons.js';
import './genai-placeholder.js';
@ -29,9 +28,11 @@ import {usePlatformHandler} from '../core/lit/context.js';
import {
GenaiResultType,
ModelResponse,
ModelResponseError,
ModelState,
} from '../core/on_device_model/types.js';
import {ReactiveLitElement} from '../core/reactive/lit.js';
import {signal} from '../core/reactive/signal.js';
import {computed, signal} from '../core/reactive/signal.js';
import {LanguageCode} from '../core/soda/language_info.js';
import {Transcription} from '../core/soda/soda.js';
import {settings, SummaryEnableState} from '../core/state/settings.js';
@ -40,6 +41,7 @@ import {
assert,
assertExhaustive,
assertExists,
assertNotReached,
} from '../core/utils/assert.js';
export class SummarizationView extends ReactiveLitElement {
@ -186,6 +188,9 @@ export class SummarizationView extends ReactiveLitElement {
private readonly downloadRequested = signal(false);
private readonly modelState =
computed(() => this.platformHandler.summaryModelLoader.state.value);
get summaryContainerForTest(): HTMLDivElement {
return assertExists(this.summaryContainer.value);
}
@ -200,9 +205,8 @@ export class SummarizationView extends ReactiveLitElement {
}
override updated(): void {
const summaryState = this.platformHandler.summaryModelLoader.state;
if (settings.value.summaryEnabled === SummaryEnableState.ENABLED &&
summaryState.value.kind === 'installing') {
this.modelState.value.kind === 'installing') {
this.downloadRequested.value = true;
}
}
@ -267,7 +271,7 @@ export class SummarizationView extends ReactiveLitElement {
});
}
private renderSummaryContent() {
private renderSummary() {
const summary = this.summary.value;
if (summary === null) {
return html`
@ -302,7 +306,8 @@ export class SummarizationView extends ReactiveLitElement {
}
private onSummaryExpanded() {
if (!this.summaryRequested.value) {
if (this.modelState.value.kind === 'installed' &&
!this.summaryRequested.value) {
// TODO(pihsun): Better handling for promise.
void this.requestSummary();
} else {
@ -314,17 +319,74 @@ export class SummarizationView extends ReactiveLitElement {
this.summaryOpened.value = false;
}
private renderSummary() {
// TODO: b/336963138 - Implement error state.
const downloadStatus = html`<spoken-message
role="status"
aria-live="polite"
>
${i18n.summaryDownloadFinishedStatusMessage}
</spoken-message>`;
private onDownloadClicked() {
// TODO: b/399016315 - Have a wrapper to download models together.
this.platformHandler.summaryModelLoader.download();
this.platformHandler.titleSuggestionModelLoader.download();
}
private renderDownloadStatus(state: ModelState) {
switch (state.kind) {
case 'installing':
return html`<spoken-message role="status" aria-live="polite">
${i18n.summaryDownloadStartedStatusMessage}
</spoken-message>`;
case 'error':
return html`<spoken-message role="status" aria-live="polite">
${i18n.summaryDownloadErrorStatusMessage}
</spoken-message>`;
case 'installed':
if (!this.downloadRequested.value) {
return nothing;
}
return html`<spoken-message role="status" aria-live="polite">
${i18n.summaryDownloadFinishedStatusMessage}
</spoken-message>`;
case 'notInstalled':
case 'unavailable':
return assertNotReached();
default:
return assertExhaustive(state);
}
}
private renderAccordionContent(state: ModelState) {
switch (state.kind) {
case 'installing':
return nothing;
case 'error':
return html`
<genai-error
.error=${ModelResponseError.LOAD_FAILURE}
@download-clicked=${this.onDownloadClicked}
></genai-error>
`;
case 'installed':
return this.renderSummary();
case 'notInstalled':
case 'unavailable':
return assertNotReached();
default:
return assertExhaustive(state);
}
}
private renderSummaryRow(state: ModelState) {
// TODO: b/384418702 - Have different tooltip for error state.
const tooltipLabel = this.summaryOpened.value ?
i18n.summaryCollapseTooltip :
i18n.summaryExpandTooltip;
let progress: RenderResult = nothing;
// TODO: b/384418702 - Render downloading spinner.
if (state.kind === 'installing') {
progress = html`
<span class="progress">
${i18n.summaryDownloadingProgressDescription(state.progress)}
</span>
`;
}
// The accordion will automatically expand when model fails to install, and
// collapse when switch to other model states.
return html`
<cros-accordion variant="compact">
<cros-accordion-item
@ -332,47 +394,26 @@ export class SummarizationView extends ReactiveLitElement {
@cros-accordion-item-collapsed=${this.onSummaryCollapsed}
show-button-tooltip
button-tooltip-label=${tooltipLabel}
?disabled=${state.kind === 'installing'}
?expanded=${state.kind === 'error'}
>
<cra-icon name="summarize_auto" slot="leading"></cra-icon>
<div slot="title">
<span>${i18n.summaryHeader}</span>
<cros-badge>${i18n.genAiExperimentBadge}</cros-badge>
${progress}
</div>
<div id="main">${this.renderSummaryContent()}</div>
<div id="main">${this.renderAccordionContent(state)}</div>
</cros-accordion-item>
</cros-accordion>
${this.downloadRequested.value ? downloadStatus : nothing}
`;
}
private renderSummaryInstalling(progress: number) {
return html`
<cros-accordion
variant="compact"
aria-label=${i18n.summaryDownloadStartedStatusMessage}
aria-live="polite"
role="status"
>
<cros-accordion-item disabled>
<cra-icon name="summarize_auto" slot="leading"></cra-icon>
<div slot="title">
<span>${i18n.summaryHeader}</span>
<cros-badge>${i18n.genAiExperimentBadge}</cros-badge>
<span class="progress">
${i18n.summaryDownloadingProgressDescription(progress)}
</span>
</div>
</cros-accordion-item>
</cros-accordion>
`;
${this.renderDownloadStatus(state)}`;
}
override render(): RenderResult {
const summaryModelState = this.platformHandler.summaryModelLoader.state;
const summaryModelState = this.modelState.value;
const summaryEnabled = settings.value.summaryEnabled;
if (summaryModelState.value.kind === 'unavailable') {
if (summaryModelState.kind === 'unavailable') {
this.classList.add('empty');
return nothing;
}
@ -384,20 +425,15 @@ export class SummarizationView extends ReactiveLitElement {
case SummaryEnableState.UNKNOWN:
return html`<summary-consent-card></summary-consent-card>`;
case SummaryEnableState.ENABLED:
switch (summaryModelState.value.kind) {
case 'error':
// TODO(pihsun): Handle error
return nothing;
switch (summaryModelState.kind) {
case 'installing':
return this.renderSummaryInstalling(
summaryModelState.value.progress,
);
case 'error':
case 'installed':
return this.renderSummary();
return this.renderSummaryRow(summaryModelState);
case 'notInstalled':
return html`<summary-consent-card></summary-consent-card>`;
default:
assertExhaustive(summaryModelState.value.kind);
assertExhaustive(summaryModelState.kind);
}
// eslint doesn't detect that the above case never reaches here, but tsc
// prevents us from adding "break;" here since it's unreachable code.

@ -33,6 +33,9 @@ const noArgStringNames = [
'exportDialogTranscriptionHeader',
'genAiDisclaimerText',
'genAiErrorGeneralLabel',
'genAiErrorModelDownloadButton',
'genAiErrorModelDownloadButtonAriaLabel',
'genAiErrorModelLoadFailureLabel',
'genAiErrorSummaryLanguageUnsupportedLabel',
'genAiErrorSummaryTranscriptTooLongLabel',
'genAiErrorSummaryTranscriptTooShortLabel',

@ -30,6 +30,10 @@ export enum ModelResponseError {
// General error.
GENERAL = 'GENERAL',
// Model fails to load.
// TODO: b/366335321 - Add NeedsReboot error.
LOAD_FAILURE = 'LOAD_FAILURE',
// The transcription language is not supported.
UNSUPPORTED_LANGUAGE = 'UNSUPPORTED_LANGUAGE',

@ -227,6 +227,9 @@ function convertToModelResultStatus(
switch (responseError) {
case ModelResponseError.GENERAL:
case ModelResponseError.LOAD_FAILURE:
// Currently there's no plan to have specific error type for loading
// error.
return CrOSEvents_RecorderAppModelResultStatus.GENERAL_ERROR;
case ModelResponseError.UNSAFE:
return CrOSEvents_RecorderAppModelResultStatus.UNSAFE;

@ -50,6 +50,15 @@
<message desc="Error message for general error in generative AI features." name="IDS_RECORDER_GEN_AI_ERROR_GENERAL_LABEL">
Something went wrong
</message>
<message desc="Button to download summary and name creation model again in the generative AI features." name="IDS_RECORDER_GEN_AI_ERROR_MODEL_DOWNLOAD_BUTTON">
Download
</message>
<message desc="Accessibility label of the button to download summary and name creation model again in the generative AI features." name="IDS_RECORDER_GEN_AI_ERROR_MODEL_DOWNLOAD_BUTTON_ARIA_LABEL">
Download summary and name creation model
</message>
<message desc="Error message for model load error in generative AI features." name="IDS_RECORDER_GEN_AI_ERROR_MODEL_LOAD_FAILURE_LABEL">
Can't download. Try again or restart your Chromebook.
</message>
<message desc="Error message for unsupported transcript language input for summary." name="IDS_RECORDER_GEN_AI_ERROR_SUMMARY_LANGUAGE_UNSUPPORTED_LABEL">
Cant create summary for this transcript language
</message>

@ -0,0 +1 @@
f450ec678d92982eb2e8c36ced689b6c16b09b18

@ -0,0 +1 @@
087015a588a4834e934293875ca82288ebfe546c

@ -0,0 +1 @@
f450ec678d92982eb2e8c36ced689b6c16b09b18