@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.
@@ -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
- style={{
26
- position: "fixed",
27
- inset: 0,
28
- background: "rgba(0,0,0,0.5)",
29
- display: "flex",
30
- justifyContent: "center",
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
- style={{
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
- style={{
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
- <div style={{ display: "flex", justifyContent: "space-between", marginBottom: "20px" }}>
72
- <h2 style={{ margin: 0 }}>Request Details</h2>
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
- style={{ background: "none", border: "none", color: "var(--text-secondary)", cursor: "pointer" }}
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 style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "20px", marginBottom: "30px" }}>
83
- <div>
84
- <div style={{ color: "var(--text-secondary)", fontSize: "0.9rem" }}>Model</div>
85
- <div>
86
- {details.model} ({details.provider})
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
- <div>
90
- <div style={{ color: "var(--text-secondary)", fontSize: "0.9rem" }}>Cost</div>
91
- <div>${details.usage.cost.total.toFixed(4)}</div>
92
- </div>
93
- <div>
94
- <div style={{ color: "var(--text-secondary)", fontSize: "0.9rem" }}>Tokens</div>
95
- <div>
96
- {details.usage.totalTokens} (In: {details.usage.input}, Out: {details.usage.output})
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
- <div style={{ color: "var(--text-secondary)", fontSize: "0.9rem" }}>Duration</div>
101
- <div>{details.duration ? `${(details.duration / 1000).toFixed(2)}s` : "-"}</div>
102
- </div>
103
- <div>
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
- <div style={{ color: "var(--text-secondary)", fontSize: "0.9rem" }}>Tokens/sec</div>
109
- <div>
110
- {details.duration && details.usage.output
111
- ? ((details.usage.output * 1000) / details.duration).toFixed(1)
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
- style={{
14
- background: "var(--bg-secondary)",
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 style={{ overflowY: "auto", flex: 1 }}>
27
- <table style={{ width: "100%", borderCollapse: "collapse", fontSize: "0.9rem" }}>
28
- <thead style={{ background: "rgba(0,0,0,0.2)", position: "sticky", top: 0 }}>
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
- style={{
32
- textAlign: "left",
33
- padding: "12px 20px",
34
- color: "var(--text-secondary)",
35
- fontWeight: 500,
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
- style={{
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 style={{ padding: "12px 20px" }}>
120
- {formatDistanceToNow(req.timestamp, { addSuffix: true })}
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 style={{ padding: "12px 20px" }}>
123
- <div style={{ fontWeight: 500 }}>{req.model}</div>
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 style={{ padding: "12px 20px", textAlign: "right" }}>
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 style={{ padding: "12px 20px", textAlign: "right" }}>${req.usage.cost.total.toFixed(4)}</td>
130
- <td style={{ padding: "12px 20px", textAlign: "right" }}>
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 style={{ padding: "12px 20px", textAlign: "right" }}>
134
- {req.ttft ? `${(req.ttft / 1000).toFixed(2)}s` : "-"}
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 style={{ padding: "12px 20px", textAlign: "right" }}>
137
- {req.duration && req.usage.output
138
- ? ((req.usage.output * 1000) / req.duration).toFixed(1)
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
+ }
@@ -1,3 +1,4 @@
1
+ import "./styles.css";
1
2
  import { createRoot } from "react-dom/client";
2
3
  import App from "./App";
3
4