Files
zclaw_openfang/desktop/src/components/Settings/UsageStats.tsx
iven 1cf3f585d3 refactor(store): split gatewayStore into specialized domain stores
Major restructuring:
- Split monolithic gatewayStore into 5 focused stores:
  - connectionStore: WebSocket connection and gateway lifecycle
  - configStore: quickConfig, workspaceInfo, MCP services
  - agentStore: clones, usage stats, agent management
  - handStore: hands, approvals, triggers, hand runs
  - workflowStore: workflows, workflow runs, execution

- Update all components to use new stores with selector pattern
- Remove
2026-03-20 22:14:13 +08:00

178 lines
6.9 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useAgentStore } from '../../store/agentStore';
import { useConnectionStore } from '../../store/connectionStore';
import { BarChart3, TrendingUp, Clock, Zap } from 'lucide-react';
export function UsageStats() {
const usageStats = useAgentStore((s) => s.usageStats);
const loadUsageStats = useAgentStore((s) => s.loadUsageStats);
const connectionState = useConnectionStore((s) => s.connectionState);
const [timeRange, setTimeRange] = useState<'7d' | '30d' | 'all'>('7d');
useEffect(() => {
if (connectionState === 'connected') {
loadUsageStats();
}
}, [connectionState]);
const stats = usageStats || { totalSessions: 0, totalMessages: 0, totalTokens: 0, byModel: {} };
const models = Object.entries(stats.byModel || {});
const formatTokens = (n: number) => {
if (n >= 1_000_000) return `~${(n / 1_000_000).toFixed(1)} M`;
if (n >= 1_000) return `~${(n / 1_000).toFixed(1)} k`;
return `${n}`;
};
// 计算总输入和输出 Token
const totalInputTokens = models.reduce((sum, [_, data]) => sum + data.inputTokens, 0);
const totalOutputTokens = models.reduce((sum, [_, data]) => sum + data.outputTokens, 0);
return (
<div className="max-w-3xl">
<div className="flex justify-between items-center mb-6">
<h1 className="text-xl font-bold text-gray-900"></h1>
<div className="flex items-center gap-2">
<div className="flex items-center bg-gray-100 rounded-lg p-0.5">
{(['7d', '30d', 'all'] as const).map((range) => (
<button
key={range}
onClick={() => setTimeRange(range)}
className={`px-3 py-1 text-xs rounded-md transition-colors ${
timeRange === range
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-500 hover:text-gray-700'
}`}
>
{range === '7d' ? '近 7 天' : range === '30d' ? '近 30 天' : '全部'}
</button>
))}
</div>
<button
onClick={() => loadUsageStats()}
className="text-xs text-gray-500 hover:text-gray-700 px-3 py-1.5 border border-gray-200 rounded-lg transition-colors"
>
</button>
</div>
</div>
<div className="text-xs text-gray-500 mb-4"> Token </div>
{/* 主要统计卡片 */}
<div className="grid grid-cols-4 gap-4 mb-8">
<StatCard
icon={BarChart3}
label="会话数"
value={stats.totalSessions}
color="text-blue-500"
/>
<StatCard
icon={Zap}
label="消息数"
value={stats.totalMessages}
color="text-purple-500"
/>
<StatCard
icon={TrendingUp}
label="输入 Token"
value={formatTokens(totalInputTokens)}
color="text-green-500"
/>
<StatCard
icon={Clock}
label="输出 Token"
value={formatTokens(totalOutputTokens)}
color="text-orange-500"
/>
</div>
{/* 总 Token 使用量概览 */}
<div className="bg-white rounded-xl border border-gray-200 p-5 shadow-sm mb-6">
<h3 className="text-sm font-semibold mb-4 text-gray-900">Token 使</h3>
<div className="flex items-center gap-4">
<div className="flex-1">
<div className="flex justify-between text-xs text-gray-500 mb-1">
<span></span>
<span></span>
</div>
<div className="h-3 bg-gray-100 rounded-full overflow-hidden flex">
<div
className="bg-gradient-to-r from-green-400 to-green-500 h-full transition-all"
style={{ width: `${(totalInputTokens / Math.max(totalInputTokens + totalOutputTokens, 1)) * 100}%` }}
/>
<div
className="bg-gradient-to-r from-orange-400 to-orange-500 h-full transition-all"
style={{ width: `${(totalOutputTokens / Math.max(totalInputTokens + totalOutputTokens, 1)) * 100}%` }}
/>
</div>
</div>
<div className="text-right flex-shrink-0">
<div className="text-lg font-bold text-gray-900">{formatTokens(stats.totalTokens)}</div>
<div className="text-xs text-gray-500"></div>
</div>
</div>
</div>
{/* 按模型分组 */}
<h2 className="text-sm font-semibold mb-4 text-gray-900"></h2>
<div className="bg-white rounded-xl border border-gray-200 divide-y divide-gray-100 shadow-sm">
{models.length === 0 ? (
<div className="p-8 text-center">
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-3">
<BarChart3 className="w-6 h-6 text-gray-400" />
</div>
<p className="text-sm text-gray-400">使</p>
<p className="text-xs text-gray-300 mt-1"></p>
</div>
) : (
models.map(([model, data]) => {
const total = data.inputTokens + data.outputTokens;
const inputPct = (data.inputTokens / Math.max(total, 1)) * 100;
const outputPct = (data.outputTokens / Math.max(total, 1)) * 100;
return (
<div key={model} className="p-4">
<div className="flex justify-between items-center mb-2">
<span className="font-medium text-gray-900">{model}</span>
<span className="text-xs text-gray-500">{data.messages} </span>
</div>
<div className="h-2 bg-gray-100 rounded-full overflow-hidden mb-2 flex">
<div className="bg-orange-500 h-full" style={{ width: `${inputPct}%` }} />
<div className="bg-orange-200 h-full" style={{ width: `${outputPct}%` }} />
</div>
<div className="flex justify-between text-xs text-gray-500">
<span>: {formatTokens(data.inputTokens)}</span>
<span>: {formatTokens(data.outputTokens)}</span>
<span>: {formatTokens(total)}</span>
</div>
</div>
);
})
)}
</div>
</div>
);
}
function StatCard({
icon: Icon,
label,
value,
color,
}: {
icon: typeof BarChart3;
label: string;
value: string | number;
color: string;
}) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-4 shadow-sm">
<div className="flex items-center gap-2 mb-2">
<Icon className={`w-4 h-4 ${color}`} />
<span className="text-xs text-gray-500">{label}</span>
</div>
<div className="text-2xl font-bold text-gray-900">{value}</div>
</div>
);
}