Skip to content

Commit 6b1d718

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
RPP: let an Insight conditionally hide/show the AI button
Insights already can determine if they do or don't support the "Ask AI" flow, but for some insights we want to make this conditional based on the actual trace data. For example, this CL fixes a case where the INP insight can be used for "Ask AI" but if the trace has no interaction, this is nonsensical and does not lead to good results. Fixed: 409008968 Change-Id: I258b80104b2261ed92b2eda4607f75525d7d0a5b Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6435294 Auto-Submit: Jack Franklin <[email protected]> Commit-Queue: Jack Franklin <[email protected]> Reviewed-by: Andres Olivares <[email protected]> Commit-Queue: Andres Olivares <[email protected]>
1 parent 7d06105 commit 6b1d718

File tree

9 files changed

+109
-14
lines changed

9 files changed

+109
-14
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ ts_library("unittests") {
8383
sources = [
8484
"BaseInsightComponent.test.ts",
8585
"CLSCulprits.test.ts",
86+
"InteractionToNextPaint.test.ts",
8687
"Table.test.ts",
8788
]
8889

front_end/panels/timeline/components/insights/BaseInsightComponent.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ describeWithEnvironment('BaseInsightComponent', () => {
2121
const {BaseInsightComponent} = Insights.BaseInsightComponent;
2222
class TestInsightComponentNoAISupport extends BaseInsightComponent<Trace.Insights.Types.InsightModel> {
2323
override internalName = 'test-insight';
24+
25+
override hasAskAiSupport() {
26+
return false;
27+
}
28+
2429
override createOverlays(): TimelineOverlay[] {
2530
return [];
2631
}
@@ -30,7 +35,9 @@ describeWithEnvironment('BaseInsightComponent', () => {
3035
}
3136
class TestInsightComponentWithAISupport extends BaseInsightComponent<Trace.Insights.Types.InsightModel> {
3237
override internalName = 'test-insight';
33-
protected override hasAskAISupport = true;
38+
override hasAskAiSupport() {
39+
return true;
40+
}
3441
override createOverlays(): TimelineOverlay[] {
3542
return [];
3643
}

front_end/panels/timeline/components/insights/BaseInsightComponent.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,8 @@ export abstract class BaseInsightComponent<T extends InsightModel> extends HTMLE
6464
// So we can use the TypeScript BaseInsight class without getting warnings
6565
// about litTagName. Every child should overwrite this.
6666
static readonly litTagName = Lit.StaticHtml.literal``;
67-
6867
protected readonly shadow = this.attachShadow({mode: 'open'});
6968

70-
// Flipped to true for Insights that have support for the "Ask AI" Insights
71-
// experience. The "Ask AI" button will only be shown for an Insight if this
72-
// is true and if the feature has been enabled by the user and they meet the
73-
// requirements to use AI.
74-
protected readonly hasAskAISupport: boolean = false;
7569
// This flag tracks if the Insights AI feature is enabled within Chrome for
7670
// the active user.
7771
#insightsAskAiEnabled = false;
@@ -84,7 +78,6 @@ export abstract class BaseInsightComponent<T extends InsightModel> extends HTMLE
8478
get model(): T|null {
8579
return this.#model;
8680
}
87-
8881
protected data: BaseInsightData = {
8982
bounds: null,
9083
insightSetKey: null,
@@ -101,6 +94,14 @@ export abstract class BaseInsightComponent<T extends InsightModel> extends HTMLE
10194
void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#boundRender);
10295
}
10396

97+
// Insights that do support the AI feature can override this to return true.
98+
// The "Ask AI" button will only be shown for an Insight if this
99+
// is true and if the feature has been enabled by the user and they meet the
100+
// requirements to use AI.
101+
protected hasAskAiSupport(): boolean {
102+
return false;
103+
}
104+
104105
connectedCallback(): void {
105106
this.shadow.adoptedStyleSheets.push(baseInsightComponentStyles);
106107
this.setAttribute('jslog', `${VisualLogging.section(`timeline.insights.${this.internalName}`)}`);
@@ -327,7 +328,7 @@ export abstract class BaseInsightComponent<T extends InsightModel> extends HTMLE
327328
const aiDisabledByEnterprisePolicy = Root.Runtime.hostConfig.aidaAvailability?.enterprisePolicyValue ===
328329
Root.Runtime.GenAiEnterprisePolicyValue.DISABLE;
329330

330-
return !aiDisabledByEnterprisePolicy && this.#insightsAskAiEnabled && this.hasAskAISupport;
331+
return !aiDisabledByEnterprisePolicy && this.#insightsAskAiEnabled && this.hasAskAiSupport();
331332
}
332333

333334
#renderInsightContent(insightModel: T): Lit.LitTemplate {

front_end/panels/timeline/components/insights/DocumentLatency.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ const {html} = Lit;
1818
export class DocumentLatency extends BaseInsightComponent<DocumentLatencyInsightModel> {
1919
static override readonly litTagName = Lit.StaticHtml.literal`devtools-performance-document-latency`;
2020
override internalName = 'document-latency';
21-
protected override hasAskAISupport = true;
21+
22+
protected override hasAskAiSupport(): boolean {
23+
return true;
24+
}
2225

2326
override createOverlays(): Overlays.Overlays.TimelineOverlay[] {
2427
if (!this.model?.data?.documentRequest) {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import {
6+
renderElementIntoDOM,
7+
} from '../../../../testing/DOMHelpers.js';
8+
import {describeWithEnvironment, updateHostConfig} from '../../../../testing/EnvironmentHelpers.js';
9+
import {getInsightOrError} from '../../../../testing/InsightHelpers.js';
10+
import {TraceLoader} from '../../../../testing/TraceLoader.js';
11+
import * as RenderCoordinator from '../../../../ui/components/render_coordinator/render_coordinator.js';
12+
13+
import * as Insights from './insights.js';
14+
15+
describeWithEnvironment('Interaction to next paint component', () => {
16+
beforeEach(() => {
17+
// Ensure the environment is setup for AI being supported so we can
18+
// test the presence of the button being conditional on the insight
19+
// having a longest interaction event.
20+
updateHostConfig({
21+
devToolsAiAssistancePerformanceAgent: {
22+
enabled: true,
23+
insightsEnabled: true,
24+
}
25+
});
26+
});
27+
28+
it('enables "Ask AI" if the page has an interaction', async function() {
29+
const {parsedTrace, insights} = await TraceLoader.traceEngine(this, 'one-second-interaction.json.gz');
30+
assert.isOk(insights);
31+
const firstInsightSet = insights.values().next()?.value;
32+
assert.isOk(firstInsightSet);
33+
const [firstNav] = parsedTrace.Meta.mainFrameNavigations;
34+
const interactionInsight = getInsightOrError('InteractionToNextPaint', insights, firstNav);
35+
assert.isDefined(interactionInsight.longestInteractionEvent);
36+
37+
const component = new Insights.InteractionToNextPaint.InteractionToNextPaint();
38+
component.model = interactionInsight;
39+
component.insightSetKey = firstInsightSet.id;
40+
component.bounds = parsedTrace.Meta.traceBounds;
41+
component.selected = true;
42+
renderElementIntoDOM(component);
43+
await RenderCoordinator.done();
44+
assert.isOk(component.shadowRoot);
45+
46+
const button = component.shadowRoot.querySelector('devtools-button[data-insights-ask-ai]');
47+
assert.instanceOf(button, HTMLElement);
48+
});
49+
50+
it('disables "Ask AI" if the page has no interaction', async function() {
51+
const {parsedTrace, insights} = await TraceLoader.traceEngine(this, 'unsized-images.json.gz');
52+
assert.isOk(insights);
53+
const firstInsightSet = insights.values().next()?.value;
54+
assert.isOk(firstInsightSet);
55+
const [firstNav] = parsedTrace.Meta.mainFrameNavigations;
56+
const interactionInsight = getInsightOrError('InteractionToNextPaint', insights, firstNav);
57+
assert.isUndefined(interactionInsight.longestInteractionEvent);
58+
59+
const component = new Insights.InteractionToNextPaint.InteractionToNextPaint();
60+
component.model = interactionInsight;
61+
component.insightSetKey = firstInsightSet.id;
62+
component.bounds = parsedTrace.Meta.traceBounds;
63+
component.selected = true;
64+
renderElementIntoDOM(component);
65+
await RenderCoordinator.done();
66+
assert.isOk(component.shadowRoot);
67+
68+
const button = component.shadowRoot.querySelector('devtools-button[data-insights-ask-ai]');
69+
assert.isNull(button);
70+
});
71+
});

front_end/panels/timeline/components/insights/InteractionToNextPaint.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ const {html} = Lit;
2020
export class InteractionToNextPaint extends BaseInsightComponent<INPInsightModel> {
2121
static override readonly litTagName = Lit.StaticHtml.literal`devtools-performance-inp`;
2222
override internalName = 'inp';
23-
protected override hasAskAISupport = true;
23+
24+
protected override hasAskAiSupport(): boolean {
25+
return this.model?.longestInteractionEvent !== undefined;
26+
}
2427

2528
override createOverlays(): Overlays.Overlays.TimelineOverlay[] {
2629
if (!this.model) {

front_end/panels/timeline/components/insights/LCPDiscovery.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ function getImageData(model: LCPDiscoveryInsightModel): LCPImageDiscoveryData|nu
6363
export class LCPDiscovery extends BaseInsightComponent<LCPDiscoveryInsightModel> {
6464
static override readonly litTagName = Lit.StaticHtml.literal`devtools-performance-lcp-discovery`;
6565
override internalName = 'lcp-discovery';
66-
protected override hasAskAISupport = true;
66+
67+
protected override hasAskAiSupport(): boolean {
68+
return true;
69+
}
6770

6871
#renderDiscoveryDelay(delay: Trace.Types.Timing.Micro): Element {
6972
const timeWrapper = document.createElement('span');

front_end/panels/timeline/components/insights/LCPPhases.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@ interface PhaseData {
2525
export class LCPPhases extends BaseInsightComponent<LCPPhasesInsightModel> {
2626
static override readonly litTagName = Lit.StaticHtml.literal`devtools-performance-lcp-by-phases`;
2727
override internalName = 'lcp-by-phase';
28-
protected override hasAskAISupport = true;
2928
#overlay: Overlays.Overlays.TimespanBreakdown|null = null;
3029

30+
protected override hasAskAiSupport(): boolean {
31+
return true;
32+
}
33+
3134
#getPhaseData(): PhaseData[] {
3235
if (!this.model) {
3336
return [];

front_end/panels/timeline/components/insights/RenderBlocking.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,12 @@ export class RenderBlocking extends BaseInsightComponent<RenderBlockingInsightMo
3636
};
3737
}
3838

39-
protected override hasAskAISupport = true;
4039
override internalName = 'render-blocking-requests';
4140

41+
protected override hasAskAiSupport(): boolean {
42+
return !!this.model;
43+
}
44+
4245
override createOverlays(): Overlays.Overlays.TimelineOverlay[] {
4346
if (!this.model) {
4447
return [];

0 commit comments

Comments
 (0)