Files
cursor-byok/apps/desktop/src/pages/HomePage.tsx
T

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} />
</>;
}