chrome-devtools-frontend 1.0.1559913 → 1.0.1561080

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.
Files changed (130) hide show
  1. package/front_end/core/host/InspectorFrontendHostStub.ts +0 -3
  2. package/front_end/core/platform/ArrayUtilities.ts +13 -0
  3. package/front_end/core/root/Runtime.ts +0 -5
  4. package/front_end/core/sdk/DOMModel.ts +8 -0
  5. package/front_end/core/sdk/NetworkManager.ts +4 -0
  6. package/front_end/core/sdk/NetworkRequest.ts +9 -0
  7. package/front_end/core/sdk/OverlayModel.ts +20 -9
  8. package/front_end/entrypoints/main/MainImpl.ts +2 -1
  9. package/front_end/generated/InspectorBackendCommands.ts +4 -2
  10. package/front_end/generated/protocol-mapping.d.ts +7 -0
  11. package/front_end/generated/protocol-proxy-api.d.ts +5 -0
  12. package/front_end/generated/protocol.ts +24 -0
  13. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt +23 -22
  14. package/front_end/models/badges/UserBadges.ts +48 -16
  15. package/front_end/models/greendev/Prototypes.ts +6 -1
  16. package/front_end/models/trace/extras/TraceTree.ts +1 -1
  17. package/front_end/models/trace/handlers/PageLoadMetricsHandler.ts +8 -3
  18. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +11 -142
  19. package/front_end/panels/ai_assistance/PatchWidget.ts +90 -72
  20. package/front_end/panels/ai_assistance/ai_assistance.ts +1 -0
  21. package/front_end/panels/ai_assistance/components/ChatInput.ts +701 -0
  22. package/front_end/panels/ai_assistance/components/ChatView.ts +71 -1268
  23. package/front_end/panels/ai_assistance/components/UserActionRow.ts +514 -31
  24. package/front_end/panels/ai_assistance/components/chatInput.css +387 -0
  25. package/front_end/panels/ai_assistance/components/chatView.css +38 -599
  26. package/front_end/panels/ai_assistance/components/userActionRow.css +230 -0
  27. package/front_end/panels/autofill/AutofillView.ts +2 -2
  28. package/front_end/panels/changes/ChangesView.ts +15 -1
  29. package/front_end/panels/changes/changesView.css +6 -0
  30. package/front_end/panels/common/BadgeNotification.ts +44 -58
  31. package/front_end/panels/common/CopyChangesToPrompt.ts +233 -0
  32. package/front_end/panels/common/common.ts +1 -0
  33. package/front_end/panels/elements/ElementsTreeElement.ts +183 -359
  34. package/front_end/panels/elements/ElementsTreeOutline.ts +0 -6
  35. package/front_end/panels/elements/ShortcutTreeElement.ts +57 -50
  36. package/front_end/panels/elements/StylePropertiesSection.ts +1 -3
  37. package/front_end/panels/elements/components/AdornerManager.ts +5 -149
  38. package/front_end/panels/issues/HiddenIssuesRow.ts +1 -2
  39. package/front_end/panels/issues/IssueKindView.ts +2 -4
  40. package/front_end/panels/issues/IssueView.ts +2 -4
  41. package/front_end/panels/network/NetworkDataGridNode.ts +65 -1
  42. package/front_end/panels/network/NetworkLogView.ts +2 -4
  43. package/front_end/panels/network/NetworkLogViewColumns.ts +9 -0
  44. package/front_end/panels/screencast/ScreencastApp.ts +1 -0
  45. package/front_end/panels/settings/SettingsScreen.ts +3 -2
  46. package/front_end/panels/timeline/CompatibilityTracksAppender.ts +14 -1
  47. package/front_end/panels/timeline/ThirdPartyTreeView.ts +1 -4
  48. package/front_end/panels/timeline/TimelineController.ts +185 -3
  49. package/front_end/panels/timeline/TimelineFlameChartDataProvider.ts +52 -25
  50. package/front_end/panels/timeline/TimelineFlameChartView.ts +1 -0
  51. package/front_end/panels/timeline/TimelinePanel.ts +17 -104
  52. package/front_end/panels/timeline/TimelineTreeView.ts +1 -0
  53. package/front_end/panels/timeline/components/insights/BaseInsightComponent.ts +2 -2
  54. package/front_end/panels/timeline/components/insights/Table.ts +3 -3
  55. package/front_end/panels/whats_new/ReleaseNoteText.ts +15 -9
  56. package/front_end/panels/whats_new/resources/WNDT.md +6 -6
  57. package/front_end/third_party/chromium/README.chromium +1 -1
  58. package/front_end/third_party/codemirror.next/rebuild.sh +1 -1
  59. package/front_end/third_party/lit/rebuild.sh +1 -1
  60. package/front_end/third_party/puppeteer/README.chromium +2 -2
  61. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/api/Page.d.ts +2 -3
  62. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/api/Page.d.ts.map +1 -1
  63. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/api/Page.js.map +1 -1
  64. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/HTTPRequest.d.ts.map +1 -1
  65. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/HTTPRequest.js +9 -0
  66. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/HTTPRequest.js.map +1 -1
  67. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/HTTPResponse.d.ts +3 -0
  68. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/HTTPResponse.d.ts.map +1 -1
  69. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/HTTPResponse.js +9 -0
  70. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/HTTPResponse.js.map +1 -1
  71. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/core/Request.d.ts +3 -0
  72. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/core/Request.d.ts.map +1 -1
  73. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/core/Request.js +10 -0
  74. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/core/Request.js.map +1 -1
  75. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Browser.d.ts.map +1 -1
  76. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Browser.js +8 -4
  77. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Browser.js.map +1 -1
  78. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/injected.d.ts +1 -1
  79. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/injected.d.ts.map +1 -1
  80. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/injected.js +1 -1
  81. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/injected.js.map +1 -1
  82. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/injected/injected.d.ts +1 -1
  83. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.d.ts +3 -3
  84. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.js +3 -3
  85. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.js.map +1 -1
  86. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/Mutex.d.ts +2 -2
  87. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/version.d.ts +1 -1
  88. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/version.js +1 -1
  89. package/front_end/third_party/puppeteer/package/lib/es5-iife/puppeteer-core-browser.d.ts +10 -1
  90. package/front_end/third_party/puppeteer/package/lib/es5-iife/puppeteer-core-browser.js +13 -7
  91. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/api/Page.d.ts +2 -3
  92. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/api/Page.d.ts.map +1 -1
  93. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/api/Page.js.map +1 -1
  94. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/HTTPRequest.d.ts.map +1 -1
  95. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/HTTPRequest.js +9 -0
  96. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/HTTPRequest.js.map +1 -1
  97. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/HTTPResponse.d.ts +3 -0
  98. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/HTTPResponse.d.ts.map +1 -1
  99. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/HTTPResponse.js +9 -0
  100. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/HTTPResponse.js.map +1 -1
  101. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/core/Request.d.ts +3 -0
  102. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/core/Request.d.ts.map +1 -1
  103. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/core/Request.js +10 -0
  104. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/core/Request.js.map +1 -1
  105. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Browser.d.ts.map +1 -1
  106. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Browser.js +8 -4
  107. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Browser.js.map +1 -1
  108. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/injected.d.ts +1 -1
  109. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/injected.d.ts.map +1 -1
  110. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/injected.js +1 -1
  111. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/injected.js.map +1 -1
  112. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.d.ts +3 -3
  113. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.js +3 -3
  114. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.js.map +1 -1
  115. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/version.d.ts +1 -1
  116. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/version.js +1 -1
  117. package/front_end/third_party/puppeteer/package/lib/types.d.ts +10 -1
  118. package/front_end/third_party/puppeteer/package/package.json +3 -3
  119. package/front_end/third_party/puppeteer/package/src/api/Page.ts +2 -3
  120. package/front_end/third_party/puppeteer/package/src/bidi/HTTPRequest.ts +13 -0
  121. package/front_end/third_party/puppeteer/package/src/bidi/HTTPResponse.ts +10 -0
  122. package/front_end/third_party/puppeteer/package/src/bidi/core/Request.ts +15 -0
  123. package/front_end/third_party/puppeteer/package/src/cdp/Browser.ts +9 -4
  124. package/front_end/third_party/puppeteer/package/src/generated/injected.ts +1 -1
  125. package/front_end/third_party/puppeteer/package/src/revisions.ts +3 -3
  126. package/front_end/third_party/puppeteer/package/src/util/version.ts +1 -1
  127. package/front_end/ui/components/adorners/Adorner.ts +8 -68
  128. package/front_end/ui/legacy/TabbedPane.ts +1 -1
  129. package/front_end/ui/visual_logging/KnownContextValues.ts +3 -0
  130. package/package.json +2 -1
@@ -8,230 +8,35 @@ import '../../../ui/components/spinners/spinners.js';
8
8
 
9
9
  import * as Host from '../../../core/host/host.js';
10
10
  import * as i18n from '../../../core/i18n/i18n.js';
11
- import * as Platform from '../../../core/platform/platform.js';
12
- import * as SDK from '../../../core/sdk/sdk.js';
13
- import * as AiAssistanceModel from '../../../models/ai_assistance/ai_assistance.js';
14
- import * as GreenDev from '../../../models/greendev/greendev.js';
15
- import * as Trace from '../../../models/trace/trace.js';
16
- import * as Workspace from '../../../models/workspace/workspace.js';
17
- import * as PanelsCommon from '../../../panels/common/common.js';
18
- import * as PanelUtils from '../../../panels/utils/utils.js';
19
- import * as Marked from '../../../third_party/marked/marked.js';
11
+ import type * as Platform from '../../../core/platform/platform.js';
12
+ import type * as AiAssistanceModel from '../../../models/ai_assistance/ai_assistance.js';
20
13
  import * as Buttons from '../../../ui/components/buttons/buttons.js';
21
- import type * as MarkdownView from '../../../ui/components/markdown_view/markdown_view.js';
22
14
  import type {MarkdownLitRenderer} from '../../../ui/components/markdown_view/MarkdownView.js';
23
15
  import * as UI from '../../../ui/legacy/legacy.js';
24
16
  import * as Lit from '../../../ui/lit/lit.js';
25
- import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
26
17
  import {PatchWidget} from '../PatchWidget.js';
27
18
 
19
+ import {ChatInput} from './ChatInput.js';
28
20
  import chatViewStyles from './chatView.css.js';
29
- import {UserActionRow} from './UserActionRow.js';
21
+ import {type ChatMessage, type ModelChatMessage, UserActionRow} from './UserActionRow.js';
30
22
 
31
- const {html, Directives: {ifDefined, ref}} = Lit;
23
+ export {ChatInput, type ImageInputData} from './ChatInput.js';
32
24
 
33
- const UIStrings = {
34
- /**
35
- * @description The footer disclaimer that links to more information about the AI feature.
36
- */
37
- learnAbout: 'Learn about AI in DevTools',
38
-
39
- /**
40
- * @description Label added to the text input to describe the context for screen readers. Not shown visibly on screen.
41
- */
42
- inputTextAriaDescription: 'You can also use one of the suggested prompts above to start your conversation',
43
- /**
44
- * @description Label added to the button that reveals the selected context item in DevTools
45
- */
46
- revealContextDescription: 'Reveal the selected context item in DevTools',
47
- } as const;
25
+ const {html, Directives: {ref, repeat, createRef}} = Lit;
48
26
 
49
27
  /*
50
28
  * Strings that don't need to be translated at this time.
51
29
  */
52
30
  const UIStringsNotTranslate = {
53
- /**
54
- * @description Title for the send icon button.
55
- */
56
- sendButtonTitle: 'Send',
57
- /**
58
- * @description Title for the start new chat
59
- */
60
- startNewChat: 'Start new chat',
61
- /**
62
- * @description Title for the cancel icon button.
63
- */
64
- cancelButtonTitle: 'Cancel',
65
- /**
66
- * @description Label for the "select an element" button.
67
- */
68
- selectAnElement: 'Select an element',
69
- /**
70
- * @description Label for the "select an element" button.
71
- */
72
- noElementSelected: 'No element selected',
73
31
  /**
74
32
  * @description Text for the empty state of the AI assistance panel.
75
33
  */
76
34
  emptyStateText: 'How can I help you?',
77
- /**
78
- * @description The error message when the request to the LLM failed for some reason.
79
- */
80
- systemError:
81
- '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.',
82
- /**
83
- * @description The error message when the LLM gets stuck in a loop (max steps reached).
84
- */
85
- maxStepsError: 'Seems like I am stuck with the investigation. It would be better if you start over.',
86
- /**
87
- * @description Displayed when the user stop the response
88
- */
89
- stoppedResponse: 'You stopped this response',
90
- /**
91
- * @description Prompt for user to confirm code execution that may affect the page.
92
- */
93
- sideEffectConfirmationDescription: 'This code may modify page content. Continue?',
94
- /**
95
- * @description Button text that confirm code execution that may affect the page.
96
- */
97
- positiveSideEffectConfirmation: 'Continue',
98
- /**
99
- * @description Button text that cancels code execution that may affect the page.
100
- */
101
- negativeSideEffectConfirmation: 'Cancel',
102
- /**
103
- * @description The generic name of the AI agent (do not translate)
104
- */
105
- ai: 'AI',
106
- /**
107
- * @description The fallback text when we can't find the user full name
108
- */
109
- you: 'You',
110
- /**
111
- * @description The fallback text when a step has no title yet
112
- */
113
- investigating: 'Investigating',
114
- /**
115
- * @description Prefix to the title of each thinking step of a user action is required to continue
116
- */
117
- paused: 'Paused',
118
- /**
119
- * @description Heading text for the code block that shows the executed code.
120
- */
121
- codeExecuted: 'Code executed',
122
- /**
123
- * @description Heading text for the code block that shows the code to be executed after side effect confirmation.
124
- */
125
- codeToExecute: 'Code to execute',
126
- /**
127
- * @description Heading text for the code block that shows the returned data.
128
- */
129
- dataReturned: 'Data returned',
130
- /**
131
- * @description Aria label for the check mark icon to be read by screen reader
132
- */
133
- completed: 'Completed',
134
- /**
135
- * @description Aria label for the cancel icon to be read by screen reader
136
- */
137
- canceled: 'Canceled',
138
- /**
139
- * @description Text displayed when the chat input is disabled due to reading past conversation.
140
- */
141
- pastConversation: 'You\'re viewing a past conversation.',
142
- /**
143
- * @description Title for the take screenshot button.
144
- */
145
- takeScreenshotButtonTitle: 'Take screenshot',
146
- /**
147
- * @description Title for the remove image input button.
148
- */
149
- removeImageInputButtonTitle: 'Remove image input',
150
- /**
151
- * @description Alt text for the image input (displayed in the chat messages) that has been sent to the model.
152
- */
153
- imageInputSentToTheModel: 'Image input sent to the model',
154
- /**
155
- * @description Alt text for the account avatar.
156
- */
157
- accountAvatar: 'Account avatar',
158
- /**
159
- * @description Title for the x-link which wraps the image input rendered in chat messages.
160
- */
161
- openImageInNewTab: 'Open image in a new tab',
162
- /**
163
- * @description Alt text for image when it is not available.
164
- */
165
- imageUnavailable: 'Image unavailable',
166
- /**
167
- * @description Title for the add image button.
168
- */
169
- addImageButtonTitle: 'Add image',
170
35
  } as const;
171
36
 
172
- const str_ = i18n.i18n.registerUIStrings('panels/ai_assistance/components/ChatView.ts', UIStrings);
173
- const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
174
37
  const lockedString = i18n.i18n.lockedString;
175
38
 
176
39
  const SCROLL_ROUNDING_OFFSET = 1;
177
- const RELEVANT_DATA_LINK_FOOTER_ID = 'relevant-data-link-footer';
178
- const RELEVANT_DATA_LINK_CHAT_ID = 'relevant-data-link-chat';
179
-
180
- export interface Step {
181
- isLoading: boolean;
182
- thought?: string;
183
- title?: string;
184
- code?: string;
185
- output?: string;
186
- canceled?: boolean;
187
- sideEffect?: ConfirmSideEffectDialog;
188
- contextDetails?: [AiAssistanceModel.AiAgent.ContextDetail, ...AiAssistanceModel.AiAgent.ContextDetail[]];
189
- }
190
-
191
- interface ConfirmSideEffectDialog {
192
- onAnswer: (result: boolean) => void;
193
- }
194
-
195
- export const enum ChatMessageEntity {
196
- MODEL = 'model',
197
- USER = 'user',
198
- }
199
-
200
- export type ImageInputData = {
201
- isLoading: true,
202
- }|{
203
- isLoading: false,
204
- data: string,
205
- mimeType: string,
206
- inputType: AiAssistanceModel.AiAgent.MultimodalInputType,
207
- };
208
-
209
- export interface AnswerPart {
210
- type: 'answer';
211
- text: string;
212
- suggestions?: [string, ...string[]];
213
- }
214
-
215
- export interface StepPart {
216
- type: 'step';
217
- step: Step;
218
- }
219
-
220
- export type ModelMessagePart = AnswerPart|StepPart;
221
-
222
- export interface UserChatMessage {
223
- entity: ChatMessageEntity.USER;
224
- text: string;
225
- imageInput?: Host.AidaClient.Part;
226
- }
227
- export interface ModelChatMessage {
228
- entity: ChatMessageEntity.MODEL;
229
- parts: ModelMessagePart[];
230
- error?: AiAssistanceModel.AiAgent.ErrorType;
231
- rpcId?: Host.AidaClient.RpcGlobalId;
232
- }
233
-
234
- export type ChatMessage = UserChatMessage|ModelChatMessage;
235
40
 
