@cryptiklemur/lattice 1.5.0 → 1.7.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 +87 -0
- package/client/src/components/analytics/charts/ActivityCalendar.tsx +185 -0
- package/client/src/components/analytics/charts/CacheEfficiencyChart.tsx +60 -0
- package/client/src/components/analytics/charts/ContextUtilizationChart.tsx +110 -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/ResponseTimeScatter.tsx +101 -0
- package/client/src/components/analytics/charts/SessionTimeline.tsx +112 -0
- package/client/src/components/analytics/charts/TokenFlowChart.tsx +82 -0
- package/client/src/components/analytics/charts/TokenSankeyChart.tsx +89 -0
- package/package.json +1 -1
- package/server/src/analytics/engine.ts +237 -0
- package/shared/src/analytics.ts +9 -0
|
@@ -1,4 +1,25 @@
|
|
|
1
|
+
import { Component } from "react";
|
|
1
2
|
import { useAnalytics } from "../../hooks/useAnalytics";
|
|
3
|
+
|
|
4
|
+
class ChartErrorBoundary extends Component<{ children: React.ReactNode; name: string }, { error: Error | null }> {
|
|
5
|
+
constructor(props: { children: React.ReactNode; name: string }) {
|
|
6
|
+
super(props);
|
|
7
|
+
this.state = { error: null };
|
|
8
|
+
}
|
|
9
|
+
static getDerivedStateFromError(error: Error) {
|
|
10
|
+
return { error: error };
|
|
11
|
+
}
|
|
12
|
+
render() {
|
|
13
|
+
if (this.state.error) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex items-center justify-center h-[200px] text-base-content/25 font-mono text-[11px]">
|
|
16
|
+
Chart error: {this.props.name}
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return this.props.children;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
2
23
|
import { PeriodSelector } from "./PeriodSelector";
|
|
3
24
|
import { ChartCard } from "./ChartCard";
|
|
4
25
|
import { CostAreaChart } from "./charts/CostAreaChart";
|
|
@@ -6,6 +27,15 @@ import { CumulativeCostChart } from "./charts/CumulativeCostChart";
|
|
|
6
27
|
import { CostDonutChart } from "./charts/CostDonutChart";
|
|
7
28
|
import { CostDistributionChart } from "./charts/CostDistributionChart";
|
|
8
29
|
import { SessionBubbleChart } from "./charts/SessionBubbleChart";
|
|
30
|
+
import { TokenFlowChart } from "./charts/TokenFlowChart";
|
|
31
|
+
import { CacheEfficiencyChart } from "./charts/CacheEfficiencyChart";
|
|
32
|
+
import { ResponseTimeScatter } from "./charts/ResponseTimeScatter";
|
|
33
|
+
import { ContextUtilizationChart } from "./charts/ContextUtilizationChart";
|
|
34
|
+
import { TokenSankeyChart } from "./charts/TokenSankeyChart";
|
|
35
|
+
import { ActivityCalendar } from "./charts/ActivityCalendar";
|
|
36
|
+
import { HourlyHeatmap } from "./charts/HourlyHeatmap";
|
|
37
|
+
import { SessionTimeline } from "./charts/SessionTimeline";
|
|
38
|
+
import { DailySummaryCards } from "./charts/DailySummaryCards";
|
|
9
39
|
|
|
10
40
|
export function AnalyticsView() {
|
|
11
41
|
var analytics = useAnalytics();
|
|
@@ -49,6 +79,63 @@ export function AnalyticsView() {
|
|
|
49
79
|
<SessionBubbleChart data={analytics.data.sessionBubbles} />
|
|
50
80
|
</ChartCard>
|
|
51
81
|
</div>
|
|
82
|
+
|
|
83
|
+
<ChartCard title="Token Flow">
|
|
84
|
+
<ChartErrorBoundary name="TokenFlow">
|
|
85
|
+
<TokenFlowChart data={analytics.data.tokensOverTime} />
|
|
86
|
+
</ChartErrorBoundary>
|
|
87
|
+
</ChartCard>
|
|
88
|
+
|
|
89
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
90
|
+
<ChartCard title="Cache Efficiency">
|
|
91
|
+
<ChartErrorBoundary name="CacheEfficiency">
|
|
92
|
+
<CacheEfficiencyChart data={analytics.data.cacheHitRateOverTime} />
|
|
93
|
+
</ChartErrorBoundary>
|
|
94
|
+
</ChartCard>
|
|
95
|
+
<ChartCard title="Response Time vs Tokens">
|
|
96
|
+
<ChartErrorBoundary name="ResponseTime">
|
|
97
|
+
<ResponseTimeScatter data={analytics.data.responseTimeData} />
|
|
98
|
+
</ChartErrorBoundary>
|
|
99
|
+
</ChartCard>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
103
|
+
<ChartCard title="Context Window Usage">
|
|
104
|
+
<ChartErrorBoundary name="ContextUtilization">
|
|
105
|
+
<ContextUtilizationChart data={analytics.data.contextUtilization} />
|
|
106
|
+
</ChartErrorBoundary>
|
|
107
|
+
</ChartCard>
|
|
108
|
+
<ChartCard title="Token Flow (Sankey)">
|
|
109
|
+
<ChartErrorBoundary name="Sankey">
|
|
110
|
+
<TokenSankeyChart data={analytics.data.tokenFlowSankey} />
|
|
111
|
+
</ChartErrorBoundary>
|
|
112
|
+
</ChartCard>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<ChartCard title="Activity Calendar">
|
|
116
|
+
<ChartErrorBoundary name="Calendar">
|
|
117
|
+
<ActivityCalendar data={analytics.data.activityCalendar} />
|
|
118
|
+
</ChartErrorBoundary>
|
|
119
|
+
</ChartCard>
|
|
120
|
+
|
|
121
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
122
|
+
<ChartCard title="Hourly Activity">
|
|
123
|
+
<ChartErrorBoundary name="HourlyHeatmap">
|
|
124
|
+
<HourlyHeatmap data={analytics.data.hourlyHeatmap} />
|
|
125
|
+
</ChartErrorBoundary>
|
|
126
|
+
</ChartCard>
|
|
127
|
+
<ChartCard title="Session Timeline">
|
|
128
|
+
<ChartErrorBoundary name="Timeline">
|
|
129
|
+
<SessionTimeline data={analytics.data.sessionTimeline} />
|
|
130
|
+
</ChartErrorBoundary>
|
|
131
|
+
</ChartCard>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<ChartCard title="Daily Summary">
|
|
135
|
+
<ChartErrorBoundary name="DailySummary">
|
|
136
|
+
<DailySummaryCards data={analytics.data.dailySummaries} />
|
|
137
|
+
</ChartErrorBoundary>
|
|
138
|
+
</ChartCard>
|
|
52
139
|
</div>
|
|
53
140
|
)}
|
|
54
141
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
interface CalendarDatum {
|
|
4
|
+
date: string;
|
|
5
|
+
count: number;
|
|
6
|
+
tokens: number;
|
|
7
|
+
cost: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ActivityCalendarProps {
|
|
11
|
+
data: CalendarDatum[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
var CELL_SIZE = 11;
|
|
15
|
+
var CELL_GAP = 2;
|
|
16
|
+
var CELL_STEP = CELL_SIZE + CELL_GAP;
|
|
17
|
+
var DAY_LABEL_WIDTH = 28;
|
|
18
|
+
var MONTH_LABEL_HEIGHT = 14;
|
|
19
|
+
|
|
20
|
+
var INTENSITY_OPACITIES = [0.05, 0.15, 0.3, 0.5, 0.8];
|
|
21
|
+
var PRIMARY_COLOR = "oklch(55% 0.25 280)";
|
|
22
|
+
|
|
23
|
+
var MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
24
|
+
var DAY_LABELS = [
|
|
25
|
+
{ index: 1, label: "Mon" },
|
|
26
|
+
{ index: 3, label: "Wed" },
|
|
27
|
+
{ index: 5, label: "Fri" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function getIntensity(count: number, maxCount: number): number {
|
|
31
|
+
if (count === 0 || maxCount === 0) return -1;
|
|
32
|
+
var ratio = count / maxCount;
|
|
33
|
+
if (ratio <= 0.2) return 0;
|
|
34
|
+
if (ratio <= 0.4) return 1;
|
|
35
|
+
if (ratio <= 0.6) return 2;
|
|
36
|
+
if (ratio <= 0.8) return 3;
|
|
37
|
+
return 4;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseDateParts(dateStr: string): { year: number; month: number; day: number } {
|
|
41
|
+
var parts = dateStr.split("-");
|
|
42
|
+
return { year: parseInt(parts[0], 10), month: parseInt(parts[1], 10) - 1, day: parseInt(parts[2], 10) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function ActivityCalendar({ data }: ActivityCalendarProps) {
|
|
46
|
+
var [hover, setHover] = useState<{ x: number; y: number; datum: CalendarDatum } | null>(null);
|
|
47
|
+
|
|
48
|
+
if (!data || data.length === 0) {
|
|
49
|
+
return (
|
|
50
|
+
<div className="flex items-center justify-center h-[120px] text-base-content/25 font-mono text-[11px]">
|
|
51
|
+
No data
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
var dataMap = new Map<string, CalendarDatum>();
|
|
57
|
+
var maxCount = 0;
|
|
58
|
+
for (var i = 0; i < data.length; i++) {
|
|
59
|
+
dataMap.set(data[i].date, data[i]);
|
|
60
|
+
if (data[i].count > maxCount) maxCount = data[i].count;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
var lastDate = new Date(data[data.length - 1].date + "T00:00:00");
|
|
64
|
+
var startDate = new Date(lastDate);
|
|
65
|
+
startDate.setDate(startDate.getDate() - 364);
|
|
66
|
+
var startDay = startDate.getDay();
|
|
67
|
+
if (startDay !== 0) {
|
|
68
|
+
startDate.setDate(startDate.getDate() - startDay);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var weeks: Array<Array<{ date: string; datum: CalendarDatum | undefined } | null>> = [];
|
|
72
|
+
var cursor = new Date(startDate);
|
|
73
|
+
var currentWeek: Array<{ date: string; datum: CalendarDatum | undefined } | null> = [];
|
|
74
|
+
|
|
75
|
+
while (cursor <= lastDate) {
|
|
76
|
+
var dow = cursor.getDay();
|
|
77
|
+
if (dow === 0 && currentWeek.length > 0) {
|
|
78
|
+
weeks.push(currentWeek);
|
|
79
|
+
currentWeek = [];
|
|
80
|
+
}
|
|
81
|
+
var key = cursor.getFullYear() + "-" + String(cursor.getMonth() + 1).padStart(2, "0") + "-" + String(cursor.getDate()).padStart(2, "0");
|
|
82
|
+
currentWeek.push({ date: key, datum: dataMap.get(key) });
|
|
83
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
84
|
+
}
|
|
85
|
+
if (currentWeek.length > 0) {
|
|
86
|
+
while (currentWeek.length < 7) currentWeek.push(null);
|
|
87
|
+
weeks.push(currentWeek);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
var monthLabels: Array<{ col: number; label: string }> = [];
|
|
91
|
+
var lastMonth = -1;
|
|
92
|
+
for (var wi = 0; wi < weeks.length; wi++) {
|
|
93
|
+
var firstCell = weeks[wi][0];
|
|
94
|
+
if (!firstCell) continue;
|
|
95
|
+
var parts = parseDateParts(firstCell.date);
|
|
96
|
+
if (parts.month !== lastMonth) {
|
|
97
|
+
monthLabels.push({ col: wi, label: MONTH_NAMES[parts.month] });
|
|
98
|
+
lastMonth = parts.month;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
var svgWidth = DAY_LABEL_WIDTH + weeks.length * CELL_STEP;
|
|
103
|
+
var svgHeight = MONTH_LABEL_HEIGHT + 7 * CELL_STEP;
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className="relative overflow-x-auto">
|
|
107
|
+
<svg width={svgWidth} height={svgHeight} className="block">
|
|
108
|
+
{monthLabels.map(function (ml, idx) {
|
|
109
|
+
return (
|
|
110
|
+
<text
|
|
111
|
+
key={idx}
|
|
112
|
+
x={DAY_LABEL_WIDTH + ml.col * CELL_STEP}
|
|
113
|
+
y={10}
|
|
114
|
+
className="fill-base-content/30"
|
|
115
|
+
style={{ fontSize: 10, fontFamily: "var(--font-mono)" }}
|
|
116
|
+
>
|
|
117
|
+
{ml.label}
|
|
118
|
+
</text>
|
|
119
|
+
);
|
|
120
|
+
})}
|
|
121
|
+
|
|
122
|
+
{DAY_LABELS.map(function (dl) {
|
|
123
|
+
return (
|
|
124
|
+
<text
|
|
125
|
+
key={dl.index}
|
|
126
|
+
x={0}
|
|
127
|
+
y={MONTH_LABEL_HEIGHT + dl.index * CELL_STEP + CELL_SIZE - 1}
|
|
128
|
+
className="fill-base-content/30"
|
|
129
|
+
style={{ fontSize: 10, fontFamily: "var(--font-mono)" }}
|
|
130
|
+
>
|
|
131
|
+
{dl.label}
|
|
132
|
+
</text>
|
|
133
|
+
);
|
|
134
|
+
})}
|
|
135
|
+
|
|
136
|
+
{weeks.map(function (week, col) {
|
|
137
|
+
return week.map(function (cell, row) {
|
|
138
|
+
if (!cell) return null;
|
|
139
|
+
var intensity = getIntensity(cell.datum?.count || 0, maxCount);
|
|
140
|
+
var x = DAY_LABEL_WIDTH + col * CELL_STEP;
|
|
141
|
+
var y = MONTH_LABEL_HEIGHT + row * CELL_STEP;
|
|
142
|
+
var opacity = intensity >= 0 ? INTENSITY_OPACITIES[intensity] : 0.05;
|
|
143
|
+
var fillColor = intensity >= 0 ? PRIMARY_COLOR : undefined;
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<rect
|
|
147
|
+
key={cell.date}
|
|
148
|
+
x={x}
|
|
149
|
+
y={y}
|
|
150
|
+
width={CELL_SIZE}
|
|
151
|
+
height={CELL_SIZE}
|
|
152
|
+
rx={2}
|
|
153
|
+
ry={2}
|
|
154
|
+
fill={fillColor || "currentColor"}
|
|
155
|
+
opacity={opacity}
|
|
156
|
+
className={fillColor ? "" : "text-base-content/5"}
|
|
157
|
+
onMouseEnter={function (e) {
|
|
158
|
+
setHover({
|
|
159
|
+
x: e.clientX,
|
|
160
|
+
y: e.clientY,
|
|
161
|
+
datum: cell!.datum || { date: cell!.date, count: 0, tokens: 0, cost: 0 },
|
|
162
|
+
});
|
|
163
|
+
}}
|
|
164
|
+
onMouseLeave={function () { setHover(null); }}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
})}
|
|
169
|
+
</svg>
|
|
170
|
+
|
|
171
|
+
{hover && (
|
|
172
|
+
<div
|
|
173
|
+
className="fixed z-50 rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg pointer-events-none"
|
|
174
|
+
style={{ left: hover.x + 12, top: hover.y - 40 }}
|
|
175
|
+
>
|
|
176
|
+
<p className="text-[10px] font-mono text-base-content/50">{hover.datum.date}</p>
|
|
177
|
+
<div className="text-[11px] font-mono text-base-content/70 space-y-0.5">
|
|
178
|
+
<p><span className="text-base-content/40">sessions </span>{hover.datum.count}</p>
|
|
179
|
+
<p><span className="text-base-content/40">cost </span>${hover.datum.cost.toFixed(4)}</p>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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 CacheEfficiencyDatum {
|
|
20
|
+
date: string;
|
|
21
|
+
rate: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CacheEfficiencyChartProps {
|
|
25
|
+
data: CacheEfficiencyDatum[];
|
|
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 * 100).toFixed(1)}%</p>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function CacheEfficiencyChart({ data }: CacheEfficiencyChartProps) {
|
|
39
|
+
var displayData = data.map(function (d) {
|
|
40
|
+
return { date: d.date, rate: d.rate * 100 };
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<ResponsiveContainer width="100%" height={200}>
|
|
45
|
+
<AreaChart data={displayData} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
46
|
+
<defs>
|
|
47
|
+
<linearGradient id="cacheEffGrad" x1="0" y1="0" x2="0" y2="1">
|
|
48
|
+
<stop offset="5%" stopColor="#22c55e" stopOpacity={0.4} />
|
|
49
|
+
<stop offset="95%" stopColor="#22c55e" stopOpacity={0.02} />
|
|
50
|
+
</linearGradient>
|
|
51
|
+
</defs>
|
|
52
|
+
<CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} vertical={false} />
|
|
53
|
+
<XAxis dataKey="date" tick={TICK_STYLE} axisLine={false} tickLine={false} />
|
|
54
|
+
<YAxis domain={[0, 100]} tick={TICK_STYLE} axisLine={false} tickLine={false} tickFormatter={function (v) { return v + "%"; }} />
|
|
55
|
+
<Tooltip content={<CustomTooltip />} />
|
|
56
|
+
<Area type="monotone" dataKey="rate" stroke="#22c55e" fill="url(#cacheEffGrad)" strokeWidth={1.5} />
|
|
57
|
+
</AreaChart>
|
|
58
|
+
</ResponsiveContainer>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -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
|
+
interface DailySummaryDatum {
|
|
2
|
+
date: string;
|
|
3
|
+
sessions: number;
|
|
4
|
+
cost: number;
|
|
5
|
+
tokens: number;
|
|
6
|
+
topTool: string;
|
|
7
|
+
modelMix: Record<string, number>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface DailySummaryCardsProps {
|
|
11
|
+
data: DailySummaryDatum[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
var MODEL_COLORS: Record<string, string> = {
|
|
15
|
+
opus: "oklch(55% 0.25 280)",
|
|
16
|
+
sonnet: "#a855f7",
|
|
17
|
+
haiku: "#22c55e",
|
|
18
|
+
other: "#f59e0b",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function formatCardDate(dateStr: string): string {
|
|
22
|
+
var d = new Date(dateStr + "T00:00:00");
|
|
23
|
+
var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
24
|
+
return days[d.getDay()] + " " + (d.getMonth() + 1) + "/" + d.getDate();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function DailySummaryCards({ data }: DailySummaryCardsProps) {
|
|
28
|
+
if (!data || data.length === 0) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="flex items-center justify-center h-[100px] text-base-content/25 font-mono text-[11px]">
|
|
31
|
+
No data
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
var reversed = data.slice().reverse();
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex gap-3 overflow-x-auto snap-x snap-mandatory pb-2 scrollbar-thin">
|
|
40
|
+
{reversed.map(function (d) {
|
|
41
|
+
var mixEntries = Object.entries(d.modelMix);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
key={d.date}
|
|
46
|
+
className="flex-shrink-0 snap-start rounded-lg bg-base-300 border border-base-content/5 px-3 py-2.5 w-[140px]"
|
|
47
|
+
>
|
|
48
|
+
<p className="text-[11px] font-mono font-bold text-base-content/60 mb-1.5">
|
|
49
|
+
{formatCardDate(d.date)}
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<div className="text-[10px] font-mono text-base-content/50 space-y-0.5">
|
|
53
|
+
<p><span className="text-base-content/30">sessions </span>{d.sessions}</p>
|
|
54
|
+
<p><span className="text-base-content/30">cost </span>${d.cost.toFixed(2)}</p>
|
|
55
|
+
{d.topTool && (
|
|
56
|
+
<p className="truncate"><span className="text-base-content/30">tool </span>{d.topTool}</p>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{mixEntries.length > 0 && (
|
|
61
|
+
<div className="mt-2 h-[4px] rounded-full overflow-hidden flex bg-base-content/5">
|
|
62
|
+
{mixEntries.map(function (entry) {
|
|
63
|
+
var model = entry[0];
|
|
64
|
+
var pct = entry[1];
|
|
65
|
+
if (pct <= 0) return null;
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
key={model}
|
|
69
|
+
style={{
|
|
70
|
+
width: (pct * 100) + "%",
|
|
71
|
+
backgroundColor: MODEL_COLORS[model] || MODEL_COLORS.other,
|
|
72
|
+
}}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
})}
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
})}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
interface HeatmapDatum {
|
|
4
|
+
day: number;
|
|
5
|
+
hour: number;
|
|
6
|
+
count: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface HourlyHeatmapProps {
|
|
10
|
+
data: HeatmapDatum[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
var CELL_SIZE = 18;
|
|
14
|
+
var CELL_GAP = 2;
|
|
15
|
+
var CELL_STEP = CELL_SIZE + CELL_GAP;
|
|
16
|
+
var DAY_LABEL_WIDTH = 32;
|
|
17
|
+
var HOUR_LABEL_HEIGHT = 16;
|
|
18
|
+
|
|
19
|
+
var DAY_ORDER = [1, 2, 3, 4, 5, 6, 0];
|
|
20
|
+
var DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
21
|
+
var HOUR_LABELS = [0, 3, 6, 9, 12, 15, 18, 21];
|
|
22
|
+
|
|
23
|
+
var PRIMARY_COLOR = "oklch(55% 0.25 280)";
|
|
24
|
+
|
|
25
|
+
export function HourlyHeatmap({ data }: HourlyHeatmapProps) {
|
|
26
|
+
var [hover, setHover] = useState<{ x: number; y: number; day: string; hour: number; count: number } | null>(null);
|
|
27
|
+
|
|
28
|
+
if (!data || data.length === 0) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="flex items-center justify-center h-[200px] text-base-content/25 font-mono text-[11px]">
|
|
31
|
+
No data
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
var grid = new Map<string, number>();
|
|
37
|
+
var maxCount = 0;
|
|
38
|
+
for (var i = 0; i < data.length; i++) {
|
|
39
|
+
var key = data[i].day + ":" + data[i].hour;
|
|
40
|
+
grid.set(key, data[i].count);
|
|
41
|
+
if (data[i].count > maxCount) maxCount = data[i].count;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
var svgWidth = DAY_LABEL_WIDTH + 24 * CELL_STEP;
|
|
45
|
+
var svgHeight = HOUR_LABEL_HEIGHT + 7 * CELL_STEP;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="relative overflow-x-auto">
|
|
49
|
+
<svg width={svgWidth} height={svgHeight} className="block">
|
|
50
|
+
{HOUR_LABELS.map(function (h) {
|
|
51
|
+
return (
|
|
52
|
+
<text
|
|
53
|
+
key={h}
|
|
54
|
+
x={DAY_LABEL_WIDTH + h * CELL_STEP + CELL_SIZE / 2}
|
|
55
|
+
y={11}
|
|
56
|
+
textAnchor="middle"
|
|
57
|
+
className="fill-base-content/30"
|
|
58
|
+
style={{ fontSize: 10, fontFamily: "var(--font-mono)" }}
|
|
59
|
+
>
|
|
60
|
+
{h}
|
|
61
|
+
</text>
|
|
62
|
+
);
|
|
63
|
+
})}
|
|
64
|
+
|
|
65
|
+
{DAY_ORDER.map(function (dayIdx, row) {
|
|
66
|
+
return (
|
|
67
|
+
<text
|
|
68
|
+
key={dayIdx}
|
|
69
|
+
x={0}
|
|
70
|
+
y={HOUR_LABEL_HEIGHT + row * CELL_STEP + CELL_SIZE - 3}
|
|
71
|
+
className="fill-base-content/30"
|
|
72
|
+
style={{ fontSize: 10, fontFamily: "var(--font-mono)" }}
|
|
73
|
+
>
|
|
74
|
+
{DAY_NAMES[dayIdx]}
|
|
75
|
+
</text>
|
|
76
|
+
);
|
|
77
|
+
})}
|
|
78
|
+
|
|
79
|
+
{DAY_ORDER.map(function (dayIdx, row) {
|
|
80
|
+
return Array.from({ length: 24 }, function (_, h) {
|
|
81
|
+
var cellKey = dayIdx + ":" + h;
|
|
82
|
+
var count = grid.get(cellKey) || 0;
|
|
83
|
+
var opacity = maxCount > 0 && count > 0 ? Math.max(0.1, count / maxCount) : 0.05;
|
|
84
|
+
var x = DAY_LABEL_WIDTH + h * CELL_STEP;
|
|
85
|
+
var y = HOUR_LABEL_HEIGHT + row * CELL_STEP;
|
|
86
|
+
var dayName = DAY_NAMES[dayIdx];
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<rect
|
|
90
|
+
key={cellKey}
|
|
91
|
+
x={x}
|
|
92
|
+
y={y}
|
|
93
|
+
width={CELL_SIZE}
|
|
94
|
+
height={CELL_SIZE}
|
|
95
|
+
rx={2}
|
|
96
|
+
ry={2}
|
|
97
|
+
fill={count > 0 ? PRIMARY_COLOR : "currentColor"}
|
|
98
|
+
opacity={opacity}
|
|
99
|
+
className={count > 0 ? "" : "text-base-content/5"}
|
|
100
|
+
onMouseEnter={function (e) {
|
|
101
|
+
setHover({
|
|
102
|
+
x: e.clientX,
|
|
103
|
+
y: e.clientY,
|
|
104
|
+
day: dayName,
|
|
105
|
+
hour: h,
|
|
106
|
+
count: count,
|
|
107
|
+
});
|
|
108
|
+
}}
|
|
109
|
+
onMouseLeave={function () { setHover(null); }}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
})}
|
|
114
|
+
</svg>
|
|
115
|
+
|
|
116
|
+
{hover && (
|
|
117
|
+
<div
|
|
118
|
+
className="fixed z-50 rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg pointer-events-none"
|
|
119
|
+
style={{ left: hover.x + 12, top: hover.y - 40 }}
|
|
120
|
+
>
|
|
121
|
+
<div className="text-[11px] font-mono text-base-content/70 space-y-0.5">
|
|
122
|
+
<p><span className="text-base-content/40">{hover.day} </span>{hover.hour}:00</p>
|
|
123
|
+
<p><span className="text-base-content/40">sessions </span>{hover.count}</p>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|