@cryptiklemur/lattice 1.11.0 → 1.11.2
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 +44 -2
- package/client/src/components/analytics/ChartCard.tsx +39 -9
- package/client/src/components/analytics/charts/CacheEfficiencyChart.tsx +2 -2
- package/client/src/components/analytics/charts/ContextUtilizationChart.tsx +2 -2
- package/client/src/components/analytics/charts/CostAreaChart.tsx +2 -2
- package/client/src/components/analytics/charts/CostDistributionChart.tsx +2 -2
- package/client/src/components/analytics/charts/CostDonutChart.tsx +2 -2
- package/client/src/components/analytics/charts/CumulativeCostChart.tsx +2 -2
- package/client/src/components/analytics/charts/ResponseTimeScatter.tsx +2 -2
- package/client/src/components/analytics/charts/SessionBubbleChart.tsx +2 -2
- package/client/src/components/analytics/charts/TokenFlowChart.tsx +2 -2
- package/client/src/components/analytics/charts/TokenSankeyChart.tsx +2 -2
- package/client/src/components/analytics/charts/ToolTreemap.tsx +2 -2
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Component } from "react";
|
|
2
|
+
import { BarChart3 } from "lucide-react";
|
|
2
3
|
import { useAnalytics } from "../../hooks/useAnalytics";
|
|
3
4
|
import { useMesh } from "../../hooks/useMesh";
|
|
4
5
|
|
|
@@ -21,8 +22,20 @@ class ChartErrorBoundary extends Component<{ children: React.ReactNode; name: st
|
|
|
21
22
|
return this.props.children;
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
|
|
26
|
+
function SectionHeader(props: { label: string }) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="flex items-center gap-3 pt-4 pb-1">
|
|
29
|
+
<div className="h-px flex-1 bg-base-content/6" />
|
|
30
|
+
<span className="text-[9px] font-mono font-bold uppercase tracking-[0.15em] text-base-content/20">{props.label}</span>
|
|
31
|
+
<div className="h-px flex-1 bg-base-content/6" />
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
import { PeriodSelector } from "./PeriodSelector";
|
|
25
37
|
import { ChartCard } from "./ChartCard";
|
|
38
|
+
import { QuickStats } from "./QuickStats";
|
|
26
39
|
import { CostAreaChart } from "./charts/CostAreaChart";
|
|
27
40
|
import { CumulativeCostChart } from "./charts/CumulativeCostChart";
|
|
28
41
|
import { CostDonutChart } from "./charts/CostDonutChart";
|
|
@@ -58,7 +71,18 @@ export function AnalyticsView() {
|
|
|
58
71
|
|
|
59
72
|
<div className="flex-1 overflow-y-auto px-6 py-4">
|
|
60
73
|
{analytics.loading && !analytics.data && (
|
|
61
|
-
<div className="
|
|
74
|
+
<div className="flex flex-col gap-4 max-w-[1200px] mx-auto">
|
|
75
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
76
|
+
{[0, 1, 2, 3].map(function (i) {
|
|
77
|
+
return <div key={i} className="h-24 rounded-xl bg-base-content/[0.03] animate-pulse" />;
|
|
78
|
+
})}
|
|
79
|
+
</div>
|
|
80
|
+
<div className="h-[240px] rounded-xl bg-base-content/[0.03] animate-pulse" />
|
|
81
|
+
<div className="grid grid-cols-2 gap-4">
|
|
82
|
+
<div className="h-[240px] rounded-xl bg-base-content/[0.03] animate-pulse" />
|
|
83
|
+
<div className="h-[240px] rounded-xl bg-base-content/[0.03] animate-pulse" />
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
62
86
|
)}
|
|
63
87
|
|
|
64
88
|
{analytics.error && (
|
|
@@ -67,6 +91,10 @@ export function AnalyticsView() {
|
|
|
67
91
|
|
|
68
92
|
{analytics.data && (
|
|
69
93
|
<div className="flex flex-col gap-4 max-w-[1200px] mx-auto pb-8">
|
|
94
|
+
<QuickStats />
|
|
95
|
+
|
|
96
|
+
<SectionHeader label="Cost" />
|
|
97
|
+
|
|
70
98
|
<ChartCard title="Cost Over Time">
|
|
71
99
|
<CostAreaChart data={analytics.data.costOverTime} />
|
|
72
100
|
</ChartCard>
|
|
@@ -89,6 +117,8 @@ export function AnalyticsView() {
|
|
|
89
117
|
</ChartCard>
|
|
90
118
|
</div>
|
|
91
119
|
|
|
120
|
+
<SectionHeader label="Tokens & Performance" />
|
|
121
|
+
|
|
92
122
|
<ChartCard title="Token Flow">
|
|
93
123
|
<ChartErrorBoundary name="TokenFlow">
|
|
94
124
|
<TokenFlowChart data={analytics.data.tokensOverTime} />
|
|
@@ -121,6 +151,8 @@ export function AnalyticsView() {
|
|
|
121
151
|
</ChartCard>
|
|
122
152
|
</div>
|
|
123
153
|
|
|
154
|
+
<SectionHeader label="Activity" />
|
|
155
|
+
|
|
124
156
|
<ChartCard title="Activity Calendar">
|
|
125
157
|
<ChartErrorBoundary name="Calendar">
|
|
126
158
|
<ActivityCalendar data={analytics.data.activityCalendar} />
|
|
@@ -146,6 +178,8 @@ export function AnalyticsView() {
|
|
|
146
178
|
</ChartErrorBoundary>
|
|
147
179
|
</ChartCard>
|
|
148
180
|
|
|
181
|
+
<SectionHeader label="Tools & Projects" />
|
|
182
|
+
|
|
149
183
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
150
184
|
<ChartCard title="Tool Usage (Treemap)">
|
|
151
185
|
<ChartErrorBoundary name="Treemap">
|
|
@@ -178,6 +212,8 @@ export function AnalyticsView() {
|
|
|
178
212
|
</ChartErrorBoundary>
|
|
179
213
|
</ChartCard>
|
|
180
214
|
|
|
215
|
+
<SectionHeader label="Fleet" />
|
|
216
|
+
|
|
181
217
|
<ChartCard title="Node Fleet">
|
|
182
218
|
<ChartErrorBoundary name="Fleet">
|
|
183
219
|
<NodeFleetOverview nodes={nodes} />
|
|
@@ -187,7 +223,13 @@ export function AnalyticsView() {
|
|
|
187
223
|
)}
|
|
188
224
|
|
|
189
225
|
{!analytics.loading && !analytics.error && !analytics.data && (
|
|
190
|
-
<div className="
|
|
226
|
+
<div className="flex flex-col items-center justify-center py-24 gap-4">
|
|
227
|
+
<BarChart3 size={40} className="text-base-content/10" />
|
|
228
|
+
<div className="text-center">
|
|
229
|
+
<p className="text-[14px] font-mono text-base-content/40">No analytics data yet</p>
|
|
230
|
+
<p className="text-[12px] text-base-content/25 mt-1">Start a Claude session to begin tracking usage</p>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
191
233
|
)}
|
|
192
234
|
</div>
|
|
193
235
|
</div>
|
|
@@ -2,12 +2,44 @@ import { useState, useEffect, useRef, createContext, useContext } from "react";
|
|
|
2
2
|
import { Maximize2, Minimize2 } from "lucide-react";
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
4
|
|
|
5
|
-
var ChartFullscreenContext = createContext(false);
|
|
5
|
+
var ChartFullscreenContext = createContext<number | false>(false);
|
|
6
6
|
|
|
7
|
-
export function useChartFullscreen():
|
|
7
|
+
export function useChartFullscreen(): number | false {
|
|
8
8
|
return useContext(ChartFullscreenContext);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function FullscreenChartArea(props: { children: ReactNode; ready: boolean }) {
|
|
12
|
+
var containerRef = useRef<HTMLDivElement>(null);
|
|
13
|
+
var [height, setHeight] = useState(0);
|
|
14
|
+
|
|
15
|
+
useEffect(function () {
|
|
16
|
+
if (!props.ready || !containerRef.current) return;
|
|
17
|
+
var h = containerRef.current.clientHeight;
|
|
18
|
+
if (h > 0) setHeight(h - 48);
|
|
19
|
+
|
|
20
|
+
var observer = new ResizeObserver(function (entries) {
|
|
21
|
+
for (var i = 0; i < entries.length; i++) {
|
|
22
|
+
var newH = entries[i].contentRect.height;
|
|
23
|
+
if (newH > 0) setHeight(newH - 48);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
observer.observe(containerRef.current);
|
|
27
|
+
return function () { observer.disconnect(); };
|
|
28
|
+
}, [props.ready]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div ref={containerRef} className="flex-1 p-6 overflow-auto min-h-0">
|
|
32
|
+
{height > 0 ? (
|
|
33
|
+
<ChartFullscreenContext.Provider value={height}>
|
|
34
|
+
{props.children}
|
|
35
|
+
</ChartFullscreenContext.Provider>
|
|
36
|
+
) : (
|
|
37
|
+
<div className="flex items-center justify-center h-full text-base-content/20 font-mono text-[11px]">Expanding...</div>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
11
43
|
interface ChartCardProps {
|
|
12
44
|
title: string;
|
|
13
45
|
children: ReactNode;
|
|
@@ -77,7 +109,7 @@ export function ChartCard(props: ChartCardProps) {
|
|
|
77
109
|
openFullscreen();
|
|
78
110
|
}
|
|
79
111
|
}}
|
|
80
|
-
className="text-base-content/20 hover:text-base-content/50 transition-
|
|
112
|
+
className="opacity-0 group-hover:opacity-100 text-base-content/20 hover:text-base-content/50 transition-all duration-200 cursor-pointer p-0.5 rounded hover:bg-base-content/5"
|
|
81
113
|
aria-label={isFullscreen ? "Exit fullscreen" : "Fullscreen"}
|
|
82
114
|
title={isFullscreen ? "Exit fullscreen (Esc)" : "Fullscreen"}
|
|
83
115
|
>
|
|
@@ -150,11 +182,9 @@ export function ChartCard(props: ChartCardProps) {
|
|
|
150
182
|
</button>
|
|
151
183
|
</div>
|
|
152
184
|
</div>
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
</ChartFullscreenContext.Provider>
|
|
157
|
-
</div>
|
|
185
|
+
<FullscreenChartArea ready={!animating}>
|
|
186
|
+
{props.children}
|
|
187
|
+
</FullscreenChartArea>
|
|
158
188
|
</div>
|
|
159
189
|
</div>
|
|
160
190
|
</>
|
|
@@ -164,7 +194,7 @@ export function ChartCard(props: ChartCardProps) {
|
|
|
164
194
|
return (
|
|
165
195
|
<div
|
|
166
196
|
ref={cardRef}
|
|
167
|
-
className={"rounded-xl border border-base-content/8 bg-base-300/50 p-4 " + (props.className || "")}
|
|
197
|
+
className={"group rounded-xl border border-base-content/8 bg-base-300/50 p-4 cursor-pointer hover:border-base-content/12 transition-all duration-200 " + (props.className || "")}
|
|
168
198
|
>
|
|
169
199
|
{cardContent}
|
|
170
200
|
</div>
|
|
@@ -37,13 +37,13 @@ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?:
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export function CacheEfficiencyChart({ data }: CacheEfficiencyChartProps) {
|
|
40
|
-
var
|
|
40
|
+
var fullscreenHeight = useChartFullscreen();
|
|
41
41
|
var displayData = data.map(function (d) {
|
|
42
42
|
return { date: d.date, rate: d.rate * 100 };
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
return (
|
|
46
|
-
<ResponsiveContainer width="100%" height={
|
|
46
|
+
<ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
|
|
47
47
|
<AreaChart data={displayData} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
48
48
|
<defs>
|
|
49
49
|
<linearGradient id="cacheEffGrad" x1="0" y1="0" x2="0" y2="1">
|
|
@@ -54,7 +54,7 @@ function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export function ContextUtilizationChart({ data }: ContextUtilizationChartProps) {
|
|
57
|
-
var
|
|
57
|
+
var fullscreenHeight = useChartFullscreen();
|
|
58
58
|
var sessionMap = new Map<string, { title: string; points: Array<{ messageIndex: number; contextPercent: number }> }>();
|
|
59
59
|
for (var i = 0; i < data.length; i++) {
|
|
60
60
|
var d = data[i];
|
|
@@ -86,7 +86,7 @@ export function ContextUtilizationChart({ data }: ContextUtilizationChartProps)
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
return (
|
|
89
|
-
<ResponsiveContainer width="100%" height={
|
|
89
|
+
<ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
|
|
90
90
|
<LineChart data={merged} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
91
91
|
<CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} vertical={false} />
|
|
92
92
|
<XAxis dataKey="messageIndex" tick={TICK_STYLE} axisLine={false} tickLine={false} />
|
|
@@ -49,9 +49,9 @@ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?:
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export function CostAreaChart({ data }: CostAreaChartProps) {
|
|
52
|
-
var
|
|
52
|
+
var fullscreenHeight = useChartFullscreen();
|
|
53
53
|
return (
|
|
54
|
-
<ResponsiveContainer width="100%" height={
|
|
54
|
+
<ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
|
|
55
55
|
<AreaChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
56
56
|
<defs>
|
|
57
57
|
<linearGradient id="opusGrad" x1="0" y1="0" x2="0" y2="1">
|
|
@@ -37,9 +37,9 @@ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?:
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export function CostDistributionChart({ data }: CostDistributionChartProps) {
|
|
40
|
-
var
|
|
40
|
+
var fullscreenHeight = useChartFullscreen();
|
|
41
41
|
return (
|
|
42
|
-
<ResponsiveContainer width="100%" height={
|
|
42
|
+
<ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
|
|
43
43
|
<AreaChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
44
44
|
<defs>
|
|
45
45
|
<linearGradient id="distGrad" x1="0" y1="0" x2="0" y2="1">
|
|
@@ -53,10 +53,10 @@ function CenterLabel({ totalCost }: { totalCost: number }) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export function CostDonutChart({ modelUsage, totalCost }: CostDonutChartProps) {
|
|
56
|
-
var
|
|
56
|
+
var fullscreenHeight = useChartFullscreen();
|
|
57
57
|
return (
|
|
58
58
|
<div>
|
|
59
|
-
<ResponsiveContainer width="100%" height={
|
|
59
|
+
<ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
|
|
60
60
|
<PieChart>
|
|
61
61
|
<Pie
|
|
62
62
|
data={modelUsage}
|
|
@@ -37,9 +37,9 @@ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?:
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export function CumulativeCostChart({ data }: CumulativeCostChartProps) {
|
|
40
|
-
var
|
|
40
|
+
var fullscreenHeight = useChartFullscreen();
|
|
41
41
|
return (
|
|
42
|
-
<ResponsiveContainer width="100%" height={
|
|
42
|
+
<ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
|
|
43
43
|
<AreaChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
44
44
|
<defs>
|
|
45
45
|
<linearGradient id="cumulativeGrad" x1="0" y1="0" x2="0" y2="1">
|
|
@@ -50,7 +50,7 @@ function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export function ResponseTimeScatter({ data }: ResponseTimeScatterProps) {
|
|
53
|
-
var
|
|
53
|
+
var fullscreenHeight = useChartFullscreen();
|
|
54
54
|
var models = Array.from(new Set(data.map(function (d) { return d.model; })));
|
|
55
55
|
|
|
56
56
|
var byModel = models.map(function (model) {
|
|
@@ -64,7 +64,7 @@ export function ResponseTimeScatter({ data }: ResponseTimeScatterProps) {
|
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
return (
|
|
67
|
-
<ResponsiveContainer width="100%" height={
|
|
67
|
+
<ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
|
|
68
68
|
<ScatterChart margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
69
69
|
<CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} />
|
|
70
70
|
<XAxis
|
|
@@ -63,7 +63,7 @@ function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
export function SessionBubbleChart({ data }: SessionBubbleChartProps) {
|
|
66
|
-
var
|
|
66
|
+
var fullscreenHeight = useChartFullscreen();
|
|
67
67
|
var projects = Array.from(new Set(data.map(function (d) { return d.project; })));
|
|
68
68
|
|
|
69
69
|
function getColor(project: string): string {
|
|
@@ -85,7 +85,7 @@ export function SessionBubbleChart({ data }: SessionBubbleChartProps) {
|
|
|
85
85
|
var maxTs = Math.max(...data.map(function (d) { return d.timestamp; }));
|
|
86
86
|
|
|
87
87
|
return (
|
|
88
|
-
<ResponsiveContainer width="100%" height={
|
|
88
|
+
<ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
|
|
89
89
|
<ScatterChart margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
90
90
|
<CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} />
|
|
91
91
|
<XAxis
|
|
@@ -53,9 +53,9 @@ function formatTokens(v: number): string {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export function TokenFlowChart({ data }: TokenFlowChartProps) {
|
|
56
|
-
var
|
|
56
|
+
var fullscreenHeight = useChartFullscreen();
|
|
57
57
|
return (
|
|
58
|
-
<ResponsiveContainer width="100%" height={
|
|
58
|
+
<ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
|
|
59
59
|
<AreaChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
60
60
|
<defs>
|
|
61
61
|
<linearGradient id="inputGrad" x1="0" y1="0" x2="0" y2="1">
|
|
@@ -65,7 +65,7 @@ function SankeyNode({ x, y, width, height, index, payload }: { x: number; y: num
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
export function TokenSankeyChart({ data }: TokenSankeyChartProps) {
|
|
68
|
-
var
|
|
68
|
+
var fullscreenHeight = useChartFullscreen();
|
|
69
69
|
if (!data.links || data.links.length === 0) {
|
|
70
70
|
return (
|
|
71
71
|
<div className="flex items-center justify-center h-[250px] text-base-content/30 font-mono text-[12px]">
|
|
@@ -75,7 +75,7 @@ export function TokenSankeyChart({ data }: TokenSankeyChartProps) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
return (
|
|
78
|
-
<ResponsiveContainer width="100%" height={
|
|
78
|
+
<ResponsiveContainer width="100%" height={fullscreenHeight || 250}>
|
|
79
79
|
<Sankey
|
|
80
80
|
data={data}
|
|
81
81
|
node={<SankeyNode x={0} y={0} width={0} height={0} index={0} payload={{ name: "" }} />}
|
|
@@ -77,7 +77,7 @@ function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export function ToolTreemap({ data }: ToolTreemapProps) {
|
|
80
|
-
var
|
|
80
|
+
var fullscreenHeight = useChartFullscreen();
|
|
81
81
|
if (!data || data.length === 0) {
|
|
82
82
|
return (
|
|
83
83
|
<div className="flex items-center justify-center h-[250px] text-base-content/25 font-mono text-[11px]">
|
|
@@ -96,7 +96,7 @@ export function ToolTreemap({ data }: ToolTreemapProps) {
|
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
return (
|
|
99
|
-
<ResponsiveContainer width="100%" height={
|
|
99
|
+
<ResponsiveContainer width="100%" height={fullscreenHeight || 250}>
|
|
100
100
|
<Treemap
|
|
101
101
|
data={treemapData}
|
|
102
102
|
dataKey="size"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.2",
|
|
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>",
|