@oh-my-pi/omp-stats 12.1.0 → 12.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/package.json +14 -7
- package/src/client/App.tsx +58 -555
- package/src/client/components/ChartsContainer.tsx +185 -0
- package/src/client/components/Header.tsx +48 -0
- package/src/client/components/ModelsTable.tsx +343 -0
- package/src/client/components/RequestDetail.tsx +105 -101
- package/src/client/components/RequestList.tsx +37 -112
- package/src/client/components/StatsGrid.tsx +88 -0
- package/src/client/index.tsx +1 -0
- package/src/client/styles.css +277 -0
- package/src/server.ts +39 -30
- package/src/client/components/StatCard.tsx +0 -32
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { X } from "lucide-react";
|
|
1
|
+
import { Clock, Coins, FileJson, Gauge, Hash, X, Zap } from "lucide-react";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { getRequestDetails } from "../api";
|
|
4
4
|
import type { RequestDetails } from "../types";
|
|
@@ -21,18 +21,13 @@ export function RequestDetail({ id, onClose }: RequestDetailProps) {
|
|
|
21
21
|
|
|
22
22
|
if (!details && loading) {
|
|
23
23
|
return (
|
|
24
|
-
<div
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
alignItems: "center",
|
|
32
|
-
zIndex: 100,
|
|
33
|
-
}}
|
|
34
|
-
>
|
|
35
|
-
<div style={{ background: "var(--bg-secondary)", padding: "20px", borderRadius: "8px" }}>Loading...</div>
|
|
24
|
+
<div className="fixed inset-0 bg-[var(--bg-overlay)] flex justify-center items-center z-[100]">
|
|
25
|
+
<div className="surface px-8 py-6">
|
|
26
|
+
<div className="flex items-center gap-3 text-[var(--text-secondary)]">
|
|
27
|
+
<div className="w-5 h-5 border-2 border-[var(--border-default)] border-t-[var(--accent-cyan)] rounded-full spin" />
|
|
28
|
+
<span>Loading...</span>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
36
31
|
</div>
|
|
37
32
|
);
|
|
38
33
|
}
|
|
@@ -43,118 +38,127 @@ export function RequestDetail({ id, onClose }: RequestDetailProps) {
|
|
|
43
38
|
// biome-ignore lint/a11y/noStaticElementInteractions: modal backdrop dismissal
|
|
44
39
|
<div
|
|
45
40
|
role="presentation"
|
|
46
|
-
|
|
47
|
-
position: "fixed",
|
|
48
|
-
inset: 0,
|
|
49
|
-
background: "rgba(0,0,0,0.8)",
|
|
50
|
-
display: "flex",
|
|
51
|
-
justifyContent: "end",
|
|
52
|
-
zIndex: 100,
|
|
53
|
-
}}
|
|
41
|
+
className="fixed inset-0 bg-[var(--bg-overlay)] backdrop-blur-sm flex justify-end z-[100] animate-fade-in"
|
|
54
42
|
onClick={onClose}
|
|
55
43
|
>
|
|
56
44
|
{/* biome-ignore lint/a11y/useKeyWithClickEvents: stopPropagation for modal content */}
|
|
57
45
|
<div
|
|
58
46
|
role="dialog"
|
|
59
47
|
aria-modal="true"
|
|
60
|
-
|
|
61
|
-
width: "800px",
|
|
62
|
-
maxWidth: "100%",
|
|
63
|
-
background: "var(--bg-primary)",
|
|
64
|
-
height: "100%",
|
|
65
|
-
overflowY: "auto",
|
|
66
|
-
borderLeft: "1px solid var(--border)",
|
|
67
|
-
padding: "30px",
|
|
68
|
-
}}
|
|
48
|
+
className="w-[600px] max-w-full bg-[var(--bg-page)] h-full overflow-y-auto border-l border-[var(--border-subtle)] animate-slide-up"
|
|
69
49
|
onClick={e => e.stopPropagation()}
|
|
70
50
|
>
|
|
71
|
-
|
|
72
|
-
|
|
51
|
+
{/* Header */}
|
|
52
|
+
<div className="sticky top-0 bg-[var(--bg-page)]/95 backdrop-blur border-b border-[var(--border-subtle)] px-6 py-4 flex justify-between items-center z-10">
|
|
53
|
+
<div className="flex items-center gap-3">
|
|
54
|
+
<div className="w-8 h-8 rounded-[var(--radius-sm)] bg-gradient-to-br from-[var(--accent-pink)]/20 to-[var(--accent-cyan)]/20 flex items-center justify-center">
|
|
55
|
+
<FileJson size={16} className="text-[var(--accent-cyan)]" />
|
|
56
|
+
</div>
|
|
57
|
+
<h2 className="text-lg font-semibold text-[var(--text-primary)]">Request Details</h2>
|
|
58
|
+
</div>
|
|
73
59
|
<button
|
|
74
60
|
type="button"
|
|
75
61
|
onClick={onClose}
|
|
76
|
-
|
|
62
|
+
className="p-2 rounded-[var(--radius-sm)] text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] transition-colors"
|
|
77
63
|
>
|
|
78
|
-
<X />
|
|
64
|
+
<X size={20} />
|
|
79
65
|
</button>
|
|
80
66
|
</div>
|
|
81
67
|
|
|
82
|
-
<div
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<div>
|
|
86
|
-
|
|
68
|
+
<div className="p-6 space-y-6">
|
|
69
|
+
{/* Model Info */}
|
|
70
|
+
<div className="surface p-5">
|
|
71
|
+
<div className="flex items-center justify-between mb-4">
|
|
72
|
+
<div>
|
|
73
|
+
<div className="text-2xl font-bold text-[var(--text-primary)]">{details.model}</div>
|
|
74
|
+
<div className="text-sm text-[var(--text-muted)]">{details.provider}</div>
|
|
75
|
+
</div>
|
|
76
|
+
{details.errorMessage ? (
|
|
77
|
+
<span className="badge badge-error">Error</span>
|
|
78
|
+
) : (
|
|
79
|
+
<span className="badge badge-success">Success</span>
|
|
80
|
+
)}
|
|
87
81
|
</div>
|
|
88
82
|
</div>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
83
|
+
|
|
84
|
+
{/* Stats Grid */}
|
|
85
|
+
<div className="grid grid-cols-2 gap-4">
|
|
86
|
+
<div className="surface p-4">
|
|
87
|
+
<div className="flex items-center gap-2 text-[var(--text-muted)] mb-2">
|
|
88
|
+
<Coins size={14} />
|
|
89
|
+
<span className="text-xs uppercase tracking-wide">Cost</span>
|
|
90
|
+
</div>
|
|
91
|
+
<div className="text-xl font-semibold text-[var(--text-primary)]">
|
|
92
|
+
${details.usage.cost.total.toFixed(4)}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div className="surface p-4">
|
|
97
|
+
<div className="flex items-center gap-2 text-[var(--text-muted)] mb-2">
|
|
98
|
+
<Hash size={14} />
|
|
99
|
+
<span className="text-xs uppercase tracking-wide">Tokens</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="text-xl font-semibold text-[var(--text-primary)]">
|
|
102
|
+
{details.usage.totalTokens.toLocaleString()}
|
|
103
|
+
</div>
|
|
104
|
+
<div className="text-xs text-[var(--text-muted)] mt-1">
|
|
105
|
+
{details.usage.input.toLocaleString()} in · {details.usage.output.toLocaleString()} out
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div className="surface p-4">
|
|
110
|
+
<div className="flex items-center gap-2 text-[var(--text-muted)] mb-2">
|
|
111
|
+
<Clock size={14} />
|
|
112
|
+
<span className="text-xs uppercase tracking-wide">Duration</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="text-xl font-semibold text-[var(--text-primary)]">
|
|
115
|
+
{details.duration ? `${(details.duration / 1000).toFixed(2)}s` : "-"}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div className="surface p-4">
|
|
120
|
+
<div className="flex items-center gap-2 text-[var(--text-muted)] mb-2">
|
|
121
|
+
<Zap size={14} />
|
|
122
|
+
<span className="text-xs uppercase tracking-wide">TTFT</span>
|
|
123
|
+
</div>
|
|
124
|
+
<div className="text-xl font-semibold text-[var(--text-primary)]">
|
|
125
|
+
{details.ttft ? `${(details.ttft / 1000).toFixed(2)}s` : "-"}
|
|
126
|
+
</div>
|
|
97
127
|
</div>
|
|
98
128
|
</div>
|
|
129
|
+
|
|
130
|
+
{/* Tokens/Sec */}
|
|
131
|
+
{details.duration && details.usage.output > 0 && (
|
|
132
|
+
<div className="surface p-4">
|
|
133
|
+
<div className="flex items-center justify-between">
|
|
134
|
+
<div className="flex items-center gap-2 text-[var(--text-muted)]">
|
|
135
|
+
<Gauge size={14} />
|
|
136
|
+
<span className="text-xs uppercase tracking-wide">Throughput</span>
|
|
137
|
+
</div>
|
|
138
|
+
<span className="text-2xl font-bold gradient-text">
|
|
139
|
+
{((details.usage.output * 1000) / details.duration).toFixed(1)}
|
|
140
|
+
</span>
|
|
141
|
+
</div>
|
|
142
|
+
<div className="text-xs text-[var(--text-muted)] mt-1 text-right">tokens/second</div>
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{/* Output */}
|
|
99
147
|
<div>
|
|
100
|
-
<
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<div style={{ color: "var(--text-secondary)", fontSize: "0.9rem" }}>TTFT</div>
|
|
105
|
-
<div>{details.ttft ? `${(details.ttft / 1000).toFixed(2)}s` : "-"}</div>
|
|
148
|
+
<h3 className="text-sm font-semibold text-[var(--text-primary)] mb-3">Output</h3>
|
|
149
|
+
<pre className="surface bg-[var(--bg-elevated)] p-4 rounded-[var(--radius-md)] text-sm font-mono text-[var(--text-secondary)] overflow-x-auto">
|
|
150
|
+
{JSON.stringify(details.output, null, 2)}
|
|
151
|
+
</pre>
|
|
106
152
|
</div>
|
|
153
|
+
|
|
154
|
+
{/* Raw Metadata */}
|
|
107
155
|
<div>
|
|
108
|
-
<
|
|
109
|
-
<
|
|
110
|
-
{details
|
|
111
|
-
|
|
112
|
-
: "-"}
|
|
113
|
-
</div>
|
|
156
|
+
<h3 className="text-sm font-semibold text-[var(--text-primary)] mb-3">Raw Metadata</h3>
|
|
157
|
+
<pre className="surface bg-[var(--bg-elevated)] p-4 rounded-[var(--radius-md)] text-xs font-mono text-[var(--text-muted)] overflow-x-auto">
|
|
158
|
+
{JSON.stringify(details, null, 2)}
|
|
159
|
+
</pre>
|
|
114
160
|
</div>
|
|
115
161
|
</div>
|
|
116
|
-
|
|
117
|
-
<h3 style={{ borderBottom: "1px solid var(--border)", paddingBottom: "10px", marginBottom: "15px" }}>
|
|
118
|
-
Output
|
|
119
|
-
</h3>
|
|
120
|
-
<pre
|
|
121
|
-
style={{
|
|
122
|
-
background: "var(--bg-secondary)",
|
|
123
|
-
padding: "20px",
|
|
124
|
-
borderRadius: "8px",
|
|
125
|
-
whiteSpace: "pre-wrap",
|
|
126
|
-
overflowX: "auto",
|
|
127
|
-
fontSize: "0.9rem",
|
|
128
|
-
fontFamily: "monospace",
|
|
129
|
-
}}
|
|
130
|
-
>
|
|
131
|
-
{JSON.stringify(details.output, null, 2)}
|
|
132
|
-
</pre>
|
|
133
|
-
|
|
134
|
-
<h3
|
|
135
|
-
style={{
|
|
136
|
-
borderBottom: "1px solid var(--border)",
|
|
137
|
-
paddingBottom: "10px",
|
|
138
|
-
marginBottom: "15px",
|
|
139
|
-
marginTop: "30px",
|
|
140
|
-
}}
|
|
141
|
-
>
|
|
142
|
-
Raw Metadata
|
|
143
|
-
</h3>
|
|
144
|
-
<pre
|
|
145
|
-
style={{
|
|
146
|
-
background: "var(--bg-secondary)",
|
|
147
|
-
padding: "20px",
|
|
148
|
-
borderRadius: "8px",
|
|
149
|
-
whiteSpace: "pre-wrap",
|
|
150
|
-
overflowX: "auto",
|
|
151
|
-
fontSize: "0.8rem",
|
|
152
|
-
fontFamily: "monospace",
|
|
153
|
-
color: "var(--text-secondary)",
|
|
154
|
-
}}
|
|
155
|
-
>
|
|
156
|
-
{JSON.stringify(details, null, 2)}
|
|
157
|
-
</pre>
|
|
158
162
|
</div>
|
|
159
163
|
</div>
|
|
160
164
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatDistanceToNow } from "date-fns";
|
|
2
|
+
import { CheckCircle2, XCircle } from "lucide-react";
|
|
2
3
|
import type { MessageStats } from "../types";
|
|
3
4
|
|
|
4
5
|
interface RequestListProps {
|
|
@@ -9,94 +10,20 @@ interface RequestListProps {
|
|
|
9
10
|
|
|
10
11
|
export function RequestList({ requests, onSelect, title }: RequestListProps) {
|
|
11
12
|
return (
|
|
12
|
-
<div
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
borderRadius: "12px",
|
|
16
|
-
border: "1px solid var(--border)",
|
|
17
|
-
overflow: "hidden",
|
|
18
|
-
display: "flex",
|
|
19
|
-
flexDirection: "column",
|
|
20
|
-
height: "100%",
|
|
21
|
-
}}
|
|
22
|
-
>
|
|
23
|
-
<div style={{ padding: "16px 20px", borderBottom: "1px solid var(--border)" }}>
|
|
24
|
-
<h3 style={{ margin: 0, fontSize: "1rem" }}>{title}</h3>
|
|
13
|
+
<div className="surface overflow-hidden flex flex-col h-full">
|
|
14
|
+
<div className="px-5 py-4 border-b border-[var(--border-subtle)]">
|
|
15
|
+
<h3 className="text-sm font-semibold text-[var(--text-primary)]">{title}</h3>
|
|
25
16
|
</div>
|
|
26
|
-
<div
|
|
27
|
-
<table
|
|
28
|
-
<thead
|
|
17
|
+
<div className="overflow-auto flex-1">
|
|
18
|
+
<table className="w-full">
|
|
19
|
+
<thead className="bg-[var(--bg-elevated)] sticky top-0 z-10">
|
|
29
20
|
<tr>
|
|
30
|
-
<th
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}}
|
|
37
|
-
>
|
|
38
|
-
Time
|
|
39
|
-
</th>
|
|
40
|
-
<th
|
|
41
|
-
style={{
|
|
42
|
-
textAlign: "left",
|
|
43
|
-
padding: "12px 20px",
|
|
44
|
-
color: "var(--text-secondary)",
|
|
45
|
-
fontWeight: 500,
|
|
46
|
-
}}
|
|
47
|
-
>
|
|
48
|
-
Model
|
|
49
|
-
</th>
|
|
50
|
-
<th
|
|
51
|
-
style={{
|
|
52
|
-
textAlign: "right",
|
|
53
|
-
padding: "12px 20px",
|
|
54
|
-
color: "var(--text-secondary)",
|
|
55
|
-
fontWeight: 500,
|
|
56
|
-
}}
|
|
57
|
-
>
|
|
58
|
-
Tokens
|
|
59
|
-
</th>
|
|
60
|
-
<th
|
|
61
|
-
style={{
|
|
62
|
-
textAlign: "right",
|
|
63
|
-
padding: "12px 20px",
|
|
64
|
-
color: "var(--text-secondary)",
|
|
65
|
-
fontWeight: 500,
|
|
66
|
-
}}
|
|
67
|
-
>
|
|
68
|
-
Cost
|
|
69
|
-
</th>
|
|
70
|
-
<th
|
|
71
|
-
style={{
|
|
72
|
-
textAlign: "right",
|
|
73
|
-
padding: "12px 20px",
|
|
74
|
-
color: "var(--text-secondary)",
|
|
75
|
-
fontWeight: 500,
|
|
76
|
-
}}
|
|
77
|
-
>
|
|
78
|
-
Duration
|
|
79
|
-
</th>
|
|
80
|
-
<th
|
|
81
|
-
style={{
|
|
82
|
-
textAlign: "right",
|
|
83
|
-
padding: "12px 20px",
|
|
84
|
-
color: "var(--text-secondary)",
|
|
85
|
-
fontWeight: 500,
|
|
86
|
-
}}
|
|
87
|
-
>
|
|
88
|
-
TTFT
|
|
89
|
-
</th>
|
|
90
|
-
<th
|
|
91
|
-
style={{
|
|
92
|
-
textAlign: "right",
|
|
93
|
-
padding: "12px 20px",
|
|
94
|
-
color: "var(--text-secondary)",
|
|
95
|
-
fontWeight: 500,
|
|
96
|
-
}}
|
|
97
|
-
>
|
|
98
|
-
Tokens/s
|
|
99
|
-
</th>
|
|
21
|
+
<th className="text-left py-3 px-4 table-header">Model</th>
|
|
22
|
+
<th className="text-left py-3 px-4 table-header">Time</th>
|
|
23
|
+
<th className="text-right py-3 px-4 table-header">Tokens</th>
|
|
24
|
+
<th className="text-right py-3 px-4 table-header">Cost</th>
|
|
25
|
+
<th className="text-right py-3 px-4 table-header">Duration</th>
|
|
26
|
+
<th className="text-center py-3 px-4 table-header">Status</th>
|
|
100
27
|
</tr>
|
|
101
28
|
</thead>
|
|
102
29
|
<tbody>
|
|
@@ -104,42 +31,40 @@ export function RequestList({ requests, onSelect, title }: RequestListProps) {
|
|
|
104
31
|
<tr
|
|
105
32
|
key={`${req.sessionFile}-${req.entryId}`}
|
|
106
33
|
onClick={() => onSelect(req)}
|
|
107
|
-
|
|
108
|
-
cursor: "pointer",
|
|
109
|
-
borderBottom: "1px solid var(--border)",
|
|
110
|
-
transition: "background 0.1s",
|
|
111
|
-
}}
|
|
112
|
-
onMouseEnter={e => {
|
|
113
|
-
e.currentTarget.style.background = "rgba(255,255,255,0.05)";
|
|
114
|
-
}}
|
|
115
|
-
onMouseLeave={e => {
|
|
116
|
-
e.currentTarget.style.background = "transparent";
|
|
117
|
-
}}
|
|
34
|
+
className="table-row cursor-pointer border-b border-[var(--border-subtle)] last:border-b-0"
|
|
118
35
|
>
|
|
119
|
-
<td
|
|
120
|
-
{
|
|
36
|
+
<td className="py-3 px-4">
|
|
37
|
+
<div className="font-medium text-[var(--text-primary)] text-sm">{req.model}</div>
|
|
38
|
+
<div className="text-xs text-[var(--text-muted)]">{req.provider}</div>
|
|
121
39
|
</td>
|
|
122
|
-
<td
|
|
123
|
-
|
|
124
|
-
<div style={{ fontSize: "0.8rem", color: "var(--text-secondary)" }}>{req.provider}</div>
|
|
40
|
+
<td className="py-3 px-4 text-sm text-[var(--text-secondary)]">
|
|
41
|
+
{formatDistanceToNow(req.timestamp, { addSuffix: true })}
|
|
125
42
|
</td>
|
|
126
|
-
<td
|
|
43
|
+
<td className="py-3 px-4 text-right text-sm text-[var(--text-secondary)] font-mono">
|
|
127
44
|
{req.usage.totalTokens.toLocaleString()}
|
|
128
45
|
</td>
|
|
129
|
-
<td
|
|
130
|
-
|
|
131
|
-
{req.duration ? `${(req.duration / 1000).toFixed(1)}s` : "-"}
|
|
46
|
+
<td className="py-3 px-4 text-right text-sm text-[var(--text-secondary)] font-mono">
|
|
47
|
+
${req.usage.cost.total.toFixed(4)}
|
|
132
48
|
</td>
|
|
133
|
-
<td
|
|
134
|
-
{req.
|
|
49
|
+
<td className="py-3 px-4 text-right text-sm text-[var(--text-secondary)] font-mono">
|
|
50
|
+
{req.duration ? `${(req.duration / 1000).toFixed(1)}s` : "-"}
|
|
135
51
|
</td>
|
|
136
|
-
<td
|
|
137
|
-
{req.
|
|
138
|
-
|
|
139
|
-
|
|
52
|
+
<td className="py-3 px-4 text-center">
|
|
53
|
+
{req.errorMessage ? (
|
|
54
|
+
<XCircle size={16} className="text-[var(--accent-red)] mx-auto" />
|
|
55
|
+
) : (
|
|
56
|
+
<CheckCircle2 size={16} className="text-[var(--accent-green)] mx-auto" />
|
|
57
|
+
)}
|
|
140
58
|
</td>
|
|
141
59
|
</tr>
|
|
142
60
|
))}
|
|
61
|
+
{requests.length === 0 && (
|
|
62
|
+
<tr>
|
|
63
|
+
<td colSpan={6} className="py-12 text-center text-[var(--text-muted)] text-sm">
|
|
64
|
+
No requests found
|
|
65
|
+
</td>
|
|
66
|
+
</tr>
|
|
67
|
+
)}
|
|
143
68
|
</tbody>
|
|
144
69
|
</table>
|
|
145
70
|
</div>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Activity, AlertCircle, BarChart3, Database, Server, Zap } from "lucide-react";
|
|
2
|
+
import type { AggregatedStats } from "../types";
|
|
3
|
+
|
|
4
|
+
interface StatsGridProps {
|
|
5
|
+
stats: AggregatedStats;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const statConfig = [
|
|
9
|
+
{
|
|
10
|
+
key: "requests",
|
|
11
|
+
title: "Total Requests",
|
|
12
|
+
icon: Server,
|
|
13
|
+
color: "var(--accent-violet)",
|
|
14
|
+
getValue: (s: AggregatedStats) => s.totalRequests.toLocaleString(),
|
|
15
|
+
getDetail: (s: AggregatedStats) =>
|
|
16
|
+
`${s.successfulRequests.toLocaleString()} success · ${s.failedRequests.toLocaleString()} errors`,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
key: "cost",
|
|
20
|
+
title: "Total Cost",
|
|
21
|
+
icon: Activity,
|
|
22
|
+
color: "var(--accent-pink)",
|
|
23
|
+
getValue: (s: AggregatedStats) => `$${s.totalCost.toFixed(2)}`,
|
|
24
|
+
getDetail: (s: AggregatedStats) =>
|
|
25
|
+
s.totalRequests > 0 ? `$${(s.totalCost / s.totalRequests).toFixed(4)} avg/req` : "-",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: "cache",
|
|
29
|
+
title: "Cache Rate",
|
|
30
|
+
icon: Database,
|
|
31
|
+
color: "var(--accent-cyan)",
|
|
32
|
+
getValue: (s: AggregatedStats) => `${(s.cacheRate * 100).toFixed(1)}%`,
|
|
33
|
+
getDetail: (s: AggregatedStats) => `${(s.totalCacheReadTokens / 1000).toFixed(1)}k cached tokens`,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
key: "errors",
|
|
37
|
+
title: "Error Rate",
|
|
38
|
+
icon: AlertCircle,
|
|
39
|
+
color: "var(--accent-red)",
|
|
40
|
+
getValue: (s: AggregatedStats) => `${(s.errorRate * 100).toFixed(1)}%`,
|
|
41
|
+
getDetail: (s: AggregatedStats) => `${s.failedRequests.toLocaleString()} failed requests`,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: "tokens",
|
|
45
|
+
title: "Tokens/Sec",
|
|
46
|
+
icon: BarChart3,
|
|
47
|
+
color: "var(--accent-green)",
|
|
48
|
+
getValue: (s: AggregatedStats) => s.avgTokensPerSecond?.toFixed(1) ?? "-",
|
|
49
|
+
getDetail: (s: AggregatedStats) => `${(s.totalInputTokens + s.totalOutputTokens).toLocaleString()} total tokens`,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: "ttft",
|
|
53
|
+
title: "TTFT",
|
|
54
|
+
icon: Zap,
|
|
55
|
+
color: "var(--accent-amber)",
|
|
56
|
+
getValue: (s: AggregatedStats) => (s.avgTtft ? `${(s.avgTtft / 1000).toFixed(2)}s` : "-"),
|
|
57
|
+
getDetail: () => "Time to first token",
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export function StatsGrid({ stats }: StatsGridProps) {
|
|
62
|
+
return (
|
|
63
|
+
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4 mb-8">
|
|
64
|
+
{statConfig.map(stat => {
|
|
65
|
+
const Icon = stat.icon;
|
|
66
|
+
return (
|
|
67
|
+
<div key={stat.key} className="stat-card group">
|
|
68
|
+
<div className="flex items-center justify-between mb-3">
|
|
69
|
+
<span className="text-sm font-medium text-[var(--text-secondary)]">{stat.title}</span>
|
|
70
|
+
<div
|
|
71
|
+
className="p-2 rounded-[var(--radius-sm)] transition-colors"
|
|
72
|
+
style={{ backgroundColor: `${stat.color}15` }}
|
|
73
|
+
>
|
|
74
|
+
<Icon
|
|
75
|
+
size={18}
|
|
76
|
+
style={{ color: stat.color }}
|
|
77
|
+
className="transition-transform group-hover:scale-110"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="text-2xl font-bold text-[var(--text-primary)] mb-1">{stat.getValue(stats)}</div>
|
|
82
|
+
<div className="text-xs text-[var(--text-muted)] truncate">{stat.getDetail(stats)}</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
})}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
package/src/client/index.tsx
CHANGED