|
|
@@ -1,11 +1,8 @@
|
|
|
import { useState, useCallback } from 'react'
|
|
|
-import { format } from 'date-fns'
|
|
|
-import { CalendarIcon, DownloadIcon, RefreshCcw, Search } from 'lucide-react'
|
|
|
+import { RefreshCcw, Search } from 'lucide-react'
|
|
|
import { useTranslation } from 'react-i18next'
|
|
|
import { toast } from 'sonner'
|
|
|
-import { cn } from '@/lib/utils'
|
|
|
import { Button } from '@/components/ui/button'
|
|
|
-import { Calendar } from '@/components/ui/calendar'
|
|
|
import {
|
|
|
Card,
|
|
|
CardContent,
|
|
|
@@ -13,11 +10,6 @@ import {
|
|
|
CardHeader,
|
|
|
CardTitle,
|
|
|
} from '@/components/ui/card'
|
|
|
-import {
|
|
|
- Popover,
|
|
|
- PopoverContent,
|
|
|
- PopoverTrigger,
|
|
|
-} from '@/components/ui/popover'
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
|
import { ConfigDrawer } from '@/components/config-drawer'
|
|
|
import { LanguageSwitch } from '@/components/language-switch'
|
|
|
@@ -38,11 +30,6 @@ import { useUserStats } from './hooks/use-user-stats'
|
|
|
|
|
|
export function Dashboard() {
|
|
|
const { t } = useTranslation()
|
|
|
- const [dateRange, setDateRange] = useState<{ from: Date; to: Date }>({
|
|
|
- from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 7 days ago
|
|
|
- to: new Date(),
|
|
|
- })
|
|
|
- const [isCalendarOpen, setIsCalendarOpen] = useState(false)
|
|
|
const [searchDialogOpen, setSearchDialogOpen] = useState(false)
|
|
|
|
|
|
const {
|
|
|
@@ -52,10 +39,9 @@ export function Dashboard() {
|
|
|
refresh: refreshDashboard,
|
|
|
fetchData,
|
|
|
filters,
|
|
|
- isAdmin,
|
|
|
} = useDashboardData()
|
|
|
|
|
|
- const { user } = useUserStats()
|
|
|
+ const { user, isLoading: userLoading } = useUserStats()
|
|
|
|
|
|
// 模型监控数据
|
|
|
const {
|
|
|
@@ -67,46 +53,23 @@ export function Dashboard() {
|
|
|
filters: modelMonitoringFilters,
|
|
|
} = useModelMonitoring()
|
|
|
|
|
|
- const handleDateRangeChange = useCallback(
|
|
|
- (range: { from: Date; to: Date }) => {
|
|
|
- setDateRange(range)
|
|
|
- setIsCalendarOpen(false)
|
|
|
- // TODO: Update dashboard data with new date range
|
|
|
- toast.success('Date range updated')
|
|
|
- },
|
|
|
- []
|
|
|
- )
|
|
|
-
|
|
|
const handleRefresh = useCallback(() => {
|
|
|
refreshDashboard()
|
|
|
- toast.success('Dashboard refreshed')
|
|
|
- }, [refreshDashboard])
|
|
|
-
|
|
|
- const handleExport = useCallback(() => {
|
|
|
- // TODO: Implement data export functionality
|
|
|
- toast.success('Export started')
|
|
|
- }, [])
|
|
|
+ toast.success(t('dashboard.refresh_success'))
|
|
|
+ }, [refreshDashboard, t])
|
|
|
|
|
|
const handleAdvancedSearch = useCallback(
|
|
|
(newFilters: any) => {
|
|
|
fetchData(newFilters)
|
|
|
- toast.success('Search updated')
|
|
|
+ toast.success(t('dashboard.search_updated'))
|
|
|
},
|
|
|
- [fetchData]
|
|
|
+ [fetchData, t]
|
|
|
)
|
|
|
|
|
|
const openSearchDialog = useCallback(() => {
|
|
|
setSearchDialogOpen(true)
|
|
|
}, [])
|
|
|
|
|
|
- const formatDateRange = () => {
|
|
|
- if (!dateRange.from || !dateRange.to) return 'Select date range'
|
|
|
- if (dateRange.from.toDateString() === dateRange.to.toDateString()) {
|
|
|
- return format(dateRange.from, 'MMM dd, yyyy')
|
|
|
- }
|
|
|
- return `${format(dateRange.from, 'MMM dd')} - ${format(dateRange.to, 'MMM dd, yyyy')}`
|
|
|
- }
|
|
|
-
|
|
|
return (
|
|
|
<>
|
|
|
{/* ===== Top Heading ===== */}
|
|
|
@@ -124,75 +87,42 @@ export function Dashboard() {
|
|
|
<Main>
|
|
|
<div className='mb-2 flex items-center justify-between space-y-2'>
|
|
|
<div>
|
|
|
- <h1 className='text-2xl font-bold tracking-tight'>Dashboard</h1>
|
|
|
+ <h1 className='text-2xl font-bold tracking-tight'>
|
|
|
+ {t('dashboard.title')}
|
|
|
+ </h1>
|
|
|
<p className='text-muted-foreground'>
|
|
|
{user
|
|
|
- ? `Welcome back, ${user.display_name || user.username}`
|
|
|
- : 'Overview of your API usage'}
|
|
|
+ ? t('dashboard.welcome_back', {
|
|
|
+ name: user.display_name || user.username,
|
|
|
+ })
|
|
|
+ : t('dashboard.overview_subtitle')}
|
|
|
</p>
|
|
|
</div>
|
|
|
<div className='flex items-center space-x-2'>
|
|
|
- <Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
|
|
|
- <PopoverTrigger asChild>
|
|
|
- <Button
|
|
|
- variant='outline'
|
|
|
- className={cn(
|
|
|
- 'w-[280px] justify-start text-left font-normal',
|
|
|
- !dateRange.from && 'text-muted-foreground'
|
|
|
- )}
|
|
|
- >
|
|
|
- <CalendarIcon className='mr-2 h-4 w-4' />
|
|
|
- {formatDateRange()}
|
|
|
- </Button>
|
|
|
- </PopoverTrigger>
|
|
|
- <PopoverContent className='w-auto p-0' align='end'>
|
|
|
- <Calendar
|
|
|
- mode='range'
|
|
|
- defaultMonth={dateRange.from}
|
|
|
- selected={dateRange}
|
|
|
- onSelect={(range) => {
|
|
|
- if (range?.from && range?.to) {
|
|
|
- handleDateRangeChange({ from: range.from, to: range.to })
|
|
|
- }
|
|
|
- }}
|
|
|
- numberOfMonths={2}
|
|
|
- />
|
|
|
- </PopoverContent>
|
|
|
- </Popover>
|
|
|
<Button variant='outline' size='icon' onClick={handleRefresh}>
|
|
|
<RefreshCcw className='h-4 w-4' />
|
|
|
</Button>
|
|
|
<Button variant='outline' onClick={openSearchDialog}>
|
|
|
<Search className='mr-2 h-4 w-4' />
|
|
|
- {t('dashboard.advanced_search')}
|
|
|
- </Button>
|
|
|
- <Button onClick={handleExport}>
|
|
|
- <DownloadIcon className='mr-2 h-4 w-4' />
|
|
|
- {t('dashboard.export')}
|
|
|
+ {t('dashboard.search_button')}
|
|
|
</Button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<Tabs defaultValue='overview' className='space-y-4'>
|
|
|
<TabsList>
|
|
|
- <TabsTrigger value='overview'>Overview</TabsTrigger>
|
|
|
- <TabsTrigger value='analytics'>
|
|
|
- {t('dashboard.analytics')}
|
|
|
+ <TabsTrigger value='overview'>
|
|
|
+ {t('dashboard.overview_tab')}
|
|
|
</TabsTrigger>
|
|
|
<TabsTrigger value='models'>{t('dashboard.models')}</TabsTrigger>
|
|
|
- <TabsTrigger value='monitoring'>
|
|
|
- {t('dashboard.monitoring')}
|
|
|
- </TabsTrigger>
|
|
|
- {isAdmin && (
|
|
|
- <TabsTrigger value='admin'>{t('dashboard.admin')}</TabsTrigger>
|
|
|
- )}
|
|
|
</TabsList>
|
|
|
|
|
|
<TabsContent value='overview' className='space-y-4'>
|
|
|
{/* Stats Cards */}
|
|
|
<StatsCards
|
|
|
stats={dashboardData.stats}
|
|
|
- loading={dashboardLoading}
|
|
|
+ userStats={user}
|
|
|
+ loading={dashboardLoading || userLoading}
|
|
|
error={dashboardError}
|
|
|
/>
|
|
|
|
|
|
@@ -215,91 +145,78 @@ export function Dashboard() {
|
|
|
</div>
|
|
|
</TabsContent>
|
|
|
|
|
|
- <TabsContent value='analytics' className='space-y-4'>
|
|
|
- <Card>
|
|
|
- <CardHeader>
|
|
|
- <CardTitle>Advanced Analytics</CardTitle>
|
|
|
- <CardDescription>
|
|
|
- Detailed usage analytics and insights
|
|
|
- </CardDescription>
|
|
|
- </CardHeader>
|
|
|
- <CardContent>
|
|
|
- <div className='text-muted-foreground flex h-[400px] items-center justify-center'>
|
|
|
- <div className='text-center'>
|
|
|
- <p className='text-lg font-medium'>
|
|
|
- {t('dashboard.coming_soon')}
|
|
|
- </p>
|
|
|
- <p className='mt-2 text-sm'>
|
|
|
- Advanced analytics features are in development
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </CardContent>
|
|
|
- </Card>
|
|
|
- </TabsContent>
|
|
|
-
|
|
|
<TabsContent value='models' className='space-y-4'>
|
|
|
- <div className='grid grid-cols-1 gap-4'>
|
|
|
- <ModelUsageChart
|
|
|
- data={dashboardData.modelUsage}
|
|
|
- loading={dashboardLoading}
|
|
|
- error={dashboardError}
|
|
|
- title='Detailed Model Usage'
|
|
|
- description='Comprehensive breakdown of usage by model'
|
|
|
- />
|
|
|
-
|
|
|
- {/* Model Usage Table */}
|
|
|
- <Card>
|
|
|
- <CardHeader>
|
|
|
- <CardTitle>Model Usage Details</CardTitle>
|
|
|
- <CardDescription>
|
|
|
- Detailed statistics for each model
|
|
|
- </CardDescription>
|
|
|
- </CardHeader>
|
|
|
- <CardContent>
|
|
|
- {dashboardData.modelUsage.length > 0 ? (
|
|
|
- <div className='space-y-2'>
|
|
|
- {dashboardData.modelUsage.slice(0, 10).map((model) => (
|
|
|
- <div
|
|
|
- key={model.model}
|
|
|
- className='flex items-center justify-between rounded-lg border p-3'
|
|
|
- >
|
|
|
- <div>
|
|
|
- <p className='font-medium'>{model.model}</p>
|
|
|
- <p className='text-muted-foreground text-sm'>
|
|
|
- {model.count} requests
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- <div className='text-right'>
|
|
|
- <p className='font-medium'>
|
|
|
- {model.percentage.toFixed(1)}%
|
|
|
- </p>
|
|
|
- <p className='text-muted-foreground text-sm'>
|
|
|
- ${model.quota.toFixed(2)}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- ) : (
|
|
|
- <div className='text-muted-foreground flex h-[200px] items-center justify-center'>
|
|
|
- <p>No model usage data available</p>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </CardContent>
|
|
|
- </Card>
|
|
|
- </div>
|
|
|
- </TabsContent>
|
|
|
-
|
|
|
- <TabsContent value='monitoring' className='space-y-4'>
|
|
|
- {/* 模型监控统计 */}
|
|
|
+ {/* 模型监控统计卡片 */}
|
|
|
<ModelMonitoringStats
|
|
|
stats={modelMonitoringData.stats}
|
|
|
loading={modelMonitoringLoading}
|
|
|
error={modelMonitoringError}
|
|
|
/>
|
|
|
|
|
|
- {/* 模型监控表格 */}
|
|
|
+ {/* 图表区域 */}
|
|
|
+ <div className='grid grid-cols-1 gap-4 lg:grid-cols-7'>
|
|
|
+ <div className='col-span-1 lg:col-span-4'>
|
|
|
+ <ModelUsageChart
|
|
|
+ data={dashboardData.modelUsage}
|
|
|
+ loading={dashboardLoading}
|
|
|
+ error={dashboardError}
|
|
|
+ title={t('dashboard.model_usage_distribution')}
|
|
|
+ description={t('dashboard.model_usage_description')}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className='col-span-1 lg:col-span-3'>
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle>{t('dashboard.top_models_ranking')}</CardTitle>
|
|
|
+ <CardDescription>
|
|
|
+ {t('dashboard.top_models_description')}
|
|
|
+ </CardDescription>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ {dashboardData.modelUsage.length > 0 ? (
|
|
|
+ <div className='space-y-3'>
|
|
|
+ {dashboardData.modelUsage
|
|
|
+ .slice(0, 10)
|
|
|
+ .map((model, index) => (
|
|
|
+ <div
|
|
|
+ key={model.model}
|
|
|
+ className='flex items-center justify-between rounded-lg border p-3'
|
|
|
+ >
|
|
|
+ <div className='flex items-center space-x-3'>
|
|
|
+ <div className='bg-primary text-primary-foreground flex h-6 w-6 items-center justify-center rounded-full text-xs font-bold'>
|
|
|
+ {index + 1}
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <p className='font-medium'>{model.model}</p>
|
|
|
+ <p className='text-muted-foreground text-sm'>
|
|
|
+ {t('dashboard.requests_count', {
|
|
|
+ count: model.count,
|
|
|
+ })}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div className='text-right'>
|
|
|
+ <p className='font-medium'>
|
|
|
+ {model.percentage.toFixed(1)}%
|
|
|
+ </p>
|
|
|
+ <p className='text-muted-foreground text-sm'>
|
|
|
+ ${model.quota.toFixed(2)}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div className='text-muted-foreground flex h-[200px] items-center justify-center'>
|
|
|
+ <p>{t('dashboard.no_model_usage_data')}</p>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 详细模型监控表格 */}
|
|
|
<ModelMonitoringTable
|
|
|
models={modelMonitoringData.models}
|
|
|
loading={modelMonitoringLoading}
|
|
|
@@ -315,29 +232,6 @@ export function Dashboard() {
|
|
|
onRefresh={refreshModelMonitoring}
|
|
|
/>
|
|
|
</TabsContent>
|
|
|
-
|
|
|
- {isAdmin && (
|
|
|
- <TabsContent value='admin' className='space-y-4'>
|
|
|
- <Card>
|
|
|
- <CardHeader>
|
|
|
- <CardTitle>Admin Dashboard</CardTitle>
|
|
|
- <CardDescription>
|
|
|
- System-wide statistics and management
|
|
|
- </CardDescription>
|
|
|
- </CardHeader>
|
|
|
- <CardContent>
|
|
|
- <div className='text-muted-foreground flex h-[400px] items-center justify-center'>
|
|
|
- <div className='text-center'>
|
|
|
- <p className='text-lg font-medium'>Admin Features</p>
|
|
|
- <p className='mt-2 text-sm'>
|
|
|
- Advanced admin features are in development
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </CardContent>
|
|
|
- </Card>
|
|
|
- </TabsContent>
|
|
|
- )}
|
|
|
</Tabs>
|
|
|
</Main>
|
|
|
|