Skip to content

Commit e0032e4

Browse files
ergunshDevtools-frontend LUCI CQ
authored andcommitted
[AiAssistance] Display placeholder image when image data unavailable in history
Bug: 396620504 Change-Id: I6043e317c57ed69232925c66819f66353e260827 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6275378 Auto-Submit: Samiya Caur <[email protected]> Commit-Queue: Alex Rudenko <[email protected]> Commit-Queue: Ergün Erdoğmuş <[email protected]> Reviewed-by: Alex Rudenko <[email protected]>
1 parent ed663cc commit e0032e4

File tree

6 files changed

+172
-19
lines changed

6 files changed

+172
-19
lines changed

front_end/panels/ai_assistance/AiAssistancePanel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1270,7 +1270,7 @@ export class AiAssistancePanel extends UI.Panel.Panel {
12701270
for await (const data of items) {
12711271
// We don't want to save partial responses to the conversation history.
12721272
if (data.type !== ResponseType.ANSWER || data.complete) {
1273-
currentConversation?.addHistoryItem(data);
1273+
void currentConversation?.addHistoryItem(data);
12741274
}
12751275
yield data;
12761276
}

front_end/panels/ai_assistance/AiHistoryStorage.test.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,8 @@ describe('AiHistoryStorage', () => {
325325
});
326326

327327
it('should limit the amount of stored images', async () => {
328-
const storage = getStorage(2);
328+
const MAX_STORAGE_SIZE = 2;
329+
const storage = getStorage(MAX_STORAGE_SIZE);
329330

330331
await storage.upsertImage({
331332
id: 'image-id1',
@@ -402,5 +403,115 @@ describe('AiHistoryStorage', () => {
402403
'this is more than 80 characters because I\'m just going to keep typing words and …');
403404
});
404405
});
406+
407+
describe('addHistoryItem', () => {
408+
const historyItem1: AiAssistance.ResponseData = {
409+
type: AiAssistance.ResponseType.USER_QUERY,
410+
query: 'text',
411+
imageInput: {
412+
inlineData: {
413+
data: '1',
414+
mimeType: 'image/jpeg',
415+
}
416+
},
417+
imageId: 'image-id1',
418+
};
419+
const historyItem2: AiAssistance.ResponseData = {
420+
type: AiAssistance.ResponseType.USER_QUERY,
421+
query: 'text',
422+
imageInput: {
423+
inlineData: {
424+
data: '2',
425+
mimeType: 'image/jpeg',
426+
}
427+
},
428+
imageId: 'image-id2',
429+
};
430+
431+
it('should store images and text conversation separately', async () => {
432+
const storage = getStorage();
433+
sinon.stub(AiAssistance.AiHistoryStorage, 'instance').returns(storage);
434+
const conversation1 = new AiAssistance.Conversation(AiAssistance.ConversationType.STYLING, [], 'id1', false);
435+
await conversation1.addHistoryItem(historyItem1);
436+
const conversation2 = new AiAssistance.Conversation(AiAssistance.ConversationType.STYLING, [], 'id2', false);
437+
await conversation2.addHistoryItem(historyItem2);
438+
439+
const imageHistory = storage.getImageHistory();
440+
assert.lengthOf(imageHistory, 2);
441+
assert.deepEqual(imageHistory[0], {
442+
id: 'image-id1',
443+
data: '1',
444+
mimeType: 'image/jpeg',
445+
});
446+
assert.deepEqual(imageHistory[1], {
447+
id: 'image-id2',
448+
data: '2',
449+
mimeType: 'image/jpeg',
450+
});
451+
452+
const historyWithoutImages = storage.getHistory();
453+
assert.lengthOf(historyWithoutImages, 2);
454+
assert.deepEqual(historyWithoutImages[0], {
455+
id: 'id1',
456+
type: AiAssistance.ConversationType.STYLING,
457+
history: [{
458+
type: AiAssistance.ResponseType.USER_QUERY,
459+
query: 'text',
460+
imageId: 'image-id1',
461+
}]
462+
});
463+
assert.deepEqual(historyWithoutImages[1], {
464+
id: 'id2',
465+
type: AiAssistance.ConversationType.STYLING,
466+
history: [{
467+
type: AiAssistance.ResponseType.USER_QUERY,
468+
query: 'text',
469+
imageInput: undefined,
470+
imageId: 'image-id2',
471+
}]
472+
});
473+
});
474+
475+
it('should have empty image data for image not present in history', async () => {
476+
const MAX_STORAGE_SIZE = 1;
477+
const storage = getStorage(MAX_STORAGE_SIZE);
478+
sinon.stub(AiAssistance.AiHistoryStorage, 'instance').returns(storage);
479+
const conversation1 = new AiAssistance.Conversation(AiAssistance.ConversationType.STYLING, [], 'id1', false);
480+
await conversation1.addHistoryItem(historyItem1);
481+
const conversation2 = new AiAssistance.Conversation(AiAssistance.ConversationType.STYLING, [], 'id2', false);
482+
await conversation2.addHistoryItem(historyItem2);
483+
484+
const imageHistory = storage.getImageHistory();
485+
assert.lengthOf(imageHistory, 1);
486+
const historyWithoutImages = storage.getHistory();
487+
assert.lengthOf(historyWithoutImages, 2);
488+
const conversationFromHistory = historyWithoutImages.map(item => {
489+
return new AiAssistance.Conversation(item.type, item.history, item.id, true);
490+
});
491+
assert.lengthOf(conversationFromHistory, 2);
492+
assert.deepEqual(conversationFromHistory[0].history, [{
493+
type: AiAssistance.ResponseType.USER_QUERY,
494+
query: 'text',
495+
imageInput: {
496+
inlineData: {
497+
data: AiAssistance.NOT_FOUND_IMAGE_DATA,
498+
mimeType: 'image/jpeg',
499+
}
500+
},
501+
imageId: 'image-id1',
502+
}]);
503+
assert.deepEqual(conversationFromHistory[1].history, [{
504+
type: AiAssistance.ResponseType.USER_QUERY,
505+
query: 'text',
506+
imageInput: {
507+
inlineData: {
508+
data: '2',
509+
mimeType: 'image/jpeg',
510+
}
511+
},
512+
imageId: 'image-id2',
513+
}]);
514+
});
515+
});
405516
});
406517
});

