diff --git a/tenant-dashboard/src/components/cost/BudgetAlerts.tsx b/tenant-dashboard/src/components/cost/BudgetAlerts.tsx new file mode 100644 index 0000000..ce4ea08 --- /dev/null +++ b/tenant-dashboard/src/components/cost/BudgetAlerts.tsx @@ -0,0 +1,319 @@ +"use client"; + +import React, { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Switch } from "@/components/ui/switch"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Progress } from "@/components/ui/progress"; +import { useBudgetAlerts, useCostData } from "@/hooks/useCostData"; +import { formatCurrency } from "@/lib/utils"; +import { + AlertTriangle, + Plus, + Settings, + Bell, + BellOff, + Trash2, + Target, + DollarSign, + Percent +} from "lucide-react"; + +interface BudgetAlertsProps { + className?: string; +} + +export function BudgetAlerts({ className }: BudgetAlertsProps) { + const [showCreateDialog, setShowCreateDialog] = useState(false); + const [editingAlert, setEditingAlert] = useState(null); + const [newAlert, setNewAlert] = useState({ + name: "", + threshold: "", + type: "fixed" as "fixed" | "percentage", + period: "monthly" as "daily" | "weekly" | "monthly", + notifications: ["email"] as string[], + }); + + const { data: budgetAlerts, isLoading } = useBudgetAlerts(); + const { data: costData } = useCostData(); + + const handleCreateAlert = () => { + // This would call the API to create a new alert + console.log("Creating alert:", newAlert); + setShowCreateDialog(false); + setNewAlert({ + name: "", + threshold: "", + type: "fixed", + period: "monthly", + notifications: ["email"], + }); + }; + + const handleToggleAlert = (alertId: string, isActive: boolean) => { + // This would call the API to toggle the alert + console.log(`Toggling alert ${alertId} to ${isActive}`); + }; + + const handleDeleteAlert = (alertId: string) => { + // This would call the API to delete the alert + console.log(`Deleting alert ${alertId}`); + }; + + const calculateProgress = (alert: any) => { + if (!costData) return 0; + + if (alert.type === "percentage") { + const total = alert.period === "monthly" ? costData.projectedCost : costData.totalCost; + return Math.min((alert.currentSpend / total) * 100, 100); + } else { + return Math.min((alert.currentSpend / alert.threshold) * 100, 100); + } + }; + + const getAlertStatus = (alert: any) => { + const progress = calculateProgress(alert); + if (progress >= 95) return "critical"; + if (progress >= 80) return "warning"; + return "normal"; + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "critical": + return "text-red-600 bg-red-50 border-red-200"; + case "warning": + return "text-orange-600 bg-orange-50 border-orange-200"; + default: + return "text-green-600 bg-green-50 border-green-200"; + } + }; + + if (isLoading) { + return ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + + +
+
+
+ ))} +
+ ); + } + + return ( +
+ {/* Header */} +
+
+

Budget Alerts

+

+ Set up alerts to monitor your spending and avoid surprises +

+
+ + + + + + + + Create Budget Alert + +
+
+ + setNewAlert({ ...newAlert, name: e.target.value })} + /> +
+ +
+
+ + setNewAlert({ ...newAlert, threshold: e.target.value })} + /> +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+
+
+
+
+ + {/* Alert Cards */} +
+ {budgetAlerts?.map((alert) => { + const progress = calculateProgress(alert); + const status = getAlertStatus(alert); + + return ( + + +
+
+ {/* Alert Header */} +
+
+

{alert.name}

+ + {status === "critical" && } + {status.charAt(0).toUpperCase() + status.slice(1)} + +
+ +
+ handleToggleAlert(alert.id, checked)} + /> + +
+
+ + {/* Progress and Details */} +
+
+ + Current Spend: {formatCurrency(alert.currentSpend)} + + + {alert.type === "percentage" + ? `${alert.threshold}% of budget` + : `${formatCurrency(alert.threshold)} limit` + } + +
+ + div]:bg-red-500" : + status === "warning" ? "[&>div]:bg-orange-500" : + "[&>div]:bg-green-500" + }`} + /> + +
+ {progress.toFixed(1)}% of threshold + {alert.period} budget +
+
+ + {/* Notifications */} +
+
+ {alert.isActive ? ( + + ) : ( + + )} + + {alert.isActive ? "Active" : "Inactive"} + +
+ +
+ + + {alert.notifications.join(", ")} + +
+ +
+ {alert.type === "percentage" ? ( + + ) : ( + + )} + + {alert.type} threshold + +
+
+
+
+
+
+ ); + })} + + {(!budgetAlerts || budgetAlerts.length === 0) && ( + + +
+ +

No budget alerts set up

+

+ Create your first budget alert to monitor spending and get notified when you approach your limits. +

+ +
+
+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/tenant-dashboard/src/components/cost/CostDashboard.tsx b/tenant-dashboard/src/components/cost/CostDashboard.tsx new file mode 100644 index 0000000..df4aaa7 --- /dev/null +++ b/tenant-dashboard/src/components/cost/CostDashboard.tsx @@ -0,0 +1,288 @@ +"use client"; + +import React, { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +// Using state-based tabs instead of Radix tabs component +import { MetricCard } from "@/components/dashboard/MetricCard"; +import { UsageChart } from "./UsageChart"; +import { BudgetAlerts } from "./BudgetAlerts"; +import { ModelComparison } from "./ModelComparison"; +import { useCostSummary } from "@/hooks/useCostData"; +import { formatCurrency, formatNumber } from "@/lib/utils"; +import { + DollarSign, + TrendingUp, + AlertTriangle, + Download, + Calendar, + CreditCard, + BarChart3 +} from "lucide-react"; + +interface CostDashboardProps { + className?: string; +} + +export function CostDashboard({ className }: CostDashboardProps) { + const [period, setPeriod] = useState("monthly"); + const [activeTab, setActiveTab] = useState("overview"); + const { costData, costHistory, budgetAlerts, modelCosts, isLoading, isError } = useCostSummary(); + + const handleExportData = () => { + if (!costData || !costHistory) return; + + const exportData = { + summary: costData, + history: costHistory, + exportDate: new Date().toISOString(), + }; + + const blob = new Blob([JSON.stringify(exportData, null, 2)], { + type: 'application/json', + }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `cost-report-${new Date().toISOString().split('T')[0]}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + if (isLoading) { + return ( +
+
+

Cost Tracking

+
+
+ {Array.from({ length: 4 }).map((_, i) => ( + + +
+
+
+ ))} +
+
+ ); + } + + if (isError || !costData) { + return ( +
+
+ +

Unable to load cost data

+

Please try again later or contact support.

+
+
+ ); + } + + const activeAlerts = budgetAlerts?.filter(alert => alert.isActive) || []; + const criticalAlerts = activeAlerts.filter(alert => { + const threshold = alert.type === 'percentage' + ? (costData.totalCost / costData.projectedCost) * 100 + : alert.currentSpend; + return threshold >= alert.threshold * 0.9; // 90% of threshold + }); + + const costTrend = costHistory && costHistory.length >= 2 + ? ((costHistory[costHistory.length - 1].cost - costHistory[costHistory.length - 2].cost) / costHistory[costHistory.length - 2].cost) * 100 + : 0; + + return ( +
+ {/* Header */} +
+
+

Cost Tracking

+

+ Monitor usage and costs across your PyAirtable services +

+
+ +
+ + + +
+
+ + {/* Critical Alerts */} + {criticalAlerts.length > 0 && ( +
+ + +
+ + Budget Alerts +
+
+ +
+ {criticalAlerts.map((alert) => ( +
+ {alert.name} + + {alert.type === 'percentage' ? `${Math.round((alert.currentSpend / costData.totalCost) * 100)}%` : formatCurrency(alert.currentSpend)} of {alert.type === 'percentage' ? `${alert.threshold}%` : formatCurrency(alert.threshold)} + +
+ ))} +
+
+
+
+ )} + + {/* Key Metrics */} +
+ = 0 ? "increase" : "decrease"} + description={`${period} total`} + icon={} + /> + + } + /> + + } + /> + + } + /> +
+ + {/* Main Content Tabs */} +
+
+ +
+ + {activeTab === "overview" && ( +
+
+ {/* Cost Breakdown */} + + + Cost Breakdown + + +
+ {costData.breakdown.map((item, index) => ( +
+
+
{item.category}
+
+ {formatNumber(item.usage)} {item.unit} +
+
+
+
{formatCurrency(item.cost)}
+
+ {item.percentage.toFixed(1)}% +
+
+
+ ))} +
+
+
+ + {/* Usage Overview */} + + + Usage Overview + + +
+
+
+
Compute Hours
+
{formatNumber(costData.usage.computeHours)}
+
+
+
Storage Used
+
{(costData.usage.storage / (1024 ** 3)).toFixed(2)} GB
+
+
+
Data Transfer
+
{(costData.usage.dataTransfer / (1024 ** 2)).toFixed(0)} MB
+
+
+
Period
+
{period}
+
+
+
+
+
+
+
+ )} + + {activeTab === "usage" && } + + {activeTab === "budgets" && } + + {activeTab === "models" && } +
+
+ ); +} \ No newline at end of file diff --git a/tenant-dashboard/src/components/cost/ModelComparison.tsx b/tenant-dashboard/src/components/cost/ModelComparison.tsx new file mode 100644 index 0000000..5a5f3ca --- /dev/null +++ b/tenant-dashboard/src/components/cost/ModelComparison.tsx @@ -0,0 +1,333 @@ +"use client"; + +import React, { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Progress } from "@/components/ui/progress"; +import { MetricsChart } from "@/components/dashboard/MetricsChart"; +import { useModelCosts } from "@/hooks/useCostData"; +import { formatCurrency, formatNumber } from "@/lib/utils"; +import { + Zap, + Clock, + CheckCircle, + DollarSign, + TrendingUp, + TrendingDown, + BarChart3, + ArrowUpDown +} from "lucide-react"; + +interface ModelComparisonProps { + className?: string; +} + +export function ModelComparison({ className }: ModelComparisonProps) { + const [sortBy, setSortBy] = useState("totalCost"); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); + const [chartMetric, setChartMetric] = useState("totalCost"); + + const { data: modelCosts, isLoading, isError } = useModelCosts(); + + if (isLoading) { + return ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + + +
+
+
+ ))} +
+ ); + } + + if (isError || !modelCosts) { + return ( + + +
+ Unable to load model comparison data. Please try again. +
+
+
+ ); + } + + // Sort models based on selected criteria + const sortedModels = [...modelCosts].sort((a, b) => { + const aValue = a[sortBy as keyof typeof a] as number; + const bValue = b[sortBy as keyof typeof b] as number; + return sortOrder === "asc" ? aValue - bValue : bValue - aValue; + }); + + // Calculate totals and averages + const totalCost = modelCosts.reduce((sum, model) => sum + model.totalCost, 0); + const totalCalls = modelCosts.reduce((sum, model) => sum + model.totalCalls, 0); + const avgResponseTime = modelCosts.reduce((sum, model) => sum + model.avgResponseTime * model.totalCalls, 0) / totalCalls; + const avgSuccessRate = modelCosts.reduce((sum, model) => sum + model.successRate * model.totalCalls, 0) / totalCalls; + + // Chart data for model comparison + const chartData = modelCosts.map((model) => ({ + model: model.modelName, + totalCost: model.totalCost, + totalCalls: model.totalCalls, + avgResponseTime: model.avgResponseTime, + successRate: model.successRate, + costPerCall: model.totalCalls > 0 ? model.totalCost / model.totalCalls : 0, + })); + + const getChartConfig = () => { + switch (chartMetric) { + case "totalCost": + return { + title: "Total Cost by Model", + dataKey: "totalCost", + color: "#ef4444", + formatValue: formatCurrency, + }; + case "totalCalls": + return { + title: "API Calls by Model", + dataKey: "totalCalls", + color: "#3b82f6", + formatValue: formatNumber, + }; + case "avgResponseTime": + return { + title: "Average Response Time", + dataKey: "avgResponseTime", + color: "#f59e0b", + formatValue: (value: number) => `${value.toFixed(2)}s`, + }; + case "costPerCall": + return { + title: "Cost per API Call", + dataKey: "costPerCall", + color: "#10b981", + formatValue: (value: number) => formatCurrency(value), + }; + default: + return { + title: "Model Metrics", + dataKey: "totalCost", + color: "#3b82f6", + formatValue: (value: number) => value.toString(), + }; + } + }; + + const chartConfig = getChartConfig(); + + const getProviderColor = (provider: string) => { + switch (provider.toLowerCase()) { + case "openai": + return "bg-green-100 text-green-800"; + case "anthropic": + return "bg-purple-100 text-purple-800"; + case "google": + return "bg-blue-100 text-blue-800"; + default: + return "bg-gray-100 text-gray-800"; + } + }; + + return ( +
+ {/* Header */} +
+
+

