Files
KimiCodeSwitch/src/components/dashboard/DailyBars.tsx
T
KimiSwitch Dev 8aed04898c
Release / Version consistency (push) Canceled after 0s
Release / Build (macos-latest) (push) Canceled after 0s
Release / Build (ubuntu-latest) (push) Canceled after 0s
Release / Build (windows-latest) (push) Canceled after 0s
feat(subagent): 实验功能设置新增子代理模型配置
- 子代理设置页支持选择次主力模型,仅保存模型引用(不再写补丁字段),
  子代理直接继承所选模型的上下文长度与思考模式
- 界面只读展示继承的上下文长度与思考模式,移除补丁编辑项
- 仪表盘统计识别子代理请求(__secondary__):归为「子代理模型(估算)」
  并缓存次主力模型定价;自定义网关模型价格按官方同名模型跨 provider 匹配
- bump v0.7.1
2026-08-05 23:38:05 +08:00

163 lines
5.1 KiB
TypeScript

import { useMemo, useState } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "../../i18n";
import { useCurrency } from "../../hooks/useCurrency";
import { fmtPct, fmtTokens } from "../../lib/dashboard-format";
import type { DailyRow } from "../../types/dashboard";
import { DailyDetailModal } from "./DailyDetailModal";
type TrendDimension = "model" | "provider";
interface DailyBarsProps {
daily: DailyRow[];
/** Which per-day breakdown to render */
dimension: TrendDimension;
/** Ordered names for the active dimension (colour palette & legend) */
names: string[];
/** Label for the unresolved-provider bucket */
unknownProviderLabel: string;
}
/** Deterministic colour palette — one hue per index */
const PALETTE = [
"#3b82f6", // blue
"#22c55e", // green
"#f97316", // orange
"#a855f7", // purple
"#ec4899", // pink
"#eab308", // yellow
"#06b6d4", // cyan
"#f43f5e", // rose
"#84cc16", // lime
"#8b5cf6", // violet
];
export function modelColor(index: number): string {
return PALETTE[index % PALETTE.length];
}
/** Short display name for a raw model id */
function shortModel(model: string): string {
const bare = model.includes("/") ? model.split("/").pop()! : model;
return bare;
}
export function DailyBars({ daily, dimension, names, unknownProviderLabel }: DailyBarsProps) {
const { t } = useTranslation();
const { money } = useCurrency();
const [selected, setSelected] = useState<DailyRow | null>(null);
const colorMap = useMemo(() => {
const map: Record<string, string> = {};
names.forEach((m, i) => {
map[m] = modelColor(i);
});
return map;
}, [names]);
const displayName = (key: string): string => {
if (dimension === "provider") {
return key === "unknown" ? unknownProviderLabel : key;
}
// Subagent records (Kimi Code `__secondary__` marker) show their own label.
if (key === "__secondary__") {
return t("subagentModel");
}
return shortModel(key);
};
if (!daily?.length) {
return (
<div className="flex h-48 items-center justify-center text-sm text-content-muted">
{t("noData")}
</div>
);
}
const max = Math.max(...daily.map((d) => d.totalTokens || 0), 1);
return (
<div className="flex h-full flex-col gap-3">
{/* Legend */}
<div className="flex flex-wrap gap-x-4 gap-y-1">
{names.map((m) => (
<span key={m} className="flex items-center gap-1.5 text-[11px] text-content-muted">
<span
className="inline-block h-2.5 w-2.5 rounded-sm"
style={{ backgroundColor: colorMap[m] }}
/>
{displayName(m)}
</span>
))}
</div>
{/* Bars */}
<div className="flex min-h-[180px] flex-1 items-end gap-1 overflow-x-auto px-0.5 pb-1">
{daily.map((d) => {
const h = Math.max(4, Math.round(((d.totalTokens || 0) / max) * 170));
const total = d.totalTokens || 1;
const breakdown = dimension === "provider" ? d.byProvider : d.byModel;
const segments = Object.entries(breakdown || {})
.map(([key, tokens]) => ({
key,
tokens,
color: colorMap[key] || "#6b7280",
height: Math.max(1, Math.round((tokens / total) * h)),
}))
.sort((a, b) => b.tokens - a.tokens);
const tooltipLines = [
d.date,
`${t("totalTokens")}: ${fmtTokens(d.totalTokens)} · ${money(d.costUsd)} · ${t("colCacheHit")} ${fmtPct(d.cacheHitRate)}`,
...segments.map((s) => `${displayName(s.key)}: ${fmtTokens(s.tokens)}`),
"",
t("doubleClickHint"),
].join("\n");
return (
<button
key={d.date}
type="button"
title={tooltipLines}
onDoubleClick={() => setSelected(d)}
className="group flex min-w-[18px] flex-1 flex-col items-center gap-1.5 border-0 bg-transparent p-0 cursor-help"
>
<div
className="w-full flex flex-col-reverse rounded-t-md rounded-b-sm overflow-hidden transition-transform group-hover:scale-105 group-active:scale-95"
style={{ height: h }}
>
{segments.map((s) => (
<div
key={s.key}
className="w-full transition-opacity group-hover:opacity-80"
style={{
height: s.height,
backgroundColor: s.color,
}}
/>
))}
</div>
<span className="whitespace-nowrap text-[10px] text-content-muted">
{d.date.slice(5)}
</span>
</button>
);
})}
</div>
{selected &&
createPortal(
<DailyDetailModal
day={selected}
dimension={dimension}
colorMap={colorMap}
unknownProviderLabel={unknownProviderLabel}
onClose={() => setSelected(null)}
/>,
document.body
)}
</div>
);
}