182 lines
6.6 KiB
TypeScript
182 lines
6.6 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { auth } from '@/lib/firebase/config';
|
|
import { toast } from 'sonner';
|
|
import { DollarSign, TrendingUp, TrendingDown, Calendar } from 'lucide-react';
|
|
import { useParams } from 'next/navigation';
|
|
|
|
interface CostData {
|
|
total: number;
|
|
thisMonth: number;
|
|
lastMonth: number;
|
|
byProject: Array<{
|
|
projectId: string;
|
|
projectName: string;
|
|
cost: number;
|
|
}>;
|
|
byDate: Array<{
|
|
date: string;
|
|
cost: number;
|
|
}>;
|
|
}
|
|
|
|
export default function CostsPage() {
|
|
const params = useParams();
|
|
const workspace = params.workspace as string;
|
|
const [costs, setCosts] = useState<CostData | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
loadCosts();
|
|
}, []);
|
|
|
|
const loadCosts = async () => {
|
|
try {
|
|
const user = auth.currentUser;
|
|
if (!user) return;
|
|
|
|
const token = await user.getIdToken();
|
|
const response = await fetch('/api/costs', {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setCosts(data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading costs:', error);
|
|
toast.error('Failed to load cost data');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const percentageChange = costs && costs.lastMonth > 0
|
|
? ((costs.thisMonth - costs.lastMonth) / costs.lastMonth) * 100
|
|
: 0;
|
|
|
|
return (
|
|
<div className="flex h-full flex-col overflow-auto">
|
|
<div className="flex-1 p-8 space-y-8 max-w-7xl">
|
|
{/* Header */}
|
|
<div>
|
|
<h1 className="text-4xl font-bold mb-2">Costs</h1>
|
|
<p className="text-muted-foreground text-lg">
|
|
Track your AI usage costs across all projects
|
|
</p>
|
|
</div>
|
|
|
|
{/* Summary Cards */}
|
|
{loading ? (
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<p className="text-center text-muted-foreground">Loading cost data...</p>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<>
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Total Costs</CardTitle>
|
|
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">${costs?.total.toFixed(2) || '0.00'}</div>
|
|
<p className="text-xs text-muted-foreground">All time</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">This Month</CardTitle>
|
|
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">${costs?.thisMonth.toFixed(2) || '0.00'}</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">vs Last Month</CardTitle>
|
|
{percentageChange >= 0 ? (
|
|
<TrendingUp className="h-4 w-4 text-red-500" />
|
|
) : (
|
|
<TrendingDown className="h-4 w-4 text-green-500" />
|
|
)}
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{percentageChange >= 0 ? '+' : ''}{percentageChange.toFixed(1)}%
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Last month: ${costs?.lastMonth.toFixed(2) || '0.00'}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Costs by Project */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Costs by Project</CardTitle>
|
|
<CardDescription>Your spending broken down by project</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{costs?.byProject && costs.byProject.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{costs.byProject.map((project) => (
|
|
<div key={project.projectId} className="flex items-center justify-between p-3 rounded-lg border">
|
|
<div>
|
|
<p className="font-medium">{project.projectName}</p>
|
|
<p className="text-sm text-muted-foreground">Project ID: {project.projectId}</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-lg font-semibold">${project.cost.toFixed(2)}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{((project.cost / (costs.total || 1)) * 100).toFixed(1)}% of total
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-center text-muted-foreground py-8">No project costs yet</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Info Card */}
|
|
<Card className="border-blue-500/20 bg-blue-500/5">
|
|
<CardHeader>
|
|
<CardTitle className="text-base">About Cost Tracking</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
|
<p>
|
|
<strong>📊 Automatic Tracking:</strong> All AI API costs are automatically tracked when you use Vibn features.
|
|
</p>
|
|
<p>
|
|
<strong>💰 Your Keys, Your Costs:</strong> Costs reflect usage of your own API keys - Vibn doesn't add any markup.
|
|
</p>
|
|
<p>
|
|
<strong>📈 Project Attribution:</strong> Costs are attributed to projects based on session metadata.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|