Path: blob/main/src/vs/workbench/contrib/chat/browser/chatManagement/chatUsageWidget.ts
4780 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './media/chatUsageWidget.css';6import { Disposable } from '../../../../../base/common/lifecycle.js';7import { Emitter } from '../../../../../base/common/event.js';8import * as DOM from '../../../../../base/browser/dom.js';9import { localize } from '../../../../../nls.js';10import { IChatEntitlementService, IQuotaSnapshot } from '../../../../services/chat/common/chatEntitlementService.js';11import { language } from '../../../../../base/common/platform.js';12import { safeIntl } from '../../../../../base/common/date.js';1314const $ = DOM.$;1516export class ChatUsageWidget extends Disposable {1718private readonly _onDidChangeContentHeight = new Emitter<number>();19readonly onDidChangeContentHeight = this._onDidChangeContentHeight.event;2021readonly element: HTMLElement;22private usageSection!: HTMLElement;2324private readonly dateFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric' });25private readonly dateTimeFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });2627constructor(28@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService29) {30super();3132this.element = DOM.$('.chat-usage-widget');33this.create(this.element);34this.render();3536// Update when quotas or entitlements change37this._register(this.chatEntitlementService.onDidChangeQuotaRemaining(() => this.render()));38this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.render()));39}4041private create(container: HTMLElement): void {42// Content container43this.usageSection = DOM.append(container, $('.copilot-usage-section'));44}4546private render(): void {47DOM.clearNode(this.usageSection);4849const { chat: chatQuota, completions: completionsQuota, premiumChat: premiumChatQuota, resetDate, resetDateHasTime } = this.chatEntitlementService.quotas;5051// Anonymous Indicator - show limited quotas52if (this.chatEntitlementService.anonymous && this.chatEntitlementService.sentiment.installed && !completionsQuota && !chatQuota && !premiumChatQuota) {53this.renderLimitedQuotaItem(this.usageSection, localize('completionsLabel', 'Inline Suggestions'));54this.renderLimitedQuotaItem(this.usageSection, localize('chatsLabel', 'Chat messages'));55}56// Copilot Usage section - show detailed breakdown of all quotas57else if (completionsQuota || chatQuota || premiumChatQuota) {58// Inline Suggestions59if (completionsQuota) {60this.renderQuotaItem(this.usageSection, localize('plan.inlineSuggestions', 'Inline Suggestions'), completionsQuota);61}6263// Chat messages64if (chatQuota) {65this.renderQuotaItem(this.usageSection, localize('plan.chatMessages', 'Chat messages'), chatQuota);66}6768// Premium requests69if (premiumChatQuota) {70this.renderQuotaItem(this.usageSection, localize('plan.premiumRequests', 'Premium requests'), premiumChatQuota);7172// Additional overage message73if (premiumChatQuota.overageEnabled) {74const overageMessage = DOM.append(this.usageSection, $('.overage-message'));75overageMessage.textContent = localize('plan.additionalPaidEnabled', 'Additional paid premium requests enabled.');76}77}7879// Reset date80if (resetDate) {81const resetText = DOM.append(this.usageSection, $('.allowance-resets'));82resetText.textContent = localize('plan.allowanceResets', 'Allowance resets {0}.', resetDateHasTime ? this.dateTimeFormatter.value.format(new Date(resetDate)) : this.dateFormatter.value.format(new Date(resetDate)));83}84}8586// Emit height change87const height = this.element.offsetHeight || 400;88this._onDidChangeContentHeight.fire(height);89}9091private renderQuotaItem(container: HTMLElement, label: string, quota: IQuotaSnapshot): void {92const quotaItem = DOM.append(container, $('.quota-item'));9394const quotaItemHeader = DOM.append(quotaItem, $('.quota-item-header'));95const quotaItemLabel = DOM.append(quotaItemHeader, $('.quota-item-label'));96quotaItemLabel.textContent = label;9798const quotaItemValue = DOM.append(quotaItemHeader, $('.quota-item-value'));99if (quota.unlimited) {100quotaItemValue.textContent = localize('plan.included', 'Included');101} else {102quotaItemValue.textContent = localize('plan.included', 'Included');103}104105// Progress bar - using same structure as chat status106const progressBarContainer = DOM.append(quotaItem, $('.quota-bar'));107const progressBar = DOM.append(progressBarContainer, $('.quota-bit'));108const percentageUsed = this.getQuotaPercentageUsed(quota);109progressBar.style.width = percentageUsed + '%';110111// Apply warning/error classes based on usage112if (percentageUsed >= 90) {113quotaItem.classList.add('error');114} else if (percentageUsed >= 75) {115quotaItem.classList.add('warning');116}117}118119private getQuotaPercentageUsed(quota: IQuotaSnapshot): number {120if (quota.unlimited) {121return 0;122}123return Math.max(0, 100 - quota.percentRemaining);124}125126private renderLimitedQuotaItem(container: HTMLElement, label: string): void {127const quotaItem = DOM.append(container, $('.quota-item'));128129const quotaItemHeader = DOM.append(quotaItem, $('.quota-item-header'));130const quotaItemLabel = DOM.append(quotaItemHeader, $('.quota-item-label'));131quotaItemLabel.textContent = label;132133const quotaItemValue = DOM.append(quotaItemHeader, $('.quota-item-value'));134quotaItemValue.textContent = localize('quotaLimited', 'Limited');135136// Progress bar - using same structure as chat status137const progressBarContainer = DOM.append(quotaItem, $('.quota-bar'));138DOM.append(progressBarContainer, $('.quota-bit'));139}140}141142143