Skip to content

Commit b787b83

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
AI: build out Insight AI integration
This CL adds the basic (placeholder) UX to each Insight and hooks up the actions to open the AI panel with the right context. Bug: 394552594 Change-Id: I0551312352d18dade636d320c83d3bd5e5a01275 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6264643 Auto-Submit: Jack Franklin <[email protected]> Commit-Queue: Alex Rudenko <[email protected]> Commit-Queue: Jack Franklin <[email protected]> Reviewed-by: Alex Rudenko <[email protected]>
1 parent ac694ce commit b787b83

File tree

12 files changed

+264
-21
lines changed

12 files changed

+264
-21
lines changed

front_end/core/host/UserMetrics.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,8 @@ export enum Action {
535535
AiAssistanceSideEffectConfirmed = 179,
536536
AiAssistanceSideEffectRejected = 180,
537537
AiAssistanceError = 181,
538-
MAX_VALUE = 182,
538+
AiAssistanceOpenedFromPerformanceInsight = 182,
539+
MAX_VALUE = 183,
539540
/* eslint-enable @typescript-eslint/naming-convention */
540541
}
541542

front_end/panels/ai_assistance/AiAssistancePanel.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,14 @@ describeWithMockConnection('AI Assistance Panel', () => {
227227
},
228228
action: 'drjones.performance-panel-context'
229229
},
230+
{
231+
flavor: TimelineUtils.InsightAIContext.ActiveInsight,
232+
createContext: () => {
233+
return new AiAssistance.InsightContext(
234+
sinon.createStubInstance(TimelineUtils.InsightAIContext.ActiveInsight));
235+
},
236+
action: 'drjones.performance-insight-context'
237+
},
230238
{
231239
flavor: Workspace.UISourceCode.UISourceCode,
232240
createContext: () => {
@@ -932,6 +940,25 @@ describeWithMockConnection('AI Assistance Panel', () => {
932940
});
933941
});
934942

