Skip to content

Commit bfd51b7

Browse files
Lightning00BladeDevtools-frontend LUCI CQ
authored andcommitted
[AI assistance] Add read-only mode for history
Provides a view for when the chat is restored from history. Updates the `Ask AI` to open new conversation when needed. Remove the delete button when in empty state. Bug: 351752761 Change-Id: I06fc732ab2469bac3275dc5f8002211a189f3912 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5999258 Reviewed-by: Alex Rudenko <[email protected]> Auto-Submit: Nikolay Vitkov <[email protected]> Commit-Queue: Alex Rudenko <[email protected]> Commit-Queue: Nikolay Vitkov <[email protected]>
1 parent 204e831 commit bfd51b7

File tree

8 files changed

+92
-26
lines changed

8 files changed

+92
-26
lines changed

front_end/panels/freestyler/AiAgent.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ export abstract class AiAgent<T> {
160160
abstract readonly clientFeature: Host.AidaClient.ClientFeature;
161161
abstract readonly userTier: string|undefined;
162162
abstract handleContextDetails(select: ConversationContext<T>|null): AsyncGenerator<ContextResponse, void, void>;
163+
#generatedFromHistory = false;
163164

164165
/**
165166
* Mapping between the unique request id and
@@ -203,6 +204,10 @@ export abstract class AiAgent<T> {
203204
?.query;
204205
}
205206

207+
get isHistoryEntry(): boolean {
208+
return this.#generatedFromHistory;
209+
}
210+
206211
#structuredLog: Array<{
207212
request: Host.AidaClient.AidaRequest,
208213
response: string,
@@ -389,6 +394,10 @@ STOP`;
389394
async * run(query: string, options: {
390395
signal?: AbortSignal, selected: ConversationContext<T>|null,
391396
}): AsyncGenerator<ResponseData, void, void> {
397+
if (this.#generatedFromHistory) {
398+
throw new Error('History entries are read-only.');
399+
}
400+
392401
// First context set on the agent determines its origin from now on.
393402
if (options.selected && this.#origin === undefined && options.selected) {
394403
this.#origin = options.selected.getOrigin();
@@ -532,6 +541,11 @@ STOP`;
532541
}
533542

534543
async * runFromHistory(): AsyncGenerator<ResponseData, void, void> {
544+
if (this.isEmpty) {
545+
return;
546+
}
547+
548+
this.#generatedFromHistory = true;
535549
for (const historyChunk of this.#history.values()) {
536550
for (const entry of historyChunk) {
537551
yield entry;

front_end/panels/freestyler/FreestylerPanel.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,7 +735,7 @@ describeWithEnvironment('FreestylerPanel', () => {
735735
},
736736
]);
737737
const toolbar = panel.contentElement.querySelector('.freestyler-left-toolbar');
738-
const button = toolbar!.shadowRoot!.querySelector('devtools-button[aria-label=\'Clear chat\']');
738+
const button = toolbar!.shadowRoot!.querySelector('devtools-button[aria-label=\'Delete chat\']');
739739
assert.instanceOf(button, HTMLElement);
740740
dispatchClickEvent(button);
741741
assert.deepEqual(mockView.lastCall.args[0].messages, []);

front_end/panels/freestyler/FreestylerPanel.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ const UIStrings = {
7272
*/
7373
history: 'History',
7474
/**
75-
*@description AI assistance UI text clearing the current chat session.
75+
*@description AI assistance UI text deleting the current chat session.
7676
*/
77-
clearChat: 'Clear chat',
77+
deleteChat: 'Delete chat',
7878
/**
7979
*@description AI assistance UI text that deletes all history entries.
8080
*/
@@ -173,7 +173,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
173173
#historyEntriesButton =
174174
new UI.Toolbar.ToolbarButton(i18nString(UIStrings.history), 'history', undefined, 'freestyler.history');
175175
#deleteHistoryEntryButton =
176-
new UI.Toolbar.ToolbarButton(i18nString(UIStrings.clearChat), 'bin', undefined, 'freestyler.delete');
176+
new UI.Toolbar.ToolbarButton(i18nString(UIStrings.deleteChat), 'bin', undefined, 'freestyler.delete');
177177

178178
#agents = new Set<AiAgent<unknown>>();
179179
#currentAgent?: AiAgent<unknown>;
@@ -219,6 +219,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
219219
},
220220
selectedContext: null,
221221
blockedByCrossOrigin: false,
222+
isReadOnly: false,
222223
};
223224
}
224225

