@dungsil/dsh-plugin-ui-usage 0.1.0 → 0.2.0
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/README.md +20 -0
- package/assets/light-dark-preview.webp +0 -0
- package/dist/client.js +61 -17
- package/package.json +4 -1
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Usage Dashboard · 사용량 대시보드
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
DSH Web 사이드바에서 세션에 기록된 토큰 사용량, 일별 추이와 제공자·모델별 사용량을 확인합니다. 청구서나 제공자 계정의 잔액 조회 화면은 아닙니다.
|
|
6
|
+
|
|
7
|
+
위 이미지는 최신 빌드의 실제 플러그인 컴포넌트에 예시 사용량 응답과 공식 DSH 테마·UI primitives를 제공한 독립 브라우저 화면을 각각 캡처해 합성했습니다. 실제 DSH 세션 화면은 아니며, 모델·날짜·수치는 모두 예시입니다.
|
|
8
|
+
|
|
9
|
+
## 사용 방법
|
|
10
|
+
|
|
11
|
+
1. DSH 플러그인 설치 화면에서 `@dungsil/dsh-plugin-ui-usage`를 설치하고 활성화합니다.
|
|
12
|
+
2. 사이드바의 **사용량**을 엽니다. **최근 7일**, **최근 30일**, **전체 기간** 중 조회 범위를 선택합니다.
|
|
13
|
+
3. 일별 막대를 눌러 해당 날짜의 토큰 수를 확인합니다. 최신 기록을 다시 읽으려면 **새로고침**을 누릅니다.
|
|
14
|
+
|
|
15
|
+
## 집계 범위와 호환성
|
|
16
|
+
|
|
17
|
+
- DSH `0.2.1-rc.5` 이상 `0.3.0` 미만의 Web UI를 지원합니다. 한국어·영어·중국어(간체)와 DSH 테마를 따릅니다.
|
|
18
|
+
- 세션에 기록된 호출만 집계하며 하위 에이전트와 재시도를 포함합니다. 분기에서 상속된 기록은 한 번만 셉니다. 삭제한 세션과 제목 생성 등 세션 밖 호출은 제외합니다.
|
|
19
|
+
- 입력은 캐시 읽기·쓰기를 제외하고, 추론 토큰은 출력에 포함합니다. 제공자가 전체 토큰 수를 보고하면 해당 값을 사용합니다. 토큰 수 미보고 호출이나 읽지 못한 세션은 별도로 알립니다.
|
|
20
|
+
- 날짜는 브라우저 시간대를 사용합니다. 전체 기간을 선택해도 일별 차트는 최근 30일을 표시합니다. 자동 실시간 갱신 대신 새로고침으로 기록을 다시 조회합니다.
|
|
Binary file
|
package/dist/client.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
window.__ModuleLoader__.load({ id: "@dungsil/dsh-plugin-ui-usage", factory(require) { const module = { exports: {} }; const exports = module.exports;
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
let react = require("react");
|
|
4
|
-
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
5
4
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
5
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
6
6
|
// ../../tools/ui/src/client.ts
|
|
7
7
|
function useLocaleSnapshot(locale) {
|
|
8
8
|
const subscribe = (0, react.useCallback)((listener) => locale.subscribe(listener), [locale]);
|
|
@@ -25,6 +25,7 @@ const copy = {
|
|
|
25
25
|
month: "Last 30 days",
|
|
26
26
|
all: "All time",
|
|
27
27
|
refresh: "Refresh",
|
|
28
|
+
refreshing: "Refreshing…",
|
|
28
29
|
loading: "Loading usage…",
|
|
29
30
|
error: "Could not load usage. Try refreshing.",
|
|
30
31
|
empty: "No recorded model calls in this period.",
|
|
@@ -52,6 +53,7 @@ const copy = {
|
|
|
52
53
|
month: "최근 30일",
|
|
53
54
|
all: "전체 기간",
|
|
54
55
|
refresh: "새로고침",
|
|
56
|
+
refreshing: "새로고침 중…",
|
|
55
57
|
loading: "사용량을 불러오는 중입니다…",
|
|
56
58
|
error: "사용량을 불러오지 못했습니다. 새로고침해 주세요.",
|
|
57
59
|
empty: "이 기간에 기록된 모델 호출이 없습니다.",
|
|
@@ -79,6 +81,7 @@ const copy = {
|
|
|
79
81
|
month: "最近 30 天",
|
|
80
82
|
all: "全部时间",
|
|
81
83
|
refresh: "刷新",
|
|
84
|
+
refreshing: "正在刷新…",
|
|
82
85
|
loading: "正在加载用量…",
|
|
83
86
|
error: "无法加载用量,请刷新重试。",
|
|
84
87
|
empty: "此时间范围内没有记录的模型调用。",
|
|
@@ -111,6 +114,7 @@ const inject = [
|
|
|
111
114
|
"locale",
|
|
112
115
|
"slots"
|
|
113
116
|
];
|
|
117
|
+
const cachedViews = /* @__PURE__ */ new WeakMap();
|
|
114
118
|
const styles = `
|
|
115
119
|
.dsh-usage-page { box-sizing: border-box; height: 100%; overflow: auto; padding: 24px; padding-inline-start: max(24px, var(--dsh-frame-leading-clearance, 0px)); }
|
|
116
120
|
.dsh-usage-page > .dsh-usage { max-width: 960px; margin: 0 auto; }
|
|
@@ -118,6 +122,10 @@ const styles = `
|
|
|
118
122
|
.dsh-usage { container-type: inline-size; overflow-wrap: anywhere; color: var(--dsw-alias-label-primary); font: 14px/22px var(--dsw-font-family, system-ui); }
|
|
119
123
|
.dsh-usage-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
120
124
|
.dsh-usage-status { display: flex; align-items: center; gap: 6px; }
|
|
125
|
+
@keyframes dsh-usage-spin { to { transform: rotate(360deg); } }
|
|
126
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
127
|
+
.dsh-usage-refresh[aria-busy=true] svg { animation: dsh-usage-spin .8s linear infinite; }
|
|
128
|
+
}
|
|
121
129
|
.dsh-usage-muted, .dsh-usage dt { color: var(--dsw-alias-label-secondary); font-size: 12px; }
|
|
122
130
|
.dsh-usage h3 { margin: 24px 0 12px; font-size: 14px; font-weight: 500; }
|
|
123
131
|
.dsh-usage dl { margin: 0; }
|
|
@@ -152,30 +160,47 @@ function Usage({ ctx }) {
|
|
|
152
160
|
const locale = useLocaleSnapshot(ctx.locale);
|
|
153
161
|
const text = localizedCopy(locale.active);
|
|
154
162
|
const number = new Intl.NumberFormat(locale.active);
|
|
163
|
+
const tokens = new Intl.NumberFormat(locale.active, {
|
|
164
|
+
notation: "compact",
|
|
165
|
+
maximumFractionDigits: 1
|
|
166
|
+
});
|
|
155
167
|
const [period, setPeriod] = (0, react.useState)(7);
|
|
156
168
|
const [periodOpen, setPeriodOpen] = (0, react.useState)(false);
|
|
157
169
|
const [refresh, setRefresh] = (0, react.useState)(0);
|
|
158
|
-
const
|
|
159
|
-
const [busy, setBusy] = (0, react.useState)(true);
|
|
160
|
-
const [error, setError] = (0, react.useState)(false);
|
|
161
|
-
const [selectedDay, setSelectedDay] = (0, react.useState)("");
|
|
170
|
+
const refreshRequested = (0, react.useRef)(false);
|
|
162
171
|
const connection = ctx.get("connection");
|
|
172
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
173
|
+
const cacheKey = JSON.stringify([period, timeZone]);
|
|
174
|
+
let cache = cachedViews.get(connection);
|
|
175
|
+
if (!cache) cachedViews.set(connection, cache = /* @__PURE__ */ new Map());
|
|
176
|
+
const [view, setView] = (0, react.useState)(() => cache.get(cacheKey));
|
|
177
|
+
const [busy, setBusy] = (0, react.useState)(() => !cache.has(cacheKey));
|
|
178
|
+
const [error, setError] = (0, react.useState)(false);
|
|
179
|
+
const [selectedDay, setSelectedDay] = (0, react.useState)(() => view?.days.at(-1)?.day ?? "");
|
|
163
180
|
(0, react.useEffect)(() => {
|
|
181
|
+
const cached = cache.get(cacheKey);
|
|
182
|
+
setError(false);
|
|
183
|
+
setView(cached);
|
|
184
|
+
if (cached && !refreshRequested.current) {
|
|
185
|
+
setBusy(false);
|
|
186
|
+
setSelectedDay((day) => cached.days.some((entry) => entry.day === day) ? day : cached.days.at(-1)?.day ?? "");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
refreshRequested.current = false;
|
|
164
190
|
const controller = new AbortController();
|
|
165
191
|
setBusy(true);
|
|
166
|
-
setError(false);
|
|
167
|
-
setView(void 0);
|
|
168
192
|
(async () => {
|
|
169
193
|
try {
|
|
170
194
|
const result = await connection.rpc.call(channel, method, {
|
|
171
195
|
days: period,
|
|
172
|
-
timeZone
|
|
196
|
+
timeZone
|
|
173
197
|
}, controller.signal);
|
|
174
198
|
if (controller.signal.aborted) return;
|
|
175
199
|
if (!result.ok) throw new Error("Usage unavailable");
|
|
176
200
|
const next = result.value;
|
|
201
|
+
cache.set(cacheKey, next);
|
|
177
202
|
setView(next);
|
|
178
|
-
setSelectedDay(next.days.at(-1)?.day ?? "");
|
|
203
|
+
setSelectedDay((day) => next.days.some((entry) => entry.day === day) ? day : next.days.at(-1)?.day ?? "");
|
|
179
204
|
} catch {
|
|
180
205
|
if (!controller.signal.aborted) setError(true);
|
|
181
206
|
} finally {
|
|
@@ -186,6 +211,9 @@ function Usage({ ctx }) {
|
|
|
186
211
|
}, [
|
|
187
212
|
connection,
|
|
188
213
|
period,
|
|
214
|
+
timeZone,
|
|
215
|
+
cache,
|
|
216
|
+
cacheKey,
|
|
189
217
|
refresh
|
|
190
218
|
]);
|
|
191
219
|
const metrics = [
|
|
@@ -243,13 +271,18 @@ function Usage({ ctx }) {
|
|
|
243
271
|
})
|
|
244
272
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
245
273
|
variant: "outline",
|
|
274
|
+
className: "dsh-usage-refresh",
|
|
275
|
+
"aria-busy": busy,
|
|
246
276
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, {}),
|
|
247
277
|
disabled: busy,
|
|
248
|
-
onClick: () =>
|
|
249
|
-
|
|
278
|
+
onClick: () => {
|
|
279
|
+
refreshRequested.current = true;
|
|
280
|
+
setRefresh((value) => value + 1);
|
|
281
|
+
},
|
|
282
|
+
children: busy ? text.refreshing : text.refresh
|
|
250
283
|
})]
|
|
251
284
|
}),
|
|
252
|
-
busy && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
285
|
+
busy && !view && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
253
286
|
role: "status",
|
|
254
287
|
className: "dsh-usage-status",
|
|
255
288
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "ongoing" }), text.loading]
|
|
@@ -263,8 +296,12 @@ function Usage({ ctx }) {
|
|
|
263
296
|
className: "dsh-usage-summary",
|
|
264
297
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: text.total }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", {
|
|
265
298
|
className: "dsh-usage-total",
|
|
266
|
-
|
|
267
|
-
|
|
299
|
+
title: number.format(view.totals.total),
|
|
300
|
+
children: tokens.format(view.totals.total)
|
|
301
|
+
})] }), metrics.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: text[key] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", {
|
|
302
|
+
title: number.format(view.totals[key]),
|
|
303
|
+
children: tokens.format(view.totals[key])
|
|
304
|
+
})] }, key))]
|
|
268
305
|
}),
|
|
269
306
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
270
307
|
className: "dsh-usage-meta dsh-usage-muted",
|
|
@@ -319,7 +356,8 @@ function Usage({ ctx }) {
|
|
|
319
356
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
320
357
|
className: "dsh-usage-detail",
|
|
321
358
|
"aria-live": "polite",
|
|
322
|
-
|
|
359
|
+
title: selected && number.format(selected.total),
|
|
360
|
+
children: selected && `${date(selected.day)} · ${text.total}: ${tokens.format(selected.total)}`
|
|
323
361
|
}),
|
|
324
362
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: text.models }),
|
|
325
363
|
view.models.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -330,8 +368,14 @@ function Usage({ ctx }) {
|
|
|
330
368
|
row.provider || text.unknown,
|
|
331
369
|
" / ",
|
|
332
370
|
row.model || text.unknown
|
|
333
|
-
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
334
|
-
|
|
371
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
372
|
+
title: number.format(row.total),
|
|
373
|
+
children: tokens.format(row.total)
|
|
374
|
+
})]
|
|
375
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", { children: ["calls", ...metrics].map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: text[key] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", {
|
|
376
|
+
title: number.format(row[key]),
|
|
377
|
+
children: key === "calls" ? number.format(row[key]) : tokens.format(row[key])
|
|
378
|
+
})] }, key)) })]
|
|
335
379
|
}, JSON.stringify([row.provider, row.model])))
|
|
336
380
|
] }),
|
|
337
381
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dungsil/dsh-plugin-ui-usage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Recorded session token usage dashboard for DeepSeek Harness.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"dist",
|
|
13
13
|
"locale",
|
|
14
|
+
"assets",
|
|
14
15
|
"cordis.patch.yml"
|
|
15
16
|
],
|
|
16
17
|
"type": "module",
|
|
@@ -37,7 +38,9 @@
|
|
|
37
38
|
"@dungsil/dsh-tsdown": "0.0.0",
|
|
38
39
|
"@dungsil/dsh-ui": "0.0.0",
|
|
39
40
|
"@types/react": "18.3.31",
|
|
41
|
+
"happy-dom": "21.0.4",
|
|
40
42
|
"react": "18.3.1",
|
|
43
|
+
"react-dom": "18.3.1",
|
|
41
44
|
"tsdown": "0.23.4"
|
|
42
45
|
},
|
|
43
46
|
"peerDependencies": {
|