feat: v0.4.0 - provider icon system, settings modal & polish
- Add provider icon system with extracted icon library (cc-switch based) - Persist icon and icon_color in SQLite and config.toml - Add Settings modal (theme / language / update check / silent download) - Add useTheme and useUpdateCheck hooks - Add provider icon picker (IconPicker) with search and grid - Fix icon frame 2-layer color difference in ProviderEdit - Dashboard heatmap and daily usage improvements - Sessions page polish and adaptive layout - Bump version to 0.4.0
This commit is contained in:
1 parent
14d48500bf
commit
ebf2dd28d5
134 files changed
+2898
-326
No files matched your search
@@ -49,7 +49,7 @@ export function DailyBars({ daily, modelNames }: DailyBarsProps) {
|
||||
|
||||
if (!daily?.length) {
|
||||
return (
|
||||
<div className="flex h-48 items-center justify-center text-sm text-gray-500">
|
||||
<div className="flex h-48 items-center justify-center text-sm text-content-muted">
|
||||
{t("noData")}
|
||||
</div>
|
||||
);
|
||||
@@ -58,11 +58,11 @@ export function DailyBars({ daily, modelNames }: DailyBarsProps) {
|
||||
const max = Math.max(...daily.map((d) => d.totalTokens || 0), 1);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex h-full flex-col gap-3">
|
||||
{/* Legend */}
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
||||
{modelNames.map((m) => (
|
||||
<span key={m} className="flex items-center gap-1.5 text-[11px] text-gray-400">
|
||||
<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] }}
|
||||
@@ -73,7 +73,7 @@ export function DailyBars({ daily, modelNames }: DailyBarsProps) {
|
||||
</div>
|
||||
|
||||
{/* Bars */}
|
||||
<div className="flex h-52 items-end gap-1 overflow-x-auto px-0.5 pb-1">
|
||||
<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;
|
||||
@@ -121,7 +121,7 @@ export function DailyBars({ daily, modelNames }: DailyBarsProps) {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="whitespace-nowrap text-[10px] text-gray-500">
|
||||
<span className="whitespace-nowrap text-[10px] text-content-muted">
|
||||
{d.date.slice(5)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -56,16 +56,16 @@ export function DailyDetailModal({ day, colorMap, onClose }: DailyDetailModalPro
|
||||
aria-label={day.date}
|
||||
>
|
||||
<div
|
||||
className="w-full max-w-lg rounded-xl border border-[#2a2a2e] bg-[#16161a] shadow-2xl"
|
||||
className="w-full max-w-lg rounded-xl border border-border bg-panel shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between border-b border-[#2a2a2e] px-5 py-3.5">
|
||||
<div className="flex items-start justify-between border-b border-border px-5 py-3.5">
|
||||
<div>
|
||||
<div className="text-[11px] uppercase tracking-wider text-gray-500">
|
||||
<div className="text-[11px] uppercase tracking-wider text-content-muted">
|
||||
{t("dayDetail")}
|
||||
</div>
|
||||
<div className="mt-0.5 text-lg font-semibold text-[#e5e5e7]">
|
||||
<div className="mt-0.5 text-lg font-semibold text-content-primary">
|
||||
{day.date}
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,7 +73,7 @@ export function DailyDetailModal({ day, colorMap, onClose }: DailyDetailModalPro
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label={t("close")}
|
||||
className="rounded-md p-1 text-gray-500 hover:bg-[#2a2a2e] hover:text-gray-200 transition-colors"
|
||||
className="rounded-md p-1 text-content-muted hover:bg-border hover:text-content-primary transition-colors"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -117,11 +117,11 @@ export function DailyDetailModal({ day, colorMap, onClose }: DailyDetailModalPro
|
||||
|
||||
{/* Per-model breakdown */}
|
||||
<div className="px-5 pb-4">
|
||||
<div className="mb-2 text-[11px] uppercase tracking-wider text-gray-500">
|
||||
<div className="mb-2 text-[11px] uppercase tracking-wider text-content-muted">
|
||||
{t("modelDistribution")} · {segments.length}
|
||||
</div>
|
||||
{segments.length === 0 ? (
|
||||
<div className="py-6 text-center text-sm text-gray-500">
|
||||
<div className="py-6 text-center text-sm text-content-muted">
|
||||
{t("noData")}
|
||||
</div>
|
||||
) : (
|
||||
@@ -135,19 +135,19 @@ export function DailyDetailModal({ day, colorMap, onClose }: DailyDetailModalPro
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span
|
||||
className="truncate text-[#e5e5e7]"
|
||||
className="truncate text-content-primary"
|
||||
title={s.model}
|
||||
>
|
||||
{shortModel(s.model)}
|
||||
</span>
|
||||
<span className="shrink-0 text-xs text-gray-400 tabular-nums">
|
||||
<span className="shrink-0 text-xs text-content-muted tabular-nums">
|
||||
{fmtTokens(s.tokens)}{" "}
|
||||
<span className="text-gray-500">
|
||||
<span className="text-content-muted">
|
||||
· {s.pct.toFixed(1)}%
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 h-1.5 w-full overflow-hidden rounded-full bg-[#2a2a2e]">
|
||||
<div className="mt-1 h-1.5 w-full overflow-hidden rounded-full bg-border">
|
||||
<div
|
||||
className="h-full rounded-full transition-all"
|
||||
style={{
|
||||
@@ -164,7 +164,7 @@ export function DailyDetailModal({ day, colorMap, onClose }: DailyDetailModalPro
|
||||
</div>
|
||||
|
||||
{/* Footer hint */}
|
||||
<div className="border-t border-[#2a2a2e] px-5 py-2 text-[11px] text-gray-500">
|
||||
<div className="border-t border-border px-5 py-2 text-[11px] text-content-muted">
|
||||
{t("modalEscHint")}
|
||||
</div>
|
||||
</div>
|
||||
@@ -183,15 +183,15 @@ function SummaryChip({
|
||||
}) {
|
||||
const valueClass =
|
||||
tone === "orange"
|
||||
? "text-orange-400"
|
||||
? "text-orange-600 dark:text-orange-400"
|
||||
: tone === "blue"
|
||||
? "text-blue-400"
|
||||
: tone === "green"
|
||||
? "text-emerald-400"
|
||||
: "text-[#e5e5e7]";
|
||||
? "text-emerald-600 dark:text-emerald-400"
|
||||
: "text-content-primary";
|
||||
return (
|
||||
<div className="rounded-md bg-[#1c1c20] px-3 py-2">
|
||||
<div className="text-[10px] uppercase tracking-wider text-gray-500">
|
||||
<div className="rounded-md bg-hover px-3 py-2">
|
||||
<div className="text-[10px] uppercase tracking-wider text-content-muted">
|
||||
{label}
|
||||
</div>
|
||||
<div className={`mt-0.5 text-sm font-semibold tabular-nums ${valueClass}`}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { useDashboard, type DashboardRange } from "../../hooks/useDashboard";
|
||||
import { fmtInt, fmtPct, fmtTime, fmtTokens, fmtUsd } from "../../lib/dashboard-format";
|
||||
import { DailyBars } from "./DailyBars";
|
||||
@@ -21,10 +22,10 @@ function Card({
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-xl border border-[#2a2a2e] bg-[#16161a]">
|
||||
<div className="border-b border-[#2a2a2e] px-4 py-2.5 flex items-baseline justify-between">
|
||||
<h3 className="text-sm font-medium text-[#e5e5e7]">{title}</h3>
|
||||
{subtitle && <span className="text-xs text-gray-500">{subtitle}</span>}
|
||||
<div className="rounded-xl border border-border bg-panel">
|
||||
<div className="border-b border-border px-4 py-2.5 flex items-baseline justify-between">
|
||||
<h3 className="text-sm font-medium text-content-primary">{title}</h3>
|
||||
{subtitle && <span className="text-xs text-content-muted">{subtitle}</span>}
|
||||
</div>
|
||||
<div className="p-4">{children}</div>
|
||||
</div>
|
||||
@@ -33,12 +34,12 @@ function Card({
|
||||
|
||||
function KpiCard({ label, value, sub }: { label: string; value: string; sub?: string }) {
|
||||
return (
|
||||
<div className="rounded-xl border border-[#2a2a2e] bg-[#16161a] p-3">
|
||||
<div className="text-[11px] text-gray-500">{label}</div>
|
||||
<div className="mt-1 text-lg font-semibold text-[#e5e5e7] tracking-tight">
|
||||
<div className="rounded-xl border border-border bg-panel p-3">
|
||||
<div className="text-[11px] text-content-muted">{label}</div>
|
||||
<div className="mt-1 text-lg font-semibold text-content-primary tracking-tight">
|
||||
{value}
|
||||
</div>
|
||||
{sub && <div className="mt-0.5 text-[11px] text-gray-500">{sub}</div>}
|
||||
{sub && <div className="mt-0.5 text-[11px] text-content-muted">{sub}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -50,7 +51,7 @@ export function DashboardPage() {
|
||||
|
||||
if (loading && !data) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-gray-500">
|
||||
<div className="flex h-full items-center justify-center text-content-muted">
|
||||
<div className="animate-spin h-8 w-8 border-2 border-blue-500 border-t-transparent rounded-full" />
|
||||
</div>
|
||||
);
|
||||
@@ -61,7 +62,7 @@ export function DashboardPage() {
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-red-400 mb-2">加载失败</div>
|
||||
<div className="text-sm text-gray-500">{error}</div>
|
||||
<div className="text-sm text-content-muted">{error}</div>
|
||||
<button
|
||||
onClick={() => refresh()}
|
||||
className="mt-4 px-4 py-2 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm"
|
||||
@@ -109,7 +110,7 @@ export function DashboardPage() {
|
||||
<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-[#1f1f23] border border-[#2a2a2e] rounded p-0.5">
|
||||
<div className="flex items-center bg-input border border-border rounded p-0.5">
|
||||
{RANGES.map((r) => (
|
||||
<button
|
||||
key={r.value}
|
||||
@@ -118,7 +119,7 @@ export function DashboardPage() {
|
||||
className={`px-3 py-1 text-sm rounded transition-colors ${
|
||||
range === r.value
|
||||
? "bg-blue-600 text-white"
|
||||
: "text-gray-400 hover:text-[#e5e5e7]"
|
||||
: "text-content-muted hover:text-content-primary"
|
||||
}`}
|
||||
>
|
||||
{r.label}
|
||||
@@ -129,7 +130,7 @@ export function DashboardPage() {
|
||||
type="button"
|
||||
onClick={() => refresh()}
|
||||
disabled={loading}
|
||||
className="px-3 py-1.5 text-sm border border-[#2a2a2e] rounded hover:bg-[#252529] text-gray-400 hover:text-[#e5e5e7] disabled:opacity-50"
|
||||
className="px-3 py-1.5 text-sm border border-border rounded hover:bg-hover-2 text-content-muted hover:text-content-primary disabled:opacity-50"
|
||||
>
|
||||
{loading ? "刷新中…" : "↻ 刷新"}
|
||||
</button>
|
||||
@@ -149,14 +150,14 @@ export function DashboardPage() {
|
||||
className={`rounded-xl border p-4 text-left transition-colors ${
|
||||
active
|
||||
? "border-blue-500/50 ring-1 ring-blue-500/30 bg-blue-900/10"
|
||||
: "border-[#2a2a2e] bg-[#16161a] hover:border-[#3a3a42] hover:bg-[#1c1c20]"
|
||||
: "border-border bg-panel hover:border-strong hover:bg-hover"
|
||||
}`}
|
||||
>
|
||||
<div className="text-xs text-gray-500">{r.label}</div>
|
||||
<div className="mt-1 text-lg font-semibold text-[#e5e5e7] tracking-tight">
|
||||
<div className="text-xs text-content-muted">{r.label}</div>
|
||||
<div className="mt-1 text-lg font-semibold text-content-primary tracking-tight">
|
||||
{fmtTokens(tot.totalTokens)}
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] text-gray-500">
|
||||
<div className="mt-1 text-[11px] text-content-muted">
|
||||
{fmtInt(tot.requests)} 次 · {fmtUsd(tot.costUsd)} · {fmtPct(tot.cacheHitRate)}
|
||||
</div>
|
||||
</button>
|
||||
@@ -186,20 +187,22 @@ export function DashboardPage() {
|
||||
: `按模型着色 · ${daily.length} 天`
|
||||
}
|
||||
>
|
||||
<DailyBars
|
||||
daily={daily.length > 30 ? daily.slice(-30) : daily}
|
||||
modelNames={modelNames}
|
||||
/>
|
||||
<div className="h-full min-h-[240px]">
|
||||
<DailyBars
|
||||
daily={daily.length > 30 ? daily.slice(-30) : daily}
|
||||
modelNames={modelNames}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="模型用量" subtitle={`${models.length} 个模型`}>
|
||||
{models.length === 0 ? (
|
||||
<div className="py-8 text-center text-sm text-gray-500">暂无数据</div>
|
||||
<div className="py-8 text-center text-sm text-content-muted">暂无数据</div>
|
||||
) : (
|
||||
<div className="max-h-[360px] overflow-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 bg-[#16161a]">
|
||||
<tr className="text-left text-xs text-gray-500 border-b border-[#2a2a2e]">
|
||||
<thead className="sticky top-0 bg-panel">
|
||||
<tr className="text-left text-xs text-content-muted border-b border-border">
|
||||
<th className="pb-2 pr-3 font-normal">模型</th>
|
||||
<th className="pb-2 pr-3 font-normal text-right">请求</th>
|
||||
<th className="pb-2 pr-3 font-normal text-right">Token</th>
|
||||
@@ -211,7 +214,7 @@ export function DashboardPage() {
|
||||
{visibleModels.map((m) => (
|
||||
<tr
|
||||
key={m.model}
|
||||
className="border-b border-[#2a2a2e]/50 hover:bg-[#1c1c20]"
|
||||
className="border-b border-border/50 hover:bg-hover"
|
||||
>
|
||||
<td className="py-2 pr-3">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -225,17 +228,17 @@ export function DashboardPage() {
|
||||
}}
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<div className="text-[#e5e5e7] truncate">{m.modelDisplay || m.model}</div>
|
||||
<div className="text-content-primary truncate">{m.modelDisplay || m.model}</div>
|
||||
{m.costEstimated && (
|
||||
<span className="text-[10px] text-yellow-600">估算</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2 pr-3 text-right text-gray-400">{fmtInt(m.requests)}</td>
|
||||
<td className="py-2 pr-3 text-right text-gray-400">{fmtTokens(m.totalTokens)}</td>
|
||||
<td className="py-2 pr-3 text-right text-gray-400">{fmtPct(m.cacheHitRate)}</td>
|
||||
<td className="py-2 text-right text-orange-400">{fmtUsd(m.costUsd)}</td>
|
||||
<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>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -264,13 +267,13 @@ export function DashboardPage() {
|
||||
}
|
||||
>
|
||||
{recent.length === 0 ? (
|
||||
<div className="py-8 text-center text-sm text-gray-500">暂无数据</div>
|
||||
<div className="py-8 text-center text-sm text-content-muted">暂无数据</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 bg-[#16161a]">
|
||||
<tr className="text-left text-xs text-gray-500 border-b border-[#2a2a2e]">
|
||||
<thead className="sticky top-0 bg-panel">
|
||||
<tr className="text-left text-xs text-content-muted border-b border-border">
|
||||
<th className="pb-2 pr-4 font-normal">时间</th>
|
||||
<th className="pb-2 pr-4 font-normal">模型</th>
|
||||
<th className="pb-2 pr-4 font-normal text-right">输入</th>
|
||||
@@ -281,15 +284,15 @@ export function DashboardPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{recentPageRows.map((r, i) => (
|
||||
<tr key={recentStart + i} className="border-b border-[#2a2a2e]/50 hover:bg-[#1c1c20]">
|
||||
<td className="py-1.5 pr-4 text-gray-500 whitespace-nowrap">
|
||||
<tr key={recentStart + i} className="border-b border-border/50 hover:bg-hover">
|
||||
<td className="py-1.5 pr-4 text-content-muted whitespace-nowrap">
|
||||
{fmtTime(r.time)}
|
||||
</td>
|
||||
<td className="py-1.5 pr-4 text-[#e5e5e7]">{r.modelDisplay || r.model}</td>
|
||||
<td className="py-1.5 pr-4 text-right text-gray-400">{fmtTokens(r.inputOther)}</td>
|
||||
<td className="py-1.5 pr-4 text-right text-gray-400">{fmtTokens(r.output)}</td>
|
||||
<td className="py-1.5 pr-4 text-right text-gray-400">{fmtTokens(r.inputCacheRead)}</td>
|
||||
<td className="py-1.5 text-right text-orange-400">{fmtUsd(r.costUsd)}</td>
|
||||
<td className="py-1.5 pr-4 text-content-primary">{r.modelDisplay || r.model}</td>
|
||||
<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>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -297,7 +300,7 @@ export function DashboardPage() {
|
||||
</div>
|
||||
|
||||
{/* Pagination footer */}
|
||||
<div className="mt-2 flex items-center justify-between gap-2 border-t border-[#2a2a2e] pt-2 text-xs text-gray-500">
|
||||
<div className="mt-2 flex items-center justify-between gap-2 border-t border-border pt-2 text-xs text-content-muted">
|
||||
<span>
|
||||
第 {fmtInt(recentCurrentPage)} / {fmtInt(recentTotalPages)} 页 ·{" "}
|
||||
{fmtInt(recentStart + 1)}–{fmtInt(Math.min(recentStart + RECENT_PAGE_SIZE, recent.length))} 条 ·{" "}
|
||||
@@ -308,7 +311,7 @@ export function DashboardPage() {
|
||||
type="button"
|
||||
disabled={recentCurrentPage <= 1}
|
||||
onClick={() => setRecentPage(1)}
|
||||
className="px-2 py-1 rounded border border-[#2a2a2e] hover:bg-[#252529] disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="px-2 py-1 rounded border border-border hover:bg-hover-2 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
aria-label="首页"
|
||||
>
|
||||
«
|
||||
@@ -317,7 +320,7 @@ export function DashboardPage() {
|
||||
type="button"
|
||||
disabled={recentCurrentPage <= 1}
|
||||
onClick={() => setRecentPage((p) => Math.max(1, p - 1))}
|
||||
className="px-2 py-1 rounded border border-[#2a2a2e] hover:bg-[#252529] disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="px-2 py-1 rounded border border-border hover:bg-hover-2 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
aria-label="上一页"
|
||||
>
|
||||
‹
|
||||
@@ -326,7 +329,7 @@ export function DashboardPage() {
|
||||
type="button"
|
||||
disabled={recentCurrentPage >= recentTotalPages}
|
||||
onClick={() => setRecentPage((p) => Math.min(recentTotalPages, p + 1))}
|
||||
className="px-2 py-1 rounded border border-[#2a2a2e] hover:bg-[#252529] disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="px-2 py-1 rounded border border-border hover:bg-hover-2 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
aria-label="下一页"
|
||||
>
|
||||
›
|
||||
@@ -335,7 +338,7 @@ export function DashboardPage() {
|
||||
type="button"
|
||||
disabled={recentCurrentPage >= recentTotalPages}
|
||||
onClick={() => setRecentPage(recentTotalPages)}
|
||||
className="px-2 py-1 rounded border border-[#2a2a2e] hover:bg-[#252529] disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="px-2 py-1 rounded border border-border hover:bg-hover-2 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
aria-label="末页"
|
||||
>
|
||||
»
|
||||
@@ -347,19 +350,18 @@ export function DashboardPage() {
|
||||
</Card>
|
||||
|
||||
{/* Attribution footer */}
|
||||
<div className="text-xs text-gray-500 pb-2">
|
||||
<div className="text-xs text-content-muted pb-2">
|
||||
扫描 {data.meta.filesScanned} 个文件 · {fmtInt(data.meta.recordCount)} 条记录 ·{" "}
|
||||
{data.meta.home}
|
||||
<br />
|
||||
仪表盘功能基于{" "}
|
||||
<a
|
||||
href="https://github.com/JochenYang/kimicode-dashboard"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:text-blue-400 underline"
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openUrl("https://github.com/JochenYang/kimicode-dashboard")}
|
||||
className="text-blue-500 hover:text-blue-400 underline bg-transparent p-0 border-0 cursor-pointer"
|
||||
>
|
||||
kimicode-dashboard
|
||||
</a>{" "}
|
||||
</button>{" "}
|
||||
(MIT,© JochenYang)移植
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ interface HeatmapProps {
|
||||
}
|
||||
|
||||
const LEVEL_BG = [
|
||||
"bg-[#1a1a1e]", // 0 — empty / no usage
|
||||
"bg-cell-empty", // 0 — empty / no usage
|
||||
"bg-emerald-900/55",
|
||||
"bg-emerald-700/65",
|
||||
"bg-emerald-500/85",
|
||||
@@ -101,7 +101,7 @@ export function Heatmap({ heatmap }: HeatmapProps) {
|
||||
|
||||
if (!cells.length) {
|
||||
return (
|
||||
<div className="flex h-28 items-center justify-center text-sm text-gray-500">
|
||||
<div className="flex h-28 items-center justify-center text-sm text-content-muted">
|
||||
{t("noData")}
|
||||
</div>
|
||||
);
|
||||
@@ -129,7 +129,7 @@ export function Heatmap({ heatmap }: HeatmapProps) {
|
||||
return (
|
||||
<div
|
||||
key={wi}
|
||||
className="text-[10px] leading-none text-gray-500 whitespace-nowrap overflow-visible"
|
||||
className="text-[10px] leading-none text-content-muted whitespace-nowrap overflow-visible"
|
||||
>
|
||||
{label ? label.label : ""}
|
||||
</div>
|
||||
@@ -146,7 +146,7 @@ export function Heatmap({ heatmap }: HeatmapProps) {
|
||||
{dowLabels.map((d, i) => (
|
||||
<div
|
||||
key={d}
|
||||
className={`flex items-center text-[10px] text-gray-500 ${
|
||||
className={`flex items-center text-[10px] text-content-muted ${
|
||||
i % 2 === 1 ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
style={{ height: CELL }}
|
||||
@@ -193,7 +193,7 @@ export function Heatmap({ heatmap }: HeatmapProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-3 text-[11px] text-gray-500">
|
||||
<div className="flex items-center justify-between gap-3 text-[11px] text-content-muted">
|
||||
<span>
|
||||
{heatmap.start} → {heatmap.end}
|
||||
</span>
|
||||
@@ -234,15 +234,15 @@ function HeatmapTooltip({ state }: { state: HoverState }) {
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className="rounded-md border border-gray-700 bg-gray-900 px-3 py-1.5 text-xs text-gray-100 shadow-xl"
|
||||
className="rounded-md border border-border bg-panel px-3 py-1.5 text-xs text-content-primary shadow-xl"
|
||||
role="tooltip"
|
||||
>
|
||||
<div className="font-medium text-gray-50">{cell.date}</div>
|
||||
<div className="text-gray-400">
|
||||
<div className="font-medium text-content-primary">{cell.date}</div>
|
||||
<div className="text-content-muted">
|
||||
{fmtTokens(cell.totalTokens)} · {fmtUsd(cell.costUsd)} ·{" "}
|
||||
{fmtInt(cell.requests)} {t("requests")}
|
||||
</div>
|
||||
<div className="text-gray-400">
|
||||
<div className="text-content-muted">
|
||||
{t("cacheHitRate")}: {fmtPct(cell.cacheHitRate)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user