Model Cost Comparison

+

+ Compare performance and costs across different AI models +

+
+ +
+ + + +
+
+ + {/* Summary Cards */} +
+ + +
+
+

Total Spend

+

{formatCurrency(totalCost)}

+
+ +
+
+
+ + + +
+
+

Total Calls

+

{formatNumber(totalCalls)}

+
+ +
+
+
+ + + +
+
+

Avg Response

+

{avgResponseTime.toFixed(2)}s

+
+ +
+
+
+ + + +
+
+

Avg Success

+

{avgSuccessRate.toFixed(1)}%

+
+ +
+
+
+
+ + {/* Chart */} + + + Model Performance + + + + + + + + {/* Model Details */} +
+

Model Details

+ + {sortedModels.map((model) => { + const costPerCall = model.totalCalls > 0 ? model.totalCost / model.totalCalls : 0; + const costShare = totalCost > 0 ? (model.totalCost / totalCost) * 100 : 0; + + return ( + + +
+
+
+
{model.modelName}
+
+ + {model.provider} + + + {costShare.toFixed(1)}% of total spend + +
+
+
+ +
+
{formatCurrency(model.totalCost)}
+
+ {formatCurrency(costPerCall)} per call +
+
+
+ +
+
+
+ + API Calls +
+
{formatNumber(model.totalCalls)}
+
+ +
+
+ + Response Time +
+
{model.avgResponseTime.toFixed(2)}s
+
+ +
+
+ + Success Rate +
+
+
{model.successRate.toFixed(1)}%
+ +
+
+ +
+
+ + Pricing +
+
+
In: {formatCurrency(model.inputCost)}/1K tokens
+
Out: {formatCurrency(model.outputCost)}/1K tokens
+
+
+
+
+
+ ); + })} +
+
+ ); +} \ No newline at end of file diff --git a/tenant-dashboard/src/components/cost/UsageChart.tsx b/tenant-dashboard/src/components/cost/UsageChart.tsx new file mode 100644 index 0000000..48bd7f7 --- /dev/null +++ b/tenant-dashboard/src/components/cost/UsageChart.tsx @@ -0,0 +1,272 @@ +"use client"; + +import React, { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { MetricsChart } from "@/components/dashboard/MetricsChart"; +import { useCostHistory } from "@/hooks/useCostData"; +import { formatCurrency, formatNumber } from "@/lib/utils"; +import { format } from "date-fns"; +import { TrendingUp, DollarSign, Activity, BarChart3 } from "lucide-react"; + +interface UsageChartProps { + className?: string; +} + +export function UsageChart({ className }: UsageChartProps) { + const [timeframe, setTimeframe] = useState("30"); + const [chartType, setChartType] = useState("line"); + const [metricType, setMetricType] = useState("cost"); + + const { data: costHistory, isLoading, isError } = useCostHistory(parseInt(timeframe)); + + if (isLoading) { + return ( +
+ + +
+
+
+
+ ); + } + + if (isError || !costHistory) { + return ( + + +
+ Unable to load usage data. Please try again. +
+
+
+ ); + } + + // Transform data for charts + const chartData = costHistory.map((item) => ({ + date: format(new Date(item.date), 'MMM dd'), + timestamp: item.date, + cost: item.cost, + usage: item.usage, + apiCost: item.breakdown.find(b => b.category === 'API')?.cost || 0, + computeCost: item.breakdown.find(b => b.category === 'Compute')?.cost || 0, + })); + + // Calculate summary metrics + const totalCost = chartData.reduce((sum, item) => sum + item.cost, 0); + const totalUsage = chartData.reduce((sum, item) => sum + item.usage, 0); + const avgDailyCost = totalCost / chartData.length; + const avgDailyUsage = totalUsage / chartData.length; + + // Calculate trends + const costTrend = chartData.length >= 2 + ? ((chartData[chartData.length - 1].cost - chartData[0].cost) / chartData[0].cost) * 100 + : 0; + + const usageTrend = chartData.length >= 2 + ? ((chartData[chartData.length - 1].usage - chartData[0].usage) / chartData[0].usage) * 100 + : 0; + + const getChartConfig = () => { + switch (metricType) { + case "cost": + return { + title: "Cost Trends", + description: `Daily cost over the last ${timeframe} days`, + dataKey: "cost", + color: "#ef4444", // red + formatValue: (value: number) => formatCurrency(value), + }; + case "usage": + return { + title: "API Usage Trends", + description: `Daily API requests over the last ${timeframe} days`, + dataKey: "usage", + color: "#3b82f6", // blue + formatValue: (value: number) => formatNumber(value), + }; + case "breakdown": + return { + title: "Cost Breakdown", + description: "API vs Compute costs", + dataKey: "apiCost", + color: "#10b981", // green + formatValue: (value: number) => formatCurrency(value), + }; + default: + return { + title: "Metrics", + description: "", + dataKey: "cost", + color: "#3b82f6", + formatValue: (value: number) => value.toString(), + }; + } + }; + + const chartConfig = getChartConfig(); + + return ( +
+ {/* Controls */} +
+
+

Usage Analytics

+

+ Track your usage patterns and cost trends over time +

+
+ +
+ + + + + +
+
+ + {/* Summary Cards */} +
+ + +
+
+

Total Cost

+

{formatCurrency(totalCost)}

+
+ +
+
+ = 0 ? 'text-red-500' : 'text-green-500'}`} /> + = 0 ? 'text-red-500' : 'text-green-500'}> + {Math.abs(costTrend).toFixed(1)}% vs first day + +
+
+
+ + + +
+
+

Total Requests

+

{formatNumber(totalUsage)}

+
+ +
+
+ = 0 ? 'text-green-500' : 'text-red-500'}`} /> + = 0 ? 'text-green-500' : 'text-red-500'}> + {Math.abs(usageTrend).toFixed(1)}% vs first day + +
+
+
+ + + +
+
+

