UNPKG

chrome-devtools-frontend

Version:
275 lines (248 loc) • 9.42 kB
// 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; }