@cryptiklemur/lattice 1.22.3 → 1.23.1
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 +39 -25
- package/client/src/components/analytics/ChartCard.tsx +17 -13
- package/client/src/components/analytics/QuickStats.tsx +31 -12
- package/client/src/components/analytics/charts/ActivityCalendar.tsx +1 -1
- package/client/src/components/analytics/charts/DailySummaryCards.tsx +1 -1
- package/client/src/components/analytics/charts/HourlyHeatmap.tsx +1 -1
- package/client/src/components/analytics/charts/SessionTimeline.tsx +1 -1
- package/client/src/components/sidebar/Sidebar.tsx +17 -1
- package/package.json +1 -1
|
@@ -3,19 +3,25 @@ import { BarChart3 } from "lucide-react";
|
|
|
3
3
|
import { useAnalytics } from "../../hooks/useAnalytics";
|
|
4
4
|
import { useMesh } from "../../hooks/useMesh";
|
|
5
5
|
|
|
6
|
-
class ChartErrorBoundary extends Component<{ children: React.ReactNode; name: string }, {
|
|
6
|
+
class ChartErrorBoundary extends Component<{ children: React.ReactNode; name: string }, { hasError: boolean }> {
|
|
7
7
|
constructor(props: { children: React.ReactNode; name: string }) {
|
|
8
8
|
super(props);
|
|
9
|
-
this.state = {
|
|
9
|
+
this.state = { hasError: false };
|
|
10
10
|
}
|
|
11
|
-
static getDerivedStateFromError(
|
|
12
|
-
return {
|
|
11
|
+
static getDerivedStateFromError() {
|
|
12
|
+
return { hasError: true };
|
|
13
13
|
}
|
|
14
14
|
render() {
|
|
15
|
-
if (this.state.
|
|
15
|
+
if (this.state.hasError) {
|
|
16
16
|
return (
|
|
17
|
-
<div className="flex items-center justify-center h-[200px]
|
|
18
|
-
|
|
17
|
+
<div className="flex flex-col items-center justify-center h-[200px] gap-3">
|
|
18
|
+
<span className="text-base-content/30 font-mono text-[12px]">Unable to load chart</span>
|
|
19
|
+
<button
|
|
20
|
+
onClick={() => this.setState({ hasError: false })}
|
|
21
|
+
className="text-[11px] font-mono text-primary/60 hover:text-primary transition-colors cursor-pointer px-3 py-1 rounded-md border border-primary/20 hover:border-primary/40"
|
|
22
|
+
>
|
|
23
|
+
Retry
|
|
24
|
+
</button>
|
|
19
25
|
</div>
|
|
20
26
|
);
|
|
21
27
|
}
|
|
@@ -26,9 +32,9 @@ class ChartErrorBoundary extends Component<{ children: React.ReactNode; name: st
|
|
|
26
32
|
function SectionHeader(props: { label: string }) {
|
|
27
33
|
return (
|
|
28
34
|
<div className="flex items-center gap-3 pt-4 pb-1">
|
|
29
|
-
<div className="h-px flex-1 bg-base-content/
|
|
30
|
-
<span className="text-[9px] font-mono font-bold uppercase tracking-[0.15em] text-base-content/
|
|
31
|
-
<div className="h-px flex-1 bg-base-content/
|
|
35
|
+
<div className="h-px flex-1 bg-base-content/10" />
|
|
36
|
+
<span className="text-[9px] font-mono font-bold uppercase tracking-[0.15em] text-base-content/40">{props.label}</span>
|
|
37
|
+
<div className="h-px flex-1 bg-base-content/10" />
|
|
32
38
|
</div>
|
|
33
39
|
);
|
|
34
40
|
}
|
|
@@ -55,9 +61,9 @@ import { SessionComplexityList } from "./charts/SessionComplexityList";
|
|
|
55
61
|
import { NodeFleetOverview } from "./charts/NodeFleetOverview";
|
|
56
62
|
|
|
57
63
|
export function AnalyticsView() {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
const analytics = useAnalytics();
|
|
65
|
+
const mesh = useMesh();
|
|
66
|
+
const nodes = mesh.nodes;
|
|
61
67
|
|
|
62
68
|
return (
|
|
63
69
|
<div className="flex flex-col h-full overflow-hidden bg-base-100 bg-lattice-grid">
|
|
@@ -98,24 +104,34 @@ export function AnalyticsView() {
|
|
|
98
104
|
<SectionHeader label="Cost" />
|
|
99
105
|
|
|
100
106
|
<ChartCard title="Cost Over Time">
|
|
101
|
-
<
|
|
107
|
+
<ChartErrorBoundary name="CostArea">
|
|
108
|
+
<CostAreaChart data={analytics.data.costOverTime} />
|
|
109
|
+
</ChartErrorBoundary>
|
|
102
110
|
</ChartCard>
|
|
103
111
|
|
|
104
112
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
105
113
|
<ChartCard title="Cost Breakdown">
|
|
106
|
-
<
|
|
114
|
+
<ChartErrorBoundary name="CostDonut">
|
|
115
|
+
<CostDonutChart modelUsage={analytics.data.modelUsage} totalCost={analytics.data.totalCost} />
|
|
116
|
+
</ChartErrorBoundary>
|
|
107
117
|
</ChartCard>
|
|
108
118
|
<ChartCard title="Cumulative Cost">
|
|
109
|
-
<
|
|
119
|
+
<ChartErrorBoundary name="CumulativeCost">
|
|
120
|
+
<CumulativeCostChart data={analytics.data.cumulativeCost} />
|
|
121
|
+
</ChartErrorBoundary>
|
|
110
122
|
</ChartCard>
|
|
111
123
|
</div>
|
|
112
124
|
|
|
113
125
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
114
126
|
<ChartCard title="Cost Distribution">
|
|
115
|
-
<
|
|
127
|
+
<ChartErrorBoundary name="CostDistribution">
|
|
128
|
+
<CostDistributionChart data={analytics.data.costDistribution} />
|
|
129
|
+
</ChartErrorBoundary>
|
|
116
130
|
</ChartCard>
|
|
117
131
|
<ChartCard title="Session Costs">
|
|
118
|
-
<
|
|
132
|
+
<ChartErrorBoundary name="SessionBubble">
|
|
133
|
+
<SessionBubbleChart data={analytics.data.sessionBubbles} />
|
|
134
|
+
</ChartErrorBoundary>
|
|
119
135
|
</ChartCard>
|
|
120
136
|
</div>
|
|
121
137
|
|
|
@@ -182,13 +198,11 @@ export function AnalyticsView() {
|
|
|
182
198
|
|
|
183
199
|
<SectionHeader label="Projects" />
|
|
184
200
|
|
|
185
|
-
<
|
|
186
|
-
<
|
|
187
|
-
<
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
</ChartCard>
|
|
191
|
-
</div>
|
|
201
|
+
<ChartCard title="Project Comparison">
|
|
202
|
+
<ChartErrorBoundary name="Radar">
|
|
203
|
+
<ProjectRadar data={analytics.data.projectRadar} />
|
|
204
|
+
</ChartErrorBoundary>
|
|
205
|
+
</ChartCard>
|
|
192
206
|
|
|
193
207
|
<ChartCard title="Session Complexity">
|
|
194
208
|
<ChartErrorBoundary name="Complexity">
|
|
@@ -3,14 +3,14 @@ import { useFocusTrap } from "../../hooks/useFocusTrap";
|
|
|
3
3
|
import { Maximize2, Minimize2 } from "lucide-react";
|
|
4
4
|
import type { ReactNode } from "react";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const ChartFullscreenContext = createContext<number | false>(false);
|
|
7
7
|
|
|
8
8
|
export function useChartFullscreen(): number | false {
|
|
9
9
|
return useContext(ChartFullscreenContext);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function useViewportChartHeight(): number {
|
|
13
|
-
|
|
13
|
+
const [h, setH] = useState(Math.round(window.innerHeight * 0.5));
|
|
14
14
|
useEffect(function () {
|
|
15
15
|
function onResize() { setH(Math.round(window.innerHeight * 0.5)); }
|
|
16
16
|
window.addEventListener("resize", onResize);
|
|
@@ -27,13 +27,13 @@ interface ChartCardProps {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function ChartCard(props: ChartCardProps) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
31
|
+
const chartHeight = useViewportChartHeight();
|
|
32
|
+
const cardRef = useRef<HTMLDivElement>(null);
|
|
33
|
+
const [originRect, setOriginRect] = useState<DOMRect | null>(null);
|
|
34
|
+
const [animating, setAnimating] = useState(false);
|
|
35
|
+
const fullscreenModalRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
const closeFullscreenCb = useCallback(function () { closeFullscreen(); }, []);
|
|
37
37
|
useFocusTrap(fullscreenModalRef, closeFullscreenCb, isFullscreen);
|
|
38
38
|
|
|
39
39
|
function openFullscreen() {
|
|
@@ -67,7 +67,7 @@ export function ChartCard(props: ChartCardProps) {
|
|
|
67
67
|
return function () { document.body.style.overflow = ""; };
|
|
68
68
|
}, [isFullscreen]);
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
const cardContent = (
|
|
71
71
|
<>
|
|
72
72
|
<div className="flex items-center justify-between mb-4">
|
|
73
73
|
<span className="text-[10px] font-mono font-bold uppercase tracking-widest text-base-content/35">
|
|
@@ -91,17 +91,19 @@ export function ChartCard(props: ChartCardProps) {
|
|
|
91
91
|
</button>
|
|
92
92
|
</div>
|
|
93
93
|
</div>
|
|
94
|
-
{props.
|
|
94
|
+
<div role="img" aria-label={props.title}>
|
|
95
|
+
{props.children}
|
|
96
|
+
</div>
|
|
95
97
|
</>
|
|
96
98
|
);
|
|
97
99
|
|
|
98
100
|
if (isFullscreen) {
|
|
99
|
-
|
|
101
|
+
const overlayStyle: React.CSSProperties = {
|
|
100
102
|
transition: "opacity 250ms cubic-bezier(0.4, 0, 0.2, 1)",
|
|
101
103
|
opacity: animating ? 0 : 1,
|
|
102
104
|
};
|
|
103
105
|
|
|
104
|
-
|
|
106
|
+
const modalStyle: React.CSSProperties = {
|
|
105
107
|
transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1)",
|
|
106
108
|
};
|
|
107
109
|
|
|
@@ -118,6 +120,7 @@ export function ChartCard(props: ChartCardProps) {
|
|
|
118
120
|
<>
|
|
119
121
|
<div
|
|
120
122
|
ref={cardRef}
|
|
123
|
+
aria-label={props.title}
|
|
121
124
|
className={"rounded-xl border border-base-content/8 bg-base-300/50 p-4 invisible " + (props.className || "")}
|
|
122
125
|
>
|
|
123
126
|
{cardContent}
|
|
@@ -176,6 +179,7 @@ export function ChartCard(props: ChartCardProps) {
|
|
|
176
179
|
return (
|
|
177
180
|
<div
|
|
178
181
|
ref={cardRef}
|
|
182
|
+
aria-label={props.title}
|
|
179
183
|
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 || "")}
|
|
180
184
|
>
|
|
181
185
|
{cardContent}
|
|
@@ -31,7 +31,7 @@ function Sparkline({ data, stroke }: SparklineProps) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export function QuickStats() {
|
|
34
|
-
|
|
34
|
+
const analytics = useAnalytics();
|
|
35
35
|
|
|
36
36
|
if (!analytics.data) {
|
|
37
37
|
return (
|
|
@@ -48,22 +48,26 @@ export function QuickStats() {
|
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const d = analytics.data;
|
|
52
|
+
const colors = getChartColors();
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
const costSparkData = d.costOverTime.slice(-7).map(function (e: typeof d.costOverTime[number]) { return { v: e.total }; });
|
|
55
|
+
const sessionsSparkData = d.sessionsOverTime.slice(-7).map(function (e: typeof d.sessionsOverTime[number]) { return { v: e.count }; });
|
|
56
|
+
const tokensSparkData = d.tokensOverTime.slice(-7).map(function (e: typeof d.tokensOverTime[number]) { return { v: e.input + e.output }; });
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
const totalTokens = d.totalTokens.input + d.totalTokens.output;
|
|
59
|
+
const cacheHitPct = Math.round(d.cacheHitRate * 100);
|
|
60
60
|
|
|
61
61
|
return (
|
|
62
62
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
63
63
|
<div className="bg-base-content/[0.03] border border-base-content/8 rounded-xl p-3.5">
|
|
64
64
|
<div className="flex items-center justify-between mb-1">
|
|
65
65
|
<span className="text-[10px] font-mono font-semibold uppercase tracking-wider text-base-content/35">Cost</span>
|
|
66
|
-
{costSparkData.length > 1 &&
|
|
66
|
+
{costSparkData.length > 1 && (
|
|
67
|
+
<div role="img" aria-label={"Cost trend: " + (costSparkData[costSparkData.length - 1].v > costSparkData[0].v ? "increasing" : costSparkData[costSparkData.length - 1].v < costSparkData[0].v ? "decreasing" : "stable")}>
|
|
68
|
+
<Sparkline data={costSparkData} stroke={colors.primary} />
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
67
71
|
</div>
|
|
68
72
|
<div className="text-[22px] font-mono text-base-content/85">${d.totalCost.toFixed(2)}</div>
|
|
69
73
|
</div>
|
|
@@ -71,7 +75,11 @@ export function QuickStats() {
|
|
|
71
75
|
<div className="bg-base-content/[0.03] border border-base-content/8 rounded-xl p-3.5">
|
|
72
76
|
<div className="flex items-center justify-between mb-1">
|
|
73
77
|
<span className="text-[10px] font-mono font-semibold uppercase tracking-wider text-base-content/35">Sessions</span>
|
|
74
|
-
{sessionsSparkData.length > 1 &&
|
|
78
|
+
{sessionsSparkData.length > 1 && (
|
|
79
|
+
<div role="img" aria-label={"Sessions trend: " + (sessionsSparkData[sessionsSparkData.length - 1].v > sessionsSparkData[0].v ? "increasing" : sessionsSparkData[sessionsSparkData.length - 1].v < sessionsSparkData[0].v ? "decreasing" : "stable")}>
|
|
80
|
+
<Sparkline data={sessionsSparkData} stroke={colors.success} />
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
75
83
|
</div>
|
|
76
84
|
<div className="text-[22px] font-mono text-base-content/85">{d.totalSessions}</div>
|
|
77
85
|
</div>
|
|
@@ -79,7 +87,11 @@ export function QuickStats() {
|
|
|
79
87
|
<div className="bg-base-content/[0.03] border border-base-content/8 rounded-xl p-3.5">
|
|
80
88
|
<div className="flex items-center justify-between mb-1">
|
|
81
89
|
<span className="text-[10px] font-mono font-semibold uppercase tracking-wider text-base-content/35">Tokens</span>
|
|
82
|
-
{tokensSparkData.length > 1 &&
|
|
90
|
+
{tokensSparkData.length > 1 && (
|
|
91
|
+
<div role="img" aria-label={"Tokens trend: " + (tokensSparkData[tokensSparkData.length - 1].v > tokensSparkData[0].v ? "increasing" : tokensSparkData[tokensSparkData.length - 1].v < tokensSparkData[0].v ? "decreasing" : "stable")}>
|
|
92
|
+
<Sparkline data={tokensSparkData} stroke={colors.warning} />
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
83
95
|
</div>
|
|
84
96
|
<div className="text-[22px] font-mono text-base-content/85">{formatTokens(totalTokens)}</div>
|
|
85
97
|
</div>
|
|
@@ -89,7 +101,14 @@ export function QuickStats() {
|
|
|
89
101
|
<span className="text-[10px] font-mono font-semibold uppercase tracking-wider text-base-content/35">Cache Hit</span>
|
|
90
102
|
</div>
|
|
91
103
|
<div className="text-[22px] font-mono text-base-content/85 mb-2">{cacheHitPct}%</div>
|
|
92
|
-
<div
|
|
104
|
+
<div
|
|
105
|
+
className="w-full h-1 rounded-full bg-base-content/10 overflow-hidden"
|
|
106
|
+
role="progressbar"
|
|
107
|
+
aria-valuenow={cacheHitPct}
|
|
108
|
+
aria-valuemin={0}
|
|
109
|
+
aria-valuemax={100}
|
|
110
|
+
aria-label="Cache hit rate"
|
|
111
|
+
>
|
|
93
112
|
<div
|
|
94
113
|
className="h-full rounded-full bg-primary transition-all duration-300"
|
|
95
114
|
style={{ width: cacheHitPct + "%" }}
|
|
@@ -59,7 +59,7 @@ export function ActivityCalendar({ data }: ActivityCalendarProps) {
|
|
|
59
59
|
if (!data || data.length === 0) {
|
|
60
60
|
return (
|
|
61
61
|
<div className="flex items-center justify-center h-[120px] text-base-content/25 font-mono text-[11px]">
|
|
62
|
-
No data
|
|
62
|
+
No data for this period
|
|
63
63
|
</div>
|
|
64
64
|
);
|
|
65
65
|
}
|
|
@@ -30,7 +30,7 @@ export function DailySummaryCards({ data }: DailySummaryCardsProps) {
|
|
|
30
30
|
if (!data || data.length === 0) {
|
|
31
31
|
return (
|
|
32
32
|
<div className="flex items-center justify-center h-[100px] text-base-content/25 font-mono text-[11px]">
|
|
33
|
-
No data
|
|
33
|
+
No data for this period
|
|
34
34
|
</div>
|
|
35
35
|
);
|
|
36
36
|
}
|
|
@@ -29,7 +29,7 @@ export function HourlyHeatmap({ data }: HourlyHeatmapProps) {
|
|
|
29
29
|
if (!data || data.length === 0) {
|
|
30
30
|
return (
|
|
31
31
|
<div className="flex items-center justify-center h-[200px] text-base-content/25 font-mono text-[11px]">
|
|
32
|
-
No data
|
|
32
|
+
No data for this period
|
|
33
33
|
</div>
|
|
34
34
|
);
|
|
35
35
|
}
|
|
@@ -33,7 +33,7 @@ export function SessionTimeline({ data }: SessionTimelineProps) {
|
|
|
33
33
|
if (!data || data.length === 0) {
|
|
34
34
|
return (
|
|
35
35
|
<div className="flex items-center justify-center h-[200px] text-base-content/25 font-mono text-[11px]">
|
|
36
|
-
No data
|
|
36
|
+
No data for this period
|
|
37
37
|
</div>
|
|
38
38
|
);
|
|
39
39
|
}
|
|
@@ -10,7 +10,7 @@ import { useSidebar } from "../../hooks/useSidebar";
|
|
|
10
10
|
import { useSession } from "../../hooks/useSession";
|
|
11
11
|
import { clearSession } from "../../stores/session";
|
|
12
12
|
import { useOnline } from "../../hooks/useOnline";
|
|
13
|
-
import { openTab, openSessionTab, getWorkspaceStore } from "../../stores/workspace";
|
|
13
|
+
import { openTab, openSessionTab, closeTab, getWorkspaceStore } from "../../stores/workspace";
|
|
14
14
|
import { getSidebarStore, goToAnalytics, openSettings } from "../../stores/sidebar";
|
|
15
15
|
import { setAnalyticsScope } from "../../stores/analytics";
|
|
16
16
|
import { ProjectRail } from "./ProjectRail";
|
|
@@ -269,6 +269,22 @@ export function Sidebar({ onSessionSelect }: { onSessionSelect?: () => void }) {
|
|
|
269
269
|
</div>
|
|
270
270
|
<div className="flex-1 overflow-auto px-4 py-3 pb-16">
|
|
271
271
|
<div className="flex flex-col gap-0.5 mb-3">
|
|
272
|
+
<button
|
|
273
|
+
type="button"
|
|
274
|
+
onClick={function () {
|
|
275
|
+
var store = getWorkspaceStore();
|
|
276
|
+
var state = store.state;
|
|
277
|
+
var activePane = state.panes.find(function (p) { return p.id === state.activePaneId; });
|
|
278
|
+
var activeTab = activePane ? state.tabs.find(function (t) { return t.id === activePane!.activeTabId; }) : null;
|
|
279
|
+
if (activeTab && activeTab.id !== "chat") {
|
|
280
|
+
closeTab(activeTab.id);
|
|
281
|
+
}
|
|
282
|
+
}}
|
|
283
|
+
className="flex items-center gap-2 w-full px-2 py-1.5 rounded-lg text-[11px] text-base-content/40 hover:text-base-content/70 hover:bg-base-300/30 transition-colors"
|
|
284
|
+
>
|
|
285
|
+
<LayoutDashboard size={12} />
|
|
286
|
+
<span className="font-mono tracking-wide">Dashboard</span>
|
|
287
|
+
</button>
|
|
272
288
|
<button
|
|
273
289
|
type="button"
|
|
274
290
|
onClick={function () {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.1",
|
|
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>",
|