aicodeswitch 1.4.1 → 1.5.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.
@@ -0,0 +1,620 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { api } from '../api/client';
3
+ import type { Statistics } from '../../types';
4
+ import {
5
+ AreaChart,
6
+ Area,
7
+ BarChart,
8
+ Bar,
9
+ PieChart,
10
+ Pie,
11
+ Cell,
12
+ LineChart,
13
+ Line,
14
+ XAxis,
15
+ YAxis,
16
+ CartesianGrid,
17
+ Tooltip,
18
+ Legend,
19
+ ResponsiveContainer,
20
+ } from 'recharts';
21
+ import dayjs from 'dayjs';
22
+
23
+ const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#f97316'];
24
+
25
+ function StatisticsPage() {
26
+ const [statistics, setStatistics] = useState<Statistics | null>(null);
27
+ const [loading, setLoading] = useState(true);
28
+ const [days, setDays] = useState(30);
29
+
30
+ useEffect(() => {
31
+ loadStatistics();
32
+ }, [days]);
33
+
34
+ const loadStatistics = async () => {
35
+ try {
36
+ setLoading(true);
37
+ const data = await api.getStatistics(days);
38
+ setStatistics(data);
39
+ } catch (error) {
40
+ console.error('Failed to load statistics:', error);
41
+ } finally {
42
+ setLoading(false);
43
+ }
44
+ };
45
+
46
+ const formatNumber = (num: number): string => {
47
+ if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
48
+ if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
49
+ return num.toString();
50
+ };
51
+
52
+ const formatTime = (minutes: number): string => {
53
+ if (minutes >= 60) {
54
+ const hours = Math.floor(minutes / 60);
55
+ const mins = minutes % 60;
56
+ return `${hours}h ${mins}m`;
57
+ }
58
+ return `${minutes}m`;
59
+ };
60
+
61
+ if (loading) {
62
+ return (
63
+ <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '60vh' }}>
64
+ <div style={{ fontSize: '18px', color: 'var(--text-muted)' }}>加载统计数据中...</div>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ if (!statistics) {
70
+ return (
71
+ <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '60vh' }}>
72
+ <div style={{ fontSize: '18px', color: 'var(--text-muted)' }}>暂无统计数据</div>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ const contentTypeLabels: Record<string, string> = {
78
+ 'default': '默认请求',
79
+ 'image-understanding': '图像理解',
80
+ 'thinking': '思考模式',
81
+ 'long-context': '长上下文',
82
+ 'background': '后台任务',
83
+ 'model-mapping': '模型映射',
84
+ };
85
+
86
+ // 计算每日编程时长(基于 tokens 估算)
87
+ const codingTimeTimeline = statistics.timeline.map(day => ({
88
+ date: day.date,
89
+ codingTime: Math.round(day.totalInputTokens / 250 + day.totalOutputTokens / 100),
90
+ }));
91
+
92
+ return (
93
+ <div>
94
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
95
+ <div>
96
+ <h1 style={{ margin: 0, fontSize: '28px', fontWeight: 'bold', color: 'var(--text-primary)' }}>
97
+ 📊 数据统计
98
+ </h1>
99
+ <p style={{ margin: '4px 0 0 0', color: 'var(--text-muted)' }}>
100
+ 查看您的 AI 编程助手使用情况分析
101
+ </p>
102
+ </div>
103
+ <div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
104
+ <label style={{ color: 'var(--text-primary)', fontWeight: '500' }}>统计周期:</label>
105
+ <select
106
+ value={days}
107
+ onChange={(e) => setDays(Number(e.target.value))}
108
+ style={{
109
+ padding: '8px 16px',
110
+ borderRadius: '8px',
111
+ border: '1px solid var(--border-primary)',
112
+ background: 'var(--bg-card)',
113
+ color: 'var(--text-primary)',
114
+ fontSize: '14px',
115
+ cursor: 'pointer',
116
+ }}
117
+ >
118
+ <option value={7}>最近 7 天</option>
119
+ <option value={30}>最近 30 天</option>
120
+ <option value={90}>最近 90 天</option>
121
+ </select>
122
+ <button
123
+ onClick={loadStatistics}
124
+ style={{
125
+ padding: '8px 16px',
126
+ borderRadius: '8px',
127
+ border: '1px solid var(--border-primary)',
128
+ background: 'var(--bg-card)',
129
+ color: 'var(--text-primary)',
130
+ fontSize: '14px',
131
+ cursor: 'pointer',
132
+ }}
133
+ >
134
+ 🔄 刷新
135
+ </button>
136
+ </div>
137
+ </div>
138
+
139
+ {/* 概览卡片 */}
140
+ <div style={{
141
+ display: 'grid',
142
+ gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))',
143
+ gap: '16px',
144
+ marginBottom: '24px',
145
+ }}>
146
+ <div style={{
147
+ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
148
+ padding: '20px',
149
+ borderRadius: '16px',
150
+ color: 'white',
151
+ boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
152
+ }}>
153
+ <div style={{ fontSize: '14px', opacity: 0.9, marginBottom: '8px' }}>总请求数</div>
154
+ <div style={{ fontSize: '32px', fontWeight: 'bold' }}>{formatNumber(statistics.overview.totalRequests)}</div>
155
+ </div>
156
+
157
+ <div style={{
158
+ background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
159
+ padding: '20px',
160
+ borderRadius: '16px',
161
+ color: 'white',
162
+ boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
163
+ }}>
164
+ <div style={{ fontSize: '14px', opacity: 0.9, marginBottom: '8px' }}>总 Tokens</div>
165
+ <div style={{ fontSize: '32px', fontWeight: 'bold' }}>{formatNumber(statistics.overview.totalTokens)}</div>
166
+ </div>
167
+
168
+ <div style={{
169
+ background: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
170
+ padding: '20px',
171
+ borderRadius: '16px',
172
+ color: 'white',
173
+ boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
174
+ }}>
175
+ <div style={{ fontSize: '14px', opacity: 0.9, marginBottom: '8px' }}>编程时长</div>
176
+ <div style={{ fontSize: '32px', fontWeight: 'bold' }}>{formatTime(statistics.overview.totalCodingTime)}</div>
177
+ </div>
178
+
179
+ <div style={{
180
+ background: 'linear-gradient(135deg, #14a042 0%, #38f9d7 100%)',
181
+ padding: '20px',
182
+ borderRadius: '16px',
183
+ color: 'white',
184
+ boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
185
+ }}>
186
+ <div style={{ fontSize: '14px', opacity: 0.9, marginBottom: '8px' }}>成功率</div>
187
+ <div style={{ fontSize: '32px', fontWeight: 'bold' }}>{statistics.overview.successRate.toFixed(1)}%</div>
188
+ </div>
189
+
190
+ <div style={{
191
+ background: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
192
+ padding: '20px',
193
+ borderRadius: '16px',
194
+ color: 'white',
195
+ boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
196
+ }}>
197
+ <div style={{ fontSize: '14px', opacity: 0.9, marginBottom: '8px' }}>平均响应时间</div>
198
+ <div style={{ fontSize: '32px', fontWeight: 'bold' }}>{statistics.overview.avgResponseTime}ms</div>
199
+ </div>
200
+
201
+ <div style={{
202
+ background: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)',
203
+ padding: '20px',
204
+ borderRadius: '16px',
205
+ color: 'white',
206
+ boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
207
+ }}>
208
+ <div style={{ fontSize: '14px', opacity: 0.9, marginBottom: '8px' }}>配置统计</div>
209
+ <div style={{ fontSize: '14px', marginTop: '8px' }}>
210
+ {statistics.overview.totalVendors} 供应商 · {statistics.overview.totalServices} 服务 · {statistics.overview.totalRoutes} 路由
211
+ </div>
212
+ </div>
213
+ </div>
214
+
215
+ {/* 图表区域 */}
216
+ <div style={{
217
+ display: 'grid',
218
+ gridTemplateColumns: 'repeat(auto-fit, minmax(500px, 1fr))',
219
+ gap: '20px',
220
+ marginBottom: '24px',
221
+ }}>
222
+ {/* 时间趋势图 */}
223
+ <div style={{
224
+ background: 'var(--bg-card)',
225
+ padding: '20px',
226
+ borderRadius: '16px',
227
+ border: '1px solid var(--border-primary)',
228
+ }}>
229
+ <h3 style={{ margin: '0 0 16px 0', fontSize: '18px', color: 'var(--text-primary)' }}>
230
+ 📈 请求趋势
231
+ </h3>
232
+ <ResponsiveContainer width="100%" height={300}>
233
+ <AreaChart data={statistics.timeline}>
234
+ <defs>
235
+ <linearGradient id="colorRequests" x1="0" y1="0" x2="0" y2="1">
236
+ <stop offset="5%" stopColor="#8884d8" stopOpacity={0.8}/>
237
+ <stop offset="95%" stopColor="#8884d8" stopOpacity={0}/>
238
+ </linearGradient>
239
+ </defs>
240
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border-primary)" />
241
+ <XAxis
242
+ dataKey="date"
243
+ stroke="var(--text-muted)"
244
+ fontSize={12}
245
+ tickFormatter={(date) => dayjs(date).format('MM/DD')}
246
+ />
247
+ <YAxis stroke="var(--text-muted)" fontSize={12} />
248
+ <Tooltip
249
+ contentStyle={{
250
+ background: 'var(--bg-card)',
251
+ border: '1px solid var(--border-primary)',
252
+ borderRadius: '8px',
253
+ color: 'var(--text-primary)',
254
+ }}
255
+ labelFormatter={(date) => dayjs(date).format('YYYY-MM-DD')}
256
+ />
257
+ <Legend />
258
+ <Area
259
+ type="monotone"
260
+ dataKey="totalRequests"
261
+ name="请求数"
262
+ stroke="#8884d8"
263
+ fillOpacity={1}
264
+ fill="url(#colorRequests)"
265
+ />
266
+ </AreaChart>
267
+ </ResponsiveContainer>
268
+ </div>
269
+
270
+ {/* Token 趋势图 */}
271
+ <div style={{
272
+ background: 'var(--bg-card)',
273
+ padding: '20px',
274
+ borderRadius: '16px',
275
+ border: '1px solid var(--border-primary)',
276
+ }}>
277
+ <h3 style={{ margin: '0 0 16px 0', fontSize: '18px', color: 'var(--text-primary)' }}>
278
+ 🔢 Token 使用趋势
279
+ </h3>
280
+ <ResponsiveContainer width="100%" height={300}>
281
+ <LineChart data={statistics.timeline}>
282
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border-primary)" />
283
+ <XAxis
284
+ dataKey="date"
285
+ stroke="var(--text-muted)"
286
+ fontSize={12}
287
+ tickFormatter={(date) => dayjs(date).format('MM/DD')}
288
+ />
289
+ <YAxis stroke="var(--text-muted)" fontSize={12} tickFormatter={(value) => value ? formatNumber(Number(value)) : ''} />
290
+ <Tooltip
291
+ contentStyle={{
292
+ background: 'var(--bg-card)',
293
+ border: '1px solid var(--border-primary)',
294
+ borderRadius: '8px',
295
+ color: 'var(--text-primary)',
296
+ }}
297
+ labelFormatter={(date) => dayjs(date).format('YYYY-MM-DD')}
298
+ formatter={(value) => value ? formatNumber(Number(value)) : ''}
299
+ />
300
+ <Legend />
301
+ <Line type="monotone" dataKey="totalInputTokens" name="输入 Tokens" stroke="#10b981" strokeWidth={2} dot={false} />
302
+ <Line type="monotone" dataKey="totalOutputTokens" name="输出 Tokens" stroke="#f59e0b" strokeWidth={2} dot={false} />
303
+ </LineChart>
304
+ </ResponsiveContainer>
305
+ </div>
306
+
307
+ {/* 每日编程时长趋势 */}
308
+ <div style={{
309
+ background: 'var(--bg-card)',
310
+ padding: '20px',
311
+ borderRadius: '16px',
312
+ border: '1px solid var(--border-primary)',
313
+ }}>
314
+ <h3 style={{ margin: '0 0 16px 0', fontSize: '18px', color: 'var(--text-primary)' }}>
315
+ ⏱️ 每日编程时长趋势
316
+ </h3>
317
+ <ResponsiveContainer width="100%" height={300}>
318
+ <LineChart data={codingTimeTimeline}>
319
+ <defs>
320
+ <linearGradient id="colorCodingTime" x1="0" y1="0" x2="0" y2="1">
321
+ <stop offset="5%" stopColor="#8b5cf6" stopOpacity={0.8}/>
322
+ <stop offset="95%" stopColor="#8b5cf6" stopOpacity={0}/>
323
+ </linearGradient>
324
+ </defs>
325
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border-primary)" />
326
+ <XAxis
327
+ dataKey="date"
328
+ stroke="var(--text-muted)"
329
+ fontSize={12}
330
+ tickFormatter={(date) => dayjs(date).format('MM/DD')}
331
+ />
332
+ <YAxis
333
+ stroke="var(--text-muted)"
334
+ fontSize={12}
335
+ tickFormatter={(value) => value !== undefined && value !== null ? (value >= 60 ? `${Math.round(Number(value) / 60)}h` : `${Number(value)}m`) : ''}
336
+ />
337
+ <Tooltip
338
+ contentStyle={{
339
+ background: 'var(--bg-card)',
340
+ border: '1px solid var(--border-primary)',
341
+ borderRadius: '8px',
342
+ color: 'var(--text-primary)',
343
+ }}
344
+ labelFormatter={(date) => dayjs(date).format('YYYY-MM-DD')}
345
+ formatter={(value) => value !== undefined && value !== null ? formatTime(Number(value)) : ''}
346
+ />
347
+ <Legend />
348
+ <Area
349
+ type="monotone"
350
+ dataKey="codingTime"
351
+ name="编程时长"
352
+ stroke="#8b5cf6"
353
+ fillOpacity={1}
354
+ fill="url(#colorCodingTime)"
355
+ />
356
+ <Line
357
+ type="monotone"
358
+ dataKey="codingTime"
359
+ name="编程时长"
360
+ stroke="#8b5cf6"
361
+ strokeWidth={3}
362
+ dot={{ fill: '#8b5cf6', r: 4 }}
363
+ activeDot={{ r: 6 }}
364
+ />
365
+ </LineChart>
366
+ </ResponsiveContainer>
367
+ <div style={{
368
+ marginTop: '12px',
369
+ padding: '12px',
370
+ background: 'var(--bg-secondary)',
371
+ borderRadius: '8px',
372
+ fontSize: '14px',
373
+ color: 'var(--text-muted)',
374
+ textAlign: 'center',
375
+ }}>
376
+ 💡 编程时长基于 tokens 估算(阅读速度 250 tokens/分钟,编码速度 100 tokens/分钟)
377
+ </div>
378
+ </div>
379
+
380
+ {/* 供应商分布 */}
381
+ <div style={{
382
+ background: 'var(--bg-card)',
383
+ padding: '20px',
384
+ borderRadius: '16px',
385
+ border: '1px solid var(--border-primary)',
386
+ }}>
387
+ <h3 style={{ margin: '0 0 16px 0', fontSize: '18px', color: 'var(--text-primary)' }}>
388
+ 🏢 供应商使用分布
389
+ </h3>
390
+ <ResponsiveContainer width="100%" height={300}>
391
+ <BarChart data={statistics.byVendor}>
392
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border-primary)" />
393
+ <XAxis
394
+ dataKey="vendorName"
395
+ stroke="var(--text-muted)"
396
+ fontSize={12}
397
+ angle={-45}
398
+ textAnchor="end"
399
+ height={80}
400
+ />
401
+ <YAxis stroke="var(--text-muted)" fontSize={12} />
402
+ <Tooltip
403
+ contentStyle={{
404
+ background: 'var(--bg-card)',
405
+ border: '1px solid var(--border-primary)',
406
+ borderRadius: '8px',
407
+ color: 'var(--text-primary)',
408
+ }}
409
+ />
410
+ <Legend />
411
+ <Bar dataKey="totalRequests" name="请求数" fill="#3b82f6" />
412
+ </BarChart>
413
+ </ResponsiveContainer>
414
+ </div>
415
+
416
+ {/* 内容类型分布 */}
417
+ <div style={{
418
+ background: 'var(--bg-card)',
419
+ padding: '20px',
420
+ borderRadius: '16px',
421
+ border: '1px solid var(--border-primary)',
422
+ }}>
423
+ <h3 style={{ margin: '0 0 16px 0', fontSize: '18px', color: 'var(--text-primary)' }}>
424
+ 📋 请求类型分布
425
+ </h3>
426
+ <ResponsiveContainer width="100%" height={300}>
427
+ <PieChart>
428
+ <Pie
429
+ data={statistics.contentTypeDistribution}
430
+ cx="50%"
431
+ cy="50%"
432
+ labelLine={true}
433
+ label={(props: any) => `${contentTypeLabels[props.contentType] || props.contentType} (${props.percentage}%)`}
434
+ outerRadius={80}
435
+ fill="#8884d8"
436
+ dataKey="count"
437
+ >
438
+ {statistics.contentTypeDistribution.map((_entry, index) => (
439
+ <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
440
+ ))}
441
+ </Pie>
442
+ <Tooltip
443
+ contentStyle={{
444
+ background: 'var(--bg-card)',
445
+ border: '1px solid var(--border-primary)',
446
+ borderRadius: '8px',
447
+ color: 'var(--text-primary)',
448
+ }}
449
+ formatter={(value: any, _name: any, props: any) => {
450
+ const count = value ?? 0;
451
+ const pct = props.payload?.percentage ?? 0;
452
+ const type = props.payload?.contentType || '';
453
+ const label = contentTypeLabels[type] || type;
454
+ return `${count} (${pct}%) - ${label}`;
455
+ }}
456
+ />
457
+ </PieChart>
458
+ </ResponsiveContainer>
459
+ </div>
460
+ </div>
461
+
462
+ {/* 详细统计表格 */}
463
+ <div style={{
464
+ display: 'grid',
465
+ gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))',
466
+ gap: '20px',
467
+ }}>
468
+ {/* 按目标类型 */}
469
+ <div style={{
470
+ background: 'var(--bg-card)',
471
+ padding: '20px',
472
+ borderRadius: '16px',
473
+ border: '1px solid var(--border-primary)',
474
+ }}>
475
+ <h3 style={{ margin: '0 0 16px 0', fontSize: '18px', color: 'var(--text-primary)' }}>
476
+ 🎯 按目标类型统计
477
+ </h3>
478
+ <table style={{ width: '100%', borderCollapse: 'collapse' }}>
479
+ <thead>
480
+ <tr style={{ borderBottom: '2px solid var(--border-primary)' }}>
481
+ <th style={{ padding: '12px', textAlign: 'left', color: 'var(--text-muted)', fontSize: '14px' }}>类型</th>
482
+ <th style={{ padding: '12px', textAlign: 'right', color: 'var(--text-muted)', fontSize: '14px' }}>请求数</th>
483
+ <th style={{ padding: '12px', textAlign: 'right', color: 'var(--text-muted)', fontSize: '14px' }}>Tokens</th>
484
+ </tr>
485
+ </thead>
486
+ <tbody>
487
+ {statistics.byTargetType.map((item) => (
488
+ <tr key={item.targetType} style={{ borderBottom: '1px solid var(--border-secondary)' }}>
489
+ <td style={{ padding: '12px', color: 'var(--text-primary)' }}>
490
+ {item.targetType === 'claude-code' ? 'Claude Code' : 'Codex'}
491
+ </td>
492
+ <td style={{ padding: '12px', textAlign: 'right', color: 'var(--text-primary)' }}>
493
+ {formatNumber(item.totalRequests)}
494
+ </td>
495
+ <td style={{ padding: '12px', textAlign: 'right', color: 'var(--text-primary)' }}>
496
+ {formatNumber(item.totalTokens)}
497
+ </td>
498
+ </tr>
499
+ ))}
500
+ </tbody>
501
+ </table>
502
+ </div>
503
+
504
+ {/* 按模型统计 */}
505
+ <div style={{
506
+ background: 'var(--bg-card)',
507
+ padding: '20px',
508
+ borderRadius: '16px',
509
+ border: '1px solid var(--border-primary)',
510
+ }}>
511
+ <h3 style={{ margin: '0 0 16px 0', fontSize: '18px', color: 'var(--text-primary)' }}>
512
+ 🤖 按模型统计
513
+ </h3>
514
+ <table style={{ width: '100%', borderCollapse: 'collapse' }}>
515
+ <thead>
516
+ <tr style={{ borderBottom: '2px solid var(--border-primary)' }}>
517
+ <th style={{ padding: '12px', textAlign: 'left', color: 'var(--text-muted)', fontSize: '14px' }}>模型</th>
518
+ <th style={{ padding: '12px', textAlign: 'right', color: 'var(--text-muted)', fontSize: '14px' }}>请求数</th>
519
+ <th style={{ padding: '12px', textAlign: 'right', color: 'var(--text-muted)', fontSize: '14px' }}>Tokens</th>
520
+ </tr>
521
+ </thead>
522
+ <tbody>
523
+ {statistics.byModel.slice(0, 10).map((item) => (
524
+ <tr key={item.modelName} style={{ borderBottom: '1px solid var(--border-secondary)' }}>
525
+ <td style={{ padding: '12px', color: 'var(--text-primary)', maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
526
+ {item.modelName}
527
+ </td>
528
+ <td style={{ padding: '12px', textAlign: 'right', color: 'var(--text-primary)' }}>
529
+ {formatNumber(item.totalRequests)}
530
+ </td>
531
+ <td style={{ padding: '12px', textAlign: 'right', color: 'var(--text-primary)' }}>
532
+ {formatNumber(item.totalTokens)}
533
+ </td>
534
+ </tr>
535
+ ))}
536
+ </tbody>
537
+ </table>
538
+ </div>
539
+
540
+ {/* 按服务统计 */}
541
+ <div style={{
542
+ background: 'var(--bg-card)',
543
+ padding: '20px',
544
+ borderRadius: '16px',
545
+ border: '1px solid var(--border-primary)',
546
+ }}>
547
+ <h3 style={{ margin: '0 0 16px 0', fontSize: '18px', color: 'var(--text-primary)' }}>
548
+ 🔧 按服务统计
549
+ </h3>
550
+ <table style={{ width: '100%', borderCollapse: 'collapse' }}>
551
+ <thead>
552
+ <tr style={{ borderBottom: '2px solid var(--border-primary)' }}>
553
+ <th style={{ padding: '12px', textAlign: 'left', color: 'var(--text-muted)', fontSize: '14px' }}>服务</th>
554
+ <th style={{ padding: '12px', textAlign: 'right', color: 'var(--text-muted)', fontSize: '14px' }}>请求数</th>
555
+ <th style={{ padding: '12px', textAlign: 'right', color: 'var(--text-muted)', fontSize: '14px' }}>Tokens</th>
556
+ </tr>
557
+ </thead>
558
+ <tbody>
559
+ {statistics.byService.slice(0, 10).map((item) => (
560
+ <tr key={item.serviceId} style={{ borderBottom: '1px solid var(--border-secondary)' }}>
561
+ <td style={{ padding: '12px', color: 'var(--text-primary)' }}>
562
+ <div style={{ fontSize: '14px', fontWeight: 'bold' }}>{item.serviceName}</div>
563
+ <div style={{ fontSize: '12px', color: 'var(--text-muted)' }}>{item.vendorName}</div>
564
+ </td>
565
+ <td style={{ padding: '12px', textAlign: 'right', color: 'var(--text-primary)' }}>
566
+ {formatNumber(item.totalRequests)}
567
+ </td>
568
+ <td style={{ padding: '12px', textAlign: 'right', color: 'var(--text-primary)' }}>
569
+ {formatNumber(item.totalTokens)}
570
+ </td>
571
+ </tr>
572
+ ))}
573
+ </tbody>
574
+ </table>
575
+ </div>
576
+
577
+ {/* Token 详细统计 */}
578
+ <div style={{
579
+ background: 'var(--bg-card)',
580
+ padding: '20px',
581
+ borderRadius: '16px',
582
+ border: '1px solid var(--border-primary)',
583
+ }}>
584
+ <h3 style={{ margin: '0 0 16px 0', fontSize: '18px', color: 'var(--text-primary)' }}>
585
+ 💰 Token 详细统计
586
+ </h3>
587
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
588
+ <div style={{ display: 'flex', justifyContent: 'space-between', padding: '12px', background: 'var(--bg-secondary)', borderRadius: '8px' }}>
589
+ <span style={{ color: 'var(--text-muted)' }}>输入 Tokens</span>
590
+ <span style={{ color: 'var(--text-primary)', fontWeight: 'bold' }}>{formatNumber(statistics.overview.totalInputTokens)}</span>
591
+ </div>
592
+ <div style={{ display: 'flex', justifyContent: 'space-between', padding: '12px', background: 'var(--bg-secondary)', borderRadius: '8px' }}>
593
+ <span style={{ color: 'var(--text-muted)' }}>输出 Tokens</span>
594
+ <span style={{ color: 'var(--text-primary)', fontWeight: 'bold' }}>{formatNumber(statistics.overview.totalOutputTokens)}</span>
595
+ </div>
596
+ {statistics.overview.totalCacheReadTokens > 0 && (
597
+ <div style={{ display: 'flex', justifyContent: 'space-between', padding: '12px', background: 'var(--bg-secondary)', borderRadius: '8px' }}>
598
+ <span style={{ color: 'var(--text-muted)' }}>缓存读取 Tokens</span>
599
+ <span style={{ color: 'var(--text-primary)', fontWeight: 'bold' }}>{formatNumber(statistics.overview.totalCacheReadTokens)}</span>
600
+ </div>
601
+ )}
602
+ <div style={{ display: 'flex', justifyContent: 'space-between', padding: '12px', background: 'var(--bg-secondary)', borderRadius: '8px' }}>
603
+ <span style={{ color: 'var(--text-muted)' }}>总计 Tokens</span>
604
+ <span style={{ color: 'var(--text-primary)', fontWeight: 'bold' }}>{formatNumber(statistics.overview.totalTokens)}</span>
605
+ </div>
606
+ <div style={{ marginTop: '8px', padding: '12px', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', borderRadius: '8px', color: 'white' }}>
607
+ <div style={{ fontSize: '12px', opacity: 0.9 }}>预估节省</div>
608
+ <div style={{ fontSize: '20px', fontWeight: 'bold', marginTop: '4px' }}>
609
+ ${((statistics.overview.totalTokens / 1000000) * 15).toFixed(2)}
610
+ </div>
611
+ <div style={{ fontSize: '12px', opacity: 0.9, marginTop: '4px' }}>按 $15/1M tokens 估算</div>
612
+ </div>
613
+ </div>
614
+ </div>
615
+ </div>
616
+ </div>
617
+ );
618
+ }
619
+
620
+ export default StatisticsPage;
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import ReactMarkdown from 'react-markdown';
3
+ import usageContent from '../../../README.md?raw';
4
+
5
+ const UsagePage: React.FC = () => {
6
+ return (
7
+ <div className="markdown-content">
8
+ <ReactMarkdown>{usageContent}</ReactMarkdown>
9
+ </div>
10
+ );
11
+ };
12
+
13
+ export default UsagePage;