Skip to content

Commit 9379e99

Browse files
ergunshDevtools-frontend LUCI CQ
authored andcommitted
[AiAssistance] Add placeholder for image input while screenshot is being loaded
Also disable screenshot button during this time Bug: 396620504 Change-Id: Iccee76df07f588e3c2c2d4900efb61c4afdf2dc1 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6301342 Reviewed-by: Alex Rudenko <[email protected]> Commit-Queue: Ergün Erdoğmuş <[email protected]>
1 parent d9340d8 commit 9379e99

File tree

4 files changed

+84
-34
lines changed

4 files changed

+84
-34
lines changed

front_end/panels/ai_assistance/AiAssistancePanel.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,7 +1291,7 @@ describeWithMockConnection('AI Assistance Panel', () => {
12911291
assert.isFalse(initialViewInput.multimodalInputEnabled);
12921292
assert.notExists(initialViewInput.onTakeScreenshot);
12931293
assert.notExists(initialViewInput.onRemoveImageInput);
1294-
assert.isEmpty(initialViewInput.imageInput);
1294+
assert.notExists(initialViewInput.imageInput);
12951295
});
12961296

12971297
it('adds an image input and then removes it', async () => {
@@ -1313,12 +1313,12 @@ describeWithMockConnection('AI Assistance Panel', () => {
13131313
});
13141314

13151315
expect(captureScreenshotStub.calledOnce);
1316-
assert.deepEqual(updatedViewInput.imageInput, 'imageInput');
1316+
assert.deepEqual(updatedViewInput.imageInput, {isLoading: false, data: 'imageInput'});
13171317

13181318
const updatedViewInputAfterImageRemoval = await expectViewUpdate(() => {
13191319
updatedViewInput.onRemoveImageInput?.();
13201320
});
1321-
assert.isEmpty(updatedViewInputAfterImageRemoval.imageInput);
1321+
assert.notExists(updatedViewInputAfterImageRemoval.imageInput);
13221322
});
13231323

