@cryptiklemur/lattice 1.7.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.
@@ -36,6 +36,11 @@ import { ActivityCalendar } from "./charts/ActivityCalendar";
36
36
  import { HourlyHeatmap } from "./charts/HourlyHeatmap";
37
37
  import { SessionTimeline } from "./charts/SessionTimeline";
38
38
  import { DailySummaryCards } from "./charts/DailySummaryCards";
39
+ import { ToolTreemap } from "./charts/ToolTreemap";
40
+ import { ToolSunburst } from "./charts/ToolSunburst";
41
+ import { PermissionBreakdown } from "./charts/PermissionBreakdown";
42
+ import { ProjectRadar } from "./charts/ProjectRadar";
43
+ import { SessionComplexityList } from "./charts/SessionComplexityList";
39
44
 
40
45
  export function AnalyticsView() {
41
46
  var analytics = useAnalytics();
@@ -136,6 +141,38 @@ export function AnalyticsView() {
136
141
  <DailySummaryCards data={analytics.data.dailySummaries} />
137
142
  </ChartErrorBoundary>
138
143
  </ChartCard>
144
+
145
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
146
+ <ChartCard title="Tool Usage (Treemap)">
147
+ <ChartErrorBoundary name="Treemap">
148
+ <ToolTreemap data={analytics.data.toolTreemap} />
149
+ </ChartErrorBoundary>
150
+ </ChartCard>
151
+ <ChartCard title="Tool Categories">
152
+ <ChartErrorBoundary name="Sunburst">
153
+ <ToolSunburst data={analytics.data.toolSunburst} />
154
+ </ChartErrorBoundary>
155
+ </ChartCard>
156
+ </div>
157
+
158
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
159
+ <ChartCard title="Permissions">
160
+ <ChartErrorBoundary name="Permissions">
161
+ <PermissionBreakdown data={analytics.data.permissionStats} />
162
+ </ChartErrorBoundary>
163
+ </ChartCard>
164
+ <ChartCard title="Project Comparison">
165
+ <ChartErrorBoundary name="Radar">
166
+ <ProjectRadar data={analytics.data.projectRadar} />
167
+ </ChartErrorBoundary>
168
+ </ChartCard>
169
+ </div>
170
+
171
+ <ChartCard title="Session Complexity">
172
+ <ChartErrorBoundary name="Complexity">
173
+ <SessionComplexityList data={analytics.data.sessionComplexity} />
174
+ </ChartErrorBoundary>
175
+ </ChartCard>
139
176
  </div>
140
177
  )}
141
178
 
@@ -0,0 +1,101 @@
1
+ import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from "recharts";
2
+
3
+ interface PermissionBreakdownProps {
4
+ data: { allowed: number; denied: number; alwaysAllowed: number };
5
+ }
6
+
7
+ var COLORS = {
8
+ allowed: "#22c55e",
9
+ denied: "#ef4444",
10
+ alwaysAllowed: "oklch(55% 0.25 280)",
11
+ };
12
+
13
+ function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<{ name: string; value: number }> }) {
14
+ if (!active || !payload || payload.length === 0) return null;
15
+ var entry = payload[0];
16
+ return (
17
+ <div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
18
+ <p className="text-[11px] font-mono text-base-content">{entry.name}</p>
19
+ <p className="text-[10px] font-mono text-base-content/60">{entry.value.toLocaleString()}</p>
20
+ </div>
21
+ );
22
+ }
23
+
24
+ export function PermissionBreakdown({ data }: PermissionBreakdownProps) {
25
+ var total = data.allowed + data.denied + data.alwaysAllowed;
26
+
27
+ if (total === 0) {
28
+ return (
29
+ <div className="flex items-center justify-center h-[200px] text-base-content/25 font-mono text-[11px]">
30
+ No permission data
31
+ </div>
32
+ );
33
+ }
34
+
35
+ if (data.denied === 0 && data.alwaysAllowed === 0) {
36
+ return (
37
+ <div className="flex flex-col items-center justify-center h-[200px] gap-2">
38
+ <div className="text-[32px] font-mono font-bold text-base-content/80">{total.toLocaleString()}</div>
39
+ <div className="text-[11px] font-mono text-base-content/40">tool calls — all allowed</div>
40
+ </div>
41
+ );
42
+ }
43
+
44
+ var pieData = [
45
+ { name: "Allowed", value: data.allowed },
46
+ { name: "Denied", value: data.denied },
47
+ { name: "Always Allowed", value: data.alwaysAllowed },
48
+ ].filter(function (d) { return d.value > 0; });
49
+
50
+ var colorMap: Record<string, string> = {
51
+ Allowed: COLORS.allowed,
52
+ Denied: COLORS.denied,
53
+ "Always Allowed": COLORS.alwaysAllowed,
54
+ };
55
+
56
+ return (
57
+ <div>
58
+ <ResponsiveContainer width="100%" height={180}>
59
+ <PieChart>
60
+ <Pie
61
+ data={pieData}
62
+ dataKey="value"
63
+ nameKey="name"
64
+ innerRadius={45}
65
+ outerRadius={70}
66
+ paddingAngle={2}
67
+ startAngle={90}
68
+ endAngle={-270}
69
+ >
70
+ {pieData.map(function (entry) {
71
+ return <Cell key={entry.name} fill={colorMap[entry.name] || "#888"} />;
72
+ })}
73
+ </Pie>
74
+ <Tooltip content={<CustomTooltip />} />
75
+ <text x="50%" y="50%" textAnchor="middle" dominantBaseline="middle">
76
+ <tspan x="50%" dy="-0.3em" style={{ fontSize: 14, fontFamily: "var(--font-mono)", fill: "oklch(0.9 0.02 280 / 0.9)", fontWeight: 700 }}>
77
+ {total.toLocaleString()}
78
+ </tspan>
79
+ <tspan x="50%" dy="1.3em" style={{ fontSize: 9, fontFamily: "var(--font-mono)", fill: "oklch(0.9 0.02 280 / 0.35)" }}>
80
+ total
81
+ </tspan>
82
+ </text>
83
+ </PieChart>
84
+ </ResponsiveContainer>
85
+ <div className="flex flex-wrap justify-center gap-3 mt-1">
86
+ {pieData.map(function (entry) {
87
+ return (
88
+ <div key={entry.name} className="flex items-center gap-1.5 text-[10px] font-mono text-base-content/50">
89
+ <span
90
+ className="inline-block w-2 h-2 rounded-full flex-shrink-0"
91
+ style={{ background: colorMap[entry.name] }}
92
+ />
93
+ <span>{entry.name}</span>
94
+ <span className="text-base-content/30">{entry.value.toLocaleString()}</span>
95
+ </div>
96
+ );
97
+ })}
98
+ </div>
99
+ </div>
100
+ );
101
+ }
@@ -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,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.7.0",
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>",
@@ -638,6 +638,98 @@ function aggregate(sessions: SessionData[], period: AnalyticsPeriod): AnalyticsP
638
638
  });
639
639
  }
640
640
 
