Skip to content

Commit 7d8e5d3

Browse files
Samiya CaurDevtools-frontend LUCI CQ
authored andcommitted
Add spinner to AI code completion summary toolbar
Updated spinner to have an `active` attribute. Added event listener so that we can set the `active` state of spinner when request is fired to AIDA and response is received. Both the events in AiCodeCompletion now specify the Panel that triggered them, this will help us differentiate the ones coming from Console and Sources. - Also updated text editor config to handle deletion case better Bug: 433884684 Change-Id: I0293b2c6af96547867bd7eef45deb26762d02d17 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6780147 Reviewed-by: Ergün Erdoğmuş <[email protected]> Commit-Queue: Samiya Caur <[email protected]>
1 parent c62ba02 commit 7d8e5d3

File tree

10 files changed

+207
-49
lines changed

10 files changed

+207
-49
lines changed

front_end/models/ai_code_completion/AiCodeCompletion.ts

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -69,27 +69,32 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
6969

7070
async #requestAidaSuggestion(request: Host.AidaClient.CompletionRequest, cursor: number): Promise<void> {
7171
const startTime = performance.now();
72-
const response = await this.#aidaClient.completeCode(request);
72+
this.dispatchEventToListeners(Events.REQUEST_TRIGGERED, {});
7373

74-
if (response && response.generatedSamples.length > 0 && response.generatedSamples[0].generationString) {
75-
const remainderDelay = Math.max(DELAY_BEFORE_SHOWING_RESPONSE_MS - (performance.now() - startTime), 0);
76-
// Delays the rendering of the Code completion
77-
setTimeout(() => {
78-
// We are not cancelling the previous responses even when there are more recent responses
79-
// from the LLM as:
80-
// In case the user kept typing characters that are prefix of the previous suggestion, it
81-
// is a valid suggestion and we should display it to the user.
82-
// In case the user typed a different character, the config for AI auto complete suggestion
83-
// will set the suggestion to null.
84-
this.#editor.dispatch({
85-
effects: TextEditor.Config.setAiAutoCompleteSuggestion.of(
86-
{text: response.generatedSamples[0].generationString, from: cursor}),
87-
});
88-
const citations = response.generatedSamples[0].attributionMetadata?.citations;
89-
if (citations) {
90-
this.dispatchEventToListeners(Events.CITATIONS_UPDATED, {citations});
91-
}
92-
}, remainderDelay);
74+
try {
75+
const response = await this.#aidaClient.completeCode(request);
76+
if (response && response.generatedSamples.length > 0 && response.generatedSamples[0].generationString) {
77+
const remainderDelay = Math.max(DELAY_BEFORE_SHOWING_RESPONSE_MS - (performance.now() - startTime), 0);
78+
// Delays the rendering of the Code completion
79+
setTimeout(() => {
80+
// We are not cancelling the previous responses even when there are more recent responses
81+
// from the LLM as:
82+
// In case the user kept typing characters that are prefix of the previous suggestion, it
83+
// is a valid suggestion and we should display it to the user.
84+
// In case the user typed a different character, the config for AI auto complete suggestion
85+
// will set the suggestion to null.
86+
this.#editor.dispatch({
87+
effects: TextEditor.Config.setAiAutoCompleteSuggestion.of(
88+
{text: response.generatedSamples[0].generationString, from: cursor}),
89+
});
90+
const citations = response.generatedSamples[0].attributionMetadata?.citations;
91+
this.dispatchEventToListeners(Events.RESPONSE_RECEIVED, {citations});
92+
}, remainderDelay);
93+
} else {
94+
this.dispatchEventToListeners(Events.RESPONSE_RECEIVED, {});
95+
}
96+
} catch {
97+
this.dispatchEventToListeners(Events.RESPONSE_RECEIVED, {});
9398
}
9499
}
95100

@@ -113,13 +118,16 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
113118
}
114119

115120
export const enum Events {
116-
CITATIONS_UPDATED = 'CitationsUpdated',
121+
RESPONSE_RECEIVED = 'ResponseReceived',
122+
REQUEST_TRIGGERED = 'RequestTriggered',
117123
}
118124