13241324
it('sends image as input', async () => {

front_end/panels/ai_assistance/AiAssistancePanel.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
type ChatMessage,
5050
ChatMessageEntity,
5151
ChatView,
52+
type ImageInputData,
5253
type ModelChatMessage,
5354
type Props as ChatViewProps,
5455
State as ChatViewState,
@@ -61,6 +62,7 @@ const {html} = Lit;
6162
const AI_ASSISTANCE_SEND_FEEDBACK = 'https://crbug.com/364805393' as Platform.DevToolsPath.UrlString;
6263
const AI_ASSISTANCE_HELP = 'https://goo.gle/devtools-ai-assistance' as Platform.DevToolsPath.UrlString;
6364
const SCREENSHOT_QUALITY = 100;
65+
const SHOW_LOADING_STATE_TIMEOUT = 100;
6466

6567
const UIStrings = {
6668
/**
@@ -440,7 +442,7 @@ export class AiAssistancePanel extends UI.Panel.Panel {
440442
accountImage?: string,
441443
accountFullName?: string,
442444
};
443-
#imageInput = '';
445+
#imageInput?: ImageInputData;
444446
// Used to disable send button when there is not text input.
445447
#isTextInputEmpty = true;
446448

@@ -823,7 +825,7 @@ export class AiAssistancePanel extends UI.Panel.Panel {
823825
void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
824826
},
825827
onTextSubmit: async (text: string, imageInput?: Host.AidaClient.Part) => {
826-
this.#imageInput = '';
828+
this.#imageInput = undefined;
827829
this.#isTextInputEmpty = true;
828830
Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceQuerySubmitted);
829831
await this.#startConversation(text, imageInput);
@@ -1126,13 +1128,18 @@ export class AiAssistancePanel extends UI.Panel.Panel {
11261128
if (!model) {
11271129
throw new Error('Could not find model');
11281130
}
1131+
const showLoadingTimeout = setTimeout(() => {
1132+
this.#imageInput = {isLoading: true};
1133+
this.requestUpdate();
1134+
}, SHOW_LOADING_STATE_TIMEOUT);
11291135
const bytes = await model.captureScreenshot(
11301136
Protocol.Page.CaptureScreenshotRequestFormat.Jpeg,
11311137
SCREENSHOT_QUALITY,
11321138
SDK.ScreenCaptureModel.ScreenshotMode.FROM_VIEWPORT,
11331139
);
1140+
clearTimeout(showLoadingTimeout);
11341141
if (bytes) {
1135-
this.#imageInput = bytes;
1142+
this.#imageInput = {isLoading: false, data: bytes};
11361143
this.requestUpdate();
11371144
void this.updateComplete.then(() => {
11381145
this.#viewOutput.chatView?.focusTextInput();
@@ -1141,7 +1148,7 @@ export class AiAssistancePanel extends UI.Panel.Panel {
11411148
}
11421149

11431150
#handleRemoveImageInput(): void {
1144-
this.#imageInput = '';
1151+
this.#imageInput = undefined;
11451152
this.requestUpdate();
11461153
void this.updateComplete.then(() => {
11471154
this.#viewOutput.chatView?.focusTextInput();

front_end/panels/ai_assistance/components/ChatView.ts

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
217217
const lockedString = i18n.i18n.lockedString;
218218

219219
const SCROLL_ROUNDING_OFFSET = 1;
220+
const JPEG_MIME_TYPE = 'image/jpeg';
220221

221222
export interface Step {
222223
isLoading: boolean;
@@ -238,6 +239,13 @@ export const enum ChatMessageEntity {
238239
USER = 'user',
239240
}
240241

242+
export type ImageInputData = {
243+
isLoading: true,
244+
}|{
245+
isLoading: false,
246+
data: string,
247+
};
248+
241249
export interface UserChatMessage {
242250
entity: ChatMessageEntity.USER;
243251
text: string;
@@ -285,7 +293,7 @@ export interface Props {
285293
patchSuggestionLoading?: boolean;
286294
projectName?: string;
287295
multimodalInputEnabled?: boolean;
288-
imageInput?: string;
296+
imageInput?: ImageInputData;
289297
onApplyToWorkspace?: () => void;
290298
isTextInputDisabled: boolean;
291299
emptyStateSuggestions: string[];
@@ -413,12 +421,17 @@ export class ChatView extends HTMLElement {
413421

414422
#handleSubmit = (ev: SubmitEvent): void => {
415423
ev.preventDefault();
424+
if (this.#props.imageInput?.isLoading) {
425+
return;
426+
}
427+
416428
const textArea = this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement;
417429
if (!textArea?.value) {
418430
return;
419431
}
420-
const imageInput =
421-
this.#props.imageInput ? {inlineData: {data: this.#props.imageInput, mimeType: 'image/jpeg'}} : undefined;
432+
const imageInput = !this.#props.imageInput?.isLoading && this.#props.imageInput?.data ?
433+
{inlineData: {data: this.#props.imageInput.data, mimeType: JPEG_MIME_TYPE}} :
434+
undefined;
422435
void this.#props.onTextSubmit(textArea.value, imageInput);
423436
textArea.value = '';
424437
};
@@ -431,11 +444,12 @@ export class ChatView extends HTMLElement {
431444
// Go to a new line only when Shift + Enter is pressed.
432445
if (ev.key === 'Enter' && !ev.shiftKey) {
433446
ev.preventDefault();
434-
if (!ev.target?.value) {
447+
if (!ev.target?.value || this.#props.imageInput?.isLoading) {
435448
return;
436449
}
437-
const imageInput =
438-
this.#props.imageInput ? {inlineData: {data: this.#props.imageInput, mimeType: 'image/jpeg'}} : undefined;
450+
const imageInput = !this.#props.imageInput?.isLoading && this.#props.imageInput?.data ?
451+
{inlineData: {data: this.#props.imageInput.data, mimeType: JPEG_MIME_TYPE}} :
452+
undefined;
439453
void this.#props.onTextSubmit(ev.target.value, imageInput);
440454
ev.target.value = '';
441455
}
@@ -1053,13 +1067,12 @@ function renderReadOnlySection({onNewConversation, conversationType}: {
10531067
}
10541068

10551069
function renderChatInputButtons(
1056-
{isLoading, blockedByCrossOrigin, isTextInputDisabled, isTextInputEmpty, onCancel, onNewConversation}: {
1070+
{isLoading, blockedByCrossOrigin, isTextInputDisabled, isTextInputEmpty, imageInput, onCancel, onNewConversation}: {
10571071
isLoading: boolean,
10581072
blockedByCrossOrigin: boolean,
10591073
isTextInputDisabled: boolean,
10601074
isTextInputEmpty: boolean,
1061-
onCancel: (ev: SubmitEvent) => void,
1062-
onNewConversation: () => void,
1075+
imageInput?: ImageInputData, onCancel: (ev: SubmitEvent) => void, onNewConversation: () => void,
10631076
}): Lit.TemplateResult {
10641077
if (isLoading) {
10651078
// clang-format off
@@ -1107,7 +1120,7 @@ function renderChatInputButtons(
11071120
type: 'submit',
11081121
variant: Buttons.Button.Variant.ICON,
11091122
size: Buttons.Button.Size.REGULAR,
1110-
disabled: isTextInputDisabled || isTextInputEmpty,
1123+
disabled: isTextInputDisabled || isTextInputEmpty || imageInput?.isLoading,
11111124
iconName: 'send',
11121125
title: lockedString(UIStringsNotTranslate.sendButtonTitle),
11131126
jslogContext: 'send',
@@ -1120,11 +1133,13 @@ function renderTakeScreenshotButton({
11201133
multimodalInputEnabled,
11211134
blockedByCrossOrigin,
11221135
isTextInputDisabled,
1136+
imageInput,
11231137
onTakeScreenshot,
11241138
}: {
11251139
isTextInputDisabled: boolean,
11261140
blockedByCrossOrigin: boolean,
11271141
multimodalInputEnabled?: boolean,
1142+
imageInput?: ImageInputData,
11281143
onTakeScreenshot?: () => void,
11291144
}): Lit.LitTemplate {
11301145
if (!multimodalInputEnabled || blockedByCrossOrigin) {
@@ -1138,7 +1153,7 @@ function renderTakeScreenshotButton({
11381153
{
11391154
variant: Buttons.Button.Variant.ICON,
11401155
size: Buttons.Button.Size.REGULAR,
1141-
disabled: isTextInputDisabled,
1156+
disabled: isTextInputDisabled || imageInput?.isLoading,
11421157
iconName: 'photo-camera',
11431158
title: lockedString(UIStringsNotTranslate.takeScreenshotButtonTitle),
11441159
jslogContext: 'take-screenshot',
@@ -1153,15 +1168,14 @@ function renderImageInput({
11531168
onRemoveImageInput,
11541169
}: {
11551170
multimodalInputEnabled?: boolean,
1156-
imageInput?: string,
1171+
imageInput?: ImageInputData,
11571172
onRemoveImageInput?: () => void,
11581173
}): Lit.LitTemplate {
1159-
if (!multimodalInputEnabled || !imageInput || imageInput==='') {
1174+
if (!multimodalInputEnabled || !imageInput) {
11601175
return Lit.nothing;
11611176
}
1162-
return html`
1163-
<div class="image-input-container">
1164-
<devtools-button
1177+
1178+
const crossButton = html`<devtools-button
11651179
aria-label=${lockedString(UIStringsNotTranslate.removeImageInputButtonTitle)}
11661180
@click=${onRemoveImageInput}
11671181
.data=${
@@ -1172,8 +1186,20 @@ function renderImageInput({
11721186
title: lockedString(UIStringsNotTranslate.removeImageInputButtonTitle),
11731187
} as Buttons.Button.ButtonData
11741188
}
1175-
></devtools-button>
1176-
<img src="data:image/jpeg;base64, ${imageInput}" alt="Screenshot input" />
1189+
></devtools-button>`;
1190+
1191+
if (imageInput.isLoading) {
1192+
return html`<div class="image-input-container">
1193+
${crossButton}
1194+
<div class="loading">
1195+
<devtools-spinner></devtools-spinner>
1196+
</div>
1197+
</div>`;
1198+
}
1199+
return html`
1200+
<div class="image-input-container">
1201+
${crossButton}
1202+
<img src="data:image/jpeg;base64, ${imageInput.data}" alt="Screenshot input" />
11771203
</div>`;
11781204
}
11791205

@@ -1208,7 +1234,7 @@ function renderChatInput({
12081234
inspectElementToggled: boolean,
12091235
multimodalInputEnabled?: boolean,
12101236
conversationType?: ConversationType,
1211-
imageInput?: string,
1237+
imageInput?: ImageInputData,
12121238
isTextInputEmpty: boolean,
12131239
onContextClick: () => void ,
12141240
onInspectElementClick: () => void,
@@ -1230,10 +1256,10 @@ function renderChatInput({
12301256
'screenshot-button': Boolean(multimodalInputEnabled) && !blockedByCrossOrigin,
12311257
});
12321258

1233-
const chatInputContainerCls = Lit.Directives.classMap({
1234-
'chat-input-container': true,
1235-
disabled: isTextInputDisabled,
1236-
});
1259+
const chatInputContainerCls = Lit.Directives.classMap({
1260+
'chat-input-container': true,
1261+
disabled: isTextInputDisabled,
1262+
});
12371263

12381264
// clang-format off
12391265
return html`
@@ -1269,13 +1295,14 @@ function renderChatInput({
12691295
></textarea>
12701296
<div class="chat-input-buttons">
12711297
${renderTakeScreenshotButton({
1272-
multimodalInputEnabled, blockedByCrossOrigin, isTextInputDisabled, onTakeScreenshot
1298+
multimodalInputEnabled, blockedByCrossOrigin, isTextInputDisabled, imageInput, onTakeScreenshot
1299+
})}
1300+
${renderChatInputButtons({
1301+
isLoading, blockedByCrossOrigin, isTextInputDisabled, isTextInputEmpty, imageInput, onCancel, onNewConversation
12731302
})}
1274-
${renderChatInputButtons({ isLoading, blockedByCrossOrigin, isTextInputDisabled, isTextInputEmpty, onCancel, onNewConversation })}
12751303
</div>
12761304
</div>
1277-
</form>
1278-
`;
1305+
</form>`;
12791306
// clang-format on
12801307
}
12811308

front_end/panels/ai_assistance/components/chatView.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,22 @@
138138
border: 1px solid var(--sys-color-neutral-outline);
139139
border-radius: var(--sys-shape-corner-small);
140140
}
141+
142+
.loading {
143+
margin: var(--sys-size-4) 0;
144+
display: inline-flex;
145+
justify-content: center;
146+
align-items: center;
147+
height: var(--sys-size-18);
148+
width: var(--sys-size-19);
149+
background-color: var(--sys-color-surface3);
150+
border-radius: var(--sys-shape-corner-small);
151+
border: 1px solid var(--sys-color-neutral-outline);
152+
153+
devtools-spinner {
154+
color: var(--sys-color-state-disabled);
155+
}
156+
}
141157
}
142158
}
143159

0 commit comments

Comments
 (0)