dsh-codinput 0.3.7 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -13
- package/NOTICE +18 -3
- package/README.en.md +10 -7
- package/README.md +10 -7
- package/TESTING.md +19 -5
- package/lib/client.js +66 -56
- package/package.json +2 -2
- package/src/client/components/StatsRow.tsx +199 -78
- package/src/client/components/UserMessageMarkdown.tsx +115 -30
- package/src/client/i18n.ts +13 -9
- package/src/client/index.tsx +6 -1
- package/src/client/styles.ts +40 -24
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-codinput",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "DSH
|
|
3
|
+
"version": "0.3.8",
|
|
4
|
+
"description": "DSH 插件:把聊天输入框变成代码编辑器一样的输入风格(CodeMirror 6 接管 conversation.composer.bar),web 与桌面端通用。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Witherwithwinter",
|
|
7
7
|
"icon": "./assets/icon.png",
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/** 数据行(卡片下方):官方 composer dock 复刻——两枚胶囊(仪表盘「轮/步 · tok/s」、
|
|
2
2
|
* 数据库「累计 tok · 缓存命中」)+ 上下文小圈(0.1.7-rc.2 起官方把它挪进本行,圈+百分比
|
|
3
|
-
* 药丸),点击弹出官方同构详情面板(会话统计 / Token 用量 /
|
|
4
|
-
*
|
|
3
|
+
* 药丸),点击弹出官方同构详情面板(会话统计 / Token 用量 / 上下文分段)。面板照官方
|
|
4
|
+
* ContextMeter / useStatDialog 机制:portal 到 body + fixed,经 useAnchoredPanel(官方
|
|
5
|
+
* useAnchoredPosition 插件侧移植)按锚点视口矩形定位(side top / align start、gap 8、
|
|
6
|
+
* 视口 margin 12 夹取,滚动(捕获)/resize/面板尺寸变化重算);每枚面板一个
|
|
7
|
+
* useStatDialogSeat(锚点/面板 ref + 定位 + 点外与 Escape 关闭),三枚共用 StatsRow 的
|
|
8
|
+
* 单一展开态互斥(与官方 chat StatsPills 的 openPill 同语义)。 */
|
|
5
9
|
|
|
6
|
-
import { useEffect, useRef, useState } from 'react';
|
|
10
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
11
|
+
import { createPortal } from 'react-dom';
|
|
7
12
|
import { useSessionStats, formatTokens, formatTokensPerSecond, formatCacheHitPercent, formatDuration, type SessionStats } from '../stats';
|
|
8
13
|
import type { UseProjection } from '../host-types';
|
|
9
14
|
import { t } from '../i18n';
|
|
@@ -37,16 +42,114 @@ export function DatabaseIcon({ size = 14 }: { size?: number }): JSX.Element {
|
|
|
37
42
|
);
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
/**
|
|
45
|
+
/** 官方 useAnchoredPosition 的插件侧移植(primitives 不可 import):fixed 面板按锚点
|
|
46
|
+
* 视口矩形定位(side top / align start:面板左缘对齐锚点左缘、底距锚点顶 gap),
|
|
47
|
+
* 两侧按 margin 夹取(顶边用官方 overlayTopMargin:帧顶 clearance+20 与 margin 取大),
|
|
48
|
+
* 滚动(捕获相位)/resize/面板自身尺寸变化重算。返回 null = 尚未量得(面板隐藏)。 */
|
|
49
|
+
function useAnchoredPanel(
|
|
50
|
+
open: boolean,
|
|
51
|
+
anchorRef: React.RefObject<HTMLElement | null>,
|
|
52
|
+
panelRef: React.RefObject<HTMLElement | null>,
|
|
53
|
+
gap: number,
|
|
54
|
+
margin: number,
|
|
55
|
+
): { left: number; top: number } | null {
|
|
56
|
+
const [position, setPosition] = useState<{ left: number; top: number } | null>(null);
|
|
57
|
+
useLayoutEffect(() => {
|
|
58
|
+
if (!open) {
|
|
59
|
+
setPosition(null);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// 官方 overlayTopMargin:帧顶 clearance(data-fullscreen 归零)+20,与 margin 取大。
|
|
63
|
+
const topMargin = (): number => {
|
|
64
|
+
const clearance = Number.parseFloat(
|
|
65
|
+
getComputedStyle(document.documentElement).getPropertyValue('--dsh-frame-top-clearance'),
|
|
66
|
+
);
|
|
67
|
+
if (Number.isNaN(clearance)) return margin;
|
|
68
|
+
return Math.max(margin, (document.documentElement.hasAttribute('data-fullscreen') ? 0 : clearance) + 20);
|
|
69
|
+
};
|
|
70
|
+
// 与官方 place() 同构:rect 存在就落位,宽/高为 0 时只跳过对应轴的夹取。
|
|
71
|
+
const place = (): void => {
|
|
72
|
+
const rect = anchorRef.current?.getBoundingClientRect();
|
|
73
|
+
if (rect === undefined) return;
|
|
74
|
+
const panel = panelRef.current;
|
|
75
|
+
const width = panel?.offsetWidth ?? 0;
|
|
76
|
+
const height = panel?.offsetHeight ?? 0;
|
|
77
|
+
let left = rect.left;
|
|
78
|
+
let top = rect.top - gap - height;
|
|
79
|
+
if (width > 0) left = Math.min(Math.max(left, margin), window.innerWidth - width - margin);
|
|
80
|
+
if (height > 0) top = Math.min(Math.max(top, topMargin()), window.innerHeight - height - margin);
|
|
81
|
+
setPosition((prev) => (prev && prev.left === left && prev.top === top ? prev : { left, top }));
|
|
82
|
+
};
|
|
83
|
+
place();
|
|
84
|
+
window.addEventListener('scroll', place, true);
|
|
85
|
+
window.addEventListener('resize', place);
|
|
86
|
+
const panel = panelRef.current;
|
|
87
|
+
let observer: ResizeObserver | null = null;
|
|
88
|
+
if (typeof ResizeObserver !== 'undefined' && panel !== null) {
|
|
89
|
+
observer = new ResizeObserver(place);
|
|
90
|
+
observer.observe(panel);
|
|
91
|
+
}
|
|
92
|
+
return () => {
|
|
93
|
+
observer?.disconnect();
|
|
94
|
+
window.removeEventListener('scroll', place, true);
|
|
95
|
+
window.removeEventListener('resize', place);
|
|
96
|
+
};
|
|
97
|
+
}, [open, anchorRef, panelRef, gap, margin]);
|
|
98
|
+
return position;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 官方 useStatDialog 的插件侧等价 seat(primitives/chat 的 useStatDialog 不可 import):
|
|
102
|
+
* 每枚面板自持锚点/面板 ref + 定位,并在打开期间挂「点外关闭 + Escape 关闭」(官方语义:
|
|
103
|
+
* 锚点或 portal 面板内的 pointerdown 算「内」;Escape 走 document 冒泡)。父级用受控
|
|
104
|
+
* open 维持三枚面板互斥,与官方 chat StatsPills 的 openPill 同构。 */
|
|
105
|
+
interface StatDialogSeat {
|
|
106
|
+
rootRef: React.MutableRefObject<HTMLSpanElement | null>;
|
|
107
|
+
panelRef: React.MutableRefObject<HTMLDivElement | null>;
|
|
108
|
+
position: { left: number; top: number } | null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function useStatDialogSeat(open: boolean, onClose: () => void): StatDialogSeat {
|
|
112
|
+
const rootRef = useRef<HTMLSpanElement | null>(null);
|
|
113
|
+
const panelRef = useRef<HTMLDivElement | null>(null);
|
|
114
|
+
const position = useAnchoredPanel(open, rootRef, panelRef, 8, 12);
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (!open) return;
|
|
117
|
+
const onDown = (event: PointerEvent): void => {
|
|
118
|
+
if (!(event.target instanceof Node)) return;
|
|
119
|
+
if (rootRef.current?.contains(event.target) === true) return;
|
|
120
|
+
if (panelRef.current?.contains(event.target) === true) return;
|
|
121
|
+
onClose();
|
|
122
|
+
};
|
|
123
|
+
const onKeyDown = (event: KeyboardEvent): void => {
|
|
124
|
+
if (event.key === 'Escape') onClose();
|
|
125
|
+
};
|
|
126
|
+
document.addEventListener('pointerdown', onDown);
|
|
127
|
+
document.addEventListener('keydown', onKeyDown);
|
|
128
|
+
return () => {
|
|
129
|
+
document.removeEventListener('pointerdown', onDown);
|
|
130
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
131
|
+
};
|
|
132
|
+
}, [open, onClose]);
|
|
133
|
+
return { rootRef, panelRef, position };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 面板定位样式(未量得前官方同款:隐藏在原点)。 */
|
|
137
|
+
function panelStyle(position: { left: number; top: number } | null): React.CSSProperties {
|
|
138
|
+
return position ?? { visibility: 'hidden', left: 0, top: 0 };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 官方详情面板(bRhRbq:标题行 + 发丝线 + dt/dd 栅格)。portal 到 body,fixed 定位。 */
|
|
41
142
|
function StatPanel(props: {
|
|
42
143
|
label: string;
|
|
43
144
|
icon: JSX.Element;
|
|
44
145
|
titleValue?: string;
|
|
45
146
|
rows: readonly [string, string][];
|
|
147
|
+
panelRef: React.MutableRefObject<HTMLDivElement | null>;
|
|
148
|
+
position: { left: number; top: number } | null;
|
|
46
149
|
}): JSX.Element {
|
|
47
|
-
const { label, icon, titleValue, rows } = props;
|
|
48
|
-
return (
|
|
49
|
-
<div className="dci-stat-panel" role="dialog" aria-label={label}>
|
|
150
|
+
const { label, icon, titleValue, rows, panelRef, position } = props;
|
|
151
|
+
return createPortal(
|
|
152
|
+
<div className="dci-stat-panel" role="dialog" aria-label={label} ref={panelRef} style={panelStyle(position)}>
|
|
50
153
|
<div className="dci-stat-panelTitle">
|
|
51
154
|
<span className="dci-stat-panelLabel">
|
|
52
155
|
{icon}
|
|
@@ -63,7 +166,8 @@ function StatPanel(props: {
|
|
|
63
166
|
</div>
|
|
64
167
|
))}
|
|
65
168
|
</dl>
|
|
66
|
-
</div
|
|
169
|
+
</div>,
|
|
170
|
+
document.body,
|
|
67
171
|
);
|
|
68
172
|
}
|
|
69
173
|
|
|
@@ -76,8 +180,9 @@ function exactCount(value: number | undefined): string {
|
|
|
76
180
|
}
|
|
77
181
|
|
|
78
182
|
/** 官方 TimePill:轮/步 + · tok/s(decodeMs>0 才出),面板 = 模型/工具用时、TTFT 均值、TPS。 */
|
|
79
|
-
function Pill(props: { stats: SessionStats; open: boolean; onToggle(): void }): JSX.Element {
|
|
80
|
-
const { stats, open, onToggle } = props;
|
|
183
|
+
function Pill(props: { stats: SessionStats; open: boolean; onToggle(): void; onClose(): void }): JSX.Element {
|
|
184
|
+
const { stats, open, onToggle, onClose } = props;
|
|
185
|
+
const { rootRef, panelRef, position } = useStatDialogSeat(open, onClose);
|
|
81
186
|
const roundsPart = t('stats.turns', { turns: stats.turns ?? 0, steps: stats.steps ?? 0 });
|
|
82
187
|
const tps =
|
|
83
188
|
(stats.decodeMs ?? 0) > 0 ? formatTokensPerSecond((stats.decodeTokens ?? 0) / ((stats.decodeMs ?? 1) / 1000)) : null;
|
|
@@ -97,7 +202,7 @@ function Pill(props: { stats: SessionStats; open: boolean; onToggle(): void }):
|
|
|
97
202
|
// 官方:无任何计时数据时胶囊不可点(无面板可看)。
|
|
98
203
|
if (!hasTimings) {
|
|
99
204
|
return (
|
|
100
|
-
<span className="dci-stat-anchor">
|
|
205
|
+
<span className="dci-stat-anchor" ref={rootRef}>
|
|
101
206
|
<span className="dci-stat-pill">
|
|
102
207
|
<GaugeIcon />
|
|
103
208
|
{label}
|
|
@@ -106,7 +211,7 @@ function Pill(props: { stats: SessionStats; open: boolean; onToggle(): void }):
|
|
|
106
211
|
);
|
|
107
212
|
}
|
|
108
213
|
return (
|
|
109
|
-
<span className="dci-stat-anchor">
|
|
214
|
+
<span className="dci-stat-anchor" ref={rootRef}>
|
|
110
215
|
<button
|
|
111
216
|
type="button"
|
|
112
217
|
className="dci-stat-pill"
|
|
@@ -122,6 +227,8 @@ function Pill(props: { stats: SessionStats; open: boolean; onToggle(): void }):
|
|
|
122
227
|
<StatPanel
|
|
123
228
|
label={t("stats.session")}
|
|
124
229
|
icon={<GaugeIcon />}
|
|
230
|
+
panelRef={panelRef}
|
|
231
|
+
position={position}
|
|
125
232
|
rows={[
|
|
126
233
|
[t('stats.modelTime'), (stats.llmMs ?? 0) > 0 ? formatDuration(stats.llmMs!) : '—'],
|
|
127
234
|
[t('stats.toolTime'), (stats.toolMs ?? 0) > 0 ? formatDuration(stats.toolMs!) : '—'],
|
|
@@ -140,8 +247,9 @@ function Pill(props: { stats: SessionStats; open: boolean; onToggle(): void }):
|
|
|
140
247
|
}
|
|
141
248
|
|
|
142
249
|
/** 官方 UsagePill:总量 · 缓存命中(缓存命中用官方百分比精度规则)。 */
|
|
143
|
-
function TokenPill(props: { stats: SessionStats; open: boolean; onToggle(): void }): JSX.Element | null {
|
|
144
|
-
const { stats, open, onToggle } = props;
|
|
250
|
+
function TokenPill(props: { stats: SessionStats; open: boolean; onToggle(): void; onClose(): void }): JSX.Element | null {
|
|
251
|
+
const { stats, open, onToggle, onClose } = props;
|
|
252
|
+
const { rootRef, panelRef, position } = useStatDialogSeat(open, onClose);
|
|
145
253
|
if (stats.totalTokens === undefined) return null;
|
|
146
254
|
const total = stats.totalTokens;
|
|
147
255
|
const cacheHitText =
|
|
@@ -151,7 +259,7 @@ function TokenPill(props: { stats: SessionStats; open: boolean; onToggle(): void
|
|
|
151
259
|
(stats.cacheReadTokens ?? 0) + (stats.cacheWriteTokens ?? 0) + (stats.uncachedInputTokens ?? 0),
|
|
152
260
|
);
|
|
153
261
|
return (
|
|
154
|
-
<span className="dci-stat-anchor">
|
|
262
|
+
<span className="dci-stat-anchor" ref={rootRef}>
|
|
155
263
|
<button
|
|
156
264
|
type="button"
|
|
157
265
|
className="dci-stat-pill"
|
|
@@ -180,6 +288,8 @@ function TokenPill(props: { stats: SessionStats; open: boolean; onToggle(): void
|
|
|
180
288
|
label={t("stats.usage")}
|
|
181
289
|
icon={<DatabaseIcon />}
|
|
182
290
|
titleValue={exactCount(total)}
|
|
291
|
+
panelRef={panelRef}
|
|
292
|
+
position={position}
|
|
183
293
|
rows={[
|
|
184
294
|
[t('stats.cacheHitLabel'), cacheHitText !== null ? `${cacheHitText}%` : '—'],
|
|
185
295
|
[t('stats.uncached'), exactCount(stats.uncachedInputTokens)],
|
|
@@ -198,40 +308,40 @@ function TokenPill(props: { stats: SessionStats; open: boolean; onToggle(): void
|
|
|
198
308
|
export function StatsRow(props: StatsRowProps): JSX.Element | null {
|
|
199
309
|
const { useProjection, sessionId, chatNodes } = props;
|
|
200
310
|
// 唯一展开:三枚(轮步/用量/上下文小圈)共用一个 open 态——任意一枚展开
|
|
201
|
-
//
|
|
311
|
+
// 即收起其余(官方 chat StatsPills 的 openPill 同语义:详情面板同时只存在一个)。
|
|
202
312
|
const [open, setOpen] = useState<'rounds' | 'tokens' | 'gauge' | null>(null);
|
|
203
|
-
const
|
|
313
|
+
const close = useCallback(() => setOpen(null), []);
|
|
314
|
+
const toggle = useCallback((key: 'rounds' | 'tokens' | 'gauge') => {
|
|
315
|
+
setOpen((current) => (current === key ? null : key));
|
|
316
|
+
}, []);
|
|
204
317
|
const stats = useSessionStats(useProjection, sessionId, chatNodes);
|
|
205
318
|
|
|
206
|
-
// 点外面关闭(面板内/胶囊上不关)。
|
|
207
|
-
useEffect(() => {
|
|
208
|
-
if (open === null) return;
|
|
209
|
-
const onDown = (event: PointerEvent): void => {
|
|
210
|
-
if (rootRef.current && event.target instanceof Node && rootRef.current.contains(event.target)) return;
|
|
211
|
-
setOpen(null);
|
|
212
|
-
};
|
|
213
|
-
window.addEventListener('pointerdown', onDown, true);
|
|
214
|
-
return () => window.removeEventListener('pointerdown', onDown, true);
|
|
215
|
-
}, [open]);
|
|
216
|
-
|
|
217
319
|
// 官方 rc.2:上下文小圈独立于 stats 胶囊渲染(无 stats 也要出圈)。
|
|
218
320
|
const pressure = useProjection
|
|
219
321
|
? (useProjection('contextPressure') as PressureSnapshot | undefined)
|
|
220
322
|
: undefined;
|
|
221
323
|
const hasGauge = pressure !== undefined && !!pressure.contextWindow;
|
|
324
|
+
// 面板打开期间其数据源消失(切换会话 / 投影短暂缺席)时自动收起,避免恢复后「自己重开」。
|
|
325
|
+
useEffect(() => {
|
|
326
|
+
if (open === null) return;
|
|
327
|
+
const valid = open === 'gauge' ? hasGauge : stats !== null && stats !== undefined;
|
|
328
|
+
if (!valid) setOpen(null);
|
|
329
|
+
}, [open, hasGauge, stats]);
|
|
330
|
+
|
|
222
331
|
if (!stats && !hasGauge) return null;
|
|
223
332
|
return (
|
|
224
|
-
<div className="dci-stat-root" data-composer-stats="codinput"
|
|
333
|
+
<div className="dci-stat-root" data-composer-stats="codinput">
|
|
225
334
|
{stats ? (
|
|
226
335
|
<>
|
|
227
|
-
<Pill stats={stats} open={open === 'rounds'} onToggle={() =>
|
|
228
|
-
<TokenPill stats={stats} open={open === 'tokens'} onToggle={() =>
|
|
336
|
+
<Pill stats={stats} open={open === 'rounds'} onToggle={() => toggle('rounds')} onClose={close} />
|
|
337
|
+
<TokenPill stats={stats} open={open === 'tokens'} onToggle={() => toggle('tokens')} onClose={close} />
|
|
229
338
|
</>
|
|
230
339
|
) : null}
|
|
231
340
|
<ContextGauge
|
|
232
341
|
useProjection={useProjection}
|
|
233
342
|
open={open === 'gauge'}
|
|
234
|
-
onToggle={() =>
|
|
343
|
+
onToggle={() => toggle('gauge')}
|
|
344
|
+
onClose={close}
|
|
235
345
|
/>
|
|
236
346
|
</div>
|
|
237
347
|
);
|
|
@@ -259,14 +369,16 @@ function shortTokens(n: number): string {
|
|
|
259
369
|
|
|
260
370
|
/** 上下文占用小圈(官方 JObwrW 复刻):双环仪表 + 百分比,点击展开分段面板。
|
|
261
371
|
* 0.1.7-rc.2 起官方渲染在 dock 行 stats 胶囊之后(原先在工具行 trailing)。
|
|
262
|
-
* 展开态由 StatsRow
|
|
372
|
+
* 展开态由 StatsRow 持有(三枚详情面板唯一展开);面板 portal 到 body +
|
|
373
|
+
* useAnchoredPanel 定位(官方 ContextMeter:side top / align start / gap 8 / margin 12)。 */
|
|
263
374
|
export function ContextGauge(props: {
|
|
264
375
|
useProjection: UseProjection | undefined;
|
|
265
376
|
open: boolean;
|
|
266
377
|
onToggle(): void;
|
|
378
|
+
onClose(): void;
|
|
267
379
|
}): JSX.Element | null {
|
|
268
|
-
const { useProjection, open, onToggle } = props;
|
|
269
|
-
const rootRef
|
|
380
|
+
const { useProjection, open, onToggle, onClose } = props;
|
|
381
|
+
const { rootRef, panelRef, position } = useStatDialogSeat(open, onClose);
|
|
270
382
|
const pressure = useProjection
|
|
271
383
|
? (useProjection('contextPressure') as PressureSnapshot | undefined)
|
|
272
384
|
: undefined;
|
|
@@ -307,49 +419,58 @@ export function ContextGauge(props: {
|
|
|
307
419
|
</svg>
|
|
308
420
|
<span>{Math.round(pct)}%</span>
|
|
309
421
|
</button>
|
|
310
|
-
{open
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
422
|
+
{open
|
|
423
|
+
? createPortal(
|
|
424
|
+
<div
|
|
425
|
+
className="dci-gauge-panel"
|
|
426
|
+
role="dialog"
|
|
427
|
+
aria-label={t("tool.contextTitle")}
|
|
428
|
+
ref={panelRef}
|
|
429
|
+
style={panelStyle(position)}
|
|
430
|
+
>
|
|
431
|
+
<div className="dci-gauge-header">
|
|
432
|
+
<span className="dci-gauge-headline">{t("tool.contextTitle")}</span>
|
|
433
|
+
<span className="dci-gauge-percent">{Math.round(pct)}%</span>
|
|
434
|
+
<span className="dci-gauge-figures">
|
|
435
|
+
~{shortTokens(usedTokens)} / {shortTokens(windowTokens)}
|
|
436
|
+
</span>
|
|
437
|
+
</div>
|
|
438
|
+
<div className="dci-gauge-bar">
|
|
439
|
+
{segments.map(([cls, tokens]) => (
|
|
440
|
+
<div
|
|
441
|
+
key={cls}
|
|
442
|
+
className={`dci-gauge-segment ${cls}`}
|
|
443
|
+
style={{ width: `${(tokens / windowTokens) * 100}%` }}
|
|
444
|
+
/>
|
|
445
|
+
))}
|
|
446
|
+
</div>
|
|
447
|
+
<dl className="dci-gauge-rows">
|
|
448
|
+
<div className="dci-gauge-row">
|
|
449
|
+
<dt>
|
|
450
|
+
<span className={`dci-gauge-swatch dci-gauge-colorSystem`} aria-hidden="true" />
|
|
451
|
+
{t("stats.prompt")}
|
|
452
|
+
</dt>
|
|
453
|
+
<dd>~{shortTokens(breakdown?.systemTokens ?? 0)}</dd>
|
|
454
|
+
</div>
|
|
455
|
+
<div className="dci-gauge-row">
|
|
456
|
+
<dt>
|
|
457
|
+
<span className={`dci-gauge-swatch dci-gauge-colorTools`} aria-hidden="true" />
|
|
458
|
+
{t("stats.tools")}
|
|
459
|
+
</dt>
|
|
460
|
+
<dd>~{shortTokens(breakdown?.toolsTokens ?? 0)}</dd>
|
|
461
|
+
</div>
|
|
462
|
+
<div className="dci-gauge-row">
|
|
463
|
+
<dt>
|
|
464
|
+
<span className={`dci-gauge-swatch dci-gauge-colorMessages`} aria-hidden="true" />
|
|
465
|
+
{t("stats.messages")}
|
|
466
|
+
</dt>
|
|
467
|
+
<dd>~{shortTokens(breakdown?.messageTokens ?? 0)}</dd>
|
|
468
|
+
</div>
|
|
469
|
+
</dl>
|
|
470
|
+
</div>,
|
|
471
|
+
document.body,
|
|
472
|
+
)
|
|
473
|
+
: null}
|
|
353
474
|
</span>
|
|
354
475
|
);
|
|
355
476
|
}
|