mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 02:15:36 +00:00
refactor(analytics): inline sessions table component
This commit is contained in:
1 parent
1ea67cc13f
commit
9506327343
3 files changed
+90
-446
No files matched your search
@@ -1,269 +0,0 @@
|
||||
/**
|
||||
* Sessions Table Component
|
||||
*
|
||||
* Displays session history with pagination and filtering.
|
||||
* Shows session duration, tokens, cost, and metadata.
|
||||
*/
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { ChevronLeft, ChevronRight, Search, Clock, Zap, DollarSign } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { PaginatedSessions } from '@/hooks/use-usage';
|
||||
|
||||
interface SessionsTableProps {
|
||||
data?: PaginatedSessions;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function SessionsTable({ data, isLoading }: SessionsTableProps) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
|
||||
// Get sessions array (stable reference for memoization)
|
||||
const sessions = useMemo(() => data?.sessions ?? [], [data?.sessions]);
|
||||
|
||||
// Filter sessions based on search term
|
||||
const filteredSessions = useMemo(() => {
|
||||
if (!searchTerm) return sessions;
|
||||
|
||||
const term = searchTerm.toLowerCase();
|
||||
return sessions.filter(
|
||||
(session) =>
|
||||
session.profile.toLowerCase().includes(term) ||
|
||||
session.model.toLowerCase().includes(term) ||
|
||||
session.id.toLowerCase().includes(term)
|
||||
);
|
||||
}, [sessions, searchTerm]);
|
||||
|
||||
// Pagination for filtered data
|
||||
const pageSize = 10;
|
||||
const paginatedSessions = useMemo(() => {
|
||||
if (!filteredSessions) return [];
|
||||
const start = currentPage * pageSize;
|
||||
return filteredSessions.slice(start, start + pageSize);
|
||||
}, [filteredSessions, currentPage]);
|
||||
|
||||
const totalPages = Math.ceil((filteredSessions?.length || 0) / pageSize);
|
||||
|
||||
if (isLoading) {
|
||||
return <SessionsTableSkeleton />;
|
||||
}
|
||||
|
||||
if (!data || data.sessions.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Clock className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium mb-1">No sessions found</h3>
|
||||
<p className="text-muted-foreground">Start using Claude Code to see session history</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Search Bar */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search by profile, model, or session ID..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setCurrentPage(0);
|
||||
}}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Session ID</TableHead>
|
||||
<TableHead>Profile</TableHead>
|
||||
<TableHead>Model</TableHead>
|
||||
<TableHead>Duration</TableHead>
|
||||
<TableHead className="text-right">Tokens</TableHead>
|
||||
<TableHead className="text-right">Cost</TableHead>
|
||||
<TableHead className="text-right">Requests</TableHead>
|
||||
<TableHead>Last Used</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{paginatedSessions.map((session) => (
|
||||
<TableRow key={session.id} className="hover:bg-muted/50">
|
||||
<TableCell className="font-mono text-xs">{session.id.slice(0, 8)}...</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">{session.profile}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{session.model}</TableCell>
|
||||
<TableCell>{session.duration ? formatDuration(session.duration) : '-'}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Zap className="h-3 w-3 text-muted-foreground" />
|
||||
{formatNumber(session.tokens)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<DollarSign className="h-3 w-3 text-muted-foreground" />$
|
||||
{session.cost.toFixed(4)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{session.requests}</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{formatDistanceToNow(new Date(session.startTime), { addSuffix: true })}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Showing {currentPage * pageSize + 1} to{' '}
|
||||
{Math.min((currentPage + 1) * pageSize, filteredSessions?.length || 0)} of{' '}
|
||||
{filteredSessions?.length} sessions
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage((p) => Math.max(0, p - 1))}
|
||||
disabled={currentPage === 0}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Previous
|
||||
</Button>
|
||||
<div className="flex items-center gap-1">
|
||||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||||
const page = i;
|
||||
return (
|
||||
<Button
|
||||
key={page}
|
||||
variant={currentPage === page ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={cn('w-8 h-8 p-0')}
|
||||
onClick={() => setCurrentPage(page)}
|
||||
>
|
||||
{page + 1}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={currentPage === totalPages - 1}
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function formatDuration(ms: number): string {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m ${seconds % 60}s`;
|
||||
}
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
function formatNumber(num: number): string {
|
||||
if (num >= 1000000) {
|
||||
return `${(num / 1000000).toFixed(1)}M`;
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return `${(num / 1000).toFixed(1)}K`;
|
||||
}
|
||||
return num.toLocaleString();
|
||||
}
|
||||
|
||||
// Skeleton loading state
|
||||
function SessionsTableSkeleton() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-10 flex-1" />
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Session ID</TableHead>
|
||||
<TableHead>Profile</TableHead>
|
||||
<TableHead>Model</TableHead>
|
||||
<TableHead>Duration</TableHead>
|
||||
<TableHead className="text-right">Tokens</TableHead>
|
||||
<TableHead className="text-right">Cost</TableHead>
|
||||
<TableHead className="text-right">Requests</TableHead>
|
||||
<TableHead>Last Used</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-[60px]" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-6 w-[80px]" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-[100px]" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-[60px]" />
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Skeleton className="h-4 w-[80px]" />
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Skeleton className="h-4 w-[70px]" />
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Skeleton className="h-4 w-[60px]" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-[80px]" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -175,22 +175,6 @@ export function useModelUsage(options?: UsageQueryOptions) {
|
||||
});
|
||||
}
|
||||
|
||||
export function useSessions(options?: UsageQueryOptions) {
|
||||
return useQuery({
|
||||
queryKey: ['usage', 'sessions', options],
|
||||
queryFn: () => usageApi.sessions(options),
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
});
|
||||
}
|
||||
|
||||
export function useMonthlyUsage(months?: number, profile?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['usage', 'monthly', months, profile],
|
||||
queryFn: () => usageApi.monthly(months, profile),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to refresh all usage data
|
||||
* Clears server-side cache and invalidates React Query cache
|
||||
|
||||
+90
-161
@@ -1,14 +1,13 @@
|
||||
/**
|
||||
* Analytics Page
|
||||
*
|
||||
* Displays Claude Code usage analytics with charts and tables.
|
||||
* Features daily/monthly views, trend charts, model breakdown, and session history.
|
||||
* Displays Claude Code usage analytics with charts.
|
||||
* Features trend charts, model breakdown, and cost analysis.
|
||||
*/
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import type { DateRange } from 'react-day-picker';
|
||||
import { startOfMonth, subDays, formatDistanceToNow } from 'date-fns';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
@@ -16,27 +15,22 @@ import { DateRangeFilter } from '@/components/analytics/date-range-filter';
|
||||
import { UsageSummaryCards } from '@/components/analytics/usage-summary-cards';
|
||||
import { UsageTrendChart } from '@/components/analytics/usage-trend-chart';
|
||||
import { ModelBreakdownChart } from '@/components/analytics/model-breakdown-chart';
|
||||
import { SessionsTable } from '@/components/analytics/sessions-table';
|
||||
import { TrendingUp, PieChart, Clock, Calendar, RefreshCw } from 'lucide-react';
|
||||
import { TrendingUp, PieChart, RefreshCw } from 'lucide-react';
|
||||
import {
|
||||
useUsageSummary,
|
||||
useUsageTrends,
|
||||
useModelUsage,
|
||||
useSessions,
|
||||
useRefreshUsage,
|
||||
useUsageStatus,
|
||||
} from '@/hooks/use-usage';
|
||||
import { getModelColor } from '@/lib/utils';
|
||||
|
||||
type ViewMode = 'daily' | 'monthly' | 'sessions';
|
||||
|
||||
export function AnalyticsPage() {
|
||||
// Default to last 30 days
|
||||
const [dateRange, setDateRange] = useState<DateRange | undefined>({
|
||||
from: subDays(new Date(), 30),
|
||||
to: new Date(),
|
||||
});
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('daily');
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
// Refresh hook
|
||||
@@ -61,10 +55,6 @@ export function AnalyticsPage() {
|
||||
const { data: summary, isLoading: isSummaryLoading } = useUsageSummary(apiOptions);
|
||||
const { data: trends, isLoading: isTrendsLoading } = useUsageTrends(apiOptions);
|
||||
const { data: models, isLoading: isModelsLoading } = useModelUsage(apiOptions);
|
||||
const { data: sessions, isLoading: isSessionsLoading } = useSessions({
|
||||
...apiOptions,
|
||||
limit: 50,
|
||||
});
|
||||
const { data: status } = useUsageStatus();
|
||||
|
||||
// Format "Last updated" text
|
||||
@@ -90,6 +80,7 @@ export function AnalyticsPage() {
|
||||
{ label: '7D', range: { from: subDays(new Date(), 7), to: new Date() } },
|
||||
{ label: '30D', range: { from: subDays(new Date(), 30), to: new Date() } },
|
||||
{ label: 'Month', range: { from: startOfMonth(new Date()), to: new Date() } },
|
||||
{ label: 'All Time', range: { from: undefined, to: new Date() } },
|
||||
]}
|
||||
/>
|
||||
{lastUpdatedText && (
|
||||
@@ -112,163 +103,101 @@ export function AnalyticsPage() {
|
||||
{/* Summary Cards */}
|
||||
<UsageSummaryCards data={summary} isLoading={isSummaryLoading} />
|
||||
|
||||
{/* Main Content Tabs */}
|
||||
<Tabs
|
||||
value={viewMode}
|
||||
onValueChange={(v) => setViewMode(v as ViewMode)}
|
||||
className="flex-1 flex flex-col min-h-0"
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-3 h-9 mb-4">
|
||||
<TabsTrigger value="daily" className="text-xs">
|
||||
Daily
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="monthly" className="text-xs">
|
||||
Monthly
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="sessions" className="text-xs">
|
||||
Sessions
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col min-h-0 gap-4">
|
||||
{/* Usage Trend Chart - Full Width */}
|
||||
<Card className="flex flex-col flex-1 min-h-0 shadow-sm">
|
||||
<CardHeader className="p-4 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
Usage Trends
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 pt-0 flex-1 min-h-0 flex items-center justify-center">
|
||||
<UsageTrendChart data={trends || []} isLoading={isTrendsLoading} className="h-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex-1 min-h-0">
|
||||
{/* Daily View */}
|
||||
<TabsContent value="daily" className="flex flex-col gap-4 m-0 h-full overflow-hidden">
|
||||
{/* Usage Trend Chart - Full Width */}
|
||||
<Card className="flex flex-col flex-1 min-h-0 shadow-sm">
|
||||
<CardHeader className="p-4 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
Usage Trends
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 pt-0 flex-1 min-h-0 flex items-center justify-center">
|
||||
<UsageTrendChart
|
||||
data={trends || []}
|
||||
isLoading={isTrendsLoading}
|
||||
className="h-full"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Bottom Row - Model Usage & Cost */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 flex-1 min-h-0">
|
||||
{/* Model Distribution */}
|
||||
<Card className="flex flex-col h-full min-h-0 shadow-sm">
|
||||
<CardHeader className="p-4 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<PieChart className="w-4 h-4" />
|
||||
Model Usage
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 pt-0 flex-1 min-h-0 flex items-center justify-center">
|
||||
<div className="flex w-full h-full items-center">
|
||||
<div className="flex-1 h-full min-w-0">
|
||||
<ModelBreakdownChart
|
||||
data={models || []}
|
||||
isLoading={isModelsLoading}
|
||||
className="h-full"
|
||||
{/* Bottom Row - Model Usage & Cost */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 flex-1 min-h-0">
|
||||
{/* Model Distribution */}
|
||||
<Card className="flex flex-col h-full min-h-0 shadow-sm">
|
||||
<CardHeader className="p-4 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<PieChart className="w-4 h-4" />
|
||||
Model Usage
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 pt-0 flex-1 min-h-0 flex items-center justify-center">
|
||||
<div className="flex w-full h-full items-center">
|
||||
<div className="flex-1 h-full min-w-0">
|
||||
<ModelBreakdownChart
|
||||
data={models || []}
|
||||
isLoading={isModelsLoading}
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-[220px] shrink-0 pl-4 space-y-2 overflow-y-auto max-h-full">
|
||||
{models?.slice(0, 8).map((model) => (
|
||||
<div key={model.model} className="flex items-center gap-2 text-xs">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full shrink-0"
|
||||
style={{ backgroundColor: getModelColor(model.model) }}
|
||||
/>
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<div className="flex justify-between items-baseline gap-2">
|
||||
<span className="font-medium truncate" title={model.model}>
|
||||
{model.model}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground shrink-0">
|
||||
{model.percentage.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-[220px] shrink-0 pl-4 space-y-2 overflow-y-auto max-h-full">
|
||||
{models?.slice(0, 8).map((model) => (
|
||||
<div key={model.model} className="flex items-center gap-2 text-xs">
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Cost Breakdown */}
|
||||
<Card className="flex flex-col h-full min-h-0 shadow-sm">
|
||||
<CardHeader className="p-4 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Cost by Model</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 pt-2 flex-1 min-h-0 overflow-y-auto">
|
||||
{isModelsLoading ? (
|
||||
<Skeleton className="h-full w-full" />
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{[...(models || [])]
|
||||
.sort((a, b) => b.cost - a.cost)
|
||||
.map((model) => (
|
||||
<div
|
||||
key={model.model}
|
||||
className="flex items-center justify-between text-xs border-b border-border/50 pb-2 last:border-0 last:pb-0"
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full shrink-0"
|
||||
className="w-2.5 h-2.5 rounded-full shrink-0"
|
||||
style={{ backgroundColor: getModelColor(model.model) }}
|
||||
/>
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<div className="flex justify-between items-baseline gap-2">
|
||||
<span className="font-medium truncate" title={model.model}>
|
||||
{model.model}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground shrink-0">
|
||||
{model.percentage.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="font-medium" title={model.model}>
|
||||
{model.model}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Cost Breakdown */}
|
||||
<Card className="flex flex-col h-full min-h-0 shadow-sm">
|
||||
<CardHeader className="p-4 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Cost by Model</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 pt-2 flex-1 min-h-0 overflow-y-auto">
|
||||
{isModelsLoading ? (
|
||||
<Skeleton className="h-full w-full" />
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{[...(models || [])]
|
||||
.sort((a, b) => b.cost - a.cost)
|
||||
.map((model) => (
|
||||
<div
|
||||
key={model.model}
|
||||
className="flex items-center justify-between text-xs border-b border-border/50 pb-2 last:border-0 last:pb-0"
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div
|
||||
className="w-2.5 h-2.5 rounded-full shrink-0"
|
||||
style={{ backgroundColor: getModelColor(model.model) }}
|
||||
/>
|
||||
<span className="font-medium" title={model.model}>
|
||||
{model.model}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-muted-foreground whitespace-nowrap font-mono">
|
||||
${model.cost.toFixed(4)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Monthly View */}
|
||||
<TabsContent value="monthly" className="m-0 h-full">
|
||||
<Card className="h-full flex flex-col">
|
||||
<CardHeader className="p-4 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
Monthly Overview
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 pt-0 flex-1">
|
||||
<UsageTrendChart
|
||||
data={trends || []}
|
||||
isLoading={isTrendsLoading}
|
||||
granularity="monthly"
|
||||
className="h-full"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Sessions View */}
|
||||
<TabsContent value="sessions" className="m-0 h-full">
|
||||
<Card className="h-full flex flex-col">
|
||||
<CardHeader className="p-4 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
Session History
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0 flex-1 overflow-hidden">
|
||||
<div className="h-full overflow-y-auto">
|
||||
<SessionsTable data={sessions} isLoading={isSessionsLoading} />
|
||||
<span className="text-muted-foreground whitespace-nowrap font-mono">
|
||||
${model.cost.toFixed(4)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user