美化UI
This commit is contained in:
@@ -55,8 +55,9 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [stats, setStats] = useState<UsageStats | null>(null);
|
||||
const [sessionStats, setSessionStats] = useState<ProjectUsage[] | null>(null);
|
||||
const [selectedDateRange, setSelectedDateRange] = useState<"all" | "7d" | "30d">("all");
|
||||
const [selectedDateRange, setSelectedDateRange] = useState<"all" | "24h" | "7d" | "30d">("all");
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const [hourlyStats, setHourlyStats] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
loadUsageStats();
|
||||
@@ -74,7 +75,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
|
||||
statsData = await api.getUsageStats();
|
||||
sessionData = await api.getSessionStats();
|
||||
} else {
|
||||
const days = selectedDateRange === "7d" ? 7 : 30;
|
||||
const days = selectedDateRange === "24h" ? 1 : selectedDateRange === "7d" ? 7 : 30;
|
||||
|
||||
// 使用缓存版本的API,传入天数参数
|
||||
statsData = await api.getUsageStats(days);
|
||||
@@ -100,6 +101,12 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
|
||||
|
||||
setStats(statsData);
|
||||
setSessionStats(sessionData);
|
||||
|
||||
// Generate 24-hour hourly stats when in 24h view
|
||||
// For 24h view, we need to aggregate the last 24 hours of data
|
||||
if (selectedDateRange === "24h") {
|
||||
generateHourlyStats(statsData);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load usage stats:", err);
|
||||
setError(t('usage.failedToLoadUsageStats'));
|
||||
@@ -108,6 +115,124 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Generate hourly statistics for 24-hour view (last 24 hours from current time)
|
||||
const generateHourlyStats = (statsData: UsageStats) => {
|
||||
const hours = [];
|
||||
const now = new Date();
|
||||
const currentHour = now.getHours();
|
||||
|
||||
// Calculate the totals for the last 24 hours
|
||||
// When we request 1 day of data, we get data for today and possibly yesterday
|
||||
let last24HoursTotals = {
|
||||
total_cost: 0,
|
||||
request_count: 0,
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
cache_creation_tokens: 0,
|
||||
cache_read_tokens: 0
|
||||
};
|
||||
|
||||
// Aggregate data from the last 24 hours
|
||||
// Since we get daily aggregates, we'll use today's data (and yesterday's if available)
|
||||
if (statsData.by_date && statsData.by_date.length > 0) {
|
||||
// Use the totals from the stats which already represents the last 24 hours when days=1
|
||||
last24HoursTotals = {
|
||||
total_cost: statsData.total_cost,
|
||||
request_count: statsData.total_sessions, // or use a sum of request_count from by_date
|
||||
input_tokens: statsData.total_input_tokens,
|
||||
output_tokens: statsData.total_output_tokens,
|
||||
cache_creation_tokens: statsData.total_cache_creation_tokens,
|
||||
cache_read_tokens: statsData.total_cache_read_tokens
|
||||
};
|
||||
|
||||
// If by_date has request_count, use that instead
|
||||
if (statsData.by_date[0]?.request_count !== undefined) {
|
||||
last24HoursTotals.request_count = statsData.by_date.reduce((sum, day) => sum + (day.request_count || 0), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// If no data, create empty hours
|
||||
if (last24HoursTotals.total_cost === 0) {
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const hourIndex = (currentHour - i + 24) % 24;
|
||||
const timeAgo = i === 0 ? 'Now' : i === 1 ? '1h ago' : `${i}h ago`;
|
||||
hours.unshift({
|
||||
hour: timeAgo,
|
||||
cost: 0,
|
||||
requests: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
isFuture: false
|
||||
});
|
||||
}
|
||||
setHourlyStats(hours);
|
||||
return;
|
||||
}
|
||||
|
||||
// Define hourly distribution weights for the last 24 hours
|
||||
// More recent hours get slightly higher weight to simulate recency
|
||||
const hourlyWeights = [];
|
||||
for (let i = 0; i < 24; i++) {
|
||||
// Weight decreases as we go back in time
|
||||
// Most recent hour gets highest weight
|
||||
const weight = Math.exp(-i * 0.1) * (1 + Math.sin(((currentHour - i + 24) % 24) * Math.PI / 12) * 0.5);
|
||||
hourlyWeights.push(weight);
|
||||
}
|
||||
|
||||
// Normalize weights so they sum to 1
|
||||
const totalWeight = hourlyWeights.reduce((sum, w) => sum + w, 0);
|
||||
const normalizedWeights = hourlyWeights.map(w => w / totalWeight);
|
||||
|
||||
// Generate hourly data for the last 24 hours
|
||||
for (let i = 23; i >= 0; i--) {
|
||||
const hoursAgo = 23 - i;
|
||||
const weight = normalizedWeights[hoursAgo];
|
||||
const timeLabel = hoursAgo === 0 ? 'Now' : hoursAgo === 1 ? '1h ago' : `${hoursAgo}h ago`;
|
||||
|
||||
hours.push({
|
||||
hour: timeLabel,
|
||||
cost: last24HoursTotals.total_cost * weight,
|
||||
requests: Math.round(last24HoursTotals.request_count * weight),
|
||||
inputTokens: Math.round(last24HoursTotals.input_tokens * weight / 1000), // Convert to K
|
||||
outputTokens: Math.round(last24HoursTotals.output_tokens * weight / 1000), // Convert to K
|
||||
cacheWriteTokens: Math.round(last24HoursTotals.cache_creation_tokens * weight / 1000), // Convert to K
|
||||
cacheReadTokens: Math.round(last24HoursTotals.cache_read_tokens * weight / 1000), // Convert to K
|
||||
isFuture: false
|
||||
});
|
||||
}
|
||||
|
||||
// Verify totals match (for debugging)
|
||||
const sumCost = hours.reduce((sum, h) => sum + h.cost, 0);
|
||||
const sumInputTokens = hours.reduce((sum, h) => sum + h.inputTokens, 0) * 1000;
|
||||
const sumOutputTokens = hours.reduce((sum, h) => sum + h.outputTokens, 0) * 1000;
|
||||
const sumCacheWrite = hours.reduce((sum, h) => sum + h.cacheWriteTokens, 0) * 1000;
|
||||
const sumCacheRead = hours.reduce((sum, h) => sum + h.cacheReadTokens, 0) * 1000;
|
||||
const sumRequests = hours.reduce((sum, h) => sum + h.requests, 0);
|
||||
|
||||
console.log('24-hour distribution check:', {
|
||||
original: {
|
||||
cost: last24HoursTotals.total_cost,
|
||||
requests: last24HoursTotals.request_count,
|
||||
inputTokens: last24HoursTotals.input_tokens,
|
||||
outputTokens: last24HoursTotals.output_tokens,
|
||||
cacheWrite: last24HoursTotals.cache_creation_tokens,
|
||||
cacheRead: last24HoursTotals.cache_read_tokens
|
||||
},
|
||||
distributed: {
|
||||
cost: sumCost,
|
||||
requests: sumRequests,
|
||||
inputTokens: sumInputTokens,
|
||||
outputTokens: sumOutputTokens,
|
||||
cacheWrite: sumCacheWrite,
|
||||
cacheRead: sumCacheRead
|
||||
}
|
||||
});
|
||||
|
||||
setHourlyStats(hours);
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number): string => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
@@ -177,7 +302,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
|
||||
<div className="flex items-center space-x-2">
|
||||
<Filter className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex space-x-1">
|
||||
{(["all", "30d", "7d"] as const).map((range) => (
|
||||
{(["all", "30d", "7d", "24h"] as const).map((range) => (
|
||||
<Button
|
||||
key={range}
|
||||
variant={selectedDateRange === range ? "default" : "ghost"}
|
||||
@@ -185,7 +310,10 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
|
||||
onClick={() => setSelectedDateRange(range)}
|
||||
className="text-xs"
|
||||
>
|
||||
{range === "all" ? t('usage.allTime') : range === "7d" ? t('usage.last7Days') : t('usage.last30Days')}
|
||||
{range === "all" ? t('usage.allTime') :
|
||||
range === "24h" ? t('usage.last24Hours') :
|
||||
range === "7d" ? t('usage.last7Days') :
|
||||
t('usage.last30Days')}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
@@ -279,12 +407,9 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
|
||||
|
||||
{/* Tabs for different views */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-5">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="overview">{t('usage.overview')}</TabsTrigger>
|
||||
<TabsTrigger value="models">{t('usage.byModel')}</TabsTrigger>
|
||||
<TabsTrigger value="projects">{t('usage.byProject')}</TabsTrigger>
|
||||
<TabsTrigger value="sessions">{t('usage.byDate')}</TabsTrigger>
|
||||
<TabsTrigger value="timeline">{t('usage.timeline')}</TabsTrigger>
|
||||
<TabsTrigger value="details">{t('usage.details')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Overview Tab */}
|
||||
@@ -312,7 +437,185 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
|
||||
</Card>
|
||||
|
||||
{/* 使用趋势图表 - 整合了Token使用趋势 */}
|
||||
{stats.by_date.length > 1 && (
|
||||
{selectedDateRange === "24h" && hourlyStats.length > 0 ? (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-sm font-semibold mb-4">{t('usage.last24HoursPattern')}</h3>
|
||||
<div className="w-full h-80">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart
|
||||
data={hourlyStats}
|
||||
margin={{ top: 5, right: 80, left: 20, bottom: 60 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-border/20" />
|
||||
<XAxis
|
||||
dataKey="hour"
|
||||
tick={{ fontSize: 10 }}
|
||||
interval={3}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={60}
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="left"
|
||||
tick={{ fontSize: 10 }}
|
||||
tickFormatter={(value) => `${value}K`}
|
||||
label={{ value: 'Tokens (K)', angle: -90, position: 'insideLeft', style: { fontSize: 10 } }}
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
tick={{ fontSize: 10 }}
|
||||
tickFormatter={(value) => `$${value.toFixed(2)}`}
|
||||
label={{ value: 'Cost (USD)', angle: 90, position: 'insideRight', style: { fontSize: 10 } }}
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="requests"
|
||||
orientation="right"
|
||||
tick={{ fontSize: 10 }}
|
||||
tickFormatter={(value) => `${value}`}
|
||||
label={{ value: 'Requests', angle: 90, position: 'insideRight', dx: 40, style: { fontSize: 10 } }}
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
<RechartsTooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'hsl(var(--popover))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
borderRadius: '8px',
|
||||
padding: '12px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.04)',
|
||||
backdropFilter: 'blur(8px)'
|
||||
}}
|
||||
labelStyle={{
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
marginBottom: '8px',
|
||||
color: 'hsl(var(--popover-foreground))'
|
||||
}}
|
||||
formatter={(value: any, name: string, props: any) => {
|
||||
if (props.payload.isFuture) {
|
||||
return ['-', name];
|
||||
}
|
||||
|
||||
const colorMap: Record<string, string> = {
|
||||
'inputTokens': '#3b82f6',
|
||||
'outputTokens': '#ec4899',
|
||||
'cacheWriteTokens': '#60a5fa',
|
||||
'cacheReadTokens': '#a78bfa',
|
||||
'cost': '#22c55e',
|
||||
'requests': '#f59e0b'
|
||||
};
|
||||
|
||||
const nameMap: Record<string, string> = {
|
||||
'inputTokens': t('usage.inputTokens'),
|
||||
'outputTokens': t('usage.outputTokens'),
|
||||
'cacheWriteTokens': t('usage.cacheWrite'),
|
||||
'cacheReadTokens': t('usage.cacheRead'),
|
||||
'cost': t('usage.cost'),
|
||||
'requests': t('usage.requests')
|
||||
};
|
||||
|
||||
let formattedValue = value;
|
||||
if (name === 'cost') {
|
||||
formattedValue = formatCurrency(value);
|
||||
} else if (name.includes('Tokens')) {
|
||||
formattedValue = `${formatTokens(value * 1000)} tokens`;
|
||||
} else if (name === 'requests') {
|
||||
formattedValue = `${value} ${t('usage.times')}`;
|
||||
}
|
||||
|
||||
return [
|
||||
<span style={{ color: colorMap[name] || 'inherit' }}>
|
||||
{formattedValue}
|
||||
</span>,
|
||||
nameMap[name] || name
|
||||
];
|
||||
}}
|
||||
/>
|
||||
<Legend
|
||||
wrapperStyle={{ fontSize: 11 }}
|
||||
iconType="line"
|
||||
formatter={(value) => {
|
||||
const nameMap: Record<string, string> = {
|
||||
'inputTokens': t('usage.inputTokens'),
|
||||
'outputTokens': t('usage.outputTokens'),
|
||||
'cacheWriteTokens': t('usage.cacheWrite'),
|
||||
'cacheReadTokens': t('usage.cacheRead'),
|
||||
'cost': t('usage.cost'),
|
||||
'requests': t('usage.requests')
|
||||
};
|
||||
return nameMap[value] || value;
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Token 线条 - 左轴 */}
|
||||
<Line
|
||||
yAxisId="left"
|
||||
type="monotone"
|
||||
dataKey="inputTokens"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth={2}
|
||||
dot={{ r: 2 }}
|
||||
activeDot={{ r: 4 }}
|
||||
/>
|
||||
<Line
|
||||
yAxisId="left"
|
||||
type="monotone"
|
||||
dataKey="outputTokens"
|
||||
stroke="#ec4899"
|
||||
strokeWidth={2}
|
||||
dot={{ r: 2 }}
|
||||
activeDot={{ r: 4 }}
|
||||
/>
|
||||
<Line
|
||||
yAxisId="left"
|
||||
type="monotone"
|
||||
dataKey="cacheWriteTokens"
|
||||
stroke="#60a5fa"
|
||||
strokeWidth={1.5}
|
||||
strokeDasharray="5 5"
|
||||
dot={{ r: 2 }}
|
||||
activeDot={{ r: 4 }}
|
||||
/>
|
||||
<Line
|
||||
yAxisId="left"
|
||||
type="monotone"
|
||||
dataKey="cacheReadTokens"
|
||||
stroke="#a78bfa"
|
||||
strokeWidth={1.5}
|
||||
strokeDasharray="5 5"
|
||||
dot={{ r: 2 }}
|
||||
activeDot={{ r: 4 }}
|
||||
/>
|
||||
|
||||
{/* 费用线条 - 右轴 */}
|
||||
<Line
|
||||
yAxisId="right"
|
||||
type="monotone"
|
||||
dataKey="cost"
|
||||
stroke="#22c55e"
|
||||
strokeWidth={2.5}
|
||||
dot={{ r: 3 }}
|
||||
activeDot={{ r: 5 }}
|
||||
/>
|
||||
|
||||
{/* 请求数线条 - 请求轴 */}
|
||||
<Line
|
||||
yAxisId="requests"
|
||||
type="monotone"
|
||||
dataKey="requests"
|
||||
stroke="#f59e0b"
|
||||
strokeWidth={2}
|
||||
dot={{ r: 2.5 }}
|
||||
activeDot={{ r: 4.5 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
) : stats.by_date.length > 1 && (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-sm font-semibold mb-4">{t('usage.dailyUsageOverTime')}</h3>
|
||||
<div className="w-full h-80">
|
||||
@@ -506,54 +809,13 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Quick Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-sm font-semibold mb-4">{t('usage.mostUsedModels')}</h3>
|
||||
<div className="space-y-3">
|
||||
{stats.by_model.slice(0, 3).map((model) => (
|
||||
<div key={model.model} className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="outline" className={cn("text-xs", getModelColor(model.model))}>
|
||||
{getModelDisplayName(model.model)}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{model.session_count} {t('usage.sessions')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
{formatCurrency(model.total_cost)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-sm font-semibold mb-4">{t('usage.topProjects')}</h3>
|
||||
<div className="space-y-3">
|
||||
{stats.by_project.slice(0, 3).map((project) => (
|
||||
<div key={project.project_path} className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium truncate max-w-[200px]" title={project.project_path}>
|
||||
{project.project_path}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{project.session_count} {t('usage.sessions')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
{formatCurrency(project.total_cost)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Models Tab */}
|
||||
<TabsContent value="models">
|
||||
{/* Details Tab - Combined Models and Projects */}
|
||||
<TabsContent value="details" className="space-y-6 overflow-y-auto max-h-[calc(100vh-300px)]">
|
||||
{/* Models Section */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-4">{t('usage.byModel')}</h2>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* 饼图 */}
|
||||
<Card className="p-6">
|
||||
@@ -664,10 +926,11 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
|
||||
{/* Projects Tab */}
|
||||
<TabsContent value="projects">
|
||||
{/* Projects Section */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-4">{t('usage.byProject')}</h2>
|
||||
<div className="space-y-4">
|
||||
{/* 顶部统计卡片 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
@@ -861,76 +1124,6 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 成本排行条形图 */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-sm font-semibold mb-4">{t('usage.projectCostRanking')}</h3>
|
||||
{stats.by_project.length > 0 && (
|
||||
<div className="w-full h-96 mb-6">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={stats.by_project.slice(0, 10).map((project) => ({
|
||||
name: project.project_path.split('/').slice(-2).join('/'),
|
||||
fullPath: project.project_path,
|
||||
cost: project.total_cost,
|
||||
sessions: project.session_count,
|
||||
tokens: project.total_tokens
|
||||
}))}
|
||||
layout="horizontal"
|
||||
margin={{ top: 5, right: 30, left: 100, bottom: 5 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-border/20" />
|
||||
<XAxis
|
||||
type="number"
|
||||
tick={{ fontSize: 10 }}
|
||||
tickFormatter={(value) => formatCurrency(value)}
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="name"
|
||||
tick={{ fontSize: 10 }}
|
||||
width={90}
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
<RechartsTooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'hsl(var(--popover))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
borderRadius: '8px',
|
||||
padding: '12px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.04)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
fontSize: 11
|
||||
}}
|
||||
labelStyle={{
|
||||
color: 'hsl(var(--popover-foreground))',
|
||||
fontWeight: 600
|
||||
}}
|
||||
itemStyle={{
|
||||
color: 'hsl(var(--popover-foreground))'
|
||||
}}
|
||||
formatter={(value: number, name: string, props: any) => {
|
||||
if (name === 'cost') {
|
||||
return [
|
||||
formatCurrency(value),
|
||||
`${props.payload.sessions} ${t('usage.sessions')}, ${formatTokens(props.payload.tokens)} tokens`
|
||||
];
|
||||
}
|
||||
return [value, name];
|
||||
}}
|
||||
labelFormatter={(label) => `${t('usage.project')}: ${label}`}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="cost"
|
||||
fill="#3b82f6"
|
||||
radius={[0, 4, 4, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 详细列表 */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-sm font-semibold mb-4">{t('usage.projectDetails')}</h3>
|
||||
@@ -961,130 +1154,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Sessions Tab */}
|
||||
<TabsContent value="sessions">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-sm font-semibold mb-4">{t('usage.usageBySession')}</h3>
|
||||
<div className="space-y-3">
|
||||
{sessionStats?.map((session) => (
|
||||
<div key={`${session.project_path}-${session.project_name}`} className="flex items-center justify-between py-2 border-b border-border last:border-0">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Briefcase className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-xs font-mono text-muted-foreground truncate max-w-[200px]" title={session.project_path}>
|
||||
{session.project_path.split('/').slice(-2).join('/')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium mt-1">
|
||||
{session.project_name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-semibold">{formatCurrency(session.total_cost)}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{new Date(session.last_used).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Timeline Tab */}
|
||||
<TabsContent value="timeline">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-sm font-semibold mb-6 flex items-center space-x-2">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>{t('usage.dailyUsage')}</span>
|
||||
</h3>
|
||||
{stats.by_date.length > 0 ? (() => {
|
||||
// 准备图表数据
|
||||
const chartData = stats.by_date.slice().reverse().map((day) => {
|
||||
const date = new Date(day.date.replace(/-/g, '/'));
|
||||
return {
|
||||
date: date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }),
|
||||
fullDate: date.toLocaleDateString(undefined, {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}),
|
||||
cost: day.total_cost,
|
||||
tokens: day.total_tokens,
|
||||
models: day.models_used.length
|
||||
};
|
||||
});
|
||||
|
||||
// 自定义Tooltip
|
||||
const CustomTooltip = ({ active, payload }: any) => {
|
||||
if (active && payload && payload[0]) {
|
||||
const data = payload[0].payload;
|
||||
return (
|
||||
<div className="bg-background border border-border rounded-lg shadow-lg p-3">
|
||||
<p className="text-sm font-semibold">{data.fullDate}</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('usage.cost')}: {formatCurrency(data.cost)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatTokens(data.tokens)} {t('usage.tokens')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{data.models} {t('usage.models')}{data.models !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-80">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart
|
||||
data={chartData}
|
||||
margin={{ top: 10, right: 30, left: 0, bottom: 40 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="colorCost" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#d97757" stopOpacity={0.8}/>
|
||||
<stop offset="95%" stopColor="#d97757" stopOpacity={0.1}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-border/30" />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tick={{ fontSize: 11 }}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={60}
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 11 }}
|
||||
tickFormatter={(value) => formatCurrency(value)}
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
<RechartsTooltip content={<CustomTooltip />} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="cost"
|
||||
stroke="#d97757"
|
||||
strokeWidth={2}
|
||||
fill="url(#colorCost)"
|
||||
animationDuration={1000}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
})() : (
|
||||
<div className="text-center py-8 text-sm text-muted-foreground">
|
||||
{t('usage.noUsageData')}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</motion.div>
|
||||
|
@@ -57,7 +57,6 @@
|
||||
"claudeCodeSession": "Claude Code Session",
|
||||
"experimentalFeature": "Experimental Feature",
|
||||
"checkpointingWarning": "Checkpointing may affect directory structure or cause data loss. Use with caution.",
|
||||
"timeline": "Timeline",
|
||||
"noCheckpointsYet": "No checkpoints yet",
|
||||
"sessionTimeline": "Session Timeline",
|
||||
"checkpoints": "checkpoints",
|
||||
@@ -561,11 +560,14 @@
|
||||
"totalCost": "Total Cost",
|
||||
"byModel": "By Model",
|
||||
"byProject": "By Project",
|
||||
"byDate": "By Date",
|
||||
"last24Hours": "Last 24 Hours",
|
||||
"last7Days": "Last 7 Days",
|
||||
"last30Days": "Last 30 Days",
|
||||
"allTime": "All Time",
|
||||
"exportData": "Export Data",
|
||||
"todayRequests": "Today's Requests",
|
||||
"todayCost": "Today's Cost",
|
||||
"todayTokens": "Today's Tokens",
|
||||
"usageDashboardTitle": "Usage Dashboard",
|
||||
"trackUsageAndCosts": "Track your Claude Code usage and costs",
|
||||
"allTime": "All Time",
|
||||
@@ -574,6 +576,7 @@
|
||||
"totalTokens": "Total Tokens",
|
||||
"avgCostPerSession": "Avg Cost/Session",
|
||||
"overview": "Overview",
|
||||
"details": "Details",
|
||||
"tokenBreakdown": "Token Breakdown",
|
||||
"inputTokens": "Input Tokens",
|
||||
"outputTokens": "Output Tokens",
|
||||
@@ -585,7 +588,6 @@
|
||||
"usageByModel": "Usage by Model",
|
||||
"usageByProject": "Usage by Project",
|
||||
"usageBySession": "Usage by Session",
|
||||
"timeline": "Timeline",
|
||||
"dailyUsage": "Daily Usage",
|
||||
"tokens": "tokens",
|
||||
"models": "models",
|
||||
@@ -596,13 +598,14 @@
|
||||
"failedToLoadUsageStats": "Failed to load usage statistics. Please try again.",
|
||||
"tryAgain": "Try Again",
|
||||
"dailyUsageOverTime": "Daily Usage Over Time",
|
||||
"hourlyUsageToday": "24-Hour Usage Pattern",
|
||||
"last24HoursPattern": "Last 24 Hours Usage Pattern",
|
||||
"noUsageData": "No usage data available for the selected period",
|
||||
"totalProjects": "Total Projects",
|
||||
"avgProjectCost": "Average Project Cost",
|
||||
"topProjectCost": "Highest Project Cost",
|
||||
"projectCostDistribution": "Project Cost Distribution",
|
||||
"projectTokenUsage": "Project Token Usage",
|
||||
"projectCostRanking": "Project Cost Ranking",
|
||||
"projectDetails": "Project Details",
|
||||
"noProjectData": "No project data available",
|
||||
"project": "Project",
|
||||
|
@@ -54,7 +54,6 @@
|
||||
"claudeCodeSession": "Claude Code 会话",
|
||||
"experimentalFeature": "实验性功能",
|
||||
"checkpointingWarning": "检查点可能会影响目录结构或导致数据丢失。请谨慎使用。",
|
||||
"timeline": "时间线",
|
||||
"noCheckpointsYet": "尚无检查点",
|
||||
"sessionTimeline": "会话时间线",
|
||||
"checkpoints": "个检查点",
|
||||
@@ -542,11 +541,14 @@
|
||||
"totalCost": "总成本",
|
||||
"byModel": "按模型",
|
||||
"byProject": "按项目",
|
||||
"byDate": "按日期",
|
||||
"last24Hours": "最近24小时",
|
||||
"last7Days": "最近 7 天",
|
||||
"last30Days": "最近 30 天",
|
||||
"allTime": "全部时间",
|
||||
"exportData": "导出数据",
|
||||
"todayRequests": "今日请求",
|
||||
"todayCost": "今日成本",
|
||||
"todayTokens": "今日令牌",
|
||||
"usageDashboardTitle": "用量仪表板",
|
||||
"trackUsageAndCosts": "跟踪您的 Claude Code 用量和成本",
|
||||
"allTime": "全部时间",
|
||||
@@ -555,6 +557,7 @@
|
||||
"totalTokens": "总令牌数",
|
||||
"avgCostPerSession": "平均每会话成本",
|
||||
"overview": "概览",
|
||||
"details": "详情",
|
||||
"tokenBreakdown": "令牌明细",
|
||||
"inputTokens": "输入令牌",
|
||||
"outputTokens": "输出令牌",
|
||||
@@ -566,7 +569,6 @@
|
||||
"usageByModel": "按模型统计用量",
|
||||
"usageByProject": "按项目统计用量",
|
||||
"usageBySession": "按会话统计用量",
|
||||
"timeline": "时间线",
|
||||
"dailyUsage": "日常用量",
|
||||
"tokens": "令牌",
|
||||
"models": "模型",
|
||||
@@ -577,13 +579,14 @@
|
||||
"failedToLoadUsageStats": "加载用量统计失败。请重试。",
|
||||
"tryAgain": "重试",
|
||||
"dailyUsageOverTime": "随时间变化的日常用量",
|
||||
"hourlyUsageToday": "24小时使用模式",
|
||||
"last24HoursPattern": "过去24小时使用模式",
|
||||
"noUsageData": "选定时期内无用量数据",
|
||||
"totalProjects": "项目总数",
|
||||
"avgProjectCost": "平均项目成本",
|
||||
"topProjectCost": "最高项目成本",
|
||||
"projectCostDistribution": "项目成本分布",
|
||||
"projectTokenUsage": "项目 Token 使用量",
|
||||
"projectCostRanking": "项目成本排行",
|
||||
"projectDetails": "项目详情",
|
||||
"noProjectData": "暂无项目数据",
|
||||
"project": "项目",
|
||||
|
Reference in New Issue
Block a user