chrome-devtools-frontend
Version:
Chrome DevTools UI
237 lines (213 loc) • 9.04 kB
text/typescript
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../ui/components/spinners/spinners.js';
import '../../ui/components/tooltips/tooltips.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Root from '../../core/root/root.js';
import * as UI from '../../ui/legacy/legacy.js';
import {Directives, html, nothing, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import styles from './aiCodeCompletionDisclaimer.css.js';
const UIStringsNotTranslate = {
/**
* @description Disclaimer text for AI code completion
*/
relevantData: 'Relevant data',
/**
* @description Disclaimer text for AI code completion
*/
isSentToGoogle: 'is sent to Google',
/**
* @description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code completion.
*/
tooltipDisclaimerTextForAiCodeCompletion:
'To generate code suggestions, your console input and the history of your current console session are shared with Google. This data may be seen by human reviewers to improve this feature.',
/**
* @description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code completion.
*/
tooltipDisclaimerTextForAiCodeCompletionNoLogging:
'To generate code suggestions, your console input and the history of your current console session are shared with Google. This data will not be used to improve Google’s AI models.',
/**
* Text for tooltip shown on hovering over spinner.
*/
tooltipTextForSpinner: 'Shows when data is being sent to Google to generate code suggestions',
/**
* @description Text for tooltip button which redirects to AI settings
*/
manageInSettings: 'Manage in settings',
/**
*@description Text announced when request is sent to AIDA and the spinner is loading
*/
dataIsBeingSentToGoogle: 'Data is being sent to Google',
} as const;
const lockedString = i18n.i18n.lockedString;
export interface ViewInput {
disclaimerTooltipId?: string;
spinnerTooltipId?: string;
noLogging: boolean;
aidaAvailability?: Host.AidaClient.AidaAccessPreconditions;
onManageInSettingsTooltipClick: () => void;
}
export interface ViewOutput {
hideTooltip?: () => void;
setLoading?: (isLoading: boolean) => void;
}
export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void;
export const DEFAULT_SUMMARY_TOOLBAR_VIEW: View =
(input, output, target) => {
if (input.aidaAvailability !== Host.AidaClient.AidaAccessPreconditions.AVAILABLE || !input.disclaimerTooltipId ||
!input.spinnerTooltipId) {
render(nothing, target);
return;
}
// clang-format off
render(
html`
<style>${styles}</style>
<div class="ai-code-completion-disclaimer"><devtools-spinner
.active=${false}
${Directives.ref(el => {
if (el instanceof HTMLElement) {
output.setLoading = (isLoading: boolean) => {
el.toggleAttribute('active', isLoading);
};
}
})}
aria-details=${input.spinnerTooltipId}
aria-describedby=${input.spinnerTooltipId}></devtools-spinner>
<devtools-tooltip
id=${input.spinnerTooltipId}
variant=${'rich'}
jslogContext=${'ai-code-completion-spinner-tooltip'}>
<div class="disclaimer-tooltip-container"><div class="tooltip-text">
${lockedString(UIStringsNotTranslate.tooltipTextForSpinner)}
</div></div></devtools-tooltip>
<span
tabIndex="0"
class="link"
role="link"
jslog=${VisualLogging.link('open-ai-settings').track({
click: true,
})}
aria-details=${input.disclaimerTooltipId}
aria-describedby=${input.disclaimerTooltipId}
@click=${() => {
void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
}}
>${lockedString(UIStringsNotTranslate.relevantData)}</span>${lockedString(UIStringsNotTranslate.isSentToGoogle)}
<devtools-tooltip
id=${input.disclaimerTooltipId}
variant=${'rich'}
jslogContext=${'ai-code-completion-disclaimer'}
${Directives.ref(el => {
if (el instanceof HTMLElement) {
output.hideTooltip = () => {
el.hidePopover();
};
}
})}>
<div class="disclaimer-tooltip-container"><div class="tooltip-text">
${input.noLogging ? lockedString(UIStringsNotTranslate.tooltipDisclaimerTextForAiCodeCompletionNoLogging) : lockedString(UIStringsNotTranslate.tooltipDisclaimerTextForAiCodeCompletion)}
</div>
<span
tabIndex="0"
class="link"
role="link"
jslog=${VisualLogging.link('open-ai-settings').track({
click: true,
})}
@click=${input.onManageInSettingsTooltipClick}
>${lockedString(UIStringsNotTranslate.manageInSettings)}</span></div></devtools-tooltip>
</div>
`, target);
// clang-format on
};
const MINIMUM_LOADING_STATE_TIMEOUT = 1000;
export class AiCodeCompletionDisclaimer extends UI.Widget.Widget {
readonly #view: View;
#viewOutput: ViewOutput = {};
#spinnerTooltipId?: string;
#disclaimerTooltipId?: string;
#noLogging: boolean; // Whether the enterprise setting is `ALLOW_WITHOUT_LOGGING` or not.
#loading = false;
#loadingStartTime = 0;
#spinnerLoadingTimeout: number|undefined;
#aidaAvailability?: Host.AidaClient.AidaAccessPreconditions;
#boundOnAidaAvailabilityChange: () => Promise<void>;
constructor(element?: HTMLElement, view: View = DEFAULT_SUMMARY_TOOLBAR_VIEW) {
super(element);
this.markAsExternallyManaged();
this.#noLogging = Root.Runtime.hostConfig.aidaAvailability?.enterprisePolicyValue ===
Root.Runtime.GenAiEnterprisePolicyValue.ALLOW_WITHOUT_LOGGING;
this.#boundOnAidaAvailabilityChange = this.#onAidaAvailabilityChange.bind(this);
this.#view = view;
}
set disclaimerTooltipId(disclaimerTooltipId: string) {
this.#disclaimerTooltipId = disclaimerTooltipId;
this.requestUpdate();
}
set spinnerTooltipId(spinnerTooltipId: string) {
this.#spinnerTooltipId = spinnerTooltipId;
this.requestUpdate();
}
set loading(loading: boolean) {
if (!loading && !this.#loading) {
return;
}
if (loading) {
if (!this.#loading) {
this.#viewOutput.setLoading?.(true);
UI.ARIAUtils.LiveAnnouncer.status(lockedString(UIStringsNotTranslate.dataIsBeingSentToGoogle));
}
if (this.#spinnerLoadingTimeout) {
clearTimeout(this.#spinnerLoadingTimeout);
this.#spinnerLoadingTimeout = undefined;
}
this.#loadingStartTime = performance.now();
this.#loading = true;
} else {
this.#loading = false;
const duration = performance.now() - this.#loadingStartTime;
const remainingTime = Math.max(MINIMUM_LOADING_STATE_TIMEOUT - duration, 0);
this.#spinnerLoadingTimeout = window.setTimeout(() => {
this.#viewOutput.setLoading?.(false);
this.#spinnerLoadingTimeout = undefined;
}, remainingTime);
}
}
async #onAidaAvailabilityChange(): Promise<void> {
const currentAidaAvailability = await Host.AidaClient.AidaClient.checkAccessPreconditions();
if (currentAidaAvailability !== this.#aidaAvailability) {
this.#aidaAvailability = currentAidaAvailability;
this.requestUpdate();
}
}
#onManageInSettingsTooltipClick(): void {
this.#viewOutput.hideTooltip?.();
void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
}
override performUpdate(): void {
this.#view(
{
disclaimerTooltipId: this.#disclaimerTooltipId,
spinnerTooltipId: this.#spinnerTooltipId,
noLogging: this.#noLogging,
aidaAvailability: this.#aidaAvailability,
onManageInSettingsTooltipClick: this.#onManageInSettingsTooltipClick.bind(this),
},
this.#viewOutput, this.contentElement);
}
override wasShown(): void {
super.wasShown();
Host.AidaClient.HostConfigTracker.instance().addEventListener(
Host.AidaClient.Events.AIDA_AVAILABILITY_CHANGED, this.#boundOnAidaAvailabilityChange);
void this.#onAidaAvailabilityChange();
}
override willHide(): void {
super.willHide();
Host.AidaClient.HostConfigTracker.instance().removeEventListener(
Host.AidaClient.Events.AIDA_AVAILABILITY_CHANGED, this.#boundOnAidaAvailabilityChange);
}
}