chrome-devtools-frontend 1.0.1559913 → 1.0.1561528
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/front_end/core/host/InspectorFrontendHostStub.ts +0 -3
- package/front_end/core/platform/ArrayUtilities.ts +13 -0
- package/front_end/core/root/Runtime.ts +0 -5
- package/front_end/core/sdk/DOMModel.ts +8 -0
- package/front_end/core/sdk/NetworkManager.ts +4 -0
- package/front_end/core/sdk/NetworkRequest.ts +9 -0
- package/front_end/core/sdk/OverlayModel.ts +20 -9
- package/front_end/entrypoints/main/MainImpl.ts +2 -1
- package/front_end/generated/InspectorBackendCommands.ts +4 -2
- package/front_end/generated/protocol-mapping.d.ts +7 -0
- package/front_end/generated/protocol-proxy-api.d.ts +5 -0
- package/front_end/generated/protocol.ts +24 -0
- package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt +23 -22
- package/front_end/models/badges/UserBadges.ts +48 -16
- package/front_end/models/greendev/Prototypes.ts +6 -1
- package/front_end/models/trace/extras/TraceTree.ts +1 -1
- package/front_end/models/trace/handlers/PageLoadMetricsHandler.ts +8 -3
- package/front_end/panels/ai_assistance/AiAssistancePanel.ts +11 -142
- package/front_end/panels/ai_assistance/PatchWidget.ts +90 -72
- package/front_end/panels/ai_assistance/ai_assistance.ts +1 -0
- package/front_end/panels/ai_assistance/components/ChatInput.ts +701 -0
- package/front_end/panels/ai_assistance/components/ChatView.ts +71 -1268
- package/front_end/panels/ai_assistance/components/UserActionRow.ts +514 -31
- package/front_end/panels/ai_assistance/components/chatInput.css +387 -0
- package/front_end/panels/ai_assistance/components/chatView.css +38 -599
- package/front_end/panels/ai_assistance/components/userActionRow.css +230 -0
- package/front_end/panels/autofill/AutofillView.ts +2 -2
- package/front_end/panels/changes/ChangesView.ts +15 -1
- package/front_end/panels/changes/changesView.css +6 -0
- package/front_end/panels/common/BadgeNotification.ts +44 -58
- package/front_end/panels/common/CopyChangesToPrompt.ts +233 -0
- package/front_end/panels/common/common.ts +1 -0
- package/front_end/panels/elements/ElementsTreeElement.ts +183 -359
- package/front_end/panels/elements/ElementsTreeOutline.ts +0 -6
- package/front_end/panels/elements/ShortcutTreeElement.ts +57 -50
- package/front_end/panels/elements/StylePropertiesSection.ts +1 -3
- package/front_end/panels/elements/components/AdornerManager.ts +5 -149
- package/front_end/panels/issues/HiddenIssuesRow.ts +1 -2
- package/front_end/panels/issues/IssueKindView.ts +2 -4
- package/front_end/panels/issues/IssueView.ts +2 -4
- package/front_end/panels/network/NetworkDataGridNode.ts +65 -1
- package/front_end/panels/network/NetworkLogView.ts +2 -4
- package/front_end/panels/network/NetworkLogViewColumns.ts +9 -0
- package/front_end/panels/screencast/ScreencastApp.ts +1 -0
- package/front_end/panels/settings/SettingsScreen.ts +3 -2
- package/front_end/panels/timeline/CompatibilityTracksAppender.ts +14 -1
- package/front_end/panels/timeline/ThirdPartyTreeView.ts +1 -4
- package/front_end/panels/timeline/TimelineController.ts +185 -3
- package/front_end/panels/timeline/TimelineFlameChartDataProvider.ts +52 -25
- package/front_end/panels/timeline/TimelineFlameChartView.ts +1 -0
- package/front_end/panels/timeline/TimelinePanel.ts +17 -104
- package/front_end/panels/timeline/TimelineTreeView.ts +1 -0
- package/front_end/panels/timeline/components/insights/BaseInsightComponent.ts +2 -2
- package/front_end/panels/timeline/components/insights/Table.ts +3 -3
- package/front_end/panels/whats_new/ReleaseNoteText.ts +15 -9
- package/front_end/panels/whats_new/resources/WNDT.md +6 -6
- package/front_end/third_party/chromium/README.chromium +1 -1
- package/front_end/third_party/codemirror.next/rebuild.sh +1 -1
- package/front_end/third_party/lit/rebuild.sh +1 -1
- package/front_end/third_party/puppeteer/README.chromium +2 -2
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/api/Page.d.ts +2 -3
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/api/Page.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/api/Page.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/HTTPRequest.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/HTTPRequest.js +9 -0
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/HTTPRequest.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/HTTPResponse.d.ts +3 -0
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/HTTPResponse.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/HTTPResponse.js +9 -0
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/HTTPResponse.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/core/Request.d.ts +3 -0
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/core/Request.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/core/Request.js +10 -0
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/core/Request.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Browser.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Browser.js +8 -4
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Browser.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/injected.d.ts +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/injected.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/injected.js +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/injected.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/injected/injected.d.ts +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.d.ts +3 -3
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.js +3 -3
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/Mutex.d.ts +2 -2
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/version.d.ts +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/version.js +1 -1
- package/front_end/third_party/puppeteer/package/lib/es5-iife/puppeteer-core-browser.d.ts +10 -1
- package/front_end/third_party/puppeteer/package/lib/es5-iife/puppeteer-core-browser.js +13 -7
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/api/Page.d.ts +2 -3
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/api/Page.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/api/Page.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/HTTPRequest.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/HTTPRequest.js +9 -0
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/HTTPRequest.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/HTTPResponse.d.ts +3 -0
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/HTTPResponse.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/HTTPResponse.js +9 -0
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/HTTPResponse.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/core/Request.d.ts +3 -0
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/core/Request.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/core/Request.js +10 -0
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/core/Request.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Browser.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Browser.js +8 -4
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Browser.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/injected.d.ts +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/injected.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/injected.js +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/injected.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.d.ts +3 -3
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.js +3 -3
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/version.d.ts +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/version.js +1 -1
- package/front_end/third_party/puppeteer/package/lib/types.d.ts +10 -1
- package/front_end/third_party/puppeteer/package/package.json +3 -3
- package/front_end/third_party/puppeteer/package/src/api/Page.ts +2 -3
- package/front_end/third_party/puppeteer/package/src/bidi/HTTPRequest.ts +13 -0
- package/front_end/third_party/puppeteer/package/src/bidi/HTTPResponse.ts +10 -0
- package/front_end/third_party/puppeteer/package/src/bidi/core/Request.ts +15 -0
- package/front_end/third_party/puppeteer/package/src/cdp/Browser.ts +9 -4
- package/front_end/third_party/puppeteer/package/src/generated/injected.ts +1 -1
- package/front_end/third_party/puppeteer/package/src/revisions.ts +3 -3
- package/front_end/third_party/puppeteer/package/src/util/version.ts +1 -1
- package/front_end/ui/components/adorners/Adorner.ts +8 -68
- package/front_end/ui/legacy/TabbedPane.ts +1 -1
- package/front_end/ui/visual_logging/KnownContextValues.ts +3 -0
- package/package.json +2 -1
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
// Use of this source code is governed by a BSD-style license that can be
|
|
3
3
|
// found in the LICENSE file.
|
|
4
4
|
|
|
5
|
+
import '../../../ui/components/markdown_view/markdown_view.js';
|
|
6
|
+
|
|
5
7
|
import * as Common from '../../../core/common/common.js';
|
|
6
8
|
import * as Host from '../../../core/host/host.js';
|
|
7
9
|
import * as i18n from '../../../core/i18n/i18n.js';
|
|
8
10
|
import type * as Platform from '../../../core/platform/platform.js';
|
|
11
|
+
import * as AiAssistanceModel from '../../../models/ai_assistance/ai_assistance.js';
|
|
12
|
+
import * as Marked from '../../../third_party/marked/marked.js';
|
|
9
13
|
import * as Buttons from '../../../ui/components/buttons/buttons.js';
|
|
10
14
|
import * as Input from '../../../ui/components/input/input.js';
|
|
15
|
+
import type * as MarkdownView from '../../../ui/components/markdown_view/markdown_view.js';
|
|
16
|
+
import type {MarkdownLitRenderer} from '../../../ui/components/markdown_view/MarkdownView.js';
|
|
11
17
|
import * as UIHelpers from '../../../ui/helpers/helpers.js';
|
|
12
18
|
import * as UI from '../../../ui/legacy/legacy.js';
|
|
13
19
|
import * as Lit from '../../../ui/lit/lit.js';
|
|
@@ -15,7 +21,11 @@ import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
|
|
|
15
21
|
|
|
16
22
|
import userActionRowStyles from './userActionRow.css.js';
|
|
17
23
|
|
|
18
|
-
const {html, Directives: {ref}} = Lit;
|
|
24
|
+
const {html, Directives: {ref, ifDefined}} = Lit;
|
|
25
|
+
const lockedString = i18n.i18n.lockedString;
|
|
26
|
+
|
|
27
|
+
const REPORT_URL = 'https://crbug.com/364805393' as Platform.DevToolsPath.UrlString;
|
|
28
|
+
const SCROLL_ROUNDING_OFFSET = 1;
|
|
19
29
|
|
|
20
30
|
/*
|
|
21
31
|
* Strings that don't need to be translated at this time.
|
|
@@ -70,13 +80,132 @@ const UIStringsNotTranslate = {
|
|
|
70
80
|
* @description The title of the button that copies the AI-generated response to the clipboard.
|
|
71
81
|
*/
|
|
72
82
|
copyResponse: 'Copy response',
|
|
83
|
+
/**
|
|
84
|
+
* @description The error message when the request to the LLM failed for some reason.
|
|
85
|
+
*/
|
|
86
|
+
systemError:
|
|
87
|
+
'Something unforeseen happened and I can no longer continue. Try your request again and see if that resolves the issue. If this keeps happening, update Chrome to the latest version.',
|
|
88
|
+
/**
|
|
89
|
+
* @description The error message when the LLM gets stuck in a loop (max steps reached).
|
|
90
|
+
*/
|
|
91
|
+
maxStepsError: 'Seems like I am stuck with the investigation. It would be better if you start over.',
|
|
92
|
+
/**
|
|
93
|
+
* @description Displayed when the user stop the response
|
|
94
|
+
*/
|
|
95
|
+
stoppedResponse: 'You stopped this response',
|
|
96
|
+
/**
|
|
97
|
+
* @description Prompt for user to confirm code execution that may affect the page.
|
|
98
|
+
*/
|
|
99
|
+
sideEffectConfirmationDescription: 'This code may modify page content. Continue?',
|
|
100
|
+
/**
|
|
101
|
+
* @description Button text that confirm code execution that may affect the page.
|
|
102
|
+
*/
|
|
103
|
+
positiveSideEffectConfirmation: 'Continue',
|
|
104
|
+
/**
|
|
105
|
+
* @description Button text that cancels code execution that may affect the page.
|
|
106
|
+
*/
|
|
107
|
+
negativeSideEffectConfirmation: 'Cancel',
|
|
108
|
+
/**
|
|
109
|
+
* @description The generic name of the AI agent (do not translate)
|
|
110
|
+
*/
|
|
111
|
+
ai: 'AI',
|
|
112
|
+
/**
|
|
113
|
+
* @description The fallback text when we can't find the user full name
|
|
114
|
+
*/
|
|
115
|
+
you: 'You',
|
|
116
|
+
/**
|
|
117
|
+
* @description The fallback text when a step has no title yet
|
|
118
|
+
*/
|
|
119
|
+
investigating: 'Investigating',
|
|
120
|
+
/**
|
|
121
|
+
* @description Prefix to the title of each thinking step of a user action is required to continue
|
|
122
|
+
*/
|
|
123
|
+
paused: 'Paused',
|
|
124
|
+
/**
|
|
125
|
+
* @description Heading text for the code block that shows the executed code.
|
|
126
|
+
*/
|
|
127
|
+
codeExecuted: 'Code executed',
|
|
128
|
+
/**
|
|
129
|
+
* @description Heading text for the code block that shows the code to be executed after side effect confirmation.
|
|
130
|
+
*/
|
|
131
|
+
codeToExecute: 'Code to execute',
|
|
132
|
+
/**
|
|
133
|
+
* @description Heading text for the code block that shows the returned data.
|
|
134
|
+
*/
|
|
135
|
+
dataReturned: 'Data returned',
|
|
136
|
+
/**
|
|
137
|
+
* @description Aria label for the check mark icon to be read by screen reader
|
|
138
|
+
*/
|
|
139
|
+
completed: 'Completed',
|
|
140
|
+
/**
|
|
141
|
+
* @description Aria label for the cancel icon to be read by screen reader
|
|
142
|
+
*/
|
|
143
|
+
canceled: 'Canceled',
|
|
144
|
+
/**
|
|
145
|
+
* @description Alt text for the image input (displayed in the chat messages) that has been sent to the model.
|
|
146
|
+
*/
|
|
147
|
+
imageInputSentToTheModel: 'Image input sent to the model',
|
|
148
|
+
/**
|
|
149
|
+
* @description Alt text for the account avatar.
|
|
150
|
+
*/
|
|
151
|
+
accountAvatar: 'Account avatar',
|
|
152
|
+
/**
|
|
153
|
+
* @description Title for the x-link which wraps the image input rendered in chat messages.
|
|
154
|
+
*/
|
|
155
|
+
openImageInNewTab: 'Open image in a new tab',
|
|
156
|
+
/**
|
|
157
|
+
* @description Alt text for image when it is not available.
|
|
158
|
+
*/
|
|
159
|
+
imageUnavailable: 'Image unavailable',
|
|
73
160
|
} as const;
|
|
74
161
|
|
|
75
|
-
|
|
162
|
+
export interface Step {
|
|
163
|
+
isLoading: boolean;
|
|
164
|
+
thought?: string;
|
|
165
|
+
title?: string;
|
|
166
|
+
code?: string;
|
|
167
|
+
output?: string;
|
|
168
|
+
canceled?: boolean;
|
|
169
|
+
sideEffect?: ConfirmSideEffectDialog;
|
|
170
|
+
contextDetails?: [AiAssistanceModel.AiAgent.ContextDetail, ...AiAssistanceModel.AiAgent.ContextDetail[]];
|
|
171
|
+
}
|
|
76
172
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
173
|
+
export interface ConfirmSideEffectDialog {
|
|
174
|
+
onAnswer: (result: boolean) => void;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const enum ChatMessageEntity {
|
|
178
|
+
MODEL = 'model',
|
|
179
|
+
USER = 'user',
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface AnswerPart {
|
|
183
|
+
type: 'answer';
|
|
184
|
+
text: string;
|
|
185
|
+
suggestions?: [string, ...string[]];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface StepPart {
|
|
189
|
+
type: 'step';
|
|
190
|
+
step: Step;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export type ModelMessagePart = AnswerPart|StepPart;
|
|
194
|
+
|
|
195
|
+
export interface UserChatMessage {
|
|
196
|
+
entity: ChatMessageEntity.USER;
|
|
197
|
+
text: string;
|
|
198
|
+
imageInput?: Host.AidaClient.Part;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface ModelChatMessage {
|
|
202
|
+
entity: ChatMessageEntity.MODEL;
|
|
203
|
+
parts: ModelMessagePart[];
|
|
204
|
+
error?: AiAssistanceModel.AiAgent.ErrorType;
|
|
205
|
+
rpcId?: Host.AidaClient.RpcGlobalId;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export type ChatMessage = UserChatMessage|ModelChatMessage;
|
|
80
209
|
|
|
81
210
|
export interface RatingViewInput {
|
|
82
211
|
currentRating?: Host.AidaClient.Rating;
|
|
@@ -104,7 +233,8 @@ export interface FeedbackFormViewInput {
|
|
|
104
233
|
isSubmitButtonDisabled: boolean;
|
|
105
234
|
}
|
|
106
235
|
|
|
107
|
-
export type UserActionRowViewInput =
|
|
236
|
+
export type UserActionRowViewInput =
|
|
237
|
+
MessageInput&RatingViewInput&ActionViewInput&SuggestionViewInput&FeedbackFormViewInput;
|
|
108
238
|
|
|
109
239
|
export interface ViewOutput {
|
|
110
240
|
suggestionsLeftScrollButtonContainer?: Element;
|
|
@@ -112,20 +242,352 @@ export interface ViewOutput {
|
|
|
112
242
|
suggestionsRightScrollButtonContainer?: Element;
|
|
113
243
|
}
|
|
114
244
|
|
|
115
|
-
export interface
|
|
116
|
-
showRateButtons: boolean;
|
|
117
|
-
onFeedbackSubmit: (rate: Host.AidaClient.Rating, feedback?: string) => void;
|
|
245
|
+
export interface MessageInput {
|
|
118
246
|
suggestions?: [string, ...string[]];
|
|
119
|
-
|
|
120
|
-
|
|
247
|
+
message: ChatMessage;
|
|
248
|
+
isLoading: boolean;
|
|
249
|
+
isReadOnly: boolean;
|
|
250
|
+
isLastMessage: boolean;
|
|
121
251
|
canShowFeedbackForm: boolean;
|
|
252
|
+
userInfo: Pick<Host.InspectorFrontendHostAPI.SyncInformation, 'accountImage'|'accountFullName'>;
|
|
253
|
+
markdownRenderer: MarkdownLitRenderer;
|
|
254
|
+
onSuggestionClick: (suggestion: string) => void;
|
|
255
|
+
onFeedbackSubmit: (rpcId: Host.AidaClient.RpcGlobalId, rate: Host.AidaClient.Rating, feedback?: string) => void;
|
|
256
|
+
onCopyResponseClick: (message: ModelChatMessage) => void;
|
|
122
257
|
}
|
|
123
258
|
|
|
124
259
|
export const DEFAULT_VIEW = (input: UserActionRowViewInput, output: ViewOutput, target: HTMLElement): void => {
|
|
260
|
+
const message = input.message;
|
|
261
|
+
if (message.entity === ChatMessageEntity.USER) {
|
|
262
|
+
const name = input.userInfo.accountFullName || lockedString(UIStringsNotTranslate.you);
|
|
263
|
+
const image = input.userInfo.accountImage ?
|
|
264
|
+
html`<img src="data:image/png;base64, ${input.userInfo.accountImage}" alt=${
|
|
265
|
+
UIStringsNotTranslate.accountAvatar} />` :
|
|
266
|
+
html`<devtools-icon
|
|
267
|
+
name="profile"
|
|
268
|
+
></devtools-icon>`;
|
|
269
|
+
const imageInput = message.imageInput && 'inlineData' in message.imageInput ?
|
|
270
|
+
renderImageChatMessage(message.imageInput.inlineData) :
|
|
271
|
+
Lit.nothing;
|
|
272
|
+
// clang-format off
|
|
273
|
+
Lit.render(html`
|
|
274
|
+
<style>${Input.textInputStyles}</style>
|
|
275
|
+
<style>${userActionRowStyles}</style>
|
|
276
|
+
<section
|
|
277
|
+
class="chat-message query ${input.isLastMessage ? 'is-last-message' : ''}"
|
|
278
|
+
jslog=${VisualLogging.section('question')}
|
|
279
|
+
>
|
|
280
|
+
<div class="message-info">
|
|
281
|
+
${image}
|
|
282
|
+
<div class="message-name">
|
|
283
|
+
<h2>${name}</h2>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
${imageInput}
|
|
287
|
+
<div class="message-content">${renderTextAsMarkdown(message.text, input.markdownRenderer)}</div>
|
|
288
|
+
</section>
|
|
289
|
+
`, target);
|
|
290
|
+
// clang-format on
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
125
294
|
// clang-format off
|
|
126
295
|
Lit.render(html`
|
|
127
296
|
<style>${Input.textInputStyles}</style>
|
|
128
297
|
<style>${userActionRowStyles}</style>
|
|
298
|
+
<section
|
|
299
|
+
class="chat-message answer ${input.isLastMessage ? 'is-last-message' : ''}"
|
|
300
|
+
jslog=${VisualLogging.section('answer')}
|
|
301
|
+
>
|
|
302
|
+
<div class="message-info">
|
|
303
|
+
<devtools-icon name="smart-assistant"></devtools-icon>
|
|
304
|
+
<div class="message-name">
|
|
305
|
+
<h2>${lockedString(UIStringsNotTranslate.ai)}</h2>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
${Lit.Directives.repeat(
|
|
309
|
+
message.parts,
|
|
310
|
+
(_, index) => index,
|
|
311
|
+
(part, index) => {
|
|
312
|
+
const isLastPart = index === message.parts.length - 1;
|
|
313
|
+
if (part.type === 'answer') {
|
|
314
|
+
return html`<p>${renderTextAsMarkdown(part.text, input.markdownRenderer, { animate: !input.isReadOnly && input.isLoading && isLastPart && input.isLastMessage })}</p>`;
|
|
315
|
+
}
|
|
316
|
+
return renderStep({
|
|
317
|
+
step: part.step,
|
|
318
|
+
isLoading: input.isLoading,
|
|
319
|
+
markdownRenderer: input.markdownRenderer,
|
|
320
|
+
isLast: isLastPart,
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
)}
|
|
324
|
+
${renderError(message)}
|
|
325
|
+
${input.isLastMessage && !input.isLoading ? renderActions(input, output) : Lit.nothing}
|
|
326
|
+
</section>
|
|
327
|
+
`, target);
|
|
328
|
+
// clang-format on
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
export type View = typeof DEFAULT_VIEW;
|
|
332
|
+
|
|
333
|
+
function renderTextAsMarkdown(text: string, markdownRenderer: MarkdownLitRenderer, {animate, ref: refFn}: {
|
|
334
|
+
animate?: boolean,
|
|
335
|
+
ref?: (element?: Element) => void,
|
|
336
|
+
} = {}): Lit.TemplateResult {
|
|
337
|
+
let tokens = [];
|
|
338
|
+
try {
|
|
339
|
+
tokens = Marked.Marked.lexer(text);
|
|
340
|
+
for (const token of tokens) {
|
|
341
|
+
// Try to render all the tokens to make sure that
|
|
342
|
+
// they all have a template defined for them. If there
|
|
343
|
+
// isn't any template defined for a token, we'll fallback
|
|
344
|
+
// to rendering the text as plain text instead of markdown.
|
|
345
|
+
markdownRenderer.renderToken(token);
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
// The tokens were not parsed correctly or
|
|
349
|
+
// one of the tokens are not supported, so we
|
|
350
|
+
// continue to render this as text.
|
|
351
|
+
return html`${text}`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// clang-format off
|
|
355
|
+
return html`<devtools-markdown-view
|
|
356
|
+
.data=${{tokens, renderer: markdownRenderer, animationEnabled: animate} as MarkdownView.MarkdownView.MarkdownViewData}
|
|
357
|
+
${refFn ? ref(refFn) : Lit.nothing}>
|
|
358
|
+
</devtools-markdown-view>`;
|
|
359
|
+
// clang-format on
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function renderTitle(step: Step): Lit.LitTemplate {
|
|
363
|
+
const paused =
|
|
364
|
+
step.sideEffect ? html`<span class="paused">${lockedString(UIStringsNotTranslate.paused)}: </span>` : Lit.nothing;
|
|
365
|
+
const actionTitle = step.title ?? `${lockedString(UIStringsNotTranslate.investigating)}…`;
|
|
366
|
+
|
|
367
|
+
return html`<span class="title">${paused}${actionTitle}</span>`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function renderStepCode(step: Step): Lit.LitTemplate {
|
|
371
|
+
if (!step.code && !step.output) {
|
|
372
|
+
return Lit.nothing;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// If there is no "output" yet, it means we didn't execute the code yet (e.g. maybe it is still waiting for confirmation from the user)
|
|
376
|
+
// thus we show "Code to execute" text rather than "Code executed" text on the heading of the code block.
|
|
377
|
+
const codeHeadingText = (step.output && !step.canceled) ? lockedString(UIStringsNotTranslate.codeExecuted) :
|
|
378
|
+
lockedString(UIStringsNotTranslate.codeToExecute);
|
|
379
|
+
|
|
380
|
+
// If there is output, we don't show notice on this code block and instead show
|
|
381
|
+
// it in the data returned code block.
|
|
382
|
+
// clang-format off
|
|
383
|
+
const code = step.code ? html`<div class="action-result">
|
|
384
|
+
<devtools-code-block
|
|
385
|
+
.code=${step.code.trim()}
|
|
386
|
+
.codeLang=${'js'}
|
|
387
|
+
.displayNotice=${!Boolean(step.output)}
|
|
388
|
+
.header=${codeHeadingText}
|
|
389
|
+
.showCopyButton=${true}
|
|
390
|
+
></devtools-code-block>
|
|
391
|
+
</div>` :
|
|
392
|
+
Lit.nothing;
|
|
393
|
+
const output = step.output ? html`<div class="js-code-output">
|
|
394
|
+
<devtools-code-block
|
|
395
|
+
.code=${step.output}
|
|
396
|
+
.codeLang=${'js'}
|
|
397
|
+
.displayNotice=${true}
|
|
398
|
+
.header=${lockedString(UIStringsNotTranslate.dataReturned)}
|
|
399
|
+
.showCopyButton=${false}
|
|
400
|
+
></devtools-code-block>
|
|
401
|
+
</div>` :
|
|
402
|
+
Lit.nothing;
|
|
403
|
+
|
|
404
|
+
return html`<div class="step-code">${code}${output}</div>`;
|
|
405
|
+
// clang-format on
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function renderStepDetails({
|
|
409
|
+
step,
|
|
410
|
+
markdownRenderer,
|
|
411
|
+
isLast,
|
|
412
|
+
}: {
|
|
413
|
+
step: Step,
|
|
414
|
+
markdownRenderer: MarkdownLitRenderer,
|
|
415
|
+
isLast: boolean,
|
|
416
|
+
}): Lit.LitTemplate {
|
|
417
|
+
const sideEffects = isLast && step.sideEffect ? renderSideEffectConfirmationUi(step) : Lit.nothing;
|
|
418
|
+
const thought = step.thought ? html`<p>${renderTextAsMarkdown(step.thought, markdownRenderer)}</p>` : Lit.nothing;
|
|
419
|
+
|
|
420
|
+
// clang-format off
|
|
421
|
+
const contextDetails = step.contextDetails ?
|
|
422
|
+
html`${Lit.Directives.repeat(
|
|
423
|
+
step.contextDetails,
|
|
424
|
+
contextDetail => {
|
|
425
|
+
return html`<div class="context-details">
|
|
426
|
+
<devtools-code-block
|
|
427
|
+
.code=${contextDetail.text}
|
|
428
|
+
.codeLang=${contextDetail.codeLang || ''}
|
|
429
|
+
.displayNotice=${false}
|
|
430
|
+
.header=${contextDetail.title}
|
|
431
|
+
.showCopyButton=${true}
|
|
432
|
+
></devtools-code-block>
|
|
433
|
+
</div>`;
|
|
434
|
+
},
|
|
435
|
+
)}` : Lit.nothing;
|
|
436
|
+
|
|
437
|
+
return html`<div class="step-details">
|
|
438
|
+
${thought}
|
|
439
|
+
${renderStepCode(step)}
|
|
440
|
+
${sideEffects}
|
|
441
|
+
${contextDetails}
|
|
442
|
+
</div>`;
|
|
443
|
+
// clang-format on
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function renderStepBadge({step, isLoading, isLast}: {
|
|
447
|
+
step: Step,
|
|
448
|
+
isLoading: boolean,
|
|
449
|
+
isLast: boolean,
|
|
450
|
+
}): Lit.LitTemplate {
|
|
451
|
+
if (isLoading && isLast && !step.sideEffect) {
|
|
452
|
+
return html`<devtools-spinner></devtools-spinner>`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
let iconName = 'checkmark';
|
|
456
|
+
let ariaLabel: string|undefined = lockedString(UIStringsNotTranslate.completed);
|
|
457
|
+
let role: 'button'|undefined = 'button';
|
|
458
|
+
if (isLast && step.sideEffect) {
|
|
459
|
+
role = undefined;
|
|
460
|
+
ariaLabel = undefined;
|
|
461
|
+
iconName = 'pause-circle';
|
|
462
|
+
} else if (step.canceled) {
|
|
463
|
+
ariaLabel = lockedString(UIStringsNotTranslate.canceled);
|
|
464
|
+
iconName = 'cross';
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return html`<devtools-icon
|
|
468
|
+
class="indicator"
|
|
469
|
+
role=${ifDefined(role)}
|
|
470
|
+
aria-label=${ifDefined(ariaLabel)}
|
|
471
|
+
.name=${iconName}
|
|
472
|
+
></devtools-icon>`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function renderStep({step, isLoading, markdownRenderer, isLast}: {
|
|
476
|
+
step: Step,
|
|
477
|
+
isLoading: boolean,
|
|
478
|
+
markdownRenderer: MarkdownLitRenderer,
|
|
479
|
+
isLast: boolean,
|
|
480
|
+
}): Lit.LitTemplate {
|
|
481
|
+
const stepClasses = Lit.Directives.classMap({
|
|
482
|
+
step: true,
|
|
483
|
+
empty: !step.thought && !step.code && !step.contextDetails && !step.sideEffect,
|
|
484
|
+
paused: Boolean(step.sideEffect),
|
|
485
|
+
canceled: Boolean(step.canceled),
|
|
486
|
+
});
|
|
487
|
+
// clang-format off
|
|
488
|
+
return html`
|
|
489
|
+
<details class=${stepClasses}
|
|
490
|
+
jslog=${VisualLogging.section('step')}
|
|
491
|
+
.open=${Boolean(step.sideEffect)}>
|
|
492
|
+
<summary>
|
|
493
|
+
<div class="summary">
|
|
494
|
+
${renderStepBadge({ step, isLoading, isLast })}
|
|
495
|
+
${renderTitle(step)}
|
|
496
|
+
<devtools-icon
|
|
497
|
+
class="arrow"
|
|
498
|
+
name="chevron-down"
|
|
499
|
+
></devtools-icon>
|
|
500
|
+
</div>
|
|
501
|
+
</summary>
|
|
502
|
+
${renderStepDetails({step, markdownRenderer, isLast})}
|
|
503
|
+
</details>`;
|
|
504
|
+
// clang-format on
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function renderSideEffectConfirmationUi(step: Step): Lit.LitTemplate {
|
|
508
|
+
if (!step.sideEffect) {
|
|
509
|
+
return Lit.nothing;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// clang-format off
|
|
513
|
+
return html`<div
|
|
514
|
+
class="side-effect-confirmation"
|
|
515
|
+
jslog=${VisualLogging.section('side-effect-confirmation')}
|
|
516
|
+
>
|
|
517
|
+
<p>${lockedString(UIStringsNotTranslate.sideEffectConfirmationDescription)}</p>
|
|
518
|
+
<div class="side-effect-buttons-container">
|
|
519
|
+
<devtools-button
|
|
520
|
+
.data=${
|
|
521
|
+
{
|
|
522
|
+
variant: Buttons.Button.Variant.OUTLINED,
|
|
523
|
+
jslogContext: 'decline-execute-code',
|
|
524
|
+
} as Buttons.Button.ButtonData
|
|
525
|
+
}
|
|
526
|
+
@click=${() => step.sideEffect?.onAnswer(false)}
|
|
527
|
+
>${lockedString(
|
|
528
|
+
UIStringsNotTranslate.negativeSideEffectConfirmation,
|
|
529
|
+
)}</devtools-button>
|
|
530
|
+
<devtools-button
|
|
531
|
+
.data=${
|
|
532
|
+
{
|
|
533
|
+
variant: Buttons.Button.Variant.PRIMARY,
|
|
534
|
+
jslogContext: 'accept-execute-code',
|
|
535
|
+
iconName: 'play',
|
|
536
|
+
} as Buttons.Button.ButtonData
|
|
537
|
+
}
|
|
538
|
+
@click=${() => step.sideEffect?.onAnswer(true)}
|
|
539
|
+
>${
|
|
540
|
+
lockedString(UIStringsNotTranslate.positiveSideEffectConfirmation)
|
|
541
|
+
}</devtools-button>
|
|
542
|
+
</div>
|
|
543
|
+
</div>`;
|
|
544
|
+
// clang-format on
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function renderError(message: ModelChatMessage): Lit.LitTemplate {
|
|
548
|
+
if (message.error) {
|
|
549
|
+
let errorMessage;
|
|
550
|
+
switch (message.error) {
|
|
551
|
+
case AiAssistanceModel.AiAgent.ErrorType.UNKNOWN:
|
|
552
|
+
case AiAssistanceModel.AiAgent.ErrorType.BLOCK:
|
|
553
|
+
errorMessage = UIStringsNotTranslate.systemError;
|
|
554
|
+
break;
|
|
555
|
+
case AiAssistanceModel.AiAgent.ErrorType.MAX_STEPS:
|
|
556
|
+
errorMessage = UIStringsNotTranslate.maxStepsError;
|
|
557
|
+
break;
|
|
558
|
+
case AiAssistanceModel.AiAgent.ErrorType.ABORT:
|
|
559
|
+
return html`<p class="aborted" jslog=${VisualLogging.section('aborted')}>${
|
|
560
|
+
lockedString(UIStringsNotTranslate.stoppedResponse)}</p>`;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return html`<p class="error" jslog=${VisualLogging.section('error')}>${lockedString(errorMessage)}</p>`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return Lit.nothing;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function renderImageChatMessage(inlineData: Host.AidaClient.MediaBlob): Lit.LitTemplate {
|
|
570
|
+
if (inlineData.data === AiAssistanceModel.AiConversation.NOT_FOUND_IMAGE_DATA) {
|
|
571
|
+
// clang-format off
|
|
572
|
+
return html`<div class="unavailable-image" title=${UIStringsNotTranslate.imageUnavailable}>
|
|
573
|
+
<devtools-icon name='file-image'></devtools-icon>
|
|
574
|
+
</div>`;
|
|
575
|
+
// clang-format on
|
|
576
|
+
}
|
|
577
|
+
const imageUrl = `data:${inlineData.mimeType};base64,${inlineData.data}`;
|
|
578
|
+
// clang-format off
|
|
579
|
+
return html`<x-link
|
|
580
|
+
class="image-link" title=${UIStringsNotTranslate.openImageInNewTab}
|
|
581
|
+
href=${imageUrl}
|
|
582
|
+
>
|
|
583
|
+
<img src=${imageUrl} alt=${UIStringsNotTranslate.imageInputSentToTheModel} />
|
|
584
|
+
</x-link>`;
|
|
585
|
+
// clang-format on
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function renderActions(input: UserActionRowViewInput, output: ViewOutput): Lit.LitTemplate {
|
|
589
|
+
// clang-format off
|
|
590
|
+
return html`
|
|
129
591
|
<div class="ai-assistance-feedback-row">
|
|
130
592
|
<div class="action-buttons">
|
|
131
593
|
${input.showRateButtons ? html`
|
|
@@ -273,23 +735,22 @@ export const DEFAULT_VIEW = (input: UserActionRowViewInput, output: ViewOutput,
|
|
|
273
735
|
</div>
|
|
274
736
|
</form>
|
|
275
737
|
` : Lit.nothing}
|
|
276
|
-
|
|
738
|
+
`;
|
|
277
739
|
// clang-format on
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
export type View = typeof DEFAULT_VIEW;
|
|
740
|
+
}
|
|
281
741
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
export class UserActionRow extends UI.Widget.Widget implements UserActionRowWidgetParams {
|
|
287
|
-
showRateButtons = false;
|
|
288
|
-
onFeedbackSubmit: (rate: Host.AidaClient.Rating, feedback?: string) => void = () => {};
|
|
289
|
-
suggestions: [string, ...string[]]|undefined;
|
|
290
|
-
onCopyResponseClick: () => void = () => {};
|
|
291
|
-
onSuggestionClick: (suggestion: string) => void = () => {};
|
|
742
|
+
export class UserActionRow extends UI.Widget.Widget {
|
|
743
|
+
message: ChatMessage = {entity: ChatMessageEntity.USER, text: ''};
|
|
744
|
+
isLoading = false;
|
|
745
|
+
isReadOnly = false;
|
|
292
746
|
canShowFeedbackForm = false;
|
|
747
|
+
isLastMessage = false;
|
|
748
|
+
userInfo: Pick<Host.InspectorFrontendHostAPI.SyncInformation, 'accountImage'|'accountFullName'> = {};
|
|
749
|
+
markdownRenderer!: MarkdownLitRenderer;
|
|
750
|
+
onSuggestionClick: (suggestion: string) => void = () => {};
|
|
751
|
+
onFeedbackSubmit:
|
|
752
|
+
(rpcId: Host.AidaClient.RpcGlobalId, rate: Host.AidaClient.Rating, feedback?: string) => void = () => {};
|
|
753
|
+
onCopyResponseClick: (message: ModelChatMessage) => void = () => {};
|
|
293
754
|
|
|
294
755
|
#suggestionsResizeObserver = new ResizeObserver(() => this.#handleSuggestionsScrollOrResize());
|
|
295
756
|
#suggestionsEvaluateLayoutThrottler = new Common.Throttler.Throttler(50);
|
|
@@ -320,20 +781,36 @@ export class UserActionRow extends UI.Widget.Widget implements UserActionRowWidg
|
|
|
320
781
|
override performUpdate(): Promise<void>|void {
|
|
321
782
|
this.#view(
|
|
322
783
|
{
|
|
784
|
+
message: this.message,
|
|
785
|
+
isLoading: this.isLoading,
|
|
786
|
+
isReadOnly: this.isReadOnly,
|
|
787
|
+
canShowFeedbackForm: this.canShowFeedbackForm,
|
|
788
|
+
userInfo: this.userInfo,
|
|
789
|
+
markdownRenderer: this.markdownRenderer,
|
|
790
|
+
isLastMessage: this.isLastMessage,
|
|
323
791
|
onSuggestionClick: this.onSuggestionClick,
|
|
324
792
|
onRatingClick: this.#handleRateClick.bind(this),
|
|
325
793
|
onReportClick: () => UIHelpers.openInNewTab(REPORT_URL),
|
|
326
|
-
onCopyResponseClick:
|
|
794
|
+
onCopyResponseClick: () => {
|
|
795
|
+
if (this.message.entity === ChatMessageEntity.MODEL) {
|
|
796
|
+
this.onCopyResponseClick(this.message);
|
|
797
|
+
}
|
|
798
|
+
},
|
|
327
799
|
scrollSuggestionsScrollContainer: this.#scrollSuggestionsScrollContainer.bind(this),
|
|
328
800
|
onSuggestionsScrollOrResize: this.#handleSuggestionsScrollOrResize.bind(this),
|
|
329
801
|
onSubmit: this.#handleSubmit.bind(this),
|
|
330
802
|
onClose: this.#handleClose.bind(this),
|
|
331
803
|
onInputChange: this.#handleInputChange.bind(this),
|
|
332
804
|
isSubmitButtonDisabled: this.#isSubmitButtonDisabled,
|
|
333
|
-
|
|
334
|
-
|
|
805
|
+
// Props for actions logic
|
|
806
|
+
showRateButtons: this.message.entity === ChatMessageEntity.MODEL && !!this.message.rpcId,
|
|
807
|
+
suggestions: (this.message.entity === ChatMessageEntity.MODEL && !this.isReadOnly &&
|
|
808
|
+
this.message.parts.at(-1)?.type === 'answer') ?
|
|
809
|
+
(this.message.parts.at(-1) as AnswerPart).suggestions :
|
|
810
|
+
undefined,
|
|
335
811
|
currentRating: this.#currentRating,
|
|
336
812
|
isShowingFeedbackForm: this.#isShowingFeedbackForm,
|
|
813
|
+
onFeedbackSubmit: this.onFeedbackSubmit,
|
|
337
814
|
},
|
|
338
815
|
this.#viewOutput, this.contentElement);
|
|
339
816
|
}
|
|
@@ -395,14 +872,18 @@ export class UserActionRow extends UI.Widget.Widget implements UserActionRowWidg
|
|
|
395
872
|
this.#isShowingFeedbackForm = false;
|
|
396
873
|
this.#isSubmitButtonDisabled = true;
|
|
397
874
|
// This effectively reset the user rating
|
|
398
|
-
this.
|
|
875
|
+
if (this.message.entity === ChatMessageEntity.MODEL && this.message.rpcId) {
|
|
876
|
+
this.onFeedbackSubmit(this.message.rpcId, Host.AidaClient.Rating.SENTIMENT_UNSPECIFIED);
|
|
877
|
+
}
|
|
399
878
|
void this.performUpdate();
|
|
400
879
|
return;
|
|
401
880
|
}
|
|
402
881
|
|
|
403
882
|
this.#currentRating = rating;
|
|
404
883
|
this.#isShowingFeedbackForm = this.canShowFeedbackForm;
|
|
405
|
-
this.
|
|
884
|
+
if (this.message.entity === ChatMessageEntity.MODEL && this.message.rpcId) {
|
|
885
|
+
this.onFeedbackSubmit(this.message.rpcId, rating);
|
|
886
|
+
}
|
|
406
887
|
void this.performUpdate();
|
|
407
888
|
}
|
|
408
889
|
|
|
@@ -418,7 +899,9 @@ export class UserActionRow extends UI.Widget.Widget implements UserActionRowWidg
|
|
|
418
899
|
if (!this.#currentRating || !input) {
|
|
419
900
|
return;
|
|
420
901
|
}
|
|
421
|
-
this.
|
|
902
|
+
if (this.message.entity === ChatMessageEntity.MODEL && this.message.rpcId) {
|
|
903
|
+
this.onFeedbackSubmit(this.message.rpcId, this.#currentRating, input);
|
|
904
|
+
}
|
|
422
905
|
this.#isShowingFeedbackForm = false;
|
|
423
906
|
this.#isSubmitButtonDisabled = true;
|
|
424
907
|
void this.performUpdate();
|