feat(ui): Phase 8 UI/UX optimization and system documentation update
## Sidebar Enhancement - Change tabs to icon + small label layout for better space utilization - Add Teams tab with team collaboration entry point ## Settings Page Improvements - Connect theme toggle to gatewayStore.saveQuickConfig for persistence - Remove OpenFang backend download section, simplify UI - Add time range filter to UsageStats (7d/30d/all) - Add stat cards with icons (sessions, messages, input/output tokens) - Add token usage overview bar chart - Add 8 ZCLAW system skill definitions with categories ## Bug Fixes - Fix ChannelList duplicate content with deduplication logic - Integrate CreateTriggerModal in TriggersPanel - Add independent SecurityStatusPanel with 12 default enabled layers - Change workflow view to use SchedulerPanel as unified entry ## New Components - CreateTriggerModal: Event trigger creation modal - HandApprovalModal: Hand approval workflow dialog - HandParamsForm: Enhanced Hand parameter form - SecurityLayersPanel: 16-layer security status display ## Architecture - Add TOML config parsing support (toml-utils.ts, config-parser.ts) - Add request timeout and retry mechanism (request-helper.ts) - Add secure token storage (secure-storage.ts, secure_storage.rs) ## Tests - Add unit tests for config-parser, toml-utils, request-helper - Add team-client and teamStore tests ## Documentation - Update SYSTEM_ANALYSIS.md with Phase 8 completion - UI completion: 100% (30/30 components) - API coverage: 93% (63/68 endpoints) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useGatewayStore } from '../../store/gatewayStore';
|
||||
import { BarChart3, TrendingUp, Clock, Zap } from 'lucide-react';
|
||||
|
||||
export function UsageStats() {
|
||||
const { usageStats, loadUsageStats, connectionState } = useGatewayStore();
|
||||
const [timeRange, setTimeRange] = useState<'7d' | '30d' | 'all'>('7d');
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionState === 'connected') {
|
||||
@@ -19,62 +21,154 @@ export function UsageStats() {
|
||||
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>
|
||||
<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 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-3 gap-4 mb-8">
|
||||
<StatCard label="会话数" value={stats.totalSessions} />
|
||||
<StatCard label="消息数" value={stats.totalMessages} />
|
||||
<StatCard label="总 Token" value={formatTokens(stats.totalTokens)} />
|
||||
{/* 主要统计卡片 */}
|
||||
<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-4 text-sm text-gray-400 text-center">暂无数据</div>
|
||||
)}
|
||||
{models.map(([model, data]) => {
|
||||
const total = data.inputTokens + data.outputTokens;
|
||||
// Scale to 100% of the bar width based on max token usage across models for relative sizing.
|
||||
// Or we can just calculate input vs output within the model. Let's do input vs output within the total.
|
||||
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>
|
||||
{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({ label, value }: { label: string; value: string | number }) {
|
||||
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-5 shadow-sm">
|
||||
<div className="text-2xl font-bold mb-1 text-gray-900">{value}</div>
|
||||
<div className="text-xs text-gray-500">{label}</div>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user