mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-06 13:44:21 +08:00
163 lines
6.3 KiB
TypeScript
163 lines
6.3 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { api, type Overview } from "../api";
|
|
import { ContributionCalendarChart } from "../components/charts/ContributionCalendarChart";
|
|
import { DailyTokenUsageChart } from "../components/charts/DailyTokenUsageChart";
|
|
import { HomeMetrics } from "../components/metrics/HomeMetrics";
|
|
import { PageContent } from "../components/layout/PageContent";
|
|
import type { VirtualPageSection } from "../components/layout/VirtualPage";
|
|
import { OverviewTimeRangeFilter, type OverviewRangePreset } from "../components/overview/OverviewTimeRangeFilter";
|
|
import { PageActions } from "../layouts/PageActions";
|
|
import { appStore, useAppStore } from "../store/appStore";
|
|
import { formatTimeInput, parseTimeInput } from "../utils/parseTimeInput";
|
|
import { claudeIcon, openAiIcon } from "../components/ui/icons";
|
|
|
|
type TimeRange = { startMs: number; endMs: number };
|
|
|
|
function presetRange(preset: Exclude<OverviewRangePreset, "custom">, now = new Date()): TimeRange {
|
|
const endMs = now.getTime();
|
|
if (preset === "today") {
|
|
const start = new Date(now);
|
|
start.setHours(0, 0, 0, 0);
|
|
return { startMs: start.getTime(), endMs };
|
|
}
|
|
if (preset === "month") {
|
|
const start = new Date(now);
|
|
start.setMonth(start.getMonth() - 1);
|
|
return { startMs: start.getTime(), endMs };
|
|
}
|
|
const duration = preset === "ten-minutes" ? 10 * 60_000 : preset === "hour" ? 60 * 60_000 : 7 * 24 * 60 * 60_000;
|
|
return { startMs: now.getTime() - duration, endMs };
|
|
}
|
|
|
|
export function HomePage() {
|
|
const { overview, busy, models, providers } = useAppStore();
|
|
const [preset, setPreset] = useState<OverviewRangePreset>("month");
|
|
const [customRange, setCustomRange] = useState<TimeRange | null>(null);
|
|
const [customOpen, setCustomOpen] = useState(false);
|
|
const [customStart, setCustomStart] = useState("");
|
|
const [customEnd, setCustomEnd] = useState("");
|
|
const [selectedModels, setSelectedModels] = useState<string[]>([]);
|
|
const [selectedProviders, setSelectedProviders] = useState<string[]>([]);
|
|
const [appliedModels, setAppliedModels] = useState<string[]>([]);
|
|
const [appliedProviders, setAppliedProviders] = useState<string[]>([]);
|
|
const [rangeOverview, setRangeOverview] = useState<Overview | null>(null);
|
|
const [rangeBusy, setRangeBusy] = useState(false);
|
|
const [refreshVersion, setRefreshVersion] = useState(0);
|
|
const selectedRange = preset === "custom" ? customRange : presetRange(preset);
|
|
|
|
useEffect(() => {
|
|
if (!selectedRange) return;
|
|
let active = true;
|
|
setRangeBusy(true);
|
|
void api.overview({
|
|
...selectedRange,
|
|
modelHashes: appliedModels,
|
|
providerIds: appliedProviders.map(Number),
|
|
}).then((next) => {
|
|
if (active) setRangeOverview(next);
|
|
}).finally(() => {
|
|
if (active) setRangeBusy(false);
|
|
});
|
|
return () => { active = false; };
|
|
}, [preset, customRange, overview, refreshVersion, appliedModels, appliedProviders]);
|
|
|
|
const filteredOverview = rangeOverview ?? overview;
|
|
const dailyTokenUsage = filteredOverview.token_usage_series.map((bucket) => ({
|
|
bucketStartMs: bucket.bucket_start_ms,
|
|
inputTokens: bucket.input_tokens,
|
|
cacheReadTokens: bucket.cache_read_tokens,
|
|
cacheWriteTokens: bucket.cache_write_tokens,
|
|
outputTokens: bucket.output_tokens,
|
|
}));
|
|
const contribution = overview.token_usage_series.map((bucket) => ({
|
|
date: new Date(bucket.bucket_start_ms).toISOString().slice(0, 10),
|
|
tokens: bucket.input_tokens + bucket.cache_read_tokens + bucket.cache_write_tokens + bucket.output_tokens,
|
|
}));
|
|
const metrics = {
|
|
llmCalls: filteredOverview.metrics.llm_calls,
|
|
successfulCalls: filteredOverview.metrics.successful_calls,
|
|
failedCalls: filteredOverview.metrics.failed_calls,
|
|
tokenUsage: filteredOverview.metrics.token_usage,
|
|
promptTokens: filteredOverview.metrics.prompt_tokens,
|
|
cacheReadTokens: filteredOverview.metrics.cache_read_tokens,
|
|
cacheWriteTokens: filteredOverview.metrics.cache_write_tokens,
|
|
};
|
|
const openCustom = (open: boolean) => {
|
|
if (open && !customStart && !customEnd) {
|
|
const now = new Date();
|
|
setCustomEnd(formatTimeInput(now));
|
|
setCustomStart(formatTimeInput(new Date(now.getTime() - 60 * 60_000)));
|
|
}
|
|
setCustomOpen(open);
|
|
};
|
|
const applyCustom = () => {
|
|
const startMs = parseTimeInput(customStart);
|
|
const endMs = parseTimeInput(customEnd);
|
|
if (startMs === null || endMs === null || startMs >= endMs) return;
|
|
setCustomRange({ startMs, endMs });
|
|
setAppliedModels(selectedModels);
|
|
setAppliedProviders(selectedProviders);
|
|
setPreset("custom");
|
|
setCustomOpen(false);
|
|
};
|
|
const refresh = async () => {
|
|
await appStore.refresh();
|
|
setRefreshVersion((version) => version + 1);
|
|
};
|
|
const iconFor = (type: string) => type === "anthropic" ? claudeIcon : openAiIcon;
|
|
const modelOptions = models.map((model) => ({
|
|
value: model.model_hash,
|
|
label: model.display_name,
|
|
icon: iconFor(model.endpoint_type),
|
|
}));
|
|
const providerOptions = providers.map((provider) => ({
|
|
value: String(provider.provider_id),
|
|
label: provider.name,
|
|
icon: iconFor(provider.provider_type),
|
|
}));
|
|
const sections: VirtualPageSection[] = [
|
|
{
|
|
key: "daily-token-usage",
|
|
estimatedHeight: 240,
|
|
content: <DailyTokenUsageChart
|
|
data={dailyTokenUsage}
|
|
granularity={filteredOverview.token_usage_granularity}
|
|
/>,
|
|
},
|
|
{
|
|
key: "metrics",
|
|
estimatedHeight: 130,
|
|
content: <HomeMetrics data={metrics} />,
|
|
},
|
|
|
|
{
|
|
key: "activity",
|
|
estimatedHeight: 106,
|
|
content: <ContributionCalendarChart data={contribution} />,
|
|
},
|
|
];
|
|
|
|
return <>
|
|
<PageActions><OverviewTimeRangeFilter
|
|
value={preset}
|
|
customOpen={customOpen}
|
|
customStart={customStart}
|
|
customEnd={customEnd}
|
|
modelOptions={modelOptions}
|
|
providerOptions={providerOptions}
|
|
selectedModels={selectedModels}
|
|
selectedProviders={selectedProviders}
|
|
busy={busy || rangeBusy}
|
|
onSelect={(value) => { setPreset(value); setCustomOpen(false); }}
|
|
onCustomOpenChange={openCustom}
|
|
onCustomStartChange={setCustomStart}
|
|
onCustomEndChange={setCustomEnd}
|
|
onSelectedModelsChange={setSelectedModels}
|
|
onSelectedProvidersChange={setSelectedProviders}
|
|
onCustomApply={applyCustom}
|
|
onRefresh={() => void refresh()}
|
|
/></PageActions>
|
|
<PageContent title={t("概览")} sections={sections} />
|
|
</>;
|
|
}
|