Files
CC-Switch/src/components/usage/RequestDetailPanel.tsx
T
Jason e8b07cb2a5 feat(usage): claude-desktop filter and pricing-model audit display
- add claude-desktop to AppType/KNOWN_APP_TYPES and the dashboard app
  filter; it was hidden because its rows looked like pure failure
  noise, which was the app_type attribution bug fixed on the backend
- request detail panel now shows the requested model and the pricing
  model when they differ from the response model, making route-takeover
  bills auditable from the UI
- locale keys added for zh/en/ja/zh-TW
2026-06-11 08:29:29 +08:00

325 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useTranslation } from "react-i18next";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useRequestDetail } from "@/lib/query/usage";
import { getFreshInputTokens, isUnpricedUsage } from "@/types/usage";
interface RequestDetailPanelProps {
requestId: string;
onClose: () => void;
}
export function RequestDetailPanel({
requestId,
onClose,
}: RequestDetailPanelProps) {
const { t, i18n } = useTranslation();
const { data: request, isLoading } = useRequestDetail(requestId);
const dateLocale =
i18n.language === "zh"
? "zh-CN"
: i18n.language === "zh-TW"
? "zh-TW"
: i18n.language === "ja"
? "ja-JP"
: "en-US";
if (isLoading) {
return (
<Dialog open onOpenChange={onClose}>
<DialogContent className="max-w-2xl">
<div className="h-[400px] animate-pulse rounded bg-gray-100" />
</DialogContent>
</Dialog>
);
}
if (!request) {
return (
<Dialog open onOpenChange={onClose}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{t("usage.requestDetail", "请求详情")}</DialogTitle>
</DialogHeader>
<div className="text-center text-muted-foreground">
{t("usage.requestNotFound", "请求未找到")}
</div>
</DialogContent>
</Dialog>
);
}
const freshInput = getFreshInputTokens(request);
const isCacheInclusive = request.inputTokens !== freshInput;
const unpriced = isUnpricedUsage(request);
return (
<Dialog open onOpenChange={onClose}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{t("usage.requestDetail", "请求详情")}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{/* 基本信息 */}
<div className="rounded-lg border p-4">
<h3 className="mb-3 font-semibold">
{t("usage.basicInfo", "基本信息")}
</h3>
<dl className="grid grid-cols-2 gap-3 text-sm">
<div>
<dt className="text-muted-foreground">
{t("usage.requestId", "请求ID")}
</dt>
<dd className="font-mono">{request.requestId}</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.time", "时间")}
</dt>
<dd>
{new Date(request.createdAt * 1000).toLocaleString(
dateLocale,
)}
</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.provider", "供应商")}
</dt>
<dd className="text-sm">
<span className="font-medium">
{request.providerName || t("usage.unknownProvider", "未知")}
</span>
<span className="ml-2 font-mono text-xs text-muted-foreground">
{request.providerId}
</span>
</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.appType", "应用类型")}
</dt>
<dd>{request.appType}</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.model", "模型")}
</dt>
<dd className="font-mono">{request.model}</dd>
{request.requestModel &&
request.requestModel !== request.model && (
<>
<dt className="mt-1 text-muted-foreground">
{t("usage.requestModel", "请求模型")}
</dt>
<dd className="font-mono text-xs">
{request.requestModel}
</dd>
</>
)}
{request.pricingModel &&
request.pricingModel !== request.model && (
<>
<dt className="mt-1 text-muted-foreground">
{t("usage.pricingModel", "计价模型")}
</dt>
<dd className="font-mono text-xs">
{request.pricingModel}
</dd>
</>
)}
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.status", "状态")}
</dt>
<dd>
<span
className={`inline-flex rounded-full px-2 py-1 text-xs ${
request.statusCode >= 200 && request.statusCode < 300
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{request.statusCode}
</span>
</dd>
</div>
</dl>
</div>
{/* Token 使用量 */}
<div className="rounded-lg border p-4">
<h3 className="mb-3 font-semibold">
{t("usage.tokenUsage", "Token 使用量")}
</h3>
<dl className="grid grid-cols-2 gap-3 text-sm">
<div>
<dt className="text-muted-foreground">
{t("usage.inputTokens", "输入 Tokens")}
</dt>
<dd className="font-mono">
{freshInput.toLocaleString()}
{isCacheInclusive && (
<span className="ml-2 text-xs text-muted-foreground/70 font-normal">
({t("usage.rawInputLabel", "原始")}:{" "}
{request.inputTokens.toLocaleString()})
</span>
)}
</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.outputTokens", "输出 Tokens")}
</dt>
<dd className="font-mono">
{request.outputTokens.toLocaleString()}
</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.cacheReadTokens", "缓存读取")}
</dt>
<dd className="font-mono">
{request.cacheReadTokens.toLocaleString()}
</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.cacheCreationTokens", "缓存写入")}
</dt>
<dd className="font-mono">
{request.cacheCreationTokens.toLocaleString()}
</dd>
</div>
<div className="col-span-2">
<dt className="text-muted-foreground">
{t("usage.totalTokens", "总计")}
</dt>
<dd className="text-lg font-semibold">
{(freshInput + request.outputTokens).toLocaleString()}
</dd>
</div>
</dl>
</div>
{/* 成本明细 */}
<div className="rounded-lg border p-4">
<h3 className="mb-3 font-semibold">
{t("usage.costBreakdown", "成本明细")}
</h3>
<dl className="grid grid-cols-2 gap-3 text-sm">
<div>
<dt className="text-muted-foreground">
{t("usage.inputCost", "输入成本")}
<span className="ml-1 text-xs">
({t("usage.baseCost", "基础")})
</span>
</dt>
<dd className="font-mono">
${parseFloat(request.inputCostUsd).toFixed(6)}
</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.outputCost", "输出成本")}
<span className="ml-1 text-xs">
({t("usage.baseCost", "基础")})
</span>
</dt>
<dd className="font-mono">
${parseFloat(request.outputCostUsd).toFixed(6)}
</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.cacheReadCost", "缓存读取成本")}
<span className="ml-1 text-xs">
({t("usage.baseCost", "基础")})
</span>
</dt>
<dd className="font-mono">
${parseFloat(request.cacheReadCostUsd).toFixed(6)}
</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.cacheCreationCost", "缓存写入成本")}
<span className="ml-1 text-xs">
({t("usage.baseCost", "基础")})
</span>
</dt>
<dd className="font-mono">
${parseFloat(request.cacheCreationCostUsd).toFixed(6)}
</dd>
</div>
{/* 显示成本倍率(如果不等于1) */}
{request.costMultiplier &&
parseFloat(request.costMultiplier) !== 1 && (
<div className="col-span-2 border-t pt-3">
<dt className="text-muted-foreground">
{t("usage.costMultiplier", "成本倍率")}
</dt>
<dd className="font-mono">×{request.costMultiplier}</dd>
</div>
)}
<div
className={`col-span-2 ${request.costMultiplier && parseFloat(request.costMultiplier) !== 1 ? "" : "border-t"} pt-3`}
>
<dt className="text-muted-foreground">
{t("usage.totalCost", "总成本")}
{request.costMultiplier &&
parseFloat(request.costMultiplier) !== 1 && (
<span className="ml-1 text-xs">
({t("usage.withMultiplier", "含倍率")})
</span>
)}
</dt>
<dd
className={`text-lg font-semibold ${
unpriced ? "text-muted-foreground" : "text-primary"
}`}
>
{unpriced
? t("usage.unpriced", "未定价")
: `$${parseFloat(request.totalCostUsd).toFixed(6)}`}
</dd>
</div>
</dl>
</div>
{/* 性能信息 */}
<div className="rounded-lg border p-4">
<h3 className="mb-3 font-semibold">
{t("usage.performance", "性能信息")}
</h3>
<dl className="grid grid-cols-2 gap-3 text-sm">
<div>
<dt className="text-muted-foreground">
{t("usage.latency", "延迟")}
</dt>
<dd className="font-mono">{request.latencyMs}ms</dd>
</div>
</dl>
</div>
{/* 错误信息 */}
{request.errorMessage && (
<div className="rounded-lg border border-red-200 bg-red-50 p-4">
<h3 className="mb-2 font-semibold text-red-800">
{t("usage.errorMessage", "错误信息")}
</h3>
<p className="text-sm text-red-700">{request.errorMessage}</p>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
}