feat: add output/input ratio column to aggregation page
Add a sortable "Out/In Ratio" (completion_tokens / prompt_tokens) column with a portal-based tooltip explaining the metric. Fix hydration mismatch by switching to useSyncExternalStore for localStorage reads in TimeRangeProvider. Update CLAUDE.md with project documentation.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useState, useCallback, type ReactNode } from "react";
|
||||
import { createContext, useContext, useState, useCallback, useSyncExternalStore, type ReactNode } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { type TimeRange, getTimeRange } from "@/lib/utils";
|
||||
|
||||
@@ -25,45 +25,70 @@ const TimeRangeContext = createContext<TimeRangeContextType>({
|
||||
getEffectiveRange: () => ({}),
|
||||
});
|
||||
|
||||
function loadSaved(): { range: TimeRange; customStart: string; customEnd: string } | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
try {
|
||||
const raw = localStorage.getItem("time-range");
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
return {
|
||||
range: parsed.range || "30d",
|
||||
customStart: parsed.customStart || "",
|
||||
customEnd: parsed.customEnd || "",
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
// Use useSyncExternalStore to safely read localStorage without hydration mismatch
|
||||
const STORAGE_KEY = "time-range";
|
||||
const DEFAULT_RANGE = "30d";
|
||||
|
||||
let listeners: Array<() => void> = [];
|
||||
function subscribe(cb: () => void) {
|
||||
listeners = [...listeners, cb];
|
||||
return () => { listeners = listeners.filter(l => l !== cb); };
|
||||
}
|
||||
function emitChange() {
|
||||
for (const l of listeners) l();
|
||||
}
|
||||
|
||||
function getSnapshot(): string {
|
||||
return localStorage.getItem(STORAGE_KEY) ?? "";
|
||||
}
|
||||
function getServerSnapshot(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
function persist(range: TimeRange, customStart: string, customEnd: string) {
|
||||
localStorage.setItem("time-range", JSON.stringify({ range, customStart, customEnd }));
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({ range, customStart, customEnd }));
|
||||
emitChange();
|
||||
}
|
||||
|
||||
export function TimeRangeProvider({ children }: { children: ReactNode }) {
|
||||
const [range, setRangeState] = useState<TimeRange>(() => loadSaved()?.range ?? "30d");
|
||||
const [customStart, setCustomStartState] = useState(() => loadSaved()?.customStart || dayjs().subtract(7, "day").format("YYYY-MM-DD"));
|
||||
const [customEnd, setCustomEndState] = useState(() => loadSaved()?.customEnd || dayjs().format("YYYY-MM-DD"));
|
||||
const raw = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
||||
|
||||
const saved = (() => {
|
||||
if (!raw) return null;
|
||||
try { return JSON.parse(raw); } catch { return null; }
|
||||
})();
|
||||
|
||||
const [range, setRangeState] = useState<TimeRange>(saved?.range ?? DEFAULT_RANGE);
|
||||
const [customStart, setCustomStartState] = useState(
|
||||
saved?.customStart || dayjs().subtract(7, "day").format("YYYY-MM-DD")
|
||||
);
|
||||
const [customEnd, setCustomEndState] = useState(
|
||||
saved?.customEnd || dayjs().format("YYYY-MM-DD")
|
||||
);
|
||||
|
||||
const setRange = useCallback((r: TimeRange) => {
|
||||
setRangeState(r);
|
||||
setCustomStartState(prev => { persist(r, prev, customEnd); return prev; });
|
||||
}, [customEnd]);
|
||||
setCustomStartState(prev => {
|
||||
setCustomEndState(end => { persist(r, prev, end); return end; });
|
||||
return prev;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setCustomStart = useCallback((s: string) => {
|
||||
setCustomStartState(s);
|
||||
persist(range, s, customEnd);
|
||||
}, [range, customEnd]);
|
||||
setRangeState(r => {
|
||||
setCustomEndState(end => { persist(r, s, end); return end; });
|
||||
return r;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setCustomEnd = useCallback((e: string) => {
|
||||
setCustomEndState(e);
|
||||
persist(range, customStart, e);
|
||||
}, [range, customStart]);
|
||||
setRangeState(r => {
|
||||
setCustomStartState(start => { persist(r, start, e); return start; });
|
||||
return r;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const getEffectiveRange = useCallback(() => {
|
||||
if (range === "custom") {
|
||||
|
||||
Reference in New Issue
Block a user