943+
describe('Performance Insight agent', () => {
944+
it('should select the PERFORMANCE_INSIGHT agent when the performance panel is open and insights are enabled',
945+
async () => {
946+
updateHostConfig({
947+
devToolsAiAssistancePerformanceAgent: {
948+
enabled: true,
949+
insightsEnabled: true,
950+
},
951+
});
952+
UI.Context.Context.instance().setFlavor(
953+
TimelinePanel.TimelinePanel.TimelinePanel,
954+
sinon.createStubInstance(TimelinePanel.TimelinePanel.TimelinePanel));
955+
const {view} = await createAiAssistancePanel();
956+
sinon.assert.calledWith(view, sinon.match({
957+
agentType: AiAssistance.AgentType.PERFORMANCE_INSIGHT,
958+
}));
959+
});
960+
});
961+
935962
describe('Performance panel', () => {
936963
it('should select DRJONES_PERFORMANCE agent when the Performance panel is open in initial render', async () => {
937964
updateHostConfig({

front_end/panels/ai_assistance/AiAssistancePanel.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ function createCallTreeContext(callTree: TimelineUtils.AICallTree.AICallTree|nul
177177
}
178178
return new CallTreeContext(callTree);
179179
}
180-
function createPerfInsightContext(insight: TimelineUtils.InsightAIContext.InsightAIContext|null): InsightContext|null {
180+
function createPerfInsightContext(insight: TimelineUtils.InsightAIContext.ActiveInsight|null): InsightContext|null {
181181
if (!insight) {
182182
return null;
183183
}
@@ -229,7 +229,7 @@ export class AiAssistancePanel extends UI.Panel.Panel {
229229
#selectedFile: FileContext|null = null;
230230
#selectedElement: NodeContext|null = null;
231231
#selectedCallTree: CallTreeContext|null = null;
232-
#selectedInsight: InsightContext|null = null;
232+
#selectedPerformanceInsight: InsightContext|null = null;
233233
#selectedRequest: RequestContext|null = null;
234234

235235
// Messages displayed in the `ChatView` component.
@@ -486,8 +486,8 @@ export class AiAssistancePanel extends UI.Panel.Panel {
486486
createRequestContext(UI.Context.Context.instance().flavor(SDK.NetworkRequest.NetworkRequest));
487487
this.#selectedCallTree =
488488
createCallTreeContext(UI.Context.Context.instance().flavor(TimelineUtils.AICallTree.AICallTree));
489-
this.#selectedInsight =
490-
createPerfInsightContext(UI.Context.Context.instance().flavor(TimelineUtils.InsightAIContext.InsightAIContext));
489+
this.#selectedPerformanceInsight =
490+
createPerfInsightContext(UI.Context.Context.instance().flavor(TimelineUtils.InsightAIContext.ActiveInsight));
491491
this.#selectedFile = createFileContext(UI.Context.Context.instance().flavor(Workspace.UISourceCode.UISourceCode));
492492
this.#selectedContext = this.#getConversationContext();
493493
void this.doUpdate();
@@ -496,13 +496,17 @@ export class AiAssistancePanel extends UI.Panel.Panel {
496496
Host.AidaClient.HostConfigTracker.instance().addEventListener(
497497
Host.AidaClient.Events.AIDA_AVAILABILITY_CHANGED, this.#handleAidaAvailabilityChange);
498498
this.#toggleSearchElementAction.addEventListener(UI.ActionRegistration.Events.TOGGLED, this.doUpdate, this);
499+
499500
UI.Context.Context.instance().addFlavorChangeListener(SDK.DOMModel.DOMNode, this.#handleDOMNodeFlavorChange);
500501
UI.Context.Context.instance().addFlavorChangeListener(
501502
SDK.NetworkRequest.NetworkRequest, this.#handleNetworkRequestFlavorChange);
502503
UI.Context.Context.instance().addFlavorChangeListener(
503504
TimelineUtils.AICallTree.AICallTree, this.#handleTraceEntryNodeFlavorChange);
504505
UI.Context.Context.instance().addFlavorChangeListener(
505506
Workspace.UISourceCode.UISourceCode, this.#handleUISourceCodeFlavorChange);
507+
UI.Context.Context.instance().addFlavorChangeListener(
508+
TimelineUtils.InsightAIContext.ActiveInsight, this.#handlePerfInsightFlavorChange);
509+
506510
UI.Context.Context.instance().addFlavorChangeListener(
507511
ElementsPanel.ElementsPanel.ElementsPanel, this.#selectDefaultAgentIfNeeded, this);
508512
UI.Context.Context.instance().addFlavorChangeListener(
@@ -528,6 +532,8 @@ export class AiAssistancePanel extends UI.Panel.Panel {
528532
SDK.NetworkRequest.NetworkRequest, this.#handleNetworkRequestFlavorChange);
529533
UI.Context.Context.instance().removeFlavorChangeListener(
530534
TimelineUtils.AICallTree.AICallTree, this.#handleTraceEntryNodeFlavorChange);
535+
UI.Context.Context.instance().removeFlavorChangeListener(
536+
TimelineUtils.InsightAIContext.ActiveInsight, this.#handlePerfInsightFlavorChange);
531537
UI.Context.Context.instance().removeFlavorChangeListener(
532538
Workspace.UISourceCode.UISourceCode, this.#handleUISourceCodeFlavorChange);
533539
UI.Context.Context.instance().removeFlavorChangeListener(
@@ -604,6 +610,16 @@ export class AiAssistancePanel extends UI.Panel.Panel {
604610
this.#updateAgentState(this.#currentAgent);
605611
};
606612

613+
#handlePerfInsightFlavorChange =
614+
(ev: Common.EventTarget.EventTargetEvent<TimelineUtils.InsightAIContext.ActiveInsight>): void => {
615+
if (this.#selectedPerformanceInsight?.getItem() === ev.data) {
616+
return;
617+
}
618+
619+
this.#selectedPerformanceInsight = Boolean(ev.data) ? new InsightContext(ev.data) : null;
620+
this.#updateAgentState(this.#currentAgent);
621+
};
622+
607623
#handleUISourceCodeFlavorChange =
608624
(ev: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void => {
609625
const newFile = ev.data;
@@ -738,6 +754,11 @@ export class AiAssistancePanel extends UI.Panel.Panel {
738754
targetAgentType = AgentType.PERFORMANCE;
739755
break;
740756
}
757+
case 'drjones.performance-insight-context': {
758+
Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceOpenedFromPerformanceInsight);
759+
targetAgentType = AgentType.PERFORMANCE_INSIGHT;
760+
break;
761+
}
741762
case 'drjones.sources-floating-button': {
742763
Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceOpenedFromSourcesPanelFloatingButton);
743764
targetAgentType = AgentType.FILE;
@@ -913,7 +934,7 @@ export class AiAssistancePanel extends UI.Panel.Panel {
913934
context = this.#selectedCallTree;
914935
break;
915936
case AgentType.PERFORMANCE_INSIGHT:
916-
context = this.#selectedInsight;
937+
context = this.#selectedPerformanceInsight;
917938
break;
918939
case AgentType.PATCH:
919940
throw new Error('AI Assistance does not support direct usage of the patch agent');
@@ -1139,6 +1160,7 @@ export class ActionDelegate implements UI.ActionRegistration.ActionDelegate {
11391160
case 'drjones.network-floating-button':
11401161
case 'drjones.network-panel-context':
11411162
case 'drjones.performance-panel-context':
1163+
case 'drjones.performance-insight-context':
11421164
case 'drjones.sources-floating-button':
11431165
case 'drjones.sources-panel-context': {
11441166
void (async () => {

front_end/panels/ai_assistance/agents/PerformanceInsightsAgent.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ You have a number of functions to get information about the page and its perform
4242
`;
4343
/* clang-format on */
4444

45-
export class InsightContext extends ConversationContext<TimelineUtils.InsightAIContext.InsightAIContext> {
46-
readonly #insight: TimelineUtils.InsightAIContext.InsightAIContext;
45+
export class InsightContext extends ConversationContext<TimelineUtils.InsightAIContext.ActiveInsight> {
46+
readonly #insight: TimelineUtils.InsightAIContext.ActiveInsight;
4747

48-
constructor(insight: TimelineUtils.InsightAIContext.InsightAIContext) {
48+
constructor(insight: TimelineUtils.InsightAIContext.ActiveInsight) {
4949
super();
5050
this.#insight = insight;
5151
}
@@ -56,7 +56,7 @@ export class InsightContext extends ConversationContext<TimelineUtils.InsightAIC
5656
return '';
5757
}
5858

59-
getItem(): TimelineUtils.InsightAIContext.InsightAIContext {
59+
getItem(): TimelineUtils.InsightAIContext.ActiveInsight {
6060
return this.#insight;
6161
}
6262

@@ -71,19 +71,17 @@ export class InsightContext extends ConversationContext<TimelineUtils.InsightAIC
7171
}
7272

7373
override getTitle(): string|ReturnType<typeof Lit.Directives.until> {
74-
// TODO: calculate the actual Insight's title and put it in the context.
75-
return 'Insight';
74+
return this.#insight.title();
7675
}
7776
}
7877

79-
export class PerformanceInsightsAgent extends AiAgent<TimelineUtils.InsightAIContext.InsightAIContext> {
78+
export class PerformanceInsightsAgent extends AiAgent<TimelineUtils.InsightAIContext.ActiveInsight> {
8079
// TODO: make use of the Insight.
8180
// eslint-disable-next-line no-unused-private-class-members
82-
#insight: ConversationContext<TimelineUtils.InsightAIContext.InsightAIContext>|undefined;
81+
#insight: ConversationContext<TimelineUtils.InsightAIContext.ActiveInsight>|undefined;
8382

84-
override handleContextDetails(
85-
_activeContext: ConversationContext<TimelineUtils.InsightAIContext.InsightAIContext>|
86-
null): AsyncGenerator<ContextResponse, void, void> {
83+
override handleContextDetails(_activeContext: ConversationContext<TimelineUtils.InsightAIContext.ActiveInsight>|null):
84+
AsyncGenerator<ContextResponse, void, void> {
8785
throw new Error('not implemented');
8886
}
8987

@@ -108,7 +106,7 @@ export class PerformanceInsightsAgent extends AiAgent<TimelineUtils.InsightAICon
108106
}
109107

110108
override async * run(initialQuery: string, options: {
111-
signal?: AbortSignal, selected: ConversationContext<TimelineUtils.InsightAIContext.InsightAIContext>|null,
109+
signal?: AbortSignal, selected: ConversationContext<TimelineUtils.InsightAIContext.ActiveInsight>|null,
112110
}): AsyncGenerator<ResponseData, void, void> {
113111
this.#insight = options.selected ?? undefined;
114112

front_end/panels/ai_assistance/ai_assistance-meta.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ function isNetworkAgentFeatureAvailable(config?: Root.Runtime.HostConfig): boole
8888
function isPerformanceAgentFeatureAvailable(config?: Root.Runtime.HostConfig): boolean {
8989
return (config?.aidaAvailability?.enabled && (config?.devToolsAiAssistancePerformanceAgent?.enabled)) === true;
9090
}
91+
function isPerformanceInsightsAgentFeatureAvailable(config?: Root.Runtime.HostConfig): boolean {
92+
return (config?.aidaAvailability?.enabled && config?.devToolsAiAssistancePerformanceAgent?.enabled &&
93+
config?.devToolsAiAssistancePerformanceAgent.insightsEnabled) === true;
94+
}
9195

9296
function isFileAgentFeatureAvailable(config?: Root.Runtime.HostConfig): boolean {
9397
return (config?.aidaAvailability?.enabled && (config?.devToolsAiAssistanceFileAgent?.enabled)) === true;
@@ -221,6 +225,22 @@ UI.ActionRegistration.registerActionExtension({
221225
condition: config => isPerformanceAgentFeatureAvailable(config) && !isPolicyRestricted(config),
222226
});
223227

228+
UI.ActionRegistration.registerActionExtension({
229+
actionId: 'drjones.performance-insight-context',
230+
contextTypes(): [] {
231+
return [];
232+
},
233+
category: UI.ActionRegistration.ActionCategory.GLOBAL,
234+
title: i18nLazyString(UIStrings.askAi),
235+
async loadActionDelegate() {
236+
const AiAssistance = await loadAiAssistanceModule();
237+
return new AiAssistance.ActionDelegate();
238+
},
239+
condition: config => {
240+
return isPerformanceInsightsAgentFeatureAvailable(config) && !isPolicyRestricted(config);
241+
}
242+
});
243+
224244
UI.ActionRegistration.registerActionExtension({
225245
actionId: 'drjones.sources-floating-button',
226246
contextTypes(): [] {

front_end/panels/ai_assistance/ai_assistance.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from './agents/AiAgent.js';
77
export * from './agents/FileAgent.js';
88
export * from './agents/NetworkAgent.js';
99
export * from './agents/PerformanceAgent.js';
10+
export * from './agents/PerformanceInsightsAgent.js';
1011
export * from './agents/StylingAgent.js';
1112
export * from './agents/PatchAgent.js';
1213
export * from './AiAssistancePanel.js';

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

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
import type * as Common from '../../../../core/common/common.js';
66
import * as Trace from '../../../../models/trace/trace.js';
7-
import {renderElementIntoDOM} from '../../../../testing/DOMHelpers.js';
8-
import {describeWithEnvironment} from '../../../../testing/EnvironmentHelpers.js';
7+
import {dispatchClickEvent, renderElementIntoDOM} from '../../../../testing/DOMHelpers.js';
8+
import {describeWithEnvironment, updateHostConfig} from '../../../../testing/EnvironmentHelpers.js';
99
import * as RenderCoordinator from '../../../../ui/components/render_coordinator/render_coordinator.js';
10+
import * as UI from '../../../../ui/legacy/legacy.js';
1011
import * as Lit from '../../../../ui/lit/lit.js';
1112
import type {TimelineOverlay} from '../../overlays/OverlaysImpl.js';
13+
import * as Utils from '../../utils/utils.js';
1214

1315
import * as Insights from './insights.js';
1416

@@ -81,4 +83,100 @@ describeWithEnvironment('BaseInsightComponent', () => {
8183
assert.strictEqual(contentElement.textContent, 'test content');
8284
});
8385
});
86+
87+
describe('Ask AI Insights', () => {
88+
const FAKE_PARSED_TRACE = {} as unknown as Trace.Handlers.Types.ParsedTrace;
89+
const FAKE_LCP_MODEL = {
90+
strings: {},
91+
title: 'LCP by Phase' as Common.UIString.LocalizedString,
92+
description: 'some description' as Common.UIString.LocalizedString,
93+
category: Trace.Insights.Types.InsightCategory.ALL,
94+
state: 'fail',
95+
} as const;
96+
async function renderComponent(): Promise<TestInsightComponent> {
97+
const component = new TestInsightComponent();
98+
component.selected = true;
99+
component.model = FAKE_LCP_MODEL;
100+
// We don't need a real trace for these tests.
101+
component.parsedTrace = FAKE_PARSED_TRACE;
102+
renderElementIntoDOM(component);
103+
104+
await RenderCoordinator.done();
105+
return component;
106+
}
107+
108+
it('renders the "Ask AI" button when perf insights AI is enabled', async () => {
109+
updateHostConfig({
110+
devToolsAiAssistancePerformanceAgent: {
111+
enabled: true,
112+
insightsEnabled: true,
113+
}
114+
});
115+
const component = await renderComponent();
116+
assert.isOk(component.shadowRoot);
117+
const button = component.shadowRoot.querySelector('devtools-button[data-ask-ai]');
118+
assert.isOk(button);
119+
});
120+
121+
it('sets the context when the user clicks the button', async () => {
122+
updateHostConfig({
123+
devToolsAiAssistancePerformanceAgent: {
124+
enabled: true,
125+
insightsEnabled: true,
126+
}
127+
});
128+
const component = await renderComponent();
129+
assert.isOk(component.shadowRoot);
130+
const button = component.shadowRoot.querySelector('devtools-button[data-ask-ai]');
131+
assert.isOk(button);
132+
sinon.stub(UI.ActionRegistry.ActionRegistry.instance(), 'hasAction')
133+
.withArgs(sinon.match(/drjones\.performance-insight-context/))
134+
.returns(true);
135+
136+
const FAKE_ACTION = sinon.createStubInstance(UI.ActionRegistration.Action);
137+
sinon.stub(UI.ActionRegistry.ActionRegistry.instance(), 'getAction')
138+
.withArgs(sinon.match(/drjones\.performance-insight-context/))
139+
.returns(FAKE_ACTION);
140+
141+
dispatchClickEvent(button);
142+
const context = UI.Context.Context.instance().flavor(Utils.InsightAIContext.ActiveInsight);
143+
assert.instanceOf(context, Utils.InsightAIContext.ActiveInsight);
144+
});
145+
146+
it('clears the active context when it gets toggled shut', async () => {
147+
const FAKE_ACTIVE_INSIGHT = {} as unknown as Utils.InsightAIContext.ActiveInsight;
148+
UI.Context.Context.instance().setFlavor(Utils.InsightAIContext.ActiveInsight, FAKE_ACTIVE_INSIGHT);
149+
const component = await renderComponent();
150+
const header = component.shadowRoot?.querySelector('header');
151+
assert.isOk(header);
152+
dispatchClickEvent(header);
153+
const context = UI.Context.Context.instance().flavor(Utils.InsightAIContext.ActiveInsight);
154+
assert.isNull(context);
155+
});
156+
157+
it('does not render the "Ask AI" button when the perf agent is not enabled', async () => {
158+
updateHostConfig({
159+
devToolsAiAssistancePerformanceAgent: {
160+
enabled: false,
161+
}
162+
});
163+
const component = await renderComponent();
164+
assert.isOk(component.shadowRoot);
165+
const button = component.shadowRoot.querySelector('devtools-button[data-ask-ai]');
166+
assert.isNull(button);
167+
});
168+
169+
it('does not render the "Ask AI" button when the perf agent is enabled but the insights ai is not', async () => {
170+
updateHostConfig({
171+
devToolsAiAssistancePerformanceAgent: {
172+
enabled: true,
173+
insightsEnabled: false,
174+
}
175+
});
176+
const component = await renderComponent();
177+
assert.isOk(component.shadowRoot);
178+
const button = component.shadowRoot.querySelector('devtools-button[data-ask-ai]');
179+
assert.isNull(button);
180+
});
181+
});
84182
});

0 commit comments

Comments
 (0)