@@ -293,6 +294,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
293294

294295
#updateToolbarState(): void {
295296
this.#historyEntriesButton.applyEnabledState([...this.#agents].some(agent => !agent.isEmpty));
297+
this.#deleteHistoryEntryButton.setVisible(Boolean(this.#currentAgent));
296298
this.#deleteHistoryEntryButton.applyEnabledState(Boolean(this.#currentAgent && !this.#currentAgent.isEmpty));
297299
/*
298300
* If there is no agent disable new chat button
@@ -577,7 +579,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
577579

578580
if (!this.#currentAgent) {
579581
this.#currentAgent = this.#createAgent(targetAgentType);
580-
} else if (this.#currentAgent.type !== targetAgentType) {
582+
} else if (this.#currentAgent.type !== targetAgentType || this.#currentAgent.isHistoryEntry) {
581583
this.#currentAgent = this.#createAgent(targetAgentType);
582584
}
583585
this.#viewProps.agentType = this.#currentAgent.type;
@@ -586,6 +588,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
586588
this.#viewProps.messages = [];
587589
this.#onContextSelectionChanged();
588590
this.doUpdate();
591+
this.#viewProps.isReadOnly = false;
589592
void this.#doConversation(this.#currentAgent.runFromHistory());
590593
}
591594

@@ -644,6 +647,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
644647
this.#viewProps.messages = [];
645648
this.#viewProps.agentType = agent.type;
646649
this.#onContextSelectionChanged();
650+
this.#viewProps.isReadOnly = true;
647651
await this.#doConversation(agent.runFromHistory());
648652
}
649653

@@ -680,6 +684,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
680684
return;
681685
}
682686
this.#viewProps.blockedByCrossOrigin = !currentContext.isOriginAllowed(this.#currentAgent.origin);
687+
this.#viewProps.isReadOnly = this.#currentAgent.isHistoryEntry;
683688
this.doUpdate();
684689
}
685690

front_end/panels/freestyler/components/FreestylerChatUi.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ css
5757
canShowFeedbackForm: false,
5858
userInfo: {},
5959
blockedByCrossOrigin: false,
60+
isReadOnly: false,
6061
...options,
6162
};
6263
}

front_end/panels/freestyler/components/FreestylerChatUi.ts

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ const UIStringsNotTranslate = {
122122
/**
123123
*@description Title for the start new chat
124124
*/
125-
startNewChatButtonTitle: 'Start new chat',
125+
startNewChat: 'Start new chat',
126126
/**
127127
*@description Title for the cancel icon button.
128128
*/
@@ -208,6 +208,10 @@ const UIStringsNotTranslate = {
208208
*@description Aria label for the cancel icon to be read by screen reader
209209
*/
210210
canceled: 'Canceled',
211+
/**
212+
*@description Text displayed when the chat input is disabled due to reading past conversation.
213+
*/
214+
pastConversation: 'You\'re viewing a past conversation.',
211215
};
212216

213217
const str_ = i18n.i18n.registerUIStrings('panels/freestyler/components/FreestylerChatUi.ts', UIStrings);
@@ -270,6 +274,7 @@ export interface Props {
270274
canShowFeedbackForm: boolean;
271275
userInfo: Pick<Host.InspectorFrontendHostAPI.SyncInformation, 'accountImage'|'accountFullName'>;
272276
agentType?: AgentType;
277+
isReadOnly: boolean;
273278
blockedByCrossOrigin: boolean;
274279
}
275280

@@ -900,6 +905,26 @@ export class FreestylerChatUi extends HTMLElement {
900905
}
901906
}
902907