Avg Daily Cost

+

{formatCurrency(avgDailyCost)}

+
+ +
+

+ Per day average +

+
+
+ + + +
+
+

Avg Daily Usage

+

{formatNumber(avgDailyUsage)}

+
+ +
+

+ Requests per day +

+
+
+
+ + {/* Main Chart */} + + + {/* Additional Charts for Breakdown View */} + {metricType === "breakdown" && ( +
+ formatCurrency(value)} + /> + + formatCurrency(value)} + /> +
+ )} +
+ ); +} \ No newline at end of file diff --git a/tenant-dashboard/src/hooks/useCostData.ts b/tenant-dashboard/src/hooks/useCostData.ts new file mode 100644 index 0000000..6e90e4c --- /dev/null +++ b/tenant-dashboard/src/hooks/useCostData.ts @@ -0,0 +1,287 @@ +"use client"; + +import { useApiQuery, usePaginatedQuery } from "@/hooks/useApi"; +import { queryKeys } from "@/lib/api/types"; +import type { FilterOptions } from "@/types"; + +// Cost and usage data interfaces +export interface CostData { + period: { + start: string; + end: string; + }; + totalCost: number; + currency: string; + breakdown: CostBreakdown[]; + usage: UsageMetrics; + projectedCost: number; +} + +export interface CostBreakdown { + category: string; + service: string; + cost: number; + usage: number; + unit: string; + percentage: number; +} + +export interface UsageMetrics { + apiCalls: number; + computeHours: number; + storage: number; + dataTransfer: number; + timestamp: string; +} + +export interface BudgetAlert { + id: string; + name: string; + threshold: number; + type: 'percentage' | 'fixed'; + period: 'daily' | 'weekly' | 'monthly'; + isActive: boolean; + currentSpend: number; + notifications: string[]; +} + +export interface ModelCost { + modelId: string; + modelName: string; + provider: string; + inputCost: number; + outputCost: number; + totalCalls: number; + totalCost: number; + avgResponseTime: number; + successRate: number; +} + +export interface CostHistory { + date: string; + cost: number; + usage: number; + breakdown: CostBreakdown[]; +} + +// API functions (these would be implemented in your API client) +const costApi = { + getCostData: async (period?: string): Promise<{ data: CostData }> => { + // Mock implementation - replace with actual API call + const now = new Date(); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + + return { + data: { + period: { + start: startOfMonth.toISOString(), + end: now.toISOString(), + }, + totalCost: 156.78, + currency: 'USD', + breakdown: [ + { + category: 'API Usage', + service: 'Airtable API', + cost: 89.50, + usage: 45000, + unit: 'requests', + percentage: 57.1, + }, + { + category: 'Compute', + service: 'AI Processing', + cost: 43.20, + usage: 120.5, + unit: 'hours', + percentage: 27.6, + }, + { + category: 'Storage', + service: 'Data Storage', + cost: 24.08, + usage: 2.1, + unit: 'GB', + percentage: 15.3, + }, + ], + usage: { + apiCalls: 45000, + computeHours: 120.5, + storage: 2200000000, // 2.1 GB in bytes + dataTransfer: 890000000, // 890 MB in bytes + timestamp: now.toISOString(), + }, + projectedCost: 187.34, + }, + }; + }, + + getCostHistory: async (days: number = 30): Promise<{ data: CostHistory[] }> => { + // Mock implementation + const history: CostHistory[] = []; + const now = new Date(); + + for (let i = days; i >= 0; i--) { + const date = new Date(now); + date.setDate(date.getDate() - i); + + const baseCost = 4.5 + Math.random() * 2; + history.push({ + date: date.toISOString(), + cost: baseCost, + usage: Math.floor(1200 + Math.random() * 800), + breakdown: [ + { + category: 'API', + service: 'Airtable API', + cost: baseCost * 0.6, + usage: Math.floor(1000 + Math.random() * 500), + unit: 'requests', + percentage: 60, + }, + { + category: 'Compute', + service: 'AI Processing', + cost: baseCost * 0.4, + usage: Math.floor(3 + Math.random() * 2), + unit: 'hours', + percentage: 40, + }, + ], + }); + } + + return { data: history }; + }, + + getBudgetAlerts: async (): Promise<{ data: BudgetAlert[] }> => { + return { + data: [ + { + id: '1', + name: 'Monthly Budget Alert', + threshold: 200, + type: 'fixed', + period: 'monthly', + isActive: true, + currentSpend: 156.78, + notifications: ['email', 'dashboard'], + }, + { + id: '2', + name: 'API Usage Warning', + threshold: 80, + type: 'percentage', + period: 'monthly', + isActive: true, + currentSpend: 89.50, + notifications: ['email'], + }, + ], + }; + }, + + getModelCosts: async (): Promise<{ data: ModelCost[] }> => { + return { + data: [ + { + modelId: 'gpt-4', + modelName: 'GPT-4', + provider: 'OpenAI', + inputCost: 0.03, + outputCost: 0.06, + totalCalls: 1250, + totalCost: 34.20, + avgResponseTime: 2.3, + successRate: 98.4, + }, + { + modelId: 'claude-3', + modelName: 'Claude 3 Sonnet', + provider: 'Anthropic', + inputCost: 0.003, + outputCost: 0.015, + totalCalls: 890, + totalCost: 15.60, + avgResponseTime: 1.8, + successRate: 99.1, + }, + { + modelId: 'gemini-pro', + modelName: 'Gemini Pro', + provider: 'Google', + inputCost: 0.0005, + outputCost: 0.0015, + totalCalls: 2100, + totalCost: 8.40, + avgResponseTime: 1.5, + successRate: 97.2, + }, + ], + }; + }, +}; + +// Cost data hooks +export function useCostData(period?: string) { + return useApiQuery( + queryKeys.analytics.reports('cost', period), + () => costApi.getCostData(period), + { + staleTime: 5 * 60 * 1000, // 5 minutes + refetchInterval: 10 * 60 * 1000, // Refetch every 10 minutes + } + ); +} + +export function useCostHistory(days: number = 30) { + return useApiQuery( + [...queryKeys.analytics, 'cost-history', days], + () => costApi.getCostHistory(days), + { + staleTime: 60 * 1000, // 1 minute + refetchInterval: 5 * 60 * 1000, // Refetch every 5 minutes + } + ); +} + +export function useBudgetAlerts() { + return useApiQuery( + [...queryKeys.tenant, 'budget-alerts'], + costApi.getBudgetAlerts, + { + staleTime: 60 * 1000, // 1 minute + } + ); +} + +export function useModelCosts() { + return useApiQuery( + [...queryKeys.analytics, 'model-costs'], + costApi.getModelCosts, + { + staleTime: 5 * 60 * 1000, // 5 minutes + } + ); +} + +// Usage summary hook +export function useCostSummary() { + const costData = useCostData(); + const costHistory = useCostHistory(7); // Last 7 days + const budgetAlerts = useBudgetAlerts(); + const modelCosts = useModelCosts(); + + const isLoading = costData.isLoading || costHistory.isLoading || budgetAlerts.isLoading || modelCosts.isLoading; + const isError = costData.isError || costHistory.isError || budgetAlerts.isError || modelCosts.isError; + + return { + costData: costData.data?.data, + costHistory: costHistory.data?.data, + budgetAlerts: budgetAlerts.data?.data, + modelCosts: modelCosts.data?.data, + isLoading, + isError, + }; +} \ No newline at end of file