@cryptiklemur/lattice 1.4.0 → 1.6.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/bun.lock +71 -0
- package/client/package.json +1 -0
- package/client/src/components/analytics/AnalyticsView.tsx +119 -0
- package/client/src/components/analytics/ChartCard.tsx +22 -0
- package/client/src/components/analytics/PeriodSelector.tsx +42 -0
- package/client/src/components/analytics/QuickStats.tsx +99 -0
- package/client/src/components/analytics/charts/CacheEfficiencyChart.tsx +60 -0
- package/client/src/components/analytics/charts/ContextUtilizationChart.tsx +110 -0
- package/client/src/components/analytics/charts/CostAreaChart.tsx +83 -0
- package/client/src/components/analytics/charts/CostDistributionChart.tsx +62 -0
- package/client/src/components/analytics/charts/CostDonutChart.tsx +93 -0
- package/client/src/components/analytics/charts/CumulativeCostChart.tsx +62 -0
- package/client/src/components/analytics/charts/ResponseTimeScatter.tsx +101 -0
- package/client/src/components/analytics/charts/SessionBubbleChart.tsx +122 -0
- package/client/src/components/analytics/charts/TokenFlowChart.tsx +82 -0
- package/client/src/components/analytics/charts/TokenSankeyChart.tsx +89 -0
- package/client/src/components/dashboard/DashboardView.tsx +5 -0
- package/client/src/components/sidebar/Sidebar.tsx +10 -2
- package/client/src/hooks/useAnalytics.ts +75 -0
- package/client/src/router.tsx +4 -0
- package/client/src/stores/analytics.ts +54 -0
- package/client/src/stores/sidebar.ts +8 -0
- package/client/vite.config.ts +1 -0
- package/package.json +1 -1
- package/server/src/analytics/engine.ts +606 -0
- package/server/src/daemon.ts +1 -0
- package/server/src/handlers/analytics.ts +34 -0
- package/server/src/project/session.ts +4 -4
- package/shared/src/analytics.ts +28 -0
- package/shared/src/index.ts +1 -0
- package/shared/src/messages.ts +30 -2
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LineChart,
|
|
3
|
+
Line,
|
|
4
|
+
XAxis,
|
|
5
|
+
YAxis,
|
|
6
|
+
CartesianGrid,
|
|
7
|
+
Tooltip,
|
|
8
|
+
ResponsiveContainer,
|
|
9
|
+
} from "recharts";
|
|
10
|
+
|
|
11
|
+
var TICK_STYLE = {
|
|
12
|
+
fontSize: 10,
|
|
13
|
+
fontFamily: "var(--font-mono)",
|
|
14
|
+
fill: "oklch(0.9 0.02 280 / 0.3)",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
var GRID_COLOR = "oklch(0.9 0.02 280 / 0.06)";
|
|
18
|
+
|
|
19
|
+
var LINE_COLORS = [
|
|
20
|
+
"oklch(55% 0.25 280)",
|
|
21
|
+
"#a855f7",
|
|
22
|
+
"#22c55e",
|
|
23
|
+
"#f59e0b",
|
|
24
|
+
"oklch(65% 0.2 240)",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
interface ContextUtilDatum {
|
|
28
|
+
messageIndex: number;
|
|
29
|
+
contextPercent: number;
|
|
30
|
+
sessionId: string;
|
|
31
|
+
title: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ContextUtilizationChartProps {
|
|
35
|
+
data: ContextUtilDatum[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<{ name: string; value: number; color: string }> }) {
|
|
39
|
+
if (!active || !payload || payload.length === 0) return null;
|
|
40
|
+
return (
|
|
41
|
+
<div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
|
|
42
|
+
{payload.map(function (entry) {
|
|
43
|
+
return (
|
|
44
|
+
<div key={entry.name} className="flex items-center gap-2 text-[11px] font-mono">
|
|
45
|
+
<span className="inline-block w-2 h-2 rounded-full" style={{ background: entry.color }} />
|
|
46
|
+
<span className="text-base-content/60 truncate max-w-[120px]">{entry.name}</span>
|
|
47
|
+
<span className="text-base-content ml-auto pl-4">{entry.value.toFixed(1)}%</span>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
})}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function ContextUtilizationChart({ data }: ContextUtilizationChartProps) {
|
|
56
|
+
var sessionMap = new Map<string, { title: string; points: Array<{ messageIndex: number; contextPercent: number }> }>();
|
|
57
|
+
for (var i = 0; i < data.length; i++) {
|
|
58
|
+
var d = data[i];
|
|
59
|
+
var entry = sessionMap.get(d.sessionId);
|
|
60
|
+
if (!entry) {
|
|
61
|
+
entry = { title: d.title, points: [] };
|
|
62
|
+
sessionMap.set(d.sessionId, entry);
|
|
63
|
+
}
|
|
64
|
+
entry.points.push({ messageIndex: d.messageIndex, contextPercent: d.contextPercent });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
var sessions = Array.from(sessionMap.entries()).slice(0, 5);
|
|
68
|
+
|
|
69
|
+
var maxIndex = 0;
|
|
70
|
+
sessions.forEach(function (s) {
|
|
71
|
+
s[1].points.forEach(function (p) {
|
|
72
|
+
if (p.messageIndex > maxIndex) maxIndex = p.messageIndex;
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
var merged: Array<Record<string, number>> = [];
|
|
77
|
+
for (var mi = 0; mi <= maxIndex; mi++) {
|
|
78
|
+
var row: Record<string, number> = { messageIndex: mi };
|
|
79
|
+
sessions.forEach(function (s) {
|
|
80
|
+
var point = s[1].points.find(function (p) { return p.messageIndex === mi; });
|
|
81
|
+
if (point) row[s[0]] = point.contextPercent;
|
|
82
|
+
});
|
|
83
|
+
merged.push(row);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<ResponsiveContainer width="100%" height={200}>
|
|
88
|
+
<LineChart data={merged} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
89
|
+
<CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} vertical={false} />
|
|
90
|
+
<XAxis dataKey="messageIndex" tick={TICK_STYLE} axisLine={false} tickLine={false} />
|
|
91
|
+
<YAxis domain={[0, 100]} tick={TICK_STYLE} axisLine={false} tickLine={false} tickFormatter={function (v) { return v + "%"; }} />
|
|
92
|
+
<Tooltip content={<CustomTooltip />} />
|
|
93
|
+
{sessions.map(function (s, idx) {
|
|
94
|
+
return (
|
|
95
|
+
<Line
|
|
96
|
+
key={s[0]}
|
|
97
|
+
type="monotone"
|
|
98
|
+
dataKey={s[0]}
|
|
99
|
+
name={s[1].title.slice(0, 30)}
|
|
100
|
+
stroke={LINE_COLORS[idx % LINE_COLORS.length]}
|
|
101
|
+
strokeWidth={1.5}
|
|
102
|
+
dot={false}
|
|
103
|
+
connectNulls
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
})}
|
|
107
|
+
</LineChart>
|
|
108
|
+
</ResponsiveContainer>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AreaChart,
|
|
3
|
+
Area,
|
|
4
|
+
XAxis,
|
|
5
|
+
YAxis,
|
|
6
|
+
CartesianGrid,
|
|
7
|
+
Tooltip,
|
|
8
|
+
ResponsiveContainer,
|
|
9
|
+
} from "recharts";
|
|
10
|
+
|
|
11
|
+
var TICK_STYLE = {
|
|
12
|
+
fontSize: 10,
|
|
13
|
+
fontFamily: "var(--font-mono)",
|
|
14
|
+
fill: "oklch(0.9 0.02 280 / 0.3)",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
var GRID_COLOR = "oklch(0.9 0.02 280 / 0.06)";
|
|
18
|
+
|
|
19
|
+
interface CostAreaDatum {
|
|
20
|
+
date: string;
|
|
21
|
+
total: number;
|
|
22
|
+
opus: number;
|
|
23
|
+
sonnet: number;
|
|
24
|
+
haiku: number;
|
|
25
|
+
other: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface CostAreaChartProps {
|
|
29
|
+
data: CostAreaDatum[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: Array<{ name: string; value: number; color: string }>; label?: string }) {
|
|
33
|
+
if (!active || !payload || payload.length === 0) return null;
|
|
34
|
+
return (
|
|
35
|
+
<div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
|
|
36
|
+
<p className="text-[10px] font-mono text-base-content/50 mb-1">{label}</p>
|
|
37
|
+
{payload.map(function (entry) {
|
|
38
|
+
return (
|
|
39
|
+
<div key={entry.name} className="flex items-center gap-2 text-[11px] font-mono">
|
|
40
|
+
<span className="inline-block w-2 h-2 rounded-full" style={{ background: entry.color }} />
|
|
41
|
+
<span className="text-base-content/60 capitalize">{entry.name}</span>
|
|
42
|
+
<span className="text-base-content ml-auto pl-4">${entry.value.toFixed(4)}</span>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
})}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function CostAreaChart({ data }: CostAreaChartProps) {
|
|
51
|
+
return (
|
|
52
|
+
<ResponsiveContainer width="100%" height={200}>
|
|
53
|
+
<AreaChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
54
|
+
<defs>
|
|
55
|
+
<linearGradient id="opusGrad" x1="0" y1="0" x2="0" y2="1">
|
|
56
|
+
<stop offset="5%" stopColor="#a855f7" stopOpacity={0.4} />
|
|
57
|
+
<stop offset="95%" stopColor="#a855f7" stopOpacity={0.05} />
|
|
58
|
+
</linearGradient>
|
|
59
|
+
<linearGradient id="sonnetGrad" x1="0" y1="0" x2="0" y2="1">
|
|
60
|
+
<stop offset="5%" stopColor="oklch(55% 0.25 280)" stopOpacity={0.4} />
|
|
61
|
+
<stop offset="95%" stopColor="oklch(55% 0.25 280)" stopOpacity={0.05} />
|
|
62
|
+
</linearGradient>
|
|
63
|
+
<linearGradient id="haikuGrad" x1="0" y1="0" x2="0" y2="1">
|
|
64
|
+
<stop offset="5%" stopColor="#22c55e" stopOpacity={0.4} />
|
|
65
|
+
<stop offset="95%" stopColor="#22c55e" stopOpacity={0.05} />
|
|
66
|
+
</linearGradient>
|
|
67
|
+
<linearGradient id="otherGrad" x1="0" y1="0" x2="0" y2="1">
|
|
68
|
+
<stop offset="5%" stopColor="#f59e0b" stopOpacity={0.4} />
|
|
69
|
+
<stop offset="95%" stopColor="#f59e0b" stopOpacity={0.05} />
|
|
70
|
+
</linearGradient>
|
|
71
|
+
</defs>
|
|
72
|
+
<CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} vertical={false} />
|
|
73
|
+
<XAxis dataKey="date" tick={TICK_STYLE} axisLine={false} tickLine={false} />
|
|
74
|
+
<YAxis tick={TICK_STYLE} axisLine={false} tickLine={false} tickFormatter={function (v) { return "$" + v.toFixed(2); }} />
|
|
75
|
+
<Tooltip content={<CustomTooltip />} />
|
|
76
|
+
<Area type="monotone" dataKey="opus" stackId="1" stroke="#a855f7" fill="url(#opusGrad)" strokeWidth={1.5} />
|
|
77
|
+
<Area type="monotone" dataKey="sonnet" stackId="1" stroke="oklch(55% 0.25 280)" fill="url(#sonnetGrad)" strokeWidth={1.5} />
|
|
78
|
+
<Area type="monotone" dataKey="haiku" stackId="1" stroke="#22c55e" fill="url(#haikuGrad)" strokeWidth={1.5} />
|
|
79
|
+
<Area type="monotone" dataKey="other" stackId="1" stroke="#f59e0b" fill="url(#otherGrad)" strokeWidth={1.5} />
|
|
80
|
+
</AreaChart>
|
|
81
|
+
</ResponsiveContainer>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AreaChart,
|
|
3
|
+
Area,
|
|
4
|
+
XAxis,
|
|
5
|
+
YAxis,
|
|
6
|
+
CartesianGrid,
|
|
7
|
+
Tooltip,
|
|
8
|
+
ResponsiveContainer,
|
|
9
|
+
} from "recharts";
|
|
10
|
+
|
|
11
|
+
var TICK_STYLE = {
|
|
12
|
+
fontSize: 10,
|
|
13
|
+
fontFamily: "var(--font-mono)",
|
|
14
|
+
fill: "oklch(0.9 0.02 280 / 0.3)",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
var GRID_COLOR = "oklch(0.9 0.02 280 / 0.06)";
|
|
18
|
+
|
|
19
|
+
interface DistributionDatum {
|
|
20
|
+
bucket: string;
|
|
21
|
+
count: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CostDistributionChartProps {
|
|
25
|
+
data: DistributionDatum[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: Array<{ value: number }>; label?: string }) {
|
|
29
|
+
if (!active || !payload || payload.length === 0) return null;
|
|
30
|
+
return (
|
|
31
|
+
<div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
|
|
32
|
+
<p className="text-[10px] font-mono text-base-content/50 mb-1">{label}</p>
|
|
33
|
+
<p className="text-[11px] font-mono text-base-content">{payload[0].value} sessions</p>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function CostDistributionChart({ data }: CostDistributionChartProps) {
|
|
39
|
+
return (
|
|
40
|
+
<ResponsiveContainer width="100%" height={200}>
|
|
41
|
+
<AreaChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
42
|
+
<defs>
|
|
43
|
+
<linearGradient id="distGrad" x1="0" y1="0" x2="0" y2="1">
|
|
44
|
+
<stop offset="5%" stopColor="oklch(55% 0.25 280)" stopOpacity={0.35} />
|
|
45
|
+
<stop offset="95%" stopColor="oklch(55% 0.25 280)" stopOpacity={0.02} />
|
|
46
|
+
</linearGradient>
|
|
47
|
+
</defs>
|
|
48
|
+
<CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} vertical={false} />
|
|
49
|
+
<XAxis dataKey="bucket" tick={TICK_STYLE} axisLine={false} tickLine={false} />
|
|
50
|
+
<YAxis tick={TICK_STYLE} axisLine={false} tickLine={false} allowDecimals={false} />
|
|
51
|
+
<Tooltip content={<CustomTooltip />} />
|
|
52
|
+
<Area
|
|
53
|
+
type="monotone"
|
|
54
|
+
dataKey="count"
|
|
55
|
+
stroke="oklch(55% 0.25 280)"
|
|
56
|
+
fill="url(#distGrad)"
|
|
57
|
+
strokeWidth={2}
|
|
58
|
+
/>
|
|
59
|
+
</AreaChart>
|
|
60
|
+
</ResponsiveContainer>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from "recharts";
|
|
2
|
+
|
|
3
|
+
var MODEL_COLORS: Record<string, string> = {
|
|
4
|
+
opus: "#a855f7",
|
|
5
|
+
sonnet: "oklch(55% 0.25 280)",
|
|
6
|
+
haiku: "#22c55e",
|
|
7
|
+
other: "#f59e0b",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function getModelColor(model: string): string {
|
|
11
|
+
var key = model.toLowerCase();
|
|
12
|
+
if (key.includes("opus")) return MODEL_COLORS.opus;
|
|
13
|
+
if (key.includes("sonnet")) return MODEL_COLORS.sonnet;
|
|
14
|
+
if (key.includes("haiku")) return MODEL_COLORS.haiku;
|
|
15
|
+
return MODEL_COLORS.other;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ModelUsage {
|
|
19
|
+
model: string;
|
|
20
|
+
cost: number;
|
|
21
|
+
percentage: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CostDonutChartProps {
|
|
25
|
+
modelUsage: ModelUsage[];
|
|
26
|
+
totalCost: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<{ name: string; value: number; payload: ModelUsage }> }) {
|
|
30
|
+
if (!active || !payload || payload.length === 0) return null;
|
|
31
|
+
var entry = payload[0];
|
|
32
|
+
return (
|
|
33
|
+
<div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
|
|
34
|
+
<p className="text-[10px] font-mono text-base-content/50 mb-1">{entry.name}</p>
|
|
35
|
+
<p className="text-[11px] font-mono text-base-content">${entry.value.toFixed(4)}</p>
|
|
36
|
+
<p className="text-[10px] font-mono text-base-content/50">{entry.payload.percentage.toFixed(1)}%</p>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function CenterLabel({ totalCost }: { totalCost: number }) {
|
|
42
|
+
return (
|
|
43
|
+
<text x="50%" y="50%" textAnchor="middle" dominantBaseline="middle">
|
|
44
|
+
<tspan x="50%" dy="-0.4em" style={{ fontSize: 11, fontFamily: "var(--font-mono)", fill: "oklch(0.9 0.02 280 / 0.4)" }}>
|
|
45
|
+
TOTAL
|
|
46
|
+
</tspan>
|
|
47
|
+
<tspan x="50%" dy="1.4em" style={{ fontSize: 14, fontFamily: "var(--font-mono)", fill: "oklch(0.9 0.02 280 / 0.9)", fontWeight: 700 }}>
|
|
48
|
+
${totalCost.toFixed(2)}
|
|
49
|
+
</tspan>
|
|
50
|
+
</text>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function CostDonutChart({ modelUsage, totalCost }: CostDonutChartProps) {
|
|
55
|
+
return (
|
|
56
|
+
<div>
|
|
57
|
+
<ResponsiveContainer width="100%" height={200}>
|
|
58
|
+
<PieChart>
|
|
59
|
+
<Pie
|
|
60
|
+
data={modelUsage}
|
|
61
|
+
dataKey="cost"
|
|
62
|
+
nameKey="model"
|
|
63
|
+
innerRadius={50}
|
|
64
|
+
outerRadius={80}
|
|
65
|
+
paddingAngle={2}
|
|
66
|
+
startAngle={90}
|
|
67
|
+
endAngle={-270}
|
|
68
|
+
>
|
|
69
|
+
{modelUsage.map(function (entry, index) {
|
|
70
|
+
return <Cell key={entry.model + index} fill={getModelColor(entry.model)} />;
|
|
71
|
+
})}
|
|
72
|
+
</Pie>
|
|
73
|
+
<Tooltip content={<CustomTooltip />} />
|
|
74
|
+
<CenterLabel totalCost={totalCost} />
|
|
75
|
+
</PieChart>
|
|
76
|
+
</ResponsiveContainer>
|
|
77
|
+
<div className="flex flex-wrap justify-center gap-3 mt-2">
|
|
78
|
+
{modelUsage.map(function (entry) {
|
|
79
|
+
return (
|
|
80
|
+
<div key={entry.model} className="flex items-center gap-1.5 text-[10px] font-mono text-base-content/50">
|
|
81
|
+
<span
|
|
82
|
+
className="inline-block w-2 h-2 rounded-full flex-shrink-0"
|
|
83
|
+
style={{ background: getModelColor(entry.model) }}
|
|
84
|
+
/>
|
|
85
|
+
<span className="capitalize">{entry.model}</span>
|
|
86
|
+
<span className="text-base-content/30">{entry.percentage.toFixed(1)}%</span>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AreaChart,
|
|
3
|
+
Area,
|
|
4
|
+
XAxis,
|
|
5
|
+
YAxis,
|
|
6
|
+
CartesianGrid,
|
|
7
|
+
Tooltip,
|
|
8
|
+
ResponsiveContainer,
|
|
9
|
+
} from "recharts";
|
|
10
|
+
|
|
11
|
+
var TICK_STYLE = {
|
|
12
|
+
fontSize: 10,
|
|
13
|
+
fontFamily: "var(--font-mono)",
|
|
14
|
+
fill: "oklch(0.9 0.02 280 / 0.3)",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
var GRID_COLOR = "oklch(0.9 0.02 280 / 0.06)";
|
|
18
|
+
|
|
19
|
+
interface CumulativeDatum {
|
|
20
|
+
date: string;
|
|
21
|
+
total: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CumulativeCostChartProps {
|
|
25
|
+
data: CumulativeDatum[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: Array<{ value: number }>; label?: string }) {
|
|
29
|
+
if (!active || !payload || payload.length === 0) return null;
|
|
30
|
+
return (
|
|
31
|
+
<div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
|
|
32
|
+
<p className="text-[10px] font-mono text-base-content/50 mb-1">{label}</p>
|
|
33
|
+
<p className="text-[11px] font-mono text-base-content">${payload[0].value.toFixed(4)}</p>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function CumulativeCostChart({ data }: CumulativeCostChartProps) {
|
|
39
|
+
return (
|
|
40
|
+
<ResponsiveContainer width="100%" height={200}>
|
|
41
|
+
<AreaChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
42
|
+
<defs>
|
|
43
|
+
<linearGradient id="cumulativeGrad" x1="0" y1="0" x2="0" y2="1">
|
|
44
|
+
<stop offset="5%" stopColor="oklch(55% 0.25 280)" stopOpacity={0.3} />
|
|
45
|
+
<stop offset="95%" stopColor="oklch(55% 0.25 280)" stopOpacity={0} />
|
|
46
|
+
</linearGradient>
|
|
47
|
+
</defs>
|
|
48
|
+
<CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} vertical={false} />
|
|
49
|
+
<XAxis dataKey="date" tick={TICK_STYLE} axisLine={false} tickLine={false} />
|
|
50
|
+
<YAxis tick={TICK_STYLE} axisLine={false} tickLine={false} tickFormatter={function (v) { return "$" + v.toFixed(2); }} />
|
|
51
|
+
<Tooltip content={<CustomTooltip />} />
|
|
52
|
+
<Area
|
|
53
|
+
type="monotone"
|
|
54
|
+
dataKey="total"
|
|
55
|
+
stroke="oklch(55% 0.25 280)"
|
|
56
|
+
fill="url(#cumulativeGrad)"
|
|
57
|
+
strokeWidth={2}
|
|
58
|
+
/>
|
|
59
|
+
</AreaChart>
|
|
60
|
+
</ResponsiveContainer>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ScatterChart,
|
|
3
|
+
Scatter,
|
|
4
|
+
XAxis,
|
|
5
|
+
YAxis,
|
|
6
|
+
CartesianGrid,
|
|
7
|
+
Tooltip,
|
|
8
|
+
ResponsiveContainer,
|
|
9
|
+
} from "recharts";
|
|
10
|
+
|
|
11
|
+
var TICK_STYLE = {
|
|
12
|
+
fontSize: 10,
|
|
13
|
+
fontFamily: "var(--font-mono)",
|
|
14
|
+
fill: "oklch(0.9 0.02 280 / 0.3)",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
var GRID_COLOR = "oklch(0.9 0.02 280 / 0.06)";
|
|
18
|
+
|
|
19
|
+
var MODEL_COLORS: Record<string, string> = {
|
|
20
|
+
opus: "#a855f7",
|
|
21
|
+
sonnet: "oklch(55% 0.25 280)",
|
|
22
|
+
haiku: "#22c55e",
|
|
23
|
+
other: "#f59e0b",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
interface ResponseTimeDatum {
|
|
27
|
+
tokens: number;
|
|
28
|
+
duration: number;
|
|
29
|
+
model: string;
|
|
30
|
+
sessionId: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ResponseTimeScatterProps {
|
|
34
|
+
data: ResponseTimeDatum[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<{ payload: { tokens: number; durationSec: number; model: string } }> }) {
|
|
38
|
+
if (!active || !payload || payload.length === 0) return null;
|
|
39
|
+
var d = payload[0].payload;
|
|
40
|
+
return (
|
|
41
|
+
<div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
|
|
42
|
+
<div className="text-[11px] font-mono text-base-content/70 space-y-0.5">
|
|
43
|
+
<p><span className="text-base-content/40">tokens </span>{d.tokens.toLocaleString()}</p>
|
|
44
|
+
<p><span className="text-base-content/40">duration </span>{d.durationSec.toFixed(1)}s</p>
|
|
45
|
+
<p><span className="text-base-content/40">model </span>{d.model}</p>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function ResponseTimeScatter({ data }: ResponseTimeScatterProps) {
|
|
52
|
+
var models = Array.from(new Set(data.map(function (d) { return d.model; })));
|
|
53
|
+
|
|
54
|
+
var byModel = models.map(function (model) {
|
|
55
|
+
return {
|
|
56
|
+
model: model,
|
|
57
|
+
color: MODEL_COLORS[model] || "#f59e0b",
|
|
58
|
+
points: data
|
|
59
|
+
.filter(function (d) { return d.model === model; })
|
|
60
|
+
.map(function (d) { return { tokens: d.tokens, durationSec: d.duration / 1000, model: d.model }; }),
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<ResponsiveContainer width="100%" height={200}>
|
|
66
|
+
<ScatterChart margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
67
|
+
<CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} />
|
|
68
|
+
<XAxis
|
|
69
|
+
dataKey="tokens"
|
|
70
|
+
type="number"
|
|
71
|
+
tick={TICK_STYLE}
|
|
72
|
+
axisLine={false}
|
|
73
|
+
tickLine={false}
|
|
74
|
+
tickFormatter={function (v) { return v >= 1000 ? (v / 1000).toFixed(0) + "k" : String(v); }}
|
|
75
|
+
name="tokens"
|
|
76
|
+
/>
|
|
77
|
+
<YAxis
|
|
78
|
+
dataKey="durationSec"
|
|
79
|
+
type="number"
|
|
80
|
+
tick={TICK_STYLE}
|
|
81
|
+
axisLine={false}
|
|
82
|
+
tickLine={false}
|
|
83
|
+
tickFormatter={function (v) { return v.toFixed(0) + "s"; }}
|
|
84
|
+
name="duration"
|
|
85
|
+
/>
|
|
86
|
+
<Tooltip content={<CustomTooltip />} cursor={{ strokeDasharray: "3 3", stroke: GRID_COLOR }} />
|
|
87
|
+
{byModel.map(function (group) {
|
|
88
|
+
return (
|
|
89
|
+
<Scatter
|
|
90
|
+
key={group.model}
|
|
91
|
+
name={group.model}
|
|
92
|
+
data={group.points}
|
|
93
|
+
fill={group.color}
|
|
94
|
+
fillOpacity={0.7}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
})}
|
|
98
|
+
</ScatterChart>
|
|
99
|
+
</ResponsiveContainer>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ScatterChart,
|
|
3
|
+
Scatter,
|
|
4
|
+
XAxis,
|
|
5
|
+
YAxis,
|
|
6
|
+
CartesianGrid,
|
|
7
|
+
Tooltip,
|
|
8
|
+
ResponsiveContainer,
|
|
9
|
+
ZAxis,
|
|
10
|
+
} from "recharts";
|
|
11
|
+
|
|
12
|
+
var TICK_STYLE = {
|
|
13
|
+
fontSize: 10,
|
|
14
|
+
fontFamily: "var(--font-mono)",
|
|
15
|
+
fill: "oklch(0.9 0.02 280 / 0.3)",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
var GRID_COLOR = "oklch(0.9 0.02 280 / 0.06)";
|
|
19
|
+
|
|
20
|
+
var PROJECT_PALETTE = [
|
|
21
|
+
"oklch(55% 0.25 280)",
|
|
22
|
+
"#a855f7",
|
|
23
|
+
"#22c55e",
|
|
24
|
+
"#f59e0b",
|
|
25
|
+
"oklch(65% 0.2 240)",
|
|
26
|
+
"oklch(65% 0.25 25)",
|
|
27
|
+
"oklch(65% 0.25 150)",
|
|
28
|
+
"oklch(70% 0.2 60)",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
interface SessionBubbleDatum {
|
|
32
|
+
id: string;
|
|
33
|
+
title: string;
|
|
34
|
+
cost: number;
|
|
35
|
+
tokens: number;
|
|
36
|
+
timestamp: number;
|
|
37
|
+
project: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface SessionBubbleChartProps {
|
|
41
|
+
data: SessionBubbleDatum[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatDate(ts: number): string {
|
|
45
|
+
var d = new Date(ts);
|
|
46
|
+
return (d.getMonth() + 1) + "/" + d.getDate();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<{ payload: SessionBubbleDatum }> }) {
|
|
50
|
+
if (!active || !payload || payload.length === 0) return null;
|
|
51
|
+
var d = payload[0].payload;
|
|
52
|
+
return (
|
|
53
|
+
<div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg max-w-[180px]">
|
|
54
|
+
<p className="text-[10px] font-mono text-base-content/50 mb-1 truncate">{d.title || d.id}</p>
|
|
55
|
+
<div className="text-[11px] font-mono text-base-content/70 space-y-0.5">
|
|
56
|
+
<p><span className="text-base-content/40">cost </span>${d.cost.toFixed(4)}</p>
|
|
57
|
+
<p><span className="text-base-content/40">tokens </span>{d.tokens.toLocaleString()}</p>
|
|
58
|
+
<p><span className="text-base-content/40">project </span>{d.project}</p>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function SessionBubbleChart({ data }: SessionBubbleChartProps) {
|
|
65
|
+
var projects = Array.from(new Set(data.map(function (d) { return d.project; })));
|
|
66
|
+
|
|
67
|
+
function getColor(project: string): string {
|
|
68
|
+
var idx = projects.indexOf(project);
|
|
69
|
+
return PROJECT_PALETTE[idx % PROJECT_PALETTE.length];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
var byProject = projects.map(function (project) {
|
|
73
|
+
return {
|
|
74
|
+
project,
|
|
75
|
+
color: getColor(project),
|
|
76
|
+
points: data
|
|
77
|
+
.filter(function (d) { return d.project === project; })
|
|
78
|
+
.map(function (d) { return { ...d, x: d.timestamp, y: d.tokens, z: Math.max(d.cost * 1000, 20) }; }),
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
var minTs = Math.min(...data.map(function (d) { return d.timestamp; }));
|
|
83
|
+
var maxTs = Math.max(...data.map(function (d) { return d.timestamp; }));
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<ResponsiveContainer width="100%" height={200}>
|
|
87
|
+
<ScatterChart margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
88
|
+
<CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} />
|
|
89
|
+
<XAxis
|
|
90
|
+
dataKey="x"
|
|
91
|
+
type="number"
|
|
92
|
+
domain={[minTs, maxTs]}
|
|
93
|
+
tick={TICK_STYLE}
|
|
94
|
+
axisLine={false}
|
|
95
|
+
tickLine={false}
|
|
96
|
+
tickFormatter={function (v) { return formatDate(v); }}
|
|
97
|
+
/>
|
|
98
|
+
<YAxis
|
|
99
|
+
dataKey="y"
|
|
100
|
+
type="number"
|
|
101
|
+
tick={TICK_STYLE}
|
|
102
|
+
axisLine={false}
|
|
103
|
+
tickLine={false}
|
|
104
|
+
tickFormatter={function (v) { return v >= 1000 ? (v / 1000).toFixed(0) + "k" : String(v); }}
|
|
105
|
+
/>
|
|
106
|
+
<ZAxis dataKey="z" range={[20, 300]} />
|
|
107
|
+
<Tooltip content={<CustomTooltip />} cursor={{ strokeDasharray: "3 3", stroke: GRID_COLOR }} />
|
|
108
|
+
{byProject.map(function (group) {
|
|
109
|
+
return (
|
|
110
|
+
<Scatter
|
|
111
|
+
key={group.project}
|
|
112
|
+
name={group.project}
|
|
113
|
+
data={group.points}
|
|
114
|
+
fill={group.color}
|
|
115
|
+
fillOpacity={0.7}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
})}
|
|
119
|
+
</ScatterChart>
|
|
120
|
+
</ResponsiveContainer>
|
|
121
|
+
);
|
|
122
|
+
}
|