119-
export interface CitationsUpdatedEvent {
120-
citations: Host.AidaClient.Citation[];
125+
export interface ResponseReceivedEvent {
126+
citations?: Host.AidaClient.Citation[];
121127
}
122128

123129
export interface EventTypes {
124-
[Events.CITATIONS_UPDATED]: CitationsUpdatedEvent;
130+
[Events.RESPONSE_RECEIVED]: ResponseReceivedEvent;
131+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
132+
[Events.REQUEST_TRIGGERED]: {};
125133
}

front_end/panels/common/AiCodeCompletionSummaryToolbar.ts

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import '../../ui/components/spinners/spinners.js';
56
import '../../ui/components/tooltips/tooltips.js';
67

78
import * as i18n from '../../core/i18n/i18n.js';
@@ -57,14 +58,13 @@ export interface ViewInput {
5758
}
5859

5960
export interface ViewOutput {
60-
tooltipRef?: Directives.Ref<HTMLElement>;
61+
hideTooltip?: () => void;
62+
setLoading?: (isLoading: boolean) => void;
6163
}
6264

6365
export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void;
6466

6567
export const DEFAULT_SUMMARY_TOOLBAR_VIEW: View = (input, output, target) => {
66-
output.tooltipRef = output.tooltipRef ?? Directives.createRef<HTMLElement>();
67-
6868
// clang-format off
6969
const viewSourcesSpan = input.citations && input.citations.length > 0 ?
7070
html`<span class="link" role="link" aria-details=${input.citationsTooltipId}>
@@ -86,6 +86,15 @@ export const DEFAULT_SUMMARY_TOOLBAR_VIEW: View = (input, output, target) => {
8686
<style>${styles}</style>
8787
<div class="ai-code-completion-summary-toolbar">
8888
<div class="ai-code-completion-disclaimer">
89+
<devtools-spinner
90+
.active=${false}
91+
${Directives.ref(el => {
92+
if (el instanceof HTMLElement) {
93+
output.setLoading = (isLoading: boolean) => {
94+
el.toggleAttribute('active', isLoading);
95+
};
96+
}
97+
})}></devtools-spinner>
8998
<span
9099
class="link"
91100
role="link"
@@ -96,12 +105,18 @@ export const DEFAULT_SUMMARY_TOOLBAR_VIEW: View = (input, output, target) => {
96105
@click=${() => {
97106
void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
98107
}}
99-
>${lockedString(UIStrings.relevantData)}</span>&nbsp;${lockedString(UIStrings.isSentToGoogle)}
108+
>${lockedString(UIStrings.relevantData)}</span>${lockedString(UIStrings.isSentToGoogle)}
100109
<devtools-tooltip
101110
id=${input.disclaimerTooltipId}
102111
variant=${'rich'}
103112
jslogContext=${input.panelName + '.ai-code-completion-disclaimer'}
104-
${Directives.ref(output.tooltipRef)}
113+
${Directives.ref(el => {
114+
if (el instanceof HTMLElement) {
115+
output.hideTooltip = () => {
116+
el.hidePopover();
117+
};
118+
}
119+
})}
105120
><div class="disclaimer-tooltip-container">
106121
<div class="tooltip-text">
107122
${input.noLogging ? lockedString(UIStrings.tooltipDisclaimerTextForAiCodeCompletionNoLogging) : lockedString(UIStrings.tooltipDisclaimerTextForAiCodeCompletion)}
@@ -124,6 +139,8 @@ export const DEFAULT_SUMMARY_TOOLBAR_VIEW: View = (input, output, target) => {
124139
// clang-format on
125140
};
126141

142+
const MINIMUM_LOADING_STATE_TIMEOUT = 1000;
143+
127144
export class AiCodeCompletionSummaryToolbar extends UI.Widget.Widget {
128145
readonly #view: View;
129146
#viewOutput: ViewOutput = {};
@@ -132,8 +149,10 @@ export class AiCodeCompletionSummaryToolbar extends UI.Widget.Widget {
132149
#citationsTooltipId: string;
133150
#panelName: string;
134151
#citations: string[] = [];
135-
136152
#noLogging: boolean; // Whether the enterprise setting is `ALLOW_WITHOUT_LOGGING` or not.
153+
#loading = false;
154+
#loadingStartTime = 0;
155+
#spinnerLoadingTimeout: number|undefined;
137156

138157
constructor(disclaimerTooltipId: string, citationsTooltipId: string, panelName: string, view?: View) {
139158
super();
@@ -147,10 +166,36 @@ export class AiCodeCompletionSummaryToolbar extends UI.Widget.Widget {
147166
}
148167

149168
#onManageInSettingsTooltipClick(): void {
150-
this.#viewOutput.tooltipRef?.value?.hidePopover();
169+
this.#viewOutput.hideTooltip?.();
151170
void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
152171
}
153172

173+
setLoading(loading: boolean): void {
174+
if (!loading && !this.#loading) {
175+
return;
176+
}
177+
178+
if (loading) {
179+
if (!this.#loading) {
180+
this.#viewOutput.setLoading?.(true);
181+
}
182+
if (this.#spinnerLoadingTimeout) {
183+
clearTimeout(this.#spinnerLoadingTimeout);
184+
this.#spinnerLoadingTimeout = undefined;
185+
}
186+
this.#loadingStartTime = performance.now();
187+
this.#loading = true;
188+
} else {
189+
this.#loading = false;
190+
const duration = performance.now() - this.#loadingStartTime;
191+
const remainingTime = Math.max(MINIMUM_LOADING_STATE_TIMEOUT - duration, 0);
192+
this.#spinnerLoadingTimeout = window.setTimeout(() => {
193+
this.#viewOutput.setLoading?.(false);
194+
this.#spinnerLoadingTimeout = undefined;
195+
}, remainingTime);
196+
}
197+
}
198+
154199
updateCitations(citations: string[]): void {
155200
citations.forEach(citation => {
156201
if (!this.#citations.includes(citation)) {

front_end/panels/common/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ devtools_module("common") {
3131
"../../core/platform:bundle",
3232
"../../ui/components/buttons:bundle",
3333
"../../ui/components/snackbars:bundle",
34+
"../../ui/components/spinners:bundle",
3435
"../../ui/components/tooltips:bundle",
3536
"../../ui/legacy:bundle",
3637
"../../ui/lit:bundle",

front_end/panels/common/aiCodeCompletionSummaryToolbar.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@
2121
.ai-code-completion-disclaimer {
2222
border-right: var(--sys-size-1) solid var(--sys-color-divider);
2323
padding-right: var(--sys-size-5);
24+
gap: 5px;
25+
display: flex;
26+
27+
devtools-spinner {
28+
margin-top: var(--sys-size-2);
29+
padding: var(--sys-size-1);
30+
height: var(--sys-size-6);
31+
width: var(--sys-size-6);
32+
}
2433
}
2534

2635
.ai-code-completion-recitation-notice {

front_end/panels/console/ConsolePrompt.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export class ConsolePrompt extends Common.ObjectWrapper.eventMixin<EventTypes, t
7676
private teaserContainer?: HTMLDivElement;
7777
private aiCodeCompletionSetting =
7878
Common.Settings.Settings.instance().createSetting('ai-code-completion-fre-completed', false);
79+
private aiCodeCompletionCitations?: Host.AidaClient.Citation[] = [];
7980

8081
#getJavaScriptCompletionExtensions(): CodeMirror.Extension {
8182
if (this.#selfXssWarningShown) {
@@ -357,7 +358,12 @@ export class ConsolePrompt extends Common.ObjectWrapper.eventMixin<EventTypes, t
357358
keymap.push({
358359
key: 'Tab',
359360
run: (): boolean => {
360-
return TextEditor.Config.acceptAiAutoCompleteSuggestion(this.editor.editor);
361+
const accepted = TextEditor.Config.acceptAiAutoCompleteSuggestion(this.editor.editor);
362+
if (accepted) {
363+
this.dispatchEventToListeners(
364+
Events.AI_CODE_COMPLETION_SUGGESTION_ACCEPTED, {citations: this.aiCodeCompletionCitations});
365+
}
366+
return accepted;
361367
},
362368
});
363369
}
@@ -495,15 +501,21 @@ export class ConsolePrompt extends Common.ObjectWrapper.eventMixin<EventTypes, t
495501
this.editor.focus();
496502
}
497503

504+
// TODO(b/435654172): Refactor and move aiCodeCompletion model one level up to avoid
505+
// defining additional listeners and events.
498506
private setAiCodeCompletion(): void {
499507
if (!this.aidaClient) {
500508
this.aidaClient = new Host.AidaClient.AidaClient();
501509
}
502510
this.aiCodeCompletion =
503511
new AiCodeCompletion.AiCodeCompletion.AiCodeCompletion({aidaClient: this.aidaClient}, this.editor);
504-
this.aiCodeCompletion.addEventListener(
505-
AiCodeCompletion.AiCodeCompletion.Events.CITATIONS_UPDATED,
506-
event => this.dispatchEventToListeners(Events.CITATIONS_UPDATED, event.data));
512+
this.aiCodeCompletion.addEventListener(AiCodeCompletion.AiCodeCompletion.Events.RESPONSE_RECEIVED, event => {
513+
this.aiCodeCompletionCitations = event.data.citations;
514+
this.dispatchEventToListeners(Events.AI_CODE_COMPLETION_RESPONSE_RECEIVED, event.data);
515+
});
516+
this.aiCodeCompletion.addEventListener(AiCodeCompletion.AiCodeCompletion.Events.REQUEST_TRIGGERED, event => {
517+
this.dispatchEventToListeners(Events.AI_CODE_COMPLETION_REQUEST_TRIGGERED, event.data);
518+
});
507519
}
508520

509521
private onAiCodeCompletionSettingChanged(): void {
@@ -535,10 +547,15 @@ export class ConsolePrompt extends Common.ObjectWrapper.eventMixin<EventTypes, t
535547

536548
export const enum Events {
537549
TEXT_CHANGED = 'TextChanged',
538-
CITATIONS_UPDATED = 'CitationsUpdated',
550+
AI_CODE_COMPLETION_SUGGESTION_ACCEPTED = 'AiCodeCompletionSuggestionAccepted',
551+
AI_CODE_COMPLETION_RESPONSE_RECEIVED = 'AiCodeCompletionResponseReceived',
552+
AI_CODE_COMPLETION_REQUEST_TRIGGERED = 'AiCodeCompletionRequestTriggered'
539553
}
540554

541555
export interface EventTypes {
542556
[Events.TEXT_CHANGED]: void;
543-
[Events.CITATIONS_UPDATED]: AiCodeCompletion.AiCodeCompletion.CitationsUpdatedEvent;
557+
[Events.AI_CODE_COMPLETION_SUGGESTION_ACCEPTED]: AiCodeCompletion.AiCodeCompletion.ResponseReceivedEvent;
558+
[Events.AI_CODE_COMPLETION_RESPONSE_RECEIVED]: AiCodeCompletion.AiCodeCompletion.ResponseReceivedEvent;
559+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
560+
[Events.AI_CODE_COMPLETION_REQUEST_TRIGGERED]: {};
544561
}

front_end/panels/console/ConsoleView.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,11 @@ export class ConsoleView extends UI.Widget.VBox implements
558558
this.aiCodeCompletionSetting.addChangeListener(this.onAiCodeCompletionSettingChanged.bind(this));
559559
this.onAiCodeCompletionSettingChanged();
560560
this.prompt.addEventListener(
561-
ConsolePromptEvents.CITATIONS_UPDATED, this.#onAiCodeCompletionCitationsUpdated, this);
561+
ConsolePromptEvents.AI_CODE_COMPLETION_SUGGESTION_ACCEPTED, this.#onAiCodeCompletionSuggestionAccepted, this);
562+
this.prompt.addEventListener(
563+
ConsolePromptEvents.AI_CODE_COMPLETION_REQUEST_TRIGGERED, this.#onAiCodeCompletionRequestTriggered, this);
564+
this.prompt.addEventListener(
565+
ConsolePromptEvents.AI_CODE_COMPLETION_RESPONSE_RECEIVED, this.#onAiCodeCompletionResponseReceived, this);
562566
}
563567

564568
this.messagesElement.addEventListener('keydown', this.messagesKeyDown.bind(this), false);
@@ -624,9 +628,9 @@ export class ConsoleView extends UI.Widget.VBox implements
624628
this.aiCodeCompletionSummaryToolbar.show(this.aiCodeCompletionSummaryToolbarContainer, undefined, true);
625629
}
626630

627-
#onAiCodeCompletionCitationsUpdated(
628-
event: Common.EventTarget.EventTargetEvent<AiCodeCompletion.AiCodeCompletion.CitationsUpdatedEvent>): void {
629-
if (!this.aiCodeCompletionSummaryToolbar) {
631+
#onAiCodeCompletionSuggestionAccepted(
632+
event: Common.EventTarget.EventTargetEvent<AiCodeCompletion.AiCodeCompletion.ResponseReceivedEvent>): void {
633+
if (!this.aiCodeCompletionSummaryToolbar || !event.data.citations || event.data.citations.length === 0) {
630634
return;
631635
}
632636
const citations: string[] = [];
@@ -636,7 +640,15 @@ export class ConsoleView extends UI.Widget.VBox implements
636640
citations.push(uri);
637641
}
638642
});
639-
this.aiCodeCompletionSummaryToolbar?.updateCitations(citations);
643+
this.aiCodeCompletionSummaryToolbar.updateCitations(citations);
644+
}
645+
646+
#onAiCodeCompletionRequestTriggered(): void {
647+
this.aiCodeCompletionSummaryToolbar?.setLoading(true);
648+
}
649+
650+
#onAiCodeCompletionResponseReceived(): void {
651+
this.aiCodeCompletionSummaryToolbar?.setLoading(false);
640652
}
641653

642654
static clearConsole(): void {

0 commit comments

Comments
 (0)