Oliver-ai-bot_2.0/frontend/components/admin/analytics-tab.tsx
Vadym Samoilenko bfd536afcf Fix deploy: type all apiClient calls, handle duplicate enum in migration
- Replace {data} destructuring with typed direct returns across all
  apiClient.get/post calls (assistant, chat, admin tabs)
- Remove unused accessToken variable in chat page
- Wrap CREATE TYPE in migration 002 with exception handler to ignore
  duplicate_object error on re-deploy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:39:02 +00:00

213 lines
8.4 KiB
TypeScript

'use client';
import { useEffect, useState, useCallback } from 'react';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import apiClient from '@/lib/api-client';
import {
Loader2, MessageSquare, MessagesSquare, Users, Zap, BookOpenCheck, Sparkles,
} from 'lucide-react';
import type { AnalyticsResponse } from '@/types';
export function AnalyticsTab() {
const [analytics, setAnalytics] = useState<AnalyticsResponse | null>(null);
const [loading, setLoading] = useState(true);
const [days, setDays] = useState(30);
const fetchAnalytics = useCallback(async () => {
setLoading(true);
try {
const data = await apiClient.get<AnalyticsResponse>(`/admin/analytics?days=${days}`);
setAnalytics(data);
} catch (error) {
console.error('Failed to fetch analytics:', error);
} finally {
setLoading(false);
}
}, [days]);
useEffect(() => { fetchAnalytics(); }, [fetchAnalytics]);
const formatNumber = (n: number) => {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
return String(n);
};
if (loading) {
return (
<div className="flex justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
if (!analytics) {
return (
<Card className="p-6">
<p className="text-center text-muted-foreground">Failed to load analytics</p>
</Card>
);
}
const { overview, token_usage_by_mode, top_users, conversations_per_day } = analytics;
const maxDaily = Math.max(...conversations_per_day.map((d) => d.count), 1);
return (
<div className="space-y-6">
{/* Period Selector */}
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Period:</span>
{[7, 30, 90].map((d) => (
<button
key={d}
onClick={() => setDays(d)}
className={`rounded-md px-3 py-1 text-sm font-medium transition-colors ${
days === d
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:bg-muted/80'
}`}
>
{d}d
</button>
))}
</div>
{/* Overview Cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100 dark:bg-blue-900/30">
<MessageSquare className="h-5 w-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<p className="text-sm text-muted-foreground">Conversations</p>
<p className="text-2xl font-bold text-foreground">{formatNumber(overview.total_conversations)}</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-green-100 dark:bg-green-900/30">
<MessagesSquare className="h-5 w-5 text-green-600 dark:text-green-400" />
</div>
<div>
<p className="text-sm text-muted-foreground">Messages</p>
<p className="text-2xl font-bold text-foreground">{formatNumber(overview.total_messages)}</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-purple-100 dark:bg-purple-900/30">
<Users className="h-5 w-5 text-purple-600 dark:text-purple-400" />
</div>
<div>
<p className="text-sm text-muted-foreground">Active Users (7d)</p>
<p className="text-2xl font-bold text-foreground">{overview.active_users_7d}</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-orange-100 dark:bg-orange-900/30">
<Zap className="h-5 w-5 text-orange-600 dark:text-orange-400" />
</div>
<div>
<p className="text-sm text-muted-foreground">Total Tokens</p>
<p className="text-2xl font-bold text-foreground">
{formatNumber(overview.total_input_tokens + overview.total_output_tokens)}
</p>
</div>
</div>
</Card>
</div>
{/* Token Usage by Mode */}
<div className="grid gap-4 md:grid-cols-2">
{token_usage_by_mode.map((mode) => (
<Card key={mode.mode} className="p-4">
<div className="mb-3 flex items-center gap-2">
{mode.mode === 'rag' ? (
<BookOpenCheck className="h-5 w-5 text-primary" />
) : (
<Sparkles className="h-5 w-5 text-accent" />
)}
<h4 className="font-semibold text-foreground capitalize">{mode.mode}</h4>
<Badge variant="outline" className="ml-auto text-xs">
{mode.conversation_count} conversations
</Badge>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-xs text-muted-foreground">Input Tokens</p>
<p className="text-lg font-semibold text-foreground">{formatNumber(mode.total_input_tokens)}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Output Tokens</p>
<p className="text-lg font-semibold text-foreground">{formatNumber(mode.total_output_tokens)}</p>
</div>
</div>
</Card>
))}
{token_usage_by_mode.length === 0 && (
<Card className="col-span-2 p-6 text-center text-muted-foreground">No usage data yet</Card>
)}
</div>
{/* Top Users + Conversations per Day */}
<div className="grid gap-6 lg:grid-cols-2">
{/* Top Users */}
<Card className="p-4">
<h4 className="mb-3 font-semibold text-foreground">Top Users</h4>
{top_users.length === 0 ? (
<p className="py-4 text-center text-muted-foreground">No data yet</p>
) : (
<div className="space-y-2">
{top_users.map((u, i) => (
<div key={u.user_id} className="flex items-center gap-3">
<span className="w-6 text-right text-sm font-medium text-muted-foreground">{i + 1}</span>
<div className="flex-1 min-w-0">
<p className="truncate text-sm font-medium text-foreground">{u.display_name}</p>
<p className="truncate text-xs text-muted-foreground">{u.email}</p>
</div>
<Badge variant="outline">{u.message_count} msgs</Badge>
</div>
))}
</div>
)}
</Card>
{/* Conversations per Day */}
<Card className="p-4">
<h4 className="mb-3 font-semibold text-foreground">Conversations per Day</h4>
{conversations_per_day.length === 0 ? (
<p className="py-4 text-center text-muted-foreground">No data yet</p>
) : (
<div className="space-y-1.5">
{conversations_per_day.slice(-14).map((day) => (
<div key={day.date} className="flex items-center gap-2">
<span className="w-20 shrink-0 text-xs text-muted-foreground">
{new Date(day.date).toLocaleDateString('en-GB', { month: 'short', day: 'numeric' })}
</span>
<div className="flex-1">
<div
className="h-5 rounded bg-primary/20"
style={{ width: `${Math.max((day.count / maxDaily) * 100, 2)}%` }}
>
<div
className="h-full rounded bg-primary transition-all"
style={{ width: '100%' }}
/>
</div>
</div>
<span className="w-8 text-right text-xs font-medium text-foreground">{day.count}</span>
</div>
))}
</div>
)}
</Card>
</div>
</div>
);
}