feat: add Token Usage Trends timeline graph with 14D/30D/90D range toggle and stacked billed vs cached bars

This commit is contained in:
katR 2026-10-10 19:01:38 +05:30
parent 7cb4ec6282
commit 2b15144ae5
3 changed files with 544 additions and 2 deletions

View file

@ -68,7 +68,9 @@ function HeatmapInnerView() {
const [error, setError] = (0, import_react.useState)(null);
const [selectedSessionId, setSelectedSessionId] = (0, import_react.useState)("all");
const [selectedDate, setSelectedDate] = (0, import_react.useState)(null);
const [chartRange, setChartRange] = (0, import_react.useState)(30);
const [hoveredDay, setHoveredDay] = (0, import_react.useState)(null);
const [hoveredBar, setHoveredBar] = (0, import_react.useState)(null);
const [tooltipPos, setTooltipPos] = (0, import_react.useState)(null);
const scrollContainerRef = (0, import_react.useRef)(null);
const fetchStats = () => {
@ -210,6 +212,36 @@ function HeatmapInnerView() {
scrollContainerRef.current.scrollLeft = scrollContainerRef.current.scrollWidth;
}
}, [calendarGrid]);
const chartData = (0, import_react.useMemo)(() => {
if (!activeView) return [];
const now = /* @__PURE__ */ new Date();
const items = [];
for (let i = chartRange - 1; i >= 0; i--) {
const d = new Date(now);
d.setDate(d.getDate() - i);
const dateKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
const stats = activeView.days[dateKey] || {
totalTokens: 0,
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
turns: 0
};
items.push({
date: dateKey,
label: `${MONTH_NAMES[d.getMonth()]} ${d.getDate()}`,
totalTokens: stats.totalTokens,
billedTokens: stats.inputTokens + stats.outputTokens,
cachedTokens: stats.cacheReadTokens,
turns: stats.turns
});
}
return items;
}, [activeView, chartRange]);
const maxChartTokens = (0, import_react.useMemo)(() => {
return Math.max(...chartData.map((d) => d.totalTokens), 1e6);
}, [chartData]);
if (loading && !data) {
return /* @__PURE__ */ import_react.default.createElement("div", { style: { padding: "40px 20px", textAlign: "center", color: "var(--dsw-alias-label-secondary)" } }, "Loading token usage statistics...");
}
@ -552,6 +584,182 @@ function HeatmapInnerView() {
)
)
),
/* @__PURE__ */ import_react.default.createElement(
"div",
{
style: {
marginTop: 18,
background: "var(--dsw-alias-bg-layer-2, #2c2c2c)",
border: "1px solid var(--dsw-alias-border-l2, #494949)",
borderRadius: 12,
padding: "20px 22px",
position: "relative",
boxShadow: "0 4px 16px rgba(0, 0, 0, 0.2)"
}
},
/* @__PURE__ */ import_react.default.createElement(
"div",
{
style: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexWrap: "wrap",
gap: 12,
marginBottom: 16
}
},
/* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 14, fontWeight: 650, color: "var(--dsw-alias-label-primary, #ffffff)" } }, "Token Usage Trends"), /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 12, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", marginTop: 2 } }, "Daily billed vs cached token volume over time")),
/* @__PURE__ */ import_react.default.createElement("div", { style: { display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap" } }, /* @__PURE__ */ import_react.default.createElement("div", { style: { display: "flex", alignItems: "center", gap: 12, fontSize: 11, color: "var(--dsw-alias-label-secondary)" } }, /* @__PURE__ */ import_react.default.createElement("div", { style: { display: "flex", alignItems: "center", gap: 5 } }, /* @__PURE__ */ import_react.default.createElement("span", { style: { width: 9, height: 9, borderRadius: 2, background: "#4f85ff" } }), /* @__PURE__ */ import_react.default.createElement("span", null, "Billed Tokens")), /* @__PURE__ */ import_react.default.createElement("div", { style: { display: "flex", alignItems: "center", gap: 5 } }, /* @__PURE__ */ import_react.default.createElement("span", { style: { width: 9, height: 9, borderRadius: 2, background: "#253b82" } }), /* @__PURE__ */ import_react.default.createElement("span", null, "Cache Reads"))), /* @__PURE__ */ import_react.default.createElement(
"div",
{
style: {
display: "inline-flex",
background: "var(--dsw-alias-bg-layer-1, #1e1e1e)",
border: "1px solid var(--dsw-alias-border-l2, #494949)",
borderRadius: 8,
padding: 2
}
},
[14, 30, 90].map((days) => /* @__PURE__ */ import_react.default.createElement(
"button",
{
key: days,
onClick: () => setChartRange(days),
style: {
border: 0,
background: chartRange === days ? "var(--dsw-alias-bg-layer-2, #2c2c2c)" : "transparent",
color: chartRange === days ? "#4f85ff" : "var(--dsw-alias-label-tertiary)",
fontSize: 11,
padding: "3px 8px",
borderRadius: 6,
cursor: "pointer",
fontWeight: chartRange === days ? 600 : 400
}
},
days,
"D"
))
))
),
/* @__PURE__ */ import_react.default.createElement("div", { style: { width: "100%", overflowX: "auto", WebkitOverflowScrolling: "touch" } }, /* @__PURE__ */ import_react.default.createElement("div", { style: { minWidth: "720px", width: "100%" } }, /* @__PURE__ */ import_react.default.createElement(
"svg",
{
viewBox: "0 0 880 180",
style: { width: "100%", height: "180px", display: "block", overflow: "visible" }
},
[1, 0.66, 0.33, 0].map((ratio, i) => {
const y = 20 + (1 - ratio) * 125;
const tokenVal = Math.round(maxChartTokens * ratio);
return /* @__PURE__ */ import_react.default.createElement("g", { key: i }, /* @__PURE__ */ import_react.default.createElement(
"line",
{
x1: 45,
y1: y,
x2: 870,
y2: y,
stroke: "var(--dsw-alias-border-l1, rgba(255,255,255,0.08))",
strokeDasharray: "4 4"
}
), /* @__PURE__ */ import_react.default.createElement(
"text",
{
x: 40,
y: y + 3,
fontSize: 9,
fill: "var(--dsw-alias-label-tertiary, #888888)",
textAnchor: "end",
fontFamily: "inherit"
},
formatNumber(tokenVal)
));
}),
chartData.map((d, idx) => {
const totalBarWidth = 825 / chartData.length;
const barWidth = Math.max(5, Math.min(22, totalBarWidth - 5));
const x = 50 + idx * totalBarWidth + (totalBarWidth - barWidth) / 2;
const totalH = d.totalTokens / maxChartTokens * 125;
const cachedH = d.totalTokens > 0 ? d.cachedTokens / d.totalTokens * totalH : 0;
const billedH = totalH - cachedH;
const yBase = 145;
const isHovered = hoveredBar?.date === d.date;
return /* @__PURE__ */ import_react.default.createElement(
"g",
{
key: idx,
style: { cursor: "pointer" },
onMouseEnter: (e) => {
setHoveredBar(d);
const rect = e.currentTarget.getBoundingClientRect();
setTooltipPos({ x: rect.left + rect.width / 2, y: rect.top - 8 });
},
onMouseLeave: () => {
setHoveredBar(null);
setTooltipPos(null);
},
onClick: () => setSelectedDate(selectedDate === d.date ? null : d.date)
},
/* @__PURE__ */ import_react.default.createElement(
"rect",
{
x: x - 2,
y: 20,
width: barWidth + 4,
height: 125,
fill: isHovered ? "rgba(79, 133, 255, 0.08)" : "transparent",
rx: 3
}
),
d.totalTokens === 0 ? /* @__PURE__ */ import_react.default.createElement(
"rect",
{
x,
y: yBase - 2,
width: barWidth,
height: 2,
fill: "rgba(255, 255, 255, 0.08)",
rx: 1
}
) : /* @__PURE__ */ import_react.default.createElement("g", null, cachedH > 0 && /* @__PURE__ */ import_react.default.createElement(
"rect",
{
x,
y: yBase - cachedH,
width: barWidth,
height: cachedH,
fill: "#253b82",
rx: billedH <= 0 ? 2 : 0
}
), billedH > 0 && /* @__PURE__ */ import_react.default.createElement(
"rect",
{
x,
y: yBase - totalH,
width: barWidth,
height: billedH,
fill: isHovered ? "#60a5fa" : "#4f85ff",
rx: 2,
style: {
filter: isHovered ? "drop-shadow(0 0 5px rgba(79, 133, 255, 0.8))" : "none"
}
}
)),
(chartRange === 14 || idx % (chartRange === 30 ? 3 : 7) === 0) && /* @__PURE__ */ import_react.default.createElement(
"text",
{
x: x + barWidth / 2,
y: 162,
fontSize: 9,
fill: "var(--dsw-alias-label-tertiary, #888888)",
textAnchor: "middle",
fontFamily: "inherit"
},
d.label
)
);
})
)))
),
selectedDate && /* @__PURE__ */ import_react.default.createElement(
"div",
{
@ -661,6 +869,34 @@ function HeatmapInnerView() {
day: "numeric"
})),
hoveredDay.stats.totalTokens === 0 ? /* @__PURE__ */ import_react.default.createElement("div", { style: { color: "var(--dsw-alias-label-tertiary)" } }, "No activity recorded") : /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("div", { style: { color: "#4f85ff", fontWeight: 600 } }, formatExact(hoveredDay.stats.totalTokens), " tokens"), /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-secondary)", marginTop: 2 } }, hoveredDay.stats.turns, " requests / steps"), /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 10, color: "var(--dsw-alias-label-tertiary)", marginTop: 4 } }, "Click to inspect sessions on this day"))
),
hoveredBar && tooltipPos && /* @__PURE__ */ import_react.default.createElement(
"div",
{
style: {
position: "fixed",
left: tooltipPos.x,
top: tooltipPos.y,
transform: "translate(-50%, -100%)",
background: "var(--dsw-alias-bg-layer-1, #1e1e1e)",
border: "1px solid var(--dsw-alias-border-l3, #555555)",
borderRadius: 8,
padding: "8px 12px",
fontSize: 12,
lineHeight: 1.4,
boxShadow: "0 6px 18px rgba(0,0,0,0.35)",
pointerEvents: "none",
zIndex: 9999,
whiteSpace: "nowrap"
}
},
/* @__PURE__ */ import_react.default.createElement("div", { style: { fontWeight: 650, color: "var(--dsw-alias-label-primary, #ffffff)", marginBottom: 4 } }, (/* @__PURE__ */ new Date(hoveredBar.date + "T00:00:00")).toLocaleDateString(void 0, {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric"
})),
hoveredBar.totalTokens === 0 ? /* @__PURE__ */ import_react.default.createElement("div", { style: { color: "var(--dsw-alias-label-tertiary)" } }, "No activity recorded") : /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("div", { style: { color: "#4f85ff", fontWeight: 600 } }, formatExact(hoveredBar.totalTokens), " total tokens"), /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-secondary)", marginTop: 2 } }, "Billed: ", formatNumber(hoveredBar.billedTokens), " \xB7 Cached: ", formatNumber(hoveredBar.cachedTokens)), /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 10, color: "var(--dsw-alias-label-tertiary)", marginTop: 4 } }, hoveredBar.turns, " turns \xB7 Click to inspect day"))
)
);
}

