chrome-devtools-frontend
Version:
Chrome DevTools UI
275 lines (248 loc) • 9.42 kB
text/typescript
// Copyright 2025 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as Root from '../../core/root/root.js';
let builtInAiInstance: BuiltInAi|undefined;
export interface LanguageModel {
promptStreaming: (arg0: string, opts?: {
signal?: AbortSignal,
}) => AsyncGenerator<string>;
clone: () => Promise<LanguageModel>;
destroy: () => void;
}
export const enum LanguageModelAvailability {
UNAVAILABLE = 'unavailable',
DOWNLOADABLE = 'downloadable',
DOWNLOADING = 'downloading',
AVAILABLE = 'available',
DISABLED = 'disabled',
}
export class BuiltInAi extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
#availability: LanguageModelAvailability|null = null;
#hasGpu: boolean;
#consoleInsightsSession?: LanguageModel;
initDoneForTesting: Promise<void>;
#downloadProgress: number|null = null;
#currentlyCreatingSession = false;
static instance(): BuiltInAi {
if (builtInAiInstance === undefined) {
builtInAiInstance = new BuiltInAi();
}
return builtInAiInstance;
}
constructor() {
super();
this.#hasGpu = this.#isGpuAvailable();
this.initDoneForTesting =
this.getLanguageModelAvailability().then(() => this.#sendAvailabilityMetrics()).then(() => this.initialize());
}
async getLanguageModelAvailability(): Promise<LanguageModelAvailability> {
if (!Root.Runtime.hostConfig.devToolsAiPromptApi?.enabled) {
this.#availability = LanguageModelAvailability.DISABLED;
return this.#availability;
}
try {
// @ts-expect-error
this.#availability = await window.LanguageModel.availability({
expectedInputs: [{
type: 'text',
languages: ['en'],
}],
expectedOutputs: [{
type: 'text',
languages: ['en'],
}],
}) as LanguageModelAvailability;
} catch {
this.#availability = LanguageModelAvailability.UNAVAILABLE;
}
return this.#availability;
}
isDownloading(): boolean {
return this.#availability === LanguageModelAvailability.DOWNLOADING;
}
isEventuallyAvailable(): boolean {
if (!this.#hasGpu && !Boolean(Root.Runtime.hostConfig.devToolsAiPromptApi?.allowWithoutGpu)) {
return false;
}
return this.#availability === LanguageModelAvailability.AVAILABLE ||
this.#availability === LanguageModelAvailability.DOWNLOADING ||
this.#availability === LanguageModelAvailability.DOWNLOADABLE;
}
#setDownloadProgress(newValue: number): void {
this.#downloadProgress = newValue;
this.dispatchEventToListeners(Events.DOWNLOAD_PROGRESS_CHANGED, this.#downloadProgress);
}
getDownloadProgress(): number|null {
return this.#downloadProgress;
}
startDownloadingModel(): void {
if (!Root.Runtime.hostConfig.devToolsAiPromptApi?.allowWithoutGpu && !this.#hasGpu) {
return;
}
if (this.#availability !== LanguageModelAvailability.DOWNLOADABLE) {
return;
}
void this.#createSession();
// Without the timeout, the returned availability would still be `downloadable`
setTimeout(() => {
void this.getLanguageModelAvailability();
}, 1000);
}
#isGpuAvailable(): boolean {
const canvas = document.createElement('canvas');
try {
const webgl = canvas.getContext('webgl');
if (!webgl) {
return false;
}
const debugInfo = webgl.getExtension('WEBGL_debug_renderer_info');
if (!debugInfo) {
return false;
}
const renderer = webgl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
if (renderer.includes('SwiftShader')) {
return false;
}
} catch {
return false;
}
return true;
}
hasSession(): boolean {
return Boolean(this.#consoleInsightsSession);
}
async initialize(): Promise<void> {
if (!Root.Runtime.hostConfig.devToolsAiPromptApi?.allowWithoutGpu && !this.#hasGpu) {
return;
}
if (this.#availability !== LanguageModelAvailability.AVAILABLE &&
this.#availability !== LanguageModelAvailability.DOWNLOADING) {
return;
}
await this.#createSession();
}
async #createSession(): Promise<void> {
if (this.#currentlyCreatingSession) {
return;
}
this.#currentlyCreatingSession = true;
const monitor = (m: EventTarget): void => {
m.addEventListener('downloadprogress', ((e: {loaded: number}) => {
this.#setDownloadProgress(e.loaded);
}) as unknown as EventListener);
};
try {
// @ts-expect-error
this.#consoleInsightsSession = await window.LanguageModel.create({
monitor,
initialPrompts: [{
role: 'system',
content: `
You are an expert web developer. Your goal is to help a human web developer who
is using Chrome DevTools to debug a web site or web app. The Chrome DevTools
console is showing a message which is either an error or a warning. Please help
the user understand the problematic console message.
Your instructions are as follows:
- Explain the reason why the error or warning is showing up.
- The explanation has a maximum length of 200 characters. Anything beyond this
length will be cut off. Make sure that your explanation is at most 200 characters long.
- Your explanation should not end in the middle of a sentence.
- Your explanation should consist of a single paragraph only. Do not include any
headings or code blocks. Only write a single paragraph of text.
- Your response should be concise and to the point. Avoid lengthy explanations
or unnecessary details.
`
}],
expectedInputs: [{
type: 'text',
languages: ['en'],
}],
expectedOutputs: [{
type: 'text',
languages: ['en'],
}],
});
if (this.#availability !== LanguageModelAvailability.AVAILABLE) {
this.dispatchEventToListeners(Events.DOWNLOADED_AND_SESSION_CREATED);
void this.getLanguageModelAvailability();
}
} catch (e) {
console.error('Error when creating LanguageModel session', e.message);
}
this.#currentlyCreatingSession = false;
}
static removeInstance(): void {
builtInAiInstance = undefined;
}
async * getConsoleInsight(prompt: string, abortController: AbortController): AsyncGenerator<string> {
if (!this.#consoleInsightsSession) {
return;
}
// Clone the session to start a fresh conversation for each answer. Otherwise
// previous dialog would pollute the context resulting in worse answers.
let session: LanguageModel|null = null;
try {
session = await this.#consoleInsightsSession.clone();
const stream = session.promptStreaming(prompt, {
signal: abortController.signal,
});
for await (const chunk of stream) {
yield chunk;
}
} finally {
if (session) {
session.destroy();
}
}
}
#sendAvailabilityMetrics(): void {
if (this.#hasGpu) {
switch (this.#availability) {
case LanguageModelAvailability.UNAVAILABLE:
Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.UNAVAILABLE_HAS_GPU);
break;
case LanguageModelAvailability.DOWNLOADABLE:
Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.DOWNLOADABLE_HAS_GPU);
break;
case LanguageModelAvailability.DOWNLOADING:
Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.DOWNLOADING_HAS_GPU);
break;
case LanguageModelAvailability.AVAILABLE:
Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.AVAILABLE_HAS_GPU);
break;
case LanguageModelAvailability.DISABLED:
Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.DISABLED_HAS_GPU);
break;
}
} else {
switch (this.#availability) {
case LanguageModelAvailability.UNAVAILABLE:
Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.UNAVAILABLE_NO_GPU);
break;
case LanguageModelAvailability.DOWNLOADABLE:
Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.DOWNLOADABLE_NO_GPU);
break;
case LanguageModelAvailability.DOWNLOADING:
Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.DOWNLOADING_NO_GPU);
break;
case LanguageModelAvailability.AVAILABLE:
Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.AVAILABLE_NO_GPU);
break;
case LanguageModelAvailability.DISABLED:
Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.DISABLED_NO_GPU);
break;
}
}
}
}
export const enum Events {
DOWNLOAD_PROGRESS_CHANGED = 'downloadProgressChanged',
DOWNLOADED_AND_SESSION_CREATED = 'downloadedAndSessionCreated',
}
export interface EventTypes {
[Events.DOWNLOAD_PROGRESS_CHANGED]: number;
[Events.DOWNLOADED_AND_SESSION_CREATED]: void;
}