@cryptiklemur/lattice 1.6.0 → 1.8.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/client/src/components/analytics/AnalyticsView.tsx +66 -0
- package/client/src/components/analytics/charts/ActivityCalendar.tsx +185 -0
- package/client/src/components/analytics/charts/DailySummaryCards.tsx +83 -0
- package/client/src/components/analytics/charts/HourlyHeatmap.tsx +129 -0
- package/client/src/components/analytics/charts/PermissionBreakdown.tsx +101 -0
- package/client/src/components/analytics/charts/ProjectRadar.tsx +122 -0
- package/client/src/components/analytics/charts/SessionComplexityList.tsx +67 -0
- package/client/src/components/analytics/charts/SessionTimeline.tsx +112 -0
- package/client/src/components/analytics/charts/ToolSunburst.tsx +126 -0
- package/client/src/components/analytics/charts/ToolTreemap.tsx +108 -0
- package/package.json +1 -1
- package/server/src/analytics/engine.ts +219 -0
- package/shared/src/analytics.ts +11 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RadarChart,
|
|
3
|
+
Radar,
|
|
4
|
+
PolarGrid,
|
|
5
|
+
PolarAngleAxis,
|
|
6
|
+
PolarRadiusAxis,
|
|
7
|
+
Tooltip,
|
|
8
|
+
ResponsiveContainer,
|
|
9
|
+
Legend,
|
|
10
|
+
} from "recharts";
|
|
11
|
+
|
|
12
|
+
interface ProjectRadarProps {
|
|
13
|
+
data: Array<{ project: string; cost: number; sessions: number; avgDuration: number; toolDiversity: number; tokensPerSession: number }>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
var PROJECT_COLORS = ["#a855f7", "#22c55e", "#f59e0b", "#ef4444", "oklch(55% 0.25 280)"];
|
|
17
|
+
|
|
18
|
+
var AXIS_KEYS = ["cost", "sessions", "avgDuration", "toolDiversity", "tokensPerSession"] as const;
|
|
19
|
+
var AXIS_LABELS: Record<string, string> = {
|
|
20
|
+
cost: "Cost",
|
|
21
|
+
sessions: "Sessions",
|
|
22
|
+
avgDuration: "Duration",
|
|
23
|
+
toolDiversity: "Tool Diversity",
|
|
24
|
+
tokensPerSession: "Tokens/Session",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function normalize(values: number[]): number[] {
|
|
28
|
+
var max = 0;
|
|
29
|
+
for (var i = 0; i < values.length; i++) {
|
|
30
|
+
if (values[i] > max) max = values[i];
|
|
31
|
+
}
|
|
32
|
+
if (max === 0) return values.map(function () { return 0; });
|
|
33
|
+
return values.map(function (v) { return Math.round((v / max) * 100); });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: Array<{ name: string; value: number; color: string }>; label?: string }) {
|
|
37
|
+
if (!active || !payload || payload.length === 0) return null;
|
|
38
|
+
return (
|
|
39
|
+
<div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
|
|
40
|
+
<p className="text-[10px] font-mono text-base-content/50 mb-1">{label}</p>
|
|
41
|
+
{payload.map(function (entry) {
|
|
42
|
+
return (
|
|
43
|
+
<p key={entry.name} className="text-[10px] font-mono text-base-content/70">
|
|
44
|
+
<span style={{ color: entry.color }}>{entry.name}</span>: {entry.value}
|
|
45
|
+
</p>
|
|
46
|
+
);
|
|
47
|
+
})}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function ProjectRadar({ data }: ProjectRadarProps) {
|
|
53
|
+
if (!data || data.length === 0) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="flex items-center justify-center h-[250px] text-base-content/25 font-mono text-[11px]">
|
|
56
|
+
No project data
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var projects = data.slice(0, 5);
|
|
62
|
+
|
|
63
|
+
var normalized = new Map<string, Map<string, number>>();
|
|
64
|
+
for (var ai = 0; ai < AXIS_KEYS.length; ai++) {
|
|
65
|
+
var key = AXIS_KEYS[ai];
|
|
66
|
+
var rawValues = projects.map(function (p) { return p[key] as number; });
|
|
67
|
+
var normValues = normalize(rawValues);
|
|
68
|
+
for (var pi = 0; pi < projects.length; pi++) {
|
|
69
|
+
var projMap = normalized.get(projects[pi].project);
|
|
70
|
+
if (!projMap) {
|
|
71
|
+
projMap = new Map();
|
|
72
|
+
normalized.set(projects[pi].project, projMap);
|
|
73
|
+
}
|
|
74
|
+
projMap.set(key, normValues[pi]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
var radarData = AXIS_KEYS.map(function (key) {
|
|
79
|
+
var entry: Record<string, string | number> = { axis: AXIS_LABELS[key] };
|
|
80
|
+
for (var pi = 0; pi < projects.length; pi++) {
|
|
81
|
+
var projMap = normalized.get(projects[pi].project);
|
|
82
|
+
entry[projects[pi].project] = projMap ? projMap.get(key) || 0 : 0;
|
|
83
|
+
}
|
|
84
|
+
return entry;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<ResponsiveContainer width="100%" height={280}>
|
|
89
|
+
<RadarChart data={radarData} margin={{ top: 10, right: 30, bottom: 10, left: 30 }}>
|
|
90
|
+
<PolarGrid stroke="oklch(0.9 0.02 280 / 0.08)" />
|
|
91
|
+
<PolarAngleAxis
|
|
92
|
+
dataKey="axis"
|
|
93
|
+
tick={{ fontSize: 9, fontFamily: "var(--font-mono)", fill: "oklch(0.9 0.02 280 / 0.4)" }}
|
|
94
|
+
/>
|
|
95
|
+
<PolarRadiusAxis
|
|
96
|
+
angle={90}
|
|
97
|
+
domain={[0, 100]}
|
|
98
|
+
tick={false}
|
|
99
|
+
axisLine={false}
|
|
100
|
+
/>
|
|
101
|
+
{projects.map(function (project, index) {
|
|
102
|
+
return (
|
|
103
|
+
<Radar
|
|
104
|
+
key={project.project}
|
|
105
|
+
name={project.project}
|
|
106
|
+
dataKey={project.project}
|
|
107
|
+
stroke={PROJECT_COLORS[index % PROJECT_COLORS.length]}
|
|
108
|
+
fill={PROJECT_COLORS[index % PROJECT_COLORS.length]}
|
|
109
|
+
fillOpacity={0.1}
|
|
110
|
+
strokeWidth={1.5}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
})}
|
|
114
|
+
<Tooltip content={<CustomTooltip />} />
|
|
115
|
+
<Legend
|
|
116
|
+
wrapperStyle={{ fontSize: 10, fontFamily: "var(--font-mono)" }}
|
|
117
|
+
iconSize={8}
|
|
118
|
+
/>
|
|
119
|
+
</RadarChart>
|
|
120
|
+
</ResponsiveContainer>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
interface SessionComplexityListProps {
|
|
2
|
+
data: Array<{ id: string; title: string; score: number; messages: number; tools: number; contextPercent: number }>;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function getScoreColor(score: number, maxScore: number): string {
|
|
6
|
+
var intensity = maxScore > 0 ? Math.min(score / maxScore, 1) : 0;
|
|
7
|
+
var lightness = 0.45 - intensity * 0.1;
|
|
8
|
+
var chroma = 0.1 + intensity * 0.18;
|
|
9
|
+
return "oklch(" + lightness + " " + chroma + " 280)";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function SessionComplexityList({ data }: SessionComplexityListProps) {
|
|
13
|
+
if (!data || data.length === 0) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex items-center justify-center h-[200px] text-base-content/25 font-mono text-[11px]">
|
|
16
|
+
No session data
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var maxScore = data.length > 0 ? data[0].score : 1;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex flex-col gap-1 max-h-[400px] overflow-y-auto">
|
|
25
|
+
<div className="grid grid-cols-[2.5rem_1fr_4rem_3rem_3rem_3.5rem] gap-2 px-2 py-1 text-[9px] font-mono text-base-content/30 uppercase tracking-wider">
|
|
26
|
+
<span>Rank</span>
|
|
27
|
+
<span>Session</span>
|
|
28
|
+
<span className="text-right">Score</span>
|
|
29
|
+
<span className="text-right">Msgs</span>
|
|
30
|
+
<span className="text-right">Tools</span>
|
|
31
|
+
<span className="text-right">Ctx%</span>
|
|
32
|
+
</div>
|
|
33
|
+
{data.map(function (entry, index) {
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
key={entry.id}
|
|
37
|
+
className="grid grid-cols-[2.5rem_1fr_4rem_3rem_3rem_3.5rem] gap-2 px-2 py-1.5 rounded hover:bg-base-content/[0.03] transition-colors"
|
|
38
|
+
>
|
|
39
|
+
<span className="font-mono text-[11px] text-base-content/40 font-bold tabular-nums">
|
|
40
|
+
#{index + 1}
|
|
41
|
+
</span>
|
|
42
|
+
<span className="font-mono text-[11px] text-base-content/70 truncate" title={entry.title}>
|
|
43
|
+
{entry.title}
|
|
44
|
+
</span>
|
|
45
|
+
<span className="text-right">
|
|
46
|
+
<span
|
|
47
|
+
className="inline-block px-1.5 py-0.5 rounded text-[10px] font-mono font-bold text-white/90 tabular-nums"
|
|
48
|
+
style={{ background: getScoreColor(entry.score, maxScore) }}
|
|
49
|
+
>
|
|
50
|
+
{entry.score}
|
|
51
|
+
</span>
|
|
52
|
+
</span>
|
|
53
|
+
<span className="text-right font-mono text-[10px] text-base-content/50 tabular-nums self-center">
|
|
54
|
+
{entry.messages}
|
|
55
|
+
</span>
|
|
56
|
+
<span className="text-right font-mono text-[10px] text-base-content/50 tabular-nums self-center">
|
|
57
|
+
{entry.tools}
|
|
58
|
+
</span>
|
|
59
|
+
<span className="text-right font-mono text-[10px] text-base-content/50 tabular-nums self-center">
|
|
60
|
+
{entry.contextPercent}%
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
interface TimelineDatum {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
project: string;
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
cost: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SessionTimelineProps {
|
|
13
|
+
data: TimelineDatum[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
var ROW_HEIGHT = 16;
|
|
17
|
+
var ROW_GAP = 2;
|
|
18
|
+
var ROW_STEP = ROW_HEIGHT + ROW_GAP;
|
|
19
|
+
var LEFT_MARGIN = 8;
|
|
20
|
+
var RIGHT_MARGIN = 8;
|
|
21
|
+
|
|
22
|
+
var PROJECT_PALETTE = [
|
|
23
|
+
"oklch(55% 0.25 280)",
|
|
24
|
+
"#a855f7",
|
|
25
|
+
"#22c55e",
|
|
26
|
+
"#f59e0b",
|
|
27
|
+
"oklch(65% 0.2 240)",
|
|
28
|
+
"oklch(65% 0.25 25)",
|
|
29
|
+
"oklch(65% 0.25 150)",
|
|
30
|
+
"oklch(70% 0.2 60)",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function formatTime(ts: number): string {
|
|
34
|
+
var d = new Date(ts);
|
|
35
|
+
return (d.getMonth() + 1) + "/" + d.getDate() + " " + String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function SessionTimeline({ data }: SessionTimelineProps) {
|
|
39
|
+
var [hover, setHover] = useState<{ x: number; y: number; datum: TimelineDatum } | null>(null);
|
|
40
|
+
|
|
41
|
+
if (!data || data.length === 0) {
|
|
42
|
+
return (
|
|
43
|
+
<div className="flex items-center justify-center h-[200px] text-base-content/25 font-mono text-[11px]">
|
|
44
|
+
No data
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var projects = Array.from(new Set(data.map(function (d) { return d.project; })));
|
|
50
|
+
function getColor(project: string): string {
|
|
51
|
+
var idx = projects.indexOf(project);
|
|
52
|
+
return PROJECT_PALETTE[idx % PROJECT_PALETTE.length];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
var minTime = Infinity;
|
|
56
|
+
var maxTime = -Infinity;
|
|
57
|
+
for (var i = 0; i < data.length; i++) {
|
|
58
|
+
if (data[i].start < minTime) minTime = data[i].start;
|
|
59
|
+
if (data[i].end > maxTime) maxTime = data[i].end;
|
|
60
|
+
}
|
|
61
|
+
var timeRange = maxTime - minTime || 1;
|
|
62
|
+
|
|
63
|
+
var svgHeight = data.length * ROW_STEP;
|
|
64
|
+
var maxHeight = 300;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="relative overflow-y-auto overflow-x-hidden" style={{ maxHeight: maxHeight }}>
|
|
68
|
+
<svg width="100%" height={svgHeight} className="block" viewBox={"0 0 600 " + svgHeight} preserveAspectRatio="none">
|
|
69
|
+
{data.map(function (d, idx) {
|
|
70
|
+
var barWidth = 600 - LEFT_MARGIN - RIGHT_MARGIN;
|
|
71
|
+
var x1 = LEFT_MARGIN + ((d.start - minTime) / timeRange) * barWidth;
|
|
72
|
+
var x2 = LEFT_MARGIN + ((d.end - minTime) / timeRange) * barWidth;
|
|
73
|
+
var w = Math.max(x2 - x1, 3);
|
|
74
|
+
var y = idx * ROW_STEP;
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<rect
|
|
78
|
+
key={d.id}
|
|
79
|
+
x={x1}
|
|
80
|
+
y={y}
|
|
81
|
+
width={w}
|
|
82
|
+
height={ROW_HEIGHT}
|
|
83
|
+
rx={3}
|
|
84
|
+
ry={3}
|
|
85
|
+
fill={getColor(d.project)}
|
|
86
|
+
opacity={0.7}
|
|
87
|
+
onMouseEnter={function (e) {
|
|
88
|
+
setHover({ x: e.clientX, y: e.clientY, datum: d });
|
|
89
|
+
}}
|
|
90
|
+
onMouseLeave={function () { setHover(null); }}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
})}
|
|
94
|
+
</svg>
|
|
95
|
+
|
|
96
|
+
{hover && (
|
|
97
|
+
<div
|
|
98
|
+
className="fixed z-50 rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg pointer-events-none max-w-[200px]"
|
|
99
|
+
style={{ left: hover.x + 12, top: hover.y - 50 }}
|
|
100
|
+
>
|
|
101
|
+
<p className="text-[10px] font-mono text-base-content/50 mb-1 truncate">{hover.datum.title}</p>
|
|
102
|
+
<div className="text-[11px] font-mono text-base-content/70 space-y-0.5">
|
|
103
|
+
<p><span className="text-base-content/40">project </span>{hover.datum.project}</p>
|
|
104
|
+
<p><span className="text-base-content/40">start </span>{formatTime(hover.datum.start)}</p>
|
|
105
|
+
<p><span className="text-base-content/40">end </span>{formatTime(hover.datum.end)}</p>
|
|
106
|
+
<p><span className="text-base-content/40">cost </span>${hover.datum.cost.toFixed(4)}</p>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from "recharts";
|
|
2
|
+
|
|
3
|
+
interface ToolSunburstProps {
|
|
4
|
+
data: Array<{ name: string; category: string; count: number }>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
var CATEGORY_COLORS: Record<string, string> = {
|
|
8
|
+
Read: "#22c55e",
|
|
9
|
+
Write: "#f59e0b",
|
|
10
|
+
Execute: "#ef4444",
|
|
11
|
+
AI: "#a855f7",
|
|
12
|
+
Other: "oklch(55% 0.15 280)",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function getCategoryColor(category: string): string {
|
|
16
|
+
return CATEGORY_COLORS[category] || CATEGORY_COLORS.Other;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getToolColor(category: string, index: number): string {
|
|
20
|
+
var base = getCategoryColor(category);
|
|
21
|
+
var opacity = 0.9 - index * 0.12;
|
|
22
|
+
if (opacity < 0.4) opacity = 0.4;
|
|
23
|
+
return base.startsWith("oklch") ? base.replace(")", " / " + opacity + ")") : base + String(Math.round(opacity * 255).toString(16)).padStart(2, "0");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<{ name: string; value: number; payload: { category?: string } }> }) {
|
|
27
|
+
if (!active || !payload || payload.length === 0) return null;
|
|
28
|
+
var entry = payload[0];
|
|
29
|
+
return (
|
|
30
|
+
<div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
|
|
31
|
+
<p className="text-[11px] font-mono text-base-content font-bold">{entry.name}</p>
|
|
32
|
+
{entry.payload.category && (
|
|
33
|
+
<p className="text-[10px] font-mono text-base-content/40">{entry.payload.category}</p>
|
|
34
|
+
)}
|
|
35
|
+
<p className="text-[10px] font-mono text-base-content/60">{entry.value.toLocaleString()} calls</p>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function ToolSunburst({ data }: ToolSunburstProps) {
|
|
41
|
+
if (!data || data.length === 0) {
|
|
42
|
+
return (
|
|
43
|
+
<div className="flex items-center justify-center h-[250px] text-base-content/25 font-mono text-[11px]">
|
|
44
|
+
No tool data
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var categoryTotals = new Map<string, number>();
|
|
50
|
+
for (var i = 0; i < data.length; i++) {
|
|
51
|
+
var cat = data[i].category;
|
|
52
|
+
categoryTotals.set(cat, (categoryTotals.get(cat) || 0) + data[i].count);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
var innerData: Array<{ name: string; value: number }> = [];
|
|
56
|
+
categoryTotals.forEach(function (count, category) {
|
|
57
|
+
innerData.push({ name: category, value: count });
|
|
58
|
+
});
|
|
59
|
+
innerData.sort(function (a, b) { return b.value - a.value; });
|
|
60
|
+
|
|
61
|
+
var categoryOrder: string[] = innerData.map(function (d) { return d.name; });
|
|
62
|
+
|
|
63
|
+
var outerData: Array<{ name: string; category: string; value: number }> = [];
|
|
64
|
+
for (var ci = 0; ci < categoryOrder.length; ci++) {
|
|
65
|
+
var catName = categoryOrder[ci];
|
|
66
|
+
var catTools = data
|
|
67
|
+
.filter(function (d) { return d.category === catName; })
|
|
68
|
+
.sort(function (a, b) { return b.count - a.count; });
|
|
69
|
+
for (var ti = 0; ti < catTools.length; ti++) {
|
|
70
|
+
outerData.push({ name: catTools[ti].name, category: catName, value: catTools[ti].count });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div>
|
|
76
|
+
<ResponsiveContainer width="100%" height={220}>
|
|
77
|
+
<PieChart>
|
|
78
|
+
<Pie
|
|
79
|
+
data={innerData}
|
|
80
|
+
dataKey="value"
|
|
81
|
+
nameKey="name"
|
|
82
|
+
innerRadius={35}
|
|
83
|
+
outerRadius={60}
|
|
84
|
+
paddingAngle={2}
|
|
85
|
+
startAngle={90}
|
|
86
|
+
endAngle={-270}
|
|
87
|
+
>
|
|
88
|
+
{innerData.map(function (entry) {
|
|
89
|
+
return <Cell key={"inner-" + entry.name} fill={getCategoryColor(entry.name)} />;
|
|
90
|
+
})}
|
|
91
|
+
</Pie>
|
|
92
|
+
<Pie
|
|
93
|
+
data={outerData}
|
|
94
|
+
dataKey="value"
|
|
95
|
+
nameKey="name"
|
|
96
|
+
innerRadius={65}
|
|
97
|
+
outerRadius={90}
|
|
98
|
+
paddingAngle={1}
|
|
99
|
+
startAngle={90}
|
|
100
|
+
endAngle={-270}
|
|
101
|
+
>
|
|
102
|
+
{outerData.map(function (entry, index) {
|
|
103
|
+
var catIndex = outerData.slice(0, index).filter(function (d) { return d.category === entry.category; }).length;
|
|
104
|
+
return <Cell key={"outer-" + entry.name} fill={getToolColor(entry.category, catIndex)} />;
|
|
105
|
+
})}
|
|
106
|
+
</Pie>
|
|
107
|
+
<Tooltip content={<CustomTooltip />} />
|
|
108
|
+
</PieChart>
|
|
109
|
+
</ResponsiveContainer>
|
|
110
|
+
<div className="flex flex-wrap justify-center gap-3 mt-1">
|
|
111
|
+
{innerData.map(function (entry) {
|
|
112
|
+
return (
|
|
113
|
+
<div key={entry.name} className="flex items-center gap-1.5 text-[10px] font-mono text-base-content/50">
|
|
114
|
+
<span
|
|
115
|
+
className="inline-block w-2 h-2 rounded-full flex-shrink-0"
|
|
116
|
+
style={{ background: getCategoryColor(entry.name) }}
|
|
117
|
+
/>
|
|
118
|
+
<span>{entry.name}</span>
|
|
119
|
+
<span className="text-base-content/30">{entry.value}</span>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
})}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Treemap, ResponsiveContainer, Tooltip } from "recharts";
|
|
2
|
+
|
|
3
|
+
interface ToolTreemapProps {
|
|
4
|
+
data: Array<{ name: string; count: number; avgCost: number }>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
var MAX_COST_INTENSITY = 0.5;
|
|
8
|
+
|
|
9
|
+
function getColor(avgCost: number, maxCost: number): string {
|
|
10
|
+
var intensity = maxCost > 0 ? Math.min(avgCost / maxCost, 1) : 0.3;
|
|
11
|
+
var lightness = 0.45 - intensity * 0.15;
|
|
12
|
+
var chroma = 0.15 + intensity * 0.12;
|
|
13
|
+
return "oklch(" + lightness + " " + chroma + " 280)";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function CustomContent(props: {
|
|
17
|
+
x?: number; y?: number; width?: number; height?: number;
|
|
18
|
+
name?: string; count?: number; avgCost?: number;
|
|
19
|
+
maxCost?: number;
|
|
20
|
+
}) {
|
|
21
|
+
var { x = 0, y = 0, width = 0, height = 0, name = "", count = 0, avgCost = 0, maxCost = MAX_COST_INTENSITY } = props;
|
|
22
|
+
if (width < 4 || height < 4) return null;
|
|
23
|
+
var showLabel = width > 40 && height > 20;
|
|
24
|
+
var showCount = width > 50 && height > 34;
|
|
25
|
+
return (
|
|
26
|
+
<g>
|
|
27
|
+
<rect
|
|
28
|
+
x={x + 1}
|
|
29
|
+
y={y + 1}
|
|
30
|
+
width={width - 2}
|
|
31
|
+
height={height - 2}
|
|
32
|
+
rx={3}
|
|
33
|
+
fill={getColor(avgCost, maxCost)}
|
|
34
|
+
fillOpacity={0.85}
|
|
35
|
+
stroke="oklch(0.2 0.02 280)"
|
|
36
|
+
strokeWidth={1}
|
|
37
|
+
/>
|
|
38
|
+
{showLabel && (
|
|
39
|
+
<text
|
|
40
|
+
x={x + width / 2}
|
|
41
|
+
y={y + height / 2 + (showCount ? -5 : 0)}
|
|
42
|
+
textAnchor="middle"
|
|
43
|
+
dominantBaseline="middle"
|
|
44
|
+
style={{ fontSize: 10, fontFamily: "var(--font-mono)", fill: "oklch(0.95 0.02 280 / 0.9)" }}
|
|
45
|
+
>
|
|
46
|
+
{name}
|
|
47
|
+
</text>
|
|
48
|
+
)}
|
|
49
|
+
{showCount && (
|
|
50
|
+
<text
|
|
51
|
+
x={x + width / 2}
|
|
52
|
+
y={y + height / 2 + 9}
|
|
53
|
+
textAnchor="middle"
|
|
54
|
+
dominantBaseline="middle"
|
|
55
|
+
style={{ fontSize: 9, fontFamily: "var(--font-mono)", fill: "oklch(0.95 0.02 280 / 0.5)" }}
|
|
56
|
+
>
|
|
57
|
+
{count}
|
|
58
|
+
</text>
|
|
59
|
+
)}
|
|
60
|
+
</g>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<{ payload: { name: string; count: number; avgCost: number } }> }) {
|
|
65
|
+
if (!active || !payload || payload.length === 0) return null;
|
|
66
|
+
var d = payload[0].payload;
|
|
67
|
+
return (
|
|
68
|
+
<div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
|
|
69
|
+
<p className="text-[11px] font-mono text-base-content font-bold mb-1">{d.name}</p>
|
|
70
|
+
<div className="text-[10px] font-mono text-base-content/60 space-y-0.5">
|
|
71
|
+
<p><span className="text-base-content/40">calls </span>{d.count.toLocaleString()}</p>
|
|
72
|
+
<p><span className="text-base-content/40">avg cost </span>${d.avgCost.toFixed(4)}</p>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function ToolTreemap({ data }: ToolTreemapProps) {
|
|
79
|
+
if (!data || data.length === 0) {
|
|
80
|
+
return (
|
|
81
|
+
<div className="flex items-center justify-center h-[250px] text-base-content/25 font-mono text-[11px]">
|
|
82
|
+
No tool data
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
var maxCost = 0;
|
|
88
|
+
for (var i = 0; i < data.length; i++) {
|
|
89
|
+
if (data[i].avgCost > maxCost) maxCost = data[i].avgCost;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var treemapData = data.map(function (d) {
|
|
93
|
+
return { name: d.name, size: d.count, count: d.count, avgCost: d.avgCost, maxCost: maxCost };
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<ResponsiveContainer width="100%" height={250}>
|
|
98
|
+
<Treemap
|
|
99
|
+
data={treemapData}
|
|
100
|
+
dataKey="size"
|
|
101
|
+
aspectRatio={4 / 3}
|
|
102
|
+
content={<CustomContent maxCost={maxCost} />}
|
|
103
|
+
>
|
|
104
|
+
<Tooltip content={<CustomTooltip />} />
|
|
105
|
+
</Treemap>
|
|
106
|
+
</ResponsiveContainer>
|
|
107
|
+
);
|
|
108
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Multi-machine agentic dashboard for Claude Code. Monitor sessions, manage MCP servers and skills, orchestrate across mesh-networked nodes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Aaron Scherer <me@aaronscherer.me>",
|