Skip to content

Commit df4d71f

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
RPP: add rich tooltip to annotation label generation
Fixed: 405061222 Change-Id: I3a7bc85e40512e3f9de244a0258377b9e3da2077 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6375726 Commit-Queue: Jack Franklin <[email protected]> Reviewed-by: Ergün Erdoğmuş <[email protected]> Auto-Submit: Jack Franklin <[email protected]>
1 parent ed9d0d5 commit df4d71f

File tree

5 files changed

+110
-51
lines changed

5 files changed

+110
-51
lines changed

front_end/panels/timeline/overlays/OverlaysImpl.test.ts

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import * as Common from '../../../core/common/common.js';
66
import * as Trace from '../../../models/trace/trace.js';
7-
import {dispatchClickEvent} from '../../../testing/DOMHelpers.js';
7+
import {cleanTextContent, dispatchClickEvent} from '../../../testing/DOMHelpers.js';
88
import {describeWithEnvironment, updateHostConfig} from '../../../testing/EnvironmentHelpers.js';
99
import {
1010
makeInstantEvent,
@@ -568,42 +568,40 @@ describeWithEnvironment('Overlays', () => {
568568
elementsWrapper.querySelector<HTMLElement>('.ai-label-button-wrapper') as HTMLSpanElement;
569569
assert.isOk(aiLabelButtonWrapper);
570570

571-
const infoIconTooltip = aiLabelButtonWrapper.querySelector('.pen-icon[title]');
572-
assert.isNotNull(infoIconTooltip);
571+
const tooltip = aiLabelButtonWrapper.querySelector<HTMLElement>('devtools-tooltip');
572+
assert.isOk(tooltip);
573573
assert.strictEqual(
574-
infoIconTooltip.getAttribute('title'),
575-
'The selected call stack is sent to Google. The content you submit and that is generated by this feature will be used to improve Google’s AI models. This is an experimental AI feature and won’t always get it right.',
574+
cleanTextContent(tooltip.innerText),
575+
'The selected call stack is sent to Google. The content you submit and that is generated by this feature will be used to improve Google’s AI models. This is an experimental AI feature and won’t always get it right. Learn more',
576576
);
577577
});
578578

579-
it('Correct security tooltip on the `generate ai label` info icon hover for the users with logging disabled',
580-
async function() {
581-
updateHostConfig({
582-
aidaAvailability: {
583-
enabled: true,
584-
blockedByAge: false,
585-
blockedByEnterprisePolicy: false,
586-
blockedByGeo: false,
587-
disallowLogging: true,
588-
enterprisePolicyValue: 1,
589-
},
590-
});
591-
592-
const {elementsWrapper, inputField} =
593-
await createAnnotationsLabelElement(this, 'web-dev.json.gz', 50, 'entry label');
594-
assert.strictEqual(inputField?.innerText, 'entry label');
579+
it('shows correct tooltip text on `generate ai label` hover for the users with logging disabled', async function() {
580+
updateHostConfig({
581+
aidaAvailability: {
582+
enabled: true,
583+
blockedByAge: false,
584+
blockedByEnterprisePolicy: false,
585+
blockedByGeo: false,
586+
disallowLogging: true,
587+
enterprisePolicyValue: 1,
588+
},
589+
});
595590

596-
const aiLabelButtonWrapper =
597-
elementsWrapper.querySelector<HTMLElement>('.ai-label-button-wrapper') as HTMLSpanElement;
598-
assert.isOk(aiLabelButtonWrapper);
591+
const {elementsWrapper, inputField} =
592+
await createAnnotationsLabelElement(this, 'web-dev.json.gz', 50, 'entry label');
593+
assert.strictEqual(inputField?.innerText, 'entry label');
599594

600-
const infoIconTooltip = aiLabelButtonWrapper.querySelector('.pen-icon[title]');
601-
assert.isNotNull(infoIconTooltip);
602-
assert.strictEqual(
603-
infoIconTooltip.getAttribute('title'),
604-
'The selected call stack is sent to Google. The content you submit and that is generated by this feature will not be used to improve Google’s AI models. This is an experimental AI feature and won’t always get it right.',
605-
);
606-
});
595+
const aiLabelButtonWrapper =
596+
elementsWrapper.querySelector<HTMLElement>('.ai-label-button-wrapper') as HTMLSpanElement;
597+
assert.isOk(aiLabelButtonWrapper);
598+
const tooltip = aiLabelButtonWrapper.querySelector<HTMLElement>('devtools-tooltip');
599+
assert.isOk(tooltip);
600+
assert.strictEqual(
601+
cleanTextContent(tooltip.innerText),
602+
'The selected call stack is sent to Google. The content you submit and that is generated by this feature will not be used to improve Google’s AI models. This is an experimental AI feature and won’t always get it right. Learn more',
603+
);
604+
});
607605

608606
it('Shows disabled `generate ai label` button if the user is not logged into their google account or is under 18',
609607
async function() {
@@ -626,11 +624,11 @@ describeWithEnvironment('Overlays', () => {
626624
elementsWrapper.querySelector<HTMLElement>('.ai-label-disabled-button-wrapper') as HTMLSpanElement;
627625
assert.isOk(aiLabelButtonWrapper);
628626

629-
const infoIconTooltip = aiLabelButtonWrapper.querySelector('.pen-icon[title]');
630-
assert.isNotNull(infoIconTooltip);
627+
const tooltip = aiLabelButtonWrapper.querySelector<HTMLElement>('devtools-tooltip');
628+
assert.isOk(tooltip);
631629
assert.strictEqual(
632-
infoIconTooltip.getAttribute('title'),
633-
'Auto anotations are not available, go to AI Innovations in Settings to learn more.',
630+
cleanTextContent(tooltip.innerText),
631+
'Auto annotations are not available. Learn more',
634632
);
635633
});
636634

@@ -654,11 +652,11 @@ describeWithEnvironment('Overlays', () => {
654652
elementsWrapper.querySelector<HTMLElement>('.ai-label-disabled-button-wrapper') as HTMLSpanElement;
655653
assert.isOk(aiLabelButtonWrapper);
656654

657-
const infoIconTooltip = aiLabelButtonWrapper.querySelector('.pen-icon[title]');
658-
assert.isNotNull(infoIconTooltip);
655+
const tooltip = aiLabelButtonWrapper.querySelector<HTMLElement>('devtools-tooltip');
656+
assert.isOk(tooltip);
659657
assert.strictEqual(
660-
infoIconTooltip.getAttribute('title'),
661-
'Auto anotations are not available, go to AI Innovations in Settings to learn more.',
658+
cleanTextContent(tooltip.innerText),
659+
'Auto annotations are not available. Learn more',
662660
);
663661
});
664662

front_end/panels/timeline/overlays/components/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ devtools_module("components") {
3030
"../../../../panels/common:bundle",
3131
"../../../../ui/components/helpers:bundle",
3232
"../../../../ui/components/icon_button:bundle",
33+
"../../../../ui/components/tooltips:bundle",
3334
"../../../../ui/lit:bundle",
3435
"../../../../ui/visual_logging:bundle",
3536
]

front_end/panels/timeline/overlays/components/EntryLabelOverlay.ts

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
// found in the LICENSE file.
44

55
import '../../../../ui/components/icon_button/icon_button.js';
6+
import '../../../../ui/components/tooltips/tooltips.js';
67

78
import * as Common from '../../../../core/common/common.js';
89
import * as i18n from '../../../../core/i18n/i18n.js';
910
import * as Platform from '../../../../core/platform/platform.js';
1011
import * as Root from '../../../../core/root/root.js';
12+
import * as Buttons from '../../../../ui/components/buttons/buttons.js';
1113
import * as ComponentHelpers from '../../../../ui/components/helpers/helpers.js';
1214
import * as UI from '../../../../ui/legacy/legacy.js';
1315
import * as ThemeSupport from '../../../../ui/legacy/theme_support/theme_support.js';
@@ -17,7 +19,7 @@ import * as PanelCommon from '../../../common/common.js';
1719

1820
import stylesRaw from './entryLabelOverlay.css.js';
1921

20-
const {html} = Lit;
22+
const {html, Directives} = Lit;
2123

2224
// TODO(crbug.com/391381439): Fully migrate off of constructed style sheets.
2325
const styles = new CSSStyleSheet();
@@ -42,6 +44,10 @@ const UIStrings = {
4244
* Strings that don't need to be translated at this time.
4345
*/
4446
const UIStringsNotTranslate = {
47+
/**
48+
*@description Tooltip link for the navigating to "AI innovations" page in settings.
49+
*/
50+
learnMore: 'Learn more',
4551
/**
4652
*@description Security disclaimer text displayed when the information icon on a button that generates an AI label is hovered.
4753
*/
@@ -55,12 +61,11 @@ const UIStringsNotTranslate = {
5561
/**
5662
*@description The `Generate AI label button` tooltip disclaimer for when the feature is not available and the reason can be checked in settings.
5763
*/
58-
autoAnnotationNotAvailableDisclaimer:
59-
'Auto anotations are not available, go to AI Innovations in Settings to learn more.',
64+
autoAnnotationNotAvailableDisclaimer: 'Auto annotations are not available.',
6065
/**
6166
*@description The `Generate AI label button` tooltip disclaimer for when the feature is not available because the user is offline.
6267
*/
63-
autoAnnotationNotAvailableOfflineDisclaimer: 'Auto anotations are not available because you are offline.',
68+
autoAnnotationNotAvailableOfflineDisclaimer: 'Auto annotations are not available because you are offline.',
6469
/**
6570
*@description Header text for the AI-powered annotations suggestions disclaimer dialog.
6671
*/
@@ -137,6 +142,7 @@ export class EntryLabelOverlay extends HTMLElement {
137142
#connectorLineContainer: SVGAElement|null = null;
138143
#label: string;
139144
#shouldDrawBelowEntry: boolean;
145+
#richTooltip: Lit.Directives.Ref<HTMLElement> = Directives.createRef();
140146
/**
141147
* The entry label overlay consists of 3 parts - the label part with the label string inside,
142148
* the line connecting the label to the entry, and a black box around an entry to highlight the entry with a label.
@@ -459,6 +465,26 @@ export class EntryLabelOverlay extends HTMLElement {
459465
!Root.Runtime.hostConfig.aidaAvailability?.blockedByGeo && !navigator.onLine === false;
460466
}
461467

468+
#renderAITooltip(opts: {textContent: string, includeSettingsButton: boolean}): Lit.TemplateResult {
469+
// clang-format off
470+
return html`<devtools-tooltip variant="rich" id="info-tooltip" ${Directives.ref(this.#richTooltip)}>
471+
<div class="info-tooltip-container">
472+
${opts.textContent}
473+
${opts.includeSettingsButton ? html`
474+
<button
475+
class="link tooltip-link"
476+
role="link"
477+
jslog=${VisualLogging.link('open-ai-settings').track({
478+
click: true,
479+
})}
480+
@click=${this.#onTooltipLearnMoreClick}
481+
>${lockedString(UIStringsNotTranslate.learnMore)}</button>
482+
` : Lit.nothing}
483+
</div>
484+
</devtools-tooltip>`;
485+
// clang-format on
486+
}
487+
462488
#renderAiButton(): Lit.TemplateResult {
463489
const noLogging = Root.Runtime.hostConfig.aidaAvailability?.enterprisePolicyValue ===
464490
Root.Runtime.GenAiEnterprisePolicyValue.ALLOW_WITHOUT_LOGGING;
@@ -480,18 +506,26 @@ export class EntryLabelOverlay extends HTMLElement {
480506
</devtools-icon>
481507
<span class="generate-label-text">${i18nString(UIStrings.generateLabelButton)}</span>
482508
</button>
483-
<devtools-icon
509+
<devtools-button
510+
aria-details="info-tooltip"
484511
class="pen-icon"
485-
title=${noLogging ? lockedString(UIStringsNotTranslate.generateLabelSecurityDisclaimerLogginOff) : lockedString(UIStringsNotTranslate.generateLabelSecurityDisclaimer)}
486-
.name=${'info'}
487-
.data=${{
488-
iconName: 'info', color: 'var(--color-background-inverted)', width: '20px'}}>
489-
</devtools-icon>
512+
.iconName=${'info'}
513+
.variant=${Buttons.Button.Variant.ICON}
514+
></devtools-button>
515+
${this.#renderAITooltip({
516+
textContent: noLogging ? lockedString(UIStringsNotTranslate.generateLabelSecurityDisclaimerLogginOff) : lockedString(UIStringsNotTranslate.generateLabelSecurityDisclaimer),
517+
includeSettingsButton: true,
518+
})}
490519
</span>
491520
`;
492521
// clang-format on
493522
}
494523

524+
#onTooltipLearnMoreClick(): void {
525+
this.#richTooltip?.value?.hidePopover();
526+
void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
527+
}
528+
495529
// The disabled button rendered when the `generate AI label` feature is not available
496530
// because of the geolocation, age or if they are not logged in into the google account.
497531
//
@@ -509,13 +543,17 @@ export class EntryLabelOverlay extends HTMLElement {
509543
?disabled=${true}
510544
@click=${this.#handleAiButtonClick}>
511545
<devtools-icon
546+
aria-details="info-tooltip"
512547
class="pen-icon"
513-
title=${noConnection ? lockedString(UIStringsNotTranslate.autoAnnotationNotAvailableOfflineDisclaimer) : lockedString(UIStringsNotTranslate.autoAnnotationNotAvailableDisclaimer)}
514548
.name=${'pen-spark'}
515549
.data=${{
516550
iconName: 'pen-spark', color: 'var(--sys-color-state-disabled)', width: '20px'}}>
517551
</devtools-icon>
518552
</button>
553+
${this.#renderAITooltip({
554+
textContent: noConnection ? lockedString(UIStringsNotTranslate.autoAnnotationNotAvailableOfflineDisclaimer) : lockedString(UIStringsNotTranslate.autoAnnotationNotAvailableDisclaimer),
555+
includeSettingsButton: !noConnection,
556+
})}
519557
</span>
520558
`;
521559
// clang-format on

front_end/panels/timeline/overlays/components/entryLabelOverlay.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,21 @@
122122
border-left: none;
123123
}
124124
}
125+
126+
/* The tooltip for the AI label generation info */
127+
.info-tooltip-container {
128+
max-width: var(--sys-size-28);
129+
130+
button.link {
131+
cursor: pointer;
132+
text-decoration: underline;
133+
border: none;
134+
padding: 0;
135+
background: none;
136+
font: inherit;
137+
font-weight: var(--ref-typeface-weight-medium);
138+
display: block;
139+
margin-top: var(--sys-size-4);
140+
color: var(--sys-color-primary);
141+
}
142+
}

front_end/testing/DOMHelpers.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,11 @@ export function getCleanTextContentFromElements(el: ShadowRoot|HTMLElement, sele
290290
export function getCleanTextContentFromSingleElement(el: ShadowRoot|HTMLElement, selector: string): string {
291291
const element = el.querySelector(selector);
292292
assert.isOk(element, `Could not find element with selector ${selector}`);
293-
return element.textContent ? element.textContent.trim().replace(/[ \n]{2,}/g, ' ') : '';
293+
return element.textContent ? cleanTextContent(element.textContent) : '';
294+
}
295+
296+
export function cleanTextContent(input: string): string {
297+
return input.trim().replace(/[ \n]{2,}/g, ' ');
294298
}
295299

296300
export function assertNodeTextContent(component: NodeText.NodeText.NodeText, expectedContent: string) {

0 commit comments

Comments
 (0)