feat/fix: ai page improvements, bump, update ccusage
add more content on ai, add heatmap, fix header glow, add price to ai tools, fix NowPlaying style issue, new ccombine utility, better claude code usage page w/ model labels+better typing, bump, update ccusage (restore old dates)
This commit is contained in:
parent
57dd627ca3
commit
77a6266c71
20 changed files with 1380 additions and 444 deletions
172
app/ai/claude/components/Activity.tsx
Normal file
172
app/ai/claude/components/Activity.tsx
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
"use client"
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
CartesianGrid,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts'
|
||||
import { DailyData } from './types'
|
||||
import {
|
||||
buildDailyTrendData,
|
||||
formatCurrency,
|
||||
formatTokens,
|
||||
getHeatmapColor,
|
||||
prepareHeatmapData,
|
||||
} from './utils'
|
||||
|
||||
export default function Activity({ daily }: { daily: DailyData[] }) {
|
||||
const [viewMode, setViewMode] = useState<'heatmap' | 'chart'>('heatmap')
|
||||
const [selectedMetric, setSelectedMetric] = useState<'cost' | 'tokens'>('cost')
|
||||
|
||||
const dailyTrendData = useMemo(() => buildDailyTrendData(daily), [daily])
|
||||
const heatmapWeeks = useMemo(() => prepareHeatmapData(daily), [daily])
|
||||
const maxCost = useMemo(
|
||||
() => (daily.length ? Math.max(...daily.map(d => d.totalCost)) : 0),
|
||||
[daily]
|
||||
)
|
||||
|
||||
return (
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300 relative md:col-span-2 lg:col-span-1">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-semibold text-gray-200">Activity</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-400">{viewMode === 'heatmap' ? 'Heatmap' : 'Chart'}</span>
|
||||
<button
|
||||
onClick={() => setViewMode(viewMode === 'heatmap' ? 'chart' : 'heatmap')}
|
||||
className="relative inline-flex h-6 w-11 items-center rounded-full bg-gray-700 transition-colors focus:outline-none focus:ring-2 focus:ring-[#c15f3c] focus:ring-offset-2 focus:ring-offset-gray-900"
|
||||
>
|
||||
<span className="sr-only">Toggle view mode</span>
|
||||
<span
|
||||
className={`${viewMode === 'chart' ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-[#c15f3c] transition-transform`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{viewMode === 'heatmap' ? (
|
||||
<div className="overflow-x-auto pb-6">
|
||||
<div className="min-w-[900px]">
|
||||
<div className="flex gap-1">
|
||||
<div className="flex flex-col gap-1 text-xs text-gray-400 w-10 pr-2">
|
||||
<div className="h-4"></div>
|
||||
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((day) => (
|
||||
<div key={day} className="h-4 flex items-center justify-end text-[10px]">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="h-4 mb-1 text-xs text-gray-400">
|
||||
{(() => {
|
||||
const monthLabels: { month: string; position: number }[] = []
|
||||
let lastMonth = -1
|
||||
heatmapWeeks.forEach((week, weekIndex) => {
|
||||
const firstDay = week.find(d => d !== null)
|
||||
if (firstDay) {
|
||||
const date = new Date(firstDay.date + 'T00:00:00Z')
|
||||
const month = date.getUTCMonth()
|
||||
if (month !== lastMonth) {
|
||||
monthLabels.push({
|
||||
month: date.toLocaleDateString('en-US', { month: 'short', timeZone: 'UTC' }),
|
||||
position: weekIndex * 20
|
||||
})
|
||||
lastMonth = month
|
||||
}
|
||||
}
|
||||
})
|
||||
return (
|
||||
<div className="flex relative">
|
||||
{monthLabels.map((label, idx) => (
|
||||
<div key={idx} style={{ position: 'absolute', left: label.position }} className="w-10">
|
||||
{label.month}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
{heatmapWeeks.map((week, weekIndex) => (
|
||||
<div key={weekIndex} className="flex flex-col gap-1">
|
||||
{week.map((day, dayIndex) => (
|
||||
<div key={dayIndex} className="relative group">
|
||||
<div
|
||||
className="w-4 h-4 rounded-sm"
|
||||
style={{ backgroundColor: getHeatmapColor(maxCost, day?.value || 0) }}
|
||||
/>
|
||||
{day && (
|
||||
<div className="absolute z-10 invisible group-hover:visible -top-2 left-6">
|
||||
<div className="bg-gray-900 border border-gray-700 rounded-lg p-2 shadow-lg whitespace-nowrap">
|
||||
<p className="text-gray-300 text-xs font-medium mb-1">{day.formattedDate}</p>
|
||||
<p className="text-[#c15f3c] font-bold text-sm">Cost: ${day.cost.toFixed(2)}</p>
|
||||
<p className="text-gray-400 text-xs">Tokens: {(day.tokens / 1000000).toFixed(2)}M</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-4 text-xs text-gray-400">
|
||||
<span>Less</span>
|
||||
<div className="flex gap-1">
|
||||
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: '#1f2937' }}></div>
|
||||
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: '#4a3328' }}></div>
|
||||
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: '#6b4530' }}></div>
|
||||
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: '#8d5738' }}></div>
|
||||
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: '#c15f3c' }}></div>
|
||||
</div>
|
||||
<span>More</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex gap-2 mb-4">
|
||||
<button
|
||||
onClick={() => setSelectedMetric('cost')}
|
||||
className={`px-3 py-1 rounded ${selectedMetric === 'cost' ? 'bg-[#c15f3c] text-white' : 'bg-gray-700 text-gray-300'}`}
|
||||
>
|
||||
Cost
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedMetric('tokens')}
|
||||
className={`px-3 py-1 rounded ${selectedMetric === 'tokens' ? 'bg-[#c15f3c] text-white' : 'bg-gray-700 text-gray-300'}`}
|
||||
>
|
||||
Tokens
|
||||
</button>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<AreaChart data={dailyTrendData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||
<XAxis dataKey="date" stroke="#9ca3af" />
|
||||
<YAxis
|
||||
stroke="#9ca3af"
|
||||
tickFormatter={selectedMetric === 'cost' ? formatCurrency : formatTokens}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151' }}
|
||||
formatter={(value: number) => selectedMetric === 'cost' ? formatCurrency(value) : formatTokens(value)}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey={selectedMetric === 'cost' ? 'cost' : 'tokens'}
|
||||
stroke="#c15f3c"
|
||||
fill="#c15f3c"
|
||||
fillOpacity={0.3}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
177
app/ai/claude/components/LoadingSkeleton.tsx
Normal file
177
app/ai/claude/components/LoadingSkeleton.tsx
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
"use client"
|
||||
|
||||
import PageHeader from './PageHeader'
|
||||
|
||||
export default function LoadingSkeleton() {
|
||||
return (
|
||||
<main className="w-full relative">
|
||||
<PageHeader />
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 px-4">
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Total Cost</h3>
|
||||
<div className="h-9 w-32 bg-gray-800 rounded animate-pulse" />
|
||||
</div>
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Total Tokens</h3>
|
||||
<div className="h-9 w-32 bg-gray-800 rounded animate-pulse" />
|
||||
</div>
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Days Active</h3>
|
||||
<div className="flex items-center">
|
||||
<div className="h-9 w-16 bg-gray-800 rounded animate-pulse" />
|
||||
<div className="ml-3 h-5 w-12 bg-gray-800 rounded-full animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Avg Daily Cost</h3>
|
||||
<div className="h-9 w-32 bg-gray-800 rounded animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 pb-0">
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300 relative md:col-span-2 lg:col-span-1">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-semibold text-gray-200">Activity</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-400">Heatmap</span>
|
||||
<div className="h-6 w-11 bg-gray-700 rounded-full animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto pb-6">
|
||||
<div className="min-w-[900px]">
|
||||
<div className="flex gap-1">
|
||||
<div className="flex flex-col gap-1 text-xs text-gray-400 w-10 pr-2">
|
||||
<div className="h-4"></div>
|
||||
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((day) => (
|
||||
<div key={day} className="h-4 flex items-center justify-end text-[10px]">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="h-4 mb-1 text-xs text-gray-400">
|
||||
<div className="flex gap-16">
|
||||
{['Jan', 'Mar', 'May', 'Jul', 'Sep', 'Nov'].map((month) => (
|
||||
<div key={month} className="w-12 h-3 bg-gray-800 rounded animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
{(() => {
|
||||
const today = new Date()
|
||||
const startOfYear = new Date(Date.UTC(today.getUTCFullYear(), 0, 1))
|
||||
const endDate = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()))
|
||||
|
||||
const firstDay = startOfYear.getUTCDay()
|
||||
const startDate = new Date(startOfYear)
|
||||
startDate.setUTCDate(startDate.getUTCDate() - firstDay)
|
||||
|
||||
const msPerWeek = 7 * 24 * 60 * 60 * 1000
|
||||
const weekCount = Math.ceil((endDate.getTime() - startDate.getTime()) / msPerWeek)
|
||||
|
||||
return [...Array(weekCount)].map((_, weekIndex) => (
|
||||
<div key={weekIndex} className="flex flex-col gap-1">
|
||||
{[...Array(7)].map((_, dayIndex) => (
|
||||
<div key={dayIndex} className="w-4 h-4 bg-gray-800 rounded-sm animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-4 text-xs text-gray-400">
|
||||
<span>Less</span>
|
||||
<div className="flex gap-1">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="w-3 h-3 bg-gray-800 rounded-sm animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
<span>More</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 p-4">
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300 col-span-2 lg:col-span-1">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Model Usage Distribution</h2>
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
<div className="h-[300px] bg-gray-800 rounded animate-pulse" />
|
||||
<div className="flex flex-col justify-center space-y-3">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={i} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-gray-800 rounded-full animate-pulse" />
|
||||
<div className="h-4 w-20 bg-gray-800 rounded animate-pulse" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-4 w-10 bg-gray-800 rounded animate-pulse" />
|
||||
<div className="h-4 w-16 bg-gray-800 rounded animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="pt-3 mt-3 border-t border-gray-700">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-400">Total Models Used</span>
|
||||
<div className="h-5 w-8 bg-gray-800 rounded animate-pulse" />
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<span className="text-gray-400">Most Used</span>
|
||||
<div className="h-4 w-20 bg-gray-800 rounded animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300 col-span-2 lg:col-span-1">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Token Type Breakdown</h2>
|
||||
<div className="h-[300px] bg-gray-800 rounded animate-pulse" />
|
||||
</section>
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300 sm:col-span-2">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Token Composition</h2>
|
||||
<div className="h-[300px] bg-gray-800 rounded animate-pulse" />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="px-4 pb-4">
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Recent Sessions</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-700">
|
||||
<th className="py-2 px-4 text-gray-400">Date</th>
|
||||
<th className="py-2 px-4 text-gray-400">Models Used</th>
|
||||
<th className="py-2 px-4 text-gray-400">Total Tokens</th>
|
||||
<th className="py-2 px-4 text-gray-400">Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<tr key={index} className="border-b border-gray-800">
|
||||
<td className="py-2 px-4">
|
||||
<div className="h-5 w-24 bg-gray-800 rounded animate-pulse" />
|
||||
</td>
|
||||
<td className="py-2 px-4">
|
||||
<div className="h-5 w-96 bg-gray-800 rounded animate-pulse" />
|
||||
</td>
|
||||
<td className="py-2 px-4">
|
||||
<div className="h-5 w-16 bg-gray-800 rounded animate-pulse" />
|
||||
</td>
|
||||
<td className="py-2 px-4">
|
||||
<div className="h-5 w-20 bg-gray-800 rounded animate-pulse" />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
73
app/ai/claude/components/ModelUsageCard.tsx
Normal file
73
app/ai/claude/components/ModelUsageCard.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
"use client"
|
||||
|
||||
import { ResponsiveContainer, PieChart, Pie, Cell, Tooltip } from 'recharts'
|
||||
import { DailyData } from './types'
|
||||
import { COLORS, buildModelUsageData, formatCurrency } from './utils'
|
||||
|
||||
export default function ModelUsageCard({ daily, totalCost }: { daily: DailyData[]; totalCost: number }) {
|
||||
const modelUsageData = buildModelUsageData(daily)
|
||||
return (
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300 col-span-2 lg:col-span-1">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Model Usage Distribution</h2>
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={modelUsageData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={100}
|
||||
fill="#8884d8"
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
>
|
||||
{modelUsageData.map((_entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151', borderRadius: '8px' }}
|
||||
formatter={(value: number) => formatCurrency(value)}
|
||||
labelStyle={{ color: '#fff' }}
|
||||
itemStyle={{ color: '#fff' }}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="flex flex-col justify-center space-y-3">
|
||||
{modelUsageData.map((model, index) => {
|
||||
const percentage = ((model.value / Math.max(totalCost, 1)) * 100).toFixed(1)
|
||||
return (
|
||||
<div key={index} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: COLORS[index % COLORS.length] }}
|
||||
/>
|
||||
<span className="text-gray-300 font-medium text-xs">{model.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-gray-400 text-sm">{percentage}%</span>
|
||||
<span className="text-gray-200 font-semibold">${model.value.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="pt-3 mt-3 border-t border-gray-700">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-400">Total Models Used</span>
|
||||
<span className="text-gray-200 font-bold">{modelUsageData.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<span className="text-gray-400">Most Used</span>
|
||||
<span className="text-gray-200 font-bold text-xs">
|
||||
{modelUsageData[0]?.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
26
app/ai/claude/components/PageHeader.tsx
Normal file
26
app/ai/claude/components/PageHeader.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
"use client"
|
||||
|
||||
import Link from 'next/link'
|
||||
import { SiClaude } from 'react-icons/si'
|
||||
|
||||
export default function PageHeader() {
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
href="/ai"
|
||||
className="absolute top-4 left-4 text-gray-400 hover:text-gray-200 hover:underline transition-colors duration-200 z-10 px-2 py-1 text-sm sm:text-base"
|
||||
>
|
||||
← Back to AI
|
||||
</Link>
|
||||
|
||||
<div className="my-12 text-center">
|
||||
<div className="flex justify-center mb-6">
|
||||
<SiClaude size={60} />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-2 text-gray-100 glow">Claude Code Usage</h1>
|
||||
<p className="text-gray-400">How much I use Claude Code!</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
37
app/ai/claude/components/RecentSessions.tsx
Normal file
37
app/ai/claude/components/RecentSessions.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
"use client"
|
||||
|
||||
import { DailyData } from './types'
|
||||
import { getModelLabel } from './utils'
|
||||
|
||||
export default function RecentSessions({ daily }: { daily: DailyData[] }) {
|
||||
return (
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Recent Sessions</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-700">
|
||||
<th className="py-2 px-4 text-gray-400">Date</th>
|
||||
<th className="py-2 px-4 text-gray-400">Models Used</th>
|
||||
<th className="py-2 px-4 text-gray-400">Total Tokens</th>
|
||||
<th className="py-2 px-4 text-gray-400">Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{daily.slice(-5).reverse().map((day, index) => (
|
||||
<tr key={index} className="border-b border-gray-800 hover:bg-gray-800/50">
|
||||
<td className="py-2 px-4 text-gray-300">{new Date(day.date + 'T00:00:00').toLocaleDateString()}</td>
|
||||
<td className="py-2 px-4 text-gray-300">
|
||||
{day.modelsUsed.map(getModelLabel).join(', ')}
|
||||
</td>
|
||||
<td className="py-2 px-4 text-gray-300">{(day.totalTokens / 1000000).toFixed(2)}M</td>
|
||||
<td className="py-2 px-4 text-[#c15f3c] font-semibold">${day.totalCost.toFixed(2)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
34
app/ai/claude/components/StatsGrid.tsx
Normal file
34
app/ai/claude/components/StatsGrid.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
"use client"
|
||||
|
||||
import { CCData, DailyData } from './types'
|
||||
import { formatStreakCompact, computeStreak } from './utils'
|
||||
|
||||
export default function StatsGrid({ totals, daily }: { totals: CCData['totals']; daily: DailyData[] }) {
|
||||
const streak = computeStreak(daily)
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 px-4">
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Total Cost</h3>
|
||||
<p className="text-3xl font-bold text-[#c15f3c]">${totals.totalCost.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Total Tokens</h3>
|
||||
<p className="text-3xl font-bold text-[#c15f3c]">{(totals.totalTokens / 1000000).toFixed(1)}M</p>
|
||||
</div>
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Days Active</h3>
|
||||
<p className="text-3xl font-bold text-[#c15f3c] flex items-center">
|
||||
{daily.length}
|
||||
<span className="ml-3 text-xs font-semibold text-gray-300 bg-gray-800 px-2 py-0.5 rounded-full">
|
||||
🔥 {formatStreakCompact(streak)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Avg Daily Cost</h3>
|
||||
<p className="text-3xl font-bold text-[#c15f3c]">${(totals.totalCost / Math.max(daily.length, 1)).toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
30
app/ai/claude/components/TokenComposition.tsx
Normal file
30
app/ai/claude/components/TokenComposition.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
"use client"
|
||||
|
||||
import { ResponsiveContainer, ComposedChart, CartesianGrid, XAxis, YAxis, Tooltip, Legend, Bar, Line } from 'recharts'
|
||||
import { DailyData } from './types'
|
||||
import { buildDailyTrendData } from './utils'
|
||||
|
||||
export default function TokenComposition({ daily }: { daily: DailyData[] }) {
|
||||
const dailyTrendData = buildDailyTrendData(daily)
|
||||
return (
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300 sm:col-span-2">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Token Composition</h2>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<ComposedChart data={dailyTrendData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||
<XAxis dataKey="date" stroke="#9ca3af" />
|
||||
<YAxis stroke="#9ca3af" tickFormatter={(value) => `${value}K`} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151' }}
|
||||
formatter={(value: number) => `${value.toFixed(1)}K tokens`}
|
||||
/>
|
||||
<Legend />
|
||||
<Bar dataKey="inputTokens" stackId="a" fill="#c15f3c" name="Input (K)" />
|
||||
<Bar dataKey="outputTokens" stackId="a" fill="#b1ada1" name="Output (K)" />
|
||||
<Line type="monotone" dataKey="cacheTokens" stroke="#f4f3ee" name="Cache (M)" strokeWidth={2} />
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
27
app/ai/claude/components/TokenTypeBreakdown.tsx
Normal file
27
app/ai/claude/components/TokenTypeBreakdown.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
"use client"
|
||||
|
||||
import { ResponsiveContainer, BarChart, CartesianGrid, XAxis, YAxis, Tooltip, Bar } from 'recharts'
|
||||
import { CCData } from './types'
|
||||
import { buildTokenTypeData } from './utils'
|
||||
|
||||
export default function TokenTypeBreakdown({ totals }: { totals: CCData['totals'] }) {
|
||||
const tokenTypeData = buildTokenTypeData(totals)
|
||||
return (
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300 col-span-2 lg:col-span-1">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Token Type Breakdown</h2>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={tokenTypeData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||
<XAxis dataKey="name" stroke="#9ca3af" />
|
||||
<YAxis stroke="#9ca3af" tickFormatter={(value) => `${(value / 1000000).toFixed(0)}M`} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: 'rgba(31, 41, 55)', border: '1px solid #374151' }}
|
||||
formatter={(value: number) => `${(value / 1000000).toFixed(2)}M tokens`}
|
||||
/>
|
||||
<Bar dataKey="value" fill="#b1ada1" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
42
app/ai/claude/components/types.ts
Normal file
42
app/ai/claude/components/types.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
export interface ModelBreakdown {
|
||||
modelName: string
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheCreationTokens: number
|
||||
cacheReadTokens: number
|
||||
cost: number
|
||||
}
|
||||
|
||||
export interface DailyData {
|
||||
date: string
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheCreationTokens: number
|
||||
cacheReadTokens: number
|
||||
totalTokens: number
|
||||
totalCost: number
|
||||
modelsUsed: string[]
|
||||
modelBreakdowns: ModelBreakdown[]
|
||||
}
|
||||
|
||||
export interface CCData {
|
||||
daily: DailyData[]
|
||||
totals: {
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheCreationTokens: number
|
||||
cacheReadTokens: number
|
||||
totalCost: number
|
||||
totalTokens: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface HeatmapDay {
|
||||
date: string
|
||||
value: number
|
||||
tokens: number
|
||||
cost: number
|
||||
day: number
|
||||
formattedDate: string
|
||||
}
|
||||
|
||||
191
app/ai/claude/components/utils.ts
Normal file
191
app/ai/claude/components/utils.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import { CCData, DailyData, HeatmapDay } from './types'
|
||||
|
||||
export const COLORS = ['#c15f3c', '#b1ada1', '#f4f3ee', '#c15f3c', '#b1ada1', '#f4f3ee']
|
||||
|
||||
export const MODEL_LABELS: Record<string, string> = {
|
||||
'claude-sonnet-4-20250514': 'Sonnet 4',
|
||||
'claude-opus-4-1-20250805': 'Opus 4.1',
|
||||
}
|
||||
|
||||
export const getModelLabel = (modelName: string): string => {
|
||||
return MODEL_LABELS[modelName] || modelName
|
||||
}
|
||||
|
||||
export const formatCurrency = (value: number) => `$${value.toFixed(2)}`
|
||||
export const formatTokens = (value: number) => `${value.toFixed(1)}M`
|
||||
|
||||
export const computeStreak = (daily: DailyData[]): number => {
|
||||
if (!daily.length) return 0
|
||||
const datesSet = new Set(daily.map(d => d.date))
|
||||
const latest = daily
|
||||
.map(d => new Date(d.date + 'T00:00:00Z'))
|
||||
.reduce((a, b) => (a > b ? a : b))
|
||||
|
||||
const toKey = (d: Date) => {
|
||||
const y = d.getUTCFullYear()
|
||||
const m = (d.getUTCMonth() + 1).toString().padStart(2, '0')
|
||||
const day = d.getUTCDate().toString().padStart(2, '0')
|
||||
return `${y}-${m}-${day}`
|
||||
}
|
||||
|
||||
let count = 0
|
||||
for (
|
||||
let d = new Date(latest.getTime());
|
||||
;
|
||||
d = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - 1))
|
||||
) {
|
||||
const key = toKey(d)
|
||||
if (datesSet.has(key)) count++
|
||||
else break
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
export const formatStreakCompact = (days: number) => {
|
||||
if (days >= 365) return `${Math.floor(days / 365)}y`
|
||||
if (days >= 30) return `${Math.floor(days / 30)}mo`
|
||||
if (days >= 7) return `${Math.floor(days / 7)}w`
|
||||
return `${days}d`
|
||||
}
|
||||
|
||||
export const computeFilledDailyRange = (daily: DailyData[]): DailyData[] => {
|
||||
if (!daily.length) return []
|
||||
|
||||
const dates = daily.map(d => new Date(d.date + 'T00:00:00Z'))
|
||||
const start = dates.reduce((a, b) => (a < b ? a : b))
|
||||
const end = dates.reduce((a, b) => (a > b ? a : b))
|
||||
|
||||
const byDate = new Map<string, DailyData>(
|
||||
daily.map(d => [d.date, d] as const)
|
||||
)
|
||||
|
||||
const result: DailyData[] = []
|
||||
for (
|
||||
let d = new Date(start.getTime());
|
||||
d <= end;
|
||||
d = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + 1))
|
||||
) {
|
||||
const y = d.getUTCFullYear()
|
||||
const m = (d.getUTCMonth() + 1).toString().padStart(2, '0')
|
||||
const day = d.getUTCDate().toString().padStart(2, '0')
|
||||
const key = `${y}-${m}-${day}`
|
||||
|
||||
if (byDate.has(key)) {
|
||||
result.push(byDate.get(key)!)
|
||||
} else {
|
||||
result.push({
|
||||
date: key,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheCreationTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
totalTokens: 0,
|
||||
totalCost: 0,
|
||||
modelsUsed: [],
|
||||
modelBreakdowns: [],
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const buildDailyTrendData = (daily: DailyData[]) => {
|
||||
const filled = computeFilledDailyRange(daily)
|
||||
return filled.map(day => ({
|
||||
date: new Date(day.date + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
||||
cost: day.totalCost,
|
||||
tokens: day.totalTokens / 1000000,
|
||||
inputTokens: day.inputTokens / 1000,
|
||||
outputTokens: day.outputTokens / 1000,
|
||||
cacheTokens: (day.cacheCreationTokens + day.cacheReadTokens) / 1000000,
|
||||
}))
|
||||
}
|
||||
|
||||
export const prepareHeatmapData = (daily: DailyData[]): (HeatmapDay | null)[][] => {
|
||||
const dayMap = new Map<string, DailyData>()
|
||||
daily.forEach(day => {
|
||||
dayMap.set(day.date, day)
|
||||
})
|
||||
|
||||
const today = new Date()
|
||||
const startOfYear = new Date(Date.UTC(today.getUTCFullYear(), 0, 1))
|
||||
const endDate = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()))
|
||||
|
||||
const weeks: (HeatmapDay | null)[][] = []
|
||||
let currentWeek: (HeatmapDay | null)[] = []
|
||||
|
||||
const firstDay = startOfYear.getUTCDay()
|
||||
const startDate = new Date(startOfYear)
|
||||
startDate.setUTCDate(startDate.getUTCDate() - firstDay)
|
||||
|
||||
for (
|
||||
let d = new Date(startDate);
|
||||
d <= endDate;
|
||||
d = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + 1))
|
||||
) {
|
||||
if (d < startOfYear) {
|
||||
currentWeek.push(null)
|
||||
if (d.getUTCDay() === 6) {
|
||||
weeks.push(currentWeek)
|
||||
currentWeek = []
|
||||
}
|
||||
continue
|
||||
}
|
||||
const dateStr = `${d.getUTCFullYear()}-${(d.getUTCMonth() + 1).toString().padStart(2, '0')}-${d.getUTCDate().toString().padStart(2, '0')}`
|
||||
const dayData = dayMap.get(dateStr)
|
||||
|
||||
currentWeek.push({
|
||||
date: dateStr,
|
||||
value: dayData ? dayData.totalCost : 0,
|
||||
tokens: dayData ? dayData.totalTokens : 0,
|
||||
cost: dayData ? dayData.totalCost : 0,
|
||||
day: d.getUTCDay(),
|
||||
formattedDate: d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' })
|
||||
})
|
||||
|
||||
if (d.getUTCDay() === 6 || d.getTime() === endDate.getTime()) {
|
||||
while (currentWeek.length < 7) {
|
||||
currentWeek.push(null)
|
||||
}
|
||||
weeks.push(currentWeek)
|
||||
currentWeek = []
|
||||
}
|
||||
}
|
||||
|
||||
return weeks
|
||||
}
|
||||
|
||||
export const getHeatmapColor = (maxCost: number, value: number) => {
|
||||
if (value === 0) return '#1f2937'
|
||||
const denominator = maxCost === 0 ? 1 : maxCost
|
||||
const intensity = value / denominator
|
||||
|
||||
if (intensity < 0.25) return '#4a3328'
|
||||
if (intensity < 0.5) return '#6b4530'
|
||||
if (intensity < 0.75) return '#8d5738'
|
||||
return '#c15f3c'
|
||||
}
|
||||
|
||||
export const buildModelUsageData = (daily: DailyData[]) => {
|
||||
const raw = daily.reduce((acc, day) => {
|
||||
day.modelBreakdowns.forEach(model => {
|
||||
const label = getModelLabel(model.modelName)
|
||||
const existing = acc.find(m => m.name === label)
|
||||
if (existing) {
|
||||
existing.value += model.cost
|
||||
} else {
|
||||
acc.push({ name: label, value: model.cost })
|
||||
}
|
||||
})
|
||||
return acc
|
||||
}, [] as { name: string; value: number }[])
|
||||
return raw.sort((a, b) => b.value - a.value)
|
||||
}
|
||||
|
||||
export const buildTokenTypeData = (totals: CCData['totals']) => ([
|
||||
{ name: 'Input', value: totals.inputTokens },
|
||||
{ name: 'Output', value: totals.outputTokens },
|
||||
{ name: 'Cache Creation', value: totals.cacheCreationTokens },
|
||||
{ name: 'Cache Read', value: totals.cacheReadTokens },
|
||||
])
|
||||
|
||||
|
|
@ -2,67 +2,21 @@
|
|||
|
||||
import Header from '@/components/Header'
|
||||
import Footer from '@/components/Footer'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { SiClaude } from 'react-icons/si'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
Line,
|
||||
BarChart,
|
||||
Bar,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
Area,
|
||||
AreaChart,
|
||||
ComposedChart,
|
||||
} from 'recharts'
|
||||
|
||||
interface ModelBreakdown {
|
||||
modelName: string
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheCreationTokens: number
|
||||
cacheReadTokens: number
|
||||
cost: number
|
||||
}
|
||||
|
||||
interface DailyData {
|
||||
date: string
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheCreationTokens: number
|
||||
cacheReadTokens: number
|
||||
totalTokens: number
|
||||
totalCost: number
|
||||
modelsUsed: string[]
|
||||
modelBreakdowns: ModelBreakdown[]
|
||||
}
|
||||
|
||||
interface CCData {
|
||||
daily: DailyData[]
|
||||
totals: {
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheCreationTokens: number
|
||||
cacheReadTokens: number
|
||||
totalCost: number
|
||||
totalTokens: number
|
||||
}
|
||||
}
|
||||
|
||||
const COLORS = ['#c15f3c', '#b1ada1', '#f4f3ee', '#c15f3c', '#b1ada1', '#f4f3ee']
|
||||
import { useEffect, useState } from 'react'
|
||||
import LoadingSkeleton from './components/LoadingSkeleton'
|
||||
import PageHeader from './components/PageHeader'
|
||||
import StatsGrid from './components/StatsGrid'
|
||||
import Activity from './components/Activity'
|
||||
import ModelUsageCard from './components/ModelUsageCard'
|
||||
import TokenTypeBreakdown from './components/TokenTypeBreakdown'
|
||||
import TokenComposition from './components/TokenComposition'
|
||||
import RecentSessions from './components/RecentSessions'
|
||||
import { CCData } from './components/types'
|
||||
|
||||
export default function AI() {
|
||||
const [data, setData] = useState<CCData | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [selectedMetric, setSelectedMetric] = useState<'cost' | 'tokens'>('cost')
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/data/cc.json')
|
||||
|
|
@ -84,104 +38,7 @@ export default function AI() {
|
|||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="w-full relative">
|
||||
<Link
|
||||
href="/ai"
|
||||
className="absolute top-4 left-4 text-gray-400 hover:text-gray-200 hover:underline transition-colors duration-200 z-10 px-2 py-1 text-sm sm:text-base"
|
||||
>
|
||||
← Back to AI
|
||||
</Link>
|
||||
|
||||
<div className="my-12 text-center">
|
||||
<div className="flex justify-center mb-6">
|
||||
<SiClaude size={60} />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-2 text-gray-100 glow">Claude Code Usage</h1>
|
||||
<p className="text-gray-400">How much I use Claude Code!</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 px-4">
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Total Cost</h3>
|
||||
<div className="h-9 w-32 bg-gray-800 rounded animate-pulse" />
|
||||
</div>
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Total Tokens</h3>
|
||||
<div className="h-9 w-32 bg-gray-800 rounded animate-pulse" />
|
||||
</div>
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Days Active</h3>
|
||||
<div className="h-9 w-32 bg-gray-800 rounded animate-pulse" />
|
||||
</div>
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Avg Daily Cost</h3>
|
||||
<div className="h-9 w-32 bg-gray-800 rounded animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 p-4">
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Daily Usage Trend</h2>
|
||||
<div className="flex gap-2 mb-4">
|
||||
<button className="px-3 py-1 rounded bg-gray-700 text-gray-300" disabled>
|
||||
Cost
|
||||
</button>
|
||||
<button className="px-3 py-1 rounded bg-gray-700 text-gray-300" disabled>
|
||||
Tokens
|
||||
</button>
|
||||
</div>
|
||||
<div className="h-[300px] bg-gray-800 rounded animate-pulse" />
|
||||
</section>
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Model Usage Distribution</h2>
|
||||
<div className="h-[300px] bg-gray-800 rounded animate-pulse" />
|
||||
</section>
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Token Type Breakdown</h2>
|
||||
<div className="h-[300px] bg-gray-800 rounded animate-pulse" />
|
||||
</section>
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Daily Token Composition</h2>
|
||||
<div className="h-[300px] bg-gray-800 rounded animate-pulse" />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="px-4 pb-4">
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Recent Sessions</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-700">
|
||||
<th className="py-2 px-4 text-gray-400">Date</th>
|
||||
<th className="py-2 px-4 text-gray-400">Models Used</th>
|
||||
<th className="py-2 px-4 text-gray-400">Total Tokens</th>
|
||||
<th className="py-2 px-4 text-gray-400">Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<tr key={index} className="border-b border-gray-800">
|
||||
<td className="py-2 px-4">
|
||||
<div className="h-5 w-24 bg-gray-800 rounded animate-pulse" />
|
||||
</td>
|
||||
<td className="py-2 px-4">
|
||||
<div className="h-5 w-96 bg-gray-800 rounded animate-pulse" />
|
||||
</td>
|
||||
<td className="py-2 px-4">
|
||||
<div className="h-5 w-16 bg-gray-800 rounded animate-pulse" />
|
||||
</td>
|
||||
<td className="py-2 px-4">
|
||||
<div className="h-5 w-20 bg-gray-800 rounded animate-pulse" />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<LoadingSkeleton />
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
|
|
@ -199,247 +56,30 @@ export default function AI() {
|
|||
)
|
||||
}
|
||||
|
||||
const modelUsageData = data.daily.reduce((acc, day) => {
|
||||
day.modelBreakdowns.forEach(model => {
|
||||
const existing = acc.find(m => m.name === model.modelName)
|
||||
if (existing) {
|
||||
existing.value += model.cost
|
||||
} else {
|
||||
acc.push({ name: model.modelName, value: model.cost })
|
||||
}
|
||||
})
|
||||
return acc
|
||||
}, [] as { name: string; value: number }[])
|
||||
.sort((a, b) => b.value - a.value)
|
||||
|
||||
const tokenTypeData = [
|
||||
{ name: 'Input', value: data.totals.inputTokens },
|
||||
{ name: 'Output', value: data.totals.outputTokens },
|
||||
{ name: 'Cache Creation', value: data.totals.cacheCreationTokens },
|
||||
{ name: 'Cache Read', value: data.totals.cacheReadTokens },
|
||||
]
|
||||
|
||||
const dailyTrendData = data.daily.map(day => ({
|
||||
date: new Date(day.date + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
||||
cost: day.totalCost,
|
||||
tokens: day.totalTokens / 1000000,
|
||||
inputTokens: day.inputTokens / 1000,
|
||||
outputTokens: day.outputTokens / 1000,
|
||||
cacheTokens: (day.cacheCreationTokens + day.cacheReadTokens) / 1000000,
|
||||
}))
|
||||
|
||||
const formatCurrency = (value: number) => `$${value.toFixed(2)}`
|
||||
const formatTokens = (value: number) => `${value.toFixed(1)}M`
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="w-full relative">
|
||||
<Link
|
||||
href="/ai"
|
||||
className="absolute top-4 left-4 text-gray-400 hover:text-gray-200 hover:underline transition-colors duration-200 z-10 px-2 py-1 text-sm sm:text-base"
|
||||
>
|
||||
← Back to AI
|
||||
</Link>
|
||||
<div className="my-12 text-center">
|
||||
<div className="flex justify-center mb-6">
|
||||
<SiClaude size={60} />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-2 text-gray-100 glow">Claude Code Usage</h1>
|
||||
<p className="text-gray-400">How much I use Claude Code!</p>
|
||||
</div>
|
||||
<PageHeader />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 px-4">
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Total Cost</h3>
|
||||
<p className="text-3xl font-bold text-[#c15f3c]">${data.totals.totalCost.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Total Tokens</h3>
|
||||
<p className="text-3xl font-bold text-[#c15f3c]">{(data.totals.totalTokens / 1000000).toFixed(1)}M</p>
|
||||
</div>
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Days Active</h3>
|
||||
<p className="text-3xl font-bold text-[#c15f3c]">{data.daily.length}</p>
|
||||
</div>
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Avg Daily Cost</h3>
|
||||
<p className="text-3xl font-bold text-[#c15f3c]">${(data.totals.totalCost / data.daily.length).toFixed(2)}</p>
|
||||
</div>
|
||||
<StatsGrid totals={data.totals} daily={data.daily} />
|
||||
|
||||
<div className="p-4 pb-0">
|
||||
<Activity daily={data.daily} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 p-4">
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Daily Usage Trend</h2>
|
||||
<div className="flex gap-2 mb-4">
|
||||
<button
|
||||
onClick={() => setSelectedMetric('cost')}
|
||||
className={`px-3 py-1 rounded ${selectedMetric === 'cost' ? 'bg-[#c15f3c] text-white' : 'bg-gray-700 text-gray-300'}`}
|
||||
>
|
||||
Cost
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedMetric('tokens')}
|
||||
className={`px-3 py-1 rounded ${selectedMetric === 'tokens' ? 'bg-[#c15f3c] text-white' : 'bg-gray-700 text-gray-300'}`}
|
||||
>
|
||||
Tokens
|
||||
</button>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={dailyTrendData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||
<XAxis dataKey="date" stroke="#9ca3af" />
|
||||
<YAxis
|
||||
stroke="#9ca3af"
|
||||
tickFormatter={selectedMetric === 'cost' ? formatCurrency : formatTokens}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151' }}
|
||||
formatter={(value: number) => selectedMetric === 'cost' ? formatCurrency(value) : formatTokens(value)}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey={selectedMetric === 'cost' ? 'cost' : 'tokens'}
|
||||
stroke="#c15f3c"
|
||||
fill="#c15f3c"
|
||||
fillOpacity={0.3}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</section>
|
||||
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Model Usage Distribution</h2>
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={modelUsageData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={100}
|
||||
fill="#8884d8"
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
>
|
||||
{modelUsageData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151', borderRadius: '8px' }}
|
||||
formatter={(value: number) => formatCurrency(value)}
|
||||
labelStyle={{ color: '#fff' }}
|
||||
itemStyle={{ color: '#fff' }}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="flex flex-col justify-center space-y-3">
|
||||
{modelUsageData.map((model, index) => {
|
||||
const percentage = ((model.value / data.totals.totalCost) * 100).toFixed(1)
|
||||
return (
|
||||
<div key={index} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: COLORS[index % COLORS.length] }}
|
||||
/>
|
||||
<span className="text-gray-300 font-medium text-xs">{model.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-gray-400 text-sm">{percentage}%</span>
|
||||
<span className="text-gray-200 font-semibold">${model.value.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="pt-3 mt-3 border-t border-gray-700">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-400">Total Models Used</span>
|
||||
<span className="text-gray-200 font-bold">{modelUsageData.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<span className="text-gray-400">Most Used</span>
|
||||
<span className="text-gray-200 font-bold text-xs">
|
||||
{modelUsageData[0]?.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Token Type Breakdown</h2>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={tokenTypeData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||
<XAxis dataKey="name" stroke="#9ca3af" />
|
||||
<YAxis stroke="#9ca3af" tickFormatter={(value) => `${(value / 1000000).toFixed(0)}M`} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: 'rgba(31, 41, 55)', border: '1px solid #374151' }}
|
||||
formatter={(value: number) => `${(value / 1000000).toFixed(2)}M tokens`}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="value"
|
||||
fill="#b1ada1"
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</section>
|
||||
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Daily Token Composition</h2>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<ComposedChart data={dailyTrendData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||
<XAxis dataKey="date" stroke="#9ca3af" />
|
||||
<YAxis stroke="#9ca3af" tickFormatter={(value) => `${value}K`} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151' }}
|
||||
formatter={(value: number) => `${value.toFixed(1)}K tokens`}
|
||||
/>
|
||||
<Legend />
|
||||
<Bar dataKey="inputTokens" stackId="a" fill="#c15f3c" name="Input (K)" />
|
||||
<Bar dataKey="outputTokens" stackId="a" fill="#b1ada1" name="Output (K)" />
|
||||
<Line type="monotone" dataKey="cacheTokens" stroke="#f4f3ee" name="Cache (M)" strokeWidth={2} />
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</section>
|
||||
<ModelUsageCard daily={data.daily} totalCost={data.totals.totalCost} />
|
||||
<TokenTypeBreakdown totals={data.totals} />
|
||||
<TokenComposition daily={data.daily} />
|
||||
</div>
|
||||
|
||||
<div className="px-4 pb-4">
|
||||
<section className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Recent Sessions</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-700">
|
||||
<th className="py-2 px-4 text-gray-400">Date</th>
|
||||
<th className="py-2 px-4 text-gray-400">Models Used</th>
|
||||
<th className="py-2 px-4 text-gray-400">Total Tokens</th>
|
||||
<th className="py-2 px-4 text-gray-400">Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.daily.slice(-5).reverse().map((day, index) => (
|
||||
<tr key={index} className="border-b border-gray-800 hover:bg-gray-800/50">
|
||||
<td className="py-2 px-4 text-gray-300">{new Date(day.date + 'T00:00:00').toLocaleDateString()}</td>
|
||||
<td className="py-2 px-4 text-gray-300">
|
||||
{day.modelsUsed.join(', ')}
|
||||
</td>
|
||||
<td className="py-2 px-4 text-gray-300">{(day.totalTokens / 1000000).toFixed(2)}M</td>
|
||||
<td className="py-2 px-4 text-[#c15f3c] font-semibold">${day.totalCost.toFixed(2)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<RecentSessions daily={data.daily} />
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue