chrome-devtools-frontend
Version:
Chrome DevTools UI
201 lines (177 loc) • 7.17 kB
text/typescript
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as i18n from '../../../core/i18n/i18n.js';
import type * as SDK from '../../../core/sdk/sdk.js';
import {
type ContextDetail,
ConversationContext,
type ConversationSuggestions,
} from '../agents/AiAgent.js';
const UIStringsNotTranslate = {
/**
* @description Heading text for context details of DevTools AI Agent.
*/
dataUsed: 'Data used',
} as const;
const lockedString = i18n.i18n.lockedString;
export class DOMNodeContext extends ConversationContext<SDK.DOMModel.DOMNode> {
#node: SDK.DOMModel.DOMNode;
constructor(node: SDK.DOMModel.DOMNode) {
super();
this.#node = node;
}
override getURL(): string {
const ownerDocument = this.#node.ownerDocument;
if (!ownerDocument) {
// The node is detached from a document.
return 'detached';
}
return ownerDocument.documentURL;
}
getItem(): SDK.DOMModel.DOMNode {
return this.#node;
}
override getTitle(): string {
throw new Error('Not implemented');
}
override async getSuggestions(): Promise<ConversationSuggestions|undefined> {
const layoutProps = await this.#node.domModel().cssModel().getLayoutPropertiesFromComputedStyle(this.#node.id);
if (!layoutProps) {
return;
}
if (layoutProps.isFlex) {
return [
{title: 'How can I make flex items wrap?', jslogContext: 'flex-wrap'},
{title: 'How do I distribute flex items evenly?', jslogContext: 'flex-distribute'},
{title: 'What is flexbox?', jslogContext: 'flex-what'},
];
}
if (layoutProps.isSubgrid) {
return [
{title: 'Where is this grid defined?', jslogContext: 'subgrid-where'},
{title: 'How to overwrite parent grid properties?', jslogContext: 'subgrid-override'},
{title: 'How do subgrids work? ', jslogContext: 'subgrid-how'},
];
}
if (layoutProps.isGrid) {
return [
{title: 'How do I align items in a grid?', jslogContext: 'grid-align'},
{title: 'How to add spacing between grid items?', jslogContext: 'grid-gap'},
{title: 'How does grid layout work?', jslogContext: 'grid-how'},
];
}
if (layoutProps.hasScroll) {
return [
{title: 'How do I remove scrollbars for this element?', jslogContext: 'scroll-remove'},
{title: 'How can I style a scrollbar?', jslogContext: 'scroll-style'},
{title: 'Why does this element scroll?', jslogContext: 'scroll-why'},
];
}
if (layoutProps.containerType) {
return [
{title: 'What are container queries?', jslogContext: 'container-what'},
{title: 'How do I use container-type?', jslogContext: 'container-how'},
{title: 'What\'s the container context for this element?', jslogContext: 'container-context'},
];
}
return;
}
override async getPromptDetails(): Promise<string|null> {
return `# Inspected element
${await this.describe()}`;
}
override async getUserFacingDetails(): Promise<[ContextDetail, ...ContextDetail[]]|null> {
return [
{
title: lockedString(UIStringsNotTranslate.dataUsed),
text: await this.describe(),
},
];
}
async describe(): Promise<string> {
const element = this.#node;
let output = `* Element's uid is ${element.backendNodeId()}.
* Its selector is \`${element.simpleSelector()}\``;
const childNodes = await element.getChildNodesPromise();
if (childNodes) {
const textChildNodes = childNodes.filter(childNode => childNode.nodeType() === Node.TEXT_NODE);
const elementChildNodes = childNodes.filter(childNode => childNode.nodeType() === Node.ELEMENT_NODE);
switch (elementChildNodes.length) {
case 0:
output += '\n* It doesn\'t have any child element nodes';
break;
case 1:
output += `\n* It only has 1 child element node: \`${elementChildNodes[0].simpleSelector()}\``;
break;
default:
output += `\n* It has ${elementChildNodes.length} child element nodes: ${
elementChildNodes.map(node => `\`${node.simpleSelector()}\` (uid=${node.backendNodeId()})`).join(', ')}`;
}
switch (textChildNodes.length) {
case 0:
output += '\n* It doesn\'t have any child text nodes';
break;
case 1:
output += '\n* It only has 1 child text node';
break;
default:
output += `\n* It has ${textChildNodes.length} child text nodes`;
}
}
if (element.nextSibling) {
const elementOrNodeElementNodeText = element.nextSibling.nodeType() === Node.ELEMENT_NODE ?
`an element (uid=${element.nextSibling.backendNodeId()})` :
'a non element';
output += `\n* It has a next sibling and it is ${elementOrNodeElementNodeText} node`;
}
if (element.previousSibling) {
const elementOrNodeElementNodeText = element.previousSibling.nodeType() === Node.ELEMENT_NODE ?
`an element (uid=${element.previousSibling.backendNodeId()})` :
'a non element';
output += `\n* It has a previous sibling and it is ${elementOrNodeElementNodeText} node`;
}
if (element.isInShadowTree()) {
output += '\n* It is in a shadow DOM tree.';
}
const parentNode = element.parentNode;
if (parentNode) {
const parentChildrenNodes = await parentNode.getChildNodesPromise();
output += `\n* Its parent's selector is \`${parentNode.simpleSelector()}\` (uid=${parentNode.backendNodeId()})`;
const elementOrNodeElementNodeText = parentNode.nodeType() === Node.ELEMENT_NODE ? 'an element' : 'a non element';
output += `\n* Its parent is ${elementOrNodeElementNodeText} node`;
if (parentNode.isShadowRoot()) {
output += '\n* Its parent is a shadow root.';
}
if (parentChildrenNodes) {
const childElementNodes =
parentChildrenNodes.filter(siblingNode => siblingNode.nodeType() === Node.ELEMENT_NODE);
switch (childElementNodes.length) {
case 0:
break;
case 1:
output += '\n* Its parent has only 1 child element node';
break;
default:
output += `\n* Its parent has ${childElementNodes.length} child element nodes: ${
childElementNodes.map(node => `\`${node.simpleSelector()}\` (uid=${node.backendNodeId()})`)
.join(', ')}`;
break;
}
const siblingTextNodes = parentChildrenNodes.filter(siblingNode => siblingNode.nodeType() === Node.TEXT_NODE);
switch (siblingTextNodes.length) {
case 0:
break;
case 1:
output += '\n* Its parent has only 1 child text node';
break;
default:
output += `\n* Its parent has ${siblingTextNodes.length} child text nodes: ${
siblingTextNodes.map(node => `\`${node.simpleSelector()}\``).join(', ')}`;
break;
}
}
}
return output.trim();
}
}