641
+ var toolTreemap: AnalyticsPayload["toolTreemap"] = [];
642
+ toolStats.forEach(function (val, key) {
643
+ toolTreemap.push({
644
+ name: key,
645
+ count: val.count,
646
+ avgCost: val.sessions > 0 ? val.totalCost / val.sessions : 0,
647
+ });
648
+ });
649
+ toolTreemap.sort(function (a, b) { return b.count - a.count; });
650
+
651
+ var toolCategoryMap: Record<string, string> = {
652
+ Read: "Read", Glob: "Read", Grep: "Read", LS: "Read",
653
+ Edit: "Write", Write: "Write", MultiEdit: "Write",
654
+ Bash: "Execute",
655
+ Agent: "AI", Skill: "AI",
656
+ };
657
+ var toolSunburst: AnalyticsPayload["toolSunburst"] = [];
658
+ toolStats.forEach(function (val, key) {
659
+ var category = toolCategoryMap[key] || "Other";
660
+ toolSunburst.push({ name: key, category: category, count: val.count });
661
+ });
662
+ toolSunburst.sort(function (a, b) { return b.count - a.count; });
663
+
664
+ var totalToolCalls = 0;
665
+ toolStats.forEach(function (val) { totalToolCalls += val.count; });
666
+ var permissionStats: AnalyticsPayload["permissionStats"] = {
667
+ allowed: totalToolCalls,
668
+ denied: 0,
669
+ alwaysAllowed: 0,
670
+ };
671
+
672
+ var projectRadarMap = new Map<string, { cost: number; sessions: number; totalDuration: number; durationCount: number; tools: Set<string>; totalTokens: number }>();
673
+ for (var pri = 0; pri < filtered.length; pri++) {
674
+ var prSess = filtered[pri];
675
+ var prEntry = projectRadarMap.get(prSess.project);
676
+ if (!prEntry) {
677
+ prEntry = { cost: 0, sessions: 0, totalDuration: 0, durationCount: 0, tools: new Set(), totalTokens: 0 };
678
+ projectRadarMap.set(prSess.project, prEntry);
679
+ }
680
+ prEntry.cost += prSess.cost;
681
+ prEntry.sessions++;
682
+ prEntry.totalTokens += prSess.inputTokens + prSess.outputTokens;
683
+ if (prSess.startTime > 0 && prSess.endTime > prSess.startTime) {
684
+ prEntry.totalDuration += prSess.endTime - prSess.startTime;
685
+ prEntry.durationCount++;
686
+ }
687
+ prSess.tools.forEach(function (_count, tool) { prEntry!.tools.add(tool); });
688
+ }
689
+ var projectRadar: AnalyticsPayload["projectRadar"] = [];
690
+ projectRadarMap.forEach(function (val, key) {
691
+ projectRadar.push({
692
+ project: key,
693
+ cost: val.cost,
694
+ sessions: val.sessions,
695
+ avgDuration: val.durationCount > 0 ? val.totalDuration / val.durationCount : 0,
696
+ toolDiversity: val.tools.size,
697
+ tokensPerSession: val.sessions > 0 ? val.totalTokens / val.sessions : 0,
698
+ });
699
+ });
700
+ projectRadar.sort(function (a, b) { return b.cost - a.cost; });
701
+ if (projectRadar.length > 5) projectRadar.length = 5;
702
+
703
+ var contextWindowSizesForComplexity: Record<string, number> = { opus: 200000, sonnet: 200000, haiku: 200000, other: 200000 };
704
+ var sessionComplexity: AnalyticsPayload["sessionComplexity"] = [];
705
+ for (var sci = 0; sci < filtered.length; sci++) {
706
+ var scSess = filtered[sci];
707
+ var scUniqueTools = scSess.tools.size;
708
+ var scMessages = scSess.contextMessages.length;
709
+ var scRunning = 0;
710
+ var scPrimaryModel = "other";
711
+ var scMaxTokens = 0;
712
+ scSess.models.forEach(function (val, key) {
713
+ if (val.tokens > scMaxTokens) { scMaxTokens = val.tokens; scPrimaryModel = key; }
714
+ });
715
+ var scWindowSize = contextWindowSizesForComplexity[scPrimaryModel] || 200000;
716
+ for (var scmi = 0; scmi < scSess.contextMessages.length; scmi++) {
717
+ scRunning += scSess.contextMessages[scmi].inputTokens;
718
+ }
719
+ var scContextPercent = Math.min((scRunning / scWindowSize) * 100, 100);
720
+ var scScore = (scMessages * 1) + (scUniqueTools * 5) + (scContextPercent * 0.5);
721
+ sessionComplexity.push({
722
+ id: scSess.id,
723
+ title: scSess.title,
724
+ score: Math.round(scScore * 10) / 10,
725
+ messages: scMessages,
726
+ tools: scUniqueTools,
727
+ contextPercent: Math.round(scContextPercent * 10) / 10,
728
+ });
729
+ }
730
+ sessionComplexity.sort(function (a, b) { return b.score - a.score; });
731
+ if (sessionComplexity.length > 20) sessionComplexity.length = 20;
732
+
641
733
  return {
642
734
  totalCost: totalCost,
643
735
  totalSessions: filtered.length,
@@ -667,6 +759,11 @@ function aggregate(sessions: SessionData[], period: AnalyticsPeriod): AnalyticsP
667
759
  hourlyHeatmap: hourlyHeatmap,
668
760
  sessionTimeline: sessionTimeline,
669
761
  dailySummaries: dailySummaries,
762
+ toolTreemap: toolTreemap,
763
+ toolSunburst: toolSunburst,
764
+ permissionStats: permissionStats,
765
+ projectRadar: projectRadar,
766
+ sessionComplexity: sessionComplexity,
670
767
  };
671
768
  }
672
769
 
@@ -27,6 +27,12 @@ export interface AnalyticsPayload {
27
27
  hourlyHeatmap: Array<{ day: number; hour: number; count: number }>;
28
28
  sessionTimeline: Array<{ id: string; title: string; project: string; start: number; end: number; cost: number }>;
29
29
  dailySummaries: Array<{ date: string; sessions: number; cost: number; tokens: number; topTool: string; modelMix: Record<string, number> }>;
30
+
31
+ toolTreemap: Array<{ name: string; count: number; avgCost: number }>;
32
+ toolSunburst: Array<{ name: string; category: string; count: number }>;
33
+ permissionStats: { allowed: number; denied: number; alwaysAllowed: number };
34
+ projectRadar: Array<{ project: string; cost: number; sessions: number; avgDuration: number; toolDiversity: number; tokensPerSession: number }>;
35
+ sessionComplexity: Array<{ id: string; title: string; score: number; messages: number; tools: number; contextPercent: number }>;
30
36
  }
31
37
 
32
38
  export type AnalyticsPeriod = "24h" | "7d" | "30d" | "90d" | "all";