mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-08 07:21:13 +08:00
158 lines
6.0 KiB
TypeScript
158 lines
6.0 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { api, configuredPluginModels, type Overview } from "../../shared/api";
|
|
import { ContributionCalendarChart } from "./charts/ContributionCalendarChart";
|
|
import { DailyTokenUsageChart } from "./charts/DailyTokenUsageChart";
|
|
import { HomeMetrics } from "./metrics/HomeMetrics";
|
|
import { PageContent } from "../../shell/layout/PageContent";
|
|
import type { VirtualPageSection } from "../../shell/layout/VirtualPage";
|
|
import { OverviewTimeRangeFilter, type OverviewRangePreset } from "./overview/OverviewTimeRangeFilter";
|
|
import { PageActions } from "../../shell/PageActions";
|
|
import { appStore, useAppStore } from "../../shared/store/appStore";
|
|
import { formatTimeInput, parseTimeInput } from "../../shared/utils/parseTimeInput";
|
|
import { claudeIcon, flatColorOrganizationIcon, openAiIcon } from "../../shared/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, plugins } = 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 [appliedModels, setAppliedModels] = 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,
|
|
}).then((next) => {
|
|
if (active) setRangeOverview(next);
|
|
}).finally(() => {
|
|
if (active) setRangeBusy(false);
|
|
});
|
|
return () => { active = false; };
|
|
}, [preset, customRange, overview, refreshVersion, appliedModels]);
|
|
|
|
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);
|
|
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.type),
|
|
})),
|
|
...configuredPluginModels(plugins).map((model) => ({
|
|
value: model.id,
|
|
label: model.displayName,
|
|
icon: flatColorOrganizationIcon,
|
|
})),
|
|
];
|
|
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} refreshVersion={refreshVersion} />,
|
|
},
|
|
|
|
{
|
|
key: "activity",
|
|
estimatedHeight: 106,
|
|
content: <ContributionCalendarChart data={contribution} />,
|
|
},
|
|
];
|
|
|
|
return <>
|
|
<PageActions><OverviewTimeRangeFilter
|
|
value={preset}
|
|
customOpen={customOpen}
|
|
customStart={customStart}
|
|
customEnd={customEnd}
|
|
modelOptions={modelOptions}
|
|
selectedModels={selectedModels}
|
|
busy={busy || rangeBusy}
|
|
onSelect={(value) => { setPreset(value); setCustomOpen(false); }}
|
|
onCustomOpenChange={openCustom}
|
|
onCustomStartChange={setCustomStart}
|
|
onCustomEndChange={setCustomEnd}
|
|
onSelectedModelsChange={setSelectedModels}
|
|
onCustomApply={applyCustom}
|
|
onRefresh={() => void refresh()}
|
|
/></PageActions>
|
|
<PageContent title={t("概览")} sections={sections} />
|
|
</>;
|
|
}
|