claude-session-dashboard 0.1.3 → 0.3.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/README.md +156 -14
- package/dist/client/assets/_dashboard-I7m6D7BE.js +1 -0
- package/dist/client/assets/_sessionId-DEliIff6.js +12 -0
- package/dist/client/assets/app-D7yorIIh.css +1 -0
- package/dist/client/assets/{createServerFn-Le0d8Pjz.js → createServerFn-Bn6_ISOt.js} +1 -1
- package/dist/client/assets/format-Bsprb3az.js +1 -0
- package/dist/client/assets/index-BkqRvnEf.js +1 -0
- package/dist/client/assets/{main-CzD8HjLq.js → main-CfJIADCp.js} +7 -7
- package/dist/client/assets/sessions.queries-CrJg4dYU.js +1 -0
- package/dist/client/assets/settings-C4_lsEzl.js +1 -0
- package/dist/client/assets/{settings.types-B4841OLF.js → settings.types-9Qf5WcRY.js} +1 -1
- package/dist/client/assets/stats-_r1gmaTe.js +4 -0
- package/dist/client/assets/useSessionCost-DPZ-ubM1.js +65 -0
- package/dist/client/favicon.svg +3 -0
- package/dist/server/assets/_dashboard-TUzgwLqB.js +112 -0
- package/dist/server/assets/{_sessionId-BwZK4Ezz.js → _sessionId-C-XZIPqn.js} +57 -35
- package/dist/server/assets/_tanstack-start-manifest_v-B51mSkGz.js +4 -0
- package/dist/server/assets/{claude-path-CkuljM34.js → claude-path-BdwflgZ1.js} +9 -3
- package/dist/server/assets/{format-CGmJnuhZ.js → format-DIZHV7IJ.js} +3 -3
- package/dist/server/assets/{index-D4VWrt2z.js → index-CKfH7HpA.js} +28 -60
- package/dist/server/assets/project-analytics.server-BkWSd6a8.js +61 -0
- package/dist/server/assets/{router-xTSe9UH_.js → router-Cb_hBXHI.js} +62 -31
- package/dist/server/assets/{session-detail.server-azkRfON2.js → session-detail.server-DLXl-Pn-.js} +1 -1
- package/dist/server/assets/session-scanner-CLfls9u-.js +93 -0
- package/dist/server/assets/sessions.queries-B5ZBiVJy.js +42 -0
- package/dist/server/assets/{sessions.server-B8zbmvSM.js → sessions.server-CUhasKW2.js} +5 -89
- package/dist/server/assets/{settings-ko61yfVs.js → settings-C0_KyVQQ.js} +66 -20
- package/dist/server/assets/stats-BtgVene-.js +886 -0
- package/dist/server/assets/{stats.server-BZWxV-mC.js → stats.server-qTOvID9-.js} +62 -3
- package/dist/server/assets/useSessionCost-CYs5UOX-.js +209 -0
- package/dist/server/server.js +13 -10
- package/package.json +11 -1
- package/dist/client/assets/_dashboard-CYwTENkn.js +0 -1
- package/dist/client/assets/_sessionId-Bwfhm_El.js +0 -12
- package/dist/client/assets/app-DhZyFob1.css +0 -1
- package/dist/client/assets/format-Bf-cSf6L.js +0 -1
- package/dist/client/assets/index-DXhX1hdS.js +0 -1
- package/dist/client/assets/settings-BSPc79zZ.js +0 -1
- package/dist/client/assets/stats-CDIvpOt9.js +0 -4
- package/dist/client/assets/useSessionCost-9NP6uhla.js +0 -61
- package/dist/server/assets/_dashboard--ukhquwO.js +0 -97
- package/dist/server/assets/_tanstack-start-manifest_v-gtQY7f-T.js +0 -4
- package/dist/server/assets/stats-DItsFPp5.js +0 -266
- package/dist/server/assets/useSessionCost-EB0VxklP.js +0 -76
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo } from "react";
|
|
3
|
+
import { queryOptions, useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { c as createSsrRpc } from "./createSsrRpc-CVg2UDl0.js";
|
|
5
|
+
import { c as createServerFn } from "../server.js";
|
|
6
|
+
import { ResponsiveContainer, BarChart, CartesianGrid, XAxis, YAxis, Tooltip, Bar, AreaChart, Area, PieChart, Pie, Cell, Legend } from "recharts";
|
|
7
|
+
import { format, addDays, getDay, parseISO, startOfISOWeek } from "date-fns";
|
|
8
|
+
import { createPortal } from "react-dom";
|
|
9
|
+
import { f as formatTokenCount, a as formatDuration, b as formatRelativeTime, c as formatUSD } from "./format-DIZHV7IJ.js";
|
|
10
|
+
import { Link } from "@tanstack/react-router";
|
|
11
|
+
import { u as usePrivacy, R as Route } from "./router-Cb_hBXHI.js";
|
|
12
|
+
import { u as useSessionCost, E as ExportDropdown, d as downloadFile, a as dailyActivityToCSV, b as dailyTokensToCSV, m as modelUsageToCSV, s as statsToJSON } from "./useSessionCost-CYs5UOX-.js";
|
|
13
|
+
import "@tanstack/history";
|
|
14
|
+
import "@tanstack/router-core/ssr/client";
|
|
15
|
+
import "@tanstack/router-core";
|
|
16
|
+
import "node:async_hooks";
|
|
17
|
+
import "@tanstack/router-core/ssr/server";
|
|
18
|
+
import "h3-v2";
|
|
19
|
+
import "tiny-invariant";
|
|
20
|
+
import "seroval";
|
|
21
|
+
import "@tanstack/react-router/ssr/server";
|
|
22
|
+
import "zod";
|
|
23
|
+
import "./settings.queries-DSQd324O.js";
|
|
24
|
+
import "./settings.types-DntadCHo.js";
|
|
25
|
+
const getStats = createServerFn({
|
|
26
|
+
method: "GET"
|
|
27
|
+
}).handler(createSsrRpc("4b9a58c176f487b49800a372100037cdf33cf048f3592a449f115c7e3f5ea799"));
|
|
28
|
+
const statsQuery = queryOptions({
|
|
29
|
+
queryKey: ["stats"],
|
|
30
|
+
queryFn: () => getStats(),
|
|
31
|
+
refetchInterval: 6e4
|
|
32
|
+
});
|
|
33
|
+
function ActivityChart({ data }) {
|
|
34
|
+
const chartData = data.map((d) => ({
|
|
35
|
+
...d,
|
|
36
|
+
dateLabel: format(new Date(d.date), "MMM d")
|
|
37
|
+
}));
|
|
38
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
|
|
39
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Daily Activity" }),
|
|
40
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Messages, sessions, and tool calls per day" }),
|
|
41
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4 h-64", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(BarChart, { data: chartData, children: [
|
|
42
|
+
/* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "#2a2926" }),
|
|
43
|
+
/* @__PURE__ */ jsx(
|
|
44
|
+
XAxis,
|
|
45
|
+
{
|
|
46
|
+
dataKey: "dateLabel",
|
|
47
|
+
tick: { fill: "#7a7668", fontSize: 10 },
|
|
48
|
+
tickLine: false
|
|
49
|
+
}
|
|
50
|
+
),
|
|
51
|
+
/* @__PURE__ */ jsx(
|
|
52
|
+
YAxis,
|
|
53
|
+
{
|
|
54
|
+
tick: { fill: "#7a7668", fontSize: 10 },
|
|
55
|
+
tickLine: false,
|
|
56
|
+
axisLine: false
|
|
57
|
+
}
|
|
58
|
+
),
|
|
59
|
+
/* @__PURE__ */ jsx(
|
|
60
|
+
Tooltip,
|
|
61
|
+
{
|
|
62
|
+
contentStyle: {
|
|
63
|
+
backgroundColor: "#1c1c1a",
|
|
64
|
+
border: "1px solid #3d3b36",
|
|
65
|
+
borderRadius: "8px",
|
|
66
|
+
fontSize: "12px"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
),
|
|
70
|
+
/* @__PURE__ */ jsx(
|
|
71
|
+
Bar,
|
|
72
|
+
{
|
|
73
|
+
dataKey: "messageCount",
|
|
74
|
+
name: "Messages",
|
|
75
|
+
fill: "#d97757",
|
|
76
|
+
radius: [2, 2, 0, 0]
|
|
77
|
+
}
|
|
78
|
+
),
|
|
79
|
+
/* @__PURE__ */ jsx(
|
|
80
|
+
Bar,
|
|
81
|
+
{
|
|
82
|
+
dataKey: "toolCallCount",
|
|
83
|
+
name: "Tool Calls",
|
|
84
|
+
fill: "#8b5cf6",
|
|
85
|
+
radius: [2, 2, 0, 0]
|
|
86
|
+
}
|
|
87
|
+
),
|
|
88
|
+
/* @__PURE__ */ jsx(
|
|
89
|
+
Bar,
|
|
90
|
+
{
|
|
91
|
+
dataKey: "sessionCount",
|
|
92
|
+
name: "Sessions",
|
|
93
|
+
fill: "#10b981",
|
|
94
|
+
radius: [2, 2, 0, 0]
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
] }) }) })
|
|
98
|
+
] });
|
|
99
|
+
}
|
|
100
|
+
const INTENSITY_COLORS = [
|
|
101
|
+
"#2a2926",
|
|
102
|
+
// Level 0: warm gray-800 (no activity)
|
|
103
|
+
"#3d2a1e",
|
|
104
|
+
// Level 1: dark terracotta
|
|
105
|
+
"#a8512eb3",
|
|
106
|
+
// Level 2: brand-700 at ~70% opacity
|
|
107
|
+
"#d97757cc",
|
|
108
|
+
// Level 3: brand-500 at ~80% opacity
|
|
109
|
+
"#e09070"
|
|
110
|
+
// Level 4: brand-400 (most intense)
|
|
111
|
+
];
|
|
112
|
+
const DAY_LABELS = ["Mon", "", "Wed", "", "Fri", "", ""];
|
|
113
|
+
const CELL_SIZE = 10;
|
|
114
|
+
const CELL_GAP = 2;
|
|
115
|
+
const DAY_LABEL_WIDTH = 28;
|
|
116
|
+
function computePercentiles(values) {
|
|
117
|
+
if (values.length === 0) return { p25: 0, p50: 0, p75: 0 };
|
|
118
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
119
|
+
const percentile = (p) => {
|
|
120
|
+
const idx = p / 100 * (sorted.length - 1);
|
|
121
|
+
const lower = Math.floor(idx);
|
|
122
|
+
const upper = Math.ceil(idx);
|
|
123
|
+
if (lower === upper) return sorted[lower];
|
|
124
|
+
return sorted[lower] + (sorted[upper] - sorted[lower]) * (idx - lower);
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
p25: percentile(25),
|
|
128
|
+
p50: percentile(50),
|
|
129
|
+
p75: percentile(75)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function getIntensityLevel(tokens, percentiles) {
|
|
133
|
+
if (tokens === 0) return 0;
|
|
134
|
+
if (tokens <= percentiles.p25) return 1;
|
|
135
|
+
if (tokens <= percentiles.p50) return 2;
|
|
136
|
+
if (tokens <= percentiles.p75) return 3;
|
|
137
|
+
return 4;
|
|
138
|
+
}
|
|
139
|
+
function toMondayFirst(jsDay) {
|
|
140
|
+
return jsDay === 0 ? 6 : jsDay - 1;
|
|
141
|
+
}
|
|
142
|
+
function ContributionHeatmap({
|
|
143
|
+
dailyActivity,
|
|
144
|
+
dailyModelTokens
|
|
145
|
+
}) {
|
|
146
|
+
const [tooltip, setTooltip] = useState(null);
|
|
147
|
+
const { days, monthLabels } = useMemo(() => {
|
|
148
|
+
const dataMap = /* @__PURE__ */ new Map();
|
|
149
|
+
for (const entry of dailyActivity) {
|
|
150
|
+
const existing = dataMap.get(entry.date) ?? { sessionCount: 0, totalTokens: 0 };
|
|
151
|
+
existing.sessionCount = entry.sessionCount;
|
|
152
|
+
dataMap.set(entry.date, existing);
|
|
153
|
+
}
|
|
154
|
+
for (const entry of dailyModelTokens) {
|
|
155
|
+
const existing = dataMap.get(entry.date) ?? { sessionCount: 0, totalTokens: 0 };
|
|
156
|
+
existing.totalTokens = Object.values(entry.tokensByModel).reduce((sum, t) => sum + t, 0);
|
|
157
|
+
dataMap.set(entry.date, existing);
|
|
158
|
+
}
|
|
159
|
+
const today = /* @__PURE__ */ new Date();
|
|
160
|
+
today.setHours(0, 0, 0, 0);
|
|
161
|
+
let startDate = addDays(today, -364);
|
|
162
|
+
const startDayOfWeek = toMondayFirst(getDay(startDate));
|
|
163
|
+
if (startDayOfWeek !== 0) {
|
|
164
|
+
startDate = addDays(startDate, -startDayOfWeek);
|
|
165
|
+
}
|
|
166
|
+
const allDays = [];
|
|
167
|
+
let currentDate = startDate;
|
|
168
|
+
while (currentDate <= today) {
|
|
169
|
+
const dateStr = format(currentDate, "yyyy-MM-dd");
|
|
170
|
+
const dayOfWeek = toMondayFirst(getDay(currentDate));
|
|
171
|
+
const daysSinceStart = Math.floor(
|
|
172
|
+
(currentDate.getTime() - startDate.getTime()) / (1e3 * 60 * 60 * 24)
|
|
173
|
+
);
|
|
174
|
+
const weekIndex = Math.floor(daysSinceStart / 7);
|
|
175
|
+
const data = dataMap.get(dateStr);
|
|
176
|
+
allDays.push({
|
|
177
|
+
date: dateStr,
|
|
178
|
+
dateFormatted: format(currentDate, "MMM d, yyyy"),
|
|
179
|
+
sessionCount: data?.sessionCount ?? 0,
|
|
180
|
+
totalTokens: data?.totalTokens ?? 0,
|
|
181
|
+
intensity: 0,
|
|
182
|
+
// computed below
|
|
183
|
+
weekIndex,
|
|
184
|
+
dayOfWeek
|
|
185
|
+
});
|
|
186
|
+
currentDate = addDays(currentDate, 1);
|
|
187
|
+
}
|
|
188
|
+
const nonZeroTokens = allDays.filter((d) => d.totalTokens > 0).map((d) => d.totalTokens);
|
|
189
|
+
const percentiles = computePercentiles(nonZeroTokens);
|
|
190
|
+
for (const day of allDays) {
|
|
191
|
+
day.intensity = getIntensityLevel(day.totalTokens, percentiles);
|
|
192
|
+
}
|
|
193
|
+
const labels = [];
|
|
194
|
+
let lastMonth = -1;
|
|
195
|
+
for (const day of allDays) {
|
|
196
|
+
if (day.dayOfWeek === 0) {
|
|
197
|
+
const parsed = parseISO(day.date);
|
|
198
|
+
const month = parsed.getMonth();
|
|
199
|
+
if (month !== lastMonth) {
|
|
200
|
+
labels.push({
|
|
201
|
+
label: format(parsed, "MMM"),
|
|
202
|
+
weekIndex: day.weekIndex
|
|
203
|
+
});
|
|
204
|
+
lastMonth = month;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { days: allDays, monthLabels: labels };
|
|
209
|
+
}, [dailyActivity, dailyModelTokens]);
|
|
210
|
+
if (days.length === 0) {
|
|
211
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
|
|
212
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Activity" }),
|
|
213
|
+
/* @__PURE__ */ jsx("div", { className: "flex h-32 items-center justify-center", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500", children: "No activity data available" }) })
|
|
214
|
+
] });
|
|
215
|
+
}
|
|
216
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
|
|
217
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Activity" }),
|
|
218
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Token usage intensity over the past year" }),
|
|
219
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4 overflow-x-auto", children: /* @__PURE__ */ jsx("div", { className: "relative w-full", children: /* @__PURE__ */ jsxs("div", { className: "flex", children: [
|
|
220
|
+
/* @__PURE__ */ jsxs(
|
|
221
|
+
"div",
|
|
222
|
+
{
|
|
223
|
+
className: "flex flex-col text-[10px] text-gray-500",
|
|
224
|
+
style: { width: DAY_LABEL_WIDTH },
|
|
225
|
+
children: [
|
|
226
|
+
/* @__PURE__ */ jsx("div", { style: { height: 16, marginBottom: CELL_GAP } }),
|
|
227
|
+
DAY_LABELS.map((label, i) => /* @__PURE__ */ jsx(
|
|
228
|
+
"div",
|
|
229
|
+
{
|
|
230
|
+
className: "flex items-center",
|
|
231
|
+
style: { height: CELL_SIZE, marginTop: i > 0 ? CELL_GAP : 0 },
|
|
232
|
+
children: label
|
|
233
|
+
},
|
|
234
|
+
i
|
|
235
|
+
))
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
),
|
|
239
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col", children: [
|
|
240
|
+
/* @__PURE__ */ jsx(
|
|
241
|
+
"div",
|
|
242
|
+
{
|
|
243
|
+
className: "grid text-[10px] text-gray-500",
|
|
244
|
+
style: {
|
|
245
|
+
gridAutoFlow: "column",
|
|
246
|
+
gridAutoColumns: `minmax(${CELL_SIZE}px, 1fr)`,
|
|
247
|
+
gap: CELL_GAP,
|
|
248
|
+
height: 16,
|
|
249
|
+
marginBottom: CELL_GAP
|
|
250
|
+
},
|
|
251
|
+
children: monthLabels.map((ml, i) => /* @__PURE__ */ jsx(
|
|
252
|
+
"div",
|
|
253
|
+
{
|
|
254
|
+
className: "flex items-end",
|
|
255
|
+
style: { gridColumn: ml.weekIndex + 1 },
|
|
256
|
+
children: ml.label
|
|
257
|
+
},
|
|
258
|
+
`${ml.label}-${i}`
|
|
259
|
+
))
|
|
260
|
+
}
|
|
261
|
+
),
|
|
262
|
+
/* @__PURE__ */ jsx(
|
|
263
|
+
"div",
|
|
264
|
+
{
|
|
265
|
+
className: "relative grid",
|
|
266
|
+
style: {
|
|
267
|
+
gridTemplateRows: `repeat(7, ${CELL_SIZE}px)`,
|
|
268
|
+
gridAutoFlow: "column",
|
|
269
|
+
gridAutoColumns: `minmax(${CELL_SIZE}px, 1fr)`,
|
|
270
|
+
gap: CELL_GAP
|
|
271
|
+
},
|
|
272
|
+
children: days.map((day) => /* @__PURE__ */ jsx(
|
|
273
|
+
"div",
|
|
274
|
+
{
|
|
275
|
+
className: "rounded-sm transition-colors",
|
|
276
|
+
style: {
|
|
277
|
+
width: CELL_SIZE,
|
|
278
|
+
height: CELL_SIZE,
|
|
279
|
+
backgroundColor: INTENSITY_COLORS[day.intensity],
|
|
280
|
+
gridRow: day.dayOfWeek + 1,
|
|
281
|
+
gridColumn: day.weekIndex + 1
|
|
282
|
+
},
|
|
283
|
+
onMouseEnter: (e) => {
|
|
284
|
+
const rect = e.target.getBoundingClientRect();
|
|
285
|
+
setTooltip({
|
|
286
|
+
date: day.date,
|
|
287
|
+
dateFormatted: day.dateFormatted,
|
|
288
|
+
sessionCount: day.sessionCount,
|
|
289
|
+
totalTokens: day.totalTokens,
|
|
290
|
+
x: rect.left + rect.width / 2,
|
|
291
|
+
y: rect.top - 8
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
onMouseLeave: () => setTooltip(null)
|
|
295
|
+
},
|
|
296
|
+
day.date
|
|
297
|
+
))
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
] })
|
|
301
|
+
] }) }) }),
|
|
302
|
+
tooltip && createPortal(
|
|
303
|
+
/* @__PURE__ */ jsxs(
|
|
304
|
+
"div",
|
|
305
|
+
{
|
|
306
|
+
className: "pointer-events-none fixed z-50 rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-xs shadow-lg",
|
|
307
|
+
style: {
|
|
308
|
+
left: tooltip.x,
|
|
309
|
+
top: tooltip.y,
|
|
310
|
+
transform: "translate(-50%, -100%)",
|
|
311
|
+
whiteSpace: "nowrap"
|
|
312
|
+
},
|
|
313
|
+
children: [
|
|
314
|
+
/* @__PURE__ */ jsx("p", { className: "font-medium text-gray-300", children: tooltip.dateFormatted }),
|
|
315
|
+
tooltip.totalTokens > 0 || tooltip.sessionCount > 0 ? /* @__PURE__ */ jsxs("div", { className: "mt-1 space-y-0.5 text-gray-400", children: [
|
|
316
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
317
|
+
tooltip.sessionCount,
|
|
318
|
+
" ",
|
|
319
|
+
tooltip.sessionCount === 1 ? "session" : "sessions"
|
|
320
|
+
] }),
|
|
321
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
322
|
+
formatTokenCount(tooltip.totalTokens),
|
|
323
|
+
" tokens"
|
|
324
|
+
] })
|
|
325
|
+
] }) : /* @__PURE__ */ jsx("p", { className: "mt-1 text-gray-500", children: "No activity" })
|
|
326
|
+
]
|
|
327
|
+
}
|
|
328
|
+
),
|
|
329
|
+
document.body
|
|
330
|
+
)
|
|
331
|
+
] });
|
|
332
|
+
}
|
|
333
|
+
const COLORS$1 = ["#d97757", "#8b5cf6", "#10b981", "#f59e0b", "#ef4444", "#b07cc5"];
|
|
334
|
+
function normalizeModelName(model) {
|
|
335
|
+
return model.replace(/^claude-/, "").split("-202")[0];
|
|
336
|
+
}
|
|
337
|
+
function getTopModels(data, limit) {
|
|
338
|
+
const totals = {};
|
|
339
|
+
for (const day of data) {
|
|
340
|
+
for (const [model, tokens] of Object.entries(day.tokensByModel)) {
|
|
341
|
+
const normalized = normalizeModelName(model);
|
|
342
|
+
totals[normalized] = (totals[normalized] ?? 0) + tokens;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const sorted = Object.entries(totals).sort((a, b) => b[1] - a[1]);
|
|
346
|
+
const topModels = sorted.slice(0, limit).map(([name]) => name);
|
|
347
|
+
const hasOther = sorted.length > limit;
|
|
348
|
+
return { topModels, hasOther };
|
|
349
|
+
}
|
|
350
|
+
function processDaily(data, topModels, hasOther) {
|
|
351
|
+
return data.map((day) => {
|
|
352
|
+
const entry = {
|
|
353
|
+
dateLabel: format(parseISO(day.date), "MMM d"),
|
|
354
|
+
sortKey: day.date
|
|
355
|
+
};
|
|
356
|
+
for (const model of topModels) {
|
|
357
|
+
entry[model] = 0;
|
|
358
|
+
}
|
|
359
|
+
if (hasOther) {
|
|
360
|
+
entry["Other"] = 0;
|
|
361
|
+
}
|
|
362
|
+
for (const [rawModel, tokens] of Object.entries(day.tokensByModel)) {
|
|
363
|
+
const normalized = normalizeModelName(rawModel);
|
|
364
|
+
if (topModels.includes(normalized)) {
|
|
365
|
+
entry[normalized] = entry[normalized] + tokens;
|
|
366
|
+
} else if (hasOther) {
|
|
367
|
+
entry["Other"] = entry["Other"] + tokens;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return entry;
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
function processWeekly(data, topModels, hasOther) {
|
|
374
|
+
const weekMap = /* @__PURE__ */ new Map();
|
|
375
|
+
for (const day of data) {
|
|
376
|
+
const weekStart = startOfISOWeek(parseISO(day.date));
|
|
377
|
+
const weekKey = format(weekStart, "yyyy-MM-dd");
|
|
378
|
+
if (!weekMap.has(weekKey)) {
|
|
379
|
+
const entry2 = {
|
|
380
|
+
dateLabel: `Week of ${format(weekStart, "MMM d")}`,
|
|
381
|
+
sortKey: weekKey
|
|
382
|
+
};
|
|
383
|
+
for (const model of topModels) {
|
|
384
|
+
entry2[model] = 0;
|
|
385
|
+
}
|
|
386
|
+
if (hasOther) {
|
|
387
|
+
entry2["Other"] = 0;
|
|
388
|
+
}
|
|
389
|
+
weekMap.set(weekKey, entry2);
|
|
390
|
+
}
|
|
391
|
+
const entry = weekMap.get(weekKey);
|
|
392
|
+
for (const [rawModel, tokens] of Object.entries(day.tokensByModel)) {
|
|
393
|
+
const normalized = normalizeModelName(rawModel);
|
|
394
|
+
if (topModels.includes(normalized)) {
|
|
395
|
+
entry[normalized] = entry[normalized] + tokens;
|
|
396
|
+
} else if (hasOther) {
|
|
397
|
+
entry["Other"] = entry["Other"] + tokens;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return Array.from(weekMap.values()).sort(
|
|
402
|
+
(a, b) => a.sortKey.localeCompare(b.sortKey)
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
function CustomTooltip({ active, payload, label }) {
|
|
406
|
+
if (!active || !payload || payload.length === 0) return null;
|
|
407
|
+
const total = payload.reduce((sum, entry) => sum + entry.value, 0);
|
|
408
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-gray-700 bg-gray-800 p-3 text-xs shadow-lg", children: [
|
|
409
|
+
/* @__PURE__ */ jsx("p", { className: "mb-2 font-medium text-gray-300", children: label }),
|
|
410
|
+
payload.filter((entry) => entry.value > 0).sort((a, b) => b.value - a.value).map((entry) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-4", children: [
|
|
411
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
412
|
+
/* @__PURE__ */ jsx(
|
|
413
|
+
"div",
|
|
414
|
+
{
|
|
415
|
+
className: "h-2 w-2 rounded-full",
|
|
416
|
+
style: { backgroundColor: entry.color }
|
|
417
|
+
}
|
|
418
|
+
),
|
|
419
|
+
/* @__PURE__ */ jsx("span", { className: "text-gray-400", children: entry.name })
|
|
420
|
+
] }),
|
|
421
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-gray-300", children: formatTokenCount(entry.value) })
|
|
422
|
+
] }, entry.name)),
|
|
423
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-1.5 border-t border-gray-700 pt-1.5 flex justify-between", children: [
|
|
424
|
+
/* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "Total" }),
|
|
425
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono font-medium text-white", children: formatTokenCount(total) })
|
|
426
|
+
] })
|
|
427
|
+
] });
|
|
428
|
+
}
|
|
429
|
+
function TokenTrendChart({ data }) {
|
|
430
|
+
const [granularity, setGranularity] = useState("daily");
|
|
431
|
+
const { topModels, hasOther } = useMemo(
|
|
432
|
+
() => getTopModels(data, 5),
|
|
433
|
+
[data]
|
|
434
|
+
);
|
|
435
|
+
const allModelKeys = useMemo(() => {
|
|
436
|
+
const keys = [...topModels];
|
|
437
|
+
if (hasOther) keys.push("Other");
|
|
438
|
+
return keys;
|
|
439
|
+
}, [topModels, hasOther]);
|
|
440
|
+
const chartData = useMemo(() => {
|
|
441
|
+
if (granularity === "weekly") {
|
|
442
|
+
return processWeekly(data, topModels, hasOther);
|
|
443
|
+
}
|
|
444
|
+
return processDaily(data, topModels, hasOther);
|
|
445
|
+
}, [data, topModels, hasOther, granularity]);
|
|
446
|
+
if (data.length === 0) {
|
|
447
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
|
|
448
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Token Usage Over Time" }),
|
|
449
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Total tokens by model per day" }),
|
|
450
|
+
/* @__PURE__ */ jsx("div", { className: "flex h-64 items-center justify-center", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500", children: "No data available" }) })
|
|
451
|
+
] });
|
|
452
|
+
}
|
|
453
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
|
|
454
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
455
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
456
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Token Usage Over Time" }),
|
|
457
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Total tokens by model per day" })
|
|
458
|
+
] }),
|
|
459
|
+
/* @__PURE__ */ jsxs("div", { className: "flex rounded-lg border border-gray-700 text-xs", children: [
|
|
460
|
+
/* @__PURE__ */ jsx(
|
|
461
|
+
"button",
|
|
462
|
+
{
|
|
463
|
+
type: "button",
|
|
464
|
+
onClick: () => setGranularity("daily"),
|
|
465
|
+
className: `rounded-l-lg px-3 py-1 ${granularity === "daily" ? "bg-gray-700 text-white" : "text-gray-400 hover:text-gray-300"}`,
|
|
466
|
+
children: "Daily"
|
|
467
|
+
}
|
|
468
|
+
),
|
|
469
|
+
/* @__PURE__ */ jsx(
|
|
470
|
+
"button",
|
|
471
|
+
{
|
|
472
|
+
type: "button",
|
|
473
|
+
onClick: () => setGranularity("weekly"),
|
|
474
|
+
className: `rounded-r-lg px-3 py-1 ${granularity === "weekly" ? "bg-gray-700 text-white" : "text-gray-400 hover:text-gray-300"}`,
|
|
475
|
+
children: "Weekly"
|
|
476
|
+
}
|
|
477
|
+
)
|
|
478
|
+
] })
|
|
479
|
+
] }),
|
|
480
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4 h-72", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(AreaChart, { data: chartData, children: [
|
|
481
|
+
/* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "#2a2926" }),
|
|
482
|
+
/* @__PURE__ */ jsx(
|
|
483
|
+
XAxis,
|
|
484
|
+
{
|
|
485
|
+
dataKey: "dateLabel",
|
|
486
|
+
tick: { fill: "#7a7668", fontSize: 10 },
|
|
487
|
+
tickLine: false
|
|
488
|
+
}
|
|
489
|
+
),
|
|
490
|
+
/* @__PURE__ */ jsx(
|
|
491
|
+
YAxis,
|
|
492
|
+
{
|
|
493
|
+
tick: { fill: "#7a7668", fontSize: 10 },
|
|
494
|
+
tickLine: false,
|
|
495
|
+
axisLine: false,
|
|
496
|
+
tickFormatter: (value) => formatTokenCount(value)
|
|
497
|
+
}
|
|
498
|
+
),
|
|
499
|
+
/* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(CustomTooltip, {}) }),
|
|
500
|
+
allModelKeys.map((model, i) => /* @__PURE__ */ jsx(
|
|
501
|
+
Area,
|
|
502
|
+
{
|
|
503
|
+
type: "monotone",
|
|
504
|
+
dataKey: model,
|
|
505
|
+
stackId: "1",
|
|
506
|
+
stroke: COLORS$1[i % COLORS$1.length],
|
|
507
|
+
fill: COLORS$1[i % COLORS$1.length],
|
|
508
|
+
fillOpacity: 0.6
|
|
509
|
+
},
|
|
510
|
+
model
|
|
511
|
+
))
|
|
512
|
+
] }) }) })
|
|
513
|
+
] });
|
|
514
|
+
}
|
|
515
|
+
const COLORS = ["#d97757", "#8b5cf6", "#10b981", "#f59e0b", "#ef4444", "#b07cc5"];
|
|
516
|
+
function ModelUsageChart({ data }) {
|
|
517
|
+
const chartData = Object.entries(data).map(([model, usage]) => ({
|
|
518
|
+
name: model.replace(/^claude-/, "").split("-202")[0],
|
|
519
|
+
fullName: model,
|
|
520
|
+
totalTokens: usage.inputTokens + usage.outputTokens,
|
|
521
|
+
outputTokens: usage.outputTokens
|
|
522
|
+
}));
|
|
523
|
+
chartData.sort((a, b) => b.totalTokens - a.totalTokens);
|
|
524
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
|
|
525
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Model Usage" }),
|
|
526
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Token usage by model" }),
|
|
527
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4 h-64", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(PieChart, { children: [
|
|
528
|
+
/* @__PURE__ */ jsx(
|
|
529
|
+
Pie,
|
|
530
|
+
{
|
|
531
|
+
data: chartData,
|
|
532
|
+
cx: "50%",
|
|
533
|
+
cy: "50%",
|
|
534
|
+
innerRadius: 50,
|
|
535
|
+
outerRadius: 80,
|
|
536
|
+
dataKey: "totalTokens",
|
|
537
|
+
nameKey: "name",
|
|
538
|
+
strokeWidth: 0,
|
|
539
|
+
children: chartData.map((_, i) => /* @__PURE__ */ jsx(Cell, { fill: COLORS[i % COLORS.length], opacity: 0.8 }, i))
|
|
540
|
+
}
|
|
541
|
+
),
|
|
542
|
+
/* @__PURE__ */ jsx(
|
|
543
|
+
Tooltip,
|
|
544
|
+
{
|
|
545
|
+
formatter: (value) => formatTokenCount(value),
|
|
546
|
+
contentStyle: {
|
|
547
|
+
backgroundColor: "#1c1c1a",
|
|
548
|
+
border: "1px solid #3d3b36",
|
|
549
|
+
borderRadius: "8px",
|
|
550
|
+
fontSize: "12px"
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
),
|
|
554
|
+
/* @__PURE__ */ jsx(
|
|
555
|
+
Legend,
|
|
556
|
+
{
|
|
557
|
+
wrapperStyle: { fontSize: "11px" }
|
|
558
|
+
}
|
|
559
|
+
)
|
|
560
|
+
] }) }) })
|
|
561
|
+
] });
|
|
562
|
+
}
|
|
563
|
+
function HourlyDistribution({
|
|
564
|
+
hourCounts
|
|
565
|
+
}) {
|
|
566
|
+
const hours = Array.from({ length: 24 }, (_, i) => ({
|
|
567
|
+
hour: i,
|
|
568
|
+
label: `${i.toString().padStart(2, "0")}:00`,
|
|
569
|
+
count: hourCounts[String(i)] ?? 0
|
|
570
|
+
}));
|
|
571
|
+
const maxCount = Math.max(...hours.map((h) => h.count), 1);
|
|
572
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
|
|
573
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Hourly Distribution" }),
|
|
574
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Session starts by hour of day" }),
|
|
575
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4 flex items-end gap-0.5", style: { height: "120px" }, children: hours.map((h) => {
|
|
576
|
+
const height = h.count > 0 ? Math.max(h.count / maxCount * 100, 4) : 0;
|
|
577
|
+
const intensity = h.count / maxCount;
|
|
578
|
+
return /* @__PURE__ */ jsxs(
|
|
579
|
+
"div",
|
|
580
|
+
{
|
|
581
|
+
className: "group relative flex-1",
|
|
582
|
+
style: { height: "100%" },
|
|
583
|
+
children: [
|
|
584
|
+
/* @__PURE__ */ jsx(
|
|
585
|
+
"div",
|
|
586
|
+
{
|
|
587
|
+
className: "absolute bottom-0 w-full rounded-t transition-colors",
|
|
588
|
+
style: {
|
|
589
|
+
height: `${height}%`,
|
|
590
|
+
backgroundColor: `rgba(217, 119, 87, ${0.2 + intensity * 0.6})`
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
),
|
|
594
|
+
/* @__PURE__ */ jsxs("div", { className: "absolute -top-8 left-1/2 hidden -translate-x-1/2 whitespace-nowrap rounded bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-300 group-hover:block", children: [
|
|
595
|
+
h.label,
|
|
596
|
+
": ",
|
|
597
|
+
h.count
|
|
598
|
+
] })
|
|
599
|
+
]
|
|
600
|
+
},
|
|
601
|
+
h.hour
|
|
602
|
+
);
|
|
603
|
+
}) }),
|
|
604
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-1 flex justify-between text-[10px] text-gray-600", children: [
|
|
605
|
+
/* @__PURE__ */ jsx("span", { children: "00:00" }),
|
|
606
|
+
/* @__PURE__ */ jsx("span", { children: "06:00" }),
|
|
607
|
+
/* @__PURE__ */ jsx("span", { children: "12:00" }),
|
|
608
|
+
/* @__PURE__ */ jsx("span", { children: "18:00" }),
|
|
609
|
+
/* @__PURE__ */ jsx("span", { children: "23:00" })
|
|
610
|
+
] })
|
|
611
|
+
] });
|
|
612
|
+
}
|
|
613
|
+
const getProjectAnalytics = createServerFn({
|
|
614
|
+
method: "GET"
|
|
615
|
+
}).handler(createSsrRpc("64052f224a1d6696436e5d3deeee2b798f0742e1292ffabd038c3a7bf75e6fcb"));
|
|
616
|
+
const projectAnalyticsQuery = queryOptions({
|
|
617
|
+
queryKey: ["projects", "analytics"],
|
|
618
|
+
queryFn: () => getProjectAnalytics(),
|
|
619
|
+
refetchInterval: 6e4
|
|
620
|
+
});
|
|
621
|
+
const COLUMNS = [
|
|
622
|
+
{ key: "projectName", label: "Project" },
|
|
623
|
+
{ key: "totalSessions", label: "Sessions", align: "right" },
|
|
624
|
+
{ key: "totalMessages", label: "Messages", align: "right" },
|
|
625
|
+
{ key: "totalDurationMs", label: "Duration", align: "right" },
|
|
626
|
+
{ key: "lastSessionAt", label: "Last Active", align: "right" }
|
|
627
|
+
];
|
|
628
|
+
function ProjectTable({ projects }) {
|
|
629
|
+
const { anonymizeProjectName } = usePrivacy();
|
|
630
|
+
const [sortField, setSortField] = useState("lastSessionAt");
|
|
631
|
+
const [sortDir, setSortDir] = useState("desc");
|
|
632
|
+
const sorted = useMemo(() => {
|
|
633
|
+
const copy = [...projects];
|
|
634
|
+
copy.sort((a, b) => {
|
|
635
|
+
let cmp = 0;
|
|
636
|
+
switch (sortField) {
|
|
637
|
+
case "projectName":
|
|
638
|
+
cmp = a.projectName.localeCompare(b.projectName);
|
|
639
|
+
break;
|
|
640
|
+
case "totalSessions":
|
|
641
|
+
cmp = a.totalSessions - b.totalSessions;
|
|
642
|
+
break;
|
|
643
|
+
case "totalMessages":
|
|
644
|
+
cmp = a.totalMessages - b.totalMessages;
|
|
645
|
+
break;
|
|
646
|
+
case "totalDurationMs":
|
|
647
|
+
cmp = a.totalDurationMs - b.totalDurationMs;
|
|
648
|
+
break;
|
|
649
|
+
case "lastSessionAt":
|
|
650
|
+
cmp = a.lastSessionAt.localeCompare(b.lastSessionAt);
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
return sortDir === "asc" ? cmp : -cmp;
|
|
654
|
+
});
|
|
655
|
+
return copy;
|
|
656
|
+
}, [projects, sortField, sortDir]);
|
|
657
|
+
function handleSort(field) {
|
|
658
|
+
if (field === sortField) {
|
|
659
|
+
setSortDir((prev) => prev === "asc" ? "desc" : "asc");
|
|
660
|
+
} else {
|
|
661
|
+
setSortField(field);
|
|
662
|
+
setSortDir("desc");
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
function renderSortIndicator(field) {
|
|
666
|
+
if (field !== sortField) return null;
|
|
667
|
+
return /* @__PURE__ */ jsx("span", { className: "ml-1", children: sortDir === "asc" ? "▲" : "▼" });
|
|
668
|
+
}
|
|
669
|
+
return /* @__PURE__ */ jsx("div", { className: "overflow-x-auto rounded-xl border border-gray-800 bg-gray-900/50", children: /* @__PURE__ */ jsxs("table", { className: "w-full", children: [
|
|
670
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { className: "border-b border-gray-800", children: COLUMNS.map((col) => /* @__PURE__ */ jsxs(
|
|
671
|
+
"th",
|
|
672
|
+
{
|
|
673
|
+
onClick: () => handleSort(col.key),
|
|
674
|
+
className: `cursor-pointer px-4 py-3 text-xs font-medium text-gray-400 hover:text-gray-200 ${col.align === "right" ? "text-right" : "text-left"}`,
|
|
675
|
+
children: [
|
|
676
|
+
col.label,
|
|
677
|
+
renderSortIndicator(col.key)
|
|
678
|
+
]
|
|
679
|
+
},
|
|
680
|
+
col.key
|
|
681
|
+
)) }) }),
|
|
682
|
+
/* @__PURE__ */ jsx("tbody", { children: sorted.map((project) => /* @__PURE__ */ jsxs(
|
|
683
|
+
"tr",
|
|
684
|
+
{
|
|
685
|
+
className: "border-b border-gray-800/50 transition-colors hover:bg-gray-800/30",
|
|
686
|
+
children: [
|
|
687
|
+
/* @__PURE__ */ jsxs("td", { className: "px-4 py-3", children: [
|
|
688
|
+
/* @__PURE__ */ jsx(
|
|
689
|
+
Link,
|
|
690
|
+
{
|
|
691
|
+
to: "/sessions",
|
|
692
|
+
search: { project: project.projectName },
|
|
693
|
+
className: "text-sm text-brand-300 hover:underline",
|
|
694
|
+
children: anonymizeProjectName(project.projectName)
|
|
695
|
+
}
|
|
696
|
+
),
|
|
697
|
+
project.activeSessions > 0 && /* @__PURE__ */ jsxs("span", { className: "ml-2 rounded-full bg-emerald-500/20 px-1.5 py-0.5 text-[10px] font-medium text-emerald-400", children: [
|
|
698
|
+
project.activeSessions,
|
|
699
|
+
" active"
|
|
700
|
+
] })
|
|
701
|
+
] }),
|
|
702
|
+
/* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-right font-mono text-sm text-gray-300", children: project.totalSessions }),
|
|
703
|
+
/* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-right font-mono text-sm text-gray-300", children: project.totalMessages.toLocaleString() }),
|
|
704
|
+
/* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-right font-mono text-sm text-gray-300", children: formatDuration(project.totalDurationMs) }),
|
|
705
|
+
/* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-right text-sm text-gray-400", children: formatRelativeTime(project.lastSessionAt) })
|
|
706
|
+
]
|
|
707
|
+
},
|
|
708
|
+
project.projectPath
|
|
709
|
+
)) })
|
|
710
|
+
] }) });
|
|
711
|
+
}
|
|
712
|
+
function ProjectAnalytics() {
|
|
713
|
+
const { anonymizeProjectName } = usePrivacy();
|
|
714
|
+
const { data, isLoading } = useQuery(projectAnalyticsQuery);
|
|
715
|
+
if (isLoading) {
|
|
716
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
717
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-3 md:grid-cols-3", children: Array.from({ length: 3 }).map((_, i) => /* @__PURE__ */ jsx(
|
|
718
|
+
"div",
|
|
719
|
+
{
|
|
720
|
+
className: "h-20 animate-pulse rounded-xl bg-gray-900/50"
|
|
721
|
+
},
|
|
722
|
+
i
|
|
723
|
+
)) }),
|
|
724
|
+
/* @__PURE__ */ jsx("div", { className: "h-64 animate-pulse rounded-xl bg-gray-900/50" })
|
|
725
|
+
] });
|
|
726
|
+
}
|
|
727
|
+
const projects = data?.projects ?? [];
|
|
728
|
+
if (projects.length === 0) {
|
|
729
|
+
return /* @__PURE__ */ jsx("div", { className: "py-12 text-center text-sm text-gray-500", children: "No projects found. Sessions will appear here once scanned." });
|
|
730
|
+
}
|
|
731
|
+
const totalSessions = projects.reduce((sum, p) => sum + p.totalSessions, 0);
|
|
732
|
+
const totalDurationMs = projects.reduce((sum, p) => sum + p.totalDurationMs, 0);
|
|
733
|
+
const mostActive = projects.reduce(
|
|
734
|
+
(max, p) => p.totalSessions > max.totalSessions ? p : max
|
|
735
|
+
);
|
|
736
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
737
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3 md:grid-cols-4", children: [
|
|
738
|
+
/* @__PURE__ */ jsx(SummaryCard, { label: "Total Projects", value: String(projects.length) }),
|
|
739
|
+
/* @__PURE__ */ jsx(
|
|
740
|
+
SummaryCard,
|
|
741
|
+
{
|
|
742
|
+
label: "Total Sessions",
|
|
743
|
+
value: totalSessions.toLocaleString()
|
|
744
|
+
}
|
|
745
|
+
),
|
|
746
|
+
/* @__PURE__ */ jsx(
|
|
747
|
+
SummaryCard,
|
|
748
|
+
{
|
|
749
|
+
label: "Total Duration",
|
|
750
|
+
value: formatDuration(totalDurationMs)
|
|
751
|
+
}
|
|
752
|
+
),
|
|
753
|
+
/* @__PURE__ */ jsx(
|
|
754
|
+
SummaryCard,
|
|
755
|
+
{
|
|
756
|
+
label: "Most Active",
|
|
757
|
+
value: anonymizeProjectName(mostActive.projectName),
|
|
758
|
+
sub: `${mostActive.totalSessions} sessions`
|
|
759
|
+
}
|
|
760
|
+
)
|
|
761
|
+
] }),
|
|
762
|
+
/* @__PURE__ */ jsx(ProjectTable, { projects })
|
|
763
|
+
] });
|
|
764
|
+
}
|
|
765
|
+
function SummaryCard({
|
|
766
|
+
label,
|
|
767
|
+
value,
|
|
768
|
+
sub
|
|
769
|
+
}) {
|
|
770
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
|
|
771
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: label }),
|
|
772
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 truncate text-xl font-bold text-white", children: value }),
|
|
773
|
+
sub && /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: sub })
|
|
774
|
+
] });
|
|
775
|
+
}
|
|
776
|
+
const EMPTY_TOKENS_BY_MODEL = {};
|
|
777
|
+
function StatsPage() {
|
|
778
|
+
const {
|
|
779
|
+
tab
|
|
780
|
+
} = Route.useSearch();
|
|
781
|
+
const navigate = Route.useNavigate();
|
|
782
|
+
const {
|
|
783
|
+
data: stats,
|
|
784
|
+
isLoading
|
|
785
|
+
} = useQuery(statsQuery);
|
|
786
|
+
const tokensByModel = useMemo(() => {
|
|
787
|
+
if (!stats) return EMPTY_TOKENS_BY_MODEL;
|
|
788
|
+
const result = {};
|
|
789
|
+
for (const [model, usage] of Object.entries(stats.modelUsage)) {
|
|
790
|
+
result[model] = {
|
|
791
|
+
inputTokens: usage.inputTokens,
|
|
792
|
+
outputTokens: usage.outputTokens,
|
|
793
|
+
cacheReadInputTokens: usage.cacheReadInputTokens,
|
|
794
|
+
cacheCreationInputTokens: usage.cacheCreationInputTokens
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
return result;
|
|
798
|
+
}, [stats]);
|
|
799
|
+
const {
|
|
800
|
+
cost
|
|
801
|
+
} = useSessionCost(tokensByModel);
|
|
802
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
803
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
804
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
805
|
+
/* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold text-white", children: "Stats" }),
|
|
806
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-gray-400", children: "Usage analytics and project insights" })
|
|
807
|
+
] }),
|
|
808
|
+
tab === "overview" && stats && /* @__PURE__ */ jsx(ExportDropdown, { options: [{
|
|
809
|
+
label: "Daily Activity (CSV)",
|
|
810
|
+
onClick: () => downloadFile(dailyActivityToCSV(stats), "daily-activity.csv", "text/csv")
|
|
811
|
+
}, {
|
|
812
|
+
label: "Token Usage (CSV)",
|
|
813
|
+
onClick: () => downloadFile(dailyTokensToCSV(stats), "daily-tokens.csv", "text/csv")
|
|
814
|
+
}, {
|
|
815
|
+
label: "Model Usage (CSV)",
|
|
816
|
+
onClick: () => downloadFile(modelUsageToCSV(stats), "model-usage.csv", "text/csv")
|
|
817
|
+
}, {
|
|
818
|
+
label: "Full Stats (JSON)",
|
|
819
|
+
onClick: () => downloadFile(statsToJSON(stats), "stats.json", "application/json")
|
|
820
|
+
}] })
|
|
821
|
+
] }),
|
|
822
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-4 flex gap-1 border-b border-gray-800", children: [
|
|
823
|
+
/* @__PURE__ */ jsx("button", { onClick: () => navigate({
|
|
824
|
+
search: {
|
|
825
|
+
tab: "overview"
|
|
826
|
+
}
|
|
827
|
+
}), className: `px-4 py-2 text-sm border-b-2 transition-colors ${tab === "overview" ? "border-brand-500 text-white" : "border-transparent text-gray-400 hover:text-gray-200"}`, children: "Overview" }),
|
|
828
|
+
/* @__PURE__ */ jsx("button", { onClick: () => navigate({
|
|
829
|
+
search: {
|
|
830
|
+
tab: "projects"
|
|
831
|
+
}
|
|
832
|
+
}), className: `px-4 py-2 text-sm border-b-2 transition-colors ${tab === "projects" ? "border-brand-500 text-white" : "border-transparent text-gray-400 hover:text-gray-200"}`, children: "Projects" })
|
|
833
|
+
] }),
|
|
834
|
+
tab === "overview" ? /* @__PURE__ */ jsx(StatsOverview, { stats, isLoading, cost }) : /* @__PURE__ */ jsx("div", { className: "mt-6", children: /* @__PURE__ */ jsx(ProjectAnalytics, {}) })
|
|
835
|
+
] });
|
|
836
|
+
}
|
|
837
|
+
function StatsOverview({
|
|
838
|
+
stats,
|
|
839
|
+
isLoading,
|
|
840
|
+
cost
|
|
841
|
+
}) {
|
|
842
|
+
if (isLoading) {
|
|
843
|
+
return /* @__PURE__ */ jsxs("div", { className: "mt-6 space-y-4", children: [
|
|
844
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-3 md:grid-cols-5", children: Array.from({
|
|
845
|
+
length: 5
|
|
846
|
+
}).map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-20 animate-pulse rounded-xl bg-gray-900/50" }, i)) }),
|
|
847
|
+
Array.from({
|
|
848
|
+
length: 3
|
|
849
|
+
}).map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-64 animate-pulse rounded-xl bg-gray-900/50" }, i))
|
|
850
|
+
] });
|
|
851
|
+
}
|
|
852
|
+
if (!stats) {
|
|
853
|
+
return /* @__PURE__ */ jsx("div", { className: "py-12 text-center text-sm text-gray-500", children: "No stats data found. Check ~/.claude/stats-cache.json" });
|
|
854
|
+
}
|
|
855
|
+
const totalTokens = Object.values(stats.modelUsage).reduce((sum, m) => sum + m.inputTokens + m.outputTokens, 0);
|
|
856
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
857
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-6 grid grid-cols-2 gap-3 md:grid-cols-5", children: [
|
|
858
|
+
/* @__PURE__ */ jsx(StatCard, { label: "Total Sessions", value: String(stats.totalSessions) }),
|
|
859
|
+
/* @__PURE__ */ jsx(StatCard, { label: "Total Messages", value: stats.totalMessages.toLocaleString() }),
|
|
860
|
+
/* @__PURE__ */ jsx(StatCard, { label: "Total Tokens", value: formatTokenCount(totalTokens), sub: cost ? `~${formatUSD(cost.totalUSD)}` : void 0 }),
|
|
861
|
+
/* @__PURE__ */ jsx(StatCard, { label: "Total Estimated Cost", value: cost ? `~${formatUSD(cost.totalUSD)}` : "N/A" }),
|
|
862
|
+
/* @__PURE__ */ jsx(StatCard, { label: "Longest Session", value: formatDuration(stats.longestSession.duration), sub: `${stats.longestSession.messageCount} messages` })
|
|
863
|
+
] }),
|
|
864
|
+
/* @__PURE__ */ jsx("div", { className: "mt-6", children: /* @__PURE__ */ jsx(ContributionHeatmap, { dailyActivity: stats.dailyActivity, dailyModelTokens: stats.dailyModelTokens }) }),
|
|
865
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsx(ActivityChart, { data: stats.dailyActivity }) }),
|
|
866
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsx(TokenTrendChart, { data: stats.dailyModelTokens }) }),
|
|
867
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-4 grid grid-cols-1 gap-4 md:grid-cols-2", children: [
|
|
868
|
+
/* @__PURE__ */ jsx(ModelUsageChart, { data: stats.modelUsage }),
|
|
869
|
+
/* @__PURE__ */ jsx(HourlyDistribution, { hourCounts: stats.hourCounts })
|
|
870
|
+
] })
|
|
871
|
+
] });
|
|
872
|
+
}
|
|
873
|
+
function StatCard({
|
|
874
|
+
label,
|
|
875
|
+
value,
|
|
876
|
+
sub
|
|
877
|
+
}) {
|
|
878
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
|
|
879
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: label }),
|
|
880
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xl font-bold text-white", children: value }),
|
|
881
|
+
sub && /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: sub })
|
|
882
|
+
] });
|
|
883
|
+
}
|
|
884
|
+
export {
|
|
885
|
+
StatsPage as component
|
|
886
|
+
};
|