@oh-my-pi/omp-stats 14.9.3 → 14.9.7

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,35 +1,21 @@
1
- import { useMemo } from "react";
2
1
  import type { CostTimeSeriesPoint } from "../types";
3
2
 
4
3
  interface CostSummaryProps {
5
4
  costSeries: CostTimeSeriesPoint[];
6
5
  }
7
6
 
8
- const SUMMARY_DAYS = 30;
9
-
10
7
  function formatCost(value: number): string {
11
8
  return `$${Math.round(value)}`;
12
9
  }
13
10
 
14
11
  export function CostSummary({ costSeries }: CostSummaryProps) {
15
- const cutoff = Date.now() - SUMMARY_DAYS * 86400000;
16
- const prevCutoff = cutoff - SUMMARY_DAYS * 86400000;
17
-
18
- const current = useMemo(() => costSeries.filter(p => p.timestamp >= cutoff), [costSeries, cutoff]);
19
- const previous = useMemo(
20
- () => costSeries.filter(p => p.timestamp >= prevCutoff && p.timestamp < cutoff),
21
- [costSeries, prevCutoff, cutoff],
22
- );
23
-
24
- const totalCost = current.reduce((sum, p) => sum + p.cost, 0);
25
- const prevTotalCost = previous.reduce((sum, p) => sum + p.cost, 0);
26
-
27
- const dayBuckets = new Set(current.map(p => p.timestamp)).size;
12
+ const totalCost = costSeries.reduce((sum, p) => sum + p.cost, 0);
13
+ const dayBuckets = new Set(costSeries.map(p => p.timestamp)).size;
28
14
  const avgDaily = dayBuckets > 0 ? totalCost / dayBuckets : 0;
29
15
 
30
- // Most expensive model over current period
16
+ // Most expensive model over the visible window
31
17
  const modelTotals = new Map<string, number>();
32
- for (const point of current) {
18
+ for (const point of costSeries) {
33
19
  modelTotals.set(point.model, (modelTotals.get(point.model) ?? 0) + point.cost);
34
20
  }
35
21
  let topModel = "";
@@ -41,47 +27,22 @@ export function CostSummary({ costSeries }: CostSummaryProps) {
41
27
  }
42
28
  }
43
29
 
44
- const trend = prevTotalCost > 0 ? ((totalCost - prevTotalCost) / prevTotalCost) * 100 : null;
45
-
46
30
  const cards = [
47
- {
48
- label: "Total (30d)",
49
- value: formatCost(totalCost),
50
- positive: null as boolean | null,
51
- },
52
- {
53
- label: "Avg / day",
54
- value: formatCost(avgDaily),
55
- positive: null as boolean | null,
56
- },
31
+ { label: "Total", value: formatCost(totalCost) },
32
+ { label: "Avg / day", value: formatCost(avgDaily) },
57
33
  {
58
34
  label: "Top model",
59
35
  value: topModel || "—",
60
36
  sub: topModel ? formatCost(topModelCost) : undefined,
61
- positive: null as boolean | null,
62
- },
63
- {
64
- label: "vs prev 30d",
65
- value: trend !== null ? `${trend >= 0 ? "+" : ""}${Math.round(trend)}%` : "—",
66
- sub: undefined as string | undefined,
67
- positive: trend !== null ? trend <= 0 : null,
68
37
  },
69
38
  ];
70
39
 
71
40
  return (
72
- <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
41
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
73
42
  {cards.map(card => (
74
43
  <div key={card.label} className="surface px-4 py-3">
75
44
  <p className="text-xs text-[var(--text-muted)] mb-1">{card.label}</p>
76
- <p
77
- className={`text-lg font-semibold ${
78
- card.positive === true
79
- ? "text-[var(--accent-green,#4ade80)]"
80
- : card.positive === false
81
- ? "text-[var(--accent-pink)]"
82
- : "text-[var(--text-primary)]"
83
- }`}
84
- >
45
+ <p className="text-lg font-semibold text-[var(--text-primary)] truncate" title={card.value}>
85
46
  {card.value}
86
47
  </p>
87
48
  {card.sub && <p className="text-xs text-[var(--text-muted)] mt-0.5">{card.sub}</p>}
@@ -1,17 +1,28 @@
1
1
  import { Activity, RefreshCw } from "lucide-react";
2
+ import type { TimeRange } from "../types";
2
3
 
3
- type Tab = "overview" | "requests" | "errors" | "models" | "costs";
4
+ type Tab = "overview" | "requests" | "errors" | "models" | "costs" | "behavior";
5
+
6
+ const tabs: Tab[] = ["overview", "requests", "errors", "models", "costs", "behavior"];
7
+ const timeRanges: { label: string; value: TimeRange }[] = [
8
+ { label: "1h", value: "1h" },
9
+ { label: "24h", value: "24h" },
10
+ { label: "7d", value: "7d" },
11
+ { label: "30d", value: "30d" },
12
+ { label: "90d", value: "90d" },
13
+ { label: "All", value: "all" },
14
+ ];
4
15
 
5
16
  interface HeaderProps {
6
17
  activeTab: Tab;
7
18
  onTabChange: (tab: Tab) => void;
8
19
  onSync: () => void;
9
20
  syncing: boolean;
21
+ timeRange: TimeRange;
22
+ onTimeRangeChange: (timeRange: TimeRange) => void;
10
23
  }
11
24
 
12
- const tabs: Tab[] = ["overview", "requests", "errors", "models", "costs"];
13
-
14
- export function Header({ activeTab, onTabChange, onSync, syncing }: HeaderProps) {
25
+ export function Header({ activeTab, onTabChange, onSync, syncing, timeRange, onTimeRangeChange }: HeaderProps) {
15
26
  return (
16
27
  <header className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-6 mb-8 border-b border-[var(--border-subtle)]">
17
28
  <div className="flex items-center gap-3">
@@ -37,6 +48,19 @@ export function Header({ activeTab, onTabChange, onSync, syncing }: HeaderProps)
37
48
  </button>
38
49
  ))}
39
50
  </div>
51
+ <div className="flex bg-[var(--bg-surface)] rounded-[var(--radius-md)] p-1 border border-[var(--border-subtle)]">
52
+ {timeRanges.map(range => (
53
+ <button
54
+ key={range.value}
55
+ type="button"
56
+ onClick={() => onTimeRangeChange(range.value)}
57
+ className={`tab-btn ${timeRange === range.value ? "active" : ""}`}
58
+ title={range.value === "all" ? "All time" : `Last ${range.label}`}
59
+ >
60
+ {range.label}
61
+ </button>
62
+ ))}
63
+ </div>
40
64
 
41
65
  <button type="button" onClick={onSync} disabled={syncing} className="btn btn-primary">
42
66
  <RefreshCw size={16} className={syncing ? "spin" : ""} />
@@ -5,6 +5,15 @@ interface StatsGridProps {
5
5
  stats: AggregatedStats;
6
6
  }
7
7
 
8
+ const compactNumberFormatter = new Intl.NumberFormat(undefined, {
9
+ notation: "compact",
10
+ maximumFractionDigits: 1,
11
+ });
12
+
13
+ function formatCompactNumber(value: number): string {
14
+ return compactNumberFormatter.format(value);
15
+ }
16
+
8
17
  const statConfig = [
9
18
  {
10
19
  key: "requests",
@@ -39,7 +48,7 @@ const statConfig = [
39
48
  icon: Database,
40
49
  color: "var(--accent-cyan)",
41
50
  getValue: (s: AggregatedStats) => `${(s.cacheRate * 100).toFixed(1)}%`,
42
- getDetail: (s: AggregatedStats) => `${(s.totalCacheReadTokens / 1000).toFixed(1)}k cached tokens`,
51
+ getDetail: (s: AggregatedStats) => `${formatCompactNumber(s.totalCacheReadTokens)} cached tokens`,
43
52
  },
44
53
  {
45
54
  key: "errors",
@@ -59,6 +59,7 @@ export interface AggregatedStats {
59
59
  lastTimestamp: number;
60
60
  }
61
61
 
62
+ export type TimeRange = "1h" | "24h" | "7d" | "30d" | "90d" | "all";
62
63
  export interface ModelStats extends AggregatedStats {
63
64
  model: string;
64
65
  provider: string;
@@ -113,3 +114,65 @@ export interface DashboardStats {
113
114
  modelPerformanceSeries: ModelPerformancePoint[];
114
115
  costSeries: CostTimeSeriesPoint[];
115
116
  }
117
+
118
+ export interface OverviewStats {
119
+ overall: AggregatedStats;
120
+ timeSeries: TimeSeriesPoint[];
121
+ }
122
+
123
+ export interface ModelDashboardStats {
124
+ byModel: ModelStats[];
125
+ modelSeries: ModelTimeSeriesPoint[];
126
+ modelPerformanceSeries: ModelPerformancePoint[];
127
+ }
128
+
129
+ export interface CostDashboardStats {
130
+ costSeries: CostTimeSeriesPoint[];
131
+ }
132
+
133
+ export interface BehaviorTimeSeriesPoint {
134
+ timestamp: number;
135
+ model: string;
136
+ provider: string;
137
+ messages: number;
138
+ yelling: number;
139
+ profanity: number;
140
+ anguish: number;
141
+ negation: number;
142
+ repetition: number;
143
+ blame: number;
144
+ chars: number;
145
+ }
146
+
147
+ export interface BehaviorOverallStats {
148
+ totalMessages: number;
149
+ totalYelling: number;
150
+ totalProfanity: number;
151
+ totalAnguish: number;
152
+ totalNegation: number;
153
+ totalRepetition: number;
154
+ totalBlame: number;
155
+ totalChars: number;
156
+ firstTimestamp: number;
157
+ lastTimestamp: number;
158
+ }
159
+
160
+ export interface BehaviorModelStats {
161
+ model: string;
162
+ provider: string;
163
+ totalMessages: number;
164
+ totalYelling: number;
165
+ totalProfanity: number;
166
+ totalAnguish: number;
167
+ totalNegation: number;
168
+ totalRepetition: number;
169
+ totalBlame: number;
170
+ totalChars: number;
171
+ lastTimestamp: number;
172
+ }
173
+
174
+ export interface BehaviorDashboardStats {
175
+ overall: BehaviorOverallStats;
176
+ byModel: BehaviorModelStats[];
177
+ behaviorSeries: BehaviorTimeSeriesPoint[];
178
+ }