236
41
  export interface Props {
237
42
  onTextSubmit:
@@ -243,10 +48,6 @@ export interface Props {
243
48
  onContextClick: () => void;
244
49
  onNewConversation: () => void;
245
50
  onCopyResponseClick: (message: ModelChatMessage) => void;
246
- onTakeScreenshot?: () => void;
247
- onRemoveImageInput?: () => void;
248
- onTextInputChange: (input: string) => void;
249
- onLoadImage?: (file: File) => Promise<void>;
250
51
  changeManager: AiAssistanceModel.ChangeManager.ChangeManager;
251
52
  inspectElementToggled: boolean;
252
53
  messages: ChatMessage[];
@@ -259,12 +60,10 @@ export interface Props {
259
60
  blockedByCrossOrigin: boolean;
260
61
  changeSummary?: string;
261
62
  multimodalInputEnabled?: boolean;
262
- imageInput?: ImageInputData;
263
63
  isTextInputDisabled: boolean;
264
64
  emptyStateSuggestions: AiAssistanceModel.AiAgent.ConversationSuggestion[];
265
65
  inputPlaceholder: Platform.UIString.LocalizedString;
266
66
  disclaimerText: Platform.UIString.LocalizedString;
267
- isTextInputEmpty: boolean;
268
67
  isArtifactsSidebarOpen: boolean;
269
68
  uploadImageInputEnabled?: boolean;
270
69
  markdownRenderer: MarkdownLitRenderer;
@@ -276,7 +75,7 @@ export class ChatView extends HTMLElement {
276
75
  #scrollTop?: number;
277
76
  #props: Props;
278
77
  #messagesContainerElement?: Element;
279
- #mainElementRef?: Lit.Directives.Ref<Element> = Lit.Directives.createRef();
78
+ #mainElementRef = createRef<HTMLElement>();
280
79
  #messagesContainerResizeObserver = new ResizeObserver(() => this.#handleMessagesContainerResize());
281
80
  /**
282
81
  * Indicates whether the chat scroll position should be pinned to the bottom.
@@ -295,6 +94,7 @@ export class ChatView extends HTMLElement {
295
94
  * whether to pin the content to the bottom.
296
95
  */
297
96
  #isProgrammaticScroll = false;
97
+ #inputRef = createRef<UI.Widget.WidgetElement<ChatInput>>();
298
98
 
299
99
  constructor(props: Props) {
300
100
  super();
@@ -371,16 +171,6 @@ export class ChatView extends HTMLElement {
371
171
  this.#mainElementRef.value.scrollTop = scrollTop;
372
172
  }
373
173
 
374
- #setInputText(text: string): void {
375
- const textArea = this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement;
376
- if (!textArea) {
377
- return;
378
- }
379
-
380
- textArea.value = text;
381
- this.#props.onTextInputChange(text);
382
- }
383
-
384
174
  #handleMessageContainerRef(el: Element|undefined): void {
385
175
  this.#messagesContainerElement = el;
386
176
 
@@ -410,121 +200,19 @@ export class ChatView extends HTMLElement {
410
200
  ev.target.scrollTop + ev.target.clientHeight + SCROLL_ROUNDING_OFFSET > ev.target.scrollHeight;
411
201
  };
412
202
 
413
- #handleSubmit = (ev: SubmitEvent): void => {
414
- ev.preventDefault();
415
- if (this.#props.imageInput?.isLoading) {
416
- return;
417
- }
418
-
419
- const textArea = this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement;
420
- if (!textArea?.value) {
421
- return;
422
- }
423
- const imageInput = !this.#props.imageInput?.isLoading && this.#props.imageInput?.data ?
424
- {inlineData: {data: this.#props.imageInput.data, mimeType: this.#props.imageInput.mimeType}} :
425
- undefined;
426
- void this.#props.onTextSubmit(textArea.value, imageInput, this.#props.imageInput?.inputType);
427
- textArea.value = '';
428
- };
429
-
430
- #handleTextAreaKeyDown = (ev: KeyboardEvent): void => {
431
- if (!ev.target || !(ev.target instanceof HTMLTextAreaElement)) {
432
- return;
433
- }
434
-
435
- // Go to a new line on Shift+Enter. On Enter, submit unless the
436
- // user is in IME composition.
437
- if (ev.key === 'Enter' && !ev.shiftKey && !ev.isComposing) {
438
- ev.preventDefault();
439
- if (!ev.target?.value || this.#props.imageInput?.isLoading) {
440
- return;
441
- }
442
- const imageInput = !this.#props.imageInput?.isLoading && this.#props.imageInput?.data ?
443
- {inlineData: {data: this.#props.imageInput.data, mimeType: this.#props.imageInput.mimeType}} :
444
- undefined;
445
- void this.#props.onTextSubmit(ev.target.value, imageInput, this.#props.imageInput?.inputType);
446
- ev.target.value = '';
447
- }
448
- };
449
-
450
- #handleCancel = (ev: SubmitEvent): void => {
451
- ev.preventDefault();
452
-
453
- if (!this.#props.isLoading) {
454
- return;
455
- }
456
-
457
- this.#props.onCancelClick();
458
- };
459
-
460
- #handleImageUpload = (ev: Event): void => {
461
- ev.stopPropagation();
462
- if (this.#props.onLoadImage) {
463
- const fileSelector = UI.UIUtils.createFileSelectorElement(this.#props.onLoadImage.bind(this), '.jpeg,.jpg,.png');
464
- fileSelector.click();
465
- }
466
- };
467
-
468
203
  #handleSuggestionClick = (suggestion: string): void => {
469
- this.#setInputText(suggestion);
204
+ this.#inputRef.value?.getWidget()?.setInputValue(suggestion);
205
+ this.#render();
470
206
  this.focusTextInput();
471
207
  Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceDynamicSuggestionClicked);
472
208
  };
473
209
 
474
210
  #render(): void {
475
- const renderFooter = (): Lit.LitTemplate => {
476
- const classes = Lit.Directives.classMap({
477
- 'chat-view-footer': true,
478
- 'is-read-only': this.#props.isReadOnly,
479
- });
480
211
 
481
- // clang-format off
482
- return html`
483
- <footer class=${classes} jslog=${VisualLogging.section('footer')}>
484
- ${renderRelevantDataDisclaimer({
485
- isLoading: this.#props.isLoading,
486
- blockedByCrossOrigin: this.#props.blockedByCrossOrigin,
487
- tooltipId: RELEVANT_DATA_LINK_FOOTER_ID,
488
- disclaimerText: this.#props.disclaimerText
489
- })}
490
- </footer>
491
- `;
492
- // clang-format on
493
- };
494
-
495
- const renderInputOrReadOnlySection = (): Lit.LitTemplate => {
496
- if (this.#props.isReadOnly) {
497
- return renderReadOnlySection({
498
- onNewConversation: this.#props.onNewConversation,
499
- });
500
- }
501
-
502
- return renderChatInput({
503
- isLoading: this.#props.isLoading,
504
- blockedByCrossOrigin: this.#props.blockedByCrossOrigin,
505
- isTextInputDisabled: this.#props.isTextInputDisabled,
506
- inputPlaceholder: this.#props.inputPlaceholder,
507
- disclaimerText: this.#props.disclaimerText,
508
- selectedContext: this.#props.selectedContext,
509
- inspectElementToggled: this.#props.inspectElementToggled,
510
- multimodalInputEnabled: this.#props.multimodalInputEnabled,
511
- conversationType: this.#props.conversationType,
512
- imageInput: this.#props.imageInput,
513
- isTextInputEmpty: this.#props.isTextInputEmpty,
514
- uploadImageInputEnabled: this.#props.uploadImageInputEnabled,
515
- onContextClick: this.#props.onContextClick,
516
- onInspectElementClick: this.#props.onInspectElementClick,
517
- onSubmit: this.#handleSubmit,
518
- onTextAreaKeyDown: this.#handleTextAreaKeyDown,
519
- onCancel: this.#handleCancel,
520
- onNewConversation: this.#props.onNewConversation,
521
- onTakeScreenshot: this.#props.onTakeScreenshot,
522
- onRemoveImageInput: this.#props.onRemoveImageInput,
523
- onTextInputChange: this.#props.onTextInputChange,
524
- onImageUpload: this.#handleImageUpload,
525
- additionalFloatyContext: this.#props.additionalFloatyContext,
526
- });
527
- };
212
+ const inputWidgetClasses = Lit.Directives.classMap({
213
+ 'chat-input-widget': true,
214
+ sticky: !this.#props.isReadOnly,
215
+ });
528
216
 
529
217
  // clang-format off
530
218
  Lit.render(html`
@@ -547,474 +235,86 @@ export class ChatView extends HTMLElement {
547
235
  onMessageContainerRef: this.#handleMessageContainerRef,
548
236
  onCopyResponseClick: this.#props.onCopyResponseClick,
549
237
  })}
550
- ${renderInputOrReadOnlySection()}
238
+ <devtools-widget class=${inputWidgetClasses} .widgetConfig=${UI.Widget.widgetConfig(ChatInput, {
239
+ isLoading: this.#props.isLoading,
240
+ blockedByCrossOrigin: this.#props.blockedByCrossOrigin,
241
+ isTextInputDisabled: this.#props.isTextInputDisabled,
242
+ inputPlaceholder: this.#props.inputPlaceholder,
243
+ disclaimerText: this.#props.disclaimerText,
244
+ selectedContext: this.#props.selectedContext,
245
+ inspectElementToggled: this.#props.inspectElementToggled,
246
+ multimodalInputEnabled: this.#props.multimodalInputEnabled ?? false,
247
+ conversationType: this.#props.conversationType,
248
+ uploadImageInputEnabled: this.#props.uploadImageInputEnabled ?? false,
249
+ isReadOnly: this.#props.isReadOnly,
250
+ additionalFloatyContext: this.#props.additionalFloatyContext,
251
+ onContextClick: this.#props.onContextClick,
252
+ onInspectElementClick: this.#props.onInspectElementClick,
253
+ onTextSubmit: (
254
+ text: string, imageInput?: Host.AidaClient.Part,
255
+ multimodalInputType?: AiAssistanceModel.AiAgent.MultimodalInputType) => {
256
+ this.#props.onTextSubmit(text, imageInput, multimodalInputType);
257
+ this.#render();
258
+ },
259
+ onCancelClick: this.#props.onCancelClick,
260
+ onNewConversation: this.#props.onNewConversation,
261
+ })} ${ref(this.#inputRef)}></devtools-widget>
551
262
  </main>
552
- ${renderFooter()}
553
263
  </div>
554
264
  `, this.#shadow, {host: this});
555
265
  // clang-format on
556
266
  }
557
267
  }
558
268
 
559
- function renderTextAsMarkdown(text: string, markdownRenderer: MarkdownLitRenderer, {animate, ref: refFn}: {
560
- animate?: boolean,
561
- ref?: (element?: Element) => void,
562
- } = {}): Lit.TemplateResult {
563
- let tokens = [];
564
- try {
565
- tokens = Marked.Marked.lexer(text);
566
- for (const token of tokens) {
567
- // Try to render all the tokens to make sure that
568
- // they all have a template defined for them. If there
569
- // isn't any template defined for a token, we'll fallback
570
- // to rendering the text as plain text instead of markdown.
571
- markdownRenderer.renderToken(token);
572
- }
573
- } catch {
574
- // The tokens were not parsed correctly or
575
- // one of the tokens are not supported, so we
576
- // continue to render this as text.
577
- return html`${text}`;
578
- }
579
-
580
- // clang-format off
581
- return html`<devtools-markdown-view
582
- .data=${{tokens, renderer: markdownRenderer, animationEnabled: animate} as MarkdownView.MarkdownView.MarkdownViewData}
583
- ${refFn ? ref(refFn) : Lit.nothing}>
584
- </devtools-markdown-view>`;
585
- // clang-format on
586
- }
587
-
588
- function renderTitle(step: Step): Lit.LitTemplate {
589
- const paused =
590
- step.sideEffect ? html`<span class="paused">${lockedString(UIStringsNotTranslate.paused)}: </span>` : Lit.nothing;
591
- const actionTitle = step.title ?? `${lockedString(UIStringsNotTranslate.investigating)}…`;
592
-
593
- return html`<span class="title">${paused}${actionTitle}</span>`;
594
- }
595
-
596
- function renderStepCode(step: Step): Lit.LitTemplate {
597
- if (!step.code && !step.output) {
598
- return Lit.nothing;
599
- }
600
-
601
- // 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)
602
- // thus we show "Code to execute" text rather than "Code executed" text on the heading of the code block.
603
- const codeHeadingText = (step.output && !step.canceled) ? lockedString(UIStringsNotTranslate.codeExecuted) :
604
- lockedString(UIStringsNotTranslate.codeToExecute);
605
-
606
- // If there is output, we don't show notice on this code block and instead show
607
- // it in the data returned code block.
608
- // clang-format off
609
- const code = step.code ? html`<div class="action-result">
610
- <devtools-code-block
611
- .code=${step.code.trim()}
612
- .codeLang=${'js'}
613
- .displayNotice=${!Boolean(step.output)}
614
- .header=${codeHeadingText}
615
- .showCopyButton=${true}
616
- ></devtools-code-block>
617
- </div>` :
618
- Lit.nothing;
619
- const output = step.output ? html`<div class="js-code-output">
620
- <devtools-code-block
621
- .code=${step.output}
622
- .codeLang=${'js'}
623
- .displayNotice=${true}
624
- .header=${lockedString(UIStringsNotTranslate.dataReturned)}
625
- .showCopyButton=${false}
626
- ></devtools-code-block>
627
- </div>` :
628
- Lit.nothing;
629
-
630
- return html`<div class="step-code">${code}${output}</div>`;
631
- // clang-format on
632
- }
633
-
634
- function renderStepDetails({
635
- step,
636
- markdownRenderer,
637
- isLast,
638
- }: {
639
- step: Step,
640
- markdownRenderer: MarkdownLitRenderer,
641
- isLast: boolean,
642
- }): Lit.LitTemplate {
643
- const sideEffects = isLast && step.sideEffect ? renderSideEffectConfirmationUi(step) : Lit.nothing;
644
- const thought = step.thought ? html`<p>${renderTextAsMarkdown(step.thought, markdownRenderer)}</p>` : Lit.nothing;
645
-
646
- // clang-format off
647
- const contextDetails = step.contextDetails ?
648
- html`${Lit.Directives.repeat(
649
- step.contextDetails,
650
- contextDetail => {
651
- return html`<div class="context-details">
652
- <devtools-code-block
653
- .code=${contextDetail.text}
654
- .codeLang=${contextDetail.codeLang || ''}
655
- .displayNotice=${false}
656
- .header=${contextDetail.title}
657
- .showCopyButton=${true}
658
- ></devtools-code-block>
659
- </div>`;
660
- },
661
- )}` : Lit.nothing;
662
-
663
- return html`<div class="step-details">
664
- ${thought}
665
- ${renderStepCode(step)}
666
- ${sideEffects}
667
- ${contextDetails}
668
- </div>`;
669
- // clang-format on
670
- }
671
-
672
- function renderStepBadge({step, isLoading, isLast}: {
673
- step: Step,
674
- isLoading: boolean,
675
- isLast: boolean,
676
- }): Lit.LitTemplate {
677
- if (isLoading && isLast && !step.sideEffect) {
678
- return html`<devtools-spinner></devtools-spinner>`;
679
- }
680
-
681
- let iconName = 'checkmark';
682
- let ariaLabel: string|undefined = lockedString(UIStringsNotTranslate.completed);
683
- let role: 'button'|undefined = 'button';
684
- if (isLast && step.sideEffect) {
685
- role = undefined;
686
- ariaLabel = undefined;
687
- iconName = 'pause-circle';
688
- } else if (step.canceled) {
689
- ariaLabel = lockedString(UIStringsNotTranslate.canceled);
690
- iconName = 'cross';
691
- }
692
-
693
- return html`<devtools-icon
694
- class="indicator"
695
- role=${ifDefined(role)}
696
- aria-label=${ifDefined(ariaLabel)}
697
- .name=${iconName}
698
- ></devtools-icon>`;
699
- }
700
-
701
- function renderStep({step, isLoading, markdownRenderer, isLast}: {
702
- step: Step,
703
- isLoading: boolean,
704
- markdownRenderer: MarkdownLitRenderer,
705
- isLast: boolean,
706
- }): Lit.LitTemplate {
707
- const stepClasses = Lit.Directives.classMap({
708
- step: true,
709
- empty: !step.thought && !step.code && !step.contextDetails && !step.sideEffect,
710
- paused: Boolean(step.sideEffect),
711
- canceled: Boolean(step.canceled),
712
- });
713
- // clang-format off
714
- return html`
715
- <details class=${stepClasses}
716
- jslog=${VisualLogging.section('step')}
717
- .open=${Boolean(step.sideEffect)}>
718
- <summary>
719
- <div class="summary">
720
- ${renderStepBadge({ step, isLoading, isLast })}
721
- ${renderTitle(step)}
722
- <devtools-icon
723
- class="arrow"
724
- name="chevron-down"
725
- ></devtools-icon>
726
- </div>
727
- </summary>
728
- ${renderStepDetails({step, markdownRenderer, isLast})}
729
- </details>`;
730
- // clang-format on
731
- }
732
-
733
- function renderSideEffectConfirmationUi(step: Step): Lit.LitTemplate {
734
- if (!step.sideEffect) {
735
- return Lit.nothing;
736
- }
737
-
738
- // clang-format off
739
- return html`<div
740
- class="side-effect-confirmation"
741
- jslog=${VisualLogging.section('side-effect-confirmation')}
742
- >
743
- <p>${lockedString(UIStringsNotTranslate.sideEffectConfirmationDescription)}</p>
744
- <div class="side-effect-buttons-container">
745
- <devtools-button
746
- .data=${
747
- {
748
- variant: Buttons.Button.Variant.OUTLINED,
749
- jslogContext: 'decline-execute-code',
750
- } as Buttons.Button.ButtonData
751
- }
752
- @click=${() => step.sideEffect?.onAnswer(false)}
753
- >${lockedString(
754
- UIStringsNotTranslate.negativeSideEffectConfirmation,
755
- )}</devtools-button>
756
- <devtools-button
757
- .data=${
758
- {
759
- variant: Buttons.Button.Variant.PRIMARY,
760
- jslogContext: 'accept-execute-code',
761
- iconName: 'play',
762
- } as Buttons.Button.ButtonData
763
- }
764
- @click=${() => step.sideEffect?.onAnswer(true)}
765
- >${
766
- lockedString(UIStringsNotTranslate.positiveSideEffectConfirmation)
767
- }</devtools-button>
768
- </div>
769
- </div>`;
770
- // clang-format on
771
- }
772
-
773
- function renderError(message: ModelChatMessage): Lit.LitTemplate {
774
- if (message.error) {
775
- let errorMessage;
776
- switch (message.error) {
777
- case AiAssistanceModel.AiAgent.ErrorType.UNKNOWN:
778
- case AiAssistanceModel.AiAgent.ErrorType.BLOCK:
779
- errorMessage = UIStringsNotTranslate.systemError;
780
- break;
781
- case AiAssistanceModel.AiAgent.ErrorType.MAX_STEPS:
782
- errorMessage = UIStringsNotTranslate.maxStepsError;
783
- break;
784
- case AiAssistanceModel.AiAgent.ErrorType.ABORT:
785
- return html`<p class="aborted" jslog=${VisualLogging.section('aborted')}>${
786
- lockedString(UIStringsNotTranslate.stoppedResponse)}</p>`;
787
- }
788
-
789
- return html`<p class="error" jslog=${VisualLogging.section('error')}>${lockedString(errorMessage)}</p>`;
790
- }
791
-
792
- return Lit.nothing;
793
- }
794
-
795
- function renderChatMessage({
796
- message,
269
+ function renderMainContents({
270
+ messages,
797
271
  isLoading,
798
272
  isReadOnly,
799
273
  canShowFeedbackForm,
800
- isLast,
274
+ isTextInputDisabled,
275
+ suggestions,
801
276
  userInfo,
802
277
  markdownRenderer,
278
+ changeSummary,
279
+ changeManager,
803
280
  onSuggestionClick,
804
281
  onFeedbackSubmit,
805
282
  onCopyResponseClick,
283
+ onMessageContainerRef,
806
284
  }: {
807
- message: ChatMessage,
285
+ messages: ChatMessage[],
808
286
  isLoading: boolean,
809
287
  isReadOnly: boolean,
810
288
  canShowFeedbackForm: boolean,
811
- isLast: boolean,
289
+ isTextInputDisabled: boolean,
290
+ suggestions: AiAssistanceModel.AiAgent.ConversationSuggestion[],
812
291
  userInfo: Pick<Host.InspectorFrontendHostAPI.SyncInformation, 'accountImage'|'accountFullName'>,
813
292
  markdownRenderer: MarkdownLitRenderer,
293
+ changeManager: AiAssistanceModel.ChangeManager.ChangeManager,
814
294
  onSuggestionClick: (suggestion: string) => void,
815
295
  onFeedbackSubmit: (rpcId: Host.AidaClient.RpcGlobalId, rate: Host.AidaClient.Rating, feedback?: string) => void,
816
296
  onCopyResponseClick: (message: ModelChatMessage) => void,
817
- }): Lit.TemplateResult {
818
- if (message.entity === ChatMessageEntity.USER) {
819
- const name = userInfo.accountFullName || lockedString(UIStringsNotTranslate.you);
820
- const image = userInfo.accountImage ?
821
- html`<img src="data:image/png;base64, ${userInfo.accountImage}" alt=${UIStringsNotTranslate.accountAvatar} />` :
822
- html`<devtools-icon
823
- name="profile"
824
- ></devtools-icon>`;
825
- const imageInput = message.imageInput && 'inlineData' in message.imageInput ?
826
- renderImageChatMessage(message.imageInput.inlineData) :
827
- Lit.nothing;
828
- // clang-format off
829
- return html`<section
830
- class="chat-message query"
831
- jslog=${VisualLogging.section('question')}
832
- >
833
- <div class="message-info">
834
- ${image}
835
- <div class="message-name">
836
- <h2>${name}</h2>
837
- </div>
838
- </div>
839
- ${imageInput}
840
- <div class="message-content">${renderTextAsMarkdown(message.text, markdownRenderer)}</div>
841
- </section>`;
842
- // clang-format on
843
- }
844
-
845
- // clang-format off
846
- return html`
847
- <section
848
- class="chat-message answer"
849
- jslog=${VisualLogging.section('answer')}
850
- >
851
- <div class="message-info">
852
- <devtools-icon name="smart-assistant"></devtools-icon>
853
- <div class="message-name">
854
- <h2>${lockedString(UIStringsNotTranslate.ai)}</h2>
855
- </div>
856
- </div>
857
- ${Lit.Directives.repeat(
858
- message.parts,
859
- (_, index) => index,
860
- (part, index) => {
861
- const isLastPart = index === message.parts.length - 1;
862
- if (part.type === 'answer') {
863
- return html`<p>${renderTextAsMarkdown(part.text, markdownRenderer, { animate: !isReadOnly && isLoading && isLast && isLastPart })}</p>`;
864
- }
865
- return renderStep({
866
- step: part.step,
867
- isLoading,
868
- markdownRenderer,
869
- isLast: isLastPart && isLast,
870
- });
871
- },
872
- )}
873
- ${renderError(message)}
874
- ${isLast && isLoading
875
- ? Lit.nothing
876
- : html`<devtools-widget class="actions" .widgetConfig=${UI.Widget.widgetConfig(UserActionRow, {
877
- showRateButtons: message.rpcId !== undefined,
878
- onFeedbackSubmit: (rating: Host.AidaClient.Rating, feedback?: string) => {
879
- if (!message.rpcId) {
880
- return;
881
- }
882
- onFeedbackSubmit(message.rpcId, rating, feedback);
883
- },
884
- suggestions: (isLast && !isReadOnly && message.parts.at(-1)?.type === 'answer') ? (message.parts.at(-1) as AnswerPart).suggestions : undefined,
885
- onSuggestionClick,
886
- onCopyResponseClick: () => onCopyResponseClick(message),
887
- canShowFeedbackForm,
888
- })}></devtools-widget>`
889
- }
890
- </section>
891
- `;
892
- // clang-format on
893
- }
894
-
895
- function renderImageChatMessage(inlineData: Host.AidaClient.MediaBlob): Lit.LitTemplate {
896
- if (inlineData.data === AiAssistanceModel.AiConversation.NOT_FOUND_IMAGE_DATA) {
897
- // clang-format off
898
- return html`<div class="unavailable-image" title=${UIStringsNotTranslate.imageUnavailable}>
899
- <devtools-icon name='file-image'></devtools-icon>
900
- </div>`;
901
- // clang-format on
902
- }
903
- const imageUrl = `data:${inlineData.mimeType};base64,${inlineData.data}`;
904
- // clang-format off
905
- return html`<x-link
906
- class="image-link" title=${UIStringsNotTranslate.openImageInNewTab}
907
- href=${imageUrl}
908
- >
909
- <img src=${imageUrl} alt=${UIStringsNotTranslate.imageInputSentToTheModel} />
910
- </x-link>`;
911
- // clang-format on
912
- }
913
-
914
- function renderContextIcon(context: AiAssistanceModel.AiAgent.ConversationContext<unknown>|null): Lit.LitTemplate {
915
- if (!context) {
916
- return Lit.nothing;
917
- }
918
- const item = context.getItem();
919
- // FIXME: move this to presenter once PanelUtils are declarative. The instance
920
- // checking should be in the presenter and the rendering in the view function.
921
- if (item instanceof SDK.NetworkRequest.NetworkRequest) {
922
- return PanelUtils.PanelUtils.getIconForNetworkRequest(item);
923
- }
924
- if (item instanceof Workspace.UISourceCode.UISourceCode) {
925
- return PanelUtils.PanelUtils.getIconForSourceFile(item);
926
- }
927
- if (item instanceof AiAssistanceModel.AIContext.AgentFocus) {
928
- return html`<devtools-icon name="performance" title="Performance"></devtools-icon>`;
929
- }
930
- if (item instanceof SDK.DOMModel.DOMNode) {
931
- return Lit.nothing;
932
- }
933
- return Lit.nothing;
934
- }
935
-
936
- function renderContextTitle(
937
- context: AiAssistanceModel.AiAgent.ConversationContext<unknown>, disabled: boolean): Lit.TemplateResult|string {
938
- const item = context.getItem();
939
- if (item instanceof SDK.DOMModel.DOMNode) {
940
- // FIXME: move this to the model code.
941
- const hiddenClassList = item.classNames().filter(
942
- className => className.startsWith(AiAssistanceModel.Injected.AI_ASSISTANCE_CSS_CLASS_NAME));
943
- return html`<devtools-widget .widgetConfig=${UI.Widget.widgetConfig(PanelsCommon.DOMLinkifier.DOMNodeLink, {
944
- node: item,
945
- options: {hiddenClassList, disabled}
946
- })}></devtools-widget>`;
947
- }
948
- return context.getTitle();
949
- }
950
-
951
- function renderSelection({
952
- selectedContext,
953
- inspectElementToggled,
954
- conversationType,
955
- isTextInputDisabled,
956
- onContextClick,
957
- onInspectElementClick,
958
- }: {
959
- selectedContext: AiAssistanceModel.AiAgent.ConversationContext<unknown>|null,
960
- inspectElementToggled: boolean,
961
- isTextInputDisabled: boolean,
962
- onContextClick: () => void | Promise<void>,
963
- onInspectElementClick: () => void,
964
- conversationType: AiAssistanceModel.AiHistoryStorage.ConversationType,
297
+ onMessageContainerRef: (el: Element|undefined) => void,
298
+ changeSummary?: string,
965
299
  }): Lit.LitTemplate {
966
- if (!selectedContext) {
967
- return Lit.nothing;
300
+ if (messages.length > 0) {
301
+ return renderMessages({
302
+ messages,
303
+ isLoading,
304
+ isReadOnly,
305
+ canShowFeedbackForm,
306
+ userInfo,
307
+ markdownRenderer,
308
+ changeSummary,
309
+ changeManager,
310
+ onSuggestionClick,
311
+ onFeedbackSubmit,
312
+ onMessageContainerRef,
313
+ onCopyResponseClick
314
+ });
968
315
  }
969
- // TODO: currently the picker behavior is SDKNode specific.
970
- const hasPickerBehavior = conversationType === AiAssistanceModel.AiHistoryStorage.ConversationType.STYLING;
971
316
 
972
- const resourceClass = Lit.Directives.classMap({
973
- 'not-selected': !selectedContext,
974
- 'resource-link': true,
975
- 'has-picker-behavior': hasPickerBehavior,
976
- disabled: isTextInputDisabled,
977
- });
978
-
979
- const handleKeyDown = (ev: KeyboardEvent): void => {
980
- if (ev.key === 'Enter' || ev.key === ' ') {
981
- void onContextClick();
982
- }
983
- };
984
-
985
- // clang-format off
986
- return html`<div class="select-element">
987
- ${
988
- hasPickerBehavior ? html`
989
- <devtools-button
990
- .data=${{
991
- variant: Buttons.Button.Variant.ICON_TOGGLE,
992
- size: Buttons.Button.Size.SMALL,
993
- iconName: 'select-element',
994
- toggledIconName: 'select-element',
995
- toggleType: Buttons.Button.ToggleType.PRIMARY,
996
- toggled: inspectElementToggled,
997
- title: lockedString(UIStringsNotTranslate.selectAnElement),
998
- jslogContext: 'select-element',
999
- disabled: isTextInputDisabled,
1000
- } as Buttons.Button.ButtonData}
1001
- @click=${onInspectElementClick}
1002
- ></devtools-button>
1003
- ` : Lit.nothing
1004
- }
1005
- <div
1006
- role=button
1007
- class=${resourceClass}
1008
- tabindex=${(hasPickerBehavior || isTextInputDisabled) ? '-1' : '0'}
1009
- @click=${onContextClick}
1010
- @keydown=${handleKeyDown}
1011
- aria-description=${i18nString(UIStrings.revealContextDescription)}
1012
- >
1013
- ${renderContextIcon(selectedContext)}
1014
- <span class="title">${selectedContext ? renderContextTitle(selectedContext, isTextInputDisabled) : lockedString(UIStringsNotTranslate.noElementSelected)}</span>
1015
- </div>
1016
- </div>`;
1017
- // clang-format on
317
+ return renderEmptyState({isTextInputDisabled, suggestions, onSuggestionClick});
1018
318
  }
1019
319
 
1020
320
  function renderMessages({
@@ -1062,19 +362,19 @@ function renderMessages({
1062
362
  // clang-format off
1063
363
  return html`
1064
364
  <div class="messages-container" ${ref(onMessageContainerRef)}>
1065
- ${messages.map((message, _, array) =>
1066
- renderChatMessage({
365
+ ${repeat(messages, message =>
366
+ html`<devtools-widget .widgetConfig=${UI.Widget.widgetConfig(UserActionRow, {
1067
367
  message,
1068
368
  isLoading,
1069
369
  isReadOnly,
1070
370
  canShowFeedbackForm,
1071
- isLast: array.at(-1) === message,
1072
371
  userInfo,
1073
372
  markdownRenderer,
373
+ isLastMessage: messages.at(-1) === message,
1074
374
  onSuggestionClick,
1075
375
  onFeedbackSubmit,
1076
376
  onCopyResponseClick,
1077
- }),
377
+ })}></devtools-widget>`
1078
378
  )}
1079
379
  ${renderPatchWidget()}
1080
380
  </div>
@@ -1118,503 +418,6 @@ function renderEmptyState({isTextInputDisabled, suggestions, onSuggestionClick}:
1118
418
  // clang-format on
1119
419
  }
1120
420
 
1121
- function renderReadOnlySection({onNewConversation}: {
1122
- onNewConversation: () => void,
1123
- }): Lit.LitTemplate {
1124
- // clang-format off
1125
- return html`<div
1126
- class="chat-readonly-container"
1127
- jslog=${VisualLogging.section('read-only')}
1128
- >
1129
- <span>${lockedString(UIStringsNotTranslate.pastConversation)}</span>
1130
- <devtools-button
1131
- aria-label=${lockedString(UIStringsNotTranslate.startNewChat)}
1132
- class="chat-inline-button"
1133
- @click=${onNewConversation}
1134
- .data=${{
1135
- variant: Buttons.Button.Variant.TEXT,
1136
- title: lockedString(UIStringsNotTranslate.startNewChat),
1137
- jslogContext: 'start-new-chat',
1138
- } as Buttons.Button.ButtonData}
1139
- >${lockedString(UIStringsNotTranslate.startNewChat)}</devtools-button>
1140
- </div>`;
1141
- // clang-format on
1142
- }
1143
-
1144
- function renderChatInputButtons(
1145
- {isLoading, blockedByCrossOrigin, isTextInputDisabled, isTextInputEmpty, imageInput, onCancel, onNewConversation}: {
1146
- isLoading: boolean,
1147
- blockedByCrossOrigin: boolean,
1148
- isTextInputDisabled: boolean,
1149
- isTextInputEmpty: boolean,
1150
- onCancel: (ev: SubmitEvent) => void,
1151
- onNewConversation: () => void,
1152
- imageInput?: ImageInputData,
1153
- }): Lit.TemplateResult {
1154
- if (isLoading) {
1155
- // clang-format off
1156
- return html`<devtools-button
1157
- class="chat-input-button"
1158
- aria-label=${lockedString(UIStringsNotTranslate.cancelButtonTitle)}
1159
- @click=${onCancel}
1160
- .data=${
1161
- {
1162
- variant: Buttons.Button.Variant.ICON,
1163
- size: Buttons.Button.Size.REGULAR,
1164
- iconName: 'record-stop',
1165
- title: lockedString(UIStringsNotTranslate.cancelButtonTitle),
1166
- jslogContext: 'stop',
1167
- } as Buttons.Button.ButtonData
1168
- }
1169
- ></devtools-button>`;
1170
- // clang-format on
1171
- }
1172
- if (blockedByCrossOrigin) {
1173
- // clang-format off
1174
- return html`
1175
- <devtools-button
1176
- class="start-new-chat-button"
1177
- aria-label=${lockedString(UIStringsNotTranslate.startNewChat)}
1178
- @click=${onNewConversation}
1179
- .data=${
1180
- {
1181
- variant: Buttons.Button.Variant.OUTLINED,
1182
- size: Buttons.Button.Size.SMALL,
1183
- title: lockedString(UIStringsNotTranslate.startNewChat),
1184
- jslogContext: 'start-new-chat',
1185
- } as Buttons.Button.ButtonData
1186
- }
1187
- >${lockedString(UIStringsNotTranslate.startNewChat)}</devtools-button>
1188
- `;
1189
- // clang-format on
1190
- }
1191
- // clang-format off
1192
- return html`<devtools-button
1193
- class="chat-input-button"
1194
- aria-label=${lockedString(UIStringsNotTranslate.sendButtonTitle)}
1195
- .data=${
1196
- {
1197
- type: 'submit',
1198
- variant: Buttons.Button.Variant.ICON,
1199
- size: Buttons.Button.Size.REGULAR,
1200
- disabled: isTextInputDisabled || isTextInputEmpty || imageInput?.isLoading,
1201
- iconName: 'send',
1202
- title: lockedString(UIStringsNotTranslate.sendButtonTitle),
1203
- jslogContext: 'send',
1204
- } as Buttons.Button.ButtonData
1205
- }
1206
- ></devtools-button>`;
1207
- }
1208
-
1209
- function renderMultimodalInputButtons({
1210
- multimodalInputEnabled,
1211
- blockedByCrossOrigin,
1212
- isTextInputDisabled,
1213
- imageInput,
1214
- uploadImageInputEnabled,
1215
- onTakeScreenshot,
1216
- onImageUpload,
1217
- }: {
1218
- isTextInputDisabled: boolean,
1219
- blockedByCrossOrigin: boolean,
1220
- multimodalInputEnabled?: boolean,
1221
- imageInput?: ImageInputData,
1222
- uploadImageInputEnabled?: boolean,
1223
- onTakeScreenshot?: () => void,
1224
- onImageUpload?: (ev: Event) => void,
1225
- }): Lit.LitTemplate {
1226
- if (!multimodalInputEnabled || blockedByCrossOrigin) {
1227
- return Lit.nothing;
1228
- }
1229
- // clang-format off
1230
- const addImageButton = uploadImageInputEnabled ? html`<devtools-button
1231
- class="chat-input-button"
1232
- aria-label=${lockedString(UIStringsNotTranslate.addImageButtonTitle)}
1233
- @click=${onImageUpload}
1234
- .data=${
1235
- {
1236
- variant: Buttons.Button.Variant.ICON,
1237
- size: Buttons.Button.Size.REGULAR,
1238
- disabled: isTextInputDisabled || imageInput?.isLoading,
1239
- iconName: 'add-photo',
1240
- title: lockedString(UIStringsNotTranslate.addImageButtonTitle),
1241
- jslogContext: 'upload-image',
1242
- } as Buttons.Button.ButtonData
1243
- }
1244
- ></devtools-button>` : Lit.nothing;
1245
-
1246
- return html`${addImageButton}<devtools-button
1247
- class="chat-input-button"
1248
- aria-label=${lockedString(UIStringsNotTranslate.takeScreenshotButtonTitle)}
1249
- @click=${onTakeScreenshot}
1250
- .data=${
1251
- {
1252
- variant: Buttons.Button.Variant.ICON,
1253
- size: Buttons.Button.Size.REGULAR,
1254
- disabled: isTextInputDisabled || imageInput?.isLoading,
1255
- iconName: 'photo-camera',
1256
- title: lockedString(UIStringsNotTranslate.takeScreenshotButtonTitle),
1257
- jslogContext: 'take-screenshot',
1258
- } as Buttons.Button.ButtonData
1259
- }
1260
- ></devtools-button>`;
1261
- // clang-format on
1262
- }
1263
-
1264
- function renderImageInput({
1265
- multimodalInputEnabled,
1266
- imageInput,
1267
- isTextInputDisabled,
1268
- onRemoveImageInput,
1269
- }: {
1270
- multimodalInputEnabled?: boolean,
1271
- imageInput?: ImageInputData,
1272
- isTextInputDisabled?: boolean,
1273
- onRemoveImageInput?: () => void,
1274
- }): Lit.LitTemplate {
1275
- if (!multimodalInputEnabled || !imageInput || isTextInputDisabled) {
1276
- return Lit.nothing;
1277
- }
1278
- // clang-format off
1279
- const crossButton = html`<devtools-button
1280
- aria-label=${lockedString(UIStringsNotTranslate.removeImageInputButtonTitle)}
1281
- @click=${onRemoveImageInput}
1282
- .data=${
1283
- {
1284
- variant: Buttons.Button.Variant.ICON,
1285
- size: Buttons.Button.Size.MICRO,
1286
- iconName: 'cross',
1287
- title: lockedString(UIStringsNotTranslate.removeImageInputButtonTitle),
1288
- } as Buttons.Button.ButtonData
1289
- }
1290
- ></devtools-button>`;
1291
- // clang-format on
1292
-
1293
- if (imageInput.isLoading) {
1294
- // clang-format off
1295
- return html`<div class="image-input-container">
1296
- ${crossButton}
1297
- <div class="loading">
1298
- <devtools-spinner></devtools-spinner>
1299
- </div>
1300
- </div>`;
1301
- // clang-format on
1302
- }
1303
- // clang-format off
1304
- return html`
1305
- <div class="image-input-container">
1306
- ${crossButton}
1307
- <img src="data:${imageInput.mimeType};base64, ${imageInput.data}" alt="Image input" />
1308
- </div>`;
1309
- // clang-format on
1310
- }
1311
-
1312
- function renderRelevantDataDisclaimer({isLoading, blockedByCrossOrigin, tooltipId, disclaimerText}: {
1313
- isLoading: boolean,
1314
- blockedByCrossOrigin: boolean,
1315
- tooltipId: string,
1316
- disclaimerText: string,
1317
- }): Lit.LitTemplate {
1318
- const classes = Lit.Directives.classMap({
1319
- 'chat-input-disclaimer': true,
1320
- 'hide-divider': !isLoading && blockedByCrossOrigin,
1321
- });
1322
- // clang-format off
1323
- return html`
1324
- <p class=${classes}>
1325
- <button
1326
- class="link"
1327
- role="link"
1328
- aria-details=${tooltipId}
1329
- jslog=${VisualLogging.link('open-ai-settings').track({
1330
- click: true,
1331
- })}
1332
- @click=${() => {
1333
- void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
1334
- }}
1335
- >${lockedString('Relevant data')}</button>&nbsp;${lockedString('is sent to Google')}
1336
- ${renderDisclaimerTooltip(tooltipId, disclaimerText)}
1337
- </p>
1338
- `;
1339
- // clang-format on
1340
- }
1341
-
1342
- function renderChatInput({
1343
- isLoading,
1344
- blockedByCrossOrigin,
1345
- isTextInputDisabled,
1346
- inputPlaceholder,
1347
- selectedContext,
1348
- inspectElementToggled,
1349
- multimodalInputEnabled,
1350
- conversationType,
1351
- imageInput,
1352
- isTextInputEmpty,
1353
- uploadImageInputEnabled,
1354
- disclaimerText,
1355
- additionalFloatyContext,
1356
- onContextClick,
1357
- onInspectElementClick,
1358
- onSubmit,
1359
- onTextAreaKeyDown,
1360
- onCancel,
1361
- onNewConversation,
1362
- onTakeScreenshot,
1363
- onRemoveImageInput,
1364
- onTextInputChange,
1365
- onImageUpload,
1366
- }: {
1367
- isLoading: boolean,
1368
- blockedByCrossOrigin: boolean,
1369
- isTextInputDisabled: boolean,
1370
- inputPlaceholder: Platform.UIString.LocalizedString,
1371
- selectedContext: AiAssistanceModel.AiAgent.ConversationContext<unknown>|null,
1372
- inspectElementToggled: boolean,
1373
- isTextInputEmpty: boolean,
1374
- additionalFloatyContext: UI.Floaty.FloatyContextSelection[],
1375
- disclaimerText: string,
1376
- onContextClick: () => void,
1377
- onInspectElementClick: () => void,
1378
- onSubmit: (ev: SubmitEvent) => void,
1379
- onTextAreaKeyDown: (ev: KeyboardEvent) => void,
1380
- onCancel: (ev: SubmitEvent) => void,
1381
- onNewConversation: () => void,
1382
- onTextInputChange: (input: string) => void,
1383
- conversationType: AiAssistanceModel.AiHistoryStorage.ConversationType,
1384
- multimodalInputEnabled?: boolean,
1385
- imageInput?: ImageInputData,
1386
- uploadImageInputEnabled?: boolean,
1387
- onTakeScreenshot?: () => void,
1388
- onRemoveImageInput?: () => void,
1389
- onImageUpload?: (ev: Event) => void,
1390
- }): Lit.LitTemplate {
1391
- const chatInputContainerCls = Lit.Directives.classMap({
1392
- 'chat-input-container': true,
1393
- 'single-line-layout': !selectedContext,
1394
- disabled: isTextInputDisabled,
1395
- });
1396
-
1397
- // clang-format off
1398
- return html` <form class="input-form" @submit=${onSubmit}>
1399
- ${renderFloatyExtraContext(additionalFloatyContext)}
1400
- <div class=${chatInputContainerCls}>
1401
- ${renderImageInput({
1402
- multimodalInputEnabled,
1403
- imageInput,
1404
- isTextInputDisabled,
1405
- onRemoveImageInput,
1406
- })}
1407
- <textarea
1408
- class="chat-input"
1409
- .disabled=${isTextInputDisabled}
1410
- wrap="hard"
1411
- maxlength="10000"
1412
- @keydown=${onTextAreaKeyDown}
1413
- @input=${(event: KeyboardEvent) =>
1414
- onTextInputChange((event.target as HTMLInputElement).value)}
1415
- placeholder=${inputPlaceholder}
1416
- jslog=${VisualLogging.textField('query').track({
1417
- change: true,
1418
- keydown: 'Enter',
1419
- })}
1420
- aria-description=${i18nString(UIStrings.inputTextAriaDescription)}
1421
- ${ref(el => {
1422
- // If the elements is disabled reset the text to show
1423
- // the place holder
1424
- if (el && isTextInputDisabled) {
1425
- (el as HTMLInputElement).value = '';
1426
- }
1427
- })}
1428
- ></textarea>
1429
- <div class="chat-input-actions">
1430
- <div class="chat-input-actions-left">
1431
- ${renderSelection({
1432
- selectedContext,
1433
- inspectElementToggled,
1434
- conversationType,
1435
- isTextInputDisabled,
1436
- onContextClick,
1437
- onInspectElementClick,
1438
- })}
1439
- </div>
1440
- <div class="chat-input-actions-right">
1441
- <div class="chat-input-disclaimer-container">
1442
- ${renderRelevantDataDisclaimer({
1443
- isLoading,
1444
- blockedByCrossOrigin,
1445
- tooltipId: RELEVANT_DATA_LINK_CHAT_ID,
1446
- disclaimerText,
1447
- })}
1448
- </div>
1449
- ${renderMultimodalInputButtons({
1450
- multimodalInputEnabled,
1451
- blockedByCrossOrigin,
1452
- isTextInputDisabled,
1453
- imageInput,
1454
- uploadImageInputEnabled,
1455
- onTakeScreenshot,
1456
- onImageUpload,
1457
- })}
1458
- ${renderChatInputButtons({
1459
- isLoading,
1460
- blockedByCrossOrigin,
1461
- isTextInputDisabled,
1462
- isTextInputEmpty,
1463
- imageInput,
1464
- onCancel,
1465
- onNewConversation,
1466
- })}
1467
- </div>
1468
- </div>
1469
- </div>
1470
- </form>`;
1471
- // clang-format on
1472
- }
1473
-
1474
- function renderFloatyExtraContext(contexts: UI.Floaty.FloatyContextSelection[]): Lit.LitTemplate {
1475
- if (!GreenDev.Prototypes.instance().isEnabled('inDevToolsFloaty')) {
1476
- return Lit.nothing;
1477
- }
1478
-
1479
- // clang-format off
1480
- return html`
1481
- <ul class="floaty">
1482
- ${contexts.map(c => {
1483
- function onDelete(e: MouseEvent): void {
1484
- e.preventDefault();
1485
- UI.Floaty.onFloatyContextDelete(c);
1486
- }
1487
-
1488
- return html`<li>
1489
- <span class="context-item">
1490
- ${renderFloatyContext(c)}
1491
- </span>
1492
- <devtools-button
1493
- class="floaty-delete-button"
1494
- @click=${onDelete}
1495
- .data=${{
1496
- variant: Buttons.Button.Variant.ICON,
1497
- iconName: 'cross',
1498
- title: 'Delete',
1499
- size: Buttons.Button.Size.SMALL,
1500
- } as Buttons.Button.ButtonData}
1501
- ></devtools-button>
1502
- </li>`;
1503
- })}
1504
- <li class="open-floaty">
1505
- <devtools-button
1506
- class="floaty-add-button"
1507
- @click=${UI.Floaty.onFloatyOpen}
1508
- .data=${{
1509
- variant: Buttons.Button.Variant.ICON,
1510
- iconName: 'select-element',
1511
- title: 'Open context picker',
1512
- size: Buttons.Button.Size.SMALL,
1513
- } as Buttons.Button.ButtonData}
1514
- ></devtools-button>
1515
- </li>
1516
- </ul>
1517
- `;
1518
- // clang-format on
1519
- }
1520
-
1521
- function renderFloatyContext(context: UI.Floaty.FloatyContextSelection): Lit.TemplateResult {
1522
- if (context instanceof SDK.NetworkRequest.NetworkRequest) {
1523
- return html`${context.url()}`;
1524
- }
1525
-
1526
- if (context instanceof SDK.DOMModel.DOMNode) {
1527
- return html`<devtools-widget .widgetConfig=${
1528
- UI.Widget.widgetConfig(PanelsCommon.DOMLinkifier.DOMNodeLink, {node: context})}>`;
1529
- }
1530
-
1531
- if ('insight' in context) {
1532
- return html`${context.insight.title}`;
1533
- }
1534
-
1535
- if ('event' in context && 'traceStartTime' in context) {
1536
- const time = Trace.Types.Timing.Micro(context.event.ts - context.traceStartTime);
1537
- return html`${context.event.name} @ ${i18n.TimeUtilities.formatMicroSecondsAsMillisFixed(time)}`;
1538
- }
1539
-
1540
- Platform.assertNever(context, 'Unsupported context');
1541
- }
1542
-
1543
- function renderMainContents({
1544
- messages,
1545
- isLoading,
1546
- isReadOnly,
1547
- canShowFeedbackForm,
1548
- isTextInputDisabled,
1549
- suggestions,
1550
- userInfo,
1551
- markdownRenderer,
1552
- changeSummary,
1553
- changeManager,
1554
- onSuggestionClick,
1555
- onFeedbackSubmit,
1556
- onCopyResponseClick,
1557
- onMessageContainerRef,
1558
- }: {
1559
- messages: ChatMessage[],
1560
- isLoading: boolean,
1561
- isReadOnly: boolean,
1562
- canShowFeedbackForm: boolean,
1563
- isTextInputDisabled: boolean,
1564
- suggestions: AiAssistanceModel.AiAgent.ConversationSuggestion[],
1565
- userInfo: Pick<Host.InspectorFrontendHostAPI.SyncInformation, 'accountImage'|'accountFullName'>,
1566
- markdownRenderer: MarkdownLitRenderer,
1567
- changeManager: AiAssistanceModel.ChangeManager.ChangeManager,
1568
- onSuggestionClick: (suggestion: string) => void,
1569
- onFeedbackSubmit: (rpcId: Host.AidaClient.RpcGlobalId, rate: Host.AidaClient.Rating, feedback?: string) => void,
1570
- onCopyResponseClick: (message: ModelChatMessage) => void,
1571
- onMessageContainerRef: (el: Element|undefined) => void,
1572
- changeSummary?: string,
1573
- }): Lit.LitTemplate {
1574
- if (messages.length > 0) {
1575
- return renderMessages({
1576
- messages,
1577
- isLoading,
1578
- isReadOnly,
1579
- canShowFeedbackForm,
1580
- userInfo,
1581
- markdownRenderer,
1582
- changeSummary,
1583
- changeManager,
1584
- onSuggestionClick,
1585
- onFeedbackSubmit,
1586
- onMessageContainerRef,
1587
- onCopyResponseClick
1588
- });
1589
- }
1590
-
1591
- return renderEmptyState({isTextInputDisabled, suggestions, onSuggestionClick});
1592
- }
1593
-
1594
- function renderDisclaimerTooltip(id: string, disclaimerText: string): Lit.TemplateResult {
1595
- // clang-format off
1596
- return html`
1597
- <devtools-tooltip
1598
- id=${id}
1599
- variant="rich"
1600
- >
1601
- <div class="info-tooltip-container">
1602
- ${disclaimerText}
1603
- <button
1604
- class="link tooltip-link"
1605
- role="link"
1606
- jslog=${VisualLogging.link('open-ai-settings').track({
1607
- click: true,
1608
- })}
1609
- @click=${() => {
1610
- void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
1611
- }}>${i18nString(UIStrings.learnAbout)}
1612
- </button>
1613
- </div>
1614
- </devtools-tooltip>`;
1615
- // clang-format on
1616
- }
1617
-
1618
421
  declare global {
1619
422
  interface HTMLElementTagNameMap {
1620
423
  'devtools-ai-chat-view': ChatView;