VIBN Frontend for Coolify deployment
This commit is contained in:
34
app/[workspace]/costs/layout.tsx
Normal file
34
app/[workspace]/costs/layout.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { WorkspaceLeftRail } from "@/components/layout/workspace-left-rail";
|
||||
import { RightPanel } from "@/components/layout/right-panel";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
export default function CostsLayout({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [activeSection, setActiveSection] = useState<string>("costs");
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-screen w-full overflow-hidden bg-background">
|
||||
{/* Left Rail - Workspace Navigation */}
|
||||
<WorkspaceLeftRail activeSection={activeSection} onSectionChange={setActiveSection} />
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="flex-1 flex flex-col overflow-hidden">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
{/* Right Panel - AI Chat */}
|
||||
<RightPanel />
|
||||
</div>
|
||||
|
||||
<Toaster position="top-center" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
181
app/[workspace]/costs/page.tsx
Normal file
181
app/[workspace]/costs/page.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user