908+
#renderReadOnlySection(): LitHtml.TemplateResult {
909+
// clang-format off
910+
return html`<div
911+
class="chat-readonly-container"
912+
jslog=${VisualLogging.section('read-only')}
913+
>
914+
<span>${lockedString(UIStringsNotTranslate.pastConversation)}</span>
915+
<devtools-button
916+
aria-label=${lockedString(UIStringsNotTranslate.startNewChat)}
917+
@click=${this.#onNewConversation}
918+
.data=${{
919+
variant: Buttons.Button.Variant.TEXT,
920+
title: lockedString(UIStringsNotTranslate.startNewChat),
921+
jslogContext: 'start-new-chat',
922+
} as Buttons.Button.ButtonData}
923+
>${lockedString(UIStringsNotTranslate.startNewChat)}</devtools-button>
924+
</div>`;
925+
// clang-format on
926+
}
927+
903928
#renderChatInputButton(): LitHtml.TemplateResult {
904929
if (this.#props.isLoading) {
905930
// clang-format off
@@ -923,17 +948,17 @@ export class FreestylerChatUi extends HTMLElement {
923948
// clang-format off
924949
return html`<devtools-button
925950
class="chat-input-button"
926-
aria-label=${lockedString(UIStringsNotTranslate.startNewChatButtonTitle)}
951+
aria-label=${lockedString(UIStringsNotTranslate.startNewChat)}
927952
@click=${this.#onNewConversation}
928953
.data=${
929954
{
930955
variant: Buttons.Button.Variant.PRIMARY,
931956
size: Buttons.Button.Size.REGULAR,
932-
title: lockedString(UIStringsNotTranslate.startNewChatButtonTitle),
957+
title: lockedString(UIStringsNotTranslate.startNewChat),
933958
jslogContext: 'start-new-chat',
934959
} as Buttons.Button.ButtonData
935960
}
936-
>${lockedString(UIStringsNotTranslate.startNewChatButtonTitle)}</devtools-button>`;
961+
>${lockedString(UIStringsNotTranslate.startNewChat)}</devtools-button>`;
937962
// clang-format on
938963
}
939964
// clang-format off
@@ -952,7 +977,6 @@ export class FreestylerChatUi extends HTMLElement {
952977
} as Buttons.Button.ButtonData
953978
}
954979
></devtools-button>`;
955-
// clang-format on
956980
}
957981

958982
#renderChatInput = (): LitHtml.LitTemplate => {
@@ -962,15 +986,25 @@ export class FreestylerChatUi extends HTMLElement {
962986

963987
// clang-format off
964988
return html`
989+
<form class="input-form" @submit=${this.#handleSubmit}>
990+
${this.#props.state !== State.CONSENT_VIEW ? html`
991+
<div class="input-header">
992+
<div class="header-link-container">
993+
${this.#renderSelection()}
994+
</div>
995+
</div>
996+
` : LitHtml.nothing}
965997
<div class="chat-input-container">
966998
<textarea class="chat-input"
967999
.disabled=${this.#isTextInputDisabled()}
9681000
wrap="hard"
9691001
@keydown=${this.#handleTextAreaKeyDown}
9701002
placeholder=${this.#getInputPlaceholderString()}
971-
jslog=${VisualLogging.textField('query').track({ keydown: 'Enter' })}></textarea>
972-
${this.#renderChatInputButton()}
973-
</div>`;
1003+
jslog=${VisualLogging.textField('query').track({ keydown: 'Enter' })}
1004+
></textarea>
1005+
${this.#renderChatInputButton()}
1006+
</div>
1007+
</form>`;
9741008
// clang-format on
9751009
};
9761010

@@ -1153,16 +1187,10 @@ export class FreestylerChatUi extends HTMLElement {
11531187
<div class="chat-ui">
11541188
<main @scroll=${this.#handleScroll}>
11551189
${this.#renderMainContents()}
1156-
<form class="input-form" @submit=${this.#handleSubmit}>
1157-
${this.#props.state !== State.CONSENT_VIEW ? html`
1158-
<div class="input-header">
1159-
<div class="header-link-container">
1160-
${this.#renderSelection()}
1161-
</div>
1162-
</div>
1163-
` : LitHtml.nothing}
1164-
${this.#renderChatInput()}
1165-
</form>
1190+
${this.#props.isReadOnly ?
1191+
this.#renderReadOnlySection() :
1192+
this.#renderChatInput()
1193+
}
11661194
</main>
11671195
<footer class="disclaimer">
11681196
<p class="disclaimer-text">

front_end/panels/freestyler/components/freestylerChatUi.css

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
/* 688px is the max width of the input form + left and right paddings: var(--sys-size-36) + 2 * var(--sys-size-5) */
4848
/* stylelint-disable-next-line at-rule-no-unknown */
4949
@container (width > 688px) {
50-
--half-scrollbar-width: calc((100cqw - 100%) / 2); /* stylelint-disable-line unit-no-unknown */
50+
/* stylelint-disable-next-line unit-no-unknown */
51+
--half-scrollbar-width: calc((100cqw - 100%) / 2);
5152

5253
margin-left: var(--half-scrollbar-width);
5354
margin-right: calc(-1 * var(--half-scrollbar-width));
@@ -62,6 +63,19 @@
6263
}
6364
}
6465

66+
.chat-readonly-container {
67+
display: flex;
68+
width: 100%;
69+
max-width: var(--sys-size-36);
70+
justify-content: center;
71+
align-items: center;
72+
background-color: var(--sys-color-surface3);
73+
font: var(--sys-typescale-body4-regular);
74+
padding: var(--sys-size-5) 0;
75+
border-radius: var(--sys-shape-corner-medium-small);
76+
margin-bottom: var(--sys-size-5);
77+
}
78+
6579
.chat-input-container {
6680
margin: var(--sys-size-4) 0;
6781
width: 100%;
@@ -138,7 +152,8 @@
138152
/* 688px is the max width of the input form + left and right paddings: var(--sys-size-36) + 2 * var(--sys-size-5) */
139153
/* stylelint-disable-next-line at-rule-no-unknown */
140154
@container (width > 688px) {
141-
--half-scrollbar-width: calc((100cqw - 100%) / 2); /* stylelint-disable-line unit-no-unknown */
155+
/* stylelint-disable-next-line unit-no-unknown */
156+
--half-scrollbar-width: calc((100cqw - 100%) / 2);
142157

143158
margin-left: var(--half-scrollbar-width);
144159
margin-right: calc(-1 * var(--half-scrollbar-width));
@@ -466,7 +481,8 @@ main {
466481
/* 688px is the max width of the input form + left and right paddings: var(--sys-size-36) + 2 * var(--sys-size-5) */
467482
/* stylelint-disable-next-line at-rule-no-unknown */
468483
@container (width > 688px) {
469-
--half-scrollbar-width: calc((100cqw - 100%) / 2); /* stylelint-disable-line unit-no-unknown */
484+
/* stylelint-disable-next-line unit-no-unknown */
485+
--half-scrollbar-width: calc((100cqw - 100%) / 2);
470486

471487
margin-left: var(--half-scrollbar-width);
472488
margin-right: calc(-1 * var(--half-scrollbar-width));

front_end/ui/components/docs/freestyler/basic.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const component = new Freestyler.FreestylerChatUi({
5555
canShowFeedbackForm: false,
5656
userInfo: {},
5757
blockedByCrossOrigin: false,
58+
isReadOnly: false,
5859
});
5960

6061
document.getElementById('container')?.appendChild(component);

front_end/ui/components/docs/freestyler/empty_state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const component = new Freestyler.FreestylerChatUi({
3030
canShowFeedbackForm: false,
3131
userInfo: {},
3232
blockedByCrossOrigin: false,
33+
isReadOnly: false,
3334
});
3435

3536
document.getElementById('container')?.appendChild(component);

0 commit comments

Comments
 (0)