chrome-devtools-frontend 1.0.1642899 → 1.0.1643855
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eslint.config.mjs +3 -1
- package/extension-api/ExtensionAPI.d.ts +83 -12
- package/front_end/core/host/UserMetrics.ts +0 -1
- package/front_end/core/protocol_client/InspectorBackend.ts +4 -0
- package/front_end/core/root/ExperimentNames.ts +0 -1
- package/front_end/core/sdk/ConsoleModel.ts +4 -0
- package/front_end/core/sdk/NetworkRequest.ts +12 -0
- package/front_end/core/sdk/SourceMap.ts +15 -18
- package/front_end/entrypoints/greendev_floaty/FloatyEntrypoint.ts +0 -2
- package/front_end/entrypoints/greendev_floaty/greendev_floaty.ts +0 -2
- package/front_end/entrypoints/heap_snapshot_worker/HeapSnapshot.ts +4 -5
- package/front_end/entrypoints/main/MainImpl.ts +0 -6
- package/front_end/generated/InspectorBackendCommands.ts +1 -1
- package/front_end/generated/protocol.ts +7 -0
- package/front_end/models/ai_assistance/AiAgent2.ts +24 -5
- package/front_end/models/ai_assistance/AiConversation.ts +15 -12
- package/front_end/models/ai_assistance/AiUtils.ts +71 -0
- package/front_end/models/ai_assistance/ChangeManager.ts +2 -5
- package/front_end/models/ai_assistance/{agents/ConversationSummaryAgent.ts → ConversationSummary.ts} +29 -66
- package/front_end/models/ai_assistance/ExtensionScope.ts +1 -4
- package/front_end/models/ai_assistance/{agents/PerformanceAnnotationsAgent.ts → PerformanceAnnotations.ts} +47 -89
- package/front_end/models/ai_assistance/agents/AccessibilityAgent.ts +47 -31
- package/front_end/models/ai_assistance/agents/AiAgent.ts +40 -12
- package/front_end/models/ai_assistance/agents/ContextSelectionAgent.snapshot.txt +11 -0
- package/front_end/models/ai_assistance/agents/ContextSelectionAgent.ts +58 -8
- package/front_end/models/ai_assistance/agents/ExecuteJavascript.ts +1 -92
- package/front_end/models/ai_assistance/agents/NetworkAgent.ts +25 -0
- package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +94 -79
- package/front_end/models/ai_assistance/agents/StorageAgent.ts +101 -39
- package/front_end/models/ai_assistance/agents/StylingAgent.snapshot.txt +1 -2
- package/front_end/models/ai_assistance/agents/StylingAgent.ts +27 -21
- package/front_end/models/ai_assistance/ai_assistance.ts +6 -4
- package/front_end/models/ai_assistance/skills/styling.md +12 -4
- package/front_end/models/ai_assistance/tools/ExecuteJavaScript.ts +134 -0
- package/front_end/models/ai_assistance/tools/GetStyles.ts +6 -2
- package/front_end/models/ai_assistance/tools/Tool.ts +16 -1
- package/front_end/models/ai_assistance/tools/ToolRegistry.ts +2 -0
- package/front_end/models/bindings/DebuggerWorkspaceBinding.ts +6 -9
- package/front_end/models/bindings/DefaultScriptMapping.ts +2 -1
- package/front_end/models/bindings/SymbolizedError.ts +45 -35
- package/front_end/models/extensions/ExtensionAPI.ts +138 -47
- package/front_end/models/har/Importer.ts +1 -0
- package/front_end/models/heap_snapshot/HeapSnapshotProxy.ts +5 -7
- package/front_end/models/source_map_scopes/FunctionCodeResolver.ts +12 -2
- package/front_end/models/stack_trace/DetailedErrorStackParser.ts +44 -51
- package/front_end/models/stack_trace/StackTrace.ts +7 -0
- package/front_end/models/stack_trace/StackTraceImpl.ts +13 -4
- package/front_end/models/stack_trace/StackTraceModel.ts +9 -8
- package/front_end/panels/accessibility/AccessibilitySidebarView.ts +2 -1
- package/front_end/panels/ai_assistance/AiAssistancePanel.ts +8 -8
- package/front_end/panels/ai_assistance/components/ChatMessage.ts +96 -4
- package/front_end/panels/ai_assistance/components/chatMessage.css +6 -0
- package/front_end/panels/application/ApplicationPanelSidebar.ts +39 -0
- package/front_end/panels/application/ApplicationPanelTreeElement.ts +39 -0
- package/front_end/panels/application/CookieItemsView.ts +2 -2
- package/front_end/panels/application/components/AdsView.ts +219 -0
- package/front_end/panels/application/components/adsView.css +54 -0
- package/front_end/panels/application/components/components.ts +2 -0
- package/front_end/panels/application/resourcesSidebar.css +11 -0
- package/front_end/panels/console/SymbolizedErrorWidget.ts +79 -25
- package/front_end/panels/network/NetworkLogView.ts +5 -1
- package/front_end/panels/settings/emulation/DevicesSettingsTab.ts +1 -0
- package/front_end/panels/sources/SourcesPanel.ts +2 -1
- package/front_end/panels/timeline/overlays/components/EntryLabelOverlay.ts +5 -4
- package/front_end/third_party/chromium/README.chromium +1 -1
- package/front_end/third_party/lighthouse/README.chromium +2 -2
- package/front_end/third_party/lighthouse/lighthouse-dt-bundle.js +1607 -5733
- package/front_end/third_party/lighthouse/locales/ar-XB.json +290 -65
- package/front_end/third_party/lighthouse/locales/ar.json +290 -65
- package/front_end/third_party/lighthouse/locales/bg.json +290 -65
- package/front_end/third_party/lighthouse/locales/ca.json +295 -70
- package/front_end/third_party/lighthouse/locales/cs.json +290 -65
- package/front_end/third_party/lighthouse/locales/da.json +294 -69
- package/front_end/third_party/lighthouse/locales/de.json +295 -70
- package/front_end/third_party/lighthouse/locales/el.json +290 -65
- package/front_end/third_party/lighthouse/locales/en-GB.json +290 -65
- package/front_end/third_party/lighthouse/locales/en-US.json +79 -67
- package/front_end/third_party/lighthouse/locales/en-XA.json +253 -64
- package/front_end/third_party/lighthouse/locales/en-XL.json +79 -67
- package/front_end/third_party/lighthouse/locales/es-419.json +290 -65
- package/front_end/third_party/lighthouse/locales/es.json +298 -73
- package/front_end/third_party/lighthouse/locales/fi.json +290 -65
- package/front_end/third_party/lighthouse/locales/fil.json +290 -65
- package/front_end/third_party/lighthouse/locales/fr.json +294 -69
- package/front_end/third_party/lighthouse/locales/he.json +293 -68
- package/front_end/third_party/lighthouse/locales/hi.json +291 -66
- package/front_end/third_party/lighthouse/locales/hr.json +290 -65
- package/front_end/third_party/lighthouse/locales/hu.json +290 -65
- package/front_end/third_party/lighthouse/locales/id.json +290 -65
- package/front_end/third_party/lighthouse/locales/it.json +294 -69
- package/front_end/third_party/lighthouse/locales/ja.json +290 -65
- package/front_end/third_party/lighthouse/locales/ko.json +290 -65
- package/front_end/third_party/lighthouse/locales/lt.json +290 -65
- package/front_end/third_party/lighthouse/locales/lv.json +290 -65
- package/front_end/third_party/lighthouse/locales/nl.json +290 -65
- package/front_end/third_party/lighthouse/locales/no.json +290 -65
- package/front_end/third_party/lighthouse/locales/pl.json +290 -65
- package/front_end/third_party/lighthouse/locales/pt-PT.json +291 -66
- package/front_end/third_party/lighthouse/locales/pt.json +290 -65
- package/front_end/third_party/lighthouse/locales/ro.json +290 -65
- package/front_end/third_party/lighthouse/locales/ru.json +301 -76
- package/front_end/third_party/lighthouse/locales/sk.json +291 -66
- package/front_end/third_party/lighthouse/locales/sl.json +290 -65
- package/front_end/third_party/lighthouse/locales/sr-Latn.json +290 -65
- package/front_end/third_party/lighthouse/locales/sr.json +290 -65
- package/front_end/third_party/lighthouse/locales/sv.json +297 -72
- package/front_end/third_party/lighthouse/locales/ta.json +291 -66
- package/front_end/third_party/lighthouse/locales/te.json +293 -68
- package/front_end/third_party/lighthouse/locales/th.json +291 -66
- package/front_end/third_party/lighthouse/locales/tr.json +290 -65
- package/front_end/third_party/lighthouse/locales/uk.json +290 -65
- package/front_end/third_party/lighthouse/locales/vi.json +291 -66
- package/front_end/third_party/lighthouse/locales/zh-HK.json +292 -67
- package/front_end/third_party/lighthouse/locales/zh-TW.json +291 -66
- package/front_end/third_party/lighthouse/locales/zh.json +291 -66
- package/front_end/third_party/lighthouse/report/bundle.d.ts +6 -6
- package/front_end/third_party/lighthouse/report/bundle.js +4 -7
- package/front_end/third_party/lighthouse/report-assets/report-generator.mjs +2 -2
- package/front_end/ui/legacy/StackedPane.ts +229 -0
- package/front_end/ui/legacy/ViewManager.ts +59 -169
- package/front_end/ui/legacy/Widget.ts +32 -8
- package/front_end/ui/legacy/legacy.ts +3 -1
- package/front_end/ui/visual_logging/KnownContextValues.ts +2 -0
- package/mcp/mcp.ts +1 -0
- package/package.json +1 -1
|
@@ -211,10 +211,11 @@ export class StackTraceModel extends SDK.SDKModel.SDKModel<unknown> {
|
|
|
211
211
|
let i = 0;
|
|
212
212
|
let evalI = 0;
|
|
213
213
|
for (const node of fragment.node.getCallStack()) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
214
|
+
const group = uiFrames[i++];
|
|
215
|
+
node.frames =
|
|
216
|
+
group.map((frame, index) => new FrameImpl(frame.url, frame.uiSourceCode, frame.name, frame.line, frame.column,
|
|
217
|
+
frame.missingDebugInfo, node.rawFrame.functionName,
|
|
218
|
+
node.rawFrame.isWasm, index < group.length - 1));
|
|
218
219
|
|
|
219
220
|
if (node.parsedFrameInfo?.evalOrigin) {
|
|
220
221
|
node.evalOrigin = evalOrigins[evalI++];
|
|
@@ -253,10 +254,10 @@ async function translateEvalOrigin(
|
|
|
253
254
|
rawFrame: RawFrame, rawFramesToUIFrames: TranslateRawFrames,
|
|
254
255
|
target: SDK.Target.Target): Promise<EvalOrigin|undefined> {
|
|
255
256
|
const uiFrames = await rawFramesToUIFrames([rawFrame], target);
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
257
|
+
const group = uiFrames[0];
|
|
258
|
+
const frames = group.map((frame, index) => new FrameImpl(frame.url, frame.uiSourceCode, frame.name, frame.line,
|
|
259
|
+
frame.column, frame.missingDebugInfo, rawFrame.functionName,
|
|
260
|
+
rawFrame.isWasm, index < group.length - 1));
|
|
260
261
|
|
|
261
262
|
let parentEvalOrigin: EvalOrigin|undefined;
|
|
262
263
|
if (rawFrame.parsedFrameInfo?.evalOrigin) {
|
|
@@ -33,7 +33,7 @@ export class AccessibilitySidebarView extends UI.Widget.VBox {
|
|
|
33
33
|
#node: SDK.DOMModel.DOMNode|null;
|
|
34
34
|
#axNode: SDK.AccessibilityModel.AccessibilityNode|null;
|
|
35
35
|
private skipNextPullNode: boolean;
|
|
36
|
-
private readonly sidebarPaneStack: UI.
|
|
36
|
+
private readonly sidebarPaneStack: UI.ViewManager.StackLocation;
|
|
37
37
|
private readonly ariaSubPane: ARIAAttributesPane;
|
|
38
38
|
private readonly axNodeSubPane: AXNodeSubPane;
|
|
39
39
|
private readonly sourceOrderSubPane: SourceOrderPane;
|
|
@@ -159,6 +159,7 @@ export class AccessibilitySidebarView extends UI.Widget.VBox {
|
|
|
159
159
|
|
|
160
160
|
private updateToggle(): void {
|
|
161
161
|
const isToggled = this.toggleAction.toggled();
|
|
162
|
+
this.sidebarPaneStack.notifyVisibilityChanged(isToggled);
|
|
162
163
|
// eslint-disable-next-line @devtools/no-lit-render-outside-of-view
|
|
163
164
|
render(
|
|
164
165
|
html`
|
|
@@ -657,7 +657,7 @@ export class AiAssistancePanel extends UI.Panel.Panel {
|
|
|
657
657
|
// NodeJS debugging does not have Elements panel, thus this action might not exist.
|
|
658
658
|
#toggleSearchElementAction?: UI.ActionRegistration.Action;
|
|
659
659
|
#aidaClient: Host.AidaClient.AidaClient;
|
|
660
|
-
#
|
|
660
|
+
#conversationSummary?: AiAssistanceModel.ConversationSummary.ConversationSummary;
|
|
661
661
|
#viewOutput: PanelViewOutput = {};
|
|
662
662
|
#serverSideLoggingEnabled = isAiAssistanceServerSideLoggingEnabled();
|
|
663
663
|
#aiAssistanceEnabledSetting: Common.Settings.Setting<boolean>|undefined;
|
|
@@ -784,17 +784,16 @@ export class AiAssistancePanel extends UI.Panel.Panel {
|
|
|
784
784
|
markdownRenderer,
|
|
785
785
|
conversationMarkdown: this.#conversation.getConversationMarkdown(),
|
|
786
786
|
generateConversationSummary: async (markdown: string) => {
|
|
787
|
-
if (!this.#
|
|
788
|
-
this.#
|
|
787
|
+
if (!this.#conversationSummary) {
|
|
788
|
+
this.#conversationSummary = new AiAssistanceModel.ConversationSummary.ConversationSummary({
|
|
789
789
|
aidaClient: this.#aidaClient,
|
|
790
790
|
serverSideLoggingEnabled: this.#serverSideLoggingEnabled,
|
|
791
791
|
});
|
|
792
792
|
}
|
|
793
|
-
return await this.#
|
|
793
|
+
return await this.#conversationSummary.summarizeConversation(markdown);
|
|
794
794
|
},
|
|
795
|
-
onTextSubmit: async (
|
|
796
|
-
|
|
797
|
-
multimodalInputType?: AiAssistanceModel.AiAgent.MultimodalInputType) => {
|
|
795
|
+
onTextSubmit: async (text: string, imageInput?: Host.AidaClient.Part,
|
|
796
|
+
multimodalInputType?: AiAssistanceModel.AiAgent.MultimodalInputType) => {
|
|
798
797
|
const submit = (): void => {
|
|
799
798
|
Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceQuerySubmitted);
|
|
800
799
|
void this.#startConversation(text, imageInput, multimodalInputType);
|
|
@@ -2116,7 +2115,8 @@ export class ActionDelegate implements UI.ActionRegistration.ActionDelegate {
|
|
|
2116
2115
|
case 'drjones.network-panel-context':
|
|
2117
2116
|
case 'drjones.performance-panel-context':
|
|
2118
2117
|
case 'drjones.sources-floating-button':
|
|
2119
|
-
case 'drjones.sources-panel-context':
|
|
2118
|
+
case 'drjones.sources-panel-context':
|
|
2119
|
+
case 'ai-assistance.storage-floating-button': {
|
|
2120
2120
|
void (async () => {
|
|
2121
2121
|
const view = UI.ViewManager.ViewManager.instance().view(
|
|
2122
2122
|
AiAssistancePanel.panelName,
|
|
@@ -14,9 +14,9 @@ import * as SDK from '../../../core/sdk/sdk.js';
|
|
|
14
14
|
import type * as Protocol from '../../../generated/protocol.js';
|
|
15
15
|
import type {
|
|
16
16
|
AiWidget, BottomUpTreeAiWidget, ComputedStyleAiWidget, CoreVitalsAiWidget, DomTreeAiWidget, LighthouseReportAiWidget,
|
|
17
|
-
NetworkRequestGeneralHeadersAiWidget, PerfInsightAiWidget, PerformanceTraceAiWidget,
|
|
18
|
-
SourceFileAiWidget, SourceFilesListAiWidget, StylePropertiesAiWidget,
|
|
19
|
-
TimelineRangeSummaryAiWidget} from '../../../models/ai_assistance/agents/AiAgent.js';
|
|
17
|
+
NetworkRequestGeneralHeadersAiWidget, NetworkRequestsListAiWidget, PerfInsightAiWidget, PerformanceTraceAiWidget,
|
|
18
|
+
SourceCodeAiWidget, SourceFileAiWidget, SourceFilesListAiWidget, StylePropertiesAiWidget,
|
|
19
|
+
TimelineEventSummaryAiWidget, TimelineRangeSummaryAiWidget} from '../../../models/ai_assistance/agents/AiAgent.js';
|
|
20
20
|
import * as AiAssistanceModel from '../../../models/ai_assistance/ai_assistance.js';
|
|
21
21
|
import * as ComputedStyle from '../../../models/computed_style/computed_style.js';
|
|
22
22
|
import * as Formatter from '../../../models/formatter/formatter.js';
|
|
@@ -30,6 +30,7 @@ import * as Buttons from '../../../ui/components/buttons/buttons.js';
|
|
|
30
30
|
import * as Input from '../../../ui/components/input/input.js';
|
|
31
31
|
import type * as MarkdownView from '../../../ui/components/markdown_view/markdown_view.js';
|
|
32
32
|
import type {MarkdownLitRenderer} from '../../../ui/components/markdown_view/MarkdownView.js';
|
|
33
|
+
import * as Snackbars from '../../../ui/components/snackbars/snackbars.js';
|
|
33
34
|
import * as UIHelpers from '../../../ui/helpers/helpers.js';
|
|
34
35
|
import * as UI from '../../../ui/legacy/legacy.js';
|
|
35
36
|
import * as Lit from '../../../ui/lit/lit.js';
|
|
@@ -414,6 +415,14 @@ const UIStringsNotTranslate = {
|
|
|
414
415
|
* @description Title for the character set declaration widget.
|
|
415
416
|
*/
|
|
416
417
|
characterSet: 'Character set declaration',
|
|
418
|
+
/**
|
|
419
|
+
* @description Title for the network requests list widget.
|
|
420
|
+
*/
|
|
421
|
+
networkRequests: 'Network requests',
|
|
422
|
+
/**
|
|
423
|
+
* @description Accessible label for the reveal button in the network requests list widget.
|
|
424
|
+
*/
|
|
425
|
+
revealFirstNetworkRequest: 'Reveal first network request in Network panel',
|
|
417
426
|
/**
|
|
418
427
|
* @description Title for the source files list widget.
|
|
419
428
|
*/
|
|
@@ -1300,7 +1309,12 @@ function renderWidgetResponse(response: WidgetMakerResponse|null): Lit.LitTempla
|
|
|
1300
1309
|
if (response === null) {
|
|
1301
1310
|
return;
|
|
1302
1311
|
}
|
|
1303
|
-
|
|
1312
|
+
Common.Revealer.reveal(response?.revealable).catch((error: Error) => {
|
|
1313
|
+
if (!error.message) {
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
Snackbars.Snackbar.Snackbar.show({message: error.message});
|
|
1317
|
+
});
|
|
1304
1318
|
}
|
|
1305
1319
|
|
|
1306
1320
|
const classes = Lit.Directives.classMap({
|
|
@@ -1466,6 +1480,79 @@ async function makeSourceFilesListWidget(widgetData: SourceFilesListAiWidget): P
|
|
|
1466
1480
|
};
|
|
1467
1481
|
}
|
|
1468
1482
|
|
|
1483
|
+
const expandedNetworkRequestsWidgets = new WeakSet<NetworkRequestsListAiWidget>();
|
|
1484
|
+
|
|
1485
|
+
// A widget with a table of the list of network requests sent to the agent.
|
|
1486
|
+
// Only show 15 requests maximum in collapsed version. The rest of the requests
|
|
1487
|
+
// will be hidden unless the user clicks "Show all".
|
|
1488
|
+
async function makeNetworkRequestsListWidget(widgetData: NetworkRequestsListAiWidget):
|
|
1489
|
+
Promise<WidgetMakerResponse|null> {
|
|
1490
|
+
const requests = widgetData.data.requests;
|
|
1491
|
+
if (requests.length === 0) {
|
|
1492
|
+
return null;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
const isExpanded = expandedNetworkRequestsWidgets.has(widgetData);
|
|
1496
|
+
// We only want just expanded widget to be expanded, if the user closed and reopened the walkthrought, the widget should be collapsed again.
|
|
1497
|
+
// Therefore, after rendering the widget, we remove the widget from the set of expanded widgets so that it is collapsed on next render.
|
|
1498
|
+
if (isExpanded) {
|
|
1499
|
+
expandedNetworkRequestsWidgets.delete(widgetData);
|
|
1500
|
+
}
|
|
1501
|
+
const displayedRequests = isExpanded ? requests : requests.slice(0, 15);
|
|
1502
|
+
|
|
1503
|
+
// The table contains same fields as the ones sent to the agent.
|
|
1504
|
+
// clang-format off
|
|
1505
|
+
const renderedWidget = html`
|
|
1506
|
+
<div class="network-requests-widget">
|
|
1507
|
+
<devtools-data-grid striped inline>
|
|
1508
|
+
<table>
|
|
1509
|
+
<tr>
|
|
1510
|
+
<th id="name" weight="4">${i18n.i18n.lockedString('Name')}</th>
|
|
1511
|
+
<th id="status" weight="1">${i18n.i18n.lockedString('Status')}</th>
|
|
1512
|
+
<th id="size" weight="1">${i18n.i18n.lockedString('Size')}</th>
|
|
1513
|
+
<th id="time" weight="1">${i18n.i18n.lockedString('Time')}</th>
|
|
1514
|
+
</tr>
|
|
1515
|
+
${displayedRequests.map(request => html`
|
|
1516
|
+
<tr>
|
|
1517
|
+
<td>${request.name()}</td>
|
|
1518
|
+
<td>${request.statusCode}</td>
|
|
1519
|
+
<td>${i18n.ByteUtilities.formatBytesToKb(request.transferSize)}</td>
|
|
1520
|
+
<td>${i18n.TimeUtilities.secondsToString(request.duration)}</td>
|
|
1521
|
+
</tr>
|
|
1522
|
+
`)}
|
|
1523
|
+
</table>
|
|
1524
|
+
</devtools-data-grid>
|
|
1525
|
+
${!isExpanded && requests.length > 15 ? html`
|
|
1526
|
+
<div class="show-all-container">
|
|
1527
|
+
<button class="show-all-widget-requests-button text-button"
|
|
1528
|
+
jslog=${VisualLogging.action('show-all-widget-requests-button').track({click: true})}
|
|
1529
|
+
@click=${(e: Event) => {
|
|
1530
|
+
expandedNetworkRequestsWidgets.add(widgetData);
|
|
1531
|
+
const widgetEl = (e.target as HTMLElement).closest('.widget');
|
|
1532
|
+
if (widgetEl) {
|
|
1533
|
+
const widget = UI.Widget.Widget.get(widgetEl) as ChatMessage;
|
|
1534
|
+
if (widget && widget.performUpdate) {
|
|
1535
|
+
void widget.performUpdate();
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}}>
|
|
1539
|
+
${i18n.i18n.lockedString(`Show all ${requests.length} network requests`)}
|
|
1540
|
+
</button>
|
|
1541
|
+
</div>
|
|
1542
|
+
` : Lit.nothing}
|
|
1543
|
+
</div>
|
|
1544
|
+
`;
|
|
1545
|
+
// clang-format on
|
|
1546
|
+
|
|
1547
|
+
return {
|
|
1548
|
+
renderedWidget,
|
|
1549
|
+
title: lockedString(UIStringsNotTranslate.networkRequests),
|
|
1550
|
+
revealable: requests[0],
|
|
1551
|
+
accessibleRevealLabel: lockedString(UIStringsNotTranslate.revealFirstNetworkRequest),
|
|
1552
|
+
jslogContext: 'network-requests-list-widget',
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1469
1556
|
function renderNetworkRequestPreview(networkRequest: NonNullable<DomTreeAiWidget['data']['networkRequest']>):
|
|
1470
1557
|
Lit.TemplateResult {
|
|
1471
1558
|
const filename = networkRequest.url.split('/').pop() || networkRequest.url;
|
|
@@ -1583,6 +1670,8 @@ export function getWidgetSignature(widget: AiWidget): string {
|
|
|
1583
1670
|
return `${widget.name}:${widget.data.request.requestId()}`;
|
|
1584
1671
|
case 'SOURCE_CODE':
|
|
1585
1672
|
return `${widget.name}:${widget.data.url}:${widget.data.line ?? ''}:${widget.data.column ?? ''}`;
|
|
1673
|
+
case 'NETWORK_REQUESTS_LIST':
|
|
1674
|
+
return `${widget.name}:${widget.data.requests.map(r => r.requestId()).join(',')}`;
|
|
1586
1675
|
default:
|
|
1587
1676
|
Platform.assertNever(widget, 'Unknown AiWidget name');
|
|
1588
1677
|
}
|
|
@@ -1672,6 +1761,9 @@ async function renderWidgets(
|
|
|
1672
1761
|
case 'SOURCE_FILES_LIST':
|
|
1673
1762
|
response = await makeSourceFilesListWidget(widgetData);
|
|
1674
1763
|
break;
|
|
1764
|
+
case 'NETWORK_REQUESTS_LIST':
|
|
1765
|
+
response = await makeNetworkRequestsListWidget(widgetData);
|
|
1766
|
+
break;
|
|
1675
1767
|
case 'LIGHTHOUSE_REPORT':
|
|
1676
1768
|
response = await makeLighthouseReportWidget(widgetData);
|
|
1677
1769
|
break;
|
|
@@ -40,6 +40,7 @@ import * as Platform from '../../core/platform/platform.js';
|
|
|
40
40
|
import * as Root from '../../core/root/root.js';
|
|
41
41
|
import * as SDK from '../../core/sdk/sdk.js';
|
|
42
42
|
import * as Protocol from '../../generated/protocol.js';
|
|
43
|
+
import * as AiAssistance from '../../models/ai_assistance/ai_assistance.js';
|
|
43
44
|
import * as LegacyWrapper from '../../ui/components/legacy_wrapper/legacy_wrapper.js';
|
|
44
45
|
import {createIcon} from '../../ui/kit/kit.js';
|
|
45
46
|
import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js';
|
|
@@ -1762,9 +1763,29 @@ export class DOMStorageTreeElement extends ApplicationPanelTreeElement {
|
|
|
1762
1763
|
return false;
|
|
1763
1764
|
}
|
|
1764
1765
|
|
|
1766
|
+
/**
|
|
1767
|
+
* Resolves the DOM storage partition context (`localStorage` or `sessionStorage`)
|
|
1768
|
+
* associated with this tree element for AI assistance.
|
|
1769
|
+
*/
|
|
1770
|
+
#getStorageItem(): AiAssistance.StorageItem.StorageItem|null {
|
|
1771
|
+
const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
|
|
1772
|
+
const mainPageOrigin =
|
|
1773
|
+
target?.inspectedURL() ? Common.ParsedURL.ParsedURL.extractOrigin(target.inspectedURL()) : '';
|
|
1774
|
+
if (!mainPageOrigin || !this.domStorage.storageKey) {
|
|
1775
|
+
return null;
|
|
1776
|
+
}
|
|
1777
|
+
const origin = SDK.StorageKeyManager.parseStorageKey(this.domStorage.storageKey).origin;
|
|
1778
|
+
const storageType = this.domStorage.isLocalStorage ? 'localStorage' : 'sessionStorage';
|
|
1779
|
+
return new AiAssistance.StorageItem.DOMStorageItem(mainPageOrigin, origin, this.domStorage.storageKey, storageType);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1765
1782
|
override onattach(): void {
|
|
1766
1783
|
super.onattach();
|
|
1767
1784
|
this.listItemElement.addEventListener('contextmenu', this.handleContextMenuEvent.bind(this), true);
|
|
1785
|
+
const storageItem = this.#getStorageItem();
|
|
1786
|
+
if (storageItem) {
|
|
1787
|
+
this.createAiButton(storageItem);
|
|
1788
|
+
}
|
|
1768
1789
|
}
|
|
1769
1790
|
|
|
1770
1791
|
private handleContextMenuEvent(event: MouseEvent): void {
|
|
@@ -1851,9 +1872,27 @@ export class CookieTreeElement extends ApplicationPanelTreeElement {
|
|
|
1851
1872
|
return this.#cookieDomain;
|
|
1852
1873
|
}
|
|
1853
1874
|
|
|
1875
|
+
/**
|
|
1876
|
+
* Resolves the cookie domain security context associated with this tree element
|
|
1877
|
+
* for AI assistance.
|
|
1878
|
+
*/
|
|
1879
|
+
#getStorageItem(): AiAssistance.StorageItem.StorageItem|null {
|
|
1880
|
+
const primaryTarget = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
|
|
1881
|
+
const mainPageOrigin =
|
|
1882
|
+
primaryTarget?.inspectedURL() ? Common.ParsedURL.ParsedURL.extractOrigin(primaryTarget.inspectedURL()) : '';
|
|
1883
|
+
if (!mainPageOrigin || !this.#cookieDomain) {
|
|
1884
|
+
return null;
|
|
1885
|
+
}
|
|
1886
|
+
return new AiAssistance.StorageItem.CookieItem(mainPageOrigin, this.#cookieDomain);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1854
1889
|
override onattach(): void {
|
|
1855
1890
|
super.onattach();
|
|
1856
1891
|
this.listItemElement.addEventListener('contextmenu', this.handleContextMenuEvent.bind(this), true);
|
|
1892
|
+
const storageItem = this.#getStorageItem();
|
|
1893
|
+
if (storageItem) {
|
|
1894
|
+
this.createAiButton(storageItem);
|
|
1895
|
+
}
|
|
1857
1896
|
}
|
|
1858
1897
|
|
|
1859
1898
|
private handleContextMenuEvent(event: Event): void {
|
|
@@ -2,15 +2,24 @@
|
|
|
2
2
|
// Use of this source code is governed by a BSD-style license that can be
|
|
3
3
|
// found in the LICENSE file.
|
|
4
4
|
|
|
5
|
+
/* eslint-disable @devtools/no-lit-render-outside-of-view */
|
|
6
|
+
|
|
7
|
+
import '../../ui/components/buttons/buttons.js';
|
|
8
|
+
|
|
5
9
|
import * as Common from '../../core/common/common.js';
|
|
6
10
|
import type * as Platform from '../../core/platform/platform.js';
|
|
11
|
+
import * as AiAssistance from '../../models/ai_assistance/ai_assistance.js';
|
|
7
12
|
import * as UI from '../../ui/legacy/legacy.js';
|
|
13
|
+
import * as Lit from '../../ui/lit/lit.js';
|
|
8
14
|
|
|
9
15
|
import type {ResourcesPanel} from './ResourcesPanel.js';
|
|
10
16
|
|
|
17
|
+
const {html} = Lit;
|
|
18
|
+
|
|
11
19
|
export class ApplicationPanelTreeElement extends UI.TreeOutline.TreeElement {
|
|
12
20
|
protected readonly resourcesPanel: ResourcesPanel;
|
|
13
21
|
private customItemURL?: Platform.DevToolsPath.UrlString;
|
|
22
|
+
protected aiButtonContainer?: HTMLElement;
|
|
14
23
|
|
|
15
24
|
constructor(resourcesPanel: ResourcesPanel, title: string, expandable: boolean, jslogContext: string) {
|
|
16
25
|
super(title, expandable, jslogContext);
|
|
@@ -56,6 +65,36 @@ export class ApplicationPanelTreeElement extends UI.TreeOutline.TreeElement {
|
|
|
56
65
|
showView(view: UI.Widget.AnyWidget|null): void {
|
|
57
66
|
this.resourcesPanel.showView(view);
|
|
58
67
|
}
|
|
68
|
+
|
|
69
|
+
protected createAiButton(storageItem: AiAssistance.StorageItem.StorageItem): void {
|
|
70
|
+
const STORAGE_FLOATING_BUTTON_ACTION_ID = 'ai-assistance.storage-floating-button';
|
|
71
|
+
const actionRegistry = UI.ActionRegistry.ActionRegistry.instance();
|
|
72
|
+
if (!actionRegistry.hasAction(STORAGE_FLOATING_BUTTON_ACTION_ID)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const action = actionRegistry.getAction(STORAGE_FLOATING_BUTTON_ACTION_ID);
|
|
76
|
+
if (!this.aiButtonContainer) {
|
|
77
|
+
this.aiButtonContainer = this.listItemElement.createChild('span', 'ai-button-container');
|
|
78
|
+
const icon = AiAssistance.AiUtils.getIconName();
|
|
79
|
+
const onClick = (ev: Event): void => {
|
|
80
|
+
ev.stopPropagation();
|
|
81
|
+
UI.Context.Context.instance().setFlavor(AiAssistance.StorageItem.StorageItem, storageItem);
|
|
82
|
+
void action.execute();
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// clang-format off
|
|
86
|
+
Lit.render(html`
|
|
87
|
+
<devtools-floating-button
|
|
88
|
+
icon-name=${icon}
|
|
89
|
+
title=${action.title()}
|
|
90
|
+
jslogcontext="ask-ai"
|
|
91
|
+
@click=${onClick}
|
|
92
|
+
@mousedown=${(ev: Event) => ev.stopPropagation()}>
|
|
93
|
+
</devtools-floating-button>
|
|
94
|
+
`, this.aiButtonContainer);
|
|
95
|
+
// clang-format on
|
|
96
|
+
}
|
|
97
|
+
}
|
|
59
98
|
}
|
|
60
99
|
|
|
61
100
|
export class ExpandableApplicationPanelTreeElement extends ApplicationPanelTreeElement {
|
|
@@ -317,7 +317,7 @@ export class CookieItemsView extends UI.Widget.VBox {
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
private updateAiAssistanceContext(cookie: SDK.Cookie.Cookie|null): void {
|
|
320
|
-
if (
|
|
320
|
+
if (cookie && cookie.httpOnly()) {
|
|
321
321
|
UI.Context.Context.instance().setFlavor(AiAssistanceModel.StorageItem.StorageItem, null);
|
|
322
322
|
return;
|
|
323
323
|
}
|
|
@@ -332,7 +332,7 @@ export class CookieItemsView extends UI.Widget.VBox {
|
|
|
332
332
|
return;
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
-
const storageItem = new AiAssistanceModel.StorageItem.CookieItem(mainPageOrigin, this.cookieDomain, cookie
|
|
335
|
+
const storageItem = new AiAssistanceModel.StorageItem.CookieItem(mainPageOrigin, this.cookieDomain, cookie?.name());
|
|
336
336
|
UI.Context.Context.instance().setFlavor(AiAssistanceModel.StorageItem.StorageItem, storageItem);
|
|
337
337
|
}
|
|
338
338
|
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// Copyright 2026 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 * as i18n from '../../../core/i18n/i18n.js';
|
|
6
|
+
import * as SDK from '../../../core/sdk/sdk.js';
|
|
7
|
+
import type * as Protocol from '../../../generated/protocol.js';
|
|
8
|
+
import * as UI from '../../../ui/legacy/legacy.js';
|
|
9
|
+
import * as Lit from '../../../ui/lit/lit.js';
|
|
10
|
+
|
|
11
|
+
import adsViewStyles from './adsView.css.js';
|
|
12
|
+
|
|
13
|
+
const {html} = Lit;
|
|
14
|
+
|
|
15
|
+
const UIStrings = {
|
|
16
|
+
/**
|
|
17
|
+
* @description Title for a metric showing the percentage of the viewport covered by ads.
|
|
18
|
+
*/
|
|
19
|
+
viewportAdDensity: 'Viewport ad density',
|
|
20
|
+
/**
|
|
21
|
+
* @description Title for a metric showing the number of ads in the viewport.
|
|
22
|
+
*/
|
|
23
|
+
viewportAdCount: 'Viewport ad count',
|
|
24
|
+
/**
|
|
25
|
+
* @description Title for a metric showing the total CPU usage by ads.
|
|
26
|
+
*/
|
|
27
|
+
totalCpuUsage: 'Total CPU usage by ads',
|
|
28
|
+
/**
|
|
29
|
+
* @description Title for a metric showing the total network usage by ads.
|
|
30
|
+
*/
|
|
31
|
+
totalNetworkUsage: 'Total network usage by ads',
|
|
32
|
+
/**
|
|
33
|
+
* @description Subtext showing the average value of a metric.
|
|
34
|
+
* @example {5.00%} PH1
|
|
35
|
+
*/
|
|
36
|
+
average: '(Average: {PH1})',
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
const str_ = i18n.i18n.registerUIStrings('panels/application/components/AdsView.ts', UIStrings);
|
|
40
|
+
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
|
41
|
+
|
|
42
|
+
export interface ViewInput {
|
|
43
|
+
metrics: Protocol.Ads.AdMetrics;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type View = (input: ViewInput, output: undefined, target: HTMLElement|DocumentFragment) => void;
|
|
47
|
+
|
|
48
|
+
const DEFAULT_VIEW: View = (input, output, target) => {
|
|
49
|
+
const metrics = input.metrics;
|
|
50
|
+
|
|
51
|
+
const formatValue = (val: number, isPercentage: boolean): string => {
|
|
52
|
+
if (isPercentage) {
|
|
53
|
+
return new Intl
|
|
54
|
+
.NumberFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale, {
|
|
55
|
+
style: 'percent',
|
|
56
|
+
maximumFractionDigits: 0,
|
|
57
|
+
})
|
|
58
|
+
.format(val / 100);
|
|
59
|
+
}
|
|
60
|
+
return new Intl.NumberFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale).format(val);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const formatAverage = (val: number, isPercentage: boolean): string => {
|
|
64
|
+
if (isPercentage) {
|
|
65
|
+
return new Intl
|
|
66
|
+
.NumberFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale, {
|
|
67
|
+
style: 'percent',
|
|
68
|
+
minimumFractionDigits: 2,
|
|
69
|
+
maximumFractionDigits: 2,
|
|
70
|
+
})
|
|
71
|
+
.format(val / 100);
|
|
72
|
+
}
|
|
73
|
+
return new Intl
|
|
74
|
+
.NumberFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale, {
|
|
75
|
+
minimumFractionDigits: 2,
|
|
76
|
+
maximumFractionDigits: 2,
|
|
77
|
+
})
|
|
78
|
+
.format(val);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const formatCpu = (val: number): string => {
|
|
82
|
+
return i18n.TimeUtilities.preciseMillisToString(val, 1);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const formatNetwork = (val: number): string => {
|
|
86
|
+
return i18n.ByteUtilities.bytesToString(val);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// clang-format off
|
|
90
|
+
Lit.render(html`
|
|
91
|
+
<style>${adsViewStyles}</style>
|
|
92
|
+
<dl class="metrics-container">
|
|
93
|
+
<div class="metric-box">
|
|
94
|
+
<dt class="metric-title">${i18nString(UIStrings.viewportAdDensity)}</dt>
|
|
95
|
+
<dd class="metric-value">
|
|
96
|
+
<span>${formatValue(metrics.viewportAdDensityByArea, true)}</span>
|
|
97
|
+
<span class="metric-average">${i18nString(UIStrings.average, {PH1: formatAverage(metrics.averageViewportAdDensityByArea, true)})}</span>
|
|
98
|
+
</dd>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="metric-box">
|
|
101
|
+
<dt class="metric-title">${i18nString(UIStrings.viewportAdCount)}</dt>
|
|
102
|
+
<dd class="metric-value">
|
|
103
|
+
<span>${formatValue(metrics.viewportAdCount, false)}</span>
|
|
104
|
+
<span class="metric-average">${i18nString(UIStrings.average, {PH1: formatAverage(metrics.averageViewportAdCount, false)})}</span>
|
|
105
|
+
</dd>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="metric-box">
|
|
108
|
+
<dt class="metric-title">${i18nString(UIStrings.totalCpuUsage)}</dt>
|
|
109
|
+
<dd class="metric-value">
|
|
110
|
+
<span>${formatCpu(metrics.totalAdCpuTime)}</span>
|
|
111
|
+
</dd>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="metric-box">
|
|
114
|
+
<dt class="metric-title">${i18nString(UIStrings.totalNetworkUsage)}</dt>
|
|
115
|
+
<dd class="metric-value">
|
|
116
|
+
<span>${formatNetwork(metrics.totalAdNetworkBytes)}</span>
|
|
117
|
+
</dd>
|
|
118
|
+
</div>
|
|
119
|
+
</dl>
|
|
120
|
+
`, target);
|
|
121
|
+
// clang-format on
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export class AdsView extends UI.Widget.Widget {
|
|
125
|
+
#currentMetrics: Protocol.Ads.AdMetrics;
|
|
126
|
+
#pollTimer?: number;
|
|
127
|
+
#isPolling = false;
|
|
128
|
+
#pollSessionId = 0;
|
|
129
|
+
#view: View;
|
|
130
|
+
|
|
131
|
+
constructor(view: View = DEFAULT_VIEW) {
|
|
132
|
+
super({useShadowDom: true});
|
|
133
|
+
this.#view = view;
|
|
134
|
+
this.#currentMetrics = {
|
|
135
|
+
viewportAdDensityByArea: 0,
|
|
136
|
+
averageViewportAdDensityByArea: 0,
|
|
137
|
+
viewportAdCount: 0,
|
|
138
|
+
averageViewportAdCount: 0,
|
|
139
|
+
totalAdCpuTime: 0,
|
|
140
|
+
totalAdNetworkBytes: 0,
|
|
141
|
+
};
|
|
142
|
+
this.requestUpdate();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
override wasShown(): void {
|
|
146
|
+
super.wasShown();
|
|
147
|
+
this.#startPolling();
|
|
148
|
+
SDK.TargetManager.TargetManager.instance().addModelListener(SDK.ResourceTreeModel.ResourceTreeModel,
|
|
149
|
+
SDK.ResourceTreeModel.Events.PrimaryPageChanged,
|
|
150
|
+
this.#onPrimaryPageChanged, this);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
override willHide(): void {
|
|
154
|
+
this.#stopPolling();
|
|
155
|
+
SDK.TargetManager.TargetManager.instance().removeModelListener(SDK.ResourceTreeModel.ResourceTreeModel,
|
|
156
|
+
SDK.ResourceTreeModel.Events.PrimaryPageChanged,
|
|
157
|
+
this.#onPrimaryPageChanged, this);
|
|
158
|
+
super.willHide();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#startPolling(): void {
|
|
162
|
+
if (this.#isPolling) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this.#isPolling = true;
|
|
166
|
+
this.#pollSessionId++;
|
|
167
|
+
void this.#pollMetrics(this.#pollSessionId);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#stopPolling(): void {
|
|
171
|
+
this.#isPolling = false;
|
|
172
|
+
if (this.#pollTimer !== undefined) {
|
|
173
|
+
window.clearTimeout(this.#pollTimer);
|
|
174
|
+
this.#pollTimer = undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async #pollMetrics(sessionId: number): Promise<void> {
|
|
179
|
+
if (!this.#isPolling || this.#pollSessionId !== sessionId) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
|
|
183
|
+
if (target) {
|
|
184
|
+
const adsAgent = target.adsAgent();
|
|
185
|
+
if (adsAgent) {
|
|
186
|
+
const response = await adsAgent.invoke_getAdMetrics();
|
|
187
|
+
if (!this.#isPolling || this.#pollSessionId !== sessionId) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (!response.getError()) {
|
|
191
|
+
this.#currentMetrics = response.metrics;
|
|
192
|
+
this.requestUpdate();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (this.#isPolling && this.#pollSessionId === sessionId) {
|
|
197
|
+
this.#pollTimer = window.setTimeout(() => this.#pollMetrics(sessionId), 500);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#onPrimaryPageChanged(): void {
|
|
202
|
+
this.#currentMetrics = {
|
|
203
|
+
viewportAdDensityByArea: 0,
|
|
204
|
+
averageViewportAdDensityByArea: 0,
|
|
205
|
+
viewportAdCount: 0,
|
|
206
|
+
averageViewportAdCount: 0,
|
|
207
|
+
totalAdCpuTime: 0,
|
|
208
|
+
totalAdNetworkBytes: 0,
|
|
209
|
+
};
|
|
210
|
+
this.requestUpdate();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
override performUpdate(): void {
|
|
214
|
+
const viewInput: ViewInput = {
|
|
215
|
+
metrics: this.#currentMetrics,
|
|
216
|
+
};
|
|
217
|
+
this.#view(viewInput, undefined, this.contentElement);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 The Chromium Authors
|
|
3
|
+
* Use of this source code is governed by a BSD-style license that can be
|
|
4
|
+
* found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
:host {
|
|
8
|
+
padding: 12px;
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
overflow: auto;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.metrics-container {
|
|
15
|
+
flex: 0 0 auto;
|
|
16
|
+
margin: 0 0 24px;
|
|
17
|
+
border: 1px solid var(--sys-color-divider);
|
|
18
|
+
display: grid;
|
|
19
|
+
grid-template-columns: repeat(2, 1fr);
|
|
20
|
+
gap: 1px;
|
|
21
|
+
background-color: var(--sys-color-divider);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.metric-box {
|
|
25
|
+
background-color: var(--sys-color-surface);
|
|
26
|
+
padding: 12px;
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.metric-title {
|
|
34
|
+
font-size: 12px;
|
|
35
|
+
color: var(--sys-color-on-surface-subtle);
|
|
36
|
+
margin: 0 0 4px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.metric-value {
|
|
40
|
+
font-size: 18px;
|
|
41
|
+
font-weight: bold;
|
|
42
|
+
color: var(--sys-color-on-surface);
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
align-items: center;
|
|
46
|
+
margin: 0;
|
|
47
|
+
gap: 2px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.metric-average {
|
|
51
|
+
font-size: 12px;
|
|
52
|
+
font-weight: normal;
|
|
53
|
+
color: var(--sys-color-on-surface-subtle);
|
|
54
|
+
}
|