Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsStatusBar.ts
5263 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { n } from '../../../../../base/browser/dom.js';
7
import { ActionBar, IActionBarOptions, IActionOptions } from '../../../../../base/browser/ui/actionbar/actionbar.js';
8
import { IAction } from '../../../../../base/common/actions.js';
9
import { Codicon } from '../../../../../base/common/codicons.js';
10
import { createHotClass } from '../../../../../base/common/hotReloadHelpers.js';
11
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
12
import { autorun, derived, IObservable, observableValue } from '../../../../../base/common/observable.js';
13
import { ThemeIcon } from '../../../../../base/common/themables.js';
14
import { localize } from '../../../../../nls.js';
15
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
16
import { nativeHoverDelegate } from '../../../../../platform/hover/browser/hover.js';
17
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
18
import { IStatusbarService, StatusbarAlignment } from '../../../../services/statusbar/browser/statusbar.js';
19
import { AI_STATS_SETTING_ID } from '../settingIds.js';
20
import type { AiStatsFeature } from './aiStatsFeature.js';
21
import { ChartViewMode, createAiStatsChart, ISessionData } from './aiStatsChart.js';
22
import './media.css';
23
24
export class AiStatsStatusBar extends Disposable {
25
public static readonly hot = createHotClass(this);
26
27
constructor(
28
private readonly _aiStatsFeature: AiStatsFeature,
29
@IStatusbarService private readonly _statusbarService: IStatusbarService,
30
@ICommandService private readonly _commandService: ICommandService,
31
@ITelemetryService private readonly _telemetryService: ITelemetryService,
32
) {
33
super();
34
35
this._register(autorun((reader) => {
36
const statusBarItem = this._createStatusBar().keepUpdated(reader.store);
37
38
const store = this._register(new DisposableStore());
39
40
reader.store.add(this._statusbarService.addEntry({
41
name: localize('inlineSuggestions', "Inline Suggestions"),
42
ariaLabel: localize('inlineSuggestionsStatusBar', "Inline suggestions status bar"),
43
text: '',
44
tooltip: {
45
element: async (_token) => {
46
this._sendHoverTelemetry();
47
store.clear();
48
const elem = createAiStatsHover({
49
data: this._aiStatsFeature,
50
onOpenSettings: () => openSettingsCommand({ ids: [AI_STATS_SETTING_ID] }).run(this._commandService),
51
});
52
return elem.keepUpdated(store).element;
53
},
54
markdownNotSupportedFallback: undefined,
55
},
56
content: statusBarItem.element,
57
}, 'aiStatsStatusBar', StatusbarAlignment.RIGHT, 100));
58
}));
59
}
60
61
private _sendHoverTelemetry(): void {
62
this._telemetryService.publicLog2<{
63
aiRate: number;
64
}, {
65
owner: 'hediet';
66
comment: 'Fired when the AI stats status bar hover tooltip is shown';
67
aiRate: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The current AI rate percentage' };
68
}>(
69
'aiStatsStatusBar.hover',
70
{
71
aiRate: this._aiStatsFeature.aiRate.get(),
72
}
73
);
74
}
75
76
77
private _createStatusBar() {
78
return n.div({
79
style: {
80
height: '100%',
81
display: 'flex',
82
alignItems: 'center',
83
justifyContent: 'center',
84
marginLeft: '3px',
85
marginRight: '3px',
86
}
87
}, [
88
n.div(
89
{
90
class: 'ai-stats-status-bar',
91
style: {
92
display: 'flex',
93
flexDirection: 'column',
94
95
width: 50,
96
height: 6,
97
98
borderRadius: 6,
99
borderWidth: '1px',
100
borderStyle: 'solid',
101
}
102
},
103
[
104
n.div({
105
style: {
106
flex: 1,
107
108
display: 'flex',
109
overflow: 'hidden',
110
111
borderRadius: 6,
112
border: '1px solid transparent',
113
}
114
}, [
115
n.div({
116
style: {
117
width: this._aiStatsFeature.aiRate.map(v => `${v * 100}%`),
118
backgroundColor: 'currentColor',
119
}
120
})
121
])
122
]
123
)
124
]);
125
}
126
}
127
128
export interface IAiStatsHoverData {
129
readonly aiRate: IObservable<number>;
130
readonly acceptedInlineSuggestionsToday: IObservable<number>;
131
readonly sessions: IObservable<readonly ISessionData[]>;
132
}
133
134
export interface IAiStatsHoverOptions {
135
readonly data: IAiStatsHoverData;
136
readonly onOpenSettings?: () => void;
137
}
138
139
export function createAiStatsHover(options: IAiStatsHoverOptions) {
140
const chartViewMode = observableValue<ChartViewMode>('chartViewMode', 'days');
141
const aiRatePercent = options.data.aiRate.map(r => `${Math.round(r * 100)}%`);
142
143
const createToggleButton = (mode: ChartViewMode, tooltip: string, icon: ThemeIcon) => {
144
return derived(reader => {
145
const currentMode = chartViewMode.read(reader);
146
const isActive = currentMode === mode;
147
148
return n.div({
149
class: ['chart-toggle-button', isActive ? 'active' : ''],
150
style: {
151
padding: '2px 4px',
152
borderRadius: '3px',
153
cursor: 'pointer',
154
display: 'flex',
155
alignItems: 'center',
156
justifyContent: 'center',
157
},
158
onclick: () => {
159
chartViewMode.set(mode, undefined);
160
},
161
title: tooltip,
162
}, [
163
n.div({
164
class: ThemeIcon.asClassName(icon),
165
style: { fontSize: '14px' }
166
})
167
]);
168
});
169
};
170
171
return n.div({
172
class: 'ai-stats-status-bar',
173
}, [
174
n.div({
175
class: 'header',
176
style: {
177
minWidth: '280px',
178
}
179
},
180
[
181
n.div({ style: { flex: 1 } }, [localize('aiStatsStatusBarHeader', "AI Usage Statistics")]),
182
n.div({ style: { marginLeft: 'auto' } }, options.onOpenSettings
183
? actionBar([
184
{
185
action: {
186
id: 'aiStats.statusBar.settings',
187
label: '',
188
enabled: true,
189
run: options.onOpenSettings,
190
class: ThemeIcon.asClassName(Codicon.gear),
191
tooltip: localize('aiStats.statusBar.configure', "Configure")
192
},
193
options: { icon: true, label: false, hoverDelegate: nativeHoverDelegate }
194
}
195
])
196
: [])
197
]
198
),
199
200
n.div({ style: { display: 'flex' } }, [
201
n.div({ style: { flex: 1, paddingRight: '4px' } }, [
202
localize('text1', "AI vs Typing Average: {0}", aiRatePercent.get()),
203
]),
204
]),
205
n.div({ style: { flex: 1, paddingRight: '4px' } }, [
206
localize('text2', "Accepted inline suggestions today: {0}", options.data.acceptedInlineSuggestionsToday.get()),
207
]),
208
209
// Chart section
210
n.div({
211
style: {
212
marginTop: '8px',
213
borderTop: '1px solid var(--vscode-widget-border)',
214
paddingTop: '8px',
215
}
216
}, [
217
// Chart header with toggle
218
n.div({
219
class: 'header',
220
style: {
221
display: 'flex',
222
alignItems: 'center',
223
marginBottom: '4px',
224
}
225
}, [
226
n.div({ style: { flex: 1 } }, [
227
chartViewMode.map(mode =>
228
mode === 'days'
229
? localize('chartHeaderDays', "AI Rate by Day")
230
: localize('chartHeaderSessions', "AI Rate by Session")
231
)
232
]),
233
n.div({
234
class: 'chart-view-toggle',
235
style: { marginLeft: 'auto', display: 'flex', gap: '2px' }
236
}, [
237
createToggleButton('days', localize('viewByDays', "Days"), Codicon.calendar),
238
createToggleButton('sessions', localize('viewBySessions', "Sessions"), Codicon.listFlat),
239
])
240
]),
241
242
// Chart container
243
derived(reader => {
244
const sessions = options.data.sessions.read(reader);
245
const viewMode = chartViewMode.read(reader);
246
return n.div({
247
ref: (container) => {
248
const chart = createAiStatsChart({
249
sessions,
250
viewMode,
251
});
252
container.appendChild(chart);
253
}
254
});
255
}),
256
]),
257
]);
258
}
259
260
function actionBar(actions: { action: IAction; options: IActionOptions }[], options?: IActionBarOptions) {
261
return derived((_reader) => n.div({
262
class: [],
263
style: {
264
},
265
ref: elem => {
266
const actionBar = _reader.store.add(new ActionBar(elem, options));
267
for (const { action, options } of actions) {
268
actionBar.push(action, options);
269
}
270
}
271
}));
272
}
273
274
class CommandWithArgs {
275
constructor(
276
public readonly commandId: string,
277
public readonly args: unknown[] = [],
278
) { }
279
280
public run(commandService: ICommandService): void {
281
commandService.executeCommand(this.commandId, ...this.args);
282
}
283
}
284
285
function openSettingsCommand(options: { ids?: string[] } = {}) {
286
return new CommandWithArgs('workbench.action.openSettings', [{
287
query: options.ids ? options.ids.map(id => `@id:${id}`).join(' ') : undefined,
288
}]);
289
}
290
291