front_end/panels/ai_assistance/AiHistoryStorage.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export const enum ConversationType {
1717
PERFORMANCE_INSIGHT = 'performance-insight',
1818
}
1919

20+
export const NOT_FOUND_IMAGE_DATA = '';
21+
2022
export interface SerializedConversation {
2123
id: string;
2224
type: ConversationType;
@@ -71,7 +73,8 @@ export class Conversation {
7173
for (const data of historyWithoutImages) {
7274
if (data.type === ResponseType.USER_QUERY && data.imageId) {
7375
const image = imageHistory.find(item => item.id === data.imageId);
74-
const inlineData = image ? {data: image.data, mimeType: image.mimeType} : {data: '', mimeType: 'image/jpeg'};
76+
const inlineData = image ? {data: image.data, mimeType: image.mimeType} :
77+
{data: NOT_FOUND_IMAGE_DATA, mimeType: 'image/jpeg'};
7578
history.push({...data, imageInput: {inlineData}});
7679
} else {
7780
history.push(data);
@@ -86,16 +89,16 @@ export class Conversation {
8689
this.#isReadOnly = true;
8790
}
8891

89-
addHistoryItem(item: ResponseData): void {
92+
async addHistoryItem(item: ResponseData): Promise<void> {
9093
if (item.type === ResponseType.USER_QUERY) {
9194
if (item.imageId && item.imageInput && 'inlineData' in item.imageInput) {
9295
const inlineData = item.imageInput.inlineData;
93-
void AiHistoryStorage.instance().upsertImage(
96+
await AiHistoryStorage.instance().upsertImage(
9497
{id: item.imageId, data: inlineData.data, mimeType: inlineData.mimeType});
9598
}
9699
}
97100
this.history.push(item);
98-
void AiHistoryStorage.instance().upsertHistoryEntry(this.serialize());
101+
await AiHistoryStorage.instance().upsertHistoryEntry(this.serialize());
99102
}
100103

101104
serialize(): SerializedConversation {

front_end/panels/ai_assistance/ai_assistance-meta.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,15 @@ Common.Settings.registerSettingExtension({
153153
condition: isAnyFeatureAvailable,
154154
});
155155

156+
Common.Settings.registerSettingExtension({
157+
category: Common.Settings.SettingCategory.NONE,
158+
settingName: 'ai-assistance-history-images',
159+
settingType: Common.Settings.SettingType.ARRAY,
160+
title: i18nLazyString(UIStrings.enableAiAssistance),
161+
defaultValue: [],
162+
condition: isAnyFeatureAvailable,
163+
});
164+
156165
UI.ActionRegistration.registerActionExtension({
157166
actionId: 'freestyler.elements-floating-button',
158167
contextTypes(): [] {

front_end/panels/ai_assistance/components/ChatView.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import * as UI from '../../../ui/legacy/legacy.js';
1616
import * as Lit from '../../../ui/lit/lit.js';
1717
import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
1818
import {type ContextDetail, type ConversationContext, ErrorType} from '../agents/AiAgent.js';
19-
import {ConversationType} from '../AiHistoryStorage.js';
19+
import {ConversationType, NOT_FOUND_IMAGE_DATA} from '../AiHistoryStorage.js';
2020
import {PatchWidget} from '../PatchWidget.js';
2121

2222
import stylesRaw from './chatView.css.js';
@@ -198,6 +198,10 @@ const UIStringsNotTranslate = {
198198
*@description Title for the x-link which wraps the image input rendered in chat messages.
199199
*/
200200
openImageInNewTab: 'Open image in a new tab',
201+
/**
202+
*@description Alt text for image when it is not available.
203+
*/
204+
imageUnavailable: 'Image unavailable',
201205
/**
202206
*@description Button text to change the selected workspace
203207
*/
@@ -807,18 +811,9 @@ function renderChatMessage({
807811
html`<devtools-icon
808812
.name=${'profile'}
809813
></devtools-icon>`;
810-
let imageInput = html``;
811-
if (message.imageInput && 'inlineData' in message.imageInput) {
812-
const imageUrl = `data:image/jpeg;base64,${message.imageInput.inlineData.data}`;
813-
// clang-format off
814-
imageInput = html`<x-link
815-
class="image-link" title=${UIStringsNotTranslate.openImageInNewTab}
816-
href=${imageUrl}
817-
>
818-
<img src=${imageUrl} alt=${UIStringsNotTranslate.imageInputSentToTheModel} />
819-
</x-link>`;
820-
// clang-format on
821-
}
814+
const imageInput = message.imageInput && 'inlineData' in message.imageInput ?
815+
renderImageChatMessage(message.imageInput.inlineData) :
816+
Lit.nothing;
822817
// clang-format off
823818
return html`<section
824819
class="chat-message query"
@@ -884,6 +879,25 @@ function renderChatMessage({
884879
// clang-format on
885880
}
886881

882+
function renderImageChatMessage(inlineData: Host.AidaClient.MediaBlob): Lit.LitTemplate {
883+
if (inlineData.data === NOT_FOUND_IMAGE_DATA) {
884+
// clang-format off
885+
return html`<div class="unavailable-image" title=${UIStringsNotTranslate.imageUnavailable}>
886+
<devtools-icon name='file-image'></devtools-icon>
887+
</div>`;
888+
// clang-format on
889+
}
890+
const imageUrl = `data:image/jpeg;base64,${inlineData.data}`;
891+
// clang-format off
892+
return html`<x-link
893+
class="image-link" title=${UIStringsNotTranslate.openImageInNewTab}
894+
href=${imageUrl}
895+
>
896+
<img src=${imageUrl} alt=${UIStringsNotTranslate.imageInputSentToTheModel} />
897+
</x-link>`;
898+
// clang-format on
899+
}
900+
887901
function renderSelection({
888902
selectedContext,
889903
inspectElementToggled,

front_end/panels/ai_assistance/components/chatView.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,22 @@
322322
vertical-align: bottom;
323323
}
324324
}
325+
326+
.unavailable-image {
327+
margin: var(--sys-size-4) 0;
328+
display: inline-flex;
329+
justify-content: center;
330+
align-items: center;
331+
height: var(--sys-size-17);
332+
width: var(--sys-size-18);
333+
background-color: var(--sys-color-surface3);
334+
border-radius: var(--sys-shape-corner-small);
335+
border: 1px solid var(--sys-color-neutral-outline);
336+
337+
devtools-icon {
338+
color: var(--sys-color-state-disabled);
339+
}
340+
}
325341
}
326342

327343
.select-element {

0 commit comments

Comments
 (0)