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.
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
@@ -0,0 +1,701 @@
1
+ // Copyright 2025 The Chromium Authors
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+
5
+ import '../../../ui/components/tooltips/tooltips.js';
6
+
7
+ import type * as Host from '../../../core/host/host.js';
8
+ import * as i18n from '../../../core/i18n/i18n.js';
9
+ import type * as Platform from '../../../core/platform/platform.js';
10
+ import * as SDK from '../../../core/sdk/sdk.js';
11
+ import * as Protocol from '../../../generated/protocol.js';
12
+ import * as AiAssistanceModel from '../../../models/ai_assistance/ai_assistance.js';
13
+ import * as GreenDev from '../../../models/greendev/greendev.js';
14
+ import * as Trace from '../../../models/trace/trace.js';
15
+ import * as Workspace from '../../../models/workspace/workspace.js';
16
+ import * as PanelsCommon from '../../../panels/common/common.js';
17
+ import * as PanelUtils from '../../../panels/utils/utils.js';
18
+ import * as Buttons from '../../../ui/components/buttons/buttons.js';
19
+ import * as Input from '../../../ui/components/input/input.js';
20
+ import * as Snackbars from '../../../ui/components/snackbars/snackbars.js';
21
+ import * as UI from '../../../ui/legacy/legacy.js';
22
+ import * as Lit from '../../../ui/lit/lit.js';
23
+ import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
24
+
25
+ import chatInputStyles from './chatInput.css.js';
26
+
27
+ const {html, Directives: {createRef, ref}} = Lit;
28
+
29
+ const UIStrings = {
30
+ /**
31
+ * @description Label added to the text input to describe the context for screen readers. Not shown visibly on screen.
32
+ */
33
+ inputTextAriaDescription: 'You can also use one of the suggested prompts above to start your conversation',
34
+ /**
35
+ * @description Label added to the button that reveals the selected context item in DevTools
36
+ */
37
+ revealContextDescription: 'Reveal the selected context item in DevTools',
38
+ /**
39
+ * @description The footer disclaimer that links to more information about the AI feature.
40
+ */
41
+ learnAbout: 'Learn about AI in DevTools',
42
+ } as const;
43
+
44
+ /*
45
+ * Strings that don't need to be translated at this time.
46
+ */
47
+ const UIStringsNotTranslate = {
48
+ /**
49
+ * @description Title for the send icon button.
50
+ */
51
+ sendButtonTitle: 'Send',
52
+ /**
53
+ * @description Title for the start new chat
54
+ */
55
+ startNewChat: 'Start new chat',
56
+ /**
57
+ * @description Title for the cancel icon button.
58
+ */
59
+ cancelButtonTitle: 'Cancel',
60
+ /**
61
+ * @description Label for the "select an element" button.
62
+ */
63
+ selectAnElement: 'Select an element',
64
+ /**
65
+ * @description Title for the take screenshot button.
66
+ */
67
+ takeScreenshotButtonTitle: 'Take screenshot',
68
+ /**
69
+ * @description Title for the remove image input button.
70
+ */
71
+ removeImageInputButtonTitle: 'Remove image input',
72
+ /**
73
+ * @description Title for the add image button.
74
+ */
75
+ addImageButtonTitle: 'Add image',
76
+ /**
77
+ * @description Text displayed when the chat input is disabled due to reading past conversation.
78
+ */
79
+ pastConversation: 'You\'re viewing a past conversation.',
80
+ /**
81
+ * @description Message displayed in toast in case of any failures while taking a screenshot of the page.
82
+ */
83
+ screenshotFailureMessage: 'Failed to take a screenshot. Please try again.',
84
+ /**
85
+ * @description Message displayed in toast in case of any failures while uploading an image file as input.
86
+ */
87
+ uploadImageFailureMessage: 'Failed to upload image. Please try again.',
88
+ } as const;
89
+
90
+ const str_ = i18n.i18n.registerUIStrings('panels/ai_assistance/components/ChatInput.ts', UIStrings);
91
+ const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
92
+ const lockedString = i18n.i18n.lockedString;
93
+
94
+ const SCREENSHOT_QUALITY = 80;
95
+ const JPEG_MIME_TYPE = 'image/jpeg';
96
+ const SHOW_LOADING_STATE_TIMEOUT = 100;
97
+
98
+ const RELEVANT_DATA_LINK_CHAT_ID = 'relevant-data-link-chat';
99
+ const RELEVANT_DATA_LINK_FOOTER_ID = 'relevant-data-link-footer';
100
+
101
+ export type ImageInputData = {
102
+ isLoading: true,
103
+ }|{
104
+ isLoading: false,
105
+ data: string,
106
+ mimeType: string,
107
+ inputType: AiAssistanceModel.AiAgent.MultimodalInputType,
108
+ };
109
+
110
+ export interface ViewInput {
111
+ isLoading: boolean;
112
+ isTextInputEmpty: boolean;
113
+ blockedByCrossOrigin: boolean;
114
+ isTextInputDisabled: boolean;
115
+ inputPlaceholder: Platform.UIString.LocalizedString;
116
+ selectedContext: AiAssistanceModel.AiAgent.ConversationContext<unknown>|null;
117
+ inspectElementToggled: boolean;
118
+ additionalFloatyContext: UI.Floaty.FloatyContextSelection[];
119
+ disclaimerText: string;
120
+ conversationType: AiAssistanceModel.AiHistoryStorage.ConversationType;
121
+ multimodalInputEnabled: boolean;
122
+ imageInput?: ImageInputData;
123
+ uploadImageInputEnabled: boolean;
124
+ isReadOnly: boolean;
125
+ textAreaRef: Lit.Directives.Ref<HTMLTextAreaElement>;
126
+
127
+ onContextClick: () => void;
128
+ onInspectElementClick: () => void;
129
+ onSubmit: (ev: SubmitEvent) => void;
130
+ onTextAreaKeyDown: (ev: KeyboardEvent) => void;
131
+ onCancel: (ev: SubmitEvent) => void;
132
+ onNewConversation: () => void;
133
+ onTextInputChange: (input: string) => void;
134
+ onTakeScreenshot: () => void;
135
+ onRemoveImageInput: () => void;
136
+ onImageUpload: (ev: Event) => void;
137
+ }
138
+
139
+ export type ViewOutput = undefined;
140
+
141
+ export const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLElement): void => {
142
+ const chatInputContainerCls = Lit.Directives.classMap({
143
+ 'chat-input-container': true,
144
+ 'single-line-layout': !input.selectedContext,
145
+ disabled: input.isTextInputDisabled,
146
+ });
147
+
148
+ const renderRelevantDataDisclaimer = (tooltipId: string): Lit.LitTemplate => {
149
+ const classes = Lit.Directives.classMap({
150
+ 'chat-input-disclaimer': true,
151
+ 'hide-divider': !input.isLoading && input.blockedByCrossOrigin,
152
+ });
153
+ // clang-format off
154
+ return html`
155
+ <div class=${classes}>
156
+ <button
157
+ class="link"
158
+ role="link"
159
+ aria-details=${tooltipId}
160
+ jslog=${VisualLogging.link('open-ai-settings').track({
161
+ click: true,
162
+ })}
163
+ @click=${() => {
164
+ void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
165
+ }}
166
+ >${lockedString('Relevant data')}</button>&nbsp;${lockedString('is sent to Google')}
167
+ <devtools-tooltip
168
+ id=${tooltipId}
169
+ variant="rich"
170
+ ><div class="info-tooltip-container">
171
+ ${input.disclaimerText}
172
+ <button
173
+ class="link tooltip-link"
174
+ role="link"
175
+ jslog=${VisualLogging.link('open-ai-settings').track({
176
+ click: true,
177
+ })}
178
+ @click=${() => {
179
+ void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
180
+ }}>${i18nString(UIStrings.learnAbout)}
181
+ </button>
182
+ </div></devtools-tooltip>
183
+ </div>
184
+ `;
185
+ // clang-format on
186
+ };
187
+
188
+ // clang-format off
189
+ Lit.render(html`
190
+ <style>${Input.textInputStyles}</style>
191
+ <style>${chatInputStyles}</style>
192
+ ${input.isReadOnly ?
193
+ html`
194
+ <div
195
+ class="chat-readonly-container"
196
+ jslog=${VisualLogging.section('read-only')}
197
+ >
198
+ <span>${lockedString(UIStringsNotTranslate.pastConversation)}</span>
199
+ <devtools-button
200
+ aria-label=${lockedString(UIStringsNotTranslate.startNewChat)}
201
+ class="chat-inline-button"
202
+ @click=${input.onNewConversation}
203
+ .data=${{
204
+ variant: Buttons.Button.Variant.TEXT,
205
+ title: lockedString(UIStringsNotTranslate.startNewChat),
206
+ jslogContext: 'start-new-chat',
207
+ } as Buttons.Button.ButtonData}
208
+ >${lockedString(UIStringsNotTranslate.startNewChat)}</devtools-button>
209
+ </div>`
210
+ :
211
+ html`
212
+ <form class="input-form" @submit=${input.onSubmit}>
213
+ ${GreenDev.Prototypes.instance().isEnabled('inDevToolsFloaty') ?
214
+ html`
215
+ <ul class="floaty">
216
+ ${input.additionalFloatyContext.map(c => {
217
+ return html`
218
+ <li>
219
+ <span class="context-item">
220
+ ${c instanceof SDK.NetworkRequest.NetworkRequest ? html`${c.url()}` :
221
+ c instanceof SDK.DOMModel.DOMNode ? html`
222
+ <devtools-widget .widgetConfig=${
223
+ UI.Widget.widgetConfig(PanelsCommon.DOMLinkifier.DOMNodeLink, {node: c})}
224
+ ></devtools-widget>` :
225
+ 'insight' in c ? html`${c.insight.title}` :
226
+ 'event' in c && 'traceStartTime' in c ? html`
227
+ ${c.event.name} @ ${i18n.TimeUtilities.formatMicroSecondsAsMillisFixed(Trace.Types.Timing.Micro(c.event.ts - c.traceStartTime))}` :
228
+ Lit.nothing}
229
+ </span>
230
+ <devtools-button
231
+ class="floaty-delete-button"
232
+ @click=${(e: MouseEvent) => {
233
+ e.preventDefault();
234
+ UI.Floaty.onFloatyContextDelete(c);
235
+ }}
236
+ .data=${{
237
+ variant: Buttons.Button.Variant.ICON,
238
+ iconName: 'cross',
239
+ title: 'Delete',
240
+ size: Buttons.Button.Size.SMALL,
241
+ } as Buttons.Button.ButtonData}
242
+ ></devtools-button>
243
+ </li>`;
244
+ })}
245
+ <li class="open-floaty">
246
+ <devtools-button
247
+ class="floaty-add-button"
248
+ @click=${UI.Floaty.onFloatyOpen}
249
+ .data=${{
250
+ variant: Buttons.Button.Variant.ICON,
251
+ iconName: 'select-element',
252
+ title: 'Open context picker',
253
+ size: Buttons.Button.Size.SMALL,
254
+ } as Buttons.Button.ButtonData}
255
+ ></devtools-button>
256
+ </li>
257
+ </ul>`
258
+ : Lit.nothing}
259
+ <div class=${chatInputContainerCls}>
260
+ ${(input.multimodalInputEnabled && input.imageInput && !input.isTextInputDisabled) ?
261
+ html`
262
+ <div class="image-input-container">
263
+ <devtools-button
264
+ aria-label=${lockedString(UIStringsNotTranslate.removeImageInputButtonTitle)}
265
+ @click=${input.onRemoveImageInput}
266
+ .data=${{
267
+ variant: Buttons.Button.Variant.ICON,
268
+ size: Buttons.Button.Size.MICRO,
269
+ iconName: 'cross',
270
+ title: lockedString(UIStringsNotTranslate.removeImageInputButtonTitle),
271
+ } as Buttons.Button.ButtonData}
272
+ ></devtools-button>
273
+ ${input.imageInput.isLoading ?
274
+ html`
275
+ <div class="loading">
276
+ <devtools-spinner></devtools-spinner>
277
+ </div>`
278
+ :
279
+ html`
280
+ <img src="data:${input.imageInput.mimeType};base64, ${input.imageInput.data}" alt="Image input" />`
281
+ }
282
+ </div>`
283
+ : Lit.nothing}
284
+ <textarea
285
+ class="chat-input"
286
+ .disabled=${input.isTextInputDisabled}
287
+ wrap="hard"
288
+ maxlength="10000"
289
+ @keydown=${input.onTextAreaKeyDown}
290
+ @input=${(event: KeyboardEvent) => {
291
+ input.onTextInputChange((event.target as HTMLInputElement).value);
292
+ }}
293
+ placeholder=${input.inputPlaceholder}
294
+ jslog=${VisualLogging.textField('query').track({
295
+ change: true,
296
+ keydown: 'Enter',
297
+ })}
298
+ aria-description=${i18nString(UIStrings.inputTextAriaDescription)}
299
+ ${ref(input.textAreaRef)}
300
+ ></textarea>
301
+ <div class="chat-input-actions">
302
+ <div class="chat-input-actions-left">
303
+ ${input.selectedContext ?
304
+ html`
305
+ <div class="select-element">
306
+ ${input.conversationType === AiAssistanceModel.AiHistoryStorage.ConversationType.STYLING ?
307
+ html`
308
+ <devtools-button
309
+ .data=${{
310
+ variant: Buttons.Button.Variant.ICON_TOGGLE,
311
+ size: Buttons.Button.Size.SMALL,
312
+ iconName: 'select-element',
313
+ toggledIconName: 'select-element',
314
+ toggleType: Buttons.Button.ToggleType.PRIMARY,
315
+ toggled: input.inspectElementToggled,
316
+ title: lockedString(UIStringsNotTranslate.selectAnElement),
317
+ jslogContext: 'select-element',
318
+ disabled: input.isTextInputDisabled,
319
+ } as Buttons.Button.ButtonData}
320
+ @click=${input.onInspectElementClick}
321
+ ></devtools-button>`
322
+ : Lit.nothing}
323
+ <div
324
+ role=button
325
+ class=${Lit.Directives.classMap({
326
+ 'resource-link': true,
327
+ 'has-picker-behavior': input.conversationType === AiAssistanceModel.AiHistoryStorage.ConversationType.STYLING,
328
+ disabled: input.isTextInputDisabled,
329
+ })}
330
+ tabindex=${(input.conversationType === AiAssistanceModel.AiHistoryStorage.ConversationType.STYLING || input.isTextInputDisabled) ? '-1' : '0'}
331
+ @click=${input.onContextClick}
332
+ @keydown=${(ev: KeyboardEvent) => {
333
+ if (ev.key === 'Enter' || ev.key === ' ') {
334
+ void input.onContextClick();
335
+ }
336
+ }}
337
+ aria-description=${i18nString(UIStrings.revealContextDescription)}
338
+ >
339
+ ${input.selectedContext.getItem() instanceof SDK.NetworkRequest.NetworkRequest ?
340
+ PanelUtils.PanelUtils.getIconForNetworkRequest(input.selectedContext.getItem() as SDK.NetworkRequest.NetworkRequest) :
341
+ input.selectedContext.getItem() instanceof Workspace.UISourceCode.UISourceCode ?
342
+ PanelUtils.PanelUtils.getIconForSourceFile(input.selectedContext.getItem() as Workspace.UISourceCode.UISourceCode) :
343
+ input.selectedContext.getItem() instanceof AiAssistanceModel.AIContext.AgentFocus ?
344
+ html`<devtools-icon name="performance" title="Performance"></devtools-icon>` :
345
+ Lit.nothing}
346
+ <span class="title">
347
+ ${input.selectedContext.getItem() instanceof SDK.DOMModel.DOMNode ?
348
+ html`
349
+ <devtools-widget .widgetConfig=${UI.Widget.widgetConfig(PanelsCommon.DOMLinkifier.DOMNodeLink, {
350
+ node: input.selectedContext.getItem() as SDK.DOMModel.DOMNode,
351
+ options: {
352
+ hiddenClassList: (input.selectedContext.getItem() as SDK.DOMModel.DOMNode).classNames().filter(
353
+ className => className.startsWith(AiAssistanceModel.Injected.AI_ASSISTANCE_CSS_CLASS_NAME)),
354
+ disabled: input.isTextInputDisabled,
355
+ },
356
+ })}></devtools-widget>`
357
+ :
358
+ input.selectedContext.getTitle()}
359
+ </span>
360
+ </div>
361
+ </div>`
362
+ : Lit.nothing}
363
+ </div>
364
+ <div class="chat-input-actions-right">
365
+ <div class="chat-input-disclaimer-container">
366
+ ${renderRelevantDataDisclaimer(RELEVANT_DATA_LINK_CHAT_ID)}
367
+ </div>
368
+ ${(input.multimodalInputEnabled && !input.blockedByCrossOrigin) ?
369
+ html`
370
+ ${input.uploadImageInputEnabled ?
371
+ html`
372
+ <devtools-button
373
+ class="chat-input-button"
374
+ aria-label=${lockedString(UIStringsNotTranslate.addImageButtonTitle)}
375
+ @click=${input.onImageUpload}
376
+ .data=${{
377
+ variant: Buttons.Button.Variant.ICON,
378
+ size: Buttons.Button.Size.REGULAR,
379
+ disabled: input.isTextInputDisabled || input.imageInput?.isLoading,
380
+ iconName: 'add-photo',
381
+ title: lockedString(UIStringsNotTranslate.addImageButtonTitle),
382
+ jslogContext: 'upload-image',
383
+ } as Buttons.Button.ButtonData}
384
+ ></devtools-button>`
385
+ : Lit.nothing}
386
+ <devtools-button
387
+ class="chat-input-button"
388
+ aria-label=${lockedString(UIStringsNotTranslate.takeScreenshotButtonTitle)}
389
+ @click=${input.onTakeScreenshot}
390
+ .data=${{
391
+ variant: Buttons.Button.Variant.ICON,
392
+ size: Buttons.Button.Size.REGULAR,
393
+ disabled: input.isTextInputDisabled || input.imageInput?.isLoading,
394
+ iconName: 'photo-camera',
395
+ title: lockedString(UIStringsNotTranslate.takeScreenshotButtonTitle),
396
+ jslogContext: 'take-screenshot',
397
+ } as Buttons.Button.ButtonData}
398
+ ></devtools-button>`
399
+ : Lit.nothing}
400
+ ${input.isLoading ?
401
+ html`
402
+ <devtools-button
403
+ class="chat-input-button"
404
+ aria-label=${lockedString(UIStringsNotTranslate.cancelButtonTitle)}
405
+ @click=${input.onCancel}
406
+ .data=${{
407
+ variant: Buttons.Button.Variant.ICON,
408
+ size: Buttons.Button.Size.REGULAR,
409
+ iconName: 'record-stop',
410
+ title: lockedString(UIStringsNotTranslate.cancelButtonTitle),
411
+ jslogContext: 'stop',
412
+ } as Buttons.Button.ButtonData}
413
+ ></devtools-button>`
414
+ :
415
+ input.blockedByCrossOrigin ?
416
+ html`
417
+ <devtools-button
418
+ class="start-new-chat-button"
419
+ aria-label=${lockedString(UIStringsNotTranslate.startNewChat)}
420
+ @click=${input.onNewConversation}
421
+ .data=${{
422
+ variant: Buttons.Button.Variant.OUTLINED,
423
+ size: Buttons.Button.Size.SMALL,
424
+ title: lockedString(UIStringsNotTranslate.startNewChat),
425
+ jslogContext: 'start-new-chat',
426
+ } as Buttons.Button.ButtonData}
427
+ >${lockedString(UIStringsNotTranslate.startNewChat)}</devtools-button>`
428
+ :
429
+ html`
430
+ <devtools-button
431
+ class="chat-input-button"
432
+ aria-label=${lockedString(UIStringsNotTranslate.sendButtonTitle)}
433
+ .data=${{
434
+ type: 'submit',
435
+ variant: Buttons.Button.Variant.ICON,
436
+ size: Buttons.Button.Size.REGULAR,
437
+ disabled: input.isTextInputDisabled || input.isTextInputEmpty || input.imageInput?.isLoading,
438
+ iconName: 'send',
439
+ title: lockedString(UIStringsNotTranslate.sendButtonTitle),
440
+ jslogContext: 'send',
441
+ } as Buttons.Button.ButtonData}
442
+ ></devtools-button>`
443
+ }
444
+ </div>
445
+ </div>
446
+ </div>
447
+ </form>`
448
+ }
449
+ <footer
450
+ class=${Lit.Directives.classMap({
451
+ 'chat-input-footer': true,
452
+ 'is-read-only': input.isReadOnly,
453
+ })}
454
+ jslog=${VisualLogging.section('footer')}
455
+ >
456
+ ${renderRelevantDataDisclaimer(RELEVANT_DATA_LINK_FOOTER_ID)}
457
+ </footer>
458
+ `, target);
459
+ // clang-format on
460
+ };
461
+
462
+ /**
463
+ * ChatInput is a presenter for the input area in the AI Assistance panel.
464
+ */
465
+ export class ChatInput extends UI.Widget.Widget implements SDK.TargetManager.Observer {
466
+ isLoading = false;
467
+ blockedByCrossOrigin = false;
468
+ isTextInputDisabled = false;
469
+ inputPlaceholder = '' as Platform.UIString.LocalizedString;
470
+ selectedContext = null as AiAssistanceModel.AiAgent.ConversationContext<unknown>| null;
471
+ inspectElementToggled = false;
472
+ additionalFloatyContext = [] as UI.Floaty.FloatyContextSelection[];
473
+ disclaimerText = '';
474
+ conversationType = AiAssistanceModel.AiHistoryStorage.ConversationType.STYLING;
475
+ multimodalInputEnabled = false;
476
+ uploadImageInputEnabled = false;
477
+ isReadOnly = false;
478
+
479
+ #textAreaRef = createRef<HTMLTextAreaElement>();
480
+ #imageInput?: ImageInputData;
481
+
482
+ setInputValue(text: string): void {
483
+ if (this.#textAreaRef.value) {
484
+ this.#textAreaRef.value.value = text;
485
+ }
486
+ this.performUpdate();
487
+ }
488
+
489
+ #isTextInputEmpty(): boolean {
490
+ return !this.#textAreaRef.value?.value?.trim();
491
+ }
492
+
493
+ onTextSubmit:
494
+ (text: string, imageInput?: Host.AidaClient.Part,
495
+ multimodalInputType?: AiAssistanceModel.AiAgent.MultimodalInputType) => void = () => {};
496
+ onContextClick = (): void => {};
497
+ onInspectElementClick = (): void => {};
498
+ onCancelClick = (): void => {};
499
+ onNewConversation = (): void => {};
500
+
501
+ async #handleTakeScreenshot(): Promise<void> {
502
+ const mainTarget = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
503
+ if (!mainTarget) {
504
+ throw new Error('Could not find main target');
505
+ }
506
+ const model = mainTarget.model(SDK.ScreenCaptureModel.ScreenCaptureModel);
507
+ if (!model) {
508
+ throw new Error('Could not find model');
509
+ }
510
+ const showLoadingTimeout = setTimeout(() => {
511
+ this.#imageInput = {isLoading: true};
512
+ this.performUpdate();
513
+ }, SHOW_LOADING_STATE_TIMEOUT);
514
+ const bytes = await model.captureScreenshot(
515
+ Protocol.Page.CaptureScreenshotRequestFormat.Jpeg,
516
+ SCREENSHOT_QUALITY,
517
+ SDK.ScreenCaptureModel.ScreenshotMode.FROM_VIEWPORT,
518
+ );
519
+ clearTimeout(showLoadingTimeout);
520
+ if (bytes) {
521
+ this.#imageInput = {
522
+ isLoading: false,
523
+ data: bytes,
524
+ mimeType: JPEG_MIME_TYPE,
525
+ inputType: AiAssistanceModel.AiAgent.MultimodalInputType.SCREENSHOT
526
+ };
527
+ this.performUpdate();
528
+ void this.updateComplete.then(() => {
529
+ this.focusTextInput();
530
+ });
531
+ } else {
532
+ this.#imageInput = undefined;
533
+ this.performUpdate();
534
+ Snackbars.Snackbar.Snackbar.show({message: lockedString(UIStringsNotTranslate.screenshotFailureMessage)});
535
+ }
536
+ }
537
+
538
+ targetAdded(_target: SDK.Target.Target): void {
539
+ }
540
+ targetRemoved(_target: SDK.Target.Target): void {
541
+ }
542
+
543
+ #handleRemoveImageInput(): void {
544
+ this.#imageInput = undefined;
545
+ this.performUpdate();
546
+ void this.updateComplete.then(() => {
547
+ this.focusTextInput();
548
+ });
549
+ }
550
+
551
+ async #handleLoadImage(file: File): Promise<void> {
552
+ const showLoadingTimeout = setTimeout(() => {
553
+ this.#imageInput = {isLoading: true};
554
+ this.performUpdate();
555
+ }, SHOW_LOADING_STATE_TIMEOUT);
556
+ try {
557
+ const reader = new FileReader();
558
+ const dataUrl = await new Promise<string>((resolve, reject) => {
559
+ reader.onload = () => {
560
+ if (typeof reader.result === 'string') {
561
+ resolve(reader.result);
562
+ } else {
563
+ reject(new Error('FileReader result was not a string.'));
564
+ }
565
+ };
566
+ reader.readAsDataURL(file);
567
+ });
568
+ const commaIndex = dataUrl.indexOf(',');
569
+ const bytes = dataUrl.substring(commaIndex + 1);
570
+ this.#imageInput = {
571
+ isLoading: false,
572
+ data: bytes,
573
+ mimeType: file.type,
574
+ inputType: AiAssistanceModel.AiAgent.MultimodalInputType.UPLOADED_IMAGE
575
+ };
576
+ } catch {
577
+ this.#imageInput = undefined;
578
+ Snackbars.Snackbar.Snackbar.show({message: lockedString(UIStringsNotTranslate.uploadImageFailureMessage)});
579
+ }
580
+
581
+ clearTimeout(showLoadingTimeout);
582
+ this.performUpdate();
583
+ void this.updateComplete.then(() => {
584
+ this.focusTextInput();
585
+ });
586
+ }
587
+
588
+ #view: typeof DEFAULT_VIEW;
589
+
590
+ constructor(element?: HTMLElement, view?: typeof DEFAULT_VIEW) {
591
+ super(element);
592
+ this.#view = view ?? DEFAULT_VIEW;
593
+ }
594
+
595
+ override wasShown(): void {
596
+ super.wasShown();
597
+ SDK.TargetManager.TargetManager.instance().addModelListener(
598
+ SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.PrimaryPageChanged,
599
+ this.#onPrimaryPageChanged, this);
600
+ }
601
+
602
+ override willHide(): void {
603
+ super.willHide();
604
+ SDK.TargetManager.TargetManager.instance().removeModelListener(
605
+ SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.PrimaryPageChanged,
606
+ this.#onPrimaryPageChanged, this);
607
+ }
608
+
609
+ #onPrimaryPageChanged(): void {
610
+ this.#imageInput = undefined;
611
+ this.performUpdate();
612
+ }
613
+
614
+ override performUpdate(): void {
615
+ this.#view(
616
+ {
617
+ inputPlaceholder: this.inputPlaceholder,
618
+ isLoading: this.isLoading,
619
+ blockedByCrossOrigin: this.blockedByCrossOrigin,
620
+ isTextInputDisabled: this.isTextInputDisabled,
621
+ selectedContext: this.selectedContext,
622
+ inspectElementToggled: this.inspectElementToggled,
623
+ isTextInputEmpty: this.#isTextInputEmpty(),
624
+ additionalFloatyContext: this.additionalFloatyContext,
625
+ disclaimerText: this.disclaimerText,
626
+ conversationType: this.conversationType,
627
+ multimodalInputEnabled: this.multimodalInputEnabled,
628
+ imageInput: this.#imageInput,
629
+ uploadImageInputEnabled: this.uploadImageInputEnabled,
630
+ isReadOnly: this.isReadOnly,
631
+ textAreaRef: this.#textAreaRef,
632
+ onContextClick: this.onContextClick,
633
+ onInspectElementClick: this.onInspectElementClick,
634
+ onNewConversation: this.onNewConversation,
635
+ onTextInputChange: () => {
636
+ this.requestUpdate();
637
+ },
638
+ onTakeScreenshot: this.#handleTakeScreenshot.bind(this),
639
+ onRemoveImageInput: this.#handleRemoveImageInput.bind(this),
640
+ onSubmit: this.onSubmit,
641
+ onTextAreaKeyDown: this.onTextAreaKeyDown,
642
+ onCancel: this.onCancel,
643
+ onImageUpload: this.onImageUpload,
644
+ },
645
+ undefined, this.contentElement);
646
+ }
647
+
648
+ focusTextInput(): void {
649
+ this.#textAreaRef.value?.focus();
650
+ }
651
+
652
+ onSubmit = (event: SubmitEvent): void => {
653
+ event.preventDefault();
654
+ if (this.#imageInput?.isLoading) {
655
+ return;
656
+ }
657
+ const imageInput = !this.#imageInput?.isLoading && this.#imageInput?.data ?
658
+ {inlineData: {data: this.#imageInput.data, mimeType: this.#imageInput.mimeType}} :
659
+ undefined;
660
+ this.onTextSubmit(this.#textAreaRef.value?.value ?? '', imageInput, this.#imageInput?.inputType);
661
+ this.#imageInput = undefined;
662
+ this.setInputValue('');
663
+ };
664
+
665
+ onTextAreaKeyDown = (event: KeyboardEvent): void => {
666
+ if (!event.target || !(event.target instanceof HTMLTextAreaElement)) {
667
+ return;
668
+ }
669
+
670
+ // Go to a new line on Shift+Enter. On Enter, submit unless the
671
+ // user is in IME composition.
672
+ if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
673
+ event.preventDefault();
674
+ if (!event.target?.value || this.#imageInput?.isLoading) {
675
+ return;
676
+ }
677
+ const imageInput = !this.#imageInput?.isLoading && this.#imageInput?.data ?
678
+ {inlineData: {data: this.#imageInput.data, mimeType: this.#imageInput.mimeType}} :
679
+ undefined;
680
+ this.onTextSubmit(event.target.value, imageInput, this.#imageInput?.inputType);
681
+ this.#imageInput = undefined;
682
+ this.setInputValue('');
683
+ }
684
+ };
685
+
686
+ onCancel = (ev: SubmitEvent): void => {
687
+ ev.preventDefault();
688
+
689
+ if (!this.isLoading) {
690
+ return;
691
+ }
692
+
693
+ this.onCancelClick();
694
+ };
695
+
696
+ onImageUpload = (ev: Event): void => {
697
+ ev.stopPropagation();
698
+ const fileSelector = UI.UIUtils.createFileSelectorElement(this.#handleLoadImage.bind(this), '.jpeg,.jpg,.png');
699
+ fileSelector.click();
700
+ };
701
+ }