mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-06 21:52:51 +08:00
180 lines
7.4 KiB
TypeScript
180 lines
7.4 KiB
TypeScript
import { useState } from "react";
|
||
import { formatCompactInteger, formatInteger } from "../../utils/numberFormat";
|
||
import { Icon } from "../ui/Icon";
|
||
import { Tooltip, type TooltipAnchor } from "../ui/Tooltip";
|
||
import { informationOutlineIcon } from "../ui/icons";
|
||
import { CacheHitRateChart } from "./CacheHitRateChart";
|
||
import styles from "./HomeMetrics.module.scss";
|
||
|
||
export type HomeMetricsData = {
|
||
llmCalls: number;
|
||
successfulCalls: number;
|
||
failedCalls: number;
|
||
tokenUsage: number;
|
||
promptTokens: number;
|
||
cacheReadTokens: number;
|
||
cacheWriteTokens: number;
|
||
};
|
||
|
||
const TOKEN_PRICE_PER_MILLION = {
|
||
input: 5,
|
||
output: 25,
|
||
cacheRead: 0.5,
|
||
cacheWrite: 6.25,
|
||
} as const;
|
||
|
||
function formatMetricValue(value: number) {
|
||
const full = formatInteger(value);
|
||
const compact = formatCompactInteger(value);
|
||
return full === compact ? full : `${full} (${compact})`;
|
||
}
|
||
|
||
function formatRate(value: number | null) {
|
||
return value === null ? t("暂无数据") : `${(Math.max(0, Math.min(1, value)) * 100).toFixed(2)}%`;
|
||
}
|
||
|
||
function calculateRate(numerator: number, denominator: number) {
|
||
return denominator > 0 ? numerator / denominator : null;
|
||
}
|
||
|
||
function priceTokens(tokens: number, pricePerMillion: number) {
|
||
return (tokens / 1_000_000) * pricePerMillion;
|
||
}
|
||
|
||
function formatUSD(value: number) {
|
||
return `$${value.toFixed(2)}`;
|
||
}
|
||
|
||
function elementAnchor(element: HTMLElement): TooltipAnchor {
|
||
return {
|
||
contextElement: element,
|
||
getBoundingClientRect: () => element.getBoundingClientRect(),
|
||
};
|
||
}
|
||
|
||
function InfoTooltip({ content }: { content: string }) {
|
||
const [anchor, setAnchor] = useState<TooltipAnchor | null>(null);
|
||
|
||
return <>
|
||
<button
|
||
type="button"
|
||
className={styles.info}
|
||
aria-label={t("查看说明")}
|
||
onMouseEnter={(event) => setAnchor(elementAnchor(event.currentTarget))}
|
||
onMouseLeave={() => setAnchor(null)}
|
||
onFocus={(event) => setAnchor(elementAnchor(event.currentTarget))}
|
||
onBlur={() => setAnchor(null)}
|
||
><Icon icon={informationOutlineIcon} size="1.1em" /></button>
|
||
<Tooltip anchor={anchor}>
|
||
<div className={styles.tooltipText}>{content}</div>
|
||
</Tooltip>
|
||
</>;
|
||
}
|
||
|
||
export function HomeMetrics({ data }: { data: HomeMetricsData }) {
|
||
const inputTokens = Math.max(0, data.promptTokens - data.cacheReadTokens - data.cacheWriteTokens);
|
||
const outputTokens = Math.max(0, data.tokenUsage - data.promptTokens);
|
||
const defaultCacheHitRate = calculateRate(data.cacheReadTokens, data.cacheReadTokens + inputTokens);
|
||
const cacheReuseRate = calculateRate(
|
||
data.cacheReadTokens,
|
||
data.cacheReadTokens + data.cacheWriteTokens + inputTokens,
|
||
);
|
||
const successfulCallRate = calculateRate(data.successfulCalls, data.llmCalls);
|
||
const costs = {
|
||
input: priceTokens(inputTokens, TOKEN_PRICE_PER_MILLION.input),
|
||
output: priceTokens(outputTokens, TOKEN_PRICE_PER_MILLION.output),
|
||
cacheRead: priceTokens(data.cacheReadTokens, TOKEN_PRICE_PER_MILLION.cacheRead),
|
||
cacheWrite: priceTokens(data.cacheWriteTokens, TOKEN_PRICE_PER_MILLION.cacheWrite),
|
||
};
|
||
const totalCost = costs.input + costs.output + costs.cacheRead + costs.cacheWrite;
|
||
const cacheCost = costs.cacheRead + costs.cacheWrite;
|
||
const cacheTooltip = [
|
||
t("当前:{rate}", { rate: formatRate(defaultCacheHitRate) }),
|
||
t("公式:缓存读取 /(缓存读取 + 非缓存输入)"),
|
||
t("默认 {defaultRate} / 计入创建 {reuseRate}", {
|
||
defaultRate: formatRate(defaultCacheHitRate),
|
||
reuseRate: formatRate(cacheReuseRate),
|
||
}),
|
||
].join("\n");
|
||
const callsTooltip = [
|
||
t("按历史 LLM 调用记录汇总,进行中的调用不计入。"),
|
||
"",
|
||
t("总调用:{count}", { count: formatMetricValue(data.llmCalls) }),
|
||
t("成功调用:{count}", { count: formatMetricValue(data.successfulCalls) }),
|
||
t("异常调用:{count}", { count: formatMetricValue(data.failedCalls) }),
|
||
t("成功占比:{rate}", { rate: formatRate(successfulCallRate) }),
|
||
].join("\n");
|
||
const tokensTooltip = [
|
||
t("总请求 Token 包含提示词和模型输出。"),
|
||
"",
|
||
t("总请求:{tokens}", { tokens: formatMetricValue(data.tokenUsage) }),
|
||
t("提示词:{tokens}", { tokens: formatMetricValue(data.promptTokens) }),
|
||
t("输出推算:{tokens}", { tokens: formatMetricValue(outputTokens) }),
|
||
t("非缓存输入:{tokens}", { tokens: formatMetricValue(inputTokens) }),
|
||
t("缓存读取:{tokens}", { tokens: formatMetricValue(data.cacheReadTokens) }),
|
||
t("缓存写入:{tokens}", { tokens: formatMetricValue(data.cacheWriteTokens) }),
|
||
"",
|
||
t("缓存读写已计入提示词侧统计。"),
|
||
].join("\n");
|
||
const costTooltip = [
|
||
t("按 Claude Opus 4.7 价格估算。"),
|
||
t("缓存统计策略:默认口径({rate})", { rate: formatRate(defaultCacheHitRate) }),
|
||
"",
|
||
t("普通输入:{tokens} × ${price}/1M = {cost}", {
|
||
tokens: formatMetricValue(inputTokens),
|
||
price: TOKEN_PRICE_PER_MILLION.input,
|
||
cost: formatUSD(costs.input),
|
||
}),
|
||
t("模型输出:{tokens} × ${price}/1M = {cost}", {
|
||
tokens: formatMetricValue(outputTokens),
|
||
price: TOKEN_PRICE_PER_MILLION.output,
|
||
cost: formatUSD(costs.output),
|
||
}),
|
||
t("缓存读取:{tokens} × ${price}/1M = {cost}", {
|
||
tokens: formatMetricValue(data.cacheReadTokens),
|
||
price: TOKEN_PRICE_PER_MILLION.cacheRead,
|
||
cost: formatUSD(costs.cacheRead),
|
||
}),
|
||
t("缓存写入:{tokens} × ${price}/1M = {cost}", {
|
||
tokens: formatMetricValue(data.cacheWriteTokens),
|
||
price: TOKEN_PRICE_PER_MILLION.cacheWrite,
|
||
cost: formatUSD(costs.cacheWrite),
|
||
}),
|
||
"",
|
||
t("合计:{cost}", { cost: formatUSD(totalCost) }),
|
||
].join("\n");
|
||
|
||
return <div className={styles.scroller}>
|
||
<section className={styles.root} aria-label={t("调用统计")}>
|
||
<article className={styles.metric}>
|
||
<div className={styles.label}>{t("缓存命中率")}<InfoTooltip content={cacheTooltip} /></div>
|
||
<CacheHitRateChart rate={defaultCacheHitRate ?? 0} />
|
||
</article>
|
||
<article className={styles.metric}>
|
||
<div className={styles.label}>{t("LLM 调用")}<InfoTooltip content={callsTooltip} /></div>
|
||
<div className={styles.body}>
|
||
<div className={styles.value} title={formatInteger(data.llmCalls)}>{formatCompactInteger(data.llmCalls)}</div>
|
||
<div className={styles.secondary}>{t("成功 {successful} / 异常 {failed}", {
|
||
successful: formatCompactInteger(data.successfulCalls),
|
||
failed: formatCompactInteger(data.failedCalls),
|
||
})}</div>
|
||
</div>
|
||
</article>
|
||
<article className={styles.metric}>
|
||
<div className={styles.label}>{t("Token 消耗")}<InfoTooltip content={tokensTooltip} /></div>
|
||
<div className={styles.body}>
|
||
<div className={styles.value} title={formatInteger(data.tokenUsage)}>{formatCompactInteger(data.tokenUsage)}</div>
|
||
<div className={styles.secondary}>{t("提示词 {tokens}", { tokens: formatCompactInteger(data.promptTokens) })}</div>
|
||
</div>
|
||
</article>
|
||
<article className={styles.metric}>
|
||
<div className={styles.label}>{t("价值估算")}<InfoTooltip content={costTooltip} /></div>
|
||
<div className={styles.body}>
|
||
<div className={styles.value} title={formatUSD(totalCost)}>{formatUSD(totalCost)}</div>
|
||
<div className={styles.secondary}>{t("缓存读写 {cost}", { cost: formatUSD(cacheCost) })}</div>
|
||
</div>
|
||
</article>
|
||
</section>
|
||
</div>;
|
||
}
|