@oh-my-pi/omp-stats 14.9.3 → 14.9.5
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 +6 -6
- package/src/aggregator.ts +138 -11
- package/src/client/App.tsx +125 -30
- package/src/client/api.ts +35 -3
- package/src/client/components/BehaviorChart.tsx +367 -0
- package/src/client/components/BehaviorModelsTable.tsx +422 -0
- package/src/client/components/BehaviorSummary.tsx +75 -0
- package/src/client/components/CostChart.tsx +5 -38
- package/src/client/components/CostSummary.tsx +8 -47
- package/src/client/components/Header.tsx +28 -4
- package/src/client/components/StatsGrid.tsx +10 -1
- package/src/client/types.ts +54 -0
- package/src/db.ts +307 -26
- package/src/parser.ts +75 -4
- package/src/server.ts +30 -6
- package/src/types.ts +81 -0
- package/src/user-metrics.ts +486 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/omp-stats",
|
|
4
|
-
"version": "14.9.
|
|
4
|
+
"version": "14.9.5",
|
|
5
5
|
"description": "Local observability dashboard for pi AI usage statistics",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,22 +37,22 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-ai": "14.9.
|
|
41
|
-
"@oh-my-pi/pi-utils": "14.9.
|
|
40
|
+
"@oh-my-pi/pi-ai": "14.9.5",
|
|
41
|
+
"@oh-my-pi/pi-utils": "14.9.5",
|
|
42
42
|
"@tailwindcss/node": "^4.2.4",
|
|
43
43
|
"chart.js": "^4.5.1",
|
|
44
44
|
"date-fns": "^4.1.0",
|
|
45
45
|
"lucide-react": "^1.14.0",
|
|
46
46
|
"react": "19.2.5",
|
|
47
47
|
"react-chartjs-2": "^5.3.1",
|
|
48
|
-
"react-dom": "19.2.5"
|
|
48
|
+
"react-dom": "19.2.5",
|
|
49
|
+
"tailwindcss": "^4.2.4"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@types/bun": "^1.3.13",
|
|
52
53
|
"@types/react": "^19.2.14",
|
|
53
54
|
"@types/react-dom": "^19.2.3",
|
|
54
|
-
"postcss": "^8.5.14"
|
|
55
|
-
"tailwindcss": "^4.2.4"
|
|
55
|
+
"postcss": "^8.5.14"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
58
|
"bun": ">=1.3.7"
|
package/src/aggregator.ts
CHANGED
|
@@ -2,6 +2,9 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import {
|
|
3
3
|
getRecentErrors as dbGetRecentErrors,
|
|
4
4
|
getRecentRequests as dbGetRecentRequests,
|
|
5
|
+
getBehaviorByModel,
|
|
6
|
+
getBehaviorOverall,
|
|
7
|
+
getBehaviorTimeSeries,
|
|
5
8
|
getCostTimeSeries,
|
|
6
9
|
getFileOffset,
|
|
7
10
|
getMessageById,
|
|
@@ -14,10 +17,11 @@ import {
|
|
|
14
17
|
getTimeSeries,
|
|
15
18
|
initDb,
|
|
16
19
|
insertMessageStats,
|
|
20
|
+
insertUserMessageStats,
|
|
17
21
|
setFileOffset,
|
|
18
22
|
} from "./db";
|
|
19
23
|
import { getSessionEntry, listAllSessionFiles, parseSessionFile } from "./parser";
|
|
20
|
-
import type { DashboardStats, MessageStats, RequestDetails } from "./types";
|
|
24
|
+
import type { BehaviorDashboardStats, DashboardStats, MessageStats, RequestDetails } from "./types";
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
27
|
* Sync a single session file to the database.
|
|
@@ -42,16 +46,19 @@ async function syncSessionFile(sessionFile: string): Promise<number> {
|
|
|
42
46
|
|
|
43
47
|
// Parse file from last offset
|
|
44
48
|
const fromOffset = stored?.offset ?? 0;
|
|
45
|
-
const { stats, newOffset } = await parseSessionFile(sessionFile, fromOffset);
|
|
49
|
+
const { stats, userStats, newOffset } = await parseSessionFile(sessionFile, fromOffset);
|
|
46
50
|
|
|
47
51
|
if (stats.length > 0) {
|
|
48
52
|
insertMessageStats(stats);
|
|
49
53
|
}
|
|
54
|
+
if (userStats.length > 0) {
|
|
55
|
+
insertUserMessageStats(userStats);
|
|
56
|
+
}
|
|
50
57
|
|
|
51
58
|
// Update offset tracker
|
|
52
59
|
setFileOffset(sessionFile, newOffset, lastModified);
|
|
53
60
|
|
|
54
|
-
return stats.length;
|
|
61
|
+
return stats.length + userStats.length;
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
/**
|
|
@@ -76,20 +83,130 @@ export async function syncAllSessions(): Promise<{ processed: number; files: num
|
|
|
76
83
|
return { processed: totalProcessed, files: filesProcessed };
|
|
77
84
|
}
|
|
78
85
|
|
|
86
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
87
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
88
|
+
|
|
89
|
+
type TimeRange = "1h" | "24h" | "7d" | "30d" | "90d" | "all";
|
|
90
|
+
|
|
91
|
+
interface TimeRangeConfig {
|
|
92
|
+
timeSeriesHours: number;
|
|
93
|
+
timeSeriesBucketMs: number;
|
|
94
|
+
modelSeriesDays: number;
|
|
95
|
+
modelPerformanceDays: number;
|
|
96
|
+
costSeriesDays: number;
|
|
97
|
+
cutoff: number | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const DEFAULT_TIME_RANGE: TimeRange = "24h";
|
|
101
|
+
|
|
102
|
+
const TIME_RANGE_TO_CONFIG: Record<TimeRange, Omit<TimeRangeConfig, "cutoff">> = {
|
|
103
|
+
"1h": {
|
|
104
|
+
timeSeriesHours: 1,
|
|
105
|
+
timeSeriesBucketMs: HOUR_MS,
|
|
106
|
+
modelSeriesDays: 1,
|
|
107
|
+
modelPerformanceDays: 1,
|
|
108
|
+
costSeriesDays: 1,
|
|
109
|
+
},
|
|
110
|
+
"24h": {
|
|
111
|
+
timeSeriesHours: 24,
|
|
112
|
+
timeSeriesBucketMs: HOUR_MS,
|
|
113
|
+
modelSeriesDays: 1,
|
|
114
|
+
modelPerformanceDays: 1,
|
|
115
|
+
costSeriesDays: 1,
|
|
116
|
+
},
|
|
117
|
+
"7d": {
|
|
118
|
+
timeSeriesHours: 24 * 7,
|
|
119
|
+
timeSeriesBucketMs: DAY_MS,
|
|
120
|
+
modelSeriesDays: 7,
|
|
121
|
+
modelPerformanceDays: 7,
|
|
122
|
+
costSeriesDays: 7,
|
|
123
|
+
},
|
|
124
|
+
"30d": {
|
|
125
|
+
timeSeriesHours: 24 * 30,
|
|
126
|
+
timeSeriesBucketMs: DAY_MS,
|
|
127
|
+
modelSeriesDays: 30,
|
|
128
|
+
modelPerformanceDays: 30,
|
|
129
|
+
costSeriesDays: 30,
|
|
130
|
+
},
|
|
131
|
+
"90d": {
|
|
132
|
+
timeSeriesHours: 24 * 90,
|
|
133
|
+
timeSeriesBucketMs: DAY_MS,
|
|
134
|
+
modelSeriesDays: 90,
|
|
135
|
+
modelPerformanceDays: 90,
|
|
136
|
+
costSeriesDays: 90,
|
|
137
|
+
},
|
|
138
|
+
all: {
|
|
139
|
+
timeSeriesHours: 24 * 3650,
|
|
140
|
+
timeSeriesBucketMs: DAY_MS,
|
|
141
|
+
modelSeriesDays: 3650,
|
|
142
|
+
modelPerformanceDays: 3650,
|
|
143
|
+
costSeriesDays: 3650,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
function getTimeRangeConfig(range?: string | null): TimeRangeConfig {
|
|
148
|
+
const normalized = range?.trim().toLowerCase() ?? DEFAULT_TIME_RANGE;
|
|
149
|
+
const config = TIME_RANGE_TO_CONFIG[normalized as TimeRange];
|
|
150
|
+
if (config) {
|
|
151
|
+
const cutoff = normalized === "all" ? null : Date.now() - Math.max(1, config.timeSeriesHours * 60 * 60 * 1000);
|
|
152
|
+
return { ...config, cutoff };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const fallbackConfig = TIME_RANGE_TO_CONFIG[DEFAULT_TIME_RANGE];
|
|
156
|
+
return {
|
|
157
|
+
...fallbackConfig,
|
|
158
|
+
cutoff: Date.now() - fallbackConfig.timeSeriesHours * 60 * 60 * 1000,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
79
162
|
/**
|
|
80
163
|
* Get all dashboard stats.
|
|
81
164
|
*/
|
|
82
|
-
export async function getDashboardStats(): Promise<DashboardStats> {
|
|
165
|
+
export async function getDashboardStats(range?: string | null): Promise<DashboardStats> {
|
|
166
|
+
await initDb();
|
|
167
|
+
const { timeSeriesHours, timeSeriesBucketMs, modelSeriesDays, modelPerformanceDays, costSeriesDays, cutoff } =
|
|
168
|
+
getTimeRangeConfig(range);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
overall: getOverallStats(cutoff ?? undefined),
|
|
172
|
+
byModel: getStatsByModel(cutoff ?? undefined),
|
|
173
|
+
byFolder: getStatsByFolder(cutoff ?? undefined),
|
|
174
|
+
timeSeries: getTimeSeries(timeSeriesHours, cutoff, timeSeriesBucketMs),
|
|
175
|
+
modelSeries: getModelTimeSeries(modelSeriesDays, cutoff),
|
|
176
|
+
modelPerformanceSeries: getModelPerformanceSeries(modelPerformanceDays, cutoff),
|
|
177
|
+
costSeries: getCostTimeSeries(costSeriesDays, cutoff),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function getOverviewStats(range?: string | null): Promise<Pick<DashboardStats, "overall" | "timeSeries">> {
|
|
182
|
+
await initDb();
|
|
183
|
+
const { timeSeriesHours, timeSeriesBucketMs, cutoff } = getTimeRangeConfig(range);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
overall: getOverallStats(cutoff ?? undefined),
|
|
187
|
+
timeSeries: getTimeSeries(timeSeriesHours, cutoff, timeSeriesBucketMs),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function getModelDashboardStats(
|
|
192
|
+
range?: string | null,
|
|
193
|
+
): Promise<Pick<DashboardStats, "byModel" | "modelSeries" | "modelPerformanceSeries">> {
|
|
194
|
+
await initDb();
|
|
195
|
+
const { modelSeriesDays, modelPerformanceDays, cutoff } = getTimeRangeConfig(range);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
byModel: getStatsByModel(cutoff ?? undefined),
|
|
199
|
+
modelSeries: getModelTimeSeries(modelSeriesDays, cutoff),
|
|
200
|
+
modelPerformanceSeries: getModelPerformanceSeries(modelPerformanceDays, cutoff),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function getCostDashboardStats(range?: string | null): Promise<Pick<DashboardStats, "costSeries">> {
|
|
83
205
|
await initDb();
|
|
206
|
+
const { costSeriesDays, cutoff } = getTimeRangeConfig(range);
|
|
84
207
|
|
|
85
208
|
return {
|
|
86
|
-
|
|
87
|
-
byModel: getStatsByModel(),
|
|
88
|
-
byFolder: getStatsByFolder(),
|
|
89
|
-
timeSeries: getTimeSeries(24),
|
|
90
|
-
modelSeries: getModelTimeSeries(14),
|
|
91
|
-
modelPerformanceSeries: getModelPerformanceSeries(14),
|
|
92
|
-
costSeries: getCostTimeSeries(90),
|
|
209
|
+
costSeries: getCostTimeSeries(costSeriesDays, cutoff),
|
|
93
210
|
};
|
|
94
211
|
}
|
|
95
212
|
export async function getRecentRequests(limit?: number): Promise<MessageStats[]> {
|
|
@@ -128,3 +245,13 @@ export async function getTotalMessageCount(): Promise<number> {
|
|
|
128
245
|
await initDb();
|
|
129
246
|
return getMessageCount();
|
|
130
247
|
}
|
|
248
|
+
|
|
249
|
+
export async function getBehaviorDashboardStats(range?: string | null): Promise<BehaviorDashboardStats> {
|
|
250
|
+
await initDb();
|
|
251
|
+
const { cutoff } = getTimeRangeConfig(range);
|
|
252
|
+
return {
|
|
253
|
+
overall: getBehaviorOverall(cutoff),
|
|
254
|
+
byModel: getBehaviorByModel(cutoff),
|
|
255
|
+
behaviorSeries: getBehaviorTimeSeries(cutoff),
|
|
256
|
+
};
|
|
257
|
+
}
|
package/src/client/App.tsx
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getBehaviorDashboardStats,
|
|
4
|
+
getCostDashboardStats,
|
|
5
|
+
getModelDashboardStats,
|
|
6
|
+
getOverviewStats,
|
|
7
|
+
getRecentErrors,
|
|
8
|
+
getRecentRequests,
|
|
9
|
+
sync,
|
|
10
|
+
} from "./api";
|
|
11
|
+
import { BehaviorChart } from "./components/BehaviorChart";
|
|
12
|
+
import { BehaviorModelsTable } from "./components/BehaviorModelsTable";
|
|
13
|
+
import { BehaviorSummary } from "./components/BehaviorSummary";
|
|
3
14
|
import { ChartsContainer } from "./components/ChartsContainer";
|
|
4
15
|
import { CostChart } from "./components/CostChart";
|
|
5
16
|
import { CostSummary } from "./components/CostSummary";
|
|
@@ -8,64 +19,102 @@ import { ModelsTable } from "./components/ModelsTable";
|
|
|
8
19
|
import { RequestDetail } from "./components/RequestDetail";
|
|
9
20
|
import { RequestList } from "./components/RequestList";
|
|
10
21
|
import { StatsGrid } from "./components/StatsGrid";
|
|
11
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
BehaviorDashboardStats,
|
|
24
|
+
CostDashboardStats,
|
|
25
|
+
MessageStats,
|
|
26
|
+
ModelDashboardStats,
|
|
27
|
+
OverviewStats,
|
|
28
|
+
TimeRange,
|
|
29
|
+
} from "./types";
|
|
12
30
|
|
|
13
|
-
type Tab = "overview" | "requests" | "errors" | "models" | "costs";
|
|
31
|
+
type Tab = "overview" | "requests" | "errors" | "models" | "costs" | "behavior";
|
|
14
32
|
|
|
15
33
|
export default function App() {
|
|
16
|
-
const [
|
|
34
|
+
const [overviewStats, setOverviewStats] = useState<OverviewStats | null>(null);
|
|
35
|
+
const [modelStats, setModelStats] = useState<ModelDashboardStats | null>(null);
|
|
36
|
+
const [costStats, setCostStats] = useState<CostDashboardStats | null>(null);
|
|
37
|
+
const [behaviorStats, setBehaviorStats] = useState<BehaviorDashboardStats | null>(null);
|
|
17
38
|
const [recentRequests, setRecentRequests] = useState<MessageStats[]>([]);
|
|
18
39
|
const [recentErrors, setRecentErrors] = useState<MessageStats[]>([]);
|
|
19
40
|
const [selectedRequest, setSelectedRequest] = useState<number | null>(null);
|
|
20
41
|
const [syncing, setSyncing] = useState(false);
|
|
21
42
|
const [activeTab, setActiveTab] = useState<Tab>("overview");
|
|
43
|
+
const [timeRange, setTimeRange] = useState<TimeRange>("24h");
|
|
22
44
|
|
|
23
|
-
const
|
|
45
|
+
const loadRecentLists = useCallback(async () => {
|
|
24
46
|
try {
|
|
25
|
-
const [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
setRecentErrors(e);
|
|
47
|
+
const [requests, errors] = await Promise.all([getRecentRequests(50), getRecentErrors(50)]);
|
|
48
|
+
setRecentRequests(requests);
|
|
49
|
+
setRecentErrors(errors);
|
|
29
50
|
} catch (err) {
|
|
30
51
|
console.error(err);
|
|
31
52
|
}
|
|
32
53
|
}, []);
|
|
33
54
|
|
|
55
|
+
const loadActiveTabStats = useCallback(async () => {
|
|
56
|
+
try {
|
|
57
|
+
if (activeTab === "models") {
|
|
58
|
+
setModelStats(await getModelDashboardStats(timeRange));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (activeTab === "costs") {
|
|
62
|
+
setCostStats(await getCostDashboardStats(timeRange));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (activeTab === "behavior") {
|
|
66
|
+
setBehaviorStats(await getBehaviorDashboardStats(timeRange));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (activeTab === "overview") {
|
|
70
|
+
setOverviewStats(await getOverviewStats(timeRange));
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error(err);
|
|
74
|
+
}
|
|
75
|
+
}, [activeTab, timeRange]);
|
|
76
|
+
|
|
34
77
|
const handleSync = async () => {
|
|
35
78
|
setSyncing(true);
|
|
36
79
|
try {
|
|
37
80
|
await sync();
|
|
38
|
-
await
|
|
81
|
+
await Promise.all([loadActiveTabStats(), loadRecentLists()]);
|
|
39
82
|
} finally {
|
|
40
83
|
setSyncing(false);
|
|
41
84
|
}
|
|
42
85
|
};
|
|
43
86
|
|
|
44
87
|
useEffect(() => {
|
|
45
|
-
|
|
46
|
-
const interval = setInterval(
|
|
88
|
+
loadRecentLists();
|
|
89
|
+
const interval = setInterval(loadRecentLists, 30000);
|
|
47
90
|
return () => clearInterval(interval);
|
|
48
|
-
}, [
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<span className="text-sm">Loading analytics...</span>
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
91
|
+
}, [loadRecentLists]);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
loadActiveTabStats();
|
|
95
|
+
const interval = setInterval(loadActiveTabStats, 30000);
|
|
96
|
+
return () => clearInterval(interval);
|
|
97
|
+
}, [loadActiveTabStats]);
|
|
60
98
|
|
|
61
99
|
return (
|
|
62
100
|
<div className="min-h-screen">
|
|
63
101
|
<div className="max-w-[1600px] mx-auto px-6 py-6">
|
|
64
|
-
<Header
|
|
102
|
+
<Header
|
|
103
|
+
activeTab={activeTab}
|
|
104
|
+
onTabChange={setActiveTab}
|
|
105
|
+
onSync={handleSync}
|
|
106
|
+
syncing={syncing}
|
|
107
|
+
timeRange={timeRange}
|
|
108
|
+
onTimeRangeChange={setTimeRange}
|
|
109
|
+
/>
|
|
65
110
|
|
|
66
111
|
{activeTab === "overview" && (
|
|
67
112
|
<div className="space-y-6 animate-fade-in">
|
|
68
|
-
|
|
113
|
+
{overviewStats ? (
|
|
114
|
+
<StatsGrid stats={overviewStats.overall} />
|
|
115
|
+
) : (
|
|
116
|
+
<LoadingState label="Loading overview..." />
|
|
117
|
+
)}
|
|
69
118
|
|
|
70
119
|
<div className="grid lg:grid-cols-2 gap-6">
|
|
71
120
|
<RequestList
|
|
@@ -104,15 +153,50 @@ export default function App() {
|
|
|
104
153
|
|
|
105
154
|
{activeTab === "models" && (
|
|
106
155
|
<div className="space-y-6 animate-fade-in">
|
|
107
|
-
|
|
108
|
-
|
|
156
|
+
{modelStats ? (
|
|
157
|
+
<>
|
|
158
|
+
<ChartsContainer modelSeries={modelStats.modelSeries} />
|
|
159
|
+
<ModelsTable
|
|
160
|
+
models={modelStats.byModel}
|
|
161
|
+
performanceSeries={modelStats.modelPerformanceSeries}
|
|
162
|
+
/>
|
|
163
|
+
</>
|
|
164
|
+
) : (
|
|
165
|
+
<LoadingState label="Loading models..." />
|
|
166
|
+
)}
|
|
109
167
|
</div>
|
|
110
168
|
)}
|
|
111
169
|
|
|
112
170
|
{activeTab === "costs" && (
|
|
113
171
|
<div className="space-y-6 animate-fade-in">
|
|
114
|
-
|
|
115
|
-
|
|
172
|
+
{costStats ? (
|
|
173
|
+
<>
|
|
174
|
+
<CostSummary costSeries={costStats.costSeries} />
|
|
175
|
+
<CostChart costSeries={costStats.costSeries} />
|
|
176
|
+
</>
|
|
177
|
+
) : (
|
|
178
|
+
<LoadingState label="Loading costs..." />
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
{activeTab === "behavior" && (
|
|
184
|
+
<div className="space-y-6 animate-fade-in">
|
|
185
|
+
{behaviorStats ? (
|
|
186
|
+
<>
|
|
187
|
+
<BehaviorSummary
|
|
188
|
+
overall={behaviorStats.overall}
|
|
189
|
+
behaviorSeries={behaviorStats.behaviorSeries}
|
|
190
|
+
/>
|
|
191
|
+
<BehaviorChart behaviorSeries={behaviorStats.behaviorSeries} />
|
|
192
|
+
<BehaviorModelsTable
|
|
193
|
+
models={behaviorStats.byModel}
|
|
194
|
+
behaviorSeries={behaviorStats.behaviorSeries}
|
|
195
|
+
/>
|
|
196
|
+
</>
|
|
197
|
+
) : (
|
|
198
|
+
<LoadingState label="Loading behavior..." />
|
|
199
|
+
)}
|
|
116
200
|
</div>
|
|
117
201
|
)}
|
|
118
202
|
|
|
@@ -123,3 +207,14 @@ export default function App() {
|
|
|
123
207
|
</div>
|
|
124
208
|
);
|
|
125
209
|
}
|
|
210
|
+
|
|
211
|
+
function LoadingState({ label }: { label: string }) {
|
|
212
|
+
return (
|
|
213
|
+
<div className="min-h-[180px] flex items-center justify-center">
|
|
214
|
+
<div className="flex items-center gap-3 text-[var(--text-muted)]">
|
|
215
|
+
<div className="w-5 h-5 border-2 border-[var(--border-default)] border-t-[var(--accent-cyan)] rounded-full spin" />
|
|
216
|
+
<span className="text-sm">{label}</span>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
package/src/client/api.ts
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BehaviorDashboardStats,
|
|
3
|
+
CostDashboardStats,
|
|
4
|
+
DashboardStats,
|
|
5
|
+
MessageStats,
|
|
6
|
+
ModelDashboardStats,
|
|
7
|
+
OverviewStats,
|
|
8
|
+
RequestDetails,
|
|
9
|
+
} from "./types";
|
|
2
10
|
|
|
3
11
|
const API_BASE = "/api";
|
|
4
12
|
|
|
5
|
-
export async function getStats(): Promise<DashboardStats> {
|
|
6
|
-
const res = await fetch(`${API_BASE}/stats`);
|
|
13
|
+
export async function getStats(range = "24h"): Promise<DashboardStats> {
|
|
14
|
+
const res = await fetch(`${API_BASE}/stats?range=${encodeURIComponent(range)}`);
|
|
7
15
|
if (!res.ok) throw new Error("Failed to fetch stats");
|
|
8
16
|
return res.json() as Promise<DashboardStats>;
|
|
9
17
|
}
|
|
10
18
|
|
|
19
|
+
export async function getOverviewStats(range = "24h"): Promise<OverviewStats> {
|
|
20
|
+
const res = await fetch(`${API_BASE}/stats/overview?range=${encodeURIComponent(range)}`);
|
|
21
|
+
if (!res.ok) throw new Error("Failed to fetch overview stats");
|
|
22
|
+
return res.json() as Promise<OverviewStats>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function getModelDashboardStats(range = "24h"): Promise<ModelDashboardStats> {
|
|
26
|
+
const res = await fetch(`${API_BASE}/stats/model-dashboard?range=${encodeURIComponent(range)}`);
|
|
27
|
+
if (!res.ok) throw new Error("Failed to fetch model stats");
|
|
28
|
+
return res.json() as Promise<ModelDashboardStats>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function getCostDashboardStats(range = "24h"): Promise<CostDashboardStats> {
|
|
32
|
+
const res = await fetch(`${API_BASE}/stats/costs?range=${encodeURIComponent(range)}`);
|
|
33
|
+
if (!res.ok) throw new Error("Failed to fetch cost stats");
|
|
34
|
+
return res.json() as Promise<CostDashboardStats>;
|
|
35
|
+
}
|
|
36
|
+
|
|
11
37
|
export async function getRecentRequests(limit = 50): Promise<MessageStats[]> {
|
|
12
38
|
const res = await fetch(`${API_BASE}/stats/recent?limit=${limit}`);
|
|
13
39
|
if (!res.ok) throw new Error("Failed to fetch recent requests");
|
|
@@ -31,3 +57,9 @@ export async function sync(): Promise<any> {
|
|
|
31
57
|
if (!res.ok) throw new Error("Failed to sync");
|
|
32
58
|
return res.json();
|
|
33
59
|
}
|
|
60
|
+
|
|
61
|
+
export async function getBehaviorDashboardStats(range = "24h"): Promise<BehaviorDashboardStats> {
|
|
62
|
+
const res = await fetch(`${API_BASE}/stats/behavior?range=${encodeURIComponent(range)}`);
|
|
63
|
+
if (!res.ok) throw new Error("Failed to fetch behavior stats");
|
|
64
|
+
return res.json() as Promise<BehaviorDashboardStats>;
|
|
65
|
+
}
|