feat(dashboard): 最近请求缓存命中列 + 中文模式人民币计价 + 价格解析记忆化
- 最近请求表新增缓存命中率列(缓存读取/总输入) - useCurrency:中文界面费用按最新 USD→CNY 汇率显示 ¥,英文保持 $(open.er-api.com,缓存 12h,失败兜底 7.2) - match_price 记忆化缓存:O(记录数×5910) → O(模型种类×5910),扫描从秒级降到百毫秒级 - 仪表盘右上角显示加载耗时/数据量(get_summary 往返埋点);Rust 侧分段计时
This commit is contained in:
1 parent
de132ffb3a
commit
38a11666ac
9 files changed
+173
-28
No files matched your search
@@ -1,7 +1,8 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { fmtPct, fmtTokens, fmtUsd } from "../../lib/dashboard-format";
|
||||
import { useCurrency } from "../../hooks/useCurrency";
|
||||
import { fmtPct, fmtTokens } from "../../lib/dashboard-format";
|
||||
import type { DailyRow } from "../../types/dashboard";
|
||||
import { DailyDetailModal } from "./DailyDetailModal";
|
||||
|
||||
@@ -43,6 +44,7 @@ function shortModel(model: string): string {
|
||||
|
||||
export function DailyBars({ daily, dimension, names, unknownProviderLabel }: DailyBarsProps) {
|
||||
const { t } = useTranslation();
|
||||
const { money } = useCurrency();
|
||||
const [selected, setSelected] = useState<DailyRow | null>(null);
|
||||
|
||||
const colorMap = useMemo(() => {
|
||||
@@ -103,7 +105,7 @@ export function DailyBars({ daily, dimension, names, unknownProviderLabel }: Dai
|
||||
|
||||
const tooltipLines = [
|
||||
d.date,
|
||||
`${t("totalTokens")}: ${fmtTokens(d.totalTokens)} · ${fmtUsd(d.costUsd)} · ${t("colCacheHit")} ${fmtPct(d.cacheHitRate)}`,
|
||||
`${t("totalTokens")}: ${fmtTokens(d.totalTokens)} · ${money(d.costUsd)} · ${t("colCacheHit")} ${fmtPct(d.cacheHitRate)}`,
|
||||
...segments.map((s) => `${displayName(s.key)}: ${fmtTokens(s.tokens)}`),
|
||||
"",
|
||||
t("doubleClickHint"),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { fmtPct, fmtTokens, fmtUsd } from "../../lib/dashboard-format";
|
||||
import { useCurrency } from "../../hooks/useCurrency";
|
||||
import { fmtPct, fmtTokens } from "../../lib/dashboard-format";
|
||||
import type { DailyRow } from "../../types/dashboard";
|
||||
import { modelColor } from "./DailyBars";
|
||||
|
||||
@@ -22,6 +23,7 @@ function shortModel(model: string): string {
|
||||
|
||||
export function DailyDetailModal({ day, dimension, colorMap, unknownProviderLabel, onClose }: DailyDetailModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { money } = useCurrency();
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
@@ -106,7 +108,7 @@ export function DailyDetailModal({ day, dimension, colorMap, unknownProviderLabe
|
||||
{/* Day summary chips */}
|
||||
<div className="grid grid-cols-2 gap-2 px-5 py-3 sm:grid-cols-4">
|
||||
<SummaryChip label={t("totalTokens")} value={fmtTokens(day.totalTokens)} tone="default" />
|
||||
<SummaryChip label={t("cost")} value={fmtUsd(day.costUsd)} tone="orange" />
|
||||
<SummaryChip label={t("cost")} value={money(day.costUsd)} tone="orange" />
|
||||
<SummaryChip label={t("requests")} value={Math.round(day.requests).toLocaleString()} tone="blue" />
|
||||
<SummaryChip label={t("cacheHitRate")} value={fmtPct(day.cacheHitRate)} tone="green" />
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { useDashboard, type DashboardRange } from "../../hooks/useDashboard";
|
||||
import { useCurrency } from "../../hooks/useCurrency";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { fmtInt, fmtPct, fmtTime, fmtTokens, fmtUsd } from "../../lib/dashboard-format";
|
||||
import { fmtInt, fmtPct, fmtTime, fmtTokens } from "../../lib/dashboard-format";
|
||||
import { DailyBars } from "./DailyBars";
|
||||
import { Heatmap } from "./Heatmap";
|
||||
import { TrendLineChart } from "./TrendLineChart";
|
||||
|
||||
const RANGES: DashboardRange[] = ["today", "7d", "30d", "all"];
|
||||
|
||||
/** Per-request cache hit rate: cached tokens / total input tokens. */
|
||||
function cacheHitOf(r: { inputOther: number; inputCacheRead: number }): number {
|
||||
const denom = r.inputOther + r.inputCacheRead;
|
||||
return denom > 0 ? r.inputCacheRead / denom : 0;
|
||||
}
|
||||
|
||||
function Card({
|
||||
title,
|
||||
subtitle,
|
||||
@@ -44,7 +51,8 @@ function KpiCard({ label, value, sub }: { label: string; value: string; sub?: st
|
||||
|
||||
export function DashboardPage() {
|
||||
const { t } = useTranslation();
|
||||
const { range, changeRange, data, loading, error, refresh } = useDashboard();
|
||||
const { money } = useCurrency();
|
||||
const { range, changeRange, data, loading, error, refresh, loadStats } = useDashboard();
|
||||
const [showAllModels, setShowAllModels] = useState(false);
|
||||
const [recentPage, setRecentPage] = useState(1);
|
||||
const [trendTab, setTrendTab] = useState<"daily" | "model" | "provider">("daily");
|
||||
@@ -120,28 +128,35 @@ export function DashboardPage() {
|
||||
{ label: t("kpiCacheCreation"), value: fmtTokens(totals.inputCacheCreation), sub: fmtInt(totals.inputCacheCreation) },
|
||||
{ label: t("kpiCacheHit"), value: fmtPct(totals.cacheHitRate) },
|
||||
{ label: t("totalTokens"), value: fmtTokens(totals.totalTokens), sub: fmtInt(totals.totalTokens) },
|
||||
{ label: t("kpiCost"), value: fmtUsd(totals.costUsd) },
|
||||
{ label: t("kpiCost"), value: money(totals.costUsd) },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto p-4 space-y-4">
|
||||
{/* Range tabs + refresh */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center bg-input border border-border rounded p-0.5">
|
||||
{RANGES.map((r) => (
|
||||
<button
|
||||
key={r}
|
||||
type="button"
|
||||
onClick={() => changeRange(r)}
|
||||
className={`px-3 py-1 text-sm rounded transition-colors ${
|
||||
range === r
|
||||
? "bg-blue-600 text-white"
|
||||
: "text-content-muted hover:text-content-primary"
|
||||
}`}
|
||||
>
|
||||
{RANGE_LABELS[r]}
|
||||
</button>
|
||||
))}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center bg-input border border-border rounded p-0.5">
|
||||
{RANGES.map((r) => (
|
||||
<button
|
||||
key={r}
|
||||
type="button"
|
||||
onClick={() => changeRange(r)}
|
||||
className={`px-3 py-1 text-sm rounded transition-colors ${
|
||||
range === r
|
||||
? "bg-blue-600 text-white"
|
||||
: "text-content-muted hover:text-content-primary"
|
||||
}`}
|
||||
>
|
||||
{RANGE_LABELS[r]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{loadStats && !loading && (
|
||||
<span className="hidden text-[10px] text-content-muted sm:inline">
|
||||
{t("loadStats", { ms: loadStats.ms, kb: loadStats.kb })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -175,7 +190,7 @@ export function DashboardPage() {
|
||||
{fmtTokens(tot.totalTokens)}
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] text-content-muted">
|
||||
{fmtInt(tot.requests)} {t("unitTimes")} · {fmtUsd(tot.costUsd)} · {fmtPct(tot.cacheHitRate)}
|
||||
{fmtInt(tot.requests)} {t("unitTimes")} · {money(tot.costUsd)} · {fmtPct(tot.cacheHitRate)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
@@ -305,7 +320,7 @@ export function DashboardPage() {
|
||||
<td className="py-2 pr-3 text-right text-content-muted">{fmtInt(m.requests)}</td>
|
||||
<td className="py-2 pr-3 text-right text-content-muted">{fmtTokens(m.totalTokens)}</td>
|
||||
<td className="py-2 pr-3 text-right text-content-muted">{fmtPct(m.cacheHitRate)}</td>
|
||||
<td className="py-2 text-right text-orange-600 dark:text-orange-400">{fmtUsd(m.costUsd)}</td>
|
||||
<td className="py-2 text-right text-orange-600 dark:text-orange-400">{money(m.costUsd)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -346,6 +361,7 @@ export function DashboardPage() {
|
||||
<th className="pb-2 pr-4 font-normal text-right">{t("colInput")}</th>
|
||||
<th className="pb-2 pr-4 font-normal text-right">{t("colOutput")}</th>
|
||||
<th className="pb-2 pr-4 font-normal text-right">{t("colCacheRead")}</th>
|
||||
<th className="pb-2 pr-4 font-normal text-right">{t("colCacheHit")}</th>
|
||||
<th className="pb-2 font-normal text-right">{t("cost")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -359,7 +375,8 @@ export function DashboardPage() {
|
||||
<td className="py-1.5 pr-4 text-right text-content-muted">{fmtTokens(r.inputOther)}</td>
|
||||
<td className="py-1.5 pr-4 text-right text-content-muted">{fmtTokens(r.output)}</td>
|
||||
<td className="py-1.5 pr-4 text-right text-content-muted">{fmtTokens(r.inputCacheRead)}</td>
|
||||
<td className="py-1.5 text-right text-orange-600 dark:text-orange-400">{fmtUsd(r.costUsd)}</td>
|
||||
<td className="py-1.5 pr-4 text-right text-content-muted">{fmtPct(cacheHitOf(r))}</td>
|
||||
<td className="py-1.5 text-right text-orange-600 dark:text-orange-400">{money(r.costUsd)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useMemo, useRef, useState, type MouseEvent } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { fmtPct, fmtTokens, fmtUsd } from "../../lib/dashboard-format";
|
||||
import { useCurrency } from "../../hooks/useCurrency";
|
||||
import { fmtPct, fmtTokens } from "../../lib/dashboard-format";
|
||||
import type { HeatmapCell, HeatmapData } from "../../types/dashboard";
|
||||
|
||||
interface HeatmapProps {
|
||||
@@ -220,6 +221,7 @@ export function Heatmap({ heatmap }: HeatmapProps) {
|
||||
|
||||
function HeatmapTooltip({ state }: { state: HoverState }) {
|
||||
const { t } = useTranslation();
|
||||
const { money } = useCurrency();
|
||||
const { cell, x, y } = state;
|
||||
// Position above the cursor with a small offset; flip below if too close to top.
|
||||
const offset = 14;
|
||||
@@ -239,7 +241,7 @@ function HeatmapTooltip({ state }: { state: HoverState }) {
|
||||
>
|
||||
<div className="font-medium text-content-primary">{cell.date}</div>
|
||||
<div className="text-content-muted">
|
||||
{fmtTokens(cell.totalTokens)} · {fmtUsd(cell.costUsd)} ·{" "}
|
||||
{fmtTokens(cell.totalTokens)} · {money(cell.costUsd)} ·{" "}
|
||||
{fmtInt(cell.requests)} {t("requests")}
|
||||
</div>
|
||||
<div className="text-content-muted">
|
||||
|
||||
Reference in new issue
Block a user