File diff suppressed because one or more lines are too long

View file

@ -84,7 +84,16 @@ function HeatmapInnerView() {
const [error, setError] = useState<string | null>(null);
const [selectedSessionId, setSelectedSessionId] = useState<string>("all");
const [selectedDate, setSelectedDate] = useState<string | null>(null);
const [chartRange, setChartRange] = useState<14 | 30 | 90>(30);
const [hoveredDay, setHoveredDay] = useState<{ date: string; stats: DayStats } | null>(null);
const [hoveredBar, setHoveredBar] = useState<{
date: string;
label: string;
totalTokens: number;
billedTokens: number;
cachedTokens: number;
turns: number;
} | null>(null);
const [tooltipPos, setTooltipPos] = useState<{ x: number; y: number } | null>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
@ -259,6 +268,47 @@ function HeatmapInnerView() {
}
}, [calendarGrid]);
// Daily token timeline chart data for the selected range (14, 30, or 90 days)
const chartData = useMemo(() => {
if (!activeView) return [];
const now = new Date();
const items: Array<{
date: string;
label: string;
totalTokens: number;
billedTokens: number;
cachedTokens: number;
turns: number;
}> = [];
for (let i = chartRange - 1; i >= 0; i--) {
const d = new Date(now);
d.setDate(d.getDate() - i);
const dateKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
const stats = activeView.days[dateKey] || {
totalTokens: 0,
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
turns: 0,
};
items.push({
date: dateKey,
label: `${MONTH_NAMES[d.getMonth()]} ${d.getDate()}`,
totalTokens: stats.totalTokens,
billedTokens: stats.inputTokens + stats.outputTokens,
cachedTokens: stats.cacheReadTokens,
turns: stats.turns,
});
}
return items;
}, [activeView, chartRange]);
const maxChartTokens = useMemo(() => {
return Math.max(...chartData.map((d) => d.totalTokens), 1_000_000);
}, [chartData]);
if (loading && !data) {
return (
<div style={{ padding: "40px 20px", textAlign: "center", color: "var(--dsw-alias-label-secondary)" }}>
@ -686,6 +736,216 @@ function HeatmapInnerView() {
)}
</div>
{/* Token Usage Trends Graph Card */}
<div
style={{
marginTop: 18,
background: "var(--dsw-alias-bg-layer-2, #2c2c2c)",
border: "1px solid var(--dsw-alias-border-l2, #494949)",
borderRadius: 12,
padding: "20px 22px",
position: "relative",
boxShadow: "0 4px 16px rgba(0, 0, 0, 0.2)",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexWrap: "wrap",
gap: 12,
marginBottom: 16,
}}
>
<div>
<div style={{ fontSize: 14, fontWeight: 650, color: "var(--dsw-alias-label-primary, #ffffff)" }}>
Token Usage Trends
</div>
<div style={{ fontSize: 12, color: "var(--dsw-alias-label-tertiary, #a0a0a0)", marginTop: 2 }}>
Daily billed vs cached token volume over time
</div>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap" }}>
{/* Legend: Billed vs Cached */}
<div style={{ display: "flex", alignItems: "center", gap: 12, fontSize: 11, color: "var(--dsw-alias-label-secondary)" }}>
<div style={{ display: "flex", alignItems: "center", gap: 5 }}>
<span style={{ width: 9, height: 9, borderRadius: 2, background: "#4f85ff" }} />
<span>Billed Tokens</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 5 }}>
<span style={{ width: 9, height: 9, borderRadius: 2, background: "#253b82" }} />
<span>Cache Reads</span>
</div>
</div>
{/* Range Toggle: 14D | 30D | 90D */}
<div
style={{
display: "inline-flex",
background: "var(--dsw-alias-bg-layer-1, #1e1e1e)",
border: "1px solid var(--dsw-alias-border-l2, #494949)",
borderRadius: 8,
padding: 2,
}}
>
{[14, 30, 90].map((days) => (
<button
key={days}
onClick={() => setChartRange(days as any)}
style={{
border: 0,
background: chartRange === days ? "var(--dsw-alias-bg-layer-2, #2c2c2c)" : "transparent",
color: chartRange === days ? "#4f85ff" : "var(--dsw-alias-label-tertiary)",
fontSize: 11,
padding: "3px 8px",
borderRadius: 6,
cursor: "pointer",
fontWeight: chartRange === days ? 600 : 400,
}}
>
{days}D
</button>
))}
</div>
</div>
</div>
{/* The SVG Timeline Bar Chart */}
<div style={{ width: "100%", overflowX: "auto", WebkitOverflowScrolling: "touch" }}>
<div style={{ minWidth: "720px", width: "100%" }}>
<svg
viewBox="0 0 880 180"
style={{ width: "100%", height: "180px", display: "block", overflow: "visible" }}
>
{/* Horizontal grid lines & Y labels */}
{[1, 0.66, 0.33, 0].map((ratio, i) => {
const y = 20 + (1 - ratio) * 125;
const tokenVal = Math.round(maxChartTokens * ratio);
return (
<g key={i}>
<line
x1={45}
y1={y}
x2={870}
y2={y}
stroke="var(--dsw-alias-border-l1, rgba(255,255,255,0.08))"
strokeDasharray="4 4"
/>
<text
x={40}
y={y + 3}
fontSize={9}
fill="var(--dsw-alias-label-tertiary, #888888)"
textAnchor="end"
fontFamily="inherit"
>
{formatNumber(tokenVal)}
</text>
</g>
);
})}
{/* Bars for each day in chartData */}
{chartData.map((d, idx) => {
const totalBarWidth = 825 / chartData.length;
const barWidth = Math.max(5, Math.min(22, totalBarWidth - 5));
const x = 50 + idx * totalBarWidth + (totalBarWidth - barWidth) / 2;
const totalH = (d.totalTokens / maxChartTokens) * 125;
const cachedH = d.totalTokens > 0 ? (d.cachedTokens / d.totalTokens) * totalH : 0;
const billedH = totalH - cachedH;
const yBase = 145;
const isHovered = hoveredBar?.date === d.date;
return (
<g
key={idx}
style={{ cursor: "pointer" }}
onMouseEnter={(e) => {
setHoveredBar(d);
const rect = e.currentTarget.getBoundingClientRect();
setTooltipPos({ x: rect.left + rect.width / 2, y: rect.top - 8 });
}}
onMouseLeave={() => {
setHoveredBar(null);
setTooltipPos(null);
}}
onClick={() => setSelectedDate(selectedDate === d.date ? null : d.date)}
>
{/* Hover hit area */}
<rect
x={x - 2}
y={20}
width={barWidth + 4}
height={125}
fill={isHovered ? "rgba(79, 133, 255, 0.08)" : "transparent"}
rx={3}
/>
{/* Empty/Zero line marker if 0 tokens */}
{d.totalTokens === 0 ? (
<rect
x={x}
y={yBase - 2}
width={barWidth}
height={2}
fill="rgba(255, 255, 255, 0.08)"
rx={1}
/>
) : (
<g>
{/* Cached segment (bottom) */}
{cachedH > 0 && (
<rect
x={x}
y={yBase - cachedH}
width={barWidth}
height={cachedH}
fill="#253b82"
rx={billedH <= 0 ? 2 : 0}
/>
)}
{/* Billed segment (top) */}
{billedH > 0 && (
<rect
x={x}
y={yBase - totalH}
width={barWidth}
height={billedH}
fill={isHovered ? "#60a5fa" : "#4f85ff"}
rx={2}
style={{
filter: isHovered ? "drop-shadow(0 0 5px rgba(79, 133, 255, 0.8))" : "none",
}}
/>
)}
</g>
)}
{/* X-axis date labels: show every Nth label depending on range */}
{(chartRange === 14 || idx % (chartRange === 30 ? 3 : 7) === 0) && (
<text
x={x + barWidth / 2}
y={162}
fontSize={9}
fill="var(--dsw-alias-label-tertiary, #888888)"
textAnchor="middle"
fontFamily="inherit"
>
{d.label}
</text>
)}
</g>
);
})}
</svg>
</div>
</div>
</div>
{/* Day Inspection Detail Drawer (Opens when any day is clicked) */}
{selectedDate && (
<div
@ -870,6 +1130,52 @@ function HeatmapInnerView() {
)}
</div>
)}
{/* Floating Hover Tooltip for Chart Bar */}
{hoveredBar && tooltipPos && (
<div
style={{
position: "fixed",
left: tooltipPos.x,
top: tooltipPos.y,
transform: "translate(-50%, -100%)",
background: "var(--dsw-alias-bg-layer-1, #1e1e1e)",
border: "1px solid var(--dsw-alias-border-l3, #555555)",
borderRadius: 8,
padding: "8px 12px",
fontSize: 12,
lineHeight: 1.4,
boxShadow: "0 6px 18px rgba(0,0,0,0.35)",
pointerEvents: "none",
zIndex: 9999,
whiteSpace: "nowrap",
}}
>
<div style={{ fontWeight: 650, color: "var(--dsw-alias-label-primary, #ffffff)", marginBottom: 4 }}>
{new Date(hoveredBar.date + "T00:00:00").toLocaleDateString(undefined, {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
})}
</div>
{hoveredBar.totalTokens === 0 ? (
<div style={{ color: "var(--dsw-alias-label-tertiary)" }}>No activity recorded</div>
) : (
<div>
<div style={{ color: "#4f85ff", fontWeight: 600 }}>
{formatExact(hoveredBar.totalTokens)} total tokens
</div>
<div style={{ fontSize: 11, color: "var(--dsw-alias-label-secondary)", marginTop: 2 }}>
Billed: {formatNumber(hoveredBar.billedTokens)} · Cached: {formatNumber(hoveredBar.cachedTokens)}
</div>
<div style={{ fontSize: 10, color: "var(--dsw-alias-label-tertiary)", marginTop: 4 }}>
{hoveredBar.turns} turns · Click to inspect day
</div>
</div>
)}
</div>
)}
</div>
);
}