@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.
Files changed (31) hide show
  1. package/bun.lock +71 -0
  2. package/client/package.json +1 -0
  3. package/client/src/components/analytics/AnalyticsView.tsx +119 -0
  4. package/client/src/components/analytics/ChartCard.tsx +22 -0
  5. package/client/src/components/analytics/PeriodSelector.tsx +42 -0
  6. package/client/src/components/analytics/QuickStats.tsx +99 -0
  7. package/client/src/components/analytics/charts/CacheEfficiencyChart.tsx +60 -0
  8. package/client/src/components/analytics/charts/ContextUtilizationChart.tsx +110 -0
  9. package/client/src/components/analytics/charts/CostAreaChart.tsx +83 -0
  10. package/client/src/components/analytics/charts/CostDistributionChart.tsx +62 -0
  11. package/client/src/components/analytics/charts/CostDonutChart.tsx +93 -0
  12. package/client/src/components/analytics/charts/CumulativeCostChart.tsx +62 -0
  13. package/client/src/components/analytics/charts/ResponseTimeScatter.tsx +101 -0
  14. package/client/src/components/analytics/charts/SessionBubbleChart.tsx +122 -0
  15. package/client/src/components/analytics/charts/TokenFlowChart.tsx +82 -0
  16. package/client/src/components/analytics/charts/TokenSankeyChart.tsx +89 -0
  17. package/client/src/components/dashboard/DashboardView.tsx +5 -0
  18. package/client/src/components/sidebar/Sidebar.tsx +10 -2
  19. package/client/src/hooks/useAnalytics.ts +75 -0
  20. package/client/src/router.tsx +4 -0
  21. package/client/src/stores/analytics.ts +54 -0
  22. package/client/src/stores/sidebar.ts +8 -0
  23. package/client/vite.config.ts +1 -0
  24. package/package.json +1 -1
  25. package/server/src/analytics/engine.ts +606 -0
  26. package/server/src/daemon.ts +1 -0
  27. package/server/src/handlers/analytics.ts +34 -0
  28. package/server/src/project/session.ts +4 -4
  29. package/shared/src/analytics.ts +28 -0
  30. package/shared/src/index.ts +1 -0
